feat: Implement Electron window state persistence, update checking with splash screen and banner, and external link metadata fetching.
All checks were successful
Build and Release / build-and-release (push) Successful in 12m18s
All checks were successful
Build and Release / build-and-release (push) Successful in 12m18s
This commit is contained in:
@@ -346,6 +346,44 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Flatpak update check
|
||||
ipcMain.handle('check-flatpak-update', async () => {
|
||||
const isFlatpak = fs.existsSync('/.flatpak-info') || !!process.env.FLATPAK_ID;
|
||||
if (!isFlatpak) return { isFlatpak: false };
|
||||
|
||||
try {
|
||||
const yaml = await httpGet('https://gitea.moyettes.com/Moyettes/DiscordClone/releases/download/latest/latest-linux.yml');
|
||||
if (!yaml) return { isFlatpak: true, updateAvailable: false };
|
||||
|
||||
const versionMatch = yaml.match(/^version:\s*(.+)$/m);
|
||||
if (!versionMatch) return { isFlatpak: true, updateAvailable: false };
|
||||
|
||||
const latestVersion = versionMatch[1].trim();
|
||||
const currentVersion = app.getVersion();
|
||||
|
||||
// Semver comparison: determine if update exists and its severity
|
||||
const latest = latestVersion.split('.').map(Number);
|
||||
const current = currentVersion.split('.').map(Number);
|
||||
let updateAvailable = false;
|
||||
let updateType = 'patch'; // 'major', 'minor', or 'patch'
|
||||
for (let i = 0; i < Math.max(latest.length, current.length); i++) {
|
||||
const l = latest[i] || 0;
|
||||
const c = current[i] || 0;
|
||||
if (l > c) {
|
||||
updateAvailable = true;
|
||||
updateType = i === 0 ? 'major' : i === 1 ? 'minor' : 'patch';
|
||||
break;
|
||||
}
|
||||
if (l < c) break;
|
||||
}
|
||||
|
||||
return { isFlatpak: true, updateAvailable, updateType, latestVersion, currentVersion };
|
||||
} catch (err) {
|
||||
console.error('Flatpak update check error:', err.message);
|
||||
return { isFlatpak: true, updateAvailable: false };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('open-external', async (event, url) => {
|
||||
await shell.openExternal(url);
|
||||
});
|
||||
|
||||
@@ -32,6 +32,10 @@ contextBridge.exposeInMainWorld('appSettings', {
|
||||
set: (key, value) => ipcRenderer.invoke('set-setting', key, value),
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('updateAPI', {
|
||||
checkFlatpakUpdate: () => ipcRenderer.invoke('check-flatpak-update'),
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('sessionPersistence', {
|
||||
save: (data) => ipcRenderer.invoke('save-session', data),
|
||||
load: () => ipcRenderer.invoke('load-session'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { TitleBarUpdateIcon } from './UpdateBanner';
|
||||
|
||||
const TitleBar = () => {
|
||||
return (
|
||||
@@ -26,6 +27,7 @@ const TitleBar = () => {
|
||||
</div>
|
||||
<div className="titlebar-title">Discord Clone</div>
|
||||
<div className="titlebar-buttons">
|
||||
<TitleBarUpdateIcon />
|
||||
<button
|
||||
className="titlebar-btn titlebar-minimize"
|
||||
onClick={() => window.windowControls?.minimize()}
|
||||
|
||||
95
Frontend/Electron/src/components/UpdateBanner.jsx
Normal file
95
Frontend/Electron/src/components/UpdateBanner.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useState, useEffect, createContext, useContext } from 'react';
|
||||
import updateIcon from '../assets/icons/update.svg';
|
||||
|
||||
const RELEASE_URL = 'https://gitea.moyettes.com/Moyettes/DiscordClone/releases/tag/latest';
|
||||
|
||||
const UpdateContext = createContext(null);
|
||||
|
||||
export function useUpdateCheck() {
|
||||
return useContext(UpdateContext);
|
||||
}
|
||||
|
||||
export function UpdateProvider({ children }) {
|
||||
const [state, setState] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.updateAPI) return;
|
||||
|
||||
window.updateAPI.checkFlatpakUpdate().then((result) => {
|
||||
if (!result?.updateAvailable) return;
|
||||
setState(result);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<UpdateContext.Provider value={state}>
|
||||
{children}
|
||||
{state && (state.updateType === 'major' || state.updateType === 'minor') && (
|
||||
<ForcedUpdateModal latestVersion={state.latestVersion} />
|
||||
)}
|
||||
</UpdateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function ForcedUpdateModal({ latestVersion }) {
|
||||
const handleDownload = () => {
|
||||
window.cryptoAPI?.openExternal(RELEASE_URL);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="forced-update-overlay">
|
||||
<div className="forced-update-modal">
|
||||
<h2>Update Required</h2>
|
||||
<p>
|
||||
A new version (v{latestVersion}) is available. This update is required to continue using the app.
|
||||
</p>
|
||||
<button className="forced-update-btn" onClick={handleDownload}>
|
||||
Download Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TitleBarUpdateIcon() {
|
||||
const update = useUpdateCheck();
|
||||
|
||||
if (!update) return null;
|
||||
|
||||
const handleClick = () => {
|
||||
window.cryptoAPI?.openExternal(RELEASE_URL);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="titlebar-btn"
|
||||
onClick={handleClick}
|
||||
aria-label={`Update available: v${update.latestVersion}`}
|
||||
title={`Update available: v${update.latestVersion}`}
|
||||
style={{ borderRight: '1px solid var(--app-frame-border)' }}
|
||||
>
|
||||
<div style={{ marginRight: '12px' }}>
|
||||
<div style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<img
|
||||
src={updateIcon}
|
||||
alt=""
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
transform: 'translateX(-1000px)',
|
||||
filter: `drop-shadow(1000px 0 0 #3ba55c)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -2816,6 +2816,63 @@ body {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FORCED UPDATE MODAL (Flatpak)
|
||||
============================================ */
|
||||
.forced-update-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10002;
|
||||
}
|
||||
|
||||
.forced-update-modal {
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.forced-update-modal h2 {
|
||||
color: var(--header-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.forced-update-modal p {
|
||||
color: var(--header-secondary);
|
||||
font-size: 14px;
|
||||
margin: 0 0 24px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.forced-update-btn {
|
||||
background-color: var(--brand-experiment);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
padding: 10px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.forced-update-btn:hover {
|
||||
background-color: var(--brand-experiment-hover);
|
||||
}
|
||||
|
||||
.drag-overlay-category {
|
||||
padding: 8px 12px;
|
||||
background-color: var(--bg-secondary);
|
||||
|
||||
@@ -8,6 +8,7 @@ import './index.css';
|
||||
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { VoiceProvider } from './contexts/VoiceContext';
|
||||
import { UpdateProvider } from './components/UpdateBanner';
|
||||
import TitleBar from './components/TitleBar';
|
||||
|
||||
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
|
||||
@@ -15,6 +16,7 @@ const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<UpdateProvider>
|
||||
<ConvexProvider client={convex}>
|
||||
<VoiceProvider>
|
||||
<TitleBar />
|
||||
@@ -23,6 +25,7 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
</HashRouter>
|
||||
</VoiceProvider>
|
||||
</ConvexProvider>
|
||||
</UpdateProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user