feat: Implement foundational UI styling, shared components, and web platform structure.
All checks were successful
Build and Release / build-and-release (push) Successful in 14m40s

This commit is contained in:
Bryan1029384756
2026-02-19 16:03:54 -06:00
parent 65fa715fdf
commit 0e1b7f868a
4 changed files with 153 additions and 12 deletions

View File

@@ -44,4 +44,53 @@ const webPlatform = {
}, },
}; };
// Detect Android/Capacitor and enable native APK updates
if (window.Capacitor?.isNativePlatform?.()) {
const YAML_URL = 'https://gitea.moyettes.com/Moyettes/DiscordClone/releases/download/latest/latest-android.yml';
const APK_URL = 'https://gitea.moyettes.com/Moyettes/DiscordClone/releases/download/latest/app-release.apk';
const AppUpdater = window.Capacitor.Plugins.AppUpdater;
webPlatform.updates = {
async checkUpdate() {
try {
const response = await fetch(YAML_URL);
if (!response.ok) return { updateAvailable: false };
const yaml = await response.text();
const versionMatch = yaml.match(/^version:\s*(.+)$/m);
if (!versionMatch) return { updateAvailable: false };
const latestVersion = versionMatch[1].trim();
const { version: currentVersion } = await AppUpdater.getVersion();
const latest = latestVersion.split('.').map(Number);
const current = currentVersion.split('.').map(Number);
let updateAvailable = false;
let updateType = '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 { updateAvailable, updateType, latestVersion, currentVersion, apkUrl: APK_URL };
} catch {
return { updateAvailable: false };
}
},
async installUpdate() {
await AppUpdater.downloadAndInstall({ url: APK_URL });
},
onDownloadProgress(callback) {
AppUpdater.addListener('downloadProgress', callback);
},
};
webPlatform.features.hasNativeUpdates = true;
}
export default webPlatform; export default webPlatform;

View File

@@ -35,9 +35,31 @@ export function UpdateProvider({ children }) {
} }
function ForcedUpdateModal({ latestVersion }) { function ForcedUpdateModal({ latestVersion }) {
const { links } = usePlatform(); const { links, updates } = usePlatform();
const handleDownload = () => { const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState(null);
useEffect(() => {
if (!downloading || !updates?.onDownloadProgress) return;
updates.onDownloadProgress(({ percent }) => {
setProgress(percent);
});
}, [downloading]);
const handleDownload = async () => {
if (updates?.installUpdate) {
setDownloading(true);
setError(null);
try {
await updates.installUpdate();
} catch (e) {
setError('Download failed. Please try again.');
setDownloading(false);
}
} else {
links.openExternal(RELEASE_URL); links.openExternal(RELEASE_URL);
}
}; };
return ( return (
@@ -47,30 +69,65 @@ function ForcedUpdateModal({ latestVersion }) {
<p> <p>
A new version (v{latestVersion}) is available. This update is required to continue using the app. A new version (v{latestVersion}) is available. This update is required to continue using the app.
</p> </p>
{downloading ? (
<div className="update-progress">
<div className="update-progress-bar-bg">
<div
className="update-progress-bar-fill"
style={{ width: `${progress}%` }}
/>
</div>
<span className="update-progress-text">Downloading... {progress}%</span>
</div>
) : (
<button className="forced-update-btn" onClick={handleDownload}> <button className="forced-update-btn" onClick={handleDownload}>
Download Update Download Update
</button> </button>
)}
{error && <p className="update-error-text">{error}</p>}
</div> </div>
</div> </div>
); );
} }
export function TitleBarUpdateIcon() { export function TitleBarUpdateIcon() {
const { links } = usePlatform(); const { links, updates } = usePlatform();
const update = useUpdateCheck(); const update = useUpdateCheck();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
useEffect(() => {
if (!downloading || !updates?.onDownloadProgress) return;
updates.onDownloadProgress(({ percent }) => {
setProgress(percent);
});
}, [downloading]);
if (!update) return null; if (!update) return null;
const handleClick = () => { const handleClick = async () => {
if (updates?.installUpdate && !downloading) {
setDownloading(true);
try {
await updates.installUpdate();
} catch {
setDownloading(false);
}
} else if (!downloading) {
links.openExternal(RELEASE_URL); links.openExternal(RELEASE_URL);
}
}; };
const label = downloading
? `Downloading update... ${progress}%`
: `Update available: v${update.latestVersion}`;
return ( return (
<button <button
className="titlebar-btn" className="titlebar-btn"
onClick={handleClick} onClick={handleClick}
aria-label={`Update available: v${update.latestVersion}`} aria-label={label}
title={`Update available: v${update.latestVersion}`} title={label}
style={{ borderRight: '1px solid var(--app-frame-border)' }} style={{ borderRight: '1px solid var(--app-frame-border)' }}
> >
<div style={{ marginRight: '12px' }}> <div style={{ marginRight: '12px' }}>
@@ -79,4 +136,3 @@ export function TitleBarUpdateIcon() {
</button> </button>
); );
} }

View File

@@ -3173,6 +3173,40 @@ body {
background-color: var(--brand-experiment-hover); background-color: var(--brand-experiment-hover);
} }
.update-progress {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
}
.update-progress-bar-bg {
width: 100%;
height: 8px;
background-color: var(--bg-tertiary);
border-radius: 4px;
overflow: hidden;
}
.update-progress-bar-fill {
height: 100%;
background-color: var(--brand-experiment);
border-radius: 4px;
transition: width 0.2s ease;
}
.update-progress-text {
color: var(--header-secondary);
font-size: 13px;
}
.update-error-text {
color: var(--text-danger, #ed4245);
font-size: 13px;
margin: 8px 0 0;
}
/* ============================================ /* ============================================
VOICE USER ITEM (sidebar) VOICE USER ITEM (sidebar)
============================================ */ ============================================ */

View File

@@ -55,6 +55,8 @@
/** /**
* @typedef {Object} PlatformUpdates * @typedef {Object} PlatformUpdates
* @property {() => Promise<object>} checkUpdate * @property {() => Promise<object>} checkUpdate
* @property {() => Promise<void>} [installUpdate] - Download and install APK (Android only)
* @property {(callback: function) => void} [onDownloadProgress] - Listen for download progress events (Android only)
*/ */
/** /**