feat: support mobile update checks
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
@@ -27,10 +28,10 @@ interface UpdateDialogProps {
|
||||
onDownload: () => void;
|
||||
onRestart: () => void;
|
||||
/** Runtime type to show different UI for desktop vs web */
|
||||
runtimeType?: 'desktop' | 'web' | 'vscode' | null;
|
||||
runtimeType?: 'desktop' | 'web' | 'vscode' | 'mobile' | null;
|
||||
}
|
||||
|
||||
const GITHUB_RELEASES_URL = 'https://github.com/btriapitsyn/openchamber/releases';
|
||||
const GITHUB_RELEASES_URL = 'https://github.com/openchamber/openchamber/releases';
|
||||
|
||||
type ChangelogSection = {
|
||||
version: string;
|
||||
@@ -206,14 +207,16 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
const [webError, setWebError] = useState<string | null>(null);
|
||||
|
||||
const releaseUrl = info?.version
|
||||
? `${GITHUB_RELEASES_URL}/tag/v${info.version}`
|
||||
? (info.releaseUrl || `${GITHUB_RELEASES_URL}/tag/v${info.version}`)
|
||||
: GITHUB_RELEASES_URL;
|
||||
const mobileUpdateUrl = info?.downloadUrl || releaseUrl;
|
||||
|
||||
const progressPercent = progress?.total
|
||||
? Math.round((progress.downloaded / progress.total) * 100)
|
||||
: 0;
|
||||
|
||||
const isWebRuntime = runtimeType === 'web';
|
||||
const isMobileRuntime = runtimeType === 'mobile';
|
||||
const updateCommand = info?.updateCommand || 'openchamber update';
|
||||
|
||||
// Reset state when dialog closes
|
||||
@@ -264,6 +267,10 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
}
|
||||
}, [info?.currentVersion, t]);
|
||||
|
||||
const handleMobileUpdate = useCallback(() => {
|
||||
void handleOpenExternal(mobileUpdateUrl);
|
||||
}, [handleOpenExternal, mobileUpdateUrl]);
|
||||
|
||||
const isWebUpdating = webUpdateState !== 'idle' && webUpdateState !== 'error';
|
||||
|
||||
const changelog = useMemo<ParsedChangelog | null>(() => {
|
||||
@@ -437,7 +444,7 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
)}
|
||||
|
||||
{/* Desktop progress bar */}
|
||||
{!isWebRuntime && downloading && (
|
||||
{!isWebRuntime && !isMobileRuntime && downloading && (
|
||||
<div className="space-y-2 mt-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">{t('updateDialog.status.downloadingPayload')}</span>
|
||||
@@ -474,7 +481,7 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
|
||||
<div className="flex-1 flex justify-end">
|
||||
{/* Desktop Buttons */}
|
||||
{!isWebRuntime && !downloaded && !downloading && (
|
||||
{!isWebRuntime && !isMobileRuntime && !downloaded && !downloading && (
|
||||
<button
|
||||
onClick={onDownload}
|
||||
className="flex items-center justify-center gap-2 px-5 py-2 rounded-md text-sm font-medium bg-[var(--primary-base)] text-[var(--primary-foreground)] hover:opacity-90 transition-opacity"
|
||||
@@ -484,7 +491,7 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isWebRuntime && downloading && (
|
||||
{!isWebRuntime && !isMobileRuntime && downloading && (
|
||||
<button
|
||||
disabled
|
||||
className="flex items-center justify-center gap-2 px-5 py-2 rounded-md text-sm font-medium bg-[var(--primary-base)]/50 text-[var(--primary-foreground)] cursor-not-allowed"
|
||||
@@ -494,7 +501,7 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isWebRuntime && downloaded && (
|
||||
{!isWebRuntime && !isMobileRuntime && downloaded && (
|
||||
<button
|
||||
onClick={onRestart}
|
||||
className="flex items-center justify-center gap-2 px-5 py-2 rounded-md text-sm font-medium bg-[var(--status-success)] text-white hover:opacity-90 transition-opacity"
|
||||
@@ -505,6 +512,16 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
)}
|
||||
|
||||
{/* Web Buttons */}
|
||||
{isMobileRuntime && (
|
||||
<Button
|
||||
onClick={handleMobileUpdate}
|
||||
size="default"
|
||||
>
|
||||
<Icon name="external-link" className="h-4 w-4" />
|
||||
{t('updateDialog.actions.openMobileUpdate')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isWebRuntime && !isWebUpdating && (
|
||||
<button
|
||||
onClick={handleWebUpdate}
|
||||
|
||||
@@ -17,6 +17,8 @@ export type UpdateInfo = {
|
||||
currentVersion: string;
|
||||
body?: string;
|
||||
date?: string;
|
||||
releaseUrl?: string;
|
||||
downloadUrl?: string;
|
||||
nextSuggestedCheckInSec?: number;
|
||||
// Web-specific fields
|
||||
packageManager?: string;
|
||||
|
||||
@@ -2594,6 +2594,7 @@ export const dict = {
|
||||
'updateDialog.status.downloading': 'Downloading...',
|
||||
'updateDialog.actions.restartToUpdate': 'Restart to Update',
|
||||
'updateDialog.actions.updateNow': 'Update Now',
|
||||
'updateDialog.actions.openMobileUpdate': 'Open update',
|
||||
'updateDialog.status.updating': 'Updating...',
|
||||
'updateDialog.error.updateFailed': 'Update failed',
|
||||
'updateDialog.error.takingLonger': 'Update is taking longer than expected. Wait a bit and refresh, or run: openchamber update',
|
||||
|
||||
@@ -2560,6 +2560,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"updateDialog.status.downloading": "Descargando...",
|
||||
"updateDialog.actions.restartToUpdate": "Reiniciar para actualizar",
|
||||
"updateDialog.actions.updateNow": "Actualizar ahora",
|
||||
"updateDialog.actions.openMobileUpdate": "Abrir actualización",
|
||||
"updateDialog.status.updating": "Actualizando...",
|
||||
"updateDialog.error.updateFailed": "No se pudo actualizar",
|
||||
"updateDialog.error.takingLonger": "La actualización está tardando más de lo esperado. Espera un poco y refresca, o ejecuta: openchamber update",
|
||||
|
||||
@@ -2364,6 +2364,7 @@ export const dict = {
|
||||
'updateDialog.status.downloading': 'Téléchargement...',
|
||||
'updateDialog.actions.restartToUpdate': 'Redémarrer pour mettre à jour',
|
||||
'updateDialog.actions.updateNow': 'Mettre à jour maintenant',
|
||||
'updateDialog.actions.openMobileUpdate': 'Ouvrir la mise à jour',
|
||||
'updateDialog.changelog.title': 'Nouveautés',
|
||||
'updateDialog.status.updating': 'Mise à jour...',
|
||||
'updateDialog.error.updateFailed': 'La mise à jour a échoué',
|
||||
|
||||
@@ -2590,6 +2590,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'updateDialog.status.downloading': 'ダウンロード中...',
|
||||
'updateDialog.actions.restartToUpdate': '再起動して更新',
|
||||
'updateDialog.actions.updateNow': '今すぐ更新',
|
||||
'updateDialog.actions.openMobileUpdate': '更新を開く',
|
||||
'updateDialog.status.updating': '更新中...',
|
||||
'updateDialog.error.updateFailed': '更新に失敗しました',
|
||||
'updateDialog.error.takingLonger': '更新に予想以上に時間がかかっています。しばらく待ってから更新するか、次を実行: openchamber update',
|
||||
|
||||
@@ -2594,6 +2594,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'updateDialog.status.downloading': '다운로드 중…',
|
||||
'updateDialog.actions.restartToUpdate': '업데이트를 위해 재시작',
|
||||
'updateDialog.actions.updateNow': '지금 업데이트',
|
||||
'updateDialog.actions.openMobileUpdate': '업데이트 열기',
|
||||
'updateDialog.status.updating': '업데이트 중…',
|
||||
'updateDialog.error.updateFailed': '업데이트 실패',
|
||||
'updateDialog.error.takingLonger': '업데이트가 예상보다 오래 걸립니다. 잠시 기다린 뒤 새로고침하거나 `openchamber update`를 실행하세요.',
|
||||
|
||||
@@ -2562,6 +2562,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'updateDialog.actions.copied': 'Skopiowano!',
|
||||
'updateDialog.actions.copyCommand': 'Kopiuj polecenie',
|
||||
'updateDialog.actions.downloadUpdate': 'Pobierz aktualizację',
|
||||
'updateDialog.actions.openMobileUpdate': 'Otwórz aktualizację',
|
||||
'updateDialog.actions.restartToUpdate': 'Uruchom ponownie, aby zaktualizować',
|
||||
'updateDialog.actions.updateNow': 'Aktualizuj teraz',
|
||||
'updateDialog.error.takingLonger': 'Aktualizacja trwa dłużej niż oczekiwano. Poczekaj chwilę i odśwież albo uruchom: openchamber update',
|
||||
|
||||
@@ -2560,6 +2560,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"updateDialog.status.downloading": "Baixando...",
|
||||
"updateDialog.actions.restartToUpdate": "Reiniciar para atualizar",
|
||||
"updateDialog.actions.updateNow": "Atualizar agora",
|
||||
"updateDialog.actions.openMobileUpdate": "Abrir atualização",
|
||||
"updateDialog.status.updating": "Atualizando...",
|
||||
"updateDialog.error.updateFailed": "Não foi possível atualizar",
|
||||
"updateDialog.error.takingLonger": "A atualização está demorando mais do que o esperado. Aguarde um pouco e atualize, ou execute: openchamber update",
|
||||
|
||||
@@ -2560,6 +2560,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"updateDialog.status.downloading": "Завантаження...",
|
||||
"updateDialog.actions.restartToUpdate": "Перезапустити, щоб оновити",
|
||||
"updateDialog.actions.updateNow": "Оновити зараз",
|
||||
"updateDialog.actions.openMobileUpdate": "Відкрити оновлення",
|
||||
"updateDialog.status.updating": "Оновлення...",
|
||||
"updateDialog.error.updateFailed": "Помилка оновлення",
|
||||
"updateDialog.error.takingLonger": "Оновлення триває довше, ніж очікувалося. Зачекайте трохи та оновіть або запустіть: openchamber update",
|
||||
|
||||
@@ -2560,6 +2560,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'updateDialog.status.downloading': '下载中...',
|
||||
'updateDialog.actions.restartToUpdate': '重启以更新',
|
||||
'updateDialog.actions.updateNow': '立即更新',
|
||||
'updateDialog.actions.openMobileUpdate': '打开更新',
|
||||
'updateDialog.status.updating': '更新中...',
|
||||
'updateDialog.error.updateFailed': '更新失败',
|
||||
'updateDialog.error.takingLonger': '更新耗时超出预期。请稍等后刷新,或运行:openchamber update',
|
||||
|
||||
@@ -2557,6 +2557,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'updateDialog.status.downloading': '下載中...',
|
||||
'updateDialog.actions.restartToUpdate': '重新啟動以更新',
|
||||
'updateDialog.actions.updateNow': '立即更新',
|
||||
'updateDialog.actions.openMobileUpdate': '開啟更新',
|
||||
'updateDialog.status.updating': '更新中...',
|
||||
'updateDialog.error.updateFailed': '更新失敗',
|
||||
'updateDialog.error.takingLonger': '更新耗時超出預期。請稍等後重新整理,或執行:openchamber update',
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
isWebRuntime,
|
||||
} from '@/lib/desktop';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { getClientPlatform, isCapacitorApp } from '@/lib/platform';
|
||||
|
||||
declare const __APP_VERSION__: string | undefined;
|
||||
|
||||
type UpdateState = {
|
||||
checking: boolean;
|
||||
@@ -21,7 +24,7 @@ type UpdateState = {
|
||||
info: UpdateInfo | null;
|
||||
progress: UpdateProgress | null;
|
||||
error: string | null;
|
||||
runtimeType: 'desktop' | 'web' | 'vscode' | null;
|
||||
runtimeType: 'desktop' | 'web' | 'vscode' | 'mobile' | null;
|
||||
lastChecked: number | null;
|
||||
nextCheckInSec: number | null;
|
||||
};
|
||||
@@ -34,7 +37,7 @@ interface UpdateStore extends UpdateState {
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
type ClientRuntime = 'desktop' | 'web' | 'vscode';
|
||||
type ClientRuntime = 'desktop' | 'web' | 'vscode' | 'mobile';
|
||||
|
||||
function detectDeviceClass(): 'mobile' | 'tablet' | 'desktop' | 'unknown' {
|
||||
if (typeof window === 'undefined') return 'unknown';
|
||||
@@ -64,7 +67,9 @@ function detectArch(): 'arm64' | 'x64' | 'unknown' {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function detectPlatform(): 'macos' | 'windows' | 'linux' | 'web' {
|
||||
function detectPlatform(): 'macos' | 'windows' | 'linux' | 'web' | 'android' | 'ios' {
|
||||
const clientPlatform = getClientPlatform();
|
||||
if (clientPlatform === 'android' || clientPlatform === 'ios') return clientPlatform;
|
||||
if (typeof navigator === 'undefined') return 'web';
|
||||
const platform = (navigator.platform || '').toLowerCase();
|
||||
if (platform.includes('mac')) return 'macos';
|
||||
@@ -93,6 +98,12 @@ function mapRuntimeParams(runtime: ClientRuntime): URLSearchParams {
|
||||
return params;
|
||||
}
|
||||
|
||||
if (runtime === 'mobile') {
|
||||
params.set('appType', 'mobile-capacitor');
|
||||
params.set('instanceMode', 'remote');
|
||||
return params;
|
||||
}
|
||||
|
||||
params.set('appType', 'web');
|
||||
params.set('instanceMode', 'unknown');
|
||||
return params;
|
||||
@@ -121,6 +132,8 @@ async function checkForWebUpdates(runtime: ClientRuntime, currentVersion?: strin
|
||||
version: data.version,
|
||||
currentVersion: data.currentVersion ?? 'unknown',
|
||||
body: data.body,
|
||||
releaseUrl: data.releaseUrl,
|
||||
downloadUrl: data.downloadUrl,
|
||||
nextSuggestedCheckInSec:
|
||||
typeof data.nextSuggestedCheckInSec === 'number' && Number.isFinite(data.nextSuggestedCheckInSec)
|
||||
? data.nextSuggestedCheckInSec
|
||||
@@ -134,7 +147,10 @@ async function checkForWebUpdates(runtime: ClientRuntime, currentVersion?: strin
|
||||
}
|
||||
}
|
||||
|
||||
function detectRuntimeType(): 'desktop' | 'web' | 'vscode' | null {
|
||||
function detectRuntimeType(): 'desktop' | 'web' | 'vscode' | 'mobile' | null {
|
||||
if (isCapacitorApp()) {
|
||||
return 'mobile';
|
||||
}
|
||||
if (isElectronShell()) {
|
||||
return 'desktop';
|
||||
}
|
||||
@@ -186,6 +202,10 @@ export const useUpdateStore = create<UpdateStore>()((set, get) => ({
|
||||
} else if (runtime === 'vscode') {
|
||||
const vscodeInfo = await checkForWebUpdates('vscode');
|
||||
suggestedSec = vscodeInfo?.nextSuggestedCheckInSec ?? null;
|
||||
} else if (runtime === 'mobile') {
|
||||
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : undefined;
|
||||
info = await checkForWebUpdates('mobile', appVersion);
|
||||
suggestedSec = info?.nextSuggestedCheckInSec ?? null;
|
||||
}
|
||||
|
||||
set({
|
||||
|
||||
@@ -12,6 +12,7 @@ const PACKAGE_NAME = '@openchamber/web';
|
||||
const PACKAGE_PATH_SEGMENTS = PACKAGE_NAME.split('/');
|
||||
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
|
||||
const CHANGELOG_URL = 'https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md';
|
||||
const GITHUB_RELEASES_URL = 'https://github.com/openchamber/openchamber/releases';
|
||||
let cachedDetectedPm = null;
|
||||
|
||||
function getSpawnSyncBaseOptions() {
|
||||
@@ -29,7 +30,7 @@ function getOpenChamberConfigDir() {
|
||||
}
|
||||
|
||||
function sanitizeInstallScope(scope) {
|
||||
if (scope === 'desktop-electron' || scope === 'vscode' || scope === 'web') return scope;
|
||||
if (scope === 'desktop-electron' || scope === 'vscode' || scope === 'web' || scope === 'mobile-capacitor') return scope;
|
||||
return 'web';
|
||||
}
|
||||
|
||||
@@ -65,7 +66,7 @@ function mapArch(value) {
|
||||
}
|
||||
|
||||
function normalizeAppType(value) {
|
||||
if (value === 'web' || value === 'desktop-electron' || value === 'vscode') return value;
|
||||
if (value === 'web' || value === 'desktop-electron' || value === 'vscode' || value === 'mobile-capacitor') return value;
|
||||
return 'web';
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ function normalizeDeviceClass(value) {
|
||||
}
|
||||
|
||||
function normalizePlatform(value) {
|
||||
if (value === 'macos' || value === 'windows' || value === 'linux' || value === 'web') return value;
|
||||
if (value === 'macos' || value === 'windows' || value === 'linux' || value === 'web' || value === 'android' || value === 'ios') return value;
|
||||
return mapPlatform(process.platform);
|
||||
}
|
||||
|
||||
@@ -89,8 +90,9 @@ async function checkForUpdatesFromApi(currentVersion, options = {}) {
|
||||
const appType = normalizeAppType(options.appType);
|
||||
const hostPlatform = mapPlatform(process.platform);
|
||||
const hostArch = mapArch(process.arch);
|
||||
const platform = appType === 'vscode' ? normalizePlatform(options.platform) : hostPlatform;
|
||||
const arch = appType === 'vscode' ? normalizeArch(options.arch) : hostArch;
|
||||
const shouldTrustClientPlatform = appType === 'vscode' || appType === 'mobile-capacitor';
|
||||
const platform = shouldTrustClientPlatform ? normalizePlatform(options.platform) : hostPlatform;
|
||||
const arch = shouldTrustClientPlatform ? normalizeArch(options.arch) : hostArch;
|
||||
const payload = {
|
||||
appType,
|
||||
deviceClass: normalizeDeviceClass(options.deviceClass),
|
||||
@@ -120,11 +122,19 @@ async function checkForUpdatesFromApi(currentVersion, options = {}) {
|
||||
const versionComparison = compareVersions(data.latestVersion, currentVersion);
|
||||
if (versionComparison < 0) return null;
|
||||
|
||||
const releaseUrl = `${GITHUB_RELEASES_URL}/tag/v${data.latestVersion}`;
|
||||
const downloadUrl = typeof data.downloadUrl === 'string'
|
||||
? data.downloadUrl
|
||||
: typeof data.download?.url === 'string'
|
||||
? data.download.url
|
||||
: undefined;
|
||||
return {
|
||||
available: Boolean(data.updateAvailable) && versionComparison > 0,
|
||||
version: data.latestVersion,
|
||||
currentVersion,
|
||||
body: typeof data.releaseNotes === 'string' ? data.releaseNotes : undefined,
|
||||
releaseUrl: typeof data.releaseNotesUrl === 'string' ? data.releaseNotesUrl : releaseUrl,
|
||||
downloadUrl: appType === 'mobile-capacitor' ? (downloadUrl || releaseUrl) : undefined,
|
||||
nextSuggestedCheckInSec:
|
||||
typeof data.nextSuggestedCheckInSec === 'number' && Number.isFinite(data.nextSuggestedCheckInSec)
|
||||
? data.nextSuggestedCheckInSec
|
||||
@@ -721,6 +731,7 @@ export async function checkForUpdates(options = {}) {
|
||||
const currentVersion = options.currentVersion || getCurrentVersion();
|
||||
const pm = detectPackageManager();
|
||||
const appType = normalizeAppType(options.appType);
|
||||
const platform = normalizePlatform(options.platform);
|
||||
|
||||
if (currentVersion !== 'unknown') {
|
||||
const remote = await checkForUpdatesFromApi(currentVersion, options);
|
||||
@@ -760,6 +771,8 @@ export async function checkForUpdates(options = {}) {
|
||||
version: latestVersion,
|
||||
currentVersion,
|
||||
body: changelog,
|
||||
releaseUrl: `${GITHUB_RELEASES_URL}/tag/v${latestVersion}`,
|
||||
downloadUrl: appType === 'mobile-capacitor' && platform === 'android' ? `${GITHUB_RELEASES_URL}/tag/v${latestVersion}` : undefined,
|
||||
packageManager: pm,
|
||||
// Show our CLI command, not raw package manager command
|
||||
updateCommand: 'openchamber update',
|
||||
|
||||
Reference in New Issue
Block a user