From 0e1b7f868a26ff6aed67c73a18e108973864c397 Mon Sep 17 00:00:00 2001 From: Bryan1029384756 <23323626+Bryan1029384756@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:03:54 -0600 Subject: [PATCH] feat: Implement foundational UI styling, shared components, and web platform structure. --- packages/platform-web/src/index.js | 49 ++++++++++++ .../shared/src/components/UpdateBanner.jsx | 80 ++++++++++++++++--- packages/shared/src/index.css | 34 ++++++++ packages/shared/src/platform/types.js | 2 + 4 files changed, 153 insertions(+), 12 deletions(-) diff --git a/packages/platform-web/src/index.js b/packages/platform-web/src/index.js index 8152220..d746465 100644 --- a/packages/platform-web/src/index.js +++ b/packages/platform-web/src/index.js @@ -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; diff --git a/packages/shared/src/components/UpdateBanner.jsx b/packages/shared/src/components/UpdateBanner.jsx index 84ad847..f9ae1ac 100644 --- a/packages/shared/src/components/UpdateBanner.jsx +++ b/packages/shared/src/components/UpdateBanner.jsx @@ -35,9 +35,31 @@ export function UpdateProvider({ children }) { } function ForcedUpdateModal({ latestVersion }) { - const { links } = usePlatform(); - const handleDownload = () => { - links.openExternal(RELEASE_URL); + const { links, updates } = usePlatform(); + 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); + } }; return ( @@ -47,30 +69,65 @@ function ForcedUpdateModal({ latestVersion }) {

A new version (v{latestVersion}) is available. This update is required to continue using the app.

- + {downloading ? ( +
+
+
+
+ Downloading... {progress}% +
+ ) : ( + + )} + {error &&

{error}

}
); } export function TitleBarUpdateIcon() { - const { links } = usePlatform(); + const { links, updates } = usePlatform(); 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; - const handleClick = () => { - links.openExternal(RELEASE_URL); + const handleClick = async () => { + if (updates?.installUpdate && !downloading) { + setDownloading(true); + try { + await updates.installUpdate(); + } catch { + setDownloading(false); + } + } else if (!downloading) { + links.openExternal(RELEASE_URL); + } }; + const label = downloading + ? `Downloading update... ${progress}%` + : `Update available: v${update.latestVersion}`; + return ( ); } - \ No newline at end of file diff --git a/packages/shared/src/index.css b/packages/shared/src/index.css index dc59796..b6dce6b 100644 --- a/packages/shared/src/index.css +++ b/packages/shared/src/index.css @@ -3173,6 +3173,40 @@ body { 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) ============================================ */ diff --git a/packages/shared/src/platform/types.js b/packages/shared/src/platform/types.js index 820abe1..5ef8b97 100644 --- a/packages/shared/src/platform/types.js +++ b/packages/shared/src/platform/types.js @@ -55,6 +55,8 @@ /** * @typedef {Object} PlatformUpdates * @property {() => Promise} checkUpdate + * @property {() => Promise} [installUpdate] - Download and install APK (Android only) + * @property {(callback: function) => void} [onDownloadProgress] - Listen for download progress events (Android only) */ /**