|
|
@@ -5,21 +5,44 @@ type UpdateStatus = 'idle' | 'checking' | 'update-available' | 'update-downloade
|
|
|
interface SettingsModalProps {
|
|
|
isOpen: boolean
|
|
|
onClose: () => void
|
|
|
- currentUserId: number | null
|
|
|
- currentUserName: string
|
|
|
- onLogout: () => void
|
|
|
updateStatus?: UpdateStatus
|
|
|
updateError?: string
|
|
|
updateDownloadProgress?: number | null
|
|
|
onCheckForUpdates?: () => void
|
|
|
}
|
|
|
|
|
|
+const sectionStyle: React.CSSProperties = {
|
|
|
+ padding: '18px 0',
|
|
|
+ borderBottom: '1px solid #edf0f3'
|
|
|
+}
|
|
|
+
|
|
|
+const labelStyle: React.CSSProperties = {
|
|
|
+ color: '#6b7280',
|
|
|
+ fontSize: 13,
|
|
|
+ marginBottom: 10,
|
|
|
+ fontWeight: 500
|
|
|
+}
|
|
|
+
|
|
|
+const rowStyle: React.CSSProperties = {
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ gap: 16
|
|
|
+}
|
|
|
+
|
|
|
+const switchLabelStyle: React.CSSProperties = {
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ gap: 10,
|
|
|
+ cursor: 'pointer',
|
|
|
+ userSelect: 'none',
|
|
|
+ fontSize: 14,
|
|
|
+ color: '#1f2937'
|
|
|
+}
|
|
|
+
|
|
|
export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|
|
isOpen,
|
|
|
onClose,
|
|
|
- currentUserId,
|
|
|
- currentUserName,
|
|
|
- onLogout,
|
|
|
updateStatus = 'idle',
|
|
|
updateError = '',
|
|
|
updateDownloadProgress = null,
|
|
|
@@ -29,6 +52,9 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|
|
const [openAtLogin, setOpenAtLogin] = useState(true)
|
|
|
const [isPackagedApp, setIsPackagedApp] = useState(true)
|
|
|
const [openAtLoginLoaded, setOpenAtLoginLoaded] = useState(false)
|
|
|
+ const [autoUpdateEnabled, setAutoUpdateEnabled] = useState(false)
|
|
|
+ const [autoUpdateIsDev, setAutoUpdateIsDev] = useState(false)
|
|
|
+ const [autoUpdateLoaded, setAutoUpdateLoaded] = useState(false)
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (!isOpen) return
|
|
|
@@ -40,8 +66,8 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|
|
let cancelled = false
|
|
|
ipc
|
|
|
.invoke('get-app-version')
|
|
|
- .then((v: unknown) => {
|
|
|
- if (!cancelled && typeof v === 'string') setAppVersion(v)
|
|
|
+ .then((version: unknown) => {
|
|
|
+ if (!cancelled && typeof version === 'string') setAppVersion(version)
|
|
|
})
|
|
|
.catch(() => {
|
|
|
if (!cancelled) setAppVersion(null)
|
|
|
@@ -62,12 +88,12 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|
|
setOpenAtLoginLoaded(false)
|
|
|
ipc
|
|
|
.invoke('get-open-at-login')
|
|
|
- .then((r: unknown) => {
|
|
|
+ .then((result: 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)
|
|
|
+ if (result && typeof result === 'object' && 'openAtLogin' in result) {
|
|
|
+ const payload = result as { openAtLogin: unknown; isPackaged?: unknown }
|
|
|
+ if (typeof payload.openAtLogin === 'boolean') setOpenAtLogin(payload.openAtLogin)
|
|
|
+ setIsPackagedApp(payload.isPackaged !== false)
|
|
|
}
|
|
|
setOpenAtLoginLoaded(true)
|
|
|
})
|
|
|
@@ -83,25 +109,61 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|
|
}
|
|
|
}, [isOpen])
|
|
|
|
|
|
- if (!isOpen) return null
|
|
|
+ useEffect(() => {
|
|
|
+ if (!isOpen) return
|
|
|
+ const ipc = typeof window !== 'undefined' && window.electron?.ipcRenderer
|
|
|
+ if (!ipc) {
|
|
|
+ setAutoUpdateLoaded(false)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let cancelled = false
|
|
|
+ setAutoUpdateLoaded(false)
|
|
|
+ ipc
|
|
|
+ .invoke('get-auto-update-enabled')
|
|
|
+ .then((result: unknown) => {
|
|
|
+ if (cancelled) return
|
|
|
+ if (result && typeof result === 'object' && 'enabled' in result) {
|
|
|
+ const payload = result as { enabled: unknown; isDev?: unknown }
|
|
|
+ setAutoUpdateEnabled(payload.enabled === true)
|
|
|
+ setAutoUpdateIsDev(payload.isDev === true)
|
|
|
+ } else {
|
|
|
+ setAutoUpdateEnabled(false)
|
|
|
+ setAutoUpdateIsDev(false)
|
|
|
+ }
|
|
|
+ setAutoUpdateLoaded(true)
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ if (!cancelled) {
|
|
|
+ setAutoUpdateEnabled(false)
|
|
|
+ setAutoUpdateIsDev(false)
|
|
|
+ setAutoUpdateLoaded(true)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return () => {
|
|
|
+ cancelled = true
|
|
|
+ }
|
|
|
+ }, [isOpen])
|
|
|
|
|
|
- const hasUpdateAPI = typeof window !== 'undefined' && window.electron?.ipcRenderer
|
|
|
+ if (!isOpen) return null
|
|
|
|
|
|
+ const hasIpc = typeof window !== 'undefined' && window.electron?.ipcRenderer
|
|
|
+ const updateBusy = updateStatus === 'checking' || updateStatus === 'update-available'
|
|
|
+ const updateDisabled = updateBusy || updateStatus === 'update-downloaded'
|
|
|
const updateStatusText =
|
|
|
updateStatus === 'checking'
|
|
|
- ? '检查中…'
|
|
|
+ ? '正在检查更新...'
|
|
|
: updateStatus === 'update-available'
|
|
|
- ? '发现新版本,正在下载…'
|
|
|
+ ? '发现新版本,正在下载...'
|
|
|
: updateStatus === 'update-downloaded'
|
|
|
- ? '新版本已下载,请重启以更新'
|
|
|
+ ? '新版本已下载,请重启以完成更新。'
|
|
|
: updateStatus === 'latest'
|
|
|
- ? '当前已是最新版本'
|
|
|
+ ? '当前已是最新版本。'
|
|
|
: updateStatus === 'error'
|
|
|
- ? updateError || '检查失败'
|
|
|
+ ? updateError || '检查更新失败。'
|
|
|
: ''
|
|
|
|
|
|
return (
|
|
|
- <div
|
|
|
+ <div
|
|
|
style={{
|
|
|
position: 'fixed',
|
|
|
top: 0,
|
|
|
@@ -113,54 +175,32 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
zIndex: 1000
|
|
|
- }}
|
|
|
+ }}
|
|
|
onClick={onClose}
|
|
|
>
|
|
|
- <div
|
|
|
+ <div
|
|
|
style={{
|
|
|
backgroundColor: '#fff',
|
|
|
- padding: '30px',
|
|
|
- borderRadius: '8px',
|
|
|
- minWidth: '400px',
|
|
|
- maxWidth: '500px',
|
|
|
- boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
|
|
|
- }}
|
|
|
- onClick={(e) => e.stopPropagation()}
|
|
|
+ padding: '28px 30px 30px',
|
|
|
+ borderRadius: 8,
|
|
|
+ width: 460,
|
|
|
+ maxWidth: 'calc(100vw - 48px)',
|
|
|
+ boxShadow: '0 16px 44px rgba(15,23,42,0.18)'
|
|
|
+ }}
|
|
|
+ onClick={(event) => event.stopPropagation()}
|
|
|
>
|
|
|
- <h2 style={{ marginTop: 0, marginBottom: '20px', fontSize: '20px', fontWeight: '500' }}>设置</h2>
|
|
|
- <div style={{ marginTop: '20px' }}>
|
|
|
- <div style={{ marginBottom: '15px', paddingBottom: '15px', borderBottom: '1px solid #f0f0f0' }}>
|
|
|
- <div style={{ color: '#999', fontSize: '12px', marginBottom: '5px' }}>用户ID</div>
|
|
|
- <div style={{ color: '#333', fontSize: '14px' }}>{currentUserId || '未知'}</div>
|
|
|
- </div>
|
|
|
- <div style={{ marginBottom: '20px', paddingBottom: '15px', borderBottom: '1px solid #f0f0f0' }}>
|
|
|
- <div style={{ color: '#999', fontSize: '12px', marginBottom: '5px' }}>用户名</div>
|
|
|
- <div style={{ color: '#333', fontSize: '14px' }}>{currentUserName || '未知'}</div>
|
|
|
- </div>
|
|
|
- <div style={{ marginBottom: '20px', paddingBottom: '15px', borderBottom: '1px solid #f0f0f0' }}>
|
|
|
- <div style={{ color: '#999', fontSize: '12px', marginBottom: '5px' }}>当前版本</div>
|
|
|
- <div style={{ color: '#333', fontSize: '14px' }}>{appVersion ?? '—'}</div>
|
|
|
- </div>
|
|
|
+ <h2 style={{ margin: 0, fontSize: 20, lineHeight: '28px', fontWeight: 600, color: '#111827' }}>设置</h2>
|
|
|
|
|
|
- {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'
|
|
|
- }}
|
|
|
- >
|
|
|
+ <div style={{ marginTop: 14 }}>
|
|
|
+ {hasIpc && openAtLoginLoaded && (
|
|
|
+ <div style={sectionStyle}>
|
|
|
+ <div style={labelStyle}>开机启动</div>
|
|
|
+ <label style={switchLabelStyle}>
|
|
|
<input
|
|
|
type="checkbox"
|
|
|
checked={openAtLogin}
|
|
|
- onChange={async (e) => {
|
|
|
- const next = e.target.checked
|
|
|
+ onChange={async (event) => {
|
|
|
+ const next = event.target.checked
|
|
|
setOpenAtLogin(next)
|
|
|
try {
|
|
|
await window.electron?.ipcRenderer?.invoke('set-open-at-login', next)
|
|
|
@@ -169,83 +209,101 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
|
|
|
}
|
|
|
}}
|
|
|
/>
|
|
|
- 开机时自动打开韫珠IM
|
|
|
+ <span>开机时自动打开韫珠IM</span>
|
|
|
</label>
|
|
|
{!isPackagedApp && (
|
|
|
- <div style={{ marginTop: '8px', fontSize: '12px', color: '#999', lineHeight: 1.5 }}>
|
|
|
- 开发环境不会写入系统启动项;本偏好仍保存,随安装包生效。
|
|
|
+ <div style={{ marginTop: 8, fontSize: 12, color: '#9ca3af', 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>
|
|
|
+ <div style={sectionStyle}>
|
|
|
+ <div style={rowStyle}>
|
|
|
+ <div>
|
|
|
+ <div style={labelStyle}>当前版本</div>
|
|
|
+ <div style={{ color: '#1f2937', fontSize: 14 }}>{appVersion ?? '-'}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {hasIpc && autoUpdateLoaded && (
|
|
|
+ <div style={sectionStyle}>
|
|
|
+ <div style={labelStyle}>版本更新设置</div>
|
|
|
+ <label style={switchLabelStyle}>
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ checked={autoUpdateEnabled}
|
|
|
+ onChange={async (event) => {
|
|
|
+ const next = event.target.checked
|
|
|
+ setAutoUpdateEnabled(next)
|
|
|
+ try {
|
|
|
+ const saved = await window.electron?.ipcRenderer?.invoke('set-auto-update-enabled', next)
|
|
|
+ if (typeof saved === 'boolean') setAutoUpdateEnabled(saved)
|
|
|
+ } catch {
|
|
|
+ setAutoUpdateEnabled(!next)
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <span>自动检查并下载更新</span>
|
|
|
+ </label>
|
|
|
+ <div style={{ marginTop: 8, fontSize: 12, color: '#9ca3af', lineHeight: 1.5 }}>
|
|
|
+ 默认关闭。关闭后不会在启动时或后台定时检查更新,仍可手动检查。
|
|
|
+ </div>
|
|
|
+ {autoUpdateIsDev && (
|
|
|
+ <div style={{ marginTop: 6, fontSize: 12, color: '#9ca3af', lineHeight: 1.5 }}>
|
|
|
+ 开发环境不会连接更新服务;该偏好会保存并随安装包生效。
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {hasIpc && onCheckForUpdates && (
|
|
|
+ <div style={{ paddingTop: 18 }}>
|
|
|
<button
|
|
|
onClick={onCheckForUpdates}
|
|
|
- disabled={updateStatus === 'checking' || updateStatus === 'update-available' || updateStatus === 'update-downloaded'}
|
|
|
+ disabled={updateDisabled}
|
|
|
style={{
|
|
|
width: '100%',
|
|
|
- padding: '10px 20px',
|
|
|
- backgroundColor: updateStatus === 'checking' || updateStatus === 'update-available' ? '#ccc' : '#1aad19',
|
|
|
+ padding: '11px 20px',
|
|
|
+ backgroundColor: updateBusy ? '#cbd5e1' : '#20b86f',
|
|
|
color: '#fff',
|
|
|
border: 'none',
|
|
|
- borderRadius: '4px',
|
|
|
- cursor: updateStatus === 'checking' || updateStatus === 'update-available' || updateStatus === 'update-downloaded' ? 'not-allowed' : 'pointer',
|
|
|
- fontSize: '14px',
|
|
|
- fontWeight: '500'
|
|
|
+ borderRadius: 6,
|
|
|
+ cursor: updateDisabled ? 'not-allowed' : 'pointer',
|
|
|
+ fontSize: 14,
|
|
|
+ fontWeight: 600
|
|
|
}}
|
|
|
>
|
|
|
- {updateStatus === 'checking' || updateStatus === 'update-available' ? '检查中…' : '检查更新'}
|
|
|
+ {updateBusy ? '检查中...' : '检查更新'}
|
|
|
</button>
|
|
|
+
|
|
|
{updateStatus === 'update-available' && updateDownloadProgress != null && (
|
|
|
- <div style={{ marginTop: '10px' }}>
|
|
|
- <div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
|
|
|
- 正在下载… {Math.round(updateDownloadProgress)}%
|
|
|
+ <div style={{ marginTop: 12 }}>
|
|
|
+ <div style={{ fontSize: 12, color: '#4b5563', marginBottom: 6 }}>
|
|
|
+ 正在下载...{Math.round(updateDownloadProgress)}%
|
|
|
</div>
|
|
|
- <div style={{ height: 6, backgroundColor: '#eee', borderRadius: 3, overflow: 'hidden' }}>
|
|
|
+ <div style={{ height: 6, backgroundColor: '#edf2f7', borderRadius: 999, overflow: 'hidden' }}>
|
|
|
<div
|
|
|
style={{
|
|
|
height: '100%',
|
|
|
width: `${Math.min(100, Math.round(updateDownloadProgress))}%`,
|
|
|
- backgroundColor: '#1aad19',
|
|
|
+ backgroundColor: '#20b86f',
|
|
|
transition: 'width 0.2s ease'
|
|
|
}}
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
+
|
|
|
{updateStatusText && (
|
|
|
- <div style={{ marginTop: '8px', fontSize: '13px', color: updateStatus === 'error' ? '#ff3b30' : '#666' }}>
|
|
|
+ <div style={{ marginTop: 10, fontSize: 13, color: updateStatus === 'error' ? '#ef4444' : '#4b5563' }}>
|
|
|
{updateStatusText}
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|
|
|
)}
|
|
|
-
|
|
|
- <button
|
|
|
- onClick={() => {
|
|
|
- onLogout()
|
|
|
- onClose()
|
|
|
- }}
|
|
|
- style={{
|
|
|
- width: '100%',
|
|
|
- padding: '12px 20px',
|
|
|
- backgroundColor: '#ff3b30',
|
|
|
- color: '#fff',
|
|
|
- border: 'none',
|
|
|
- borderRadius: '4px',
|
|
|
- cursor: 'pointer',
|
|
|
- fontSize: '14px',
|
|
|
- fontWeight: '500',
|
|
|
- transition: 'background-color 0.2s'
|
|
|
- }}
|
|
|
- onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#ff2d20'}
|
|
|
- onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#ff3b30'}
|
|
|
- >
|
|
|
- 退出登录
|
|
|
- </button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|