commit
8600f741bb
|
@ -92,7 +92,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
/**
|
/**
|
||||||
* Open the book in a certain chapter.
|
* 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 {
|
openBook(chapterId?: number): void {
|
||||||
CoreNavigator.navigate('contents', {
|
CoreNavigator.navigate('contents', {
|
||||||
|
|
|
@ -53,7 +53,7 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent;
|
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent;
|
||||||
|
|
||||||
title!: string;
|
title = '';
|
||||||
cmId!: number;
|
cmId!: number;
|
||||||
courseId!: number;
|
courseId!: number;
|
||||||
initialChapterId?: number;
|
initialChapterId?: number;
|
||||||
|
@ -147,6 +147,8 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy {
|
||||||
} else {
|
} else {
|
||||||
this.warning = '';
|
this.warning = '';
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||||
} finally {
|
} finally {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
|
@ -279,7 +281,7 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.book) {
|
if (this.book) {
|
||||||
AddonModBook.storeLastChapterViewed(this.book.id, chapterId);
|
AddonModBook.storeLastChapterViewed(this.book.id, chapterId, this.courseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.module) {
|
if (!this.module) {
|
||||||
|
|
|
@ -26,11 +26,6 @@ import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreFile } from '@services/file';
|
import { CoreFile } from '@services/file';
|
||||||
import { CoreError } from '@classes/errors/error';
|
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.
|
* 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';
|
static readonly COMPONENT = 'mmaModBook';
|
||||||
|
|
||||||
protected lastChapterViewedTables: LazyMap<AsyncInstance<CoreDatabaseTable<AddonModBookLastChapterViewedDBRecord>>>;
|
|
||||||
|
|
||||||
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.
|
* Get a book by course module ID.
|
||||||
*
|
*
|
||||||
|
@ -243,14 +224,12 @@ export class AddonModBookProvider {
|
||||||
* @return Promise resolved with last chapter viewed, undefined if none.
|
* @return Promise resolved with last chapter viewed, undefined if none.
|
||||||
*/
|
*/
|
||||||
async getLastChapterViewed(id: number, siteId?: string): Promise<number | undefined> {
|
async getLastChapterViewed(id: number, siteId?: string): Promise<number | undefined> {
|
||||||
try {
|
const site = await CoreSites.getSite(siteId);
|
||||||
const site = await CoreSites.getSite(siteId);
|
const entry = await site.getLastViewed(AddonModBookProvider.COMPONENT, id);
|
||||||
const entry = await this.lastChapterViewedTables[site.getId()].getOneByPrimaryKey({ id });
|
|
||||||
|
|
||||||
return entry.chapterid;
|
const chapterId = Number(entry?.value);
|
||||||
} catch {
|
|
||||||
// No last chapter viewed.
|
return isNaN(chapterId) ? undefined : chapterId;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -405,13 +384,14 @@ export class AddonModBookProvider {
|
||||||
*
|
*
|
||||||
* @param id Book instance ID.
|
* @param id Book instance ID.
|
||||||
* @param chapterId Chapter ID.
|
* @param chapterId Chapter ID.
|
||||||
|
* @param courseId Course ID.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved with last chapter viewed, undefined if none.
|
* @return Promise resolved with last chapter viewed, undefined if none.
|
||||||
*/
|
*/
|
||||||
async storeLastChapterViewed(id: number, chapterId: number, siteId?: string): Promise<void> {
|
async storeLastChapterViewed(id: number, chapterId: number, courseId: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
await this.lastChapterViewedTables[site.getId()].insert({ id, chapterid: chapterId });
|
await site.storeLastViewed(AddonModBookProvider.COMPONENT, id, chapterId, String(courseId));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
<!-- Buttons to add to the header. -->
|
<!-- Buttons to add to the header. -->
|
||||||
<core-navbar-buttons slot="end">
|
<core-navbar-buttons slot="end">
|
||||||
<ion-button *ngIf="!showLoading" (click)="showToc()" aria-haspopup="true" [attr.aria-label]="'addon.mod_imscp.toc' | translate">
|
|
||||||
<ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
|
|
||||||
<ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate">
|
<ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate">
|
||||||
<ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
@ -13,24 +9,35 @@
|
||||||
<core-loading [hideUntil]="!showLoading" class="safe-area-padding core-loading-full-height">
|
<core-loading [hideUntil]="!showLoading" class="safe-area-padding core-loading-full-height">
|
||||||
|
|
||||||
<!-- Activity info. -->
|
<!-- Activity info. -->
|
||||||
<core-course-module-info [module]="module">
|
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
|
||||||
|
[courseId]="courseId">
|
||||||
</core-course-module-info>
|
</core-course-module-info>
|
||||||
|
|
||||||
<ion-card class="core-warning-card" *ngIf="warning">
|
<ion-list>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
<ion-label>
|
||||||
<ion-label><span [innerHTML]="warning"></span></ion-label>
|
<h2>{{ 'addon.mod_imscp.toc' | translate }}</h2>
|
||||||
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-card>
|
|
||||||
|
|
||||||
<div class="addon-mod-imscp-container">
|
<ion-item class="ion-text-wrap" *ngFor="let item of items" (click)="openImscp(item.href)" button detail="true">
|
||||||
<core-iframe *ngIf="!showLoading" [src]="src" [showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true"></core-iframe>
|
<ion-label [class.core-bold]="!item.href">
|
||||||
</div>
|
<p>
|
||||||
|
<span class="ion-padding-start" *ngFor="let i of getNumberForPadding(item.level)"></span>
|
||||||
|
{{item.title}}
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
<div collapsible-footer *ngIf="!showLoading" slot="fixed">
|
<div collapsible-footer *ngIf="!showLoading" slot="fixed">
|
||||||
<!-- TODO Add a contents page to avoid having both bars. Please add here start/resume buttons. -->
|
<div class="list-item-limited-width">
|
||||||
<core-navigation-bar *ngIf="navigationItems.length > 1" [items]="navigationItems" (action)="loadItem($event)">
|
<ion-button class="ion-margin ion-text-wrap" expand="block" (click)="openImscp()">
|
||||||
</core-navigation-bar>
|
<span *ngIf="!hasStarted">{{ 'core.start' | translate }}</span>
|
||||||
|
<span *ngIf="hasStarted">{{ 'core.resume' | translate }}</span>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<core-course-module-navigation [courseId]="courseId" [currentModule]="module" (completionChanged)="onCompletionChange()">
|
<core-course-module-navigation [courseId]="courseId" [currentModule]="module" (completionChanged)="onCompletionChange()">
|
||||||
</core-course-module-navigation>
|
</core-course-module-navigation>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,14 +13,11 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, Optional } from '@angular/core';
|
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 { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
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 { AddonModImscpProvider, AddonModImscp, AddonModImscpTocItem } from '../../services/imscp';
|
||||||
import { AddonModImscpTocComponent } from '../toc/toc';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a IMSCP.
|
* Component that displays a IMSCP.
|
||||||
|
@ -33,13 +30,9 @@ import { AddonModImscpTocComponent } from '../toc/toc';
|
||||||
export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
|
export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
|
||||||
|
|
||||||
component = AddonModImscpProvider.COMPONENT;
|
component = AddonModImscpProvider.COMPONENT;
|
||||||
src = '';
|
|
||||||
warning = '';
|
|
||||||
navigationItems: CoreNavigationBarItem<AddonModImscpTocItem>[] = [];
|
|
||||||
|
|
||||||
protected items: AddonModImscpTocItem[] = [];
|
items: AddonModImscpTocItem[] = [];
|
||||||
protected currentHref?: string;
|
hasStarted = false;
|
||||||
protected displayDescription = false;
|
|
||||||
|
|
||||||
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
|
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
|
||||||
super('AddonModImscpIndexComponent', courseContentsPage);
|
super('AddonModImscpIndexComponent', courseContentsPage);
|
||||||
|
@ -73,85 +66,68 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected async fetchContent(refresh = false): Promise<void> {
|
protected async fetchContent(): Promise<void> {
|
||||||
const downloadResult = await this.downloadResourceIfNeeded(refresh);
|
await Promise.all([
|
||||||
|
this.loadImscp(),
|
||||||
const imscp = await AddonModImscp.getImscp(this.courseId, this.module.id);
|
this.loadTOC(),
|
||||||
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!) : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads an item.
|
* Load IMSCP data.
|
||||||
*
|
*
|
||||||
* @param itemHref Item Href.
|
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async loadItemHref(itemHref?: string): Promise<void> {
|
protected async loadImscp(): Promise<void> {
|
||||||
const src = await AddonModImscp.getIframeSrc(this.module, itemHref);
|
const imscp = await AddonModImscp.getImscp(this.courseId, this.module.id);
|
||||||
this.currentHref = itemHref;
|
|
||||||
|
|
||||||
this.navigationItems = this.items.map((item) => ({
|
this.dataRetrieved.emit(imscp);
|
||||||
item: item,
|
|
||||||
current: item.href == this.currentHref,
|
|
||||||
enabled: !!item.href,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (this.src && src == this.src) {
|
this.dataRetrieved.emit(imscp);
|
||||||
// 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 = '';
|
this.description = imscp.intro;
|
||||||
setTimeout(() => {
|
|
||||||
this.src = src;
|
const lastViewed = await AddonModImscp.getLastItemViewed(imscp.id);
|
||||||
});
|
this.hasStarted = lastViewed !== undefined;
|
||||||
} else {
|
|
||||||
this.src = src;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads an item.
|
* Load book TOC.
|
||||||
*
|
*
|
||||||
* @param item Item.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
loadItem(item: AddonModImscpTocItem): void {
|
protected async loadTOC(): Promise<void> {
|
||||||
this.loadItemHref(item.href);
|
// 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<void> {
|
openImscp(href?: string): void {
|
||||||
// Create the toc modal.
|
CoreNavigator.navigate('view', {
|
||||||
const modalData = await CoreDomUtils.openSideModal<string>({
|
params: {
|
||||||
component: AddonModImscpTocComponent,
|
cmId: this.module.id,
|
||||||
componentProps: {
|
courseId: this.courseId,
|
||||||
items: this.items,
|
initialHref: href,
|
||||||
selected: this.currentHref,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (modalData) {
|
this.hasStarted = true;
|
||||||
this.loadItemHref(modalData);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Get dummy array for padding.
|
||||||
|
*
|
||||||
|
* @param n Array length.
|
||||||
|
* @return Dummy array with n elements.
|
||||||
|
*/
|
||||||
|
getNumberForPadding(n: number): number[] {
|
||||||
|
return new Array(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,10 @@ const routes: Routes = [
|
||||||
path: ':courseId/:cmId',
|
path: ':courseId/:cmId',
|
||||||
component: AddonModImscpIndexPage,
|
component: AddonModImscpIndexPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ':courseId/:cmId/view',
|
||||||
|
loadChildren: () => import('./pages/view/view.module').then(m => m.AddonModImscpViewPageModule),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>
|
||||||
|
<h1>
|
||||||
|
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</h1>
|
||||||
|
</ion-title>
|
||||||
|
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button *ngIf="loaded" (click)="showToc()" aria-haspopup="true" [attr.aria-label]="'addon.mod_imscp.toc' | translate">
|
||||||
|
<ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<!-- Content. -->
|
||||||
|
<core-loading [hideUntil]="loaded" class="safe-area-padding core-loading-full-height">
|
||||||
|
|
||||||
|
<ion-card class="core-warning-card" *ngIf="warning">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-label><span [innerHTML]="warning"></span></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<div class="addon-mod-imscp-container">
|
||||||
|
<core-iframe *ngIf="loaded" [src]="src" [showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true"></core-iframe>
|
||||||
|
</div>
|
||||||
|
</core-loading>
|
||||||
|
|
||||||
|
<core-navigation-bar collapsible-footer *ngIf="loaded && navigationItems.length > 1 && false" [items]="navigationItems"
|
||||||
|
(action)="loadItem($event)">
|
||||||
|
</core-navigation-bar>
|
||||||
|
</ion-content>
|
|
@ -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 {}
|
|
@ -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<AddonModImscpTocItem>[] = [];
|
||||||
|
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<void> {
|
||||||
|
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<CoreCourseResourceDownloadResult> {
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
// Create the toc modal.
|
||||||
|
const itemHref = await CoreDomUtils.openSideModal<string>({
|
||||||
|
component: AddonModImscpTocComponent,
|
||||||
|
componentProps: {
|
||||||
|
items: this.items,
|
||||||
|
selected: this.currentHref,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (itemHref) {
|
||||||
|
this.loadItemHref(itemHref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -168,29 +168,21 @@ export class AddonModImscpProvider {
|
||||||
* Get src of a imscp item.
|
* Get src of a imscp item.
|
||||||
*
|
*
|
||||||
* @param module The module object.
|
* @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.
|
* @return Promise resolved with the item src.
|
||||||
*/
|
*/
|
||||||
async getIframeSrc(module: CoreCourseModuleData, itemHref?: string): Promise<string> {
|
async getIframeSrc(module: CoreCourseModuleData, itemHref: string): Promise<string> {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const siteId = CoreSites.getCurrentSiteId();
|
const siteId = CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module.url!);
|
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module.url || '');
|
||||||
|
|
||||||
return CoreTextUtils.concatenatePaths(dirPath, itemHref);
|
return CoreTextUtils.concatenatePaths(dirPath, itemHref);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error getting directory, there was an error downloading or we're in browser. Return online URL if connected.
|
// Error getting directory, there was an error downloading or we're in browser. Return online URL if connected.
|
||||||
if (CoreApp.isOnline()) {
|
if (CoreApp.isOnline()) {
|
||||||
|
const contents = await CoreCourse.getModuleContents(module);
|
||||||
|
|
||||||
const indexUrl = this.getFileUrlFromContents(contents, itemHref);
|
const indexUrl = this.getFileUrlFromContents(contents, itemHref);
|
||||||
|
|
||||||
if (indexUrl) {
|
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<string | undefined> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
const entry = await site.getLastViewed(AddonModImscpProvider.COMPONENT, id);
|
||||||
|
|
||||||
|
return entry?.value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate the prefetched content.
|
* 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<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.storeLastViewed(AddonModImscpProvider.COMPONENT, id, href, String(courseId));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
export const AddonModImscp = makeSingleton(AddonModImscpProvider);
|
export const AddonModImscp = makeSingleton(AddonModImscpProvider);
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ export class CoreSite {
|
||||||
// Variables for the database.
|
// Variables for the database.
|
||||||
static readonly WS_CACHE_TABLE = 'wscache_2';
|
static readonly WS_CACHE_TABLE = 'wscache_2';
|
||||||
static readonly CONFIG_TABLE = 'core_site_config';
|
static readonly CONFIG_TABLE = 'core_site_config';
|
||||||
|
static readonly LAST_VIEWED_TABLE = 'core_site_last_viewed';
|
||||||
|
|
||||||
static readonly MINIMUM_MOODLE_VERSION = '3.5';
|
static readonly MINIMUM_MOODLE_VERSION = '3.5';
|
||||||
|
|
||||||
|
@ -110,6 +111,7 @@ export class CoreSite {
|
||||||
protected db?: SQLiteDB;
|
protected db?: SQLiteDB;
|
||||||
protected cacheTable: AsyncInstance<CoreDatabaseTable<CoreSiteWSCacheRecord>>;
|
protected cacheTable: AsyncInstance<CoreDatabaseTable<CoreSiteWSCacheRecord>>;
|
||||||
protected configTable: AsyncInstance<CoreDatabaseTable<CoreSiteConfigDBRecord, 'name'>>;
|
protected configTable: AsyncInstance<CoreDatabaseTable<CoreSiteConfigDBRecord, 'name'>>;
|
||||||
|
protected lastViewedTable: AsyncInstance<CoreDatabaseTable<CoreSiteLastViewedDBRecord, 'component' | 'id'>>;
|
||||||
protected cleanUnicode = false;
|
protected cleanUnicode = false;
|
||||||
protected lastAutoLogin = 0;
|
protected lastAutoLogin = 0;
|
||||||
protected offlineDisabled = false;
|
protected offlineDisabled = false;
|
||||||
|
@ -154,6 +156,12 @@ export class CoreSite {
|
||||||
config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
|
||||||
primaryKeyColumns: ['name'],
|
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.setInfo(infos);
|
||||||
this.calculateOfflineDisabled();
|
this.calculateOfflineDisabled();
|
||||||
|
|
||||||
|
@ -1955,6 +1963,49 @@ export class CoreSite {
|
||||||
return this.containsUrl(url);
|
return this.containsUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes last viewed records based on some conditions.
|
||||||
|
*
|
||||||
|
* @param conditions Conditions.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async deleteLastViewed(conditions?: Partial<CoreSiteLastViewedDBRecord>): Promise<void> {
|
||||||
|
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<CoreSiteLastViewedDBRecord | undefined> {
|
||||||
|
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<void> {
|
||||||
|
await this.lastViewedTable.insert({
|
||||||
|
component,
|
||||||
|
id,
|
||||||
|
value: String(value),
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2281,3 +2332,10 @@ export type CoreSiteWSCacheRecord = {
|
||||||
component?: string;
|
component?: string;
|
||||||
componentId?: number;
|
componentId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CoreSiteLastViewedDBRecord = {
|
||||||
|
component: string;
|
||||||
|
id: number;
|
||||||
|
value: string;
|
||||||
|
data?: string;
|
||||||
|
};
|
||||||
|
|
|
@ -78,7 +78,7 @@ export const APP_SCHEMA: CoreAppSchema = {
|
||||||
// Schema to register for Site DB.
|
// Schema to register for Site DB.
|
||||||
export const SITE_SCHEMA: CoreSiteSchema = {
|
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||||
name: 'CoreSitesProvider',
|
name: 'CoreSitesProvider',
|
||||||
version: 2,
|
version: 3,
|
||||||
canBeCleared: [CoreSite.WS_CACHE_TABLE],
|
canBeCleared: [CoreSite.WS_CACHE_TABLE],
|
||||||
tables: [
|
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<void> {
|
async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
|
||||||
if (oldVersion < 2) {
|
if (oldVersion < 2) {
|
||||||
|
|
Loading…
Reference in New Issue