diff --git a/src/DesktopApp.vue b/src/DesktopApp.vue index 4fb4bfc5..248657da 100644 --- a/src/DesktopApp.vue +++ b/src/DesktopApp.vue @@ -34,6 +34,7 @@ import { ThemeType } from '@/core/theme.ts'; import { isProduction } from '@/lib/version.ts'; import { initMapProvider } from '@/lib/map/index.ts'; import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts'; +import { updateMapCacheExpiration } from '@/lib/cache.ts'; import { getSystemTheme, setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts'; const { tt, getCurrentLanguageInfo, setLanguage, initLocale } = useI18n(); @@ -77,6 +78,16 @@ onMounted(() => { const languageInfo = getCurrentLanguageInfo(); initMapProvider(languageInfo?.alternativeLanguageTag); }); + + if ('serviceWorker' in navigator) { + navigator.serviceWorker.addEventListener('controllerchange', () => { + updateMapCacheExpiration(settingsStore.appSettings.mapCacheExpiration); + }); + + if (navigator.serviceWorker.controller) { + updateMapCacheExpiration(settingsStore.appSettings.mapCacheExpiration); + } + } }); watch(currentNotificationContent, (newValue) => { diff --git a/src/MobileApp.vue b/src/MobileApp.vue index 4aa0b7e0..0d883afa 100644 --- a/src/MobileApp.vue +++ b/src/MobileApp.vue @@ -26,6 +26,7 @@ import { isProduction } from '@/lib/version.ts'; import { getTheme, isEnableSwipeBack, isEnableAnimate } from '@/lib/settings.ts'; import { initMapProvider } from '@/lib/map/index.ts'; import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts'; +import { updateMapCacheExpiration } from '@/lib/cache.ts'; import { setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts'; import { isiOSHomeScreenMode, isModalShowing, setAppFontSize } from '@/lib/ui/mobile.ts'; @@ -180,6 +181,16 @@ onMounted(() => { const languageInfo = getCurrentLanguageInfo(); initMapProvider(languageInfo?.alternativeLanguageTag); }); + + if ('serviceWorker' in navigator) { + navigator.serviceWorker.addEventListener('controllerchange', () => { + updateMapCacheExpiration(settingsStore.appSettings.mapCacheExpiration); + }); + + if (navigator.serviceWorker.controller) { + updateMapCacheExpiration(settingsStore.appSettings.mapCacheExpiration); + } + } }); watch(currentNotificationContent, (newValue) => { diff --git a/src/consts/cache.ts b/src/consts/cache.ts index 93a22ed7..aedc5251 100644 --- a/src/consts/cache.ts +++ b/src/consts/cache.ts @@ -3,3 +3,8 @@ export const SW_RUNTIME_CACHE_NAME_PREFIX: string = 'workbox-runtime-'; export const SW_ASSETS_CACHE_NAME: string = 'ezbookkeeping-assets-cache'; export const SW_CODE_CACHE_NAME: string = 'ezbookkeeping-code-cache'; export const SW_MAP_CACHE_NAME: string = 'ezbookkeeping-map-cache'; + +export const SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG: string = 'UPDATE_MAP_CACHE_CONFIG'; +export const SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG_RESPONSE: string = 'UPDATE_MAP_CACHE_CONFIG_RESPONSE'; + +export const MAP_CACHE_MAX_ENTRIES: number = 1000; diff --git a/src/core/base.ts b/src/core/base.ts index a93173a5..854a6281 100644 --- a/src/core/base.ts +++ b/src/core/base.ts @@ -65,12 +65,17 @@ export function* values(obj: Record } } -export interface NameValue { +export interface GenericNameValue { + readonly name: string; + readonly value: T; +} + +export interface NameValue extends GenericNameValue { readonly name: string; readonly value: string; } -export interface NameNumeralValue { +export interface NameNumeralValue extends GenericNameValue { readonly name: string; readonly value: number; } diff --git a/src/core/cache.ts b/src/core/cache.ts index b6300ad9..e445b4aa 100644 --- a/src/core/cache.ts +++ b/src/core/cache.ts @@ -5,3 +5,10 @@ export interface BrowserCacheStatistics { readonly mapCacheSize: number; readonly othersCacheSize: number; } + +export interface SWMapCacheConfig { + enabled: boolean; + patterns: string[]; + maxEntries: number; + maxAgeMilliseconds: number; +} diff --git a/src/core/setting.ts b/src/core/setting.ts index 71467168..c136259b 100644 --- a/src/core/setting.ts +++ b/src/core/setting.ts @@ -62,6 +62,8 @@ export interface ApplicationSettings extends BaseApplicationSetting { hideCategoriesWithoutAccounts: boolean; // Exchange Rates Data Page currencySortByInExchangeRatesPage: number; + // Browser Cache Settings + mapCacheExpiration: number, // Statistics Settings statistics: { defaultChartDataType: number; @@ -187,6 +189,8 @@ export const DEFAULT_APPLICATION_SETTINGS: ApplicationSettings = { hideCategoriesWithoutAccounts: false, // Exchange Rates Data Page currencySortByInExchangeRatesPage: CurrencySortingType.Default.type, + // Browser Cache Settings + mapCacheExpiration: -1, // Statistics Settings statistics: { defaultChartDataType: ChartDataType.Default.type, diff --git a/src/lib/cache.ts b/src/lib/cache.ts index 23757acd..46e1bd2f 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -1,5 +1,6 @@ import type { - BrowserCacheStatistics + BrowserCacheStatistics, + SWMapCacheConfig } from '@/core/cache.ts'; import { @@ -7,10 +8,14 @@ import { SW_RUNTIME_CACHE_NAME_PREFIX, SW_ASSETS_CACHE_NAME, SW_CODE_CACHE_NAME, - SW_MAP_CACHE_NAME + SW_MAP_CACHE_NAME, + SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG, + SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG_RESPONSE, + MAP_CACHE_MAX_ENTRIES } from '@/consts/cache.ts'; import { isFunction, isObject, isNumber } from './common.ts'; +import services from './services.ts'; import logger from './logger.ts'; function findFirstCacheName(prefix: string): Promise { @@ -113,6 +118,63 @@ export function loadBrowserCacheStatistics(): Promise { }); } +export function updateMapCacheExpiration(expireSeconds: number): Promise { + const config: SWMapCacheConfig = { + enabled: expireSeconds >= 0, + patterns: services.getMapProxyTileImageAndAnnotationImageUrlPatterns(), + maxEntries: MAP_CACHE_MAX_ENTRIES, + maxAgeMilliseconds: expireSeconds * 1000 + }; + + return new Promise((resolve, reject) => { + if (!navigator.serviceWorker || !navigator.serviceWorker.controller) { + reject(new Error('Service worker is not supported or not active')); + return; + } + + const controller = navigator.serviceWorker.controller; + + navigator.serviceWorker.ready.then(() => { + const messageChannel = new MessageChannel(); + + messageChannel.port1.onmessage = (event) => { + if (event.data && event.data.type === SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG_RESPONSE) { + logger.info('Map cache config updated successfully in service worker: ' + JSON.stringify(event.data.payload)); + resolve(); + } else { + logger.error('cannot update map cache config, invalid response from service worker', event); + reject(new Error('Invalid response from service worker')); + } + }; + + controller.postMessage({ + type: SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG, + payload: config + }, [messageChannel.port2]); + }).catch(error => { + logger.error('failed to update map cache config', error); + reject(error); + }); + }); +} + +export function clearMapDataCache(): Promise { + if (!window.caches) { + logger.error('caches API is not supported in this browser'); + return Promise.reject(); + } + + return window.caches.delete(SW_MAP_CACHE_NAME).then(success => { + if (success) { + logger.info(`cache "${SW_MAP_CACHE_NAME}" cleared successfully`); + } else { + logger.warn(`failed to clear cache "${SW_MAP_CACHE_NAME}"`); + } + }).catch(error => { + logger.error(`failed to clear cache "${SW_MAP_CACHE_NAME}"`, error); + }); +} + export function clearAllBrowserCaches(): Promise { if (!window.caches) { logger.error('caches API is not supported in this browser'); @@ -142,7 +204,7 @@ export function clearAllBrowserCaches(): Promise { resolve(); }); }).catch(error => { - logger.warn("failed to clear cache", error); + logger.error("failed to clear cache", error); reject(error); }); }); diff --git a/src/lib/common.ts b/src/lib/common.ts index 297746f2..805fc31f 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -1,5 +1,11 @@ -import { keys, keysIfValueEquals, values } from '@/core/base.ts'; -import type { NameValue, TypeAndName, TypeAndDisplayName} from '@/core/base.ts'; +import { + type GenericNameValue, + type TypeAndName, + type TypeAndDisplayName, + keys, + keysIfValueEquals, + values +} from '@/core/base.ts'; // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function isFunction(val: unknown): val is Function { @@ -285,7 +291,7 @@ export function getItemByKeyValue(src: Record[] | Record(items: GenericNameValue[], value: T): string | null { for (const item of items) { if (item.value === value) { return item.name; diff --git a/src/lib/map/index.ts b/src/lib/map/index.ts index 0a1811e1..31b7b4cf 100644 --- a/src/lib/map/index.ts +++ b/src/lib/map/index.ts @@ -27,6 +27,20 @@ export function initMapProvider(language?: string): void { } } +export function isMapProviderUseExternalSDK(): boolean { + const mapProviderType = getMapProvider(); + + if (mapProviderType === 'googlemap') { + return true; + } else if (mapProviderType === 'baidumap') { + return true; + } else if (mapProviderType === 'amap') { + return true; + } else { + return false; + } +} + export function getMapWebsite(): string { return mapProvider?.getWebsite() || ''; } diff --git a/src/lib/services.ts b/src/lib/services.ts index f746fabf..5aa5a8bf 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -831,6 +831,12 @@ export default { generateQrCodeUrl: (qrCodeName: string): string => { return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`; }, + getMapProxyTileImageAndAnnotationImageUrlPatterns(): string[] { + return [ + `.*${BASE_PROXY_URL_PATH}/map/tile/[^/]+/[^/]+/[^/]+\\.png\\?provider=[^&]+.*$`, + `.*${BASE_PROXY_URL_PATH}/map/annotation/[^/]+/[^/]+/[^/]+\\.png\\?provider=[^&]+.*$` + ]; + }, generateMapProxyTileImageUrl: (mapProvider: string, language: string): string => { const token = getCurrentToken(); let url = `${getBasePath()}${BASE_PROXY_URL_PATH}/map/tile/{z}/{x}/{y}.png?provider=${mapProvider}&token=${token}`; diff --git a/src/locales/de.json b/src/locales/de.json index 85739d11..ad99e9ee 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Sind Sie sicher, dass Sie sich erneut anmelden möchten?", "Exchange Rates Data": "Wechselkursdaten", "User Custom": "User Custom", diff --git a/src/locales/en.json b/src/locales/en.json index ed6c706b..758bc120 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Are you sure you want to re-login?", "Exchange Rates Data": "Exchange Rates Data", "User Custom": "User Custom", diff --git a/src/locales/es.json b/src/locales/es.json index 2d4788e1..39549d67 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "¿Seguro que deseas volver a iniciar sesión?", "Exchange Rates Data": "Tipos de Cambio", "User Custom": "User Custom", diff --git a/src/locales/fr.json b/src/locales/fr.json index 9153aee5..dd3862b4 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Êtes-vous sûr de vouloir vous reconnecter ?", "Exchange Rates Data": "Données de taux de change", "User Custom": "Personnalisé utilisateur", diff --git a/src/locales/it.json b/src/locales/it.json index faaf780e..faa5cded 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Sei sicuro di voler accedere di nuovo?", "Exchange Rates Data": "Dati tassi di cambio", "User Custom": "User Custom", diff --git a/src/locales/ja.json b/src/locales/ja.json index 302e0705..4c46cc7a 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "再ログインしますか?", "Exchange Rates Data": "為替レートデータ", "User Custom": "User Custom", diff --git a/src/locales/kn.json b/src/locales/kn.json index 50aa1f58..7b2cbe4a 100644 --- a/src/locales/kn.json +++ b/src/locales/kn.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "ಮತ್ತೆ ಲಾಗಿನ್ ಮಾಡಲು ನೀವು ಖಚಿತವೇ?", "Exchange Rates Data": "ವಿನಿಮಯ ದರ ಡೇಟಾ", "User Custom": "ಬಳಕೆದಾರ ಕಸ್ಟಮ್", diff --git a/src/locales/ko.json b/src/locales/ko.json index 4d6d8fbc..0dc50b07 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "다시 로그인하시겠습니까?", "Exchange Rates Data": "환율 데이터", "User Custom": "사용자 정의", diff --git a/src/locales/nl.json b/src/locales/nl.json index e36ea2dc..f84798a0 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Weet je zeker dat je opnieuw wilt inloggen?", "Exchange Rates Data": "Wisselkoersgegevens", "User Custom": "Gebruiker aangepast", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index 53446e0c..ee4c60ce 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Tem certeza de que deseja fazer login novamente?", "Exchange Rates Data": "Dados de Taxas de Câmbio", "User Custom": "Personalização do Usuário", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4b881bb7..c7e09932 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Вы уверены, что хотите войти снова?", "Exchange Rates Data": "Данные о курсах валют", "User Custom": "Пользовательские настройки", diff --git a/src/locales/sl.json b/src/locales/sl.json index a3223279..770525fe 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Ali ste prepričani, da se želite ponovno prijaviti?", "Exchange Rates Data": "Podatki o menjalnih tečajih", "User Custom": "Uporabniške nastavitve po meri", diff --git a/src/locales/ta.json b/src/locales/ta.json index aee7144d..8d3f9938 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "மீண்டும் உள்நுழை செய்ய நீங்கள் உறுதியா?", "Exchange Rates Data": "மாற்று விகிதம் தரவு", "User Custom": "தனிப்பயன்", diff --git a/src/locales/th.json b/src/locales/th.json index ba822bc1..fa433f76 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "คุณแน่ใจหรือว่าต้องการเข้าสู่ระบบอีกครั้ง?", "Exchange Rates Data": "ข้อมูลอัตราแลกเปลี่ยน", "User Custom": "กำหนดเองโดยผู้ใช้", diff --git a/src/locales/tr.json b/src/locales/tr.json index a0390cd5..f980007f 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Tekrar giriş yapmak istediğinize emin misiniz?", "Exchange Rates Data": "Döviz Kuru Verileri", "User Custom": "Kullanıcı Özel", diff --git a/src/locales/uk.json b/src/locales/uk.json index bdaba4f2..f6cc7c03 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Ви впевнені, що хочете увійти знову?", "Exchange Rates Data": "Дані про курси валют", "User Custom": "User Custom", diff --git a/src/locales/vi.json b/src/locales/vi.json index 71f33922..065b6f37 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -2512,9 +2512,13 @@ "Resource Files": "Resource Files", "Map Data": "Map Data", "Others": "Others", - "Failed to load browser cache statistics": "Failed to load browser cache statistics", - "Clear File Cache": "Clear File Cache", - "Are you sure you want to clear file cache?": "Are you sure you want to clear file cache?", + "Cache Expiration Time": "Cache Expiration Time", + "Cache Expiration for Map Data": "Cache Expiration for Map Data", + "Disable Map Cache": "Disable Map Cache", + "Clear All File Cache": "Clear All File Cache", + "Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?", + "Clear Map Data Cache": "Clear Map Data Cache", + "Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?", "Are you sure you want to re-login?": "Bạn có chắc chắn muốn đăng nhập lại không?", "Exchange Rates Data": "Dữ liệu tỷ giá hối đoái", "User Custom": "User Custom", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 1bf1f10a..531240e4 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -2512,9 +2512,13 @@ "Resource Files": "资源文件", "Map Data": "地图数据", "Others": "其他", - "Failed to load browser cache statistics": "加载浏览器缓存统计数据失败", - "Clear File Cache": "清除文件缓存", - "Are you sure you want to clear file cache?": "您确定要清除文件缓存?", + "Cache Expiration Time": "缓存过期时间", + "Cache Expiration for Map Data": "地图数据缓存过期时间", + "Disable Map Cache": "禁用地图缓存", + "Clear All File Cache": "清除所有文件缓存", + "Are you sure you want to clear all file cache?": "您确定要清除所有文件缓存?", + "Clear Map Data Cache": "清除地图数据缓存", + "Are you sure you want to clear map data cache?": "您确定要清除地图数据缓存?", "Are you sure you want to re-login?": "您确定要重新登录?", "Exchange Rates Data": "汇率数据", "User Custom": "用户自定义", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 1114d239..e314f82a 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -2512,9 +2512,13 @@ "Resource Files": "資源檔案", "Map Data": "地圖資料", "Others": "其他", - "Failed to load browser cache statistics": "載入瀏覽器快取統計資料失敗", - "Clear File Cache": "清除檔案快取", - "Are you sure you want to clear file cache?": "您確定要清除檔案快取?", + "Cache Expiration Time": "快取過期時間", + "Cache Expiration for Map Data": "地圖資料快取過期時間", + "Disable Map Cache": "停用地圖快取", + "Clear All File Cache": "清除所有檔案快取", + "Are you sure you want to clear all file cache?": "您確定要清除所有檔案快取?", + "Clear Map Data Cache": "清除地圖資料快取", + "Are you sure you want to clear map data cache?": "您確定要清除地圖資料快取?", "Are you sure you want to re-login?": "您確定要重新登入?", "Exchange Rates Data": "匯率資料", "User Custom": "使用者自訂", diff --git a/src/stores/setting.ts b/src/stores/setting.ts index 7146447f..6ffe46a6 100644 --- a/src/stores/setting.ts +++ b/src/stores/setting.ts @@ -310,6 +310,12 @@ export const useSettingsStore = defineStore('settings', () => { updateUserApplicationCloudSettingValue('currencySortByInExchangeRatesPage', value); } + // Browser Cache Settings + function setMapCacheExpiration(value: number): void { + updateApplicationSettingsValue('mapCacheExpiration', value); + appSettings.value.mapCacheExpiration = value; + } + // Statistics Settings function setStatisticsDefaultChartDataType(value: number): void { updateApplicationSettingsSubValue('statistics', 'defaultChartDataType', value); @@ -531,6 +537,8 @@ export const useSettingsStore = defineStore('settings', () => { setHideCategoriesWithoutAccounts, // -- Exchange Rates Data Page setCurrencySortByInExchangeRatesPage, + // -- Browser Cache Settings + setMapCacheExpiration, // -- Statistics Settings setStatisticsDefaultChartDataType, setStatisticsDefaultTimezoneType, diff --git a/src/sw.ts b/src/sw.ts new file mode 100644 index 00000000..3a70a03a --- /dev/null +++ b/src/sw.ts @@ -0,0 +1,309 @@ +import { clientsClaim } from 'workbox-core'; +import type { + WorkboxPlugin, + CacheDidUpdateCallbackParam, + CacheKeyWillBeUsedCallbackParam, + CacheWillUpdateCallbackParam, + CachedResponseWillBeUsedCallbackParam +} from 'workbox-core/types'; +import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'; +import { registerRoute } from 'workbox-routing'; +import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'; + +interface CacheTimestampEntry { + request: Request; + time: number; +} + +class DynamicExpirationPlugin implements WorkboxPlugin { + private static readonly SW_CACHE_TIME_HEADER: string = 'ezbookkeeping-sw-cache-time'; + private maxEntries: number; + private maxAgeMilliseconds: number; + private cleaningCache: boolean = false; + + constructor(maxEntries: number, maxAgeMilliseconds: number) { + this.maxEntries = maxEntries; + this.maxAgeMilliseconds = maxAgeMilliseconds; + } + + public getMaxEntries(): number { + return this.maxEntries; + } + + public setMaxEntries(maxEntries: number): void { + this.maxEntries = maxEntries; + } + + public getMaxAgeMilliseconds(): number { + return this.maxAgeMilliseconds; + } + + public setMaxAgeMilliseconds(maxAgeMilliseconds: number): void { + this.maxAgeMilliseconds = maxAgeMilliseconds; + } + + public async cacheWillUpdate(param: CacheWillUpdateCallbackParam): Promise { + const response = param.response; + + if (!response || response.status < 200 || response.status >= 300 || response.type === 'opaque') { + return null; + } + + const body = await response.blob(); + const headers = new Headers(response.headers); + headers.set(DynamicExpirationPlugin.SW_CACHE_TIME_HEADER, Date.now().toString()); + + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers: headers + }); + } + + public async cachedResponseWillBeUsed(param: CachedResponseWillBeUsedCallbackParam): Promise { + const cachedResponse = param.cachedResponse; + + if (!cachedResponse) { + return null; + } + + const cacheTime: string | null = cachedResponse.headers.get(DynamicExpirationPlugin.SW_CACHE_TIME_HEADER); + + if (!cacheTime) { + return cachedResponse; + } + + const age: number = Date.now() - Number(cacheTime); + + if (this.maxAgeMilliseconds > 0 && age >= this.maxAgeMilliseconds) { + if (param.cacheName) { + const cache = await caches.open(param.cacheName); + await cache.delete(param.request); + } + + return null; + } + + return cachedResponse; + } + + public async cacheDidUpdate(param: CacheDidUpdateCallbackParam): Promise { + if (this.cleaningCache || !param.cacheName) { + return; + } + + this.cleaningCache = true; + + const cache: Cache = await caches.open(param.cacheName); + const requests: readonly Request[] = await cache.keys(); + + if (requests.length <= this.maxEntries) { + this.cleaningCache = false; + return; + } + + const entries: CacheTimestampEntry[] = []; + + for (const request of requests) { + const response: Response | undefined = await cache.match(request); + + if (!response) { + continue; + } + + const cacheTime: string | null = response.headers.get(DynamicExpirationPlugin.SW_CACHE_TIME_HEADER); + let time: number = cacheTime ? Number(cacheTime) : 0; + + if (Number.isFinite(time)) { + const age: number = Date.now() - time; + + if (this.maxAgeMilliseconds > 0 && age >= this.maxAgeMilliseconds) { + await cache.delete(request); + continue; + } + } else { + time = 0; + } + + entries.push({ + request: request, + time: time + }); + } + + if (entries.length <= this.maxEntries) { + this.cleaningCache = false; + return; + } + + entries.sort((a, b) => a.time - b.time); + + const removeCount: number = entries.length - this.maxEntries; + + for (let i = 0; i < removeCount; i++) { + const entry = entries[i]; + + if (entry && entry.request) { + await cache.delete(entry.request); + } + } + + this.cleaningCache = false; + } +} + +class MapDataRequestStripTokenPlugin implements WorkboxPlugin { + public async cacheKeyWillBeUsed(param: CacheKeyWillBeUsedCallbackParam): Promise { + const url = new URL(param.request.url); + + if (url.searchParams.has('token')) { + url.searchParams.delete('token'); + return new Request(url.href, param.request); + } + + return param.request; + } +} + +interface MapCacheConfig { + enabled: boolean; + patterns: RegExp[]; + mapDataRequestStripTokenPlugin: MapDataRequestStripTokenPlugin; + expirationPlugin: DynamicExpirationPlugin; +} + +declare const self: ServiceWorkerGlobalScope; + +const SW_ASSETS_CACHE_NAME: string = 'ezbookkeeping-assets-cache'; +const SW_CODE_CACHE_NAME: string = 'ezbookkeeping-code-cache'; +const SW_MAP_CACHE_NAME: string = 'ezbookkeeping-map-cache'; + +const SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG: string = 'UPDATE_MAP_CACHE_CONFIG'; +const SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG_RESPONSE: string = 'UPDATE_MAP_CACHE_CONFIG_RESPONSE'; + +const DEFAULT_MAP_CACHE_MAX_ENTRIES: number = 1000; +const DEFAULT_MAP_CACHE_MAX_AGE_MILLISECONDS: number = 30 * 24 * 60 * 60 * 1000; + +const mapCacheConfig: MapCacheConfig = { + enabled: false, + patterns: [], + mapDataRequestStripTokenPlugin: new MapDataRequestStripTokenPlugin(), + expirationPlugin: new DynamicExpirationPlugin(DEFAULT_MAP_CACHE_MAX_ENTRIES, DEFAULT_MAP_CACHE_MAX_AGE_MILLISECONDS) +}; + +self.skipWaiting(); +clientsClaim(); +precacheAndRoute(self.__WB_MANIFEST); +cleanupOutdatedCaches(); + +registerRoute( + /.*\/img\/desktop\/.*\.(png|jpg|jpeg|gif|tiff|bmp|svg)/, + new StaleWhileRevalidate({ + cacheName: SW_ASSETS_CACHE_NAME, + }) +); + +registerRoute( + /.*\/fonts\/.*\.(eot|ttf|svg|woff)/, + new CacheFirst({ + cacheName: SW_ASSETS_CACHE_NAME, + }) +); + +registerRoute( + /.*\/(mobile|mobile\/|desktop|desktop\/)$/, + new NetworkFirst({ + cacheName: SW_CODE_CACHE_NAME, + }) +); + +registerRoute( + /.*\/(mobile|mobile\/)#!\//, + new NetworkFirst({ + cacheName: SW_CODE_CACHE_NAME, + }) +); + +registerRoute( + /.*\/(desktop|desktop\/)#\//, + new NetworkFirst({ + cacheName: SW_CODE_CACHE_NAME, + }) +); + +registerRoute( + /.*\/(index\.html|mobile\.html|desktop\.html)/, + new NetworkFirst({ + cacheName: SW_CODE_CACHE_NAME, + }) +); + +registerRoute( + /.*\/css\/.*\.css/, + new CacheFirst({ + cacheName: SW_CODE_CACHE_NAME, + }) +); + +registerRoute( + /.*\/js\/.*\.js/, + new CacheFirst({ + cacheName: SW_CODE_CACHE_NAME, + }) +); + +registerRoute( + ({ url }) => { + if (!mapCacheConfig.enabled || mapCacheConfig.patterns.length < 1) { + return false; + } + + for (const pattern of mapCacheConfig.patterns) { + if (pattern.test && pattern.test(url.href)) { + return true; + } + } + + return false; + }, + new CacheFirst({ + cacheName: SW_MAP_CACHE_NAME, + plugins: [ + mapCacheConfig.mapDataRequestStripTokenPlugin, + mapCacheConfig.expirationPlugin + ] + }) +); + +self.addEventListener('message', (event: ExtendableMessageEvent) => { + try { + if (event.data && event.data.type === SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG && 'payload' in event.data) { + mapCacheConfig.enabled = !!event.data.payload['enabled']; + mapCacheConfig.patterns = []; + mapCacheConfig.expirationPlugin.setMaxEntries(event.data.payload['maxEntries'] ?? DEFAULT_MAP_CACHE_MAX_ENTRIES); + mapCacheConfig.expirationPlugin.setMaxAgeMilliseconds(event.data.payload['maxAgeMilliseconds'] ?? DEFAULT_MAP_CACHE_MAX_AGE_MILLISECONDS); + + if (event.data.payload['patterns'] && Array.isArray(event.data.payload['patterns'])) { + for (const pattern of event.data.payload['patterns']) { + if (pattern) { + mapCacheConfig.patterns.push(new RegExp(pattern as string)); + } + } + } + + if (event.ports && event.ports[0] && typeof event.ports[0].postMessage === 'function') { + event.ports[0].postMessage({ + type: SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG_RESPONSE, + payload: { + enabled: mapCacheConfig.enabled, + patterns: event.data.payload['patterns'], + maxEntries: mapCacheConfig.expirationPlugin.getMaxEntries(), + maxAgeMilliseconds: mapCacheConfig.expirationPlugin.getMaxAgeMilliseconds() + } + }); + } + } + } catch (ex) { + console.error('failed to process message in service worker', ex); + } +}); diff --git a/src/views/desktop/app/settings/tabs/AppBrowserCacheSettingTab.vue b/src/views/desktop/app/settings/tabs/AppBrowserCacheSettingTab.vue index 4095a20a..464cd519 100644 --- a/src/views/desktop/app/settings/tabs/AppBrowserCacheSettingTab.vue +++ b/src/views/desktop/app/settings/tabs/AppBrowserCacheSettingTab.vue @@ -1,12 +1,12 @@ diff --git a/src/views/mobile/settings/BrowserCacheSettingPage.vue b/src/views/mobile/settings/BrowserCacheSettingPage.vue index 51bd0b7e..2b4f8a62 100644 --- a/src/views/mobile/settings/BrowserCacheSettingPage.vue +++ b/src/views/mobile/settings/BrowserCacheSettingPage.vue @@ -1,14 +1,14 @@