feat(screenshot): ✨ add screenshots of rounded corners and write input box
update readme update release.yml and remove pr-chatbot-review.yml closed #323
This commit is contained in:
145
.github/workflows/pr-chatbot-review.yml
vendored
145
.github/workflows/pr-chatbot-review.yml
vendored
@@ -1,145 +0,0 @@
|
||||
name: PR Review Bot
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
review:
|
||||
# 跳过 Renovate PR
|
||||
if: |
|
||||
github.actor != 'renovate[bot]' &&
|
||||
github.actor != 'renovate-preview[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get PR diff
|
||||
id: diff
|
||||
run: |
|
||||
git fetch origin ${{ github.event.pull_request.base.sha }}
|
||||
# 排除配置文件,只分析源代码文件
|
||||
DIFF=$(git diff ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} -- \
|
||||
'src/**/*.vue' \
|
||||
'src/**/*.ts' \
|
||||
'src/**/*.tsx' \
|
||||
'src-tauri/**/*.rs' \
|
||||
':!:**/*.json' \
|
||||
':!:**/*.yaml' \
|
||||
':!:**/*.yml' \
|
||||
':!:**/*.config.*' \
|
||||
':!:**/*.lock' \
|
||||
':!:**/*.toml' \
|
||||
':!:.env*' \
|
||||
':!:.eslintrc*' \
|
||||
':!:.prettierrc*')
|
||||
|
||||
# 如果没有相关文件变更,设置一个提示信息
|
||||
if [ -z "$DIFF" ]; then
|
||||
echo "NO_CHANGES=true" >> $GITHUB_ENV
|
||||
echo "DIFF=没有检测到相关文件的变更。" >> $GITHUB_ENV
|
||||
else
|
||||
echo "NO_CHANGES=false" >> $GITHUB_ENV
|
||||
echo "DIFF<<EOF" >> $GITHUB_ENV
|
||||
echo "$DIFF" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# 首先安装 pnpm
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
run_install: false
|
||||
|
||||
# 然后设置 Node.js
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm config set registry https://registry.npmmirror.com/
|
||||
pnpm install
|
||||
pnpm add openai
|
||||
|
||||
- name: Analyze PR
|
||||
id: analyze
|
||||
if: env.NO_CHANGES != 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
with:
|
||||
script: |
|
||||
const OpenAI = require('openai');
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.DASHSCOPE_API_KEY,
|
||||
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
});
|
||||
|
||||
const diff = process.env.DIFF;
|
||||
|
||||
try {
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "qwen-plus",
|
||||
messages: [{
|
||||
role: "system",
|
||||
content: "你是一个代码审查助手。请用中文分析以下代码变更,重点关注:\n" +
|
||||
"1. 代码逻辑的改动\n" +
|
||||
"2. 潜在的问题或优化空间\n" +
|
||||
"3. TypeScript 类型定义的准确性\n" +
|
||||
"4. Vue 组件的性能影响\n" +
|
||||
"5. Rust 代码的安全性和性能\n" +
|
||||
"请用中文简明扼要地总结。"
|
||||
}, {
|
||||
role: "user",
|
||||
content: `请分析以下代码变更并总结主要改动:\n\n${diff}`
|
||||
}],
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000
|
||||
});
|
||||
|
||||
const analysis = completion.choices[0].message.content;
|
||||
core.setOutput('analysis', analysis);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `
|
||||
PR 代码分析
|
||||
|
||||
${analysis}
|
||||
|
||||
*这是由通义千问 AI 自动生成的 PR 分析,仅供参考。*`
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(`分析失败: ${error.message}`);
|
||||
}
|
||||
|
||||
- name: Skip Analysis Comment
|
||||
if: env.NO_CHANGES == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `## PR 代码分析
|
||||
|
||||
本次变更不包含需要分析的代码文件(src 目录下的 .vue/.ts/.tsx 文件或 src-tauri 目录下的 .rs 文件)。
|
||||
|
||||
---
|
||||
*这是自动生成的通知。*`
|
||||
});
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -69,9 +69,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Vite + Tauri
|
||||
run: pnpm build
|
||||
|
||||
# 安装 Rust
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable # Set this to dtolnay/rust-toolchain@nightly
|
||||
@@ -98,4 +95,4 @@ jobs:
|
||||
releaseBody: 'See the assets to download and install this version.'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }}
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
@@ -237,7 +237,7 @@ HuLa is an instant messaging system built with Tauri, Vite 7, Vue 3, and TypeScr
|
||||
| Feature | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| 💻 | Windows/macOS/Linux |  |
|
||||
| 📱 | iOS/Android Adaptation |  |
|
||||
| 📱 | iOS/Android Adaptation |  |
|
||||
|
||||
### 🤖 AI Integration
|
||||
| Feature | Description | Status |
|
||||
@@ -344,6 +344,7 @@ Execute **pnpm run commit** to invoke _git commit_ interaction, complete informa
|
||||
### 🏆 Gold Sponsors ($15+)
|
||||
| 💝 Date | 👤 Sponsor | 💰 Amount | 🏷️ Platform |
|
||||
|---------|----------|--------|---------|
|
||||
| 2025-08-26 | **唐勇** | `¥200` |  |
|
||||
| 2025-04-25 | **上官俊斌** | `¥200` |  |
|
||||
| 2025-05-27 | **临安居士** | `¥188` |  |
|
||||
| 2025-04-20 | **姜兴(Simon)** | `¥188` |  |
|
||||
|
||||
@@ -238,7 +238,7 @@ HuLa 是一款基于 Tauri、Vite 7、Vue 3 和 TypeScript 构建的即时通讯
|
||||
| 功能 | 描述 | 状态 |
|
||||
|------|------|------|
|
||||
| 💻 | Windows/macOS/Linux |  |
|
||||
| 📱 | iOS/Android 适配 |  |
|
||||
| 📱 | iOS/Android 适配 |  |
|
||||
|
||||
### 🤖 AI 集成
|
||||
| 功能 | 描述 | 状态 |
|
||||
@@ -345,6 +345,7 @@ sudo xattr -r -d com.apple.quarantine /Applications/应用名称.app
|
||||
### 🏆 金牌赞助者 (¥100+)
|
||||
| 💝 日期 | 👤 赞助者 | 💰 金额 | 🏷️ 平台 |
|
||||
|---------|----------|--------|---------|
|
||||
| 2025-08-26 | **唐勇** | `¥200` |  |
|
||||
| 2025-04-25 | **上官俊斌** | `¥200` |  |
|
||||
| 2025-05-27 | **临安居士** | `¥188` |  |
|
||||
| 2025-04-20 | **姜兴(Simon)** | `¥188` |  |
|
||||
|
||||
@@ -137,7 +137,7 @@ onMounted(async () => {
|
||||
closeWindow?.close()
|
||||
})
|
||||
|
||||
addListener(
|
||||
await addListener(
|
||||
listen('refresh_token_event', (event) => {
|
||||
console.log('🔄 收到 refresh_token 事件')
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { emitTo } from '@tauri-apps/api/event'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { writeImage } from '@tauri-apps/plugin-clipboard-manager'
|
||||
import type { Ref } from 'vue'
|
||||
@@ -1180,6 +1181,33 @@ const confirmSelection = async () => {
|
||||
height // 绘制到临时 canvas 的区域
|
||||
)
|
||||
|
||||
// 如果设置了圆角,则将裁剪结果应用圆角蒙版,导出带透明圆角的 PNG
|
||||
if (borderRadius.value > 0) {
|
||||
const scale = screenConfig.value.scaleX || 1
|
||||
const r = Math.min(borderRadius.value * scale, width / 2, height / 2)
|
||||
if (r > 0) {
|
||||
offscreenCtx.save()
|
||||
// 仅保留圆角矩形内的内容
|
||||
offscreenCtx.globalCompositeOperation = 'destination-in'
|
||||
|
||||
offscreenCtx.beginPath()
|
||||
// 在 (0,0,width,height) 上构建圆角矩形路径
|
||||
offscreenCtx.moveTo(r, 0)
|
||||
offscreenCtx.lineTo(width - r, 0)
|
||||
offscreenCtx.quadraticCurveTo(width, 0, width, r)
|
||||
offscreenCtx.lineTo(width, height - r)
|
||||
offscreenCtx.quadraticCurveTo(width, height, width - r, height)
|
||||
offscreenCtx.lineTo(r, height)
|
||||
offscreenCtx.quadraticCurveTo(0, height, 0, height - r)
|
||||
offscreenCtx.lineTo(0, r)
|
||||
offscreenCtx.quadraticCurveTo(0, 0, r, 0)
|
||||
offscreenCtx.closePath()
|
||||
offscreenCtx.fill()
|
||||
|
||||
offscreenCtx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
// 测试:检查canvas数据是否有效
|
||||
try {
|
||||
offscreenCtx.getImageData(0, 0, Math.min(10, width), Math.min(10, height))
|
||||
@@ -1187,13 +1215,24 @@ const confirmSelection = async () => {
|
||||
console.error('获取ImageData失败,可能是安全限制:', error)
|
||||
}
|
||||
|
||||
// 将裁剪后的图像转换为 Blob 并复制到剪贴板
|
||||
offscreenCanvas.toBlob(async (blob) => {
|
||||
if (blob && blob.size > 0) {
|
||||
try {
|
||||
// 将 Blob 转换为 ArrayBuffer 以便通过 Tauri 事件传递
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const buffer = new Uint8Array(arrayBuffer)
|
||||
|
||||
try {
|
||||
await emitTo('home', 'screenshot', {
|
||||
type: 'image',
|
||||
buffer: Array.from(buffer),
|
||||
mimeType: 'image/png'
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('发送截图到主窗口失败:', e)
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
await writeImage(buffer)
|
||||
await resetScreenshot()
|
||||
} catch (error) {
|
||||
@@ -1304,7 +1343,7 @@ const handleScreenshot = () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('capture', () => {
|
||||
resetDrawTools()
|
||||
initCanvas()
|
||||
@@ -1314,7 +1353,7 @@ onMounted(async () => {
|
||||
)
|
||||
|
||||
// 监听窗口隐藏时的重置事件
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('capture-reset', () => {
|
||||
resetDrawTools()
|
||||
resetScreenshot()
|
||||
|
||||
@@ -433,12 +433,38 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
/** 这里使用的是窗口之间的通信来监听信息对话的变化 */
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('aloneData', (event: any) => {
|
||||
activeItem.value = { ...event.payload.item }
|
||||
}),
|
||||
'aloneData'
|
||||
)
|
||||
await addListener(
|
||||
appWindow.listen('screenshot', async (e: any) => {
|
||||
// 确保输入框获得焦点
|
||||
if (messageInputDom.value) {
|
||||
messageInputDom.value.focus()
|
||||
try {
|
||||
// 从 ArrayBuffer 数组重建 Blob 对象
|
||||
const buffer = new Uint8Array(e.payload.buffer)
|
||||
const blob = new Blob([buffer], { type: e.payload.mimeType })
|
||||
const file = new File([blob], 'screenshot.png', { type: e.payload.mimeType })
|
||||
|
||||
// 创建一个模拟的粘贴事件,包含File对象
|
||||
const mockPasteEvent = {
|
||||
preventDefault: () => {},
|
||||
clipboardData: {
|
||||
files: [file]
|
||||
}
|
||||
}
|
||||
handlePaste(mockPasteEvent, messageInputDom.value, showFileModalCallback)
|
||||
} catch (error) {
|
||||
console.error('处理截图失败:', error)
|
||||
}
|
||||
}
|
||||
}),
|
||||
'screenshot'
|
||||
)
|
||||
window.addEventListener('click', closeMenu, true)
|
||||
window.addEventListener('keydown', disableSelectAll)
|
||||
})
|
||||
|
||||
@@ -472,7 +472,7 @@ onMounted(async () => {
|
||||
handlePopoverUpdate(event.uid)
|
||||
})
|
||||
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('announcementClear', async () => {
|
||||
announNum.value = 0
|
||||
}),
|
||||
|
||||
@@ -248,7 +248,7 @@ onMounted(async () => {
|
||||
// info('ActionBar 组件已挂载')
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen(EventEnum.EXIT, async () => {
|
||||
await exit(0)
|
||||
}),
|
||||
|
||||
@@ -55,7 +55,7 @@ export const useGlobalShortcut = () => {
|
||||
|
||||
if (captureWindow) {
|
||||
// 设置关闭拦截 - 将关闭转为隐藏
|
||||
addListener(
|
||||
await addListener(
|
||||
captureWindow.onCloseRequested(async (event) => {
|
||||
event.preventDefault()
|
||||
await captureWindow.hide()
|
||||
@@ -330,7 +330,7 @@ export const useGlobalShortcut = () => {
|
||||
}
|
||||
|
||||
// 监听全局快捷键开关变化
|
||||
addListener(
|
||||
await addListener(
|
||||
listen('global-shortcut-enabled-changed', (event) => {
|
||||
const enabled = (event.payload as any)?.enabled
|
||||
if (typeof enabled === 'boolean') {
|
||||
@@ -344,7 +344,7 @@ export const useGlobalShortcut = () => {
|
||||
|
||||
// 监听每个快捷键的更新事件
|
||||
for (const config of shortcutConfigs) {
|
||||
addListener(
|
||||
await addListener(
|
||||
listen(config.updateEventName, (event) => {
|
||||
const newShortcut = (event.payload as any)?.shortcut
|
||||
if (newShortcut) {
|
||||
|
||||
@@ -327,7 +327,7 @@ export const useMessage = () => {
|
||||
|
||||
onMounted(async () => {
|
||||
const appWindow = WebviewWindow.getCurrent()
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen(EventEnum.ALONE, () => {
|
||||
emit(EventEnum.ALONE + itemRef.value?.roomId, itemRef.value)
|
||||
if (aloneWin.value.has(EventEnum.ALONE + itemRef.value?.roomId)) return
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useTauriListener = () => {
|
||||
* @param listener Promise<UnlistenFn>
|
||||
*/
|
||||
const addListener = async (listener: Promise<UnlistenFn>, id?: string) => {
|
||||
const listenerId = id || `listener_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const listenerId = id || `listener_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
|
||||
if (listenerIdMap.has(listenerId)) {
|
||||
try {
|
||||
const unlisten = await listener
|
||||
|
||||
@@ -352,7 +352,7 @@ onMounted(async () => {
|
||||
startResize()
|
||||
|
||||
// 监听自定义事件,处理设置中菜单显示模式切换和添加插件后,导致高度变化,需重新调整插件菜单显示
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('startResize', () => {
|
||||
startResize()
|
||||
}),
|
||||
|
||||
@@ -216,8 +216,8 @@ const openEditInfo = () => {
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addListener(
|
||||
onMounted(async () => {
|
||||
await addListener(
|
||||
appWindow.listen('open_edit_info', async () => {
|
||||
openEditInfo()
|
||||
}),
|
||||
|
||||
@@ -205,7 +205,7 @@ export const leftHook = () => {
|
||||
useMitt.on(MittEnum.TO_SEND_MSG, (event: any) => {
|
||||
activeUrl.value = event.url
|
||||
})
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen(EventEnum.WIN_SHOW, (e) => {
|
||||
// 如果已经存在就不添加
|
||||
if (openWindowsList.value.has(e.payload)) return
|
||||
@@ -213,7 +213,7 @@ export const leftHook = () => {
|
||||
}),
|
||||
EventEnum.WIN_SHOW
|
||||
)
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen(EventEnum.WIN_CLOSE, (e) => {
|
||||
openWindowsList.value.delete(e.payload)
|
||||
}),
|
||||
|
||||
@@ -1156,7 +1156,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
// 监听公告更新事件
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('announcementUpdated', async (event: any) => {
|
||||
if (event.payload) {
|
||||
const { hasAnnouncements, topAnnouncement: newTopAnnouncement } = event.payload
|
||||
@@ -1178,14 +1178,14 @@ onMounted(async () => {
|
||||
)
|
||||
|
||||
// 监听公告清空事件
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('announcementClear', () => {
|
||||
topAnnouncement.value = null
|
||||
}),
|
||||
'announcementClear'
|
||||
)
|
||||
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen(EventEnum.SHARE_SCREEN, async () => {
|
||||
await createWebviewWindow('共享屏幕', 'sharedScreen', 840, 840)
|
||||
}),
|
||||
|
||||
@@ -19,24 +19,26 @@ const { addListener } = useTauriListener()
|
||||
const video = ref<HTMLVideoElement>()
|
||||
const peerConnection = new RTCPeerConnection()
|
||||
|
||||
addListener(
|
||||
appWindow.listen('offer', async (event) => {
|
||||
console.log(event.payload)
|
||||
await peerConnection.setRemoteDescription(new RTCSessionDescription(event.payload as RTCSessionDescriptionInit))
|
||||
const answer = await peerConnection.createAnswer()
|
||||
await peerConnection.setLocalDescription(answer)
|
||||
// 在这里,你需要将应答发送给发送方
|
||||
// 你可以使用信令服务器来发送应答,或者将应答复制粘贴到发送方页面
|
||||
console.log(JSON.stringify(answer))
|
||||
})
|
||||
)
|
||||
|
||||
peerConnection.ontrack = (event) => {
|
||||
if (video.value) {
|
||||
video.value.srcObject = event.streams[0]
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(async () => {
|
||||
await addListener(
|
||||
appWindow.listen('offer', async (event) => {
|
||||
console.log(event.payload)
|
||||
await peerConnection.setRemoteDescription(new RTCSessionDescription(event.payload as RTCSessionDescriptionInit))
|
||||
const answer = await peerConnection.createAnswer()
|
||||
await peerConnection.setLocalDescription(answer)
|
||||
// 在这里,你需要将应答发送给发送方
|
||||
// 你可以使用信令服务器来发送应答,或者将应答复制粘贴到发送方页面
|
||||
console.log(JSON.stringify(answer))
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getCurrentWebviewWindow().show()
|
||||
await emit('SharedScreenWin')
|
||||
|
||||
@@ -245,7 +245,7 @@ onMounted(async () => {
|
||||
// 监听其他窗口发来的WebSocket发送请求
|
||||
// TODO:频繁切换会话会导致频繁请求,切换的时候也会有点卡顿
|
||||
if (appWindow.label === 'home') {
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('search_to_msg', (event: { payload: { uid: string; roomType: number } }) => {
|
||||
openMsgSession(event.payload.uid, event.payload.roomType)
|
||||
})
|
||||
|
||||
@@ -367,7 +367,7 @@ onMounted(async () => {
|
||||
// 显示窗口
|
||||
await getCurrentWebviewWindow().show()
|
||||
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('update-image', (event: any) => {
|
||||
const { list, index } = event.payload
|
||||
imageList.value = list
|
||||
|
||||
@@ -138,7 +138,7 @@ onMounted(async () => {
|
||||
const webviewWindow = getCurrentWebviewWindow()
|
||||
const label = webviewWindow.label
|
||||
|
||||
addListener(
|
||||
await addListener(
|
||||
listen(`${label}:update`, (event: any) => {
|
||||
const payload: PayloadData = event.payload.payload
|
||||
console.log('payload更新:', payload)
|
||||
|
||||
@@ -368,7 +368,7 @@ onMounted(async () => {
|
||||
await getCurrentWebviewWindow().show()
|
||||
|
||||
// 修改事件名称与发送端保持一致
|
||||
addListener(
|
||||
await addListener(
|
||||
appWindow.listen('video-updated', (event: any) => {
|
||||
const { list, index } = event.payload
|
||||
videoList.value = list
|
||||
|
||||
Reference in New Issue
Block a user