AppUpdateDialog güncellemesi
This commit is contained in:
parent
abece2b33e
commit
c4311c82e2
3 changed files with 169 additions and 292 deletions
|
|
@ -20418,156 +20418,6 @@
|
|||
"en": "An error occurred while generating the table script.",
|
||||
"tr": "Tablo scripti oluşturulurken hata oluştu."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.AssetProgress",
|
||||
"en": "{done}/{total} assets",
|
||||
"tr": "{done}/{total} bileşen"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.ChangeLogTitle",
|
||||
"en": "What's new in v{version}",
|
||||
"tr": "v{version} ile gelen yenilikler"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.Countdown",
|
||||
"en": "({seconds} s)",
|
||||
"tr": "({seconds} sn)"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.DurationMinutes",
|
||||
"en": "{value} min",
|
||||
"tr": "{value} dk"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.DurationSeconds",
|
||||
"en": "{value} s",
|
||||
"tr": "{value} sn"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateDescription.Activating",
|
||||
"en": "The page will refresh automatically.",
|
||||
"tr": "Sayfa otomatik olarak yenilenecek."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateDescription.Checking",
|
||||
"en": "Reading the version information from the server...",
|
||||
"tr": "Sunucudaki sürüm bilgisi okunuyor..."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateDescription.Completed",
|
||||
"en": "All assets have been downloaded, the application is ready to use.",
|
||||
"tr": "Tüm bileşenler indirildi, uygulama kullanıma hazır."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateDescription.Error",
|
||||
"en": "The switch could not be completed.",
|
||||
"tr": "Geçiş tamamlanamadı."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateDescription.Ready",
|
||||
"en": "Download complete, switching to the new version.",
|
||||
"tr": "İndirme tamamlandı, yeni sürüme geçiliyor."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.DownloadedAssets",
|
||||
"en": "Downloaded assets",
|
||||
"tr": "İndirilen bileşenler"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateMessage.ActivationSlow",
|
||||
"en": "The switch took longer than expected, retrying.",
|
||||
"tr": "Geçiş beklenenden uzun sürdü, yeniden deneniyor."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateMessage.DownloadFailed",
|
||||
"en": "The new version could not be downloaded, retrying.",
|
||||
"tr": "Yeni sürüm indirilemedi, yeniden deneniyor."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateMessage.InstallFailed",
|
||||
"en": "The new version could not be installed, retrying.",
|
||||
"tr": "Yeni sürüm kurulamadı, yeniden deneniyor."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateMessage.PartialDownload",
|
||||
"en": "The assets could not be fully downloaded, they will be loaded as needed.",
|
||||
"tr": "Bileşenler tamamen indirilemedi, gerektikçe yüklenecek."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateMessage.WaitingConnection",
|
||||
"en": "Waiting for a connection. The update will continue when the internet is back.",
|
||||
"tr": "Bağlantı bekleniyor. İnternet geldiğinde güncelleme sürdürülecek."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.Remaining",
|
||||
"en": "~{duration} remaining",
|
||||
"tr": "kalan ~{duration}"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.RetryNow",
|
||||
"en": "Retry now",
|
||||
"tr": "Şimdi tekrar dene"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.ReusedFromCache",
|
||||
"en": "{count} assets from cache",
|
||||
"tr": "{count} bileşen önbellekten"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateTitle.Activating",
|
||||
"en": "The application is restarting",
|
||||
"tr": "Uygulama yeniden başlatılıyor"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateTitle.Checking",
|
||||
"en": "Checking for updates",
|
||||
"tr": "Güncelleme kontrol ediliyor"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateTitle.Completed",
|
||||
"en": "Update completed",
|
||||
"tr": "Güncelleme tamamlandı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateTitle.Error",
|
||||
"en": "Update could not be completed",
|
||||
"tr": "Güncelleme tamamlanamadı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateTitle.Ready",
|
||||
"en": "Preparing the new version",
|
||||
"tr": "Yeni sürüm hazırlanıyor"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.Version",
|
||||
"en": "Version",
|
||||
"tr": "Sürüm"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform.Uploading",
|
||||
|
|
@ -25482,48 +25332,6 @@
|
|||
"en": "Light mode",
|
||||
"tr": "Açık tema"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Update.ContinueInBackground",
|
||||
"en": "Continue in the background",
|
||||
"tr": "Arka planda devam et"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateDescription.DownloadingBlocking",
|
||||
"en": "Assets are being downloaded, do not close the tab. The new version is applied automatically when the download finishes.",
|
||||
"tr": "Bileşenler indiriliyor, sekmeyi kapatmayın. İndirme bitince yeni sürüme otomatik geçilecek."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateDescription.DownloadingDismissible",
|
||||
"en": "All assets are being downloaded; screens will open instantly once it finishes. You can close this window and continue.",
|
||||
"tr": "Tüm bileşenler indiriliyor; bittiğinde ekranlar anında açılacak. Bu pencereyi kapatıp devam edebilirsiniz."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateFooter.Blocking",
|
||||
"en": "This window stays open until the update is completed.",
|
||||
"tr": "Güncelleme tamamlanana kadar bu pencere kapanmaz."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateFooter.Dismissible",
|
||||
"en": "The download continues in the background even if you close this window.",
|
||||
"tr": "Pencereyi kapatsanız da indirme arka planda sürer."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateTitle.DownloadingAssets",
|
||||
"en": "Downloading assets",
|
||||
"tr": "Bileşenler indiriliyor"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateTitle.DownloadingVersion",
|
||||
"en": "Downloading the new version",
|
||||
"tr": "Yeni sürüm indiriliyor"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.VideoRoom.ExitFullscreen",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useSyncExternalStore } from 'react'
|
||||
import { memo, useState, useSyncExternalStore } from 'react'
|
||||
import {
|
||||
applyAppUpdate,
|
||||
dismissAppUpdateDialog,
|
||||
|
|
@ -6,8 +6,6 @@ import {
|
|||
subscribeAppUpdate,
|
||||
type AppUpdateState,
|
||||
} from './swRegistration'
|
||||
import { getLocalization } from '@/services/localization.service'
|
||||
import { store } from '@/store'
|
||||
|
||||
/**
|
||||
* Sürüm indirme penceresi.
|
||||
|
|
@ -22,25 +20,57 @@ import { store } from '@/store'
|
|||
*/
|
||||
|
||||
/**
|
||||
* Metin çözümü.
|
||||
* Metinler bilerek statik (İngilizce) tutulur, localization kullanılmaz.
|
||||
*
|
||||
* Pencere uygulama ağacının DIŞINDA, kendi React kökünde mount edilir
|
||||
* (`mountAppUpdateDialog`); orada `StoreProvider` yoktur. Bu yüzden burada
|
||||
* `useLocalization` (dolayısıyla `useStoreState`) KULLANILAMAZ — hook Provider'ı
|
||||
* bulamayıp hata fırlatıyor, kök çöküyor ve indirme ilerlemesi hiç görünmüyordu.
|
||||
*
|
||||
* Metinler store'dan doğrudan (`getState`) okunur; bileşende statik metin
|
||||
* tutulmaz. Karşılık yoksa `useLocalization` ile aynı davranış: anahtar adı
|
||||
* döner.
|
||||
* (`mountAppUpdateDialog`) ve service worker kontrolü uygulama açılışında
|
||||
* başlar. Localization metinleri ise `application-localization` isteğiyle sonra
|
||||
* geldiği için pencere ilk boyamada anahtarları çevrilmemiş gösteriyor, metinler
|
||||
* ancak birkaç saniye sonra yerine oturuyordu. Statik metinle ilk kare doğru.
|
||||
*/
|
||||
const t = (key: string, params?: Record<string, string | number>) => {
|
||||
try {
|
||||
const { texts, config } = store.getState().abpConfig
|
||||
if (!texts) return key
|
||||
return getLocalization(texts, config?.localization?.defaultResourceName, key, params)
|
||||
} catch {
|
||||
return key
|
||||
}
|
||||
const TEXT = {
|
||||
title: {
|
||||
downloadingAssets: 'Downloading assets',
|
||||
downloadingVersion: 'Downloading the new version',
|
||||
completed: 'Update completed',
|
||||
ready: 'Preparing the new version',
|
||||
activating: 'The application is restarting',
|
||||
error: 'Update could not be completed',
|
||||
checking: 'Checking for updates',
|
||||
},
|
||||
description: {
|
||||
downloadingDismissible:
|
||||
'All assets are being downloaded; screens will open instantly once it finishes. You can close this window and continue.',
|
||||
downloadingBlocking:
|
||||
'Assets are being downloaded, do not close the tab. The new version is applied automatically when the download finishes.',
|
||||
completed: 'All assets have been downloaded, the application is ready to use.',
|
||||
ready: 'Download complete, switching to the new version.',
|
||||
activating: 'The page will refresh automatically.',
|
||||
error: 'The switch could not be completed.',
|
||||
checking: 'Reading the version information from the server...',
|
||||
},
|
||||
footer: {
|
||||
dismissible: 'The download continues in the background even if you close this window.',
|
||||
blocking: 'This window stays open until the update is completed.',
|
||||
},
|
||||
version: 'Version',
|
||||
showAssets: 'Show assets',
|
||||
hideAssets: 'Hide assets',
|
||||
whatsNew: "What's new",
|
||||
continueInBackground: 'Continue in the background',
|
||||
ok: 'OK',
|
||||
retryNow: 'Retry now',
|
||||
} as const
|
||||
|
||||
/** `swRegistration` yalnızca anahtar taşır; karşılıkları burada çözülür. */
|
||||
const UPDATE_MESSAGE: Record<string, string> = {
|
||||
'App.UpdateMessage.ActivationSlow': 'The switch took longer than expected, retrying.',
|
||||
'App.UpdateMessage.DownloadFailed': 'The new version could not be downloaded, retrying.',
|
||||
'App.UpdateMessage.InstallFailed': 'The new version could not be installed, retrying.',
|
||||
'App.UpdateMessage.PartialDownload':
|
||||
'The assets could not be fully downloaded, they will be loaded as needed.',
|
||||
'App.UpdateMessage.WaitingConnection':
|
||||
'Waiting for a connection. The update will continue when the internet is back.',
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
|
|
@ -49,58 +79,48 @@ const formatBytes = (bytes: number) => {
|
|||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
if (seconds < 60) {
|
||||
return t('::App.Update.DurationSeconds', { value: Math.max(1, seconds) })
|
||||
}
|
||||
return t('::App.Update.DurationMinutes', { value: Math.round(seconds / 60) })
|
||||
}
|
||||
const formatDuration = (seconds: number) =>
|
||||
seconds < 60 ? `${Math.max(1, seconds)} s` : `${Math.round(seconds / 60)} min`
|
||||
|
||||
const titleFor = (state: AppUpdateState) => {
|
||||
switch (state.status) {
|
||||
case 'downloading':
|
||||
return t(
|
||||
state.dismissible
|
||||
? '::App.UpdateTitle.DownloadingAssets'
|
||||
: '::App.UpdateTitle.DownloadingVersion',
|
||||
)
|
||||
return state.dismissible ? TEXT.title.downloadingAssets : TEXT.title.downloadingVersion
|
||||
case 'completed':
|
||||
return t('::App.UpdateTitle.Completed')
|
||||
return TEXT.title.completed
|
||||
case 'ready':
|
||||
return t('::App.UpdateTitle.Ready')
|
||||
return TEXT.title.ready
|
||||
case 'activating':
|
||||
return t('::App.UpdateTitle.Activating')
|
||||
return TEXT.title.activating
|
||||
case 'error':
|
||||
return t('::App.UpdateTitle.Error')
|
||||
return TEXT.title.error
|
||||
default:
|
||||
return t('::App.UpdateTitle.Checking')
|
||||
return TEXT.title.checking
|
||||
}
|
||||
}
|
||||
|
||||
const countdownText = (state: AppUpdateState) =>
|
||||
state.secondsLeft && state.secondsLeft > 0
|
||||
? ` ${t('::App.Update.Countdown', { seconds: state.secondsLeft })}`
|
||||
: ''
|
||||
state.secondsLeft && state.secondsLeft > 0 ? ` (${state.secondsLeft} s)` : ''
|
||||
|
||||
const descriptionFor = (state: AppUpdateState) => {
|
||||
switch (state.status) {
|
||||
case 'downloading':
|
||||
return t(
|
||||
state.dismissible
|
||||
? '::App.UpdateDescription.DownloadingDismissible'
|
||||
: '::App.UpdateDescription.DownloadingBlocking',
|
||||
)
|
||||
return state.dismissible
|
||||
? TEXT.description.downloadingDismissible
|
||||
: TEXT.description.downloadingBlocking
|
||||
case 'completed':
|
||||
return t('::App.UpdateDescription.Completed')
|
||||
return TEXT.description.completed
|
||||
case 'ready':
|
||||
return t('::App.UpdateDescription.Ready')
|
||||
return TEXT.description.ready
|
||||
case 'activating':
|
||||
return t('::App.UpdateDescription.Activating')
|
||||
case 'error':
|
||||
// `swRegistration` yalnızca anahtar taşır (ör. App.UpdateMessage.*).
|
||||
return `${t(state.message ? `::${state.message}` : '::App.UpdateDescription.Error')}${countdownText(state)}`
|
||||
return TEXT.description.activating
|
||||
case 'error': {
|
||||
const message =
|
||||
(state.message && UPDATE_MESSAGE[state.message]) || TEXT.description.error
|
||||
return `${message}${countdownText(state)}`
|
||||
}
|
||||
default:
|
||||
return t('::App.UpdateDescription.Checking')
|
||||
return TEXT.description.checking
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,14 +176,31 @@ const byActivity = (a: AppUpdateState['files'][number], b: AppUpdateState['files
|
|||
(b.seq ?? 0) - (a.seq ?? 0) ||
|
||||
a.index - b.index
|
||||
|
||||
/** Bağımlılık eklememek için ikon satır içi; buton dışında kullanılmaz. */
|
||||
const Chevron = ({ open }: { open: boolean }) => (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
className={`h-3.5 w-3.5 shrink-0 transition-transform ${open ? 'rotate-90' : ''}`}
|
||||
>
|
||||
<path d="M7.5 5.5 12 10l-4.5 4.5V5.5Z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const AppUpdateDialog = () => {
|
||||
const state = useSyncExternalStore(subscribeAppUpdate, getAppUpdateState, getAppUpdateState)
|
||||
// Bileşen listesi varsayılan olarak kapalı: ana ekran sürüm + ilerleme +
|
||||
// changelog'a ayrılır, ~500 satırlık liste yalnızca istendiğinde çizilir.
|
||||
const [assetsOpen, setAssetsOpen] = useState(false)
|
||||
|
||||
if (!state.visible) return null
|
||||
|
||||
const busy = state.status === 'downloading' || state.status === 'ready'
|
||||
const hasFiles = state.files.length > 0
|
||||
const doneCount = state.files.filter((file) => file.status === 'done').length
|
||||
const ordered = state.files.slice().sort(byActivity)
|
||||
// Sıralama yalnızca liste açıkken yapılır.
|
||||
const ordered = assetsOpen ? state.files.slice().sort(byActivity) : []
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -182,31 +219,22 @@ const AppUpdateDialog = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 px-6 pt-4 text-xs">
|
||||
<span className="rounded-full bg-gray-100 px-2.5 py-1 font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
{t('::App.Update.Version')}: v{state.currentVersion}
|
||||
</span>
|
||||
</div>
|
||||
{!!state.currentVersion && (
|
||||
<div className="flex flex-wrap items-center gap-2 px-6 pt-4 text-xs">
|
||||
<span className="rounded-full bg-gray-100 px-2.5 py-1 font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
{TEXT.version}: v{state.currentVersion}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(busy || state.files.length > 0) && (
|
||||
{(busy || hasFiles) && (
|
||||
<div className="px-6 pt-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>
|
||||
{t('::App.Update.AssetProgress', {
|
||||
done: doneCount,
|
||||
total: state.files.length,
|
||||
})}
|
||||
{state.reusedCount > 0 &&
|
||||
` · ${t('::App.Update.ReusedFromCache', { count: state.reusedCount })}`}
|
||||
{state.status === 'downloading' && state.secondsRemaining !== undefined && (
|
||||
<>
|
||||
{' '}
|
||||
·{' '}
|
||||
{t('::App.Update.Remaining', {
|
||||
duration: formatDuration(state.secondsRemaining),
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{`${doneCount}/${state.files.length} assets`}
|
||||
{state.reusedCount > 0 && ` · ${state.reusedCount} assets from cache`}
|
||||
{state.secondsRemaining !== undefined &&
|
||||
` · ~${formatDuration(state.secondsRemaining)} remaining`}
|
||||
</span>
|
||||
<span className="tabular-nums">
|
||||
{formatBytes(state.loadedBytes)}
|
||||
|
|
@ -222,24 +250,33 @@ const AppUpdateDialog = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{hasFiles && (
|
||||
<div className="px-6 pt-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={assetsOpen}
|
||||
className="inline-flex items-center gap-1.5 rounded-md text-xs font-semibold uppercase tracking-wide text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-200"
|
||||
onClick={() => setAssetsOpen((open) => !open)}
|
||||
>
|
||||
<Chevron open={assetsOpen} />
|
||||
{assetsOpen ? TEXT.hideAssets : TEXT.showAssets} ({state.files.length})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 min-h-0 flex-1 overflow-y-auto px-6">
|
||||
{state.files.length > 0 && (
|
||||
<>
|
||||
<p className="text-xs font-bold uppercase tracking-wide text-gray-400">
|
||||
{t('::App.Update.DownloadedAssets')}
|
||||
</p>
|
||||
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{ordered.map((file) => (
|
||||
<FileRow key={file.url} file={file} />
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
{assetsOpen && hasFiles && (
|
||||
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{ordered.map((file) => (
|
||||
<FileRow key={file.url} file={file} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{state.changeLog.length > 0 && (
|
||||
<div className="mt-4 border-t border-gray-100 pt-4 dark:border-gray-700">
|
||||
<div className={assetsOpen && hasFiles ? 'mt-4 border-t border-gray-100 pt-4 dark:border-gray-700' : ''}>
|
||||
<p className="text-xs font-bold uppercase tracking-wide text-gray-400">
|
||||
{t('::App.Update.ChangeLogTitle', { version: state.currentVersion })}
|
||||
{state.currentVersion ? `${TEXT.whatsNew} in v${state.currentVersion}` : TEXT.whatsNew}
|
||||
</p>
|
||||
<ul className="mt-2 flex flex-col gap-1.5 text-sm text-gray-600 dark:text-gray-300">
|
||||
{state.changeLog.map((line, index) => (
|
||||
|
|
@ -255,9 +292,7 @@ const AppUpdateDialog = () => {
|
|||
|
||||
<div className="flex items-center justify-between gap-3 px-6 py-4">
|
||||
<span className="text-xs text-gray-400">
|
||||
{t(
|
||||
state.dismissible ? '::App.UpdateFooter.Dismissible' : '::App.UpdateFooter.Blocking',
|
||||
)}
|
||||
{state.dismissible ? TEXT.footer.dismissible : TEXT.footer.blocking}
|
||||
</span>
|
||||
{state.dismissible ? (
|
||||
<button
|
||||
|
|
@ -265,11 +300,7 @@ const AppUpdateDialog = () => {
|
|||
className="shrink-0 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700"
|
||||
onClick={dismissAppUpdateDialog}
|
||||
>
|
||||
{t(
|
||||
state.status === 'completed'
|
||||
? '::App.Platform.OK'
|
||||
: '::App.Update.ContinueInBackground',
|
||||
)}
|
||||
{state.status === 'completed' ? TEXT.ok : TEXT.continueInBackground}
|
||||
</button>
|
||||
) : (
|
||||
state.status === 'error' && (
|
||||
|
|
@ -278,7 +309,7 @@ const AppUpdateDialog = () => {
|
|||
className="shrink-0 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700"
|
||||
onClick={applyAppUpdate}
|
||||
>
|
||||
{t('::App.Update.RetryNow')}
|
||||
{TEXT.retryNow}
|
||||
{countdownText(state)}
|
||||
</button>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ export type AppUpdateState = {
|
|||
percent: number
|
||||
/** Değişmediği için yeniden indirilmeyen dosya sayısı. */
|
||||
reusedCount: number
|
||||
/**
|
||||
* Durum mesajının kimliği (ör. `App.UpdateMessage.DownloadFailed`).
|
||||
* Localization anahtarı DEĞİLDİR; karşılığı `AppUpdateDialog` içindeki
|
||||
* `UPDATE_MESSAGE` tablosundan çözülür.
|
||||
*/
|
||||
message?: string
|
||||
/** Otomatik geçiş/yeniden deneme için kalan saniye. */
|
||||
secondsLeft?: number
|
||||
|
|
@ -187,6 +192,25 @@ function setState(patch: Partial<AppUpdateState>) {
|
|||
listeners.forEach((listener) => listener(state))
|
||||
}
|
||||
|
||||
/**
|
||||
* Kullanıcı pencereyi kapattı mı? ("arka planda devam et")
|
||||
*
|
||||
* İlerleme mesajları saniyede birkaç kez gelir ve her biri durumu tazeler;
|
||||
* bayrak olmadan `visible: true` yazılıp kapatma bir sonraki tick'te geri
|
||||
* alınıyor, pencere kendiliğinden açılıyordu. Bayrak yalnızca çalışan sürümü
|
||||
* değiştirecek bir geçiş başladığında `forceDialog` ile sıfırlanır — o akışta
|
||||
* pencere zaten kapatılamaz (`dismissible: false`).
|
||||
*/
|
||||
let dismissedByUser = false
|
||||
|
||||
/** Geçiş gerektiren akışlarda kapatma tercihi geçersizdir. */
|
||||
const forceDialog = () => {
|
||||
dismissedByUser = false
|
||||
}
|
||||
|
||||
/** İlerleme güncellemelerinde kullanılacak görünürlük. */
|
||||
const dialogVisible = () => !dismissedByUser
|
||||
|
||||
/* Kalan süre tahmini: oturum başından beri ölçülen ortalama hız. */
|
||||
let rateStartedAt = 0
|
||||
let rateStartBytes = 0
|
||||
|
|
@ -437,6 +461,8 @@ function postSkipWaiting() {
|
|||
}
|
||||
|
||||
function activateWaitingWorker() {
|
||||
// Yeni sürüme geçiliyor: kapatma tercihi burada geçersiz.
|
||||
forceDialog()
|
||||
if (activating) {
|
||||
setState({ visible: true })
|
||||
return
|
||||
|
|
@ -575,12 +601,14 @@ function handleWorkerMessage(event: MessageEvent) {
|
|||
case 'SW_PRECACHE_START':
|
||||
// İndirme başladı: bekleyen geri sayım varsa iptal edilir.
|
||||
clearAutoAction()
|
||||
// Geçişli akış kapatılamaz; önceki kapatma tercihi burada geçersizleşir.
|
||||
if (transitions) forceDialog()
|
||||
void showUpdateDialog('downloading')
|
||||
resetRate()
|
||||
setState({ percent: 0 })
|
||||
recalculate(toFiles(data.files), totals, {
|
||||
status: 'downloading',
|
||||
visible: true,
|
||||
visible: dialogVisible(),
|
||||
reusedCount: data.reusedCount ?? 0,
|
||||
secondsLeft: undefined,
|
||||
dismissible: !transitions,
|
||||
|
|
@ -591,7 +619,7 @@ function handleWorkerMessage(event: MessageEvent) {
|
|||
if (!dialogMountRequested) void showUpdateDialog('downloading')
|
||||
recalculate(mergeFiles(toFiles(data.updates)), totals, {
|
||||
status: 'downloading',
|
||||
visible: true,
|
||||
visible: dialogVisible(),
|
||||
dismissible: !transitions,
|
||||
})
|
||||
break
|
||||
|
|
@ -599,7 +627,7 @@ function handleWorkerMessage(event: MessageEvent) {
|
|||
recalculate(keepSeq(toFiles(data.files)), totals, {
|
||||
// Yeni sürüme geçilecekse 'ready'; ısıtmada iş bitti.
|
||||
status: transitions ? 'ready' : 'completed',
|
||||
visible: true,
|
||||
visible: transitions || dialogVisible(),
|
||||
percent: 100,
|
||||
})
|
||||
if (transitions) scheduleActivationHandoff()
|
||||
|
|
@ -610,13 +638,14 @@ function handleWorkerMessage(event: MessageEvent) {
|
|||
// Çalışan sürüm etkilenmedi; kullanıcıyı yeniden yükleme döngüsüne sokma.
|
||||
setState({
|
||||
status: 'error',
|
||||
visible: true,
|
||||
visible: dialogVisible(),
|
||||
dismissible: true,
|
||||
message: 'App.UpdateMessage.PartialDownload',
|
||||
})
|
||||
hideDialogLater()
|
||||
break
|
||||
}
|
||||
forceDialog()
|
||||
setState({
|
||||
status: 'error',
|
||||
visible: true,
|
||||
|
|
@ -630,7 +659,10 @@ function handleWorkerMessage(event: MessageEvent) {
|
|||
}
|
||||
|
||||
/** Kapatılabilir bildirimlerde pencereyi gizler (indirme arka planda sürer). */
|
||||
export const dismissAppUpdateDialog = () => setState({ visible: false })
|
||||
export const dismissAppUpdateDialog = () => {
|
||||
dismissedByUser = true
|
||||
setState({ visible: false })
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Güncelleme penceresi (lazy) */
|
||||
|
|
@ -643,7 +675,7 @@ let dialogMountRequested = false
|
|||
* olduğunda indirilir ve body'ye kendi kökünde mount edilir.
|
||||
*/
|
||||
async function showUpdateDialog(status: AppUpdateStatus) {
|
||||
setState({ status, visible: true })
|
||||
setState({ status, visible: dialogVisible() })
|
||||
if (dialogMountRequested) return
|
||||
dialogMountRequested = true
|
||||
|
||||
|
|
@ -752,6 +784,8 @@ function trackInstallingWorker(reg: ServiceWorkerRegistration) {
|
|||
if (!isUpdate) return
|
||||
|
||||
watchedWorkers.add(worker)
|
||||
// Güncelleme kurulumu: geçiş kaçınılmaz, kapatma tercihi geçersiz.
|
||||
forceDialog()
|
||||
void showUpdateDialog('downloading')
|
||||
|
||||
worker.addEventListener('statechange', () => {
|
||||
|
|
@ -763,6 +797,7 @@ function trackInstallingWorker(reg: ServiceWorkerRegistration) {
|
|||
// sürüm çalışmaya devam eder ve kullanıcı kilitli ekranda kalmaz.
|
||||
if (worker.state === 'redundant' && !activating) {
|
||||
console.warn('Yeni service worker kurulamadı, yeniden denenecek.')
|
||||
forceDialog()
|
||||
setState({
|
||||
status: 'error',
|
||||
visible: true,
|
||||
|
|
@ -903,6 +938,9 @@ export const registerServiceWorker = () => {
|
|||
|
||||
/** Kullanıcının menüden tetiklediği manuel kontrol. */
|
||||
export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => {
|
||||
// Kullanıcı kontrolü kendisi istedi; önceki "arka planda devam et" tercihi
|
||||
// bu akışta geçerli değil.
|
||||
forceDialog()
|
||||
setState({ status: state.status === 'idle' ? 'checking' : state.status })
|
||||
|
||||
if (!('serviceWorker' in navigator)) return 'unsupported'
|
||||
|
|
|
|||
Loading…
Reference in a new issue