forked from EVOgeek/Vmeda.Online
		
	MOBILE-3063 reading-mode: Implement reading mode
This commit is contained in:
		
							parent
							
								
									d23160df19
								
							
						
					
					
						commit
						6a4e9ac2fc
					
				@ -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  | 
@ -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" />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										202
									
								
								src/core/directives/reading-mode.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								src/core/directives/reading-mode.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,202 @@
 | 
			
		||||
// (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';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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 contentEl?: HTMLIonContentElement;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        element: ElementRef,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.element = element.nativeElement;
 | 
			
		||||
        this.viewportPromise = CoreDom.waitToBeInViewport(this.element);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        await this.viewportPromise;
 | 
			
		||||
        await CoreWait.nextTick();
 | 
			
		||||
        this.addTextViewerButton();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add text viewer button to enable the reading mode.
 | 
			
		||||
     */
 | 
			
		||||
    protected async addTextViewerButton(): Promise<void> {
 | 
			
		||||
        const page = CoreDom.closest(this.element, '.ion-page');
 | 
			
		||||
        this.contentEl = page?.querySelector('ion-content') ?? undefined;
 | 
			
		||||
 | 
			
		||||
        const toolbar = page?.querySelector('ion-header ion-toolbar ion-buttons[slot="end"]');
 | 
			
		||||
 | 
			
		||||
        if (!toolbar || toolbar.querySelector('.core-text-viewer-button')) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.contentEl?.classList.add('core-reading-mode-content');
 | 
			
		||||
 | 
			
		||||
        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>`;
 | 
			
		||||
        toolbar.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();
 | 
			
		||||
 | 
			
		||||
        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');
 | 
			
		||||
 | 
			
		||||
        // 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.disableReadingMode();
 | 
			
		||||
        this.viewportPromise?.cancel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <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>
 | 
			
		||||
            Theme
 | 
			
		||||
        </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>
 | 
			
		||||
</ion-content>
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								src/core/features/viewer/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/core/features/viewer/constants.ts
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										14
									
								
								src/core/features/viewer/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/core/features/viewer/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
    "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"
 | 
			
		||||
}
 | 
			
		||||
@ -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.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
										
											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,21 @@ ion-modal {
 | 
			
		||||
            justify-content: space-between;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.core-modal-auto-height {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        justify-content: flex-end;
 | 
			
		||||
 | 
			
		||||
        &::part(content) {
 | 
			
		||||
            position: relative;
 | 
			
		||||
            display: block;
 | 
			
		||||
            contain: content;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .inner-content {
 | 
			
		||||
            max-height: 80vh;
 | 
			
		||||
            overflow: auto;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										57
									
								
								src/theme/components/reading-mode.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/theme/components/reading-mode.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
        --background: var(--reading-mode-background, --ion-background-color);
 | 
			
		||||
        background: var(--background);
 | 
			
		||||
 | 
			
		||||
        [core-reading-mode] {
 | 
			
		||||
            zoom: var(--reading-mode-zoom, 1);
 | 
			
		||||
            &> * {
 | 
			
		||||
                --text-color: var(--reading-mode-text-color, --text-color);
 | 
			
		||||
                --color: var(--reading-mode-text-color, --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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user