Archived
初始化仓库
This commit is contained in:
commit
2347e5ffa9
13 files changed
+834
No files matched your search
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
release/
|
||||
dist/
|
||||
out/
|
||||
*.log
|
||||
@@ -0,0 +1,65 @@
|
||||
# FMCRAFT Chat 桌面版
|
||||
|
||||
Windows 7 及以上可用的 FMCRAFT Chat 桌面客户端。使用 Electron 22(最后一个支持 Windows 7/8/8.1 的 Electron 版本)直接嵌入网页。
|
||||
|
||||
## 功能
|
||||
|
||||
- 直接加载 `config.ini` 中配置的网页地址
|
||||
- Windows 消息通知
|
||||
- Windows 10/11:系统 Toast 通知
|
||||
- Windows 7/8/8.1:托盘气泡通知
|
||||
- 任务栏闪烁
|
||||
- 本地音效播放(不依赖网页音频权限)
|
||||
- 三项通知开关均可在设置中关闭,默认全部开启
|
||||
- 配置保存在本地 `config.ini`
|
||||
|
||||
## 配置
|
||||
|
||||
默认 `config.ini`:
|
||||
|
||||
```ini
|
||||
[app]
|
||||
url=https://chat.fmcraft.top
|
||||
|
||||
[notifications]
|
||||
windows=true
|
||||
flash=true
|
||||
sound=true
|
||||
```
|
||||
|
||||
程序会优先读写可执行文件同目录下的 `config.ini`。如果该目录不可写(例如安装在 `Program Files`),会自动回退到 `%APPDATA%\FMCRAFT Chat\config.ini`,设置页会显示实际使用的路径。
|
||||
|
||||
## 开发运行
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## 打包
|
||||
|
||||
```bash
|
||||
# 64 位 Windows 7+
|
||||
npm run dist:x64
|
||||
|
||||
# 32 位 Windows 7+
|
||||
npm run dist:ia32
|
||||
```
|
||||
|
||||
生成文件在 `release/` 目录。推荐分发 `FMCRAFT-Chat-1.0.0-x64.exe` 便携版。
|
||||
|
||||
## 通知开关
|
||||
|
||||
可以通过以下位置打开设置:
|
||||
|
||||
- 托盘图标右键 →「通知设置…」
|
||||
- 菜单栏「文件」→「通知设置…」
|
||||
- 快捷键 `Ctrl+,`
|
||||
|
||||
设置窗口内也可以直接测试当前开关组合。
|
||||
|
||||
## 说明
|
||||
|
||||
- Windows 7 建议安装 SP1 和系统证书更新。
|
||||
- 网页端通过 `window.desktopBridge.notify()` 调用桌面通知;普通浏览器中该桥接不存在,不影响网页使用。
|
||||
- 桌面通知只在当前没有停留在对应聊天窗口时触发。任务栏闪烁只在窗口未聚焦时执行。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 245 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
+23
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>FMCRAFT Chat notification sound</title>
|
||||
</head>
|
||||
<body>
|
||||
<audio id="sound" src="assets/notification.wav" preload="auto"></audio>
|
||||
<script>
|
||||
window.playNotification = function () {
|
||||
var sound = document.getElementById('sound')
|
||||
if (!sound) return
|
||||
try {
|
||||
sound.currentTime = 0
|
||||
var promise = sound.play()
|
||||
if (promise && typeof promise.catch === 'function') promise.catch(function () {})
|
||||
} catch (err) {
|
||||
/* 忽略音频播放失败 */
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
[app]
|
||||
url=https://chat.fmcraft.top
|
||||
|
||||
[notifications]
|
||||
windows=true
|
||||
flash=true
|
||||
sound=true
|
||||
@@ -0,0 +1,473 @@
|
||||
const {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Tray,
|
||||
Menu,
|
||||
Notification,
|
||||
ipcMain,
|
||||
shell,
|
||||
dialog,
|
||||
nativeImage,
|
||||
} = require('electron')
|
||||
const fs = require('node:fs')
|
||||
const os = require('node:os')
|
||||
const path = require('node:path')
|
||||
|
||||
const APP_ID = 'top.fmcraft.chat.desktop'
|
||||
const DEFAULT_URL = 'https://chat.fmcraft.top'
|
||||
const DEFAULT_CONFIG = {
|
||||
url: DEFAULT_URL,
|
||||
windows: true,
|
||||
flash: true,
|
||||
sound: true,
|
||||
}
|
||||
|
||||
const iconPath = path.join(__dirname, 'assets', 'icon.png')
|
||||
const trayIconPath = path.join(__dirname, 'assets', 'tray.png')
|
||||
|
||||
let config = { ...DEFAULT_CONFIG }
|
||||
let configPath = ''
|
||||
let mainWindow = null
|
||||
let settingsWindow = null
|
||||
let tray = null
|
||||
let audioWindow = null
|
||||
let audioReady = false
|
||||
let pendingSound = false
|
||||
|
||||
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
|
||||
app.setName('FMCRAFT Chat')
|
||||
|
||||
function defaultConfigText() {
|
||||
return [
|
||||
'[app]',
|
||||
`url=${DEFAULT_URL}`,
|
||||
'',
|
||||
'[notifications]',
|
||||
'windows=true',
|
||||
'flash=true',
|
||||
'sound=true',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function parseIni(text) {
|
||||
const result = {}
|
||||
let section = ''
|
||||
for (const raw of text.split(/\r?\n/)) {
|
||||
const line = raw.trim()
|
||||
if (!line || line.startsWith(';') || line.startsWith('#')) continue
|
||||
const sectionMatch = line.match(/^\[([^\]]+)\]$/)
|
||||
if (sectionMatch) {
|
||||
section = sectionMatch[1].trim()
|
||||
if (!result[section]) result[section] = {}
|
||||
continue
|
||||
}
|
||||
const index = line.indexOf('=')
|
||||
if (index === -1) continue
|
||||
const key = line.slice(0, index).trim()
|
||||
const value = line
|
||||
.slice(index + 1)
|
||||
.trim()
|
||||
.replace(/^"(.*)"$/, '$1')
|
||||
if (!result[section]) result[section] = {}
|
||||
result[section][key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function boolValue(value, fallback) {
|
||||
if (value === undefined || value === null || value === '') return fallback
|
||||
return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase())
|
||||
}
|
||||
|
||||
function resolveConfigPath() {
|
||||
const portableDir = process.env.PORTABLE_EXECUTABLE_DIR
|
||||
const localDir = portableDir || (app.isPackaged ? path.dirname(app.getPath('exe')) : __dirname)
|
||||
const localPath = path.join(localDir, 'config.ini')
|
||||
try {
|
||||
if (!fs.existsSync(localPath)) fs.writeFileSync(localPath, defaultConfigText(), 'utf8')
|
||||
fs.accessSync(localPath, fs.constants.W_OK)
|
||||
return localPath
|
||||
} catch {
|
||||
// Program Files 等只读目录:回退到用户数据目录,设置页会显示实际路径。
|
||||
const fallbackDir = app.getPath('userData')
|
||||
fs.mkdirSync(fallbackDir, { recursive: true })
|
||||
const fallbackPath = path.join(fallbackDir, 'config.ini')
|
||||
if (!fs.existsSync(fallbackPath)) {
|
||||
fs.writeFileSync(fallbackPath, defaultConfigText(), 'utf8')
|
||||
}
|
||||
return fallbackPath
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
configPath = resolveConfigPath()
|
||||
let raw = {}
|
||||
try {
|
||||
raw = parseIni(fs.readFileSync(configPath, 'utf8'))
|
||||
} catch {
|
||||
raw = {}
|
||||
}
|
||||
const appSection = raw.app || {}
|
||||
const notifications = raw.notifications || {}
|
||||
config = {
|
||||
url: (appSection.url || DEFAULT_URL).trim() || DEFAULT_URL,
|
||||
windows: boolValue(notifications.windows, true),
|
||||
flash: boolValue(notifications.flash, true),
|
||||
sound: boolValue(notifications.sound, true),
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
const text = [
|
||||
'[app]',
|
||||
`url=${config.url}`,
|
||||
'',
|
||||
'[notifications]',
|
||||
`windows=${config.windows ? 'true' : 'false'}`,
|
||||
`flash=${config.flash ? 'true' : 'false'}`,
|
||||
`sound=${config.sound ? 'true' : 'false'}`,
|
||||
'',
|
||||
].join('\n')
|
||||
try {
|
||||
fs.writeFileSync(configPath, text, 'utf8')
|
||||
} catch (err) {
|
||||
dialog.showErrorBox('保存配置失败', String(err && err.message ? err.message : err))
|
||||
}
|
||||
}
|
||||
|
||||
function isSameSite(url) {
|
||||
try {
|
||||
return new URL(url).origin === new URL(config.url).origin
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 820,
|
||||
minWidth: 960,
|
||||
minHeight: 600,
|
||||
title: 'FMCRAFT Chat',
|
||||
icon: iconPath,
|
||||
backgroundColor: '#0b0e14',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
spellcheck: true,
|
||||
},
|
||||
})
|
||||
|
||||
mainWindow.loadURL(config.url).catch((err) => {
|
||||
dialog.showErrorBox(
|
||||
'无法打开 FMCRAFT Chat',
|
||||
`请检查 config.ini 中的页面地址是否可访问。\n\n${String(err && err.message ? err.message : err)}`
|
||||
)
|
||||
})
|
||||
|
||||
mainWindow.on('focus', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.flashFrame(false)
|
||||
})
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
// 站内链接留在应用内,外部链接交给系统浏览器。
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
if (url.startsWith('http') && !isSameSite(url)) {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showMainWindow() {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
createMainWindow()
|
||||
return
|
||||
}
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
|
||||
function createAudioWindow() {
|
||||
audioWindow = new BrowserWindow({
|
||||
show: false,
|
||||
skipTaskbar: true,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
})
|
||||
audioWindow.loadFile(path.join(__dirname, 'audio.html'))
|
||||
audioWindow.webContents.on('did-finish-load', () => {
|
||||
audioReady = true
|
||||
if (pendingSound) {
|
||||
pendingSound = false
|
||||
playSound(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function playSound(enabled = config.sound) {
|
||||
if (!enabled) return
|
||||
if (!audioReady || !audioWindow || audioWindow.isDestroyed()) {
|
||||
pendingSound = true
|
||||
return
|
||||
}
|
||||
audioWindow.webContents
|
||||
.executeJavaScript('window.playNotification && window.playNotification()')
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function isLegacyWindows() {
|
||||
return process.platform === 'win32' && /^6\./.test(os.release())
|
||||
}
|
||||
|
||||
function showWindowsNotification(title, body) {
|
||||
if (isLegacyWindows() && tray) {
|
||||
// Windows 7/8/8.1 没有 Win10 Toast,使用托盘气泡提醒。
|
||||
tray.displayBalloon({
|
||||
title,
|
||||
content: body,
|
||||
icon: iconPath,
|
||||
noSound: true,
|
||||
respectQuietTime: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!Notification.isSupported()) {
|
||||
if (tray) {
|
||||
tray.displayBalloon({ title, content: body, icon: iconPath, noSound: true })
|
||||
}
|
||||
return
|
||||
}
|
||||
const notification = new Notification({
|
||||
title,
|
||||
body,
|
||||
icon: iconPath,
|
||||
silent: true,
|
||||
})
|
||||
notification.on('click', showMainWindow)
|
||||
notification.show()
|
||||
}
|
||||
|
||||
function handleDesktopNotification(payload, override) {
|
||||
const options = override ? { ...config, ...override } : config
|
||||
const title = String(payload && payload.title ? payload.title : 'FMCRAFT Chat').slice(0, 120)
|
||||
const body = String(payload && payload.body ? payload.body : '').slice(0, 500)
|
||||
|
||||
if (options.windows) showWindowsNotification(title, body)
|
||||
if (options.flash && mainWindow && !mainWindow.isDestroyed() && !mainWindow.isFocused()) {
|
||||
mainWindow.flashFrame(true)
|
||||
}
|
||||
if (options.sound) playSound(true)
|
||||
}
|
||||
|
||||
function updateTrayMenu() {
|
||||
if (!tray || tray.isDestroyed()) return
|
||||
tray.setContextMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{ label: '显示 FMCRAFT Chat', click: showMainWindow },
|
||||
{ label: '通知设置…', click: openSettingsWindow },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Windows 消息通知',
|
||||
type: 'checkbox',
|
||||
checked: config.windows,
|
||||
click: (item) => {
|
||||
config.windows = !!item.checked
|
||||
saveConfig()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '任务栏闪烁',
|
||||
type: 'checkbox',
|
||||
checked: config.flash,
|
||||
click: (item) => {
|
||||
config.flash = !!item.checked
|
||||
saveConfig()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '音效播放',
|
||||
type: 'checkbox',
|
||||
checked: config.sound,
|
||||
click: (item) => {
|
||||
config.sound = !!item.checked
|
||||
saveConfig()
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: '退出', click: () => app.quit() },
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
try {
|
||||
const image = nativeImage.createFromPath(trayIconPath)
|
||||
tray = new Tray(image)
|
||||
tray.setToolTip('FMCRAFT Chat')
|
||||
tray.on('double-click', showMainWindow)
|
||||
tray.on('click', showMainWindow)
|
||||
tray.on('balloon-click', showMainWindow)
|
||||
updateTrayMenu()
|
||||
} catch (err) {
|
||||
console.warn('无法创建托盘图标:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function createMenu() {
|
||||
const template = [
|
||||
{
|
||||
label: '文件',
|
||||
submenu: [
|
||||
{ label: '通知设置…', accelerator: 'CmdOrCtrl+,', click: openSettingsWindow },
|
||||
{
|
||||
label: '打开配置文件',
|
||||
click: () => {
|
||||
shell.openPath(configPath).then((error) => {
|
||||
if (error) shell.showItemInFolder(configPath)
|
||||
})
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit', label: '退出' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '查看',
|
||||
submenu: [
|
||||
{ role: 'reload', label: '刷新' },
|
||||
{ role: 'forceReload', label: '强制刷新' },
|
||||
{ role: 'toggleDevTools', label: '开发者工具' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom', label: '实际大小' },
|
||||
{ role: 'zoomIn', label: '放大' },
|
||||
{ role: 'zoomOut', label: '缩小' },
|
||||
{ role: 'togglefullscreen', label: '全屏' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '帮助',
|
||||
submenu: [
|
||||
{
|
||||
label: '关于 FMCRAFT Chat',
|
||||
click: () => {
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: '关于 FMCRAFT Chat',
|
||||
message: 'FMCRAFT Chat 桌面版',
|
||||
detail: `版本 ${app.getVersion()}\n适用于 Windows 7 及以上。\n\n配置文件:${configPath}`,
|
||||
buttons: ['确定'],
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
||||
}
|
||||
|
||||
function openSettingsWindow() {
|
||||
if (settingsWindow && !settingsWindow.isDestroyed()) {
|
||||
settingsWindow.show()
|
||||
settingsWindow.focus()
|
||||
return
|
||||
}
|
||||
settingsWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 470,
|
||||
minWidth: 460,
|
||||
minHeight: 430,
|
||||
resizable: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
title: 'FMCRAFT Chat 通知设置',
|
||||
icon: iconPath,
|
||||
parent: mainWindow || undefined,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'settings-preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
},
|
||||
})
|
||||
settingsWindow.setMenuBarVisibility(false)
|
||||
settingsWindow.loadFile(path.join(__dirname, 'settings.html'))
|
||||
settingsWindow.on('closed', () => {
|
||||
settingsWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
function registerIpc() {
|
||||
ipcMain.on('desktop:notify', (_event, payload) => {
|
||||
if (!payload || typeof payload !== 'object') return
|
||||
handleDesktopNotification(payload)
|
||||
})
|
||||
|
||||
ipcMain.handle('settings:get', () => ({ ...config, configPath }))
|
||||
|
||||
ipcMain.handle('settings:save', (_event, values) => {
|
||||
if (values && typeof values === 'object') {
|
||||
config.windows = !!values.windows
|
||||
config.flash = !!values.flash
|
||||
config.sound = !!values.sound
|
||||
saveConfig()
|
||||
updateTrayMenu()
|
||||
}
|
||||
return { ...config, configPath }
|
||||
})
|
||||
|
||||
ipcMain.handle('settings:test', (_event, values) => {
|
||||
const options = values && typeof values === 'object' ? values : config
|
||||
handleDesktopNotification(
|
||||
{
|
||||
title: 'FMCRAFT Chat 通知测试',
|
||||
body: '如果你看到这条通知,说明桌面通知工作正常。',
|
||||
},
|
||||
options
|
||||
)
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
ipcMain.on('settings:open-config', () => {
|
||||
shell.openPath(configPath).then((error) => {
|
||||
if (error) shell.showItemInFolder(configPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
if (process.platform === 'win32') app.setAppUserModelId(APP_ID)
|
||||
loadConfig()
|
||||
registerIpc()
|
||||
createMainWindow()
|
||||
createMenu()
|
||||
createTray()
|
||||
createAudioWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (!mainWindow) createMainWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (audioWindow && !audioWindow.isDestroyed()) audioWindow.destroy()
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "fmcraft-chat-desktop",
|
||||
"version": "1.0.0",
|
||||
"description": "FMCRAFT Chat desktop client for Windows 7 and above",
|
||||
"main": "main.js",
|
||||
"author": "FMCRAFT",
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"dist": "electron-builder --win portable --x64",
|
||||
"dist:x64": "electron-builder --win portable --x64",
|
||||
"dist:ia32": "electron-builder --win portable --ia32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "22.3.27",
|
||||
"electron-builder": "24.13.3"
|
||||
},
|
||||
"build": {
|
||||
"appId": "top.fmcraft.chat.desktop",
|
||||
"productName": "FMCRAFT Chat",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"settings.html",
|
||||
"settings.js",
|
||||
"settings-preload.js",
|
||||
"audio.html",
|
||||
"assets/**/*"
|
||||
],
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "config.ini",
|
||||
"to": "config.ini"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
"portable"
|
||||
],
|
||||
"icon": "assets/icon.png"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "FMCRAFT-Chat-${version}-${arch}.${ext}"
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
contextBridge.exposeInMainWorld('desktopBridge', {
|
||||
isDesktop: true,
|
||||
notify(payload) {
|
||||
if (!payload || typeof payload !== 'object') return
|
||||
ipcRenderer.send('desktop:notify', {
|
||||
title: typeof payload.title === 'string' ? payload.title : '',
|
||||
body: typeof payload.body === 'string' ? payload.body : '',
|
||||
conversationId:
|
||||
typeof payload.conversationId === 'string' ? payload.conversationId : undefined,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
contextBridge.exposeInMainWorld('settingsAPI', {
|
||||
get: () => ipcRenderer.invoke('settings:get'),
|
||||
save: (values) => ipcRenderer.invoke('settings:save', values),
|
||||
test: (values) => ipcRenderer.invoke('settings:test', values),
|
||||
openConfig: () => ipcRenderer.send('settings:open-config'),
|
||||
})
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FMCRAFT Chat 通知设置</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0b0e14;
|
||||
--panel: #141922;
|
||||
--panel2: #1b2230;
|
||||
--line: #2a3444;
|
||||
--fg: #e7edf7;
|
||||
--muted: #8b98ad;
|
||||
--accent: #3b82f6;
|
||||
--danger: #ef4444;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: "Microsoft YaHei UI", "Segoe UI", sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
.card {
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: var(--panel);
|
||||
padding: 20px;
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
h1 { margin: 0; font-size: 18px; }
|
||||
.sub { margin: 6px 0 18px; color: var(--muted); font-size: 12px; line-height: 1.7; }
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--panel2);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.row input { margin-top: 2px; width: 16px; height: 16px; accent-color: var(--accent); }
|
||||
.row strong { display: block; font-size: 13px; font-weight: 600; }
|
||||
.row span { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; line-height: 1.6; }
|
||||
.path {
|
||||
margin-top: 14px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--panel2);
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
.path code { color: var(--fg); font-family: Consolas, monospace; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
|
||||
button {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--panel2);
|
||||
color: var(--fg);
|
||||
padding: 9px 14px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background: #222c3d; }
|
||||
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
button.primary:hover { filter: brightness(1.08); }
|
||||
#status { min-height: 18px; margin: 12px 0 0; color: #34d399; font-size: 12px; }
|
||||
#status.error { color: var(--danger); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>通知设置</h1>
|
||||
<p class="sub">
|
||||
当窗口未停留在对应会话时,桌面端可以提醒新消息。所有开关默认开启,设置会保存到本地
|
||||
config.ini。
|
||||
</p>
|
||||
|
||||
<label class="row">
|
||||
<input type="checkbox" id="windows" />
|
||||
<span>
|
||||
<strong>Windows 消息通知</strong>
|
||||
<span>Windows 10/11 使用系统通知;Windows 7/8 使用托盘气泡提醒。</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<input type="checkbox" id="flash" />
|
||||
<span>
|
||||
<strong>任务栏闪烁</strong>
|
||||
<span>窗口未聚焦时闪烁任务栏,点击窗口后自动停止。</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<input type="checkbox" id="sound" />
|
||||
<span>
|
||||
<strong>音效播放</strong>
|
||||
<span>收到提醒时播放桌面端本地音效,不依赖网页音频权限。</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="path">配置文件:<code id="configPath">加载中…</code></div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="test">测试通知</button>
|
||||
<button id="openConfig">打开配置文件</button>
|
||||
<button id="save" class="primary">保存设置</button>
|
||||
</div>
|
||||
<p id="status"></p>
|
||||
</div>
|
||||
<script src="settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
const api = window.settingsAPI
|
||||
const windowsInput = document.getElementById('windows')
|
||||
const flashInput = document.getElementById('flash')
|
||||
const soundInput = document.getElementById('sound')
|
||||
const configPathText = document.getElementById('configPath')
|
||||
const statusText = document.getElementById('status')
|
||||
const saveButton = document.getElementById('save')
|
||||
const testButton = document.getElementById('test')
|
||||
const openConfigButton = document.getElementById('openConfig')
|
||||
|
||||
function values() {
|
||||
return {
|
||||
windows: !!windowsInput.checked,
|
||||
flash: !!flashInput.checked,
|
||||
sound: !!soundInput.checked,
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(text, error = false) {
|
||||
statusText.textContent = text
|
||||
statusText.classList.toggle('error', error)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const config = await api.get()
|
||||
windowsInput.checked = !!config.windows
|
||||
flashInput.checked = !!config.flash
|
||||
soundInput.checked = !!config.sound
|
||||
configPathText.textContent = config.configPath
|
||||
} catch (err) {
|
||||
setStatus(`读取配置失败:${err && err.message ? err.message : err}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
saveButton.addEventListener('click', async () => {
|
||||
saveButton.disabled = true
|
||||
try {
|
||||
const config = await api.save(values())
|
||||
configPathText.textContent = config.configPath
|
||||
setStatus('已保存,立即生效。')
|
||||
} catch (err) {
|
||||
setStatus(`保存失败:${err && err.message ? err.message : err}`, true)
|
||||
} finally {
|
||||
saveButton.disabled = false
|
||||
}
|
||||
})
|
||||
|
||||
testButton.addEventListener('click', async () => {
|
||||
testButton.disabled = true
|
||||
try {
|
||||
await api.test(values())
|
||||
setStatus('测试通知已触发。')
|
||||
} catch (err) {
|
||||
setStatus(`测试失败:${err && err.message ? err.message : err}`, true)
|
||||
} finally {
|
||||
testButton.disabled = false
|
||||
}
|
||||
})
|
||||
|
||||
openConfigButton.addEventListener('click', () => {
|
||||
api.openConfig()
|
||||
})
|
||||
|
||||
load()
|
||||
Reference in new issue
Block a user