MOBILE-3980 book: Support resuming a book

main
Dani Palou 2022-02-16 08:18:36 +01:00
parent 5f3e7bb0b5
commit 1b67036aca
10 changed files with 162 additions and 26 deletions

View File

@ -450,7 +450,6 @@
"addon.mod_bigbluebuttonbn.view_message_session_started_at": "bigbluebuttonbn", "addon.mod_bigbluebuttonbn.view_message_session_started_at": "bigbluebuttonbn",
"addon.mod_bigbluebuttonbn.view_message_viewer": "bigbluebuttonbn", "addon.mod_bigbluebuttonbn.view_message_viewer": "bigbluebuttonbn",
"addon.mod_bigbluebuttonbn.view_message_viewers": "bigbluebuttonbn", "addon.mod_bigbluebuttonbn.view_message_viewers": "bigbluebuttonbn",
"addon.mod_book.book:read": "book",
"addon.mod_book.errorchapter": "book", "addon.mod_book.errorchapter": "book",
"addon.mod_book.modulenameplural": "book", "addon.mod_book.modulenameplural": "book",
"addon.mod_book.navnexttitle": "book", "addon.mod_book.navnexttitle": "book",
@ -2127,6 +2126,7 @@
"core.resources": "moodle", "core.resources": "moodle",
"core.restore": "moodle", "core.restore": "moodle",
"core.restricted": "moodle", "core.restricted": "moodle",
"core.resume": "local_moodlemobileapp",
"core.retry": "local_moodlemobileapp", "core.retry": "local_moodlemobileapp",
"core.save": "moodle", "core.save": "moodle",
"core.savechanges": "assign", "core.savechanges": "assign",
@ -2253,7 +2253,7 @@
"core.sorry": "local_moodlemobileapp", "core.sorry": "local_moodlemobileapp",
"core.sort": "moodle", "core.sort": "moodle",
"core.sortby": "moodle", "core.sortby": "moodle",
"core.start": "grouptool", "core.start": "local_moodlemobileapp",
"core.storingfiles": "local_moodlemobileapp", "core.storingfiles": "local_moodlemobileapp",
"core.strftimedate": "langconfig", "core.strftimedate": "langconfig",
"core.strftimedatefullshort": "langconfig", "core.strftimedatefullshort": "langconfig",

View File

@ -19,7 +19,7 @@ import { CoreUtils } from '@services/utils/utils';
import { AddonCalendar, AddonCalendarEventType, AddonCalendarProvider } from '../calendar'; import { AddonCalendar, AddonCalendarEventType, AddonCalendarProvider } from '../calendar';
/** /**
* Database variables for AddonDatabase service. * Database variables for AddonCalendarProvider service.
*/ */
export const EVENTS_TABLE = 'addon_calendar_events_3'; export const EVENTS_TABLE = 'addon_calendar_events_3';
export const REMINDERS_TABLE = 'addon_calendar_reminders'; export const REMINDERS_TABLE = 'addon_calendar_reminders';

View File

@ -26,6 +26,8 @@ import { AddonModBookListLinkHandler } from './services/handlers/list-link';
import { AddonModBookPrefetchHandler } from './services/handlers/prefetch'; import { AddonModBookPrefetchHandler } from './services/handlers/prefetch';
import { AddonModBookTagAreaHandler } from './services/handlers/tag-area'; import { AddonModBookTagAreaHandler } from './services/handlers/tag-area';
import { AddonModBookProvider } from './services/book'; import { AddonModBookProvider } from './services/book';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { BOOK_SITE_SCHEMA } from './services/database/book';
export const ADDON_MOD_BOOK_SERVICES: Type<unknown>[] = [ export const ADDON_MOD_BOOK_SERVICES: Type<unknown>[] = [
AddonModBookProvider, AddonModBookProvider,
@ -44,6 +46,11 @@ const routes: Routes = [
AddonModBookComponentsModule, AddonModBookComponentsModule,
], ],
providers: [ providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [BOOK_SITE_SCHEMA],
multi: true,
},
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
multi: true, multi: true,

View File

@ -45,7 +45,8 @@
</ion-item> </ion-item>
<ion-button class="ion-margin" expand="block" (click)="openBook()"> <ion-button class="ion-margin" expand="block" (click)="openBook()">
{{ 'addon.mod_book.book:read' | translate }} <span *ngIf="!hasStartedBook">{{ 'core.start' | translate }}</span>
<span *ngIf="hasStartedBook">{{ 'core.resume' | translate }}</span>
</ion-button> </ion-button>
</ion-list> </ion-list>

View File

@ -32,6 +32,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
addPadding = true; addPadding = true;
showBullets = false; showBullets = false;
chapters: AddonModBookTocChapter[] = []; chapters: AddonModBookTocChapter[] = [];
hasStartedBook = false;
protected book?: AddonModBookBookWSData; protected book?: AddonModBookBookWSData;
@ -53,23 +54,43 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
*/ */
protected async fetchContent(refresh?: boolean): Promise<void> { protected async fetchContent(refresh?: boolean): Promise<void> {
try { try {
await Promise.all([
this.loadBook(),
this.loadTOC(),
]);
} finally {
this.fillContextMenu(refresh);
}
}
/**
* Load book data.
*
* @return Promise resolved when done.
*/
protected async loadBook(): Promise<void> {
this.book = await AddonModBook.getBook(this.courseId, this.module.id); this.book = await AddonModBook.getBook(this.courseId, this.module.id);
if (this.book) {
this.dataRetrieved.emit(this.book); this.dataRetrieved.emit(this.book);
this.description = this.book.intro; this.description = this.book.intro;
this.showNumbers = this.book.numbering == AddonModBookNumbering.NUMBERS; this.showNumbers = this.book.numbering == AddonModBookNumbering.NUMBERS;
this.showBullets = this.book.numbering == AddonModBookNumbering.BULLETS; this.showBullets = this.book.numbering == AddonModBookNumbering.BULLETS;
this.addPadding = this.book.numbering != AddonModBookNumbering.NONE; this.addPadding = this.book.numbering != AddonModBookNumbering.NONE;
const lastChapterViewed = await AddonModBook.getLastChapterViewed(this.book.id);
this.hasStartedBook = lastChapterViewed !== undefined;
} }
/**
* Load book TOC.
*
* @return Promise resolved when done.
*/
protected async loadTOC(): Promise<void> {
const contents = await CoreCourse.getModuleContents(this.module, this.courseId); const contents = await CoreCourse.getModuleContents(this.module, this.courseId);
this.chapters = AddonModBook.getTocList(contents); this.chapters = AddonModBook.getTocList(contents);
} finally {
this.fillContextMenu(refresh);
}
} }
/** /**
@ -85,6 +106,8 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
chapterId, chapterId,
}, },
}); });
this.hasStartedBook = true;
} }
/** /**

View File

@ -1,5 +1,4 @@
{ {
"book:read": "View book",
"errorchapter": "Error reading chapter of book.", "errorchapter": "Error reading chapter of book.",
"modulenameplural": "Books", "modulenameplural": "Books",
"navnexttitle": "Next: {{$a}}", "navnexttitle": "Next: {{$a}}",

View File

@ -133,13 +133,9 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy {
const downloadResult = await this.downloadResourceIfNeeded(module, refresh); const downloadResult = await this.downloadResourceIfNeeded(module, refresh);
if (book) {
this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY; this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY;
this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT; this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT;
this.title = book.name; this.title = book.name;
} else {
this.title = this.title || Translate.instant('addon.mod_book.book:read');
}
// Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.
await source.loadContents(); await source.loadContents();
@ -283,6 +279,10 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy {
this.navigationItems = this.getNavigationItems(chapterId); this.navigationItems = this.getNavigationItems(chapterId);
} }
if (this.book) {
AddonModBook.storeLastChapterViewed(this.book.id, chapterId);
}
if (!this.module) { if (!this.module) {
return; return;
} }
@ -371,6 +371,15 @@ class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSo
this.module = await CoreCourse.getModule(this.CM_ID, this.COURSE_ID); this.module = await CoreCourse.getModule(this.CM_ID, this.COURSE_ID);
this.book = await AddonModBook.getBook(this.COURSE_ID, this.CM_ID); this.book = await AddonModBook.getBook(this.COURSE_ID, this.CM_ID);
if (!this.initialItem) {
// No chapter ID specified. Calculate last viewed.
const lastViewed = await AddonModBook.getLastChapterViewed(this.book.id);
if (lastViewed) {
this.initialItem = { id: lastViewed };
}
}
return { return {
module: this.module, module: this.module,
book: this.book, book: this.book,

View File

@ -26,6 +26,11 @@ 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.
@ -56,6 +61,19 @@ 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 },
}),
),
);
}
/** /**
* Get a book by course module ID. * Get a book by course module ID.
* *
@ -216,6 +234,24 @@ export class AddonModBookProvider {
return chapters[0].id; return chapters[0].id;
} }
/**
* Get last chapter viewed in the app for a book.
*
* @param id Book instance ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with last chapter viewed, undefined if none.
*/
async getLastChapterViewed(id: number, siteId?: string): Promise<number | undefined> {
try {
const site = await CoreSites.getSite(siteId);
const entry = await this.lastChapterViewedTables[site.getId()].getOneByPrimaryKey({ id });
return entry.chapterid;
} catch {
// No last chapter viewed.
}
}
/** /**
* Get the book toc as an array. * Get the book toc as an array.
* *
@ -363,6 +399,20 @@ export class AddonModBookProvider {
); );
} }
/**
* Store last chapter viewed in the app for a book.
*
* @param id Book instance ID.
* @param chapterId Chapter 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<void> {
const site = await CoreSites.getSite(siteId);
await this.lastChapterViewedTables[site.getId()].insert({ id, chapterid: chapterId });
}
} }
export const AddonModBook = makeSingleton(AddonModBookProvider); export const AddonModBook = makeSingleton(AddonModBookProvider);

View File

@ -0,0 +1,46 @@
// (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 { CoreSiteSchema } from '@services/sites';
/**
* Database variables for AddonModBookProvider service.
*/
export const LAST_CHAPTER_VIEWED_TABLE = 'addon_mod_book_last_chapter_viewed';
export const BOOK_SITE_SCHEMA: CoreSiteSchema = {
name: 'AddonModBookProvider',
version: 1,
tables: [
{
name: LAST_CHAPTER_VIEWED_TABLE,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true,
},
{
name: 'chapterid',
type: 'INTEGER',
notNull: true,
},
],
},
],
};
export type AddonModBookLastChapterViewedDBRecord = {
id: number;
chapterid: number;
};

View File

@ -248,6 +248,7 @@
"restore": "Restore", "restore": "Restore",
"restricted": "Restricted", "restricted": "Restricted",
"retry": "Retry", "retry": "Retry",
"resume": "Resume",
"save": "Save", "save": "Save",
"savechanges": "Save changes", "savechanges": "Save changes",
"scanqr": "Scan QR code", "scanqr": "Scan QR code",