commit
0af6865703
|
@ -2680,6 +2680,19 @@
|
||||||
"core.viewcode": "local_moodlemobileapp",
|
"core.viewcode": "local_moodlemobileapp",
|
||||||
"core.vieweditor": "local_moodlemobileapp",
|
"core.vieweditor": "local_moodlemobileapp",
|
||||||
"core.viewembeddedcontent": "local_moodlemobileapp",
|
"core.viewembeddedcontent": "local_moodlemobileapp",
|
||||||
|
"core.viewer.decreasetextsize": "local_moodlemobileapp",
|
||||||
|
"core.viewer.default": "moodle",
|
||||||
|
"core.viewer.enterreadingmode": "local_moodlemobileapp",
|
||||||
|
"core.viewer.exitreadingmode": "local_moodlemobileapp",
|
||||||
|
"core.viewer.increasetextsize": "local_moodlemobileapp",
|
||||||
|
"core.viewer.openreadingmodesettings": "local_moodlemobileapp",
|
||||||
|
"core.viewer.readingthemeauto": "local_moodlemobileapp",
|
||||||
|
"core.viewer.readingthemedark": "local_moodlemobileapp/core.settings.colorscheme-dark",
|
||||||
|
"core.viewer.readingthemehcm": "local_moodlemobileapp",
|
||||||
|
"core.viewer.readingthemelight": "local_moodlemobileapp/core.settings.colorscheme-light",
|
||||||
|
"core.viewer.readingthemesepia": "local_moodlemobileapp",
|
||||||
|
"core.viewer.showmedia": "zoom",
|
||||||
|
"core.viewer.theme": "moodle",
|
||||||
"core.viewprofile": "moodle",
|
"core.viewprofile": "moodle",
|
||||||
"core.wanttochangesite": "local_moodlemobileapp",
|
"core.wanttochangesite": "local_moodlemobileapp",
|
||||||
"core.warningofflinedatadeleted": "local_moodlemobileapp",
|
"core.warningofflinedatadeleted": "local_moodlemobileapp",
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<div class="safe-area-padding-horizontal core-swipe-slides-container">
|
<div class="safe-area-padding-horizontal core-swipe-slides-container">
|
||||||
<core-swipe-slides [manager]="manager" [options]="swiperOpts">
|
<core-swipe-slides [manager]="manager" [options]="swiperOpts">
|
||||||
<ng-template let-chapter="item" let-active="active">
|
<ng-template let-chapter="item" let-active="active">
|
||||||
<div class="ion-padding">
|
<div class="ion-padding" core-reading-mode>
|
||||||
<core-format-text [component]="component" [componentId]="cmId" [text]="chapter.content" contextLevel="module"
|
<core-format-text [component]="component" [componentId]="cmId" [text]="chapter.content" contextLevel="module"
|
||||||
[contextInstanceId]="cmId" [courseId]="courseId" [disabled]="!active" />
|
[contextInstanceId]="cmId" [courseId]="courseId" [disabled]="!active" />
|
||||||
<div class="ion-margin-top" *ngIf="chapter.tags?.length > 0">
|
<div class="ion-margin-top" *ngIf="chapter.tags?.length > 0">
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
@ -8,6 +8,7 @@
|
||||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId" />
|
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId" />
|
||||||
</h1>
|
</h1>
|
||||||
</ion-title>
|
</ion-title>
|
||||||
|
<ion-buttons slot="end" />
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content [core-swipe-navigation]="entries" class="limited-width">
|
<ion-content [core-swipe-navigation]="entries" class="limited-width">
|
||||||
|
@ -23,72 +24,77 @@
|
||||||
<ion-label>{{ 'core.hasdatatosync' | translate: { $a: 'addon.mod_glossary.entry' | translate } }}</ion-label>
|
<ion-label>{{ 'core.hasdatatosync' | translate: { $a: 'addon.mod_glossary.entry' | translate } }}</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
<div core-reading-mode>
|
||||||
<core-user-avatar [user]="entry" slot="start" />
|
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
||||||
<ion-label>
|
<core-user-avatar [user]="entry" slot="start" />
|
||||||
<h2>
|
<ion-label>
|
||||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
|
<h2>
|
||||||
[courseId]="courseId" />
|
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
|
||||||
</h2>
|
[courseId]="courseId" />
|
||||||
<p *ngIf="onlineEntry">{{ onlineEntry.userfullname }}</p>
|
</h2>
|
||||||
</ion-label>
|
<p *ngIf="onlineEntry">{{ onlineEntry.userfullname }}</p>
|
||||||
<ion-note slot="end" *ngIf="showDate && onlineEntry">{{ onlineEntry.timemodified | coreDateDayOrTime }}</ion-note>
|
</ion-label>
|
||||||
</ion-item>
|
<ion-note slot="end" *ngIf="showDate && onlineEntry">{{ onlineEntry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
|
</ion-item>
|
||||||
<ion-label>
|
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
|
||||||
<p class="item-heading">
|
<ion-label>
|
||||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId" />
|
<p class="item-heading">
|
||||||
</p>
|
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId" />
|
||||||
</ion-label>
|
</p>
|
||||||
<ion-note slot="end" *ngIf="showDate && onlineEntry">{{ onlineEntry.timemodified | coreDateDayOrTime }}</ion-note>
|
</ion-label>
|
||||||
</ion-item>
|
<ion-note slot="end" *ngIf="showDate && onlineEntry">{{ onlineEntry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||||
<ion-item class="ion-text-wrap">
|
</ion-item>
|
||||||
<ion-label>
|
<ion-item class="ion-text-wrap">
|
||||||
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition" contextLevel="module"
|
<ion-label>
|
||||||
[contextInstanceId]="componentId" [courseId]="courseId" />
|
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition"
|
||||||
</ion-label>
|
contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId" />
|
||||||
</ion-item>
|
</ion-label>
|
||||||
<div *ngIf="onlineEntry && onlineEntry.attachment">
|
</ion-item>
|
||||||
<core-file *ngFor="let file of onlineEntry.attachments" [file]="file" [component]="component" [componentId]="componentId" />
|
<div *ngIf="onlineEntry && onlineEntry.attachment">
|
||||||
</div>
|
<core-file *ngFor="let file of onlineEntry.attachments" [file]="file" [component]="component"
|
||||||
<div *ngIf="offlineEntry && offlineEntry.attachments">
|
[componentId]="componentId" />
|
||||||
<core-file *ngFor="let file of offlineEntry.attachments.online" [file]="file" [component]="component"
|
|
||||||
[componentId]="componentId" />
|
|
||||||
</div>
|
|
||||||
<div *ngIf="offlineEntry && offlineEntryFiles">
|
|
||||||
<core-local-file *ngFor="let file of offlineEntryFiles" [file]="file" />
|
|
||||||
</div>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="onlineEntry && tagsEnabled && entry && onlineEntry.tags && onlineEntry.tags.length > 0">
|
|
||||||
<ion-label>
|
|
||||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
|
||||||
<core-tag-list [tags]="onlineEntry.tags" />
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item *ngIf="canDelete || canEdit">
|
|
||||||
<div slot="end">
|
|
||||||
<ion-button *ngIf="canDelete" fill="clear" color="danger" (click)="deleteEntry()"
|
|
||||||
[ariaLabel]="'addon.mod_glossary.deleteentry' | translate">
|
|
||||||
<ion-icon slot="icon-only" name="fas-trash" aria-hidden="true" />
|
|
||||||
</ion-button>
|
|
||||||
<ion-button *ngIf="canEdit" fill="clear" (click)="editEntry()" [ariaLabel]="'addon.mod_glossary.editentry' | translate">
|
|
||||||
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true" />
|
|
||||||
</ion-button>
|
|
||||||
</div>
|
</div>
|
||||||
</ion-item>
|
<div *ngIf="offlineEntry && offlineEntry.attachments">
|
||||||
<ion-item class="ion-text-wrap" *ngIf="onlineEntry && !onlineEntry.approved">
|
<core-file *ngFor="let file of offlineEntry.attachments.online" [file]="file" [component]="component"
|
||||||
<ion-label>
|
[componentId]="componentId" />
|
||||||
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
|
</div>
|
||||||
</ion-label>
|
<div *ngIf="offlineEntry && offlineEntryFiles">
|
||||||
</ion-item>
|
<core-local-file *ngFor="let file of offlineEntryFiles" [file]="file" />
|
||||||
<core-comments *ngIf="glossary && glossary.allowcomments && onlineEntry && onlineEntry.id > 0 && commentsEnabled"
|
</div>
|
||||||
contextLevel="module" [instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="onlineEntry.id"
|
<ion-item class="ion-text-wrap"
|
||||||
area="glossary_entry" [courseId]="glossary.course" [showItem]="true" />
|
*ngIf="onlineEntry && tagsEnabled && entry && onlineEntry.tags && onlineEntry.tags.length > 0">
|
||||||
<core-rating-rate *ngIf="glossary && ratingInfo && onlineEntry" [ratingInfo]="ratingInfo" contextLevel="module"
|
<ion-label>
|
||||||
[instanceId]="glossary.coursemodule" [itemId]="onlineEntry.id" [itemSetId]="0" [courseId]="glossary.course"
|
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||||
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()" />
|
<core-tag-list [tags]="onlineEntry.tags" />
|
||||||
<core-rating-aggregate *ngIf="glossary && ratingInfo && onlineEntry" [ratingInfo]="ratingInfo" contextLevel="module"
|
</ion-label>
|
||||||
[instanceId]="glossary.coursemodule" [itemId]="onlineEntry.id" [courseId]="glossary.course"
|
</ion-item>
|
||||||
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" />
|
<ion-item *ngIf="canDelete || canEdit">
|
||||||
|
<div slot="end">
|
||||||
|
<ion-button *ngIf="canDelete" fill="clear" color="danger" (click)="deleteEntry()"
|
||||||
|
[ariaLabel]="'addon.mod_glossary.deleteentry' | translate">
|
||||||
|
<ion-icon slot="icon-only" name="fas-trash" aria-hidden="true" />
|
||||||
|
</ion-button>
|
||||||
|
<ion-button *ngIf="canEdit" fill="clear" (click)="editEntry()"
|
||||||
|
[ariaLabel]="'addon.mod_glossary.editentry' | translate">
|
||||||
|
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true" />
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="onlineEntry && !onlineEntry.approved">
|
||||||
|
<ion-label>
|
||||||
|
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<core-comments *ngIf="glossary && glossary.allowcomments && onlineEntry && onlineEntry.id > 0 && commentsEnabled"
|
||||||
|
contextLevel="module" [instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="onlineEntry.id"
|
||||||
|
area="glossary_entry" [courseId]="glossary.course" [showItem]="true" />
|
||||||
|
<core-rating-rate *ngIf="glossary && ratingInfo && onlineEntry" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||||
|
[instanceId]="glossary.coursemodule" [itemId]="onlineEntry.id" [itemSetId]="0" [courseId]="glossary.course"
|
||||||
|
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()" />
|
||||||
|
<core-rating-aggregate *ngIf="glossary && ratingInfo && onlineEntry" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||||
|
[instanceId]="glossary.coursemodule" [itemId]="onlineEntry.id" [courseId]="glossary.course"
|
||||||
|
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" />
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ion-card *ngIf="!entry" class="core-warning-card">
|
<ion-card *ngIf="!entry" class="core-warning-card">
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<core-course-module-info [module]="module" [description]="displayDescription && description" [component]="component"
|
<core-course-module-info [module]="module" [description]="displayDescription && description" [component]="component"
|
||||||
[componentId]="componentId" [courseId]="courseId" (completionChanged)="onCompletionChange()" />
|
[componentId]="componentId" [courseId]="courseId" (completionChanged)="onCompletionChange()" />
|
||||||
|
|
||||||
<div class="ion-padding">
|
<div class="ion-padding" core-reading-mode>
|
||||||
<core-format-text [component]="component" [componentId]="componentId" [text]="contents" contextLevel="module"
|
<core-format-text [component]="component" [componentId]="componentId" [text]="contents" contextLevel="module"
|
||||||
[contextInstanceId]="module.id" [courseId]="courseId" />
|
[contextInstanceId]="module.id" [courseId]="courseId" />
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const header = await this.searchHeader();
|
const header = await CoreDom.findIonHeaderFromElement(this.element);
|
||||||
if (header) {
|
if (header) {
|
||||||
// Search the right buttons container (start, end or any).
|
// Search the right buttons container (start, end or any).
|
||||||
let selector = 'ion-buttons';
|
let selector = 'ion-buttons';
|
||||||
|
@ -192,43 +192,6 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||||
return componentRef.instance;
|
return componentRef.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Search the ion-header where the buttons should be added.
|
|
||||||
*
|
|
||||||
* @returns Promise resolved with the header element.
|
|
||||||
*/
|
|
||||||
protected async searchHeader(): Promise<HTMLIonHeaderElement> {
|
|
||||||
await CoreDom.waitToBeInDOM(this.element);
|
|
||||||
let parentPage: HTMLElement | null = this.element;
|
|
||||||
|
|
||||||
while (parentPage && parentPage.parentElement) {
|
|
||||||
const content = parentPage.closest<HTMLIonContentElement>('ion-content');
|
|
||||||
if (content) {
|
|
||||||
// Sometimes ion-page class is not yet added by the ViewController, wait for content to render.
|
|
||||||
await content.componentOnReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
parentPage = parentPage.parentElement.closest('.ion-page, .ion-page-hidden, .ion-page-invisible');
|
|
||||||
|
|
||||||
// Check if the page has a header. If it doesn't, search the next parent page.
|
|
||||||
let header = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
|
||||||
|
|
||||||
if (header && getComputedStyle(header).display !== 'none') {
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find using content if any.
|
|
||||||
header = content?.parentElement?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
|
||||||
|
|
||||||
if (header && getComputedStyle(header).display !== 'none') {
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header not found, reject.
|
|
||||||
throw Error('Header not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show or hide all the elements.
|
* Show or hide all the elements.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -100,6 +100,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
||||||
|
|
||||||
constructor(el: ElementRef) {
|
constructor(el: ElementRef) {
|
||||||
this.collapsedHeader = el.nativeElement;
|
this.collapsedHeader = el.nativeElement;
|
||||||
|
CoreDirectivesRegistry.register(this.collapsedHeader, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { CoreContentDirective } from './content';
|
||||||
import { CoreUpdateNonReactiveAttributesDirective } from './update-non-reactive-attributes';
|
import { CoreUpdateNonReactiveAttributesDirective } from './update-non-reactive-attributes';
|
||||||
import { CoreUserTourDirective } from './user-tour';
|
import { CoreUserTourDirective } from './user-tour';
|
||||||
import { CoreIonDatetimeDirective } from './datetime';
|
import { CoreIonDatetimeDirective } from './datetime';
|
||||||
|
import { CoreReadingModeDirective } from './reading-mode';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -59,6 +60,7 @@ import { CoreIonDatetimeDirective } from './datetime';
|
||||||
CoreUpdateNonReactiveAttributesDirective,
|
CoreUpdateNonReactiveAttributesDirective,
|
||||||
CoreUserTourDirective,
|
CoreUserTourDirective,
|
||||||
CoreIonDatetimeDirective,
|
CoreIonDatetimeDirective,
|
||||||
|
CoreReadingModeDirective,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CoreAutoFocusDirective,
|
CoreAutoFocusDirective,
|
||||||
|
@ -82,6 +84,7 @@ import { CoreIonDatetimeDirective } from './datetime';
|
||||||
CoreUpdateNonReactiveAttributesDirective,
|
CoreUpdateNonReactiveAttributesDirective,
|
||||||
CoreUserTourDirective,
|
CoreUserTourDirective,
|
||||||
CoreIonDatetimeDirective,
|
CoreIonDatetimeDirective,
|
||||||
|
CoreReadingModeDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreDirectivesModule {}
|
export class CoreDirectivesModule {}
|
||||||
|
|
|
@ -67,7 +67,7 @@ import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||||
* Please use this directive if your text needs to be filtered or it can contain links or media (images, audio, video).
|
* Please use this directive if your text needs to be filtered or it can contain links or media (images, audio, video).
|
||||||
*
|
*
|
||||||
* Example usage:
|
* Example usage:
|
||||||
* <core-format-text [text]="myText" [component]="component" [componentId]="componentId"></core-format-text>
|
* <core-format-text [text]="myText" [component]="component" [componentId]="componentId" />
|
||||||
*/
|
*/
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: 'core-format-text',
|
selector: 'core-format-text',
|
||||||
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
// (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 {
|
||||||
|
AfterViewInit,
|
||||||
|
Directive,
|
||||||
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreIcons } from '@singletons/icons';
|
||||||
|
import { CoreDom } from '@singletons/dom';
|
||||||
|
import { CoreWait } from '@singletons/wait';
|
||||||
|
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
||||||
|
import { CoreModals } from '@services/modals';
|
||||||
|
import { CoreViewer } from '@features/viewer/services/viewer';
|
||||||
|
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||||
|
import { CoreCollapsibleHeaderDirective } from './collapsible-header';
|
||||||
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive to add the reading mode to the selected html tag.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* <div core-reading-mode>
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: '[core-reading-mode]',
|
||||||
|
})
|
||||||
|
export class CoreReadingModeDirective implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
|
protected element: HTMLElement;
|
||||||
|
protected viewportPromise?: CoreCancellablePromise<void>;
|
||||||
|
protected disabledStyles: HTMLStyleElement[] = [];
|
||||||
|
protected hiddenElements: HTMLElement[] = [];
|
||||||
|
protected renamedStyles: HTMLElement[] = [];
|
||||||
|
protected enabled = false;
|
||||||
|
protected header?: CoreCollapsibleHeaderDirective;
|
||||||
|
protected logger = CoreLogger.getInstance('CoreReadingModeDirective');
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
element: ElementRef,
|
||||||
|
) {
|
||||||
|
this.element = element.nativeElement;
|
||||||
|
this.viewportPromise = CoreDom.waitToBeInViewport(this.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngAfterViewInit(): Promise<void> {
|
||||||
|
await this.viewportPromise;
|
||||||
|
await CoreWait.nextTick();
|
||||||
|
await this.addTextViewerButton();
|
||||||
|
|
||||||
|
this.enabled = document.body.classList.contains('core-reading-mode-enabled');
|
||||||
|
if (this.enabled) {
|
||||||
|
await this.enterReadingMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add text viewer button to enable the reading mode.
|
||||||
|
*/
|
||||||
|
protected async addTextViewerButton(): Promise<void> {
|
||||||
|
const page = CoreDom.closest(this.element, '.ion-page');
|
||||||
|
const contentEl = page?.querySelector('ion-content') ?? undefined;
|
||||||
|
|
||||||
|
const header = await CoreDom.findIonHeaderFromElement(this.element);
|
||||||
|
const buttonsContainer = header?.querySelector<HTMLIonButtonsElement>('ion-toolbar ion-buttons[slot="end"]');
|
||||||
|
if (!buttonsContainer || !contentEl) {
|
||||||
|
this.logger.warn('The header was not found, or it didn\'t have any ion-buttons on slot end.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentEl.classList.add('core-reading-mode-content');
|
||||||
|
|
||||||
|
if (buttonsContainer.querySelector('.core-text-viewer-button')) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collapsibleHeader = CoreDirectivesRegistry.resolve(header, CoreCollapsibleHeaderDirective);
|
||||||
|
if (collapsibleHeader) {
|
||||||
|
this.header = collapsibleHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = Translate.instant('core.viewer.enterreadingmode');
|
||||||
|
const button = document.createElement('ion-button');
|
||||||
|
|
||||||
|
button.classList.add('core-text-viewer-button');
|
||||||
|
button.setAttribute('aria-label', label);
|
||||||
|
button.setAttribute('fill', 'clear');
|
||||||
|
|
||||||
|
const iconName = 'book-open-reader';
|
||||||
|
const src = CoreIcons.getIconSrc('font-awesome', 'solid', iconName);
|
||||||
|
// Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed.
|
||||||
|
button.innerHTML = `<ion-icon name="fas-${iconName}" aria-hidden="true" src="${src}"></ion-icon>`;
|
||||||
|
buttonsContainer.appendChild(button);
|
||||||
|
|
||||||
|
button.addEventListener('click', (e: Event) => {
|
||||||
|
if (!this.element.innerHTML) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.enterReadingMode();
|
||||||
|
} else {
|
||||||
|
this.showReadingSettings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enters the reading mode.
|
||||||
|
*/
|
||||||
|
protected async enterReadingMode(): Promise<void> {
|
||||||
|
this.enabled = true;
|
||||||
|
CoreViewer.loadReadingModeSettings();
|
||||||
|
|
||||||
|
this.header?.setEnabled(false);
|
||||||
|
|
||||||
|
document.body.classList.add('core-reading-mode-enabled');
|
||||||
|
|
||||||
|
// Disable all styles in element.
|
||||||
|
this.disabledStyles = Array.from(this.element.querySelectorAll('style:not(disabled)'));
|
||||||
|
this.disabledStyles.forEach((style) => {
|
||||||
|
style.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rename style attributes on DOM elements.
|
||||||
|
this.renamedStyles = Array.from(this.element.querySelectorAll('*[style]'));
|
||||||
|
this.renamedStyles.forEach((element: HTMLElement) => {
|
||||||
|
this.renamedStyles.push(element);
|
||||||
|
element.setAttribute('data-original-style', element.getAttribute('style') || '');
|
||||||
|
element.removeAttribute('style');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigate to parent hidding all other elements.
|
||||||
|
let currentChild = this.element;
|
||||||
|
let parent = currentChild.parentElement;
|
||||||
|
while (parent && parent.tagName.toLowerCase() !== 'ion-content') {
|
||||||
|
Array.from(parent.children).forEach((child: HTMLElement) => {
|
||||||
|
if (child !== currentChild && child.tagName.toLowerCase() !== 'swiper-slide') {
|
||||||
|
this.hiddenElements.push(child);
|
||||||
|
child.classList.add('hide-on-reading-mode');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
currentChild = parent;
|
||||||
|
parent = currentChild.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the reading mode.
|
||||||
|
*/
|
||||||
|
protected async disableReadingMode(): Promise<void> {
|
||||||
|
this.enabled = false;
|
||||||
|
document.body.classList.remove('core-reading-mode-enabled');
|
||||||
|
|
||||||
|
this.header?.setEnabled(true);
|
||||||
|
|
||||||
|
// Enable all styles in element.
|
||||||
|
this.disabledStyles.forEach((style) => {
|
||||||
|
style.disabled = false;
|
||||||
|
});
|
||||||
|
this.disabledStyles = [];
|
||||||
|
|
||||||
|
// Rename style attributes on DOM elements.
|
||||||
|
this.renamedStyles.forEach((element) => {
|
||||||
|
element.setAttribute('style', element.getAttribute('data-original-style') || '');
|
||||||
|
element.removeAttribute('data-original-style');
|
||||||
|
});
|
||||||
|
this.renamedStyles = [];
|
||||||
|
|
||||||
|
this.hiddenElements.forEach((element) => {
|
||||||
|
element.classList.remove('hide-on-reading-mode');
|
||||||
|
});
|
||||||
|
this.hiddenElements = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the reading settings.
|
||||||
|
*/
|
||||||
|
protected async showReadingSettings(): Promise<void> {
|
||||||
|
const { CoreReadingModeSettingsModalComponent } =
|
||||||
|
await import('@features/viewer/components/reading-mode-settings/reading-mode-settings');
|
||||||
|
|
||||||
|
const exit = await CoreModals.openModal({
|
||||||
|
component: CoreReadingModeSettingsModalComponent,
|
||||||
|
initialBreakpoint: 1,
|
||||||
|
breakpoints: [0, 1],
|
||||||
|
cssClass: 'core-modal-auto-height',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exit) {
|
||||||
|
this.disableReadingMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.viewportPromise?.cancel();
|
||||||
|
|
||||||
|
if (this.enabled && document.body.querySelectorAll('[core-reading-mode]')) {
|
||||||
|
// Do not disable if there are more instances of the directive in the DOM.
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.disableReadingMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button fill="clear" (click)="closeModal()" [ariaLabel]="'core.close' | translate">
|
||||||
|
<ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true" />
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<div class="content-auto-height">
|
||||||
|
<ion-item lines="none">
|
||||||
|
<ion-label class="flex-row">
|
||||||
|
<div id="readingmode-range-label">{{ 'core.settings.fontsize' | translate }}</div>
|
||||||
|
</ion-label>
|
||||||
|
<div slot="end">{{settings.zoom}}% @if (defaultZoom) { {{ 'core.viewer.default' | translate }} }</div>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item lines="full">
|
||||||
|
<ion-range aria-labelledby="readingmode-range-label" [min]="MIN_TEXT_SIZE_ZOOM" [max]="MAX_TEXT_SIZE_ZOOM"
|
||||||
|
[step]="TEXT_SIZE_ZOOM_STEP" [value]="settings.zoom" (ionInput)="changeTextSizeZoom($event.detail.value)">
|
||||||
|
<ion-button slot="start" fill="clear" [ariaLabel]="'core.viewer.decreasetextsize' | translate"
|
||||||
|
(click)="changeTextSizeZoom(settings.zoom - TEXT_SIZE_ZOOM_STEP)">
|
||||||
|
<ion-icon name="fas-font" slot="icon-only" aria-hidden="true" class="zoom-decrease" />
|
||||||
|
</ion-button>
|
||||||
|
<ion-button slot="end" fill="clear" [ariaLabel]="'core.viewer.increasetextsize' | translate"
|
||||||
|
(click)="changeTextSizeZoom(settings.zoom + TEXT_SIZE_ZOOM_STEP)">
|
||||||
|
<ion-icon name="fas-font" slot="icon-only" aria-hidden="true" class="zoom-increase" />
|
||||||
|
</ion-button>
|
||||||
|
</ion-range>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>
|
||||||
|
{{ 'core.viewer.theme' | translate }}
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-radio-group [(ngModel)]="settings.theme" (ionChange)="onSettingChange()">
|
||||||
|
@for (theme of themes; track $index; let last = $last;) {
|
||||||
|
<ion-item class="ion-text-wrap" [lines]="last ? 'full': 'none'">
|
||||||
|
<ion-radio [value]="theme" class="reading-theme">
|
||||||
|
<div class="preview {{theme}}">Aa</div> {{ 'core.viewer.readingtheme'+theme | translate }}
|
||||||
|
</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
</ion-radio-group>
|
||||||
|
<ion-item class="ion-text-wrap" (ionChange)="onSettingChange()" lines="full">
|
||||||
|
<ion-toggle [(ngModel)]="settings.showMultimedia">
|
||||||
|
<p class="item-heading">{{ 'core.viewer.showmedia' | translate }}</p>
|
||||||
|
</ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
<ion-button (click)="exit()" fill="outline" expand="block" class="ion-margin ion-text-wrap">
|
||||||
|
<ion-icon name="fas-book-open-reader" slot="start" aria-hidden="true" class="icon-slash" />
|
||||||
|
{{ 'core.viewer.exitreadingmode' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
|
@ -0,0 +1,55 @@
|
||||||
|
@use "theme/globals" as *;
|
||||||
|
|
||||||
|
ion-button ion-icon.zoom-decrease {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-button ion-icon.zoom-increase {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-radio.reading-theme {
|
||||||
|
|
||||||
|
&::part(label) {
|
||||||
|
margin: 0px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 100%;
|
||||||
|
@include margin(4px, 16px, 4px, 2px);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
font-size: #{dynamic-font(16px)};
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
|
||||||
|
&.auto {
|
||||||
|
background: linear-gradient(to right, #{$background-color-dark} 50%, #{$background-color} 50%);
|
||||||
|
color: #{$text-color};
|
||||||
|
&::first-letter {
|
||||||
|
color: #{$text-color-dark};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.light {
|
||||||
|
background-color: #{$background-color};
|
||||||
|
color: #{$text-color};
|
||||||
|
}
|
||||||
|
&.dark {
|
||||||
|
background-color: #{$background-color-dark};
|
||||||
|
color: #{$text-color-dark};
|
||||||
|
}
|
||||||
|
&.sepia {
|
||||||
|
background-color: var(--core-reading-mode-sepia-background);
|
||||||
|
color: var(--core-reading-mode-sepia-text-color);
|
||||||
|
|
||||||
|
}
|
||||||
|
&.hcm {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import {
|
||||||
|
CORE_READING_MODE_DEFAULT_SETTINGS,
|
||||||
|
CoreViewerReadingModeThemes,
|
||||||
|
CoreViewerReadingModeThemesType,
|
||||||
|
} from '@features/viewer/constants';
|
||||||
|
import { CoreViewer } from '@features/viewer/services/viewer';
|
||||||
|
|
||||||
|
import { ModalController } from '@singletons';
|
||||||
|
import { CoreMath } from '@singletons/math';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display a text modal.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-reading-mode-settings-modal',
|
||||||
|
templateUrl: 'reading-mode-settings.html',
|
||||||
|
styleUrl: 'reading-mode-settings.scss',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreReadingModeSettingsModalComponent implements OnInit {
|
||||||
|
|
||||||
|
readonly MAX_TEXT_SIZE_ZOOM = 200;
|
||||||
|
readonly MIN_TEXT_SIZE_ZOOM = 75;
|
||||||
|
readonly TEXT_SIZE_ZOOM_STEP = 25;
|
||||||
|
|
||||||
|
settings = CORE_READING_MODE_DEFAULT_SETTINGS;
|
||||||
|
|
||||||
|
defaultZoom = true;
|
||||||
|
|
||||||
|
themes: CoreViewerReadingModeThemesType[] = Object.values(CoreViewerReadingModeThemes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.settings = await CoreViewer.getReadingModeSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close modal.
|
||||||
|
*/
|
||||||
|
closeModal(): void {
|
||||||
|
ModalController.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close modal.
|
||||||
|
*/
|
||||||
|
exit(): void {
|
||||||
|
ModalController.dismiss(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change text size zoom.
|
||||||
|
*
|
||||||
|
* @param newTextSizeZoom New text size zoom.
|
||||||
|
*/
|
||||||
|
changeTextSizeZoom(newTextSizeZoom: number): void {
|
||||||
|
this.settings.zoom = CoreMath.clamp(
|
||||||
|
newTextSizeZoom,
|
||||||
|
this.MIN_TEXT_SIZE_ZOOM,
|
||||||
|
this.MAX_TEXT_SIZE_ZOOM,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.defaultZoom = this.settings.zoom === 100;
|
||||||
|
this.onSettingChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save settings on any change.
|
||||||
|
*/
|
||||||
|
onSettingChange(): void {
|
||||||
|
CoreViewer.setReadingModeSettings(this.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (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 { CoreViewerReadingModeSettings } from './services/viewer';
|
||||||
|
|
||||||
|
export const CORE_READING_MODE_SETTINGS = 'CoreReadingModeSettings';
|
||||||
|
|
||||||
|
export const CoreViewerReadingModeThemes = {
|
||||||
|
AUTO: 'auto', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
LIGHT: 'light', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
DARK: 'dark', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
SEPIA: 'sepia', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
HCM: 'hcm', // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type CoreViewerReadingModeThemesType = typeof CoreViewerReadingModeThemes[keyof typeof CoreViewerReadingModeThemes];
|
||||||
|
|
||||||
|
export const CORE_READING_MODE_DEFAULT_SETTINGS: CoreViewerReadingModeSettings = {
|
||||||
|
zoom: 100,
|
||||||
|
showMultimedia: false,
|
||||||
|
theme: CoreViewerReadingModeThemes.HCM,
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"decreasetextsize": "Decrease text size",
|
||||||
|
"default": "(Default)",
|
||||||
|
"enterreadingmode": "Enter reading mode",
|
||||||
|
"exitreadingmode": "Exit reading mode",
|
||||||
|
"increasetextsize": "Increase text size",
|
||||||
|
"openreadingmodesettings": "Open reading mode settings",
|
||||||
|
"readingthemeauto": "Match app",
|
||||||
|
"readingthemedark": "Dark",
|
||||||
|
"readingthemehcm": "High contrast",
|
||||||
|
"readingthemelight": "Light",
|
||||||
|
"readingthemesepia": "Sepia",
|
||||||
|
"showmedia": "Show images and media",
|
||||||
|
"theme": "Theme"
|
||||||
|
}
|
|
@ -15,10 +15,17 @@
|
||||||
import { ContextLevel } from '@/core/constants';
|
import { ContextLevel } from '@/core/constants';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ModalOptions } from '@ionic/angular';
|
import { ModalOptions } from '@ionic/angular';
|
||||||
|
import { CoreConfig } from '@services/config';
|
||||||
import { CoreModals } from '@services/modals';
|
import { CoreModals } from '@services/modals';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreWSFile } from '@services/ws';
|
import { CoreWSFile } from '@services/ws';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
import {
|
||||||
|
CORE_READING_MODE_SETTINGS,
|
||||||
|
CoreViewerReadingModeThemes,
|
||||||
|
CoreViewerReadingModeThemesType,
|
||||||
|
CORE_READING_MODE_DEFAULT_SETTINGS,
|
||||||
|
} from '../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Viewer services.
|
* Viewer services.
|
||||||
|
@ -97,6 +104,49 @@ export class CoreViewerService {
|
||||||
await CoreNavigator.navigateToSitePath('viewer/iframe', { params: { title, url, autoLogin } });
|
await CoreNavigator.navigateToSitePath('viewer/iframe', { params: { title, url, autoLogin } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get reading mode settings.
|
||||||
|
*
|
||||||
|
* @returns Reading mode settings.
|
||||||
|
*/
|
||||||
|
async getReadingModeSettings(): Promise<CoreViewerReadingModeSettings> {
|
||||||
|
return CoreConfig.getJSON<CoreViewerReadingModeSettings>(CORE_READING_MODE_SETTINGS, CORE_READING_MODE_DEFAULT_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and apply reading mode settings.
|
||||||
|
*/
|
||||||
|
async loadReadingModeSettings(): Promise<void> {
|
||||||
|
const settings = await this.getReadingModeSettings();
|
||||||
|
|
||||||
|
this.applyReadingModeSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the reading mode settings to the DOM.
|
||||||
|
*
|
||||||
|
* @param settings Settings to apply.
|
||||||
|
*/
|
||||||
|
protected applyReadingModeSettings(settings: CoreViewerReadingModeSettings): void {
|
||||||
|
document.body.style.setProperty('--reading-mode-zoom', settings.zoom + '%');
|
||||||
|
Object.values(CoreViewerReadingModeThemes).forEach((theme) => {
|
||||||
|
document.body.classList.remove(`core-reading-mode-theme-${theme}`);
|
||||||
|
});
|
||||||
|
document.body.classList.add(`core-reading-mode-theme-${settings.theme}`);
|
||||||
|
document.body.classList.toggle('core-reading-mode-multimedia-hidden', !settings.showMultimedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save reading mode settings.
|
||||||
|
*
|
||||||
|
* @param settings Settings to save.
|
||||||
|
*/
|
||||||
|
async setReadingModeSettings(settings: CoreViewerReadingModeSettings): Promise<void> {
|
||||||
|
await CoreConfig.setJSON(CORE_READING_MODE_SETTINGS, settings);
|
||||||
|
|
||||||
|
this.applyReadingModeSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
export const CoreViewer = makeSingleton(CoreViewerService);
|
export const CoreViewer = makeSingleton(CoreViewerService);
|
||||||
|
|
||||||
|
@ -114,3 +164,9 @@ export type CoreViewerTextOptions = {
|
||||||
displayCopyButton?: boolean; // Whether to display a button to copy the text.
|
displayCopyButton?: boolean; // Whether to display a button to copy the text.
|
||||||
modalOptions?: Partial<ModalOptions>; // Modal options.
|
modalOptions?: Partial<ModalOptions>; // Modal options.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CoreViewerReadingModeSettings = {
|
||||||
|
zoom: number; // Zoom level.
|
||||||
|
showMultimedia: boolean; // Show images and multimedia.
|
||||||
|
theme: CoreViewerReadingModeThemesType; // Theme to use.
|
||||||
|
};
|
||||||
|
|
|
@ -346,7 +346,8 @@ export class CoreAppProvider {
|
||||||
*/
|
*/
|
||||||
setSystemUIColors(): void {
|
setSystemUIColors(): void {
|
||||||
this.setStatusBarColor();
|
this.setStatusBarColor();
|
||||||
this.setAndroidNavigationBarColor(); }
|
this.setAndroidNavigationBarColor();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set StatusBar color depending on platform.
|
* Set StatusBar color depending on platform.
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
import { asyncInstance } from '../utils/async-instance';
|
import { asyncInstance } from '../utils/async-instance';
|
||||||
import { CorePromisedValue } from '@classes/promised-value';
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
import { CoreBrowser } from '@singletons/browser';
|
import { CoreBrowser } from '@singletons/browser';
|
||||||
|
import { CoreText } from '@singletons/text';
|
||||||
|
|
||||||
declare module '@singletons/events' {
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
|
@ -118,6 +119,30 @@ export class CoreConfigProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an app setting with json format
|
||||||
|
*
|
||||||
|
* @param name The config name.
|
||||||
|
* @param defaultValue Default value to use if the entry is not found.
|
||||||
|
* @returns Resolves upon success along with the config data. Reject on failure.
|
||||||
|
*/
|
||||||
|
async getJSON<T>(name: string, defaultValue?: T): Promise<T> {
|
||||||
|
try {
|
||||||
|
const configString = await CoreConfig.get<string>(name);
|
||||||
|
if (!configString) {
|
||||||
|
throw new Error('Config not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreText.parseJSON<T>(configString, defaultValue);
|
||||||
|
} catch (error) {
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an app setting directly from the database, without using any optimizations..
|
* Get an app setting directly from the database, without using any optimizations..
|
||||||
*
|
*
|
||||||
|
@ -152,12 +177,21 @@ export class CoreConfigProvider {
|
||||||
*
|
*
|
||||||
* @param name The config name.
|
* @param name The config name.
|
||||||
* @param value The config value. Can only store number or strings.
|
* @param value The config value. Can only store number or strings.
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
*/
|
||||||
async set(name: string, value: number | string): Promise<void> {
|
async set(name: string, value: number | string): Promise<void> {
|
||||||
await this.table.insert({ name, value });
|
await this.table.insert({ name, value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an app setting with json format.
|
||||||
|
*
|
||||||
|
* @param name The config name.
|
||||||
|
* @param value The config value. Can only store objects.
|
||||||
|
*/
|
||||||
|
async setJSON(name: string, value: unknown): Promise<void> {
|
||||||
|
await this.set(name, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update config with the given values.
|
* Update config with the given values.
|
||||||
*
|
*
|
||||||
|
|
|
@ -781,6 +781,44 @@ export class CoreDom {
|
||||||
return !!units && units.length > 1;
|
return !!units && units.length > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search the ion-header of the page.
|
||||||
|
* This function is usually used to find the header of a page to add buttons.
|
||||||
|
*
|
||||||
|
* @returns The header element if found.
|
||||||
|
*/
|
||||||
|
static async findIonHeaderFromElement(element: HTMLElement): Promise<HTMLElement | null> {
|
||||||
|
await CoreDom.waitToBeInDOM(element);
|
||||||
|
let parentPage: HTMLElement | null = element;
|
||||||
|
|
||||||
|
while (parentPage && parentPage.parentElement) {
|
||||||
|
const content = parentPage.closest<HTMLIonContentElement>('ion-content');
|
||||||
|
if (content) {
|
||||||
|
// Sometimes ion-page class is not yet added by the ViewController, wait for content to render.
|
||||||
|
await content.componentOnReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPage = parentPage.parentElement.closest('.ion-page, .ion-page-hidden, .ion-page-invisible');
|
||||||
|
|
||||||
|
// Check if the page has a header. If it doesn't, search the next parent page.
|
||||||
|
let header = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
||||||
|
|
||||||
|
if (header && getComputedStyle(header).display !== 'none') {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find using content if any.
|
||||||
|
header = content?.parentElement?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
||||||
|
|
||||||
|
if (header && getComputedStyle(header).display !== 'none') {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header not found, reject.
|
||||||
|
throw Error('Header not found.');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
@ -105,6 +105,7 @@ ion-button {
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-button,
|
ion-button,
|
||||||
|
ion-button.button, // Add specificity
|
||||||
ion-fab-button,
|
ion-fab-button,
|
||||||
button,
|
button,
|
||||||
[role="button"] {
|
[role="button"] {
|
||||||
|
|
|
@ -122,4 +122,16 @@ ion-modal {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.core-modal-auto-height {
|
||||||
|
--height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.content-auto-height {
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
|
||||||
|
html {
|
||||||
|
--core-reading-mode-sepia-background: #f4ecd8;
|
||||||
|
--core-reading-mode-sepia-text-color: #5b4636;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.core-reading-mode-enabled {
|
||||||
|
.core-text-viewer-button {
|
||||||
|
--core-header-buttons-background: var(--stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.core-reading-mode-theme-light {
|
||||||
|
--reading-mode-background: #{$background-color};
|
||||||
|
--reading-mode-text-color: #{$text-color};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.core-reading-mode-theme-dark {
|
||||||
|
--reading-mode-background: #{$background-color-dark};
|
||||||
|
--reading-mode-text-color: #{$text-color-dark};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.core-reading-mode-theme-sepia {
|
||||||
|
--reading-mode-background: var(--core-reading-mode-sepia-background);
|
||||||
|
--reading-mode-text-color: var(--core-reading-mode-sepia-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.core-reading-mode-theme-hcm {
|
||||||
|
--reading-mode-background: #000000;
|
||||||
|
--reading-mode-text-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.core-reading-mode-multimedia-hidden {
|
||||||
|
ion-content.core-reading-mode-content {
|
||||||
|
img, video, iframe {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-content.core-reading-mode-content,
|
||||||
|
ion-content.core-reading-mode-content core-split-view ion-content {
|
||||||
|
--background: var(--reading-mode-background, --ion-background-color);
|
||||||
|
background: var(--background);
|
||||||
|
|
||||||
|
[core-reading-mode] {
|
||||||
|
zoom: var(--reading-mode-zoom, 1);
|
||||||
|
&> * {
|
||||||
|
--ion-item-background: var(--reading-mode-background, --ion-background-color);
|
||||||
|
--text-color: var(--reading-mode-text-color, --text-color);
|
||||||
|
--color: var(--reading-mode-text-color, --text-color);
|
||||||
|
--subdued-text-color: var(--text-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-on-reading-mode {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@
|
||||||
@import "components/collapsible-header.scss";
|
@import "components/collapsible-header.scss";
|
||||||
@import "components/collapsible-item.scss";
|
@import "components/collapsible-item.scss";
|
||||||
@import "components/error-accordion.scss";
|
@import "components/error-accordion.scss";
|
||||||
|
@import "components/reading-mode.scss";
|
||||||
@import "components/format-text.scss";
|
@import "components/format-text.scss";
|
||||||
@import "components/iframe.scss";
|
@import "components/iframe.scss";
|
||||||
@import "components/mod-label.scss";
|
@import "components/mod-label.scss";
|
||||||
|
|
Loading…
Reference in New Issue