commit
0af6865703
|
@ -2680,6 +2680,19 @@
|
|||
"core.viewcode": "local_moodlemobileapp",
|
||||
"core.vieweditor": "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.wanttochangesite": "local_moodlemobileapp",
|
||||
"core.warningofflinedatadeleted": "local_moodlemobileapp",
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<div class="safe-area-padding-horizontal core-swipe-slides-container">
|
||||
<core-swipe-slides [manager]="manager" [options]="swiperOpts">
|
||||
<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"
|
||||
[contextInstanceId]="cmId" [courseId]="courseId" [disabled]="!active" />
|
||||
<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" />
|
||||
</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end" />
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content [core-swipe-navigation]="entries" class="limited-width">
|
||||
|
@ -23,6 +24,7 @@
|
|||
<ion-label>{{ 'core.hasdatatosync' | translate: { $a: 'addon.mod_glossary.entry' | translate } }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
<div core-reading-mode>
|
||||
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
||||
<core-user-avatar [user]="entry" slot="start" />
|
||||
<ion-label>
|
||||
|
@ -44,12 +46,13 @@
|
|||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition" contextLevel="module"
|
||||
[contextInstanceId]="componentId" [courseId]="courseId" />
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition"
|
||||
contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId" />
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div *ngIf="onlineEntry && onlineEntry.attachment">
|
||||
<core-file *ngFor="let file of onlineEntry.attachments" [file]="file" [component]="component" [componentId]="componentId" />
|
||||
<core-file *ngFor="let file of onlineEntry.attachments" [file]="file" [component]="component"
|
||||
[componentId]="componentId" />
|
||||
</div>
|
||||
<div *ngIf="offlineEntry && offlineEntry.attachments">
|
||||
<core-file *ngFor="let file of offlineEntry.attachments.online" [file]="file" [component]="component"
|
||||
|
@ -58,7 +61,8 @@
|
|||
<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-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" />
|
||||
|
@ -70,7 +74,8 @@
|
|||
[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-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>
|
||||
|
@ -89,6 +94,7 @@
|
|||
<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>
|
||||
|
||||
<ion-card *ngIf="!entry" class="core-warning-card">
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<core-course-module-info [module]="module" [description]="displayDescription && description" [component]="component"
|
||||
[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"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId" />
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
|||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
const header = await this.searchHeader();
|
||||
const header = await CoreDom.findIonHeaderFromElement(this.element);
|
||||
if (header) {
|
||||
// Search the right buttons container (start, end or any).
|
||||
let selector = 'ion-buttons';
|
||||
|
@ -192,43 +192,6 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -100,6 +100,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
|||
|
||||
constructor(el: ElementRef) {
|
||||
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 { CoreUserTourDirective } from './user-tour';
|
||||
import { CoreIonDatetimeDirective } from './datetime';
|
||||
import { CoreReadingModeDirective } from './reading-mode';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -59,6 +60,7 @@ import { CoreIonDatetimeDirective } from './datetime';
|
|||
CoreUpdateNonReactiveAttributesDirective,
|
||||
CoreUserTourDirective,
|
||||
CoreIonDatetimeDirective,
|
||||
CoreReadingModeDirective,
|
||||
],
|
||||
exports: [
|
||||
CoreAutoFocusDirective,
|
||||
|
@ -82,6 +84,7 @@ import { CoreIonDatetimeDirective } from './datetime';
|
|||
CoreUpdateNonReactiveAttributesDirective,
|
||||
CoreUserTourDirective,
|
||||
CoreIonDatetimeDirective,
|
||||
CoreReadingModeDirective,
|
||||
],
|
||||
})
|
||||
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).
|
||||
*
|
||||
* Example usage:
|
||||
* <core-format-text [text]="myText" [component]="component" [componentId]="componentId"></core-format-text>
|
||||
* <core-format-text [text]="myText" [component]="component" [componentId]="componentId" />
|
||||
*/
|
||||
@Directive({
|
||||
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 { Injectable } from '@angular/core';
|
||||
import { ModalOptions } from '@ionic/angular';
|
||||
import { CoreConfig } from '@services/config';
|
||||
import { CoreModals } from '@services/modals';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreWSFile } from '@services/ws';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import {
|
||||
CORE_READING_MODE_SETTINGS,
|
||||
CoreViewerReadingModeThemes,
|
||||
CoreViewerReadingModeThemesType,
|
||||
CORE_READING_MODE_DEFAULT_SETTINGS,
|
||||
} from '../constants';
|
||||
|
||||
/**
|
||||
* Viewer services.
|
||||
|
@ -97,6 +104,49 @@ export class CoreViewerService {
|
|||
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);
|
||||
|
||||
|
@ -114,3 +164,9 @@ export type CoreViewerTextOptions = {
|
|||
displayCopyButton?: boolean; // Whether to display a button to copy the text.
|
||||
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 {
|
||||
this.setStatusBarColor();
|
||||
this.setAndroidNavigationBarColor(); }
|
||||
this.setAndroidNavigationBarColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set StatusBar color depending on platform.
|
||||
|
|
|
@ -24,6 +24,7 @@ import { CoreDatabaseTable } from '@classes/database/database-table';
|
|||
import { asyncInstance } from '../utils/async-instance';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
import { CoreBrowser } from '@singletons/browser';
|
||||
import { CoreText } from '@singletons/text';
|
||||
|
||||
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..
|
||||
*
|
||||
|
@ -152,12 +177,21 @@ export class CoreConfigProvider {
|
|||
*
|
||||
* @param name The config name.
|
||||
* @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> {
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -781,6 +781,44 @@ export class CoreDom {
|
|||
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.button, // Add specificity
|
||||
ion-fab-button,
|
||||
button,
|
||||
[role="button"] {
|
||||
|
|
|
@ -122,4 +122,16 @@ ion-modal {
|
|||
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-item.scss";
|
||||
@import "components/error-accordion.scss";
|
||||
@import "components/reading-mode.scss";
|
||||
@import "components/format-text.scss";
|
||||
@import "components/iframe.scss";
|
||||
@import "components/mod-label.scss";
|
||||
|
|
Loading…
Reference in New Issue