diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts index b8d7f4bea..09263edda 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -92,7 +92,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp /** * Open the book in a certain chapter. * - * @param chapterId Chapter to open, undefined for first chapter. + * @param chapterId Chapter to open, undefined for last chapter viewed. */ openBook(chapterId?: number): void { CoreNavigator.navigate('contents', { diff --git a/src/addons/mod/book/pages/contents/contents.ts b/src/addons/mod/book/pages/contents/contents.ts index 21436d557..7a2055bd3 100644 --- a/src/addons/mod/book/pages/contents/contents.ts +++ b/src/addons/mod/book/pages/contents/contents.ts @@ -53,7 +53,7 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent; - title!: string; + title = ''; cmId!: number; courseId!: number; initialChapterId?: number; @@ -147,6 +147,8 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { } else { this.warning = ''; } + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); } finally { this.loaded = true; } @@ -279,7 +281,7 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { } if (this.book) { - AddonModBook.storeLastChapterViewed(this.book.id, chapterId); + AddonModBook.storeLastChapterViewed(this.book.id, chapterId, this.courseId); } if (!this.module) { diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index 64d025dd6..16cdd0360 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -26,11 +26,6 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreFile } from '@services/file'; import { CoreError } from '@classes/errors/error'; -import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; -import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance'; -import { CoreDatabaseTable } from '@classes/database/database-table'; -import { AddonModBookLastChapterViewedDBRecord, LAST_CHAPTER_VIEWED_TABLE } from './database/book'; -import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; /** * Constants to define how the chapters and subchapters of a book should be displayed in that table of contents. @@ -61,20 +56,6 @@ export class AddonModBookProvider { static readonly COMPONENT = 'mmaModBook'; - protected lastChapterViewedTables: LazyMap>>; - - constructor() { - this.lastChapterViewedTables = lazyMap( - siteId => asyncInstance( - () => CoreSites.getSiteTable(LAST_CHAPTER_VIEWED_TABLE, { - siteId, - config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, - onDestroy: () => delete this.lastChapterViewedTables[siteId], - }), - ), - ); - } - /** * Get a book by course module ID. * @@ -243,14 +224,12 @@ export class AddonModBookProvider { * @return Promise resolved with last chapter viewed, undefined if none. */ async getLastChapterViewed(id: number, siteId?: string): Promise { - try { - const site = await CoreSites.getSite(siteId); - const entry = await this.lastChapterViewedTables[site.getId()].getOneByPrimaryKey({ id }); + const site = await CoreSites.getSite(siteId); + const entry = await site.getLastViewed(AddonModBookProvider.COMPONENT, id); - return entry.chapterid; - } catch { - // No last chapter viewed. - } + const chapterId = Number(entry?.value); + + return isNaN(chapterId) ? undefined : chapterId; } /** @@ -405,13 +384,14 @@ export class AddonModBookProvider { * * @param id Book instance ID. * @param chapterId Chapter ID. + * @param courseId Course ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with last chapter viewed, undefined if none. */ - async storeLastChapterViewed(id: number, chapterId: number, siteId?: string): Promise { + async storeLastChapterViewed(id: number, chapterId: number, courseId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - await this.lastChapterViewedTables[site.getId()].insert({ id, chapterid: chapterId }); + await site.storeLastViewed(AddonModBookProvider.COMPONENT, id, chapterId, String(courseId)); } } diff --git a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html index e4cfff579..0f43d70d2 100644 --- a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html +++ b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html @@ -1,9 +1,5 @@ - - - - @@ -13,24 +9,35 @@ - + - + - - + +

{{ 'addon.mod_imscp.toc' | translate }}

+
-
-
- -
+ + +

+ + {{item.title}} +

+
+
+
- - - +
+ + {{ 'core.start' | translate }} + {{ 'core.resume' | translate }} + +
+
diff --git a/src/addons/mod/imscp/components/index/index.ts b/src/addons/mod/imscp/components/index/index.ts index 78df65bac..6d6e501b4 100644 --- a/src/addons/mod/imscp/components/index/index.ts +++ b/src/addons/mod/imscp/components/index/index.ts @@ -13,14 +13,11 @@ // limitations under the License. import { Component, OnInit, Optional } from '@angular/core'; -import { CoreSilentError } from '@classes/errors/silenterror'; -import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; import { CoreCourse } from '@features/course/services/course'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreNavigator } from '@services/navigator'; import { AddonModImscpProvider, AddonModImscp, AddonModImscpTocItem } from '../../services/imscp'; -import { AddonModImscpTocComponent } from '../toc/toc'; /** * Component that displays a IMSCP. @@ -33,13 +30,9 @@ import { AddonModImscpTocComponent } from '../toc/toc'; export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { component = AddonModImscpProvider.COMPONENT; - src = ''; - warning = ''; - navigationItems: CoreNavigationBarItem[] = []; - protected items: AddonModImscpTocItem[] = []; - protected currentHref?: string; - protected displayDescription = false; + items: AddonModImscpTocItem[] = []; + hasStarted = false; constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { super('AddonModImscpIndexComponent', courseContentsPage); @@ -73,85 +66,68 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom /** * @inheritdoc */ - protected async fetchContent(refresh = false): Promise { - const downloadResult = await this.downloadResourceIfNeeded(refresh); - - const imscp = await AddonModImscp.getImscp(this.courseId, this.module.id); - this.description = imscp.intro; - this.dataRetrieved.emit(imscp); - - // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. - const contents = await CoreCourse.getModuleContents(this.module); - - this.items = AddonModImscp.createItemList(contents); - - if (this.items.length && this.currentHref === undefined) { - this.currentHref = this.items[0].href; - } - - try { - await this.loadItemHref(this.currentHref); - } catch (error) { - CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true); - - throw new CoreSilentError(error); - } - - this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; + protected async fetchContent(): Promise { + await Promise.all([ + this.loadImscp(), + this.loadTOC(), + ]); } /** - * Loads an item. + * Load IMSCP data. * - * @param itemHref Item Href. * @return Promise resolved when done. */ - async loadItemHref(itemHref?: string): Promise { - const src = await AddonModImscp.getIframeSrc(this.module, itemHref); - this.currentHref = itemHref; + protected async loadImscp(): Promise { + const imscp = await AddonModImscp.getImscp(this.courseId, this.module.id); - this.navigationItems = this.items.map((item) => ({ - item: item, - current: item.href == this.currentHref, - enabled: !!item.href, - })); + this.dataRetrieved.emit(imscp); - if (this.src && src == this.src) { - // Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed. - this.src = ''; - setTimeout(() => { - this.src = src; - }); - } else { - this.src = src; - } + this.dataRetrieved.emit(imscp); + + this.description = imscp.intro; + + const lastViewed = await AddonModImscp.getLastItemViewed(imscp.id); + this.hasStarted = lastViewed !== undefined; } /** - * Loads an item. + * Load book TOC. * - * @param item Item. + * @return Promise resolved when done. */ - loadItem(item: AddonModImscpTocItem): void { - this.loadItemHref(item.href); + protected async loadTOC(): Promise { + // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. + const contents = await CoreCourse.getModuleContents(this.module, this.courseId); + + this.items = AddonModImscp.createItemList(contents); } /** - * Show the TOC. + * Open IMSCP book with a certain item. + * + * @param href Item href to open, undefined for last item seen. */ - async showToc(): Promise { - // Create the toc modal. - const modalData = await CoreDomUtils.openSideModal({ - component: AddonModImscpTocComponent, - componentProps: { - items: this.items, - selected: this.currentHref, + openImscp(href?: string): void { + CoreNavigator.navigate('view', { + params: { + cmId: this.module.id, + courseId: this.courseId, + initialHref: href, }, }); - if (modalData) { - this.loadItemHref(modalData); - } + this.hasStarted = true; + } + + /** + * Get dummy array for padding. + * + * @param n Array length. + * @return Dummy array with n elements. + */ + getNumberForPadding(n: number): number[] { + return new Array(n); } } diff --git a/src/addons/mod/imscp/imscp-lazy.module.ts b/src/addons/mod/imscp/imscp-lazy.module.ts index 2fe070b1f..1fb94112d 100644 --- a/src/addons/mod/imscp/imscp-lazy.module.ts +++ b/src/addons/mod/imscp/imscp-lazy.module.ts @@ -24,6 +24,10 @@ const routes: Routes = [ path: ':courseId/:cmId', component: AddonModImscpIndexPage, }, + { + path: ':courseId/:cmId/view', + loadChildren: () => import('./pages/view/view.module').then(m => m.AddonModImscpViewPageModule), + }, ]; @NgModule({ diff --git a/src/addons/mod/imscp/pages/view/view.html b/src/addons/mod/imscp/pages/view/view.html new file mode 100644 index 000000000..309b6b94a --- /dev/null +++ b/src/addons/mod/imscp/pages/view/view.html @@ -0,0 +1,39 @@ + + + + + + +

+ + +

+
+ + + + + + +
+
+ + + + + + + + + + + +
+ +
+
+ + + +
diff --git a/src/addons/mod/imscp/pages/view/view.module.ts b/src/addons/mod/imscp/pages/view/view.module.ts new file mode 100644 index 000000000..a4232dac5 --- /dev/null +++ b/src/addons/mod/imscp/pages/view/view.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModImscpViewPage } from './view'; + +const routes: Routes = [ + { + path: '', + component: AddonModImscpViewPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + declarations: [ + AddonModImscpViewPage, + ], + exports: [RouterModule], +}) +export class AddonModImscpViewPageModule {} diff --git a/src/addons/mod/imscp/pages/view/view.ts b/src/addons/mod/imscp/pages/view/view.ts new file mode 100644 index 000000000..18da77400 --- /dev/null +++ b/src/addons/mod/imscp/pages/view/view.ts @@ -0,0 +1,289 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreConstants } from '@/core/constants'; +import { Component, OnInit } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; +import { CoreCourseResourceDownloadResult } from '@features/course/classes/main-resource-component'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreCourseModuleData } from '@features/course/services/course-helper'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { IonRefresher } from '@ionic/angular'; +import { CoreApp } from '@services/app'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; +import { AddonModImscpTocComponent } from '../../components/toc/toc'; +import { AddonModImscp, AddonModImscpImscp, AddonModImscpTocItem } from '../../services/imscp'; + +/** + * Page that displays a IMSCP content. + */ +@Component({ + selector: 'page-addon-mod-imscp-view', + templateUrl: 'view.html', +}) +export class AddonModImscpViewPage implements OnInit { + + title = ''; + cmId!: number; + courseId!: number; + initialItemHref?: string; + src = ''; + warning = ''; + navigationItems: CoreNavigationBarItem[] = []; + loaded = false; + + protected module?: CoreCourseModuleData; + protected imscp?: AddonModImscpImscp; + protected items: AddonModImscpTocItem[] = []; + protected currentHref?: string; + + /** + * @inheritdoc + */ + ngOnInit(): void { + try { + this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); + this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); + this.initialItemHref = CoreNavigator.getRouteParam('initialHref'); + } catch (error) { + CoreDomUtils.showErrorModal(error); + + CoreNavigator.back(); + + return; + } + + this.fetchContent(); + } + + /** + * Download IMSCP contents and load the current item. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + protected async fetchContent(refresh = false): Promise { + try { + const { module, imscp } = await this.loadImscpData(); + + this.title = imscp.name; + + const downloadResult = await this.downloadResourceIfNeeded(module, refresh); + + // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. + const contents = await CoreCourse.getModuleContents(module, this.courseId); + + this.items = AddonModImscp.createItemList(contents); + + if (this.items.length) { + if (this.initialItemHref) { + // Check it's valid. + if (this.items.some(item => item.href === this.initialItemHref)) { + this.currentHref = this.initialItemHref; + } + } + + if (this.currentHref === undefined) { + // Get last viewed. + const lastViewedHref = await AddonModImscp.getLastItemViewed(imscp.id); + + if (lastViewedHref !== undefined) { + this.currentHref = lastViewedHref; + } else { + // Use first one. + this.currentHref = this.items[0].href; + } + } + } + + if (this.currentHref === undefined) { + throw new CoreError('Empty TOC'); + } + + try { + await this.loadItemHref(this.currentHref); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true); + + return; + } + + if (downloadResult?.failed) { + const error = CoreTextUtils.getErrorMessageFromError(downloadResult.error) || downloadResult.error; + this.warning = Translate.instant('core.errordownloadingsomefiles') + (error ? ' ' + error : ''); + } else { + this.warning = ''; + } + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); + } finally { + this.loaded = true; + } + } + + /** + * Load IMSCP data from WS. + * + * @return Promise resolved when done. + */ + async loadImscpData(): Promise<{ module: CoreCourseModuleData; imscp: AddonModImscpImscp }> { + this.module = await CoreCourse.getModule(this.cmId, this.courseId); + this.imscp = await AddonModImscp.getImscp(this.courseId, this.cmId); + + return { + module: this.module, + imscp: this.imscp, + }; + } + + /** + * Download a resource if needed. + * If the download call fails the promise won't be rejected, but the error will be included in the returned object. + * If module.contents cannot be loaded then the Promise will be rejected. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + protected async downloadResourceIfNeeded( + module: CoreCourseModuleData, + refresh = false, + ): Promise { + + const result: CoreCourseResourceDownloadResult = { + failed: false, + }; + let contentsAlreadyLoaded = false; + + // Get module status to determine if it needs to be downloaded. + const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(module, this.courseId, undefined, refresh); + + if (status !== CoreConstants.DOWNLOADED) { + // Download content. This function also loads module contents if needed. + try { + await CoreCourseModulePrefetchDelegate.downloadModule(module, this.courseId); + + // If we reach here it means the download process already loaded the contents, no need to do it again. + contentsAlreadyLoaded = true; + } catch (error) { + // Mark download as failed but go on since the main files could have been downloaded. + result.failed = true; + result.error = error; + } + } + + if (!module.contents?.length || (refresh && !contentsAlreadyLoaded)) { + // Try to load the contents. + const ignoreCache = refresh && CoreApp.isOnline(); + + try { + await CoreCourse.loadModuleContents(module, undefined, undefined, false, ignoreCache); + } catch (error) { + // Error loading contents. If we ignored cache, try to get the cached value. + if (ignoreCache && !module.contents) { + await CoreCourse.loadModuleContents(module); + } else if (!module.contents) { + // Not able to load contents, throw the error. + throw error; + } + } + } + + return result; + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: IonRefresher): Promise { + await CoreUtils.ignoreErrors(Promise.all([ + AddonModImscp.invalidateContent(this.cmId, this.courseId), + CoreCourseModulePrefetchDelegate.invalidateCourseUpdates(this.courseId), // To detect if IMSCP was updated. + ])); + + await CoreUtils.ignoreErrors(this.fetchContent(true)); + + refresher?.complete(); + } + + /** + * Loads an item. + * + * @param itemHref Item Href. + * @return Promise resolved when done. + */ + async loadItemHref(itemHref: string): Promise { + if (!this.module) { + return; + } + + const src = await AddonModImscp.getIframeSrc(this.module, itemHref); + this.currentHref = itemHref; + + this.navigationItems = this.items.map((item) => ({ + item: item, + current: item.href == this.currentHref, + enabled: !!item.href, + })); + + if (this.src && src == this.src) { + // Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed. + this.src = ''; + setTimeout(() => { + this.src = src; + }); + } else { + this.src = src; + } + + if (this.imscp) { + AddonModImscp.storeLastItemViewed(this.imscp.id, itemHref, this.courseId); + } + } + + /** + * Loads an item. + * + * @param item Item. + */ + loadItem(item: AddonModImscpTocItem): void { + this.loadItemHref(item.href); + } + + /** + * Show the TOC. + */ + async showToc(): Promise { + // Create the toc modal. + const itemHref = await CoreDomUtils.openSideModal({ + component: AddonModImscpTocComponent, + componentProps: { + items: this.items, + selected: this.currentHref, + }, + }); + + if (itemHref) { + this.loadItemHref(itemHref); + } + } + +} diff --git a/src/addons/mod/imscp/services/imscp.ts b/src/addons/mod/imscp/services/imscp.ts index 121577ea3..abfcd0d87 100644 --- a/src/addons/mod/imscp/services/imscp.ts +++ b/src/addons/mod/imscp/services/imscp.ts @@ -168,29 +168,21 @@ export class AddonModImscpProvider { * Get src of a imscp item. * * @param module The module object. - * @param itemHref Href of item to get. If not defined, gets src of main item. + * @param itemHref Href of item to get. * @return Promise resolved with the item src. */ - async getIframeSrc(module: CoreCourseModuleData, itemHref?: string): Promise { - const contents = await CoreCourse.getModuleContents(module); - - if (!itemHref) { - const toc = this.getToc(contents); - if (!toc.length) { - throw new CoreError('Empty TOC'); - } - itemHref = toc[0].href; - } - + async getIframeSrc(module: CoreCourseModuleData, itemHref: string): Promise { const siteId = CoreSites.getCurrentSiteId(); try { - const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module.url!); + const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module.url || ''); return CoreTextUtils.concatenatePaths(dirPath, itemHref); } catch (error) { // Error getting directory, there was an error downloading or we're in browser. Return online URL if connected. if (CoreApp.isOnline()) { + const contents = await CoreCourse.getModuleContents(module); + const indexUrl = this.getFileUrlFromContents(contents, itemHref); if (indexUrl) { @@ -204,6 +196,20 @@ export class AddonModImscpProvider { } } + /** + * Get last item viewed's href in the app for a IMSCP. + * + * @param id IMSCP instance ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with last item viewed's href, undefined if none. + */ + async getLastItemViewed(id: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + const entry = await site.getLastViewed(AddonModImscpProvider.COMPONENT, id); + + return entry?.value; + } + /** * Invalidate the prefetched content. * @@ -285,6 +291,21 @@ export class AddonModImscpProvider { ); } + /** + * Store last item viewed in the app for a IMSCP. + * + * @param id IMSCP instance ID. + * @param href Item href. + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with last item viewed, undefined if none. + */ + async storeLastItemViewed(id: number, href: string, courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.storeLastViewed(AddonModImscpProvider.COMPONENT, id, href, String(courseId)); + } + } export const AddonModImscp = makeSingleton(AddonModImscpProvider); diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 99f9bb496..0f5ace6b2 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -82,6 +82,7 @@ export class CoreSite { // Variables for the database. static readonly WS_CACHE_TABLE = 'wscache_2'; static readonly CONFIG_TABLE = 'core_site_config'; + static readonly LAST_VIEWED_TABLE = 'core_site_last_viewed'; static readonly MINIMUM_MOODLE_VERSION = '3.5'; @@ -110,6 +111,7 @@ export class CoreSite { protected db?: SQLiteDB; protected cacheTable: AsyncInstance>; protected configTable: AsyncInstance>; + protected lastViewedTable: AsyncInstance>; protected cleanUnicode = false; protected lastAutoLogin = 0; protected offlineDisabled = false; @@ -154,6 +156,12 @@ export class CoreSite { config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, primaryKeyColumns: ['name'], })); + this.lastViewedTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.LAST_VIEWED_TABLE, { + siteId: this.getId(), + database: this.getDb(), + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + primaryKeyColumns: ['component', 'id'], + })); this.setInfo(infos); this.calculateOfflineDisabled(); @@ -1955,6 +1963,49 @@ export class CoreSite { return this.containsUrl(url); } + /** + * Deletes last viewed records based on some conditions. + * + * @param conditions Conditions. + * @return Promise resolved when done. + */ + async deleteLastViewed(conditions?: Partial): Promise { + await this.lastViewedTable.delete(conditions); + } + + /** + * Get a last viewed record for a component+id. + * + * @param component The component. + * @param id ID. + * @return Resolves with last viewed record, undefined if not found. + */ + async getLastViewed(component: string, id: number): Promise { + try { + return await this.lastViewedTable.getOneByPrimaryKey({ component, id }); + } catch (error) { + // Not found. + } + } + + /** + * Store a last viewed record. + * + * @param component The component. + * @param id ID. + * @param value Last viewed item value. + * @param data Other data. + * @return Promise resolved when done. + */ + async storeLastViewed(component: string, id: number, value: string | number, data?: string): Promise { + await this.lastViewedTable.insert({ + component, + id, + value: String(value), + data, + }); + } + } /** @@ -2281,3 +2332,10 @@ export type CoreSiteWSCacheRecord = { component?: string; componentId?: number; }; + +export type CoreSiteLastViewedDBRecord = { + component: string; + id: number; + value: string; + data?: string; +}; diff --git a/src/core/services/database/sites.ts b/src/core/services/database/sites.ts index 930955e46..8d2083e1d 100644 --- a/src/core/services/database/sites.ts +++ b/src/core/services/database/sites.ts @@ -78,7 +78,7 @@ export const APP_SCHEMA: CoreAppSchema = { // Schema to register for Site DB. export const SITE_SCHEMA: CoreSiteSchema = { name: 'CoreSitesProvider', - version: 2, + version: 3, canBeCleared: [CoreSite.WS_CACHE_TABLE], tables: [ { @@ -125,6 +125,29 @@ export const SITE_SCHEMA: CoreSiteSchema = { }, ], }, + { + name: CoreSite.LAST_VIEWED_TABLE, + columns: [ + { + name: 'component', + type: 'TEXT', + }, + { + name: 'id', + type: 'INTEGER', + }, + { + name: 'value', + type: 'TEXT', + notNull: true, + }, + { + name: 'data', + type: 'TEXT', + }, + ], + primaryKeys: ['component', 'id'], + }, ], async migrate(db: SQLiteDB, oldVersion: number): Promise { if (oldVersion < 2) {