ソースを参照

V1.0.14 开机自启

liuq 1 ヶ月 前
コミット
1d8cc8e8d2
2 ファイル変更146 行追加1 行削除
  1. 74 1
      src/main/index.ts
  2. 72 0
      src/renderer/src/components/Modals/SettingsModal.tsx

+ 74 - 1
src/main/index.ts

@@ -3,7 +3,7 @@ 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 { mkdirSync, appendFileSync, existsSync, readFileSync, writeFileSync } from 'fs'
 import { writeFile } from 'fs/promises'
 // --- 日志管理 ---
 let logDir: string = ''
@@ -61,6 +61,60 @@ function isHttpUrl(url: string): boolean {
   }
 }
 
+const STARTUP_PREFERENCE_FILE = 'startup-preference.json'
+
+interface StartupPreference {
+  openAtLogin: boolean
+}
+
+function getStartupPreferencePath(): string {
+  return join(app.getPath('userData'), STARTUP_PREFERENCE_FILE)
+}
+
+function readStartupPreference(): { openAtLogin: boolean; fileExists: boolean } {
+  const p = getStartupPreferencePath()
+  if (!existsSync(p)) {
+    return { openAtLogin: true, fileExists: false }
+  }
+  try {
+    const raw = readFileSync(p, 'utf-8')
+    const j = JSON.parse(raw) as Partial<StartupPreference>
+    return {
+      openAtLogin: typeof j.openAtLogin === 'boolean' ? j.openAtLogin : true,
+      fileExists: true
+    }
+  } catch {
+    return { openAtLogin: true, fileExists: true }
+  }
+}
+
+function writeStartupPreference(openAtLogin: boolean): void {
+  try {
+    const payload: StartupPreference = { openAtLogin }
+    writeFileSync(getStartupPreferencePath(), JSON.stringify(payload, null, 0), 'utf-8')
+  } catch (e) {
+    console.error('writeStartupPreference', e)
+  }
+}
+
+function applyOpenAtLoginToOS(openAtLogin: boolean): void {
+  if (!app.isPackaged) return
+  app.setLoginItemSettings({
+    openAtLogin,
+    path: app.getPath('exe')
+  })
+}
+
+function initStartupFromPreference(): void {
+  const { openAtLogin, fileExists } = readStartupPreference()
+  if (!fileExists) {
+    writeStartupPreference(true)
+    applyOpenAtLoginToOS(true)
+  } else {
+    applyOpenAtLoginToOS(openAtLogin)
+  }
+}
+
 function sanitizeFilename(name: string): string {
   return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').slice(0, 120) || 'file'
 }
@@ -877,6 +931,7 @@ app.on('second-instance', () => {
 
 app.whenReady().then(() => {
   electronApp.setAppUserModelId('com.hnyunzhu.im')
+  initStartupFromPreference()
   app.on('browser-window-created', (_, window) => {
     optimizer.watchWindowShortcuts(window)
   })
@@ -885,6 +940,24 @@ app.whenReady().then(() => {
 
   ipcMain.handle('get-app-version', () => app.getVersion())
 
+  ipcMain.handle('get-open-at-login', () => {
+    const isPackaged = app.isPackaged
+    if (isPackaged) {
+      return {
+        openAtLogin: app.getLoginItemSettings().openAtLogin,
+        isPackaged: true
+      }
+    }
+    return { openAtLogin: readStartupPreference().openAtLogin, isPackaged: false }
+  })
+
+  ipcMain.handle('set-open-at-login', (_, enabled: unknown) => {
+    const on = enabled === true
+    writeStartupPreference(on)
+    applyOpenAtLoginToOS(on)
+    return on
+  })
+
   // 生产环境自动更新(自建服务器)
   if (!is.dev) {
     autoUpdater.checkForUpdatesAndNotify()

+ 72 - 0
src/renderer/src/components/Modals/SettingsModal.tsx

@@ -26,6 +26,9 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
   onCheckForUpdates
 }) => {
   const [appVersion, setAppVersion] = useState<string | null>(null)
+  const [openAtLogin, setOpenAtLogin] = useState(true)
+  const [isPackagedApp, setIsPackagedApp] = useState(true)
+  const [openAtLoginLoaded, setOpenAtLoginLoaded] = useState(false)
 
   useEffect(() => {
     if (!isOpen) return
@@ -48,6 +51,38 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
     }
   }, [isOpen])
 
+  useEffect(() => {
+    if (!isOpen) return
+    const ipc = typeof window !== 'undefined' && window.electron?.ipcRenderer
+    if (!ipc) {
+      setOpenAtLoginLoaded(false)
+      return
+    }
+    let cancelled = false
+    setOpenAtLoginLoaded(false)
+    ipc
+      .invoke('get-open-at-login')
+      .then((r: unknown) => {
+        if (cancelled) return
+        if (r && typeof r === 'object' && 'openAtLogin' in r) {
+          const o = r as { openAtLogin: unknown; isPackaged?: unknown }
+          if (typeof o.openAtLogin === 'boolean') setOpenAtLogin(o.openAtLogin)
+          setIsPackagedApp(o.isPackaged !== false)
+        }
+        setOpenAtLoginLoaded(true)
+      })
+      .catch(() => {
+        if (!cancelled) {
+          setOpenAtLogin(true)
+          setIsPackagedApp(true)
+          setOpenAtLoginLoaded(true)
+        }
+      })
+    return () => {
+      cancelled = true
+    }
+  }, [isOpen])
+
   if (!isOpen) return null
 
   const hasUpdateAPI = typeof window !== 'undefined' && window.electron?.ipcRenderer
@@ -107,6 +142,43 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
             <div style={{ color: '#333', fontSize: '14px' }}>{appVersion ?? '—'}</div>
           </div>
 
+          {hasUpdateAPI && openAtLoginLoaded && (
+            <div style={{ marginBottom: '20px', paddingBottom: '15px', borderBottom: '1px solid #f0f0f0' }}>
+              <div style={{ color: '#999', fontSize: '12px', marginBottom: '8px' }}>开机自启</div>
+              <label
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  gap: 10,
+                  cursor: 'pointer',
+                  userSelect: 'none',
+                  fontSize: '14px',
+                  color: '#333'
+                }}
+              >
+                <input
+                  type="checkbox"
+                  checked={openAtLogin}
+                  onChange={async (e) => {
+                    const next = e.target.checked
+                    setOpenAtLogin(next)
+                    try {
+                      await window.electron?.ipcRenderer?.invoke('set-open-at-login', next)
+                    } catch {
+                      setOpenAtLogin(!next)
+                    }
+                  }}
+                />
+                开机时自动打开韫珠IM
+              </label>
+              {!isPackagedApp && (
+                <div style={{ marginTop: '8px', fontSize: '12px', color: '#999', lineHeight: 1.5 }}>
+                  开发环境不会写入系统启动项;本偏好仍保存,随安装包生效。
+                </div>
+              )}
+            </div>
+          )}
+
           {hasUpdateAPI && onCheckForUpdates && (
             <div style={{ marginBottom: '20px', paddingBottom: '15px', borderBottom: '1px solid #f0f0f0' }}>
               <div style={{ color: '#999', fontSize: '12px', marginBottom: '8px' }}>检查更新</div>