diff --git a/appinfo/routes.php b/appinfo/routes.php index c39b3802..abadaf30 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -84,6 +84,7 @@ return [ // Config ['name' => 'Other#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'], + ['name' => 'Other#getUserConfig', 'url' => '/api/config', 'verb' => 'GET'], // Admin ['name' => 'Admin#getSystemStatus', 'url' => '/api/system-status', 'verb' => 'GET'], diff --git a/lib/Controller/OtherController.php b/lib/Controller/OtherController.php index 0ee5f9ea..6cbfca64 100644 --- a/lib/Controller/OtherController.php +++ b/lib/Controller/OtherController.php @@ -56,6 +56,50 @@ class OtherController extends GenericApiController }); } + /** + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @PublicPage + */ + public function getUserConfig(): Http\Response + { + return Util::guardEx(function () { + $appManager = \OC::$server->get(\OCP\App\IAppManager::class); + + try { + $uid = Util::getUID(); + } catch (\Exception $e) { + $uid = null; + } + + $getAppConfig = function ($key, $default) use ($uid) { + return $this->config->getUserValue($uid, Application::APPNAME, $key, $default); + }; + + return new JSONResponse([ + "version" => $appManager->getAppInfo('memories')['version'], + "vod_disable" => Util::getSystemConfig('memories.vod.disable'), + "video_default_quality" => Util::getSystemConfig('memories.video_default_quality'), + "places_gis" => Util::getSystemConfig('memories.gis_type'), + + "systemtags_enabled" => Util::tagsIsEnabled(), + "recognize_enabled" => Util::recognizeIsEnabled(), + "albums_enabled" => Util::albumsIsEnabled(), + "facerecognition_installed" => Util::facerecognitionIsInstalled(), + "facerecognition_enabled" => Util::facerecognitionIsEnabled(), + + "timeline_path" => $getAppConfig('timelinePath', 'EMPTY'), + "folders_path" => $getAppConfig('foldersPath', '/'), + "show_hidden_folders" => $getAppConfig('showHidden', false) === "true", + "sort_folder_month" => $getAppConfig('sortFolderMonth', false) === "true", + "sort_album_month" => $getAppConfig('sortAlbumMonth', "true") === "true", + "enable_top_memories" => $getAppConfig('enableTopMemories', 'true') === "true", + ], Http::STATUS_OK); + }); + } + /** * @NoAdminRequired * diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 85c8e3ee..994b89b5 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -58,35 +58,6 @@ class PageController extends Controller OCPUtil::addScript($this->appName, 'memories-main'); $this->eventDispatcher->dispatchTyped(new LoadSidebar()); - // Configuration - $uid = $user->getUID(); - $pi = function ($key, $default) use ($uid) { - $this->initialState->provideInitialState($key, $this->config->getUserValue( - $uid, - Application::APPNAME, - $key, - $default - )); - }; - - // User configuration - $pi('timelinePath', 'EMPTY'); - $pi('foldersPath', '/'); - $pi('showHidden', false); - $pi('sortFolderMonth', false); - $pi('sortAlbumMonth', 'true'); - $pi('enableTopMemories', 'true'); - - // Apps enabled - $this->initialState->provideInitialState('systemtags', Util::tagsIsEnabled()); - $this->initialState->provideInitialState('recognize', Util::recognizeIsEnabled()); - $this->initialState->provideInitialState('facerecognitionInstalled', Util::facerecognitionIsInstalled()); - $this->initialState->provideInitialState('facerecognitionEnabled', Util::facerecognitionIsEnabled()); - $this->initialState->provideInitialState('albums', Util::albumsIsEnabled()); - - // Common state - self::provideCommonInitialState($this->initialState); - // Extra translations if (Util::recognizeIsEnabled()) { // Auto translation for tags @@ -138,22 +109,6 @@ class PageController extends Controller return $policy; } - /** Provide initial state for all pages */ - public static function provideCommonInitialState(IInitialState &$initialState) - { - $appManager = \OC::$server->get(\OCP\App\IAppManager::class); - - // App version - $initialState->provideInitialState('version', $appManager->getAppInfo('memories')['version']); - - // Video configuration - $initialState->provideInitialState('vod_disable', Util::getSystemConfig('memories.vod.disable')); - $initialState->provideInitialState('video_default_quality', Util::getSystemConfig('memories.video_default_quality')); - - // Geo configuration - $initialState->provideInitialState('places_gis', Util::getSystemConfig('memories.gis_type')); - } - /** * @NoAdminRequired * diff --git a/lib/Controller/PublicAlbumController.php b/lib/Controller/PublicAlbumController.php index c4b31970..89dc9e51 100644 --- a/lib/Controller/PublicAlbumController.php +++ b/lib/Controller/PublicAlbumController.php @@ -85,7 +85,6 @@ class PublicAlbumController extends Controller // Scripts Util::addScript($this->appName, 'memories-main'); - PageController::provideCommonInitialState($this->initialState); $response = new PublicTemplateResponse($this->appName, 'main'); $response->setHeaderTitle($album['name']); diff --git a/lib/Controller/PublicController.php b/lib/Controller/PublicController.php index b27ed854..209a36c4 100644 --- a/lib/Controller/PublicController.php +++ b/lib/Controller/PublicController.php @@ -111,7 +111,6 @@ class PublicController extends AuthPublicShareController // Scripts \OCP\Util::addScript($this->appName, 'memories-main'); - PageController::provideCommonInitialState($this->initialState); // Share info $this->initialState->provideInitialState('no_download', $share->getHideDownload()); diff --git a/src/App.vue b/src/App.vue index 0642dbc3..7edbe078 100644 --- a/src/App.vue +++ b/src/App.vue @@ -59,7 +59,7 @@ const NcAppNavigationItem = () => import('@nextcloud/vue/dist/Components/NcAppNa import { generateUrl } from '@nextcloud/router'; import { translate as t } from '@nextcloud/l10n'; -import { emit } from '@nextcloud/event-bus'; +import { emit, subscribe } from '@nextcloud/event-bus'; import * as utils from './services/Utils'; import UserConfig from './mixins/UserConfig'; @@ -137,11 +137,11 @@ export default defineComponent({ }, recognize(): string | false { - if (!this.config_recognizeEnabled) { + if (!this.config.recognize_enabled) { return false; } - if (this.config_facerecognitionInstalled) { + if (this.config.facerecognition_enabled) { return t('memories', 'People (Recognize)'); } @@ -149,11 +149,11 @@ export default defineComponent({ }, facerecognition(): string | false { - if (!this.config_facerecognitionInstalled) { + if (!this.config.facerecognition_installed) { return false; } - if (this.config_recognizeEnabled) { + if (this.config.recognize_enabled) { return t('memories', 'People (Face Recognition)'); } @@ -161,11 +161,15 @@ export default defineComponent({ }, isFirstStart(): boolean { - return this.config_timelinePath === 'EMPTY'; + return ( + this.config.timeline_path === 'EMPTY' && + this.$route.name !== 'folder-share' && + this.$route.name !== 'album-share' + ); }, showAlbums(): boolean { - return this.config_albumsEnabled; + return this.config.albums_enabled; }, removeOuterGap(): boolean { @@ -197,13 +201,14 @@ export default defineComponent({ window.addEventListener('resize', () => { utils.setRenewingTimeout(this, 'resizeTimer', onResize, 100); }); + + // Register navigation items on config change + subscribe(this.configEventName, this.refreshNav); }, mounted() { this.doRouteChecks(); - - // Populate navigation - this.navItems = this.navItemsAll().filter((item) => typeof item.if === 'undefined' || Boolean(item.if)); + this.refreshNav(); // Store CSS variables modified const root = document.documentElement; @@ -255,8 +260,8 @@ export default defineComponent({ }, methods: { - navItemsAll(): NavItem[] { - return [ + refreshNav() { + const navItems = [ { name: 'timeline', icon: ImageMultiple, @@ -309,7 +314,7 @@ export default defineComponent({ name: 'places', icon: MarkerIcon, title: t('memories', 'Places'), - if: this.config_placesGis > 0, + if: this.config.places_gis > 0, }, { name: 'map', @@ -320,9 +325,11 @@ export default defineComponent({ name: 'tags', icon: TagsIcon, title: t('memories', 'Tags'), - if: this.config_tagsEnabled, + if: this.config.systemtags_enabled, }, ]; + + this.navItems = navItems.filter((item) => typeof item.if === 'undefined' || Boolean(item.if)); }, linkClick() { diff --git a/src/components/ClusterView.vue b/src/components/ClusterView.vue index f7720877..bd6cf9c2 100644 --- a/src/components/ClusterView.vue +++ b/src/components/ClusterView.vue @@ -53,11 +53,11 @@ export default defineComponent({ }, created() { - subscribe(this.config_eventName, this.routeChange); + subscribe(this.configEventName, this.routeChange); }, beforeDestroy() { - unsubscribe(this.config_eventName, this.routeChange); + unsubscribe(this.configEventName, this.routeChange); }, watch: { @@ -74,7 +74,7 @@ export default defineComponent({ this.loading++; if (route === 'albums') { - this.items = await dav.getAlbums(3, this.config_albumListSort); + this.items = await dav.getAlbums(3, this.config.album_list_sort); } else if (route === 'tags') { this.items = await dav.getTags(); } else if (route === 'recognize' || route === 'facerecognition') { diff --git a/src/components/FirstStart.vue b/src/components/FirstStart.vue index cf518ca8..eae497a8 100644 --- a/src/components/FirstStart.vue +++ b/src/components/FirstStart.vue @@ -118,8 +118,8 @@ export default defineComponent({ async finish() { this.show = false; await new Promise((resolve) => setTimeout(resolve, 500)); - this.config_timelinePath = this.chosenPath; - await this.updateSetting('timelinePath'); + this.config.timeline_path = this.chosenPath; + await this.updateSetting('timeline_path', 'timelinePath'); }, async chooseFolder(title: string, initial: string) { diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue index cdf1a8a9..623b27f5 100644 --- a/src/components/SelectionManager.vue +++ b/src/components/SelectionManager.vue @@ -254,7 +254,7 @@ export default defineComponent({ /** Is album route */ routeIsAlbum() { - return this.config_albumsEnabled && this.$route.name === 'albums'; + return this.config.albums_enabled && this.$route.name === 'albums'; }, /** Public route that can't modify anything */ @@ -786,7 +786,7 @@ export default defineComponent({ * Move selected photos to another person */ async moveSelectionToPerson(selection: Selection) { - if (!this.config_showFaceRect) { + if (!this.config.show_face_rect) { showError(this.t('memories', 'You must enable "Mark person in preview" to use this feature')); return; } diff --git a/src/components/Settings.vue b/src/components/Settings.vue index da374359..3d88ba20 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -30,39 +30,51 @@ > - + - + {{ t('memories', 'Square grid mode') }} {{ t('memories', 'Show past photos on top of timeline') }} - + {{ t('memories', 'Load full size image on zoom') }} - + {{ t('memories', 'Always load full size image (not recommended)') }} - + - + {{ t('memories', 'Show hidden folders') }} @@ -72,7 +84,7 @@ @@ -147,57 +159,57 @@ export default defineComponent({ }, async chooseTimelinePath() { - (this.$refs.multiPathModal).open(this.config_timelinePath.split(';')); + (this.$refs.multiPathModal).open(this.config.timeline_path.split(';')); }, async saveTimelinePath(paths: string[]) { if (!paths || !paths.length) return; const newPath = paths.join(';'); - if (newPath !== this.config_timelinePath) { - this.config_timelinePath = newPath; - await this.updateSetting('timelinePath'); + if (newPath !== this.config.timeline_path) { + this.config.timeline_path = newPath; + await this.updateSetting('timeline_path', 'timelinePath'); } }, async chooseFoldersPath() { let newPath = await this.chooseFolder( this.t('memories', 'Choose the root for the folders view'), - this.config_foldersPath + this.config.folders_path ); if (newPath === '') newPath = '/'; - if (newPath !== this.config_foldersPath) { - this.config_foldersPath = newPath; - await this.updateSetting('foldersPath'); + if (newPath !== this.config.folders_path) { + this.config.folders_path = newPath; + await this.updateSetting('folders_path', 'foldersPath'); } }, async updateSquareThumbs() { - await this.updateSetting('squareThumbs'); + await this.updateSetting('square_thumbs'); }, async updateFullResOnZoom() { - await this.updateSetting('fullResOnZoom'); + await this.updateSetting('full_res_on_zoom'); }, async updateFullResAlways() { - await this.updateSetting('fullResAlways'); + await this.updateSetting('full_res_always'); }, async updateEnableTopMemories() { - await this.updateSetting('enableTopMemories'); + await this.updateSetting('enable_top_memories', 'enableTopMemories'); }, async updateShowHidden() { - await this.updateSetting('showHidden'); + await this.updateSetting('show_hidden_folders', 'showHidden'); }, async updateSortFolderMonth() { - await this.updateSetting('sortFolderMonth'); + await this.updateSetting('sort_folder_month', 'sortFolderMonth'); }, async updateSortAlbumMonth() { - await this.updateSetting('sortAlbumMonth'); + await this.updateSetting('sort_album_month', 'sortAlbumMonth'); }, }, }); diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 7feb30ea..6494efd6 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -30,8 +30,8 @@ @@ -204,16 +204,17 @@ export default defineComponent({ }, created() { - subscribe(this.config_eventName, this.softRefresh); + subscribe(this.configEventName, this.softRefresh); subscribe('files:file:created', this.softRefresh); subscribe('memories:window:resize', this.handleResizeWithDelay); }, beforeDestroy() { - unsubscribe(this.config_eventName, this.softRefresh); + unsubscribe(this.configEventName, this.softRefresh); unsubscribe('files:file:created', this.softRefresh); unsubscribe('memories:window:resize', this.handleResizeWithDelay); this.resetState(); + this.state = 0; }, computed: { @@ -234,8 +235,8 @@ export default defineComponent({ return ( this.$route.query.sort === 'album' || - (this.config_sortAlbumMonth && (this.$route.name === 'albums' || this.$route.name === 'album-share')) || - (this.config_sortFolderMonth && this.$route.name === 'folders') + (this.config.sort_album_month && (this.$route.name === 'albums' || this.$route.name === 'album-share')) || + (this.config.sort_folder_month && this.$route.name === 'folders') ); }, @@ -316,7 +317,7 @@ export default defineComponent({ }, allowBreakout() { - return globalThis.windowInnerWidth <= 600 && !this.config_squareThumbs; + return globalThis.windowInnerWidth <= 600 && !this.config.square_thumbs; }, /** Create new state */ @@ -412,7 +413,7 @@ export default defineComponent({ this.rowHeight = Math.floor(this.rowWidth / this.numCols); } else { // Desktop - if (this.config_squareThumbs) { + if (this.config.square_thumbs) { this.numCols = Math.max(3, Math.floor(this.rowWidth / DESKTOP_ROW_HEIGHT)); this.rowHeight = Math.floor(this.rowWidth / this.numCols); } else { @@ -557,7 +558,7 @@ export default defineComponent({ // Folder if (this.$route.name === 'folders') { - const path = utils.getFolderRoutePath(this.config_foldersPath); + const path = utils.getFolderRoutePath(this.config.folders_path); API.DAYS_FILTER(query, DaysFilterType.FOLDER, path); if (this.$route.query.recursive) { API.DAYS_FILTER(query, DaysFilterType.RECURSIVE); @@ -589,7 +590,7 @@ export default defineComponent({ API.DAYS_FILTER(query, filter, `${user}/${name}`); // Face rect - if (this.config_showFaceRect) { + if (this.config.show_face_rect) { API.DAYS_FILTER(query, DaysFilterType.FACE_RECT); } } @@ -639,7 +640,7 @@ export default defineComponent({ } // Get subfolders URL - const folder = utils.getFolderRoutePath(this.config_foldersPath); + const folder = utils.getFolderRoutePath(this.config.folders_path); const url = API.Q(API.FOLDERS_SUB(), { folder }); // Make API call to get subfolders @@ -654,7 +655,7 @@ export default defineComponent({ } // Filter out hidden folders - if (!this.config_showHidden) { + if (!this.config.show_hidden_folders) { this.folders = this.folders.filter((f) => !f.name.startsWith('.') && f.previews?.length); } }, @@ -730,6 +731,8 @@ export default defineComponent({ /** Process the data for days call including folders */ async processDays(data: IDay[]) { + if (!data || !this.state) return; + const list: typeof this.list = []; const heads: typeof this.heads = {}; @@ -941,7 +944,7 @@ export default defineComponent({ * @param data photos */ processDay(dayId: number, data: IPhoto[]) { - if (!data) return; + if (!data || !this.state) return; const head = this.heads[dayId]; if (!head) return; @@ -964,7 +967,7 @@ export default defineComponent({ } // Force all to square - const squareMode = this.isMobileLayout() || this.config_squareThumbs; + const squareMode = this.isMobileLayout() || this.config.square_thumbs; // Create justified layout with correct params const justify = getLayout( diff --git a/src/components/frame/Folder.vue b/src/components/frame/Folder.vue index dbb04c3a..133cdf9c 100644 --- a/src/components/frame/Folder.vue +++ b/src/components/frame/Folder.vue @@ -70,7 +70,7 @@ export default defineComponent({ .slice(2) as string[]; // Remove base path if present - const basePath = this.config_foldersPath.split('/').filter((x) => x); + const basePath = this.config.folders_path.split('/').filter((x) => x); if (path.length >= basePath.length && path.slice(0, basePath.length).every((x, i) => x === basePath[i])) { path.splice(0, basePath.length); } diff --git a/src/components/modal/MoveToFolderModal.vue b/src/components/modal/MoveToFolderModal.vue index dad5cff6..3ebd113d 100644 --- a/src/components/modal/MoveToFolderModal.vue +++ b/src/components/modal/MoveToFolderModal.vue @@ -71,7 +71,7 @@ export default defineComponent({ }, async chooseFolderPath() { - let destination = await this.chooseFolderModal(this.t('memories', 'Choose a folder'), this.config_foldersPath); + let destination = await this.chooseFolderModal(this.t('memories', 'Choose a folder'), this.config.folders_path); // Fails if the target exists, same behavior with Nextcloud files implementation. const gen = dav.movePhotos(this.photos, destination, false); this.processing = true; diff --git a/src/components/modal/ShareModal.vue b/src/components/modal/ShareModal.vue index e1f59c0f..f7df407a 100644 --- a/src/components/modal/ShareModal.vue +++ b/src/components/modal/ShareModal.vue @@ -71,12 +71,12 @@ import { defineComponent } from 'vue'; import { showError } from '@nextcloud/dialogs'; -import { loadState } from '@nextcloud/initial-state'; import axios from '@nextcloud/axios'; import NcListItem from '@nextcloud/vue/dist/Components/NcListItem'; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon'; import Modal from './Modal.vue'; +import UserConfig from '../../mixins/UserConfig'; import { IPhoto } from '../../types'; import { API } from '../../services/API'; @@ -88,9 +88,6 @@ import LargePhotoIcon from 'vue-material-design-icons/ImageArea.vue'; import LinkIcon from 'vue-material-design-icons/LinkVariant.vue'; import FileIcon from 'vue-material-design-icons/File.vue'; -// Is video transcoding enabled? -const config_vodDisable = loadState('memories', 'vod_disable', true); - export default defineComponent({ name: 'ShareModal', @@ -105,6 +102,8 @@ export default defineComponent({ FileIcon, }, + mixins: [UserConfig], + data: () => { return { photo: null as IPhoto | null, @@ -128,7 +127,7 @@ export default defineComponent({ }, canShareHighRes() { - return !this.isVideo || !config_vodDisable; + return !this.isVideo || !this.config.vod_disable; }, canShareLink() { diff --git a/src/components/top-matter/AlbumTopMatter.vue b/src/components/top-matter/AlbumTopMatter.vue index 4f4a9720..66094cf8 100644 --- a/src/components/top-matter/AlbumTopMatter.vue +++ b/src/components/top-matter/AlbumTopMatter.vue @@ -18,7 +18,7 @@ @@ -29,7 +29,7 @@ @@ -194,8 +194,8 @@ export default defineComponent({ * 1 = date, 2 = name */ changeSort(order: 1 | 2) { - this.config_albumListSort = order; - this.updateSetting('albumListSort'); + this.config.album_list_sort = order; + this.updateSetting('album_list_sort'); }, }, }); diff --git a/src/components/top-matter/FaceTopMatter.vue b/src/components/top-matter/FaceTopMatter.vue index 02052d3a..668f8fa9 100644 --- a/src/components/top-matter/FaceTopMatter.vue +++ b/src/components/top-matter/FaceTopMatter.vue @@ -25,7 +25,7 @@ {{ t('memories', 'Mark person in preview') }} @@ -104,10 +104,8 @@ export default defineComponent({ }, changeShowFaceRect() { - localStorage.setItem('memories_showFaceRect', this.config_showFaceRect ? '1' : '0'); - setTimeout(() => { - this.$router.go(0); // refresh page - }, 500); + this.updateSetting('show_face_rect'); + setTimeout(() => this.$router.go(0), 100); // refresh page }, }, }); diff --git a/src/components/top-matter/FolderTopMatter.vue b/src/components/top-matter/FolderTopMatter.vue index 6a358f3f..5a40a099 100644 --- a/src/components/top-matter/FolderTopMatter.vue +++ b/src/components/top-matter/FolderTopMatter.vue @@ -108,7 +108,7 @@ export default defineComponent({ }, share() { - globalThis.shareNodeLink(utils.getFolderRoutePath(this.config_foldersPath)); + globalThis.shareNodeLink(utils.getFolderRoutePath(this.config.folders_path)); }, }, }); diff --git a/src/components/viewer/PsVideo.ts b/src/components/viewer/PsVideo.ts index a884943b..3bd42991 100644 --- a/src/components/viewer/PsVideo.ts +++ b/src/components/viewer/PsVideo.ts @@ -1,5 +1,5 @@ import PhotoSwipe from 'photoswipe'; -import { loadState } from '@nextcloud/initial-state'; +import staticConfig from '../../services/static-config'; import { showError } from '@nextcloud/dialogs'; import { translate as t } from '@nextcloud/l10n'; import { getCurrentUser } from '@nextcloud/auth'; @@ -25,10 +25,6 @@ type PsVideoEvent = PsEvent & { content: VideoContent; }; -const config_vodDisable = loadState('memories', 'vod_disable', true); - -const config_video_default_quality = Number(loadState('memories', 'video_default_quality', '0') as string); - /** * Check if slide has video content */ @@ -155,7 +151,7 @@ class VideoContentSetup { type: string; }[] = []; - if (!config_vodDisable) { + if (!staticConfig.getSync('vod_disable')) { sources.push(this.getHLSsrc(content)); } @@ -205,7 +201,7 @@ class VideoContentSetup { directFailed = true; console.warn('PsVideo: Direct video stream could not be opened.'); - if (!hlsFailed && !config_vodDisable) { + if (!hlsFailed && !staticConfig.getSync('vod_disable')) { console.warn('PsVideo: Trying HLS stream'); vjs.src(this.getHLSsrc(content)); } @@ -336,7 +332,7 @@ class VideoContentSetup { // Add quality options if (qualityNums) { opts.quality = { - default: config_video_default_quality, + default: Number(staticConfig.getSync('video_default_quality')), options: qualityNums, forced: true, onChange: (quality: number) => { diff --git a/src/components/viewer/Viewer.vue b/src/components/viewer/Viewer.vue index c18c9ace..fc01eb17 100644 --- a/src/components/viewer/Viewer.vue +++ b/src/components/viewer/Viewer.vue @@ -770,7 +770,7 @@ export default defineComponent({ // Get full image URL const fullUrl = isvideo ? null : API.IMAGE_DECODABLE(photo.fileid, photo.etag); - const fullLoadCond = this.config_fullResAlways ? 'always' : this.config_fullResOnZoom ? 'zoom' : 'never'; + const fullLoadCond = this.config.full_res_always ? 'always' : this.config.full_res_on_zoom ? 'zoom' : 'never'; return { src: previewUrl, diff --git a/src/mixins/UserConfig.ts b/src/mixins/UserConfig.ts index f82d5eee..b34f6cf9 100644 --- a/src/mixins/UserConfig.ts +++ b/src/mixins/UserConfig.ts @@ -1,43 +1,30 @@ -import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'; -import { loadState } from '@nextcloud/initial-state'; import axios from '@nextcloud/axios'; +import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'; import { API } from '../services/API'; import { defineComponent } from 'vue'; +import { IConfig } from '../types'; +import staticConfig from '../services/static-config'; const eventName = 'memories:user-config-changed'; -const localSettings = ['squareThumbs', 'fullResOnZoom', 'fullResAlways', 'showFaceRect', 'albumListSort']; +const localSettings: (keyof IConfig)[] = [ + 'square_thumbs', + 'full_res_on_zoom', + 'full_res_always', + 'show_face_rect', + 'album_list_sort', +]; export default defineComponent({ name: 'UserConfig', data: () => ({ - config_timelinePath: loadState('memories', 'timelinePath', '') as string, - config_foldersPath: loadState('memories', 'foldersPath', '/') as string, - - config_showHidden: loadState('memories', 'showHidden', 'false') === 'true', - config_sortFolderMonth: loadState('memories', 'sortFolderMonth', 'false') === 'true', - config_sortAlbumMonth: loadState('memories', 'sortAlbumMonth', 'true') === 'true', - config_enableTopMemories: loadState('memories', 'enableTopMemories', 'false') === 'true', - - config_tagsEnabled: Boolean(loadState('memories', 'systemtags', '')), - config_recognizeEnabled: Boolean(loadState('memories', 'recognize', '')), - config_facerecognitionInstalled: Boolean(loadState('memories', 'facerecognitionInstalled', '')), - config_facerecognitionEnabled: Boolean(loadState('memories', 'facerecognitionEnabled', '')), - config_albumsEnabled: Boolean(loadState('memories', 'albums', '')), - - config_placesGis: Number(loadState('memories', 'places_gis', '-1')), - - config_squareThumbs: localStorage.getItem('memories_squareThumbs') === '1', - config_fullResOnZoom: localStorage.getItem('memories_fullResOnZoom') !== '0', - config_fullResAlways: localStorage.getItem('memories_fullResAlways') === '1', - config_showFaceRect: localStorage.getItem('memories_showFaceRect') === '1', - config_albumListSort: Number(localStorage.getItem('memories_albumListSort') || 1), - - config_eventName: eventName, + config: { ...staticConfig.getDefault() }, + configEventName: eventName, }), created() { subscribe(eventName, this.updateLocalSetting); + this.refreshFromConfig(); }, beforeDestroy() { @@ -45,27 +32,32 @@ export default defineComponent({ }, methods: { - updateLocalSetting({ setting, value }) { - this['config_' + setting] = value; + async refreshFromConfig() { + const config = await staticConfig.getAll(); + const changed = Object.keys(config).filter((key) => config[key] !== this.config[key]); + if (changed.length === 0) return; + + changed.forEach((key) => (this.config[key] = config[key])); + emit(eventName, { setting: null, value: null }); }, - async updateSetting(setting: string) { - const value = this['config_' + setting]; + updateLocalSetting({ setting, value }) { + if (setting) { + this.config[setting] = value; + } + }, - if (localSettings.includes(setting)) { - if (typeof value === 'boolean') { - localStorage.setItem('memories_' + setting, value ? '1' : '0'); - } else { - localStorage.setItem('memories_' + setting, value); - } - } else { - // Long time save setting - await axios.put(API.CONFIG(setting), { + async updateSetting(setting: K, remote?: string) { + const value = this.config[setting]; + + if (!localSettings.includes(setting)) { + await axios.put(API.CONFIG(remote ?? setting), { value: value.toString(), }); } - // Visible elements update setting + staticConfig.setLs(setting, value); + emit(eventName, { setting, value }); }, }, diff --git a/src/services/API.ts b/src/services/API.ts index de7638e8..2128ec34 100644 --- a/src/services/API.ts +++ b/src/services/API.ts @@ -185,6 +185,10 @@ export class API { return gen(`${BASE}/config/{setting}`, { setting }); } + static CONFIG_GET() { + return gen(`${BASE}/config`); + } + static SYSTEM_CONFIG(setting: string | null) { return setting ? gen(`${BASE}/system-config/{setting}`, { setting }) : gen(`${BASE}/system-config`); } diff --git a/src/services/static-config.ts b/src/services/static-config.ts new file mode 100644 index 00000000..745f1f37 --- /dev/null +++ b/src/services/static-config.ts @@ -0,0 +1,111 @@ +import axios from '@nextcloud/axios'; +import { API } from './API'; +import { IConfig } from '../types'; + +class StaticConfig { + private config: IConfig | null = null; + private initPromises: Array<() => void> = []; + private default: IConfig | null = null; + + public constructor() { + this.init(); + } + + private async init() { + const res = await axios.get(API.CONFIG_GET()); + this.config = res.data; + + this.getDefault(); + for (const key in this.config) { + this.default![key] = this.config[key]; + this.setLs(key as keyof IConfig, this.config[key]); + } + + this.initPromises.forEach((resolve) => resolve()); + } + + private async waitForInit() { + if (!this.config) { + await new Promise((resolve) => { + this.initPromises.push(resolve); + }); + } + } + + public async getAll() { + await this.waitForInit(); + return this.config!; + } + + public async get(key: K) { + await this.waitForInit(); + return this.config![key]; + } + + public getSync(key: K) { + return this.getDefault()[key]; + } + + public setLs(key: K, value: IConfig[K]) { + if (this.default) { + this.default[key] = value; + } + + if (this.config) { + this.config[key] = value; + } + + localStorage.setItem(`memories_${key}`, value.toString()); + } + + public getDefault(): IConfig { + if (this.default) { + return this.default; + } + + const config: IConfig = { + version: '', + vod_disable: false, + video_default_quality: '0', + places_gis: -1, + + systemtags_enabled: false, + recognize_enabled: false, + albums_enabled: false, + facerecognition_installed: false, + facerecognition_enabled: false, + + timeline_path: '', + folders_path: '', + show_hidden_folders: false, + sort_folder_month: false, + sort_album_month: true, + enable_top_memories: true, + + square_thumbs: false, + full_res_on_zoom: true, + full_res_always: false, + show_face_rect: false, + album_list_sort: 1, + }; + + for (const key in config) { + const val = localStorage.getItem(`memories_${key}`); + if (val !== null) { + if (typeof config[key] === 'boolean') { + config[key] = val === 'true'; + } else if (typeof config[key] === 'number') { + config[key] = Number(val); + } else { + config[key] = val; + } + } + } + + this.default = config; + + return config; + } +} + +export default new StaticConfig(); diff --git a/src/services/strings.ts b/src/services/strings.ts index ece1e183..d1e7e551 100644 --- a/src/services/strings.ts +++ b/src/services/strings.ts @@ -1,8 +1,6 @@ -import { loadState } from '@nextcloud/initial-state'; +import staticConfig from './static-config'; import { translate as t } from '@nextcloud/l10n'; -const config_facerecognitionEnabled = Boolean(loadState('memories', 'facerecognitionEnabled', '')); - type RouteNameType = string | null | undefined; export function emptyDescription(routeName: RouteNameType): string { @@ -14,7 +12,7 @@ export function emptyDescription(routeName: RouteNameType): string { case 'thisday': return t('memories', 'Memories from past years will appear here'); case 'facerecognition': - return config_facerecognitionEnabled + return staticConfig.getSync('facerecognition_enabled') ? t('memories', 'You will find your friends soon. Please be patient') : t('memories', 'Face Recognition is disabled. Enable in settings to find your friends'); case 'videos': diff --git a/src/services/utils/cache.ts b/src/services/utils/cache.ts index 705cd252..b8abf545 100644 --- a/src/services/utils/cache.ts +++ b/src/services/utils/cache.ts @@ -1,18 +1,23 @@ import { getCurrentUser } from '@nextcloud/auth'; -import { loadState } from '@nextcloud/initial-state'; +import staticConfig from '../static-config'; /** Cache keys */ -const memoriesVersion: string = loadState('memories', 'version', ''); const uid = getCurrentUser()?.uid || 'guest'; -const cacheName = `memories-${memoriesVersion}-${uid}`; + +async function getCacheName() { + const memoriesVersion = await staticConfig.get('version'); + return `memories-${memoriesVersion}-${uid}`; +} // Clear all caches except the current one (async function clearCaches() { - if (!memoriesVersion || uid === 'guest') return; + if (uid === 'guest') return; const keys = await window.caches?.keys(); if (!keys?.length) return; + const cacheName = await getCacheName(); + for (const key of keys) { if (key.startsWith('memories-') && key !== cacheName) { window.caches.delete(key); @@ -23,10 +28,8 @@ const cacheName = `memories-${memoriesVersion}-${uid}`; /** Singleton cache instance */ let staticCache: Cache | null = null; export async function openCache() { - if (!memoriesVersion) return null; - try { - return (staticCache ??= (await window.caches?.open(cacheName)) ?? null); + return (staticCache ??= (await window.caches?.open(await getCacheName())) ?? null); } catch { return null; } diff --git a/src/types.ts b/src/types.ts index 7f041a45..34f25cda 100644 --- a/src/types.ts +++ b/src/types.ts @@ -236,3 +236,29 @@ export type ISelectionAction = { /** Allow for public routes (default false) */ allowPublic?: boolean; }; + +export type IConfig = { + version: string; + vod_disable: boolean; + video_default_quality: string; + places_gis: number; + + systemtags_enabled: boolean; + recognize_enabled: boolean; + albums_enabled: boolean; + facerecognition_installed: boolean; + facerecognition_enabled: boolean; + + timeline_path: string; + folders_path: string; + show_hidden_folders: boolean; + sort_folder_month: boolean; + sort_album_month: boolean; + enable_top_memories: boolean; + + square_thumbs: boolean; + full_res_on_zoom: boolean; + full_res_always: boolean; + show_face_rect: boolean; + album_list_sort: 1 | 2; +}; diff --git a/src/vue-globals.d.ts b/src/vue-globals.d.ts index 3d9f4de2..70c69b26 100644 --- a/src/vue-globals.d.ts +++ b/src/vue-globals.d.ts @@ -10,29 +10,6 @@ declare module 'vue' { c: typeof constants.c; state_noDownload: boolean; - - // UserConfig.ts - config_timelinePath: string; - config_foldersPath: string; - config_showHidden: boolean; - config_sortFolderMonth: boolean; - config_sortAlbumMonth: boolean; - config_tagsEnabled: boolean; - config_recognizeEnabled: boolean; - config_facerecognitionInstalled: boolean; - config_facerecognitionEnabled: boolean; - config_albumsEnabled: boolean; - config_placesGis: number; - config_squareThumbs: boolean; - config_enableTopMemories: boolean; - config_fullResOnZoom: boolean; - config_fullResAlways: boolean; - config_showFaceRect: boolean; - config_albumListSort: 1 | 2; - config_eventName: string; - - updateSetting: (setting: string) => Promise; - updateLocalSetting: (opts: { setting: string; value: any }) => void; } }