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) => {
|
ipcMain.handle('open-external', async (event, url) => {
|
||||||
await shell.openExternal(url);
|
await shell.openExternal(url);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ contextBridge.exposeInMainWorld('appSettings', {
|
|||||||
set: (key, value) => ipcRenderer.invoke('set-setting', key, value),
|
set: (key, value) => ipcRenderer.invoke('set-setting', key, value),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('updateAPI', {
|
||||||
|
checkFlatpakUpdate: () => ipcRenderer.invoke('check-flatpak-update'),
|
||||||
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('sessionPersistence', {
|
contextBridge.exposeInMainWorld('sessionPersistence', {
|
||||||
save: (data) => ipcRenderer.invoke('save-session', data),
|
save: (data) => ipcRenderer.invoke('save-session', data),
|
||||||
load: () => ipcRenderer.invoke('load-session'),
|
load: () => ipcRenderer.invoke('load-session'),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { TitleBarUpdateIcon } from './UpdateBanner';
|
||||||
|
|
||||||
const TitleBar = () => {
|
const TitleBar = () => {
|
||||||
return (
|
return (
|
||||||
@@ -26,6 +27,7 @@ const TitleBar = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="titlebar-title">Discord Clone</div>
|
<div className="titlebar-title">Discord Clone</div>
|
||||||
<div className="titlebar-buttons">
|
<div className="titlebar-buttons">
|
||||||
|
<TitleBarUpdateIcon />
|
||||||
<button
|
<button
|
||||||
className="titlebar-btn titlebar-minimize"
|
className="titlebar-btn titlebar-minimize"
|
||||||
onClick={() => window.windowControls?.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;
|
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 {
|
.drag-overlay-category {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-secondary);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import './index.css';
|
|||||||
|
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import { VoiceProvider } from './contexts/VoiceContext';
|
import { VoiceProvider } from './contexts/VoiceContext';
|
||||||
|
import { UpdateProvider } from './components/UpdateBanner';
|
||||||
import TitleBar from './components/TitleBar';
|
import TitleBar from './components/TitleBar';
|
||||||
|
|
||||||
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
|
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(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
|
<UpdateProvider>
|
||||||
<ConvexProvider client={convex}>
|
<ConvexProvider client={convex}>
|
||||||
<VoiceProvider>
|
<VoiceProvider>
|
||||||
<TitleBar />
|
<TitleBar />
|
||||||
@@ -23,6 +25,7 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
|||||||
</HashRouter>
|
</HashRouter>
|
||||||
</VoiceProvider>
|
</VoiceProvider>
|
||||||
</ConvexProvider>
|
</ConvexProvider>
|
||||||
|
</UpdateProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user