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;
|
export default webPlatform;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|||||||
@@ -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)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user