AppUpdateDialog güncellemesi
This commit is contained in:
parent
abece2b33e
commit
853ad0240e
3 changed files with 169 additions and 292 deletions
|
|
@ -20418,156 +20418,6 @@
|
||||||
"en": "An error occurred while generating the table script.",
|
"en": "An error occurred while generating the table script.",
|
||||||
"tr": "Tablo scripti oluşturulurken hata oluştu."
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.Uploading",
|
"key": "App.Platform.Uploading",
|
||||||
|
|
@ -25482,48 +25332,6 @@
|
||||||
"en": "Light mode",
|
"en": "Light mode",
|
||||||
"tr": "Açık tema"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.VideoRoom.ExitFullscreen",
|
"key": "App.VideoRoom.ExitFullscreen",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { memo, useSyncExternalStore } from 'react'
|
import { memo, useState, useSyncExternalStore } from 'react'
|
||||||
import {
|
import {
|
||||||
applyAppUpdate,
|
applyAppUpdate,
|
||||||
dismissAppUpdateDialog,
|
dismissAppUpdateDialog,
|
||||||
|
|
@ -6,8 +6,6 @@ import {
|
||||||
subscribeAppUpdate,
|
subscribeAppUpdate,
|
||||||
type AppUpdateState,
|
type AppUpdateState,
|
||||||
} from './swRegistration'
|
} from './swRegistration'
|
||||||
import { getLocalization } from '@/services/localization.service'
|
|
||||||
import { store } from '@/store'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sürüm indirme penceresi.
|
* 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
|
* Pencere uygulama ağacının DIŞINDA, kendi React kökünde mount edilir
|
||||||
* (`mountAppUpdateDialog`); orada `StoreProvider` yoktur. Bu yüzden burada
|
* (`mountAppUpdateDialog`) ve service worker kontrolü uygulama açılışında
|
||||||
* `useLocalization` (dolayısıyla `useStoreState`) KULLANILAMAZ — hook Provider'ı
|
* başlar. Localization metinleri ise `application-localization` isteğiyle sonra
|
||||||
* bulamayıp hata fırlatıyor, kök çöküyor ve indirme ilerlemesi hiç görünmüyordu.
|
* geldiği için pencere ilk boyamada anahtarları çevrilmemiş gösteriyor, metinler
|
||||||
*
|
* ancak birkaç saniye sonra yerine oturuyordu. Statik metinle ilk kare doğru.
|
||||||
* Metinler store'dan doğrudan (`getState`) okunur; bileşende statik metin
|
|
||||||
* tutulmaz. Karşılık yoksa `useLocalization` ile aynı davranış: anahtar adı
|
|
||||||
* döner.
|
|
||||||
*/
|
*/
|
||||||
const t = (key: string, params?: Record<string, string | number>) => {
|
const TEXT = {
|
||||||
try {
|
title: {
|
||||||
const { texts, config } = store.getState().abpConfig
|
downloadingAssets: 'Downloading assets',
|
||||||
if (!texts) return key
|
downloadingVersion: 'Downloading the new version',
|
||||||
return getLocalization(texts, config?.localization?.defaultResourceName, key, params)
|
completed: 'Update completed',
|
||||||
} catch {
|
ready: 'Preparing the new version',
|
||||||
return key
|
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) => {
|
const formatBytes = (bytes: number) => {
|
||||||
|
|
@ -49,58 +79,48 @@ const formatBytes = (bytes: number) => {
|
||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDuration = (seconds: number) => {
|
const formatDuration = (seconds: number) =>
|
||||||
if (seconds < 60) {
|
seconds < 60 ? `${Math.max(1, seconds)} s` : `${Math.round(seconds / 60)} min`
|
||||||
return t('::App.Update.DurationSeconds', { value: Math.max(1, seconds) })
|
|
||||||
}
|
|
||||||
return t('::App.Update.DurationMinutes', { value: Math.round(seconds / 60) })
|
|
||||||
}
|
|
||||||
|
|
||||||
const titleFor = (state: AppUpdateState) => {
|
const titleFor = (state: AppUpdateState) => {
|
||||||
switch (state.status) {
|
switch (state.status) {
|
||||||
case 'downloading':
|
case 'downloading':
|
||||||
return t(
|
return state.dismissible ? TEXT.title.downloadingAssets : TEXT.title.downloadingVersion
|
||||||
state.dismissible
|
|
||||||
? '::App.UpdateTitle.DownloadingAssets'
|
|
||||||
: '::App.UpdateTitle.DownloadingVersion',
|
|
||||||
)
|
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return t('::App.UpdateTitle.Completed')
|
return TEXT.title.completed
|
||||||
case 'ready':
|
case 'ready':
|
||||||
return t('::App.UpdateTitle.Ready')
|
return TEXT.title.ready
|
||||||
case 'activating':
|
case 'activating':
|
||||||
return t('::App.UpdateTitle.Activating')
|
return TEXT.title.activating
|
||||||
case 'error':
|
case 'error':
|
||||||
return t('::App.UpdateTitle.Error')
|
return TEXT.title.error
|
||||||
default:
|
default:
|
||||||
return t('::App.UpdateTitle.Checking')
|
return TEXT.title.checking
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const countdownText = (state: AppUpdateState) =>
|
const countdownText = (state: AppUpdateState) =>
|
||||||
state.secondsLeft && state.secondsLeft > 0
|
state.secondsLeft && state.secondsLeft > 0 ? ` (${state.secondsLeft} s)` : ''
|
||||||
? ` ${t('::App.Update.Countdown', { seconds: state.secondsLeft })}`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
const descriptionFor = (state: AppUpdateState) => {
|
const descriptionFor = (state: AppUpdateState) => {
|
||||||
switch (state.status) {
|
switch (state.status) {
|
||||||
case 'downloading':
|
case 'downloading':
|
||||||
return t(
|
return state.dismissible
|
||||||
state.dismissible
|
? TEXT.description.downloadingDismissible
|
||||||
? '::App.UpdateDescription.DownloadingDismissible'
|
: TEXT.description.downloadingBlocking
|
||||||
: '::App.UpdateDescription.DownloadingBlocking',
|
|
||||||
)
|
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return t('::App.UpdateDescription.Completed')
|
return TEXT.description.completed
|
||||||
case 'ready':
|
case 'ready':
|
||||||
return t('::App.UpdateDescription.Ready')
|
return TEXT.description.ready
|
||||||
case 'activating':
|
case 'activating':
|
||||||
return t('::App.UpdateDescription.Activating')
|
return TEXT.description.activating
|
||||||
case 'error':
|
case 'error': {
|
||||||
// `swRegistration` yalnızca anahtar taşır (ör. App.UpdateMessage.*).
|
const message =
|
||||||
return `${t(state.message ? `::${state.message}` : '::App.UpdateDescription.Error')}${countdownText(state)}`
|
(state.message && UPDATE_MESSAGE[state.message]) || TEXT.description.error
|
||||||
|
return `${message}${countdownText(state)}`
|
||||||
|
}
|
||||||
default:
|
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) ||
|
(b.seq ?? 0) - (a.seq ?? 0) ||
|
||||||
a.index - b.index
|
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 AppUpdateDialog = () => {
|
||||||
const state = useSyncExternalStore(subscribeAppUpdate, getAppUpdateState, getAppUpdateState)
|
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
|
if (!state.visible) return null
|
||||||
|
|
||||||
const busy = state.status === 'downloading' || state.status === 'ready'
|
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 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -182,31 +219,22 @@ const AppUpdateDialog = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 px-6 pt-4 text-xs">
|
{!!state.currentVersion && (
|
||||||
<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">
|
<div className="flex flex-wrap items-center gap-2 px-6 pt-4 text-xs">
|
||||||
{t('::App.Update.Version')}: v{state.currentVersion}
|
<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">
|
||||||
</span>
|
{TEXT.version}: v{state.currentVersion}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{(busy || state.files.length > 0) && (
|
{(busy || hasFiles) && (
|
||||||
<div className="px-6 pt-4">
|
<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">
|
<div className="flex flex-wrap items-center justify-between gap-x-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
<span>
|
<span>
|
||||||
{t('::App.Update.AssetProgress', {
|
{`${doneCount}/${state.files.length} assets`}
|
||||||
done: doneCount,
|
{state.reusedCount > 0 && ` · ${state.reusedCount} assets from cache`}
|
||||||
total: state.files.length,
|
{state.secondsRemaining !== undefined &&
|
||||||
})}
|
` · ~${formatDuration(state.secondsRemaining)} remaining`}
|
||||||
{state.reusedCount > 0 &&
|
|
||||||
` · ${t('::App.Update.ReusedFromCache', { count: state.reusedCount })}`}
|
|
||||||
{state.status === 'downloading' && state.secondsRemaining !== undefined && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
·{' '}
|
|
||||||
{t('::App.Update.Remaining', {
|
|
||||||
duration: formatDuration(state.secondsRemaining),
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="tabular-nums">
|
<span className="tabular-nums">
|
||||||
{formatBytes(state.loadedBytes)}
|
{formatBytes(state.loadedBytes)}
|
||||||
|
|
@ -222,24 +250,33 @@ const AppUpdateDialog = () => {
|
||||||
</div>
|
</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">
|
<div className="mt-4 min-h-0 flex-1 overflow-y-auto px-6">
|
||||||
{state.files.length > 0 && (
|
{assetsOpen && hasFiles && (
|
||||||
<>
|
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-gray-400">
|
{ordered.map((file) => (
|
||||||
{t('::App.Update.DownloadedAssets')}
|
<FileRow key={file.url} file={file} />
|
||||||
</p>
|
))}
|
||||||
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
|
</ul>
|
||||||
{ordered.map((file) => (
|
|
||||||
<FileRow key={file.url} file={file} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state.changeLog.length > 0 && (
|
{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">
|
<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>
|
</p>
|
||||||
<ul className="mt-2 flex flex-col gap-1.5 text-sm text-gray-600 dark:text-gray-300">
|
<ul className="mt-2 flex flex-col gap-1.5 text-sm text-gray-600 dark:text-gray-300">
|
||||||
{state.changeLog.map((line, index) => (
|
{state.changeLog.map((line, index) => (
|
||||||
|
|
@ -255,9 +292,7 @@ const AppUpdateDialog = () => {
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-3 px-6 py-4">
|
<div className="flex items-center justify-between gap-3 px-6 py-4">
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">
|
||||||
{t(
|
{state.dismissible ? TEXT.footer.dismissible : TEXT.footer.blocking}
|
||||||
state.dismissible ? '::App.UpdateFooter.Dismissible' : '::App.UpdateFooter.Blocking',
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
{state.dismissible ? (
|
{state.dismissible ? (
|
||||||
<button
|
<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"
|
className="shrink-0 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700"
|
||||||
onClick={dismissAppUpdateDialog}
|
onClick={dismissAppUpdateDialog}
|
||||||
>
|
>
|
||||||
{t(
|
{state.status === 'completed' ? TEXT.ok : TEXT.continueInBackground}
|
||||||
state.status === 'completed'
|
|
||||||
? '::App.Platform.OK'
|
|
||||||
: '::App.Update.ContinueInBackground',
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
state.status === 'error' && (
|
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"
|
className="shrink-0 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700"
|
||||||
onClick={applyAppUpdate}
|
onClick={applyAppUpdate}
|
||||||
>
|
>
|
||||||
{t('::App.Update.RetryNow')}
|
{TEXT.retryNow}
|
||||||
{countdownText(state)}
|
{countdownText(state)}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,11 @@ export type AppUpdateState = {
|
||||||
percent: number
|
percent: number
|
||||||
/** Değişmediği için yeniden indirilmeyen dosya sayısı. */
|
/** Değişmediği için yeniden indirilmeyen dosya sayısı. */
|
||||||
reusedCount: number
|
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
|
message?: string
|
||||||
/** Otomatik geçiş/yeniden deneme için kalan saniye. */
|
/** Otomatik geçiş/yeniden deneme için kalan saniye. */
|
||||||
secondsLeft?: number
|
secondsLeft?: number
|
||||||
|
|
@ -187,6 +192,25 @@ function setState(patch: Partial<AppUpdateState>) {
|
||||||
listeners.forEach((listener) => listener(state))
|
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. */
|
/* Kalan süre tahmini: oturum başından beri ölçülen ortalama hız. */
|
||||||
let rateStartedAt = 0
|
let rateStartedAt = 0
|
||||||
let rateStartBytes = 0
|
let rateStartBytes = 0
|
||||||
|
|
@ -437,6 +461,8 @@ function postSkipWaiting() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateWaitingWorker() {
|
function activateWaitingWorker() {
|
||||||
|
// Yeni sürüme geçiliyor: kapatma tercihi burada geçersiz.
|
||||||
|
forceDialog()
|
||||||
if (activating) {
|
if (activating) {
|
||||||
setState({ visible: true })
|
setState({ visible: true })
|
||||||
return
|
return
|
||||||
|
|
@ -575,12 +601,14 @@ function handleWorkerMessage(event: MessageEvent) {
|
||||||
case 'SW_PRECACHE_START':
|
case 'SW_PRECACHE_START':
|
||||||
// İndirme başladı: bekleyen geri sayım varsa iptal edilir.
|
// İndirme başladı: bekleyen geri sayım varsa iptal edilir.
|
||||||
clearAutoAction()
|
clearAutoAction()
|
||||||
|
// Geçişli akış kapatılamaz; önceki kapatma tercihi burada geçersizleşir.
|
||||||
|
if (transitions) forceDialog()
|
||||||
void showUpdateDialog('downloading')
|
void showUpdateDialog('downloading')
|
||||||
resetRate()
|
resetRate()
|
||||||
setState({ percent: 0 })
|
setState({ percent: 0 })
|
||||||
recalculate(toFiles(data.files), totals, {
|
recalculate(toFiles(data.files), totals, {
|
||||||
status: 'downloading',
|
status: 'downloading',
|
||||||
visible: true,
|
visible: dialogVisible(),
|
||||||
reusedCount: data.reusedCount ?? 0,
|
reusedCount: data.reusedCount ?? 0,
|
||||||
secondsLeft: undefined,
|
secondsLeft: undefined,
|
||||||
dismissible: !transitions,
|
dismissible: !transitions,
|
||||||
|
|
@ -591,7 +619,7 @@ function handleWorkerMessage(event: MessageEvent) {
|
||||||
if (!dialogMountRequested) void showUpdateDialog('downloading')
|
if (!dialogMountRequested) void showUpdateDialog('downloading')
|
||||||
recalculate(mergeFiles(toFiles(data.updates)), totals, {
|
recalculate(mergeFiles(toFiles(data.updates)), totals, {
|
||||||
status: 'downloading',
|
status: 'downloading',
|
||||||
visible: true,
|
visible: dialogVisible(),
|
||||||
dismissible: !transitions,
|
dismissible: !transitions,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
@ -599,7 +627,7 @@ function handleWorkerMessage(event: MessageEvent) {
|
||||||
recalculate(keepSeq(toFiles(data.files)), totals, {
|
recalculate(keepSeq(toFiles(data.files)), totals, {
|
||||||
// Yeni sürüme geçilecekse 'ready'; ısıtmada iş bitti.
|
// Yeni sürüme geçilecekse 'ready'; ısıtmada iş bitti.
|
||||||
status: transitions ? 'ready' : 'completed',
|
status: transitions ? 'ready' : 'completed',
|
||||||
visible: true,
|
visible: transitions || dialogVisible(),
|
||||||
percent: 100,
|
percent: 100,
|
||||||
})
|
})
|
||||||
if (transitions) scheduleActivationHandoff()
|
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.
|
// Çalışan sürüm etkilenmedi; kullanıcıyı yeniden yükleme döngüsüne sokma.
|
||||||
setState({
|
setState({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
visible: true,
|
visible: dialogVisible(),
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
message: 'App.UpdateMessage.PartialDownload',
|
message: 'App.UpdateMessage.PartialDownload',
|
||||||
})
|
})
|
||||||
hideDialogLater()
|
hideDialogLater()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
forceDialog()
|
||||||
setState({
|
setState({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
visible: true,
|
visible: true,
|
||||||
|
|
@ -630,7 +659,10 @@ function handleWorkerMessage(event: MessageEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Kapatılabilir bildirimlerde pencereyi gizler (indirme arka planda sürer). */
|
/** 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) */
|
/* Güncelleme penceresi (lazy) */
|
||||||
|
|
@ -643,7 +675,7 @@ let dialogMountRequested = false
|
||||||
* olduğunda indirilir ve body'ye kendi kökünde mount edilir.
|
* olduğunda indirilir ve body'ye kendi kökünde mount edilir.
|
||||||
*/
|
*/
|
||||||
async function showUpdateDialog(status: AppUpdateStatus) {
|
async function showUpdateDialog(status: AppUpdateStatus) {
|
||||||
setState({ status, visible: true })
|
setState({ status, visible: dialogVisible() })
|
||||||
if (dialogMountRequested) return
|
if (dialogMountRequested) return
|
||||||
dialogMountRequested = true
|
dialogMountRequested = true
|
||||||
|
|
||||||
|
|
@ -752,6 +784,8 @@ function trackInstallingWorker(reg: ServiceWorkerRegistration) {
|
||||||
if (!isUpdate) return
|
if (!isUpdate) return
|
||||||
|
|
||||||
watchedWorkers.add(worker)
|
watchedWorkers.add(worker)
|
||||||
|
// Güncelleme kurulumu: geçiş kaçınılmaz, kapatma tercihi geçersiz.
|
||||||
|
forceDialog()
|
||||||
void showUpdateDialog('downloading')
|
void showUpdateDialog('downloading')
|
||||||
|
|
||||||
worker.addEventListener('statechange', () => {
|
worker.addEventListener('statechange', () => {
|
||||||
|
|
@ -763,6 +797,7 @@ function trackInstallingWorker(reg: ServiceWorkerRegistration) {
|
||||||
// sürüm çalışmaya devam eder ve kullanıcı kilitli ekranda kalmaz.
|
// sürüm çalışmaya devam eder ve kullanıcı kilitli ekranda kalmaz.
|
||||||
if (worker.state === 'redundant' && !activating) {
|
if (worker.state === 'redundant' && !activating) {
|
||||||
console.warn('Yeni service worker kurulamadı, yeniden denenecek.')
|
console.warn('Yeni service worker kurulamadı, yeniden denenecek.')
|
||||||
|
forceDialog()
|
||||||
setState({
|
setState({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
visible: true,
|
visible: true,
|
||||||
|
|
@ -903,6 +938,9 @@ export const registerServiceWorker = () => {
|
||||||
|
|
||||||
/** Kullanıcının menüden tetiklediği manuel kontrol. */
|
/** Kullanıcının menüden tetiklediği manuel kontrol. */
|
||||||
export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => {
|
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 })
|
setState({ status: state.status === 'idle' ? 'checking' : state.status })
|
||||||
|
|
||||||
if (!('serviceWorker' in navigator)) return 'unsupported'
|
if (!('serviceWorker' in navigator)) return 'unsupported'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue