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
All checks were successful
Build and Release / build-and-release (push) Successful in 14m40s
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -35,9 +35,31 @@ export function UpdateProvider({ children }) {
|
||||
}
|
||||
|
||||
function ForcedUpdateModal({ latestVersion }) {
|
||||
const { links } = usePlatform();
|
||||
const handleDownload = () => {
|
||||
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 }) {
|
||||
<p>
|
||||
A new version (v{latestVersion}) is available. This update is required to continue using the app.
|
||||
</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}>
|
||||
Download Update
|
||||
</button>
|
||||
)}
|
||||
{error && <p className="update-error-text">{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
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 (
|
||||
<button
|
||||
className="titlebar-btn"
|
||||
onClick={handleClick}
|
||||
aria-label={`Update available: v${update.latestVersion}`}
|
||||
title={`Update available: v${update.latestVersion}`}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
style={{ borderRight: '1px solid var(--app-frame-border)' }}
|
||||
>
|
||||
<div style={{ marginRight: '12px' }}>
|
||||
@@ -79,4 +136,3 @@ export function TitleBarUpdateIcon() {
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
============================================ */
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
/**
|
||||
* @typedef {Object} PlatformUpdates
|
||||
* @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)
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user