|
|
@@ -1,12 +1,10 @@
|
|
|
import { app, shell, BrowserWindow, ipcMain, Tray, Menu, nativeImage, WebContentsView, IpcMainEvent, dialog, screen } from 'electron'
|
|
|
-import { join } from 'path'
|
|
|
+import { basename, extname, join } from 'path'
|
|
|
+import { fileURLToPath } from 'url'
|
|
|
import { autoUpdater } from 'electron-updater'
|
|
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
|
|
import { mkdirSync, appendFileSync, existsSync, writeFileSync } from 'fs'
|
|
|
import { writeFile } from 'fs/promises'
|
|
|
-import https from 'https'
|
|
|
-import http from 'http'
|
|
|
-
|
|
|
// --- 日志管理 ---
|
|
|
let logDir: string = ''
|
|
|
|
|
|
@@ -40,8 +38,6 @@ let mainWindow: BrowserWindow | null = null
|
|
|
let tray: Tray | null = null
|
|
|
let isQuitting = false
|
|
|
let blinkInterval: NodeJS.Timeout | null = null
|
|
|
-let previewWindow: BrowserWindow | null = null
|
|
|
-let videoPlayerWindow: BrowserWindow | null = null
|
|
|
let trayPopupWindow: BrowserWindow | null = null
|
|
|
let trayPopupHideTimer: NodeJS.Timeout | null = null
|
|
|
let trayHoverCheckTimer: NodeJS.Timeout | null = null
|
|
|
@@ -53,6 +49,9 @@ interface UnreadInfo {
|
|
|
}
|
|
|
const unreadMap = new Map<number, UnreadInfo>()
|
|
|
|
|
|
+/** 渲染进程通过 GET /messages/unread-count 同步的总未读数(标题/任务栏角标);未同步时回退为 unreadMap 累加 */
|
|
|
+let appUnreadTotalFromApi: number | null = null
|
|
|
+
|
|
|
function isHttpUrl(url: string): boolean {
|
|
|
try {
|
|
|
const u = new URL(url.trim())
|
|
|
@@ -62,6 +61,71 @@ function isHttpUrl(url: string): boolean {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function sanitizeFilename(name: string): string {
|
|
|
+ return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').slice(0, 120) || 'file'
|
|
|
+}
|
|
|
+
|
|
|
+function extFromUrlOrData(url: string, fallback: string): string {
|
|
|
+ if (url.startsWith('data:')) {
|
|
|
+ const mime = /^data:([^;]+);/i.exec(url)?.[1]?.toLowerCase() || ''
|
|
|
+ if (mime.includes('jpeg')) return '.jpg'
|
|
|
+ if (mime.includes('png')) return '.png'
|
|
|
+ if (mime.includes('gif')) return '.gif'
|
|
|
+ if (mime.includes('webp')) return '.webp'
|
|
|
+ if (mime.includes('mp4')) return '.mp4'
|
|
|
+ if (mime.includes('webm')) return '.webm'
|
|
|
+ return fallback
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const pathname = new URL(url.trim()).pathname
|
|
|
+ const e = extname(pathname).toLowerCase()
|
|
|
+ if (e && e.length <= 8) return e
|
|
|
+ } catch {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+ return fallback
|
|
|
+}
|
|
|
+
|
|
|
+/** 下载媒体到临时目录后用系统默认应用打开(避免内嵌 data: 预览阻塞主进程) */
|
|
|
+async function downloadMediaToTempAndOpen(url: string, suggestedName: string): Promise<void> {
|
|
|
+ const base = sanitizeFilename(basename(suggestedName || 'media'))
|
|
|
+ let ext = extname(base)
|
|
|
+ if (!ext) {
|
|
|
+ ext = extFromUrlOrData(url, '.bin')
|
|
|
+ }
|
|
|
+ const nameWithoutExt = base.replace(/\.[^.]+$/, '') || 'media'
|
|
|
+ const fileName = `${nameWithoutExt}-${Date.now()}${ext}`
|
|
|
+
|
|
|
+ const dir = join(app.getPath('temp'), 'yunzhu-im-open')
|
|
|
+ mkdirSync(dir, { recursive: true })
|
|
|
+ const filePath = join(dir, fileName)
|
|
|
+
|
|
|
+ if (url.startsWith('data:')) {
|
|
|
+ const comma = url.indexOf(',')
|
|
|
+ if (comma === -1) throw new Error('无效的媒体数据')
|
|
|
+ const header = url.slice(0, comma)
|
|
|
+ const payload = url.slice(comma + 1)
|
|
|
+ const isBase64 = /;base64/i.test(header)
|
|
|
+ const buffer = isBase64 ? Buffer.from(payload, 'base64') : Buffer.from(decodeURIComponent(payload), 'utf-8')
|
|
|
+ await writeFile(filePath, buffer)
|
|
|
+ } else if (url.startsWith('file://')) {
|
|
|
+ const localPath = fileURLToPath(url)
|
|
|
+ const err = await shell.openPath(localPath)
|
|
|
+ if (err) throw new Error(err)
|
|
|
+ return
|
|
|
+ } else if (isHttpUrl(url)) {
|
|
|
+ const res = await fetch(url)
|
|
|
+ if (!res.ok) throw new Error(`下载失败: HTTP ${res.status}`)
|
|
|
+ const buf = Buffer.from(await res.arrayBuffer())
|
|
|
+ await writeFile(filePath, buf)
|
|
|
+ } else {
|
|
|
+ throw new Error('不支持的媒体地址')
|
|
|
+ }
|
|
|
+
|
|
|
+ const err = await shell.openPath(filePath)
|
|
|
+ if (err) throw new Error(err)
|
|
|
+}
|
|
|
+
|
|
|
// --- 浏览器视图管理器 ---
|
|
|
interface TabInfo {
|
|
|
id: string
|
|
|
@@ -248,6 +312,9 @@ function getIconPath(): string {
|
|
|
}
|
|
|
|
|
|
function getTotalUnreadCount(): number {
|
|
|
+ if (appUnreadTotalFromApi !== null) {
|
|
|
+ return appUnreadTotalFromApi
|
|
|
+ }
|
|
|
let total = 0
|
|
|
for (const info of unreadMap.values()) {
|
|
|
total += info.count
|
|
|
@@ -753,549 +820,6 @@ function createInternalBrowserWindow(targetUrl: string): void {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-// 创建图片预览窗口
|
|
|
-function createImagePreviewWindow(imageUrl: string): void {
|
|
|
- // 如果窗口已存在,先关闭
|
|
|
- if (previewWindow && !previewWindow.isDestroyed()) {
|
|
|
- previewWindow.close()
|
|
|
- }
|
|
|
-
|
|
|
- const iconPath = getIconPath()
|
|
|
-
|
|
|
- // 获取主窗口的位置和大小,使预览窗口覆盖主窗口
|
|
|
- let x = 0, y = 0, width = 1000, height = 700
|
|
|
- if (mainWindow && !mainWindow.isDestroyed()) {
|
|
|
- const bounds = mainWindow.getBounds()
|
|
|
- x = bounds.x
|
|
|
- y = bounds.y
|
|
|
- width = bounds.width
|
|
|
- height = bounds.height
|
|
|
- }
|
|
|
-
|
|
|
- previewWindow = new BrowserWindow({
|
|
|
- width: width,
|
|
|
- height: height,
|
|
|
- x: x,
|
|
|
- y: y,
|
|
|
- frame: false, // 无边框窗口
|
|
|
- alwaysOnTop: true, // 始终置顶
|
|
|
- transparent: false,
|
|
|
- backgroundColor: '#000000',
|
|
|
- show: false,
|
|
|
- resizable: true,
|
|
|
- minimizable: false,
|
|
|
- maximizable: false,
|
|
|
- closable: true,
|
|
|
- skipTaskbar: false,
|
|
|
- webPreferences: {
|
|
|
- preload: join(__dirname, '../preload/index.js'),
|
|
|
- sandbox: false,
|
|
|
- nodeIntegration: false,
|
|
|
- contextIsolation: true
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // 创建预览页面的 HTML 内容
|
|
|
- const htmlContent = `
|
|
|
-<!DOCTYPE html>
|
|
|
-<html>
|
|
|
-<head>
|
|
|
- <meta charset="UTF-8">
|
|
|
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
- <title>图片预览</title>
|
|
|
- <style>
|
|
|
- * {
|
|
|
- margin: 0;
|
|
|
- padding: 0;
|
|
|
- box-sizing: border-box;
|
|
|
- }
|
|
|
- body {
|
|
|
- width: 100vw;
|
|
|
- height: 100vh;
|
|
|
- background-color: rgba(0, 0, 0, 0.95);
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- overflow: hidden;
|
|
|
- cursor: pointer;
|
|
|
- }
|
|
|
- .image-container {
|
|
|
- position: relative;
|
|
|
- max-width: 90%;
|
|
|
- max-height: 90%;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- }
|
|
|
- img {
|
|
|
- max-width: 100%;
|
|
|
- max-height: 100%;
|
|
|
- object-fit: contain;
|
|
|
- cursor: default;
|
|
|
- }
|
|
|
- .toolbar {
|
|
|
- position: fixed;
|
|
|
- top: 20px;
|
|
|
- right: 20px;
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
- z-index: 1000;
|
|
|
- }
|
|
|
- .toolbar-btn {
|
|
|
- padding: 10px 20px;
|
|
|
- background-color: rgba(255, 255, 255, 0.2);
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
- border-radius: 4px;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 16px;
|
|
|
- transition: background-color 0.2s;
|
|
|
- }
|
|
|
- .toolbar-btn:hover {
|
|
|
- background-color: rgba(255, 255, 255, 0.3);
|
|
|
- }
|
|
|
- .toolbar-btn:active {
|
|
|
- background-color: rgba(255, 255, 255, 0.4);
|
|
|
- }
|
|
|
- .toast {
|
|
|
- position: fixed;
|
|
|
- top: 50%;
|
|
|
- left: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- background-color: rgba(0, 0, 0, 0.8);
|
|
|
- color: white;
|
|
|
- padding: 15px 30px;
|
|
|
- border-radius: 8px;
|
|
|
- font-size: 16px;
|
|
|
- z-index: 10000;
|
|
|
- display: none;
|
|
|
- pointer-events: none;
|
|
|
- }
|
|
|
- .toast.show {
|
|
|
- display: block;
|
|
|
- animation: fadeInOut 2s ease-in-out;
|
|
|
- }
|
|
|
- @keyframes fadeInOut {
|
|
|
- 0%, 100% { opacity: 0; }
|
|
|
- 10%, 90% { opacity: 1; }
|
|
|
- }
|
|
|
- </style>
|
|
|
-</head>
|
|
|
-<body>
|
|
|
- <div class="image-container">
|
|
|
- <img id="preview-image" src="${imageUrl.replace(/"/g, '"').replace(/'/g, ''')}" alt="预览图片" />
|
|
|
- <div id="toast" class="toast"></div>
|
|
|
- </div>
|
|
|
- <div class="toolbar">
|
|
|
- <button id="save-btn" class="toolbar-btn">💾 保存</button>
|
|
|
- <button id="close-btn" class="toolbar-btn">✕ 关闭</button>
|
|
|
- </div>
|
|
|
- <script>
|
|
|
- (function() {
|
|
|
- const imageUrl = ${JSON.stringify(imageUrl)};
|
|
|
-
|
|
|
- // 保存按钮点击事件
|
|
|
- document.getElementById('save-btn').addEventListener('click', (e) => {
|
|
|
- e.stopPropagation();
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.send('save-image-preview', imageUrl);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 关闭按钮点击事件
|
|
|
- document.getElementById('close-btn').addEventListener('click', (e) => {
|
|
|
- e.stopPropagation();
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.send('close-image-preview');
|
|
|
- }
|
|
|
- });
|
|
|
- // 点击背景关闭
|
|
|
- document.body.addEventListener('click', (e) => {
|
|
|
- if (e.target === document.body || e.target.classList.contains('image-container')) {
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.send('close-image-preview')
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // 按 ESC 键关闭
|
|
|
- document.addEventListener('keydown', (e) => {
|
|
|
- if (e.key === 'Escape') {
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.send('close-image-preview')
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // 阻止图片点击事件冒泡
|
|
|
- const img = document.querySelector('img')
|
|
|
- if (img) {
|
|
|
- img.addEventListener('click', (e) => {
|
|
|
- e.stopPropagation()
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- // 显示提示消息
|
|
|
- function showToast(message) {
|
|
|
- const toast = document.getElementById('toast')
|
|
|
- if (toast) {
|
|
|
- toast.textContent = message
|
|
|
- toast.classList.add('show')
|
|
|
- setTimeout(() => {
|
|
|
- toast.classList.remove('show')
|
|
|
- }, 2000)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 监听保存成功/失败消息
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.on('save-image-success', (_, filePath) => {
|
|
|
- const fileName = filePath.split(/[/\\\\]/).pop()
|
|
|
- showToast('保存成功: ' + fileName)
|
|
|
- })
|
|
|
-
|
|
|
- window.electron.ipcRenderer.on('save-image-error', (_, error) => {
|
|
|
- showToast('保存失败: ' + error)
|
|
|
- })
|
|
|
- }
|
|
|
- })()
|
|
|
- </script>
|
|
|
-</body>
|
|
|
-</html>
|
|
|
- `
|
|
|
-
|
|
|
- previewWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`)
|
|
|
-
|
|
|
- previewWindow.once('ready-to-show', () => {
|
|
|
- previewWindow?.show()
|
|
|
- previewWindow?.focus()
|
|
|
- })
|
|
|
-
|
|
|
- previewWindow.on('closed', () => {
|
|
|
- previewWindow = null
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// 创建视频播放器窗口
|
|
|
-function createVideoPlayerWindow(videoUrl: string, videoTitle?: string): void {
|
|
|
- // 如果窗口已存在,先关闭
|
|
|
- if (videoPlayerWindow && !videoPlayerWindow.isDestroyed()) {
|
|
|
- videoPlayerWindow.close()
|
|
|
- }
|
|
|
-
|
|
|
- const iconPath = getIconPath()
|
|
|
-
|
|
|
- // 获取主窗口的位置和大小,使播放器窗口覆盖主窗口
|
|
|
- let x = 0, y = 0, width = 1000, height = 700
|
|
|
- if (mainWindow && !mainWindow.isDestroyed()) {
|
|
|
- const bounds = mainWindow.getBounds()
|
|
|
- x = bounds.x
|
|
|
- y = bounds.y
|
|
|
- width = bounds.width
|
|
|
- height = bounds.height
|
|
|
- }
|
|
|
-
|
|
|
- videoPlayerWindow = new BrowserWindow({
|
|
|
- width: width,
|
|
|
- height: height,
|
|
|
- x: x,
|
|
|
- y: y,
|
|
|
- frame: false, // 无边框窗口
|
|
|
- alwaysOnTop: false, // 不置顶,允许用户切换窗口
|
|
|
- transparent: false,
|
|
|
- backgroundColor: '#000000',
|
|
|
- show: false,
|
|
|
- resizable: true,
|
|
|
- minimizable: true,
|
|
|
- maximizable: true,
|
|
|
- closable: true,
|
|
|
- skipTaskbar: false,
|
|
|
- webPreferences: {
|
|
|
- preload: join(__dirname, '../preload/index.js'),
|
|
|
- sandbox: false,
|
|
|
- nodeIntegration: false,
|
|
|
- contextIsolation: true,
|
|
|
- webSecurity: true // 启用 Web 安全,支持跨域视频
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // 创建视频播放器页面的 HTML 内容
|
|
|
- const htmlContent = `
|
|
|
-<!DOCTYPE html>
|
|
|
-<html>
|
|
|
-<head>
|
|
|
- <meta charset="UTF-8">
|
|
|
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
- <title>${videoTitle || '视频播放'}</title>
|
|
|
- <style>
|
|
|
- * {
|
|
|
- margin: 0;
|
|
|
- padding: 0;
|
|
|
- box-sizing: border-box;
|
|
|
- }
|
|
|
- body {
|
|
|
- width: 100vw;
|
|
|
- height: 100vh;
|
|
|
- background-color: #000000;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- overflow: hidden;
|
|
|
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
- }
|
|
|
- .video-container {
|
|
|
- position: relative;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- }
|
|
|
- video {
|
|
|
- max-width: 100%;
|
|
|
- max-height: 100%;
|
|
|
- width: auto;
|
|
|
- height: auto;
|
|
|
- outline: none;
|
|
|
- }
|
|
|
- .toolbar {
|
|
|
- position: fixed;
|
|
|
- top: 20px;
|
|
|
- right: 20px;
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
- z-index: 1000;
|
|
|
- opacity: 0;
|
|
|
- transition: opacity 0.3s;
|
|
|
- }
|
|
|
- .toolbar.visible {
|
|
|
- opacity: 1;
|
|
|
- }
|
|
|
- .toolbar-btn {
|
|
|
- padding: 10px 20px;
|
|
|
- background-color: rgba(255, 255, 255, 0.2);
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
- border-radius: 4px;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 16px;
|
|
|
- transition: background-color 0.2s;
|
|
|
- backdrop-filter: blur(10px);
|
|
|
- }
|
|
|
- .toolbar-btn:hover {
|
|
|
- background-color: rgba(255, 255, 255, 0.3);
|
|
|
- }
|
|
|
- .toolbar-btn:active {
|
|
|
- background-color: rgba(255, 255, 255, 0.4);
|
|
|
- }
|
|
|
- .toast {
|
|
|
- position: fixed;
|
|
|
- top: 50%;
|
|
|
- left: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- background-color: rgba(0, 0, 0, 0.8);
|
|
|
- color: white;
|
|
|
- padding: 15px 30px;
|
|
|
- border-radius: 8px;
|
|
|
- font-size: 16px;
|
|
|
- z-index: 10000;
|
|
|
- display: none;
|
|
|
- pointer-events: none;
|
|
|
- }
|
|
|
- .toast.show {
|
|
|
- display: block;
|
|
|
- animation: fadeInOut 2s ease-in-out;
|
|
|
- }
|
|
|
- @keyframes fadeInOut {
|
|
|
- 0%, 100% { opacity: 0; }
|
|
|
- 10%, 90% { opacity: 1; }
|
|
|
- }
|
|
|
- .loading {
|
|
|
- position: fixed;
|
|
|
- top: 50%;
|
|
|
- left: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- color: white;
|
|
|
- font-size: 18px;
|
|
|
- z-index: 999;
|
|
|
- }
|
|
|
- .error {
|
|
|
- position: fixed;
|
|
|
- top: 50%;
|
|
|
- left: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- color: #ff4444;
|
|
|
- font-size: 18px;
|
|
|
- text-align: center;
|
|
|
- z-index: 999;
|
|
|
- }
|
|
|
- </style>
|
|
|
-</head>
|
|
|
-<body>
|
|
|
- <div class="video-container">
|
|
|
- <video
|
|
|
- id="video-player"
|
|
|
- controls
|
|
|
- preload="auto"
|
|
|
- playsinline
|
|
|
- webkit-playsinline
|
|
|
- style="width: 100%; height: 100%; object-fit: contain;"
|
|
|
- >
|
|
|
- 您的浏览器不支持视频播放
|
|
|
- </video>
|
|
|
- <div id="loading" class="loading">加载中...</div>
|
|
|
- <div id="error" class="error" style="display: none;"></div>
|
|
|
- <div id="toast" class="toast"></div>
|
|
|
- </div>
|
|
|
- <div class="toolbar" id="toolbar">
|
|
|
- <button id="download-btn" class="toolbar-btn">⬇️ 下载</button>
|
|
|
- <button id="fullscreen-btn" class="toolbar-btn">⛶ 全屏</button>
|
|
|
- <button id="close-btn" class="toolbar-btn">✕ 关闭</button>
|
|
|
- </div>
|
|
|
- <script>
|
|
|
- (function() {
|
|
|
- const videoUrl = ${JSON.stringify(videoUrl)};
|
|
|
- const videoTitle = ${JSON.stringify(videoTitle || 'video.mp4')};
|
|
|
- const video = document.getElementById('video-player');
|
|
|
- const loading = document.getElementById('loading');
|
|
|
- const error = document.getElementById('error');
|
|
|
- const toolbar = document.getElementById('toolbar');
|
|
|
-
|
|
|
- // 设置视频源
|
|
|
- video.src = videoUrl;
|
|
|
-
|
|
|
- // 显示工具栏
|
|
|
- function showToolbar() {
|
|
|
- toolbar.classList.add('visible');
|
|
|
- }
|
|
|
-
|
|
|
- function hideToolbar() {
|
|
|
- toolbar.classList.remove('visible');
|
|
|
- }
|
|
|
-
|
|
|
- // 鼠标移动时显示/隐藏工具栏
|
|
|
- let toolbarTimeout;
|
|
|
- document.addEventListener('mousemove', () => {
|
|
|
- showToolbar();
|
|
|
- clearTimeout(toolbarTimeout);
|
|
|
- toolbarTimeout = setTimeout(hideToolbar, 3000);
|
|
|
- });
|
|
|
-
|
|
|
- // 视频加载事件
|
|
|
- video.addEventListener('loadedmetadata', () => {
|
|
|
- loading.style.display = 'none';
|
|
|
- showToolbar();
|
|
|
- setTimeout(hideToolbar, 3000);
|
|
|
- });
|
|
|
-
|
|
|
- video.addEventListener('loadstart', () => {
|
|
|
- loading.style.display = 'block';
|
|
|
- error.style.display = 'none';
|
|
|
- });
|
|
|
-
|
|
|
- video.addEventListener('error', (e) => {
|
|
|
- loading.style.display = 'none';
|
|
|
- error.style.display = 'block';
|
|
|
- error.textContent = '视频加载失败,请检查网络连接或视频链接是否有效';
|
|
|
- console.error('Video error:', e);
|
|
|
- });
|
|
|
-
|
|
|
- // 下载按钮
|
|
|
- document.getElementById('download-btn').addEventListener('click', (e) => {
|
|
|
- e.stopPropagation();
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.send('download-video', { url: videoUrl, filename: videoTitle });
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 全屏按钮
|
|
|
- document.getElementById('fullscreen-btn').addEventListener('click', (e) => {
|
|
|
- e.stopPropagation();
|
|
|
- if (video.requestFullscreen) {
|
|
|
- video.requestFullscreen();
|
|
|
- } else if (video.webkitRequestFullscreen) {
|
|
|
- video.webkitRequestFullscreen();
|
|
|
- } else if (video.mozRequestFullScreen) {
|
|
|
- video.mozRequestFullScreen();
|
|
|
- } else if (video.msRequestFullscreen) {
|
|
|
- video.msRequestFullscreen();
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 关闭按钮
|
|
|
- document.getElementById('close-btn').addEventListener('click', (e) => {
|
|
|
- e.stopPropagation();
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.send('close-video-player');
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 点击背景关闭(仅在工具栏可见时)
|
|
|
- document.body.addEventListener('click', (e) => {
|
|
|
- if (e.target === document.body || e.target.classList.contains('video-container')) {
|
|
|
- if (toolbar.classList.contains('visible')) {
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.send('close-video-player');
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 按 ESC 键关闭
|
|
|
- document.addEventListener('keydown', (e) => {
|
|
|
- if (e.key === 'Escape') {
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.send('close-video-player');
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 阻止视频点击事件冒泡
|
|
|
- video.addEventListener('click', (e) => {
|
|
|
- e.stopPropagation();
|
|
|
- });
|
|
|
-
|
|
|
- // 显示提示消息
|
|
|
- function showToast(message) {
|
|
|
- const toast = document.getElementById('toast');
|
|
|
- if (toast) {
|
|
|
- toast.textContent = message;
|
|
|
- toast.classList.add('show');
|
|
|
- setTimeout(() => {
|
|
|
- toast.classList.remove('show');
|
|
|
- }, 2000);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 监听下载成功/失败消息
|
|
|
- if (window.electron && window.electron.ipcRenderer) {
|
|
|
- window.electron.ipcRenderer.on('download-video-success', (_, filePath) => {
|
|
|
- const fileName = filePath.split(/[/\\\\]/).pop();
|
|
|
- showToast('下载成功: ' + fileName);
|
|
|
- });
|
|
|
-
|
|
|
- window.electron.ipcRenderer.on('download-video-error', (_, error) => {
|
|
|
- showToast('下载失败: ' + error);
|
|
|
- });
|
|
|
- }
|
|
|
- })();
|
|
|
- </script>
|
|
|
-</body>
|
|
|
-</html>
|
|
|
- `
|
|
|
-
|
|
|
- videoPlayerWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`)
|
|
|
-
|
|
|
- videoPlayerWindow.once('ready-to-show', () => {
|
|
|
- videoPlayerWindow?.show()
|
|
|
- videoPlayerWindow?.focus()
|
|
|
- })
|
|
|
-
|
|
|
- videoPlayerWindow.on('closed', () => {
|
|
|
- videoPlayerWindow = null
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
// 统一处理来自渲染进程的 Browser Action
|
|
|
ipcMain.on('browser-action', (event, action) => {
|
|
|
if (!viewManager || !browserWindow || browserWindow.isDestroyed()) return
|
|
|
@@ -1395,9 +919,15 @@ app.whenReady().then(() => {
|
|
|
autoUpdater.quitAndInstall(false, true)
|
|
|
})
|
|
|
|
|
|
+ ipcMain.on('sync-unread-total', (_, total: number) => {
|
|
|
+ appUnreadTotalFromApi = typeof total === 'number' && !Number.isNaN(total) ? Math.max(0, Math.floor(total)) : 0
|
|
|
+ updateUnreadStatus()
|
|
|
+ })
|
|
|
+
|
|
|
ipcMain.on('login-success', () => {
|
|
|
// 登录成功后清空消息中心未读,避免切换账号展示上个账号的残留
|
|
|
unreadMap.clear()
|
|
|
+ appUnreadTotalFromApi = null
|
|
|
updateUnreadStatus()
|
|
|
if (mainWindow) {
|
|
|
mainWindow.setSize(1000, 700)
|
|
|
@@ -1433,6 +963,7 @@ app.whenReady().then(() => {
|
|
|
// 清除所有未读消息
|
|
|
ipcMain.on('clear-all-unread', () => {
|
|
|
unreadMap.clear()
|
|
|
+ appUnreadTotalFromApi = null
|
|
|
updateUnreadStatus()
|
|
|
})
|
|
|
|
|
|
@@ -1456,188 +987,19 @@ app.whenReady().then(() => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
- // 处理打开图片预览窗口
|
|
|
+ // 图片/视频:下载到临时目录后用系统默认应用打开(不再使用内嵌预览窗口)
|
|
|
ipcMain.on('open-image-preview', (_, imageUrl: string) => {
|
|
|
- createImagePreviewWindow(imageUrl)
|
|
|
- })
|
|
|
-
|
|
|
- // 处理关闭图片预览窗口
|
|
|
- ipcMain.on('close-image-preview', () => {
|
|
|
- if (previewWindow && !previewWindow.isDestroyed()) {
|
|
|
- previewWindow.close()
|
|
|
- }
|
|
|
+ void downloadMediaToTempAndOpen(imageUrl, 'image.png').catch((e) => {
|
|
|
+ console.error(e)
|
|
|
+ dialog.showErrorBox('打开失败', e instanceof Error ? e.message : String(e))
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
- // 处理打开视频播放器窗口
|
|
|
ipcMain.on('open-video-player', (_, videoUrl: string, videoTitle?: string) => {
|
|
|
- createVideoPlayerWindow(videoUrl, videoTitle)
|
|
|
- })
|
|
|
-
|
|
|
- // 处理关闭视频播放器窗口
|
|
|
- ipcMain.on('close-video-player', () => {
|
|
|
- if (videoPlayerWindow && !videoPlayerWindow.isDestroyed()) {
|
|
|
- videoPlayerWindow.close()
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // 处理视频下载(使用 Electron 下载 API,不会被拦截)
|
|
|
- ipcMain.on('download-video', async (event, { url, filename }) => {
|
|
|
- try {
|
|
|
- if (videoPlayerWindow && !videoPlayerWindow.isDestroyed()) {
|
|
|
- // 使用视频播放器窗口的 webContents 来下载
|
|
|
- videoPlayerWindow.webContents.downloadURL(url)
|
|
|
-
|
|
|
- videoPlayerWindow.webContents.session.once('will-download', (event, item) => {
|
|
|
- const filePath = join(app.getPath('downloads'), filename)
|
|
|
- item.setSavePath(filePath)
|
|
|
-
|
|
|
- item.once('done', (event, state) => {
|
|
|
- if (state === 'completed') {
|
|
|
- if (videoPlayerWindow && !videoPlayerWindow.isDestroyed()) {
|
|
|
- videoPlayerWindow.webContents.send('download-video-success', filePath)
|
|
|
- }
|
|
|
- } else {
|
|
|
- if (videoPlayerWindow && !videoPlayerWindow.isDestroyed()) {
|
|
|
- videoPlayerWindow.webContents.send('download-video-error', '下载失败')
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
- })
|
|
|
- } else if (mainWindow && !mainWindow.isDestroyed()) {
|
|
|
- // 如果视频播放器窗口不存在,使用主窗口
|
|
|
- mainWindow.webContents.downloadURL(url)
|
|
|
-
|
|
|
- mainWindow.webContents.session.once('will-download', (event, item) => {
|
|
|
- const filePath = join(app.getPath('downloads'), filename)
|
|
|
- item.setSavePath(filePath)
|
|
|
- })
|
|
|
- }
|
|
|
- } catch (error: any) {
|
|
|
- console.error('Failed to download video:', error)
|
|
|
- if (videoPlayerWindow && !videoPlayerWindow.isDestroyed()) {
|
|
|
- videoPlayerWindow.webContents.send('download-video-error', error.message || '下载失败')
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
-
|
|
|
- // 处理保存图片
|
|
|
- ipcMain.on('save-image-preview', async (_, imageUrl: string) => {
|
|
|
- try {
|
|
|
- // 临时取消预览窗口的置顶状态,以便显示保存对话框
|
|
|
- if (previewWindow && !previewWindow.isDestroyed()) {
|
|
|
- previewWindow.setAlwaysOnTop(false)
|
|
|
- }
|
|
|
-
|
|
|
- // 推断文件扩展名
|
|
|
- let defaultExt = 'png'
|
|
|
- try {
|
|
|
- const urlObj = new URL(imageUrl)
|
|
|
- const pathname = urlObj.pathname.toLowerCase()
|
|
|
- if (pathname.endsWith('.jpg') || pathname.endsWith('.jpeg')) {
|
|
|
- defaultExt = 'jpg'
|
|
|
- } else if (pathname.endsWith('.gif')) {
|
|
|
- defaultExt = 'gif'
|
|
|
- } else if (pathname.endsWith('.webp')) {
|
|
|
- defaultExt = 'webp'
|
|
|
- } else if (pathname.endsWith('.png')) {
|
|
|
- defaultExt = 'png'
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- // URL 解析失败,使用默认扩展名
|
|
|
- }
|
|
|
-
|
|
|
- // 显示保存对话框
|
|
|
- const result = await dialog.showSaveDialog({
|
|
|
- title: '保存图片',
|
|
|
- defaultPath: `image.${defaultExt}`,
|
|
|
- filters: [
|
|
|
- { name: '图片文件', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp'] },
|
|
|
- { name: '所有文件', extensions: ['*'] }
|
|
|
- ]
|
|
|
- })
|
|
|
-
|
|
|
- // 恢复预览窗口的置顶状态
|
|
|
- if (previewWindow && !previewWindow.isDestroyed()) {
|
|
|
- previewWindow.setAlwaysOnTop(true)
|
|
|
- }
|
|
|
-
|
|
|
- if (result.canceled || !result.filePath) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const filePath = result.filePath
|
|
|
-
|
|
|
- // 处理不同类型的图片 URL
|
|
|
- if (imageUrl.startsWith('data:')) {
|
|
|
- // Data URL
|
|
|
- const base64Data = imageUrl.split(',')[1]
|
|
|
- const buffer = Buffer.from(base64Data, 'base64')
|
|
|
- await writeFile(filePath, buffer)
|
|
|
- } else if (imageUrl.startsWith('blob:') || imageUrl.startsWith('file://')) {
|
|
|
- // Blob URL 或 file:// URL,需要通过预览窗口的 webContents 获取
|
|
|
- if (previewWindow && !previewWindow.isDestroyed()) {
|
|
|
- const image = await previewWindow.webContents.downloadURL(imageUrl)
|
|
|
- // 对于 blob URL,我们需要通过其他方式获取
|
|
|
- // 这里我们尝试从预览窗口的 webContents 获取图片数据
|
|
|
- const dataUrl = await previewWindow.webContents.executeJavaScript(`
|
|
|
- new Promise((resolve) => {
|
|
|
- const img = document.getElementById('preview-image');
|
|
|
- const canvas = document.createElement('canvas');
|
|
|
- canvas.width = img.naturalWidth;
|
|
|
- canvas.height = img.naturalHeight;
|
|
|
- const ctx = canvas.getContext('2d');
|
|
|
- ctx.drawImage(img, 0, 0);
|
|
|
- resolve(canvas.toDataURL('image/png'));
|
|
|
- })
|
|
|
- `)
|
|
|
- const base64Data = dataUrl.split(',')[1]
|
|
|
- const buffer = Buffer.from(base64Data, 'base64')
|
|
|
- await writeFile(filePath, buffer)
|
|
|
- }
|
|
|
- } else if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
|
|
- // HTTP/HTTPS URL
|
|
|
- const protocol = imageUrl.startsWith('https') ? https : http
|
|
|
-
|
|
|
- await new Promise<void>((resolve, reject) => {
|
|
|
- protocol.get(imageUrl, (response) => {
|
|
|
- if (response.statusCode !== 200) {
|
|
|
- reject(new Error(`Failed to download image: ${response.statusCode}`))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const chunks: Buffer[] = []
|
|
|
- response.on('data', (chunk: Buffer) => {
|
|
|
- chunks.push(chunk)
|
|
|
- })
|
|
|
- response.on('end', async () => {
|
|
|
- try {
|
|
|
- const buffer = Buffer.concat(chunks)
|
|
|
- await writeFile(filePath, buffer)
|
|
|
- resolve()
|
|
|
- } catch (error) {
|
|
|
- reject(error)
|
|
|
- }
|
|
|
- })
|
|
|
- response.on('error', reject)
|
|
|
- }).on('error', reject)
|
|
|
- })
|
|
|
- } else {
|
|
|
- throw new Error('不支持的图片 URL 格式')
|
|
|
- }
|
|
|
-
|
|
|
- // 显示成功提示
|
|
|
- if (previewWindow && !previewWindow.isDestroyed()) {
|
|
|
- previewWindow.webContents.send('save-image-success', filePath)
|
|
|
- }
|
|
|
- } catch (error: any) {
|
|
|
- console.error('Failed to save image:', error)
|
|
|
- // 恢复预览窗口的置顶状态(即使出错也要恢复)
|
|
|
- if (previewWindow && !previewWindow.isDestroyed()) {
|
|
|
- previewWindow.setAlwaysOnTop(true)
|
|
|
- previewWindow.webContents.send('save-image-error', error.message || '保存失败')
|
|
|
- }
|
|
|
- }
|
|
|
+ void downloadMediaToTempAndOpen(videoUrl, videoTitle || 'video.mp4').catch((e) => {
|
|
|
+ console.error(e)
|
|
|
+ dialog.showErrorBox('打开失败', e instanceof Error ? e.message : String(e))
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
// Create tray
|
|
|
@@ -1695,7 +1057,9 @@ app.whenReady().then(() => {
|
|
|
})
|
|
|
|
|
|
ipcMain.on('tray-popup-dismiss', () => {
|
|
|
- hideTrayPopup()
|
|
|
+ // 「暂不处理」:清空主进程托盘未读 Map,停止闪烁;不改动 appUnreadTotalFromApi,避免干扰主界面 API 未读总数
|
|
|
+ unreadMap.clear()
|
|
|
+ updateUnreadStatus()
|
|
|
})
|
|
|
|
|
|
ipcMain.on('tray-popup-mouse-leave', () => {
|