From 6a4e9ac2fc89a611e403d5dee795c1798a365fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 20 Nov 2024 13:18:50 +0100 Subject: [PATCH 1/5] MOBILE-3063 reading-mode: Implement reading mode --- scripts/langindex.json | 13 ++ .../mod/book/pages/contents/contents.html | 2 +- ...pen-chapters-from-table-of-contents_11.png | Bin 12615 -> 13033 bytes .../index/addon-mod-page-index.html | 2 +- src/core/directives/directives.module.ts | 3 + src/core/directives/format-text.ts | 2 +- src/core/directives/reading-mode.ts | 202 ++++++++++++++++++ .../reading-mode-settings.html | 53 +++++ .../reading-mode-settings.scss | 55 +++++ .../reading-mode-settings.ts | 95 ++++++++ src/core/features/viewer/constants.ts | 33 +++ src/core/features/viewer/lang.json | 14 ++ src/core/features/viewer/services/viewer.ts | 56 +++++ src/core/services/app.ts | 3 +- src/core/services/config.ts | 36 +++- ...pp-view-fontawesome-icons-in-the-app_7.png | Bin 16582 -> 17021 bytes src/theme/components/ion-button.scss | 1 + src/theme/components/ion-modal.scss | 17 ++ src/theme/components/reading-mode.scss | 57 +++++ src/theme/theme.scss | 1 + 20 files changed, 640 insertions(+), 5 deletions(-) create mode 100644 src/core/directives/reading-mode.ts create mode 100644 src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.html create mode 100644 src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.scss create mode 100644 src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.ts create mode 100644 src/core/features/viewer/constants.ts create mode 100644 src/core/features/viewer/lang.json create mode 100644 src/theme/components/reading-mode.scss diff --git a/scripts/langindex.json b/scripts/langindex.json index 3f8d345ee..db523decd 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -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", diff --git a/src/addons/mod/book/pages/contents/contents.html b/src/addons/mod/book/pages/contents/contents.html index 22501f112..83191f3c5 100644 --- a/src/addons/mod/book/pages/contents/contents.html +++ b/src/addons/mod/book/pages/contents/contents.html @@ -24,7 +24,7 @@
-
+
diff --git a/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png b/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png index 2f932c02bd843af0a3e4814b8d59b3b366282e1a..41667409394e4c308b9ac3170fe7f2afd9f5a0a6 100644 GIT binary patch literal 13033 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 OAZk~%F5@oVdh{PqyXmR` literal 12615 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 O5M6D -
+
diff --git a/src/core/directives/directives.module.ts b/src/core/directives/directives.module.ts index 6225f56a1..f26fc2ab2 100644 --- a/src/core/directives/directives.module.ts +++ b/src/core/directives/directives.module.ts @@ -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 {} diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 800756f0a..93379a63e 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -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: - * + * */ @Directive({ selector: 'core-format-text', diff --git a/src/core/directives/reading-mode.ts b/src/core/directives/reading-mode.ts new file mode 100644 index 000000000..0534e601c --- /dev/null +++ b/src/core/directives/reading-mode.ts @@ -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: + *
+ */ +@Directive({ + selector: '[core-reading-mode]', +}) +export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { + + protected element: HTMLElement; + protected viewportPromise?: CoreCancellablePromise; + 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 { + await this.viewportPromise; + await CoreWait.nextTick(); + this.addTextViewerButton(); + } + + /** + * Add text viewer button to enable the reading mode. + */ + protected async addTextViewerButton(): Promise { + 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 = ``; + 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 { + 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 { + 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 { + 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(); + } + +} diff --git a/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.html b/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.html new file mode 100644 index 000000000..db01839d5 --- /dev/null +++ b/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.html @@ -0,0 +1,53 @@ + + + + + + + + + + + +
{{ 'core.settings.fontsize' | translate }}
+
+
{{settings.zoom}}% @if (defaultZoom) { {{ 'core.viewer.default' | translate }} }
+
+ + + + + + + + + + + Theme + + + + @for (theme of themes; track $index; let last = $last;) { + + +
Aa
{{ 'core.viewer.readingtheme'+theme | translate }} +
+
+ } +
+ + +

{{ 'core.viewer.showmedia' | translate }}

+
+
+ + +
diff --git a/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.scss b/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.scss new file mode 100644 index 000000000..93cf3faf0 --- /dev/null +++ b/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.scss @@ -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; + } + } +} diff --git a/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.ts b/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.ts new file mode 100644 index 000000000..3bf67c5c4 --- /dev/null +++ b/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.ts @@ -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 { + 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); + } + +} diff --git a/src/core/features/viewer/constants.ts b/src/core/features/viewer/constants.ts new file mode 100644 index 000000000..3b56833d1 --- /dev/null +++ b/src/core/features/viewer/constants.ts @@ -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, +}; diff --git a/src/core/features/viewer/lang.json b/src/core/features/viewer/lang.json new file mode 100644 index 000000000..ab263e5ab --- /dev/null +++ b/src/core/features/viewer/lang.json @@ -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" +} diff --git a/src/core/features/viewer/services/viewer.ts b/src/core/features/viewer/services/viewer.ts index 98b38f676..9efcfc277 100644 --- a/src/core/features/viewer/services/viewer.ts +++ b/src/core/features/viewer/services/viewer.ts @@ -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 { + return CoreConfig.getJSON(CORE_READING_MODE_SETTINGS, CORE_READING_MODE_DEFAULT_SETTINGS); + } + + /** + * Load and apply reading mode settings. + */ + async loadReadingModeSettings(): Promise { + 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 { + 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; // Modal options. }; + +export type CoreViewerReadingModeSettings = { + zoom: number; // Zoom level. + showMultimedia: boolean; // Show images and multimedia. + theme: CoreViewerReadingModeThemesType; // Theme to use. +}; diff --git a/src/core/services/app.ts b/src/core/services/app.ts index 5da40dbb9..c8c4165ee 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -346,7 +346,8 @@ export class CoreAppProvider { */ setSystemUIColors(): void { this.setStatusBarColor(); - this.setAndroidNavigationBarColor(); } + this.setAndroidNavigationBarColor(); + } /** * Set StatusBar color depending on platform. diff --git a/src/core/services/config.ts b/src/core/services/config.ts index aa27a80f2..53aa6b4f5 100644 --- a/src/core/services/config.ts +++ b/src/core/services/config.ts @@ -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(name: string, defaultValue?: T): Promise { + try { + const configString = await CoreConfig.get(name); + if (!configString) { + throw new Error('Config not found'); + } + + return CoreText.parseJSON(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 { 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 { + await this.set(name, JSON.stringify(value)); + } + /** * Update config with the given values. * diff --git a/src/core/tests/behat/snapshots/fontawesome-icons-are-correctly-shown-in-the-app-view-fontawesome-icons-in-the-app_7.png b/src/core/tests/behat/snapshots/fontawesome-icons-are-correctly-shown-in-the-app-view-fontawesome-icons-in-the-app_7.png index ce2463119318ff25a54d860c4e5f24b259aea688..703afebe6db011dbb4e51233d4f52bddfc7468ac 100644 GIT binary patch literal 17021 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 literal 16582 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 o8u|YP_#b{d|KC5B?-rNvx) * { + --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; + } + } +} diff --git a/src/theme/theme.scss b/src/theme/theme.scss index e35ae118a..23181639d 100644 --- a/src/theme/theme.scss +++ b/src/theme/theme.scss @@ -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"; From 157de8fd8a12d0812523fc6bb9c3c95dd1631d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 16 Dec 2024 12:29:35 +0100 Subject: [PATCH 2/5] MOBILE-3063 reading-mode: Fix header color when reading mode is enabled --- src/core/directives/collapsible-header.ts | 1 + src/core/directives/reading-mode.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts index 1f09bad8f..d671d98a8 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -100,6 +100,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest constructor(el: ElementRef) { this.collapsedHeader = el.nativeElement; + CoreDirectivesRegistry.register(this.collapsedHeader, this); } /** diff --git a/src/core/directives/reading-mode.ts b/src/core/directives/reading-mode.ts index 0534e601c..4c0594351 100644 --- a/src/core/directives/reading-mode.ts +++ b/src/core/directives/reading-mode.ts @@ -26,6 +26,8 @@ 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'; /** * Directive to add the reading mode to the selected html tag. @@ -45,6 +47,7 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { protected renamedStyles: HTMLElement[] = []; protected enabled = false; protected contentEl?: HTMLIonContentElement; + protected header?: CoreCollapsibleHeaderDirective; constructor( element: ElementRef, @@ -76,6 +79,11 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { } this.contentEl?.classList.add('core-reading-mode-content'); + + const header = CoreDirectivesRegistry.resolve(page?.querySelector('ion-header'), CoreCollapsibleHeaderDirective); + if (header) { + this.header = header; + } const label = Translate.instant('core.viewer.enterreadingmode'); const button = document.createElement('ion-button'); @@ -114,6 +122,8 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { this.enabled = true; CoreViewer.loadReadingModeSettings(); + this.header?.setEnabled(false); + document.body.classList.add('core-reading-mode-enabled'); // Disable all styles in element. @@ -153,6 +163,8 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { 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; @@ -181,7 +193,7 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { const exit = await CoreModals.openModal({ component: CoreReadingModeSettingsModalComponent, - initialBreakpoint: 1, + initialBreakpoint: 0.5, breakpoints: [0, 1], cssClass: 'core-modal-auto-height', }); From a3c16af58d651e13e76e16e5a32b784ba627917a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 16 Dec 2024 13:05:59 +0100 Subject: [PATCH 3/5] MOBILE-3063 reading-mode: Auto height --- src/core/directives/reading-mode.ts | 2 +- .../reading-mode-settings/reading-mode-settings.html | 6 +++--- src/core/features/viewer/lang.json | 3 ++- src/theme/components/ion-modal.scss | 9 ++------- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/core/directives/reading-mode.ts b/src/core/directives/reading-mode.ts index 4c0594351..5c5085d3f 100644 --- a/src/core/directives/reading-mode.ts +++ b/src/core/directives/reading-mode.ts @@ -193,7 +193,7 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { const exit = await CoreModals.openModal({ component: CoreReadingModeSettingsModalComponent, - initialBreakpoint: 0.5, + initialBreakpoint: 1, breakpoints: [0, 1], cssClass: 'core-modal-auto-height', }); diff --git a/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.html b/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.html index db01839d5..f50e182ba 100644 --- a/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.html +++ b/src/core/features/viewer/components/reading-mode-settings/reading-mode-settings.html @@ -7,7 +7,7 @@ - +
{{ 'core.settings.fontsize' | translate }}
@@ -29,7 +29,7 @@
- Theme + {{ 'core.viewer.theme' | translate }} @@ -50,4 +50,4 @@
diff --git a/src/core/features/viewer/lang.json b/src/core/features/viewer/lang.json index ab263e5ab..a13928951 100644 --- a/src/core/features/viewer/lang.json +++ b/src/core/features/viewer/lang.json @@ -10,5 +10,6 @@ "readingthemehcm": "High contrast", "readingthemelight": "Light", "readingthemesepia": "Sepia", - "showmedia": "Show images and media" + "showmedia": "Show images and media", + "theme": "Theme" } diff --git a/src/theme/components/ion-modal.scss b/src/theme/components/ion-modal.scss index 852ac0384..9b56548c6 100644 --- a/src/theme/components/ion-modal.scss +++ b/src/theme/components/ion-modal.scss @@ -124,17 +124,12 @@ ion-modal { } &.core-modal-auto-height { + --height: auto; display: flex; flex-direction: column; justify-content: flex-end; - &::part(content) { - position: relative; - display: block; - contain: content; - } - - .inner-content { + .content-auto-height { max-height: 80vh; overflow: auto; } From e6733bbd65f37b7b98caf11f43325b88dc3db4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 16 Dec 2024 14:11:07 +0100 Subject: [PATCH 4/5] MOBILE-3063 reading-mode: Add reading mode to glossary entries --- .../mod/glossary/pages/entry/entry.html | 136 +++++++++--------- src/core/directives/reading-mode.ts | 13 +- src/theme/components/reading-mode.scss | 2 + 3 files changed, 83 insertions(+), 68 deletions(-) diff --git a/src/addons/mod/glossary/pages/entry/entry.html b/src/addons/mod/glossary/pages/entry/entry.html index 02d6f6278..2f4a80db0 100644 --- a/src/addons/mod/glossary/pages/entry/entry.html +++ b/src/addons/mod/glossary/pages/entry/entry.html @@ -8,6 +8,7 @@ + @@ -23,72 +24,77 @@ {{ 'core.hasdatatosync' | translate: { $a: 'addon.mod_glossary.entry' | translate } }} - - - -

- -

-

{{ onlineEntry.userfullname }}

-
- {{ onlineEntry.timemodified | coreDateDayOrTime }} -
- - -

- -

-
- {{ onlineEntry.timemodified | coreDateDayOrTime }} -
- - - - - -
- -
-
- -
-
- -
- - -
{{ 'core.tag.tags' | translate }}:
- -
-
- -
- - - - +
+ + + +

+ +

+

{{ onlineEntry.userfullname }}

+
+ {{ onlineEntry.timemodified | coreDateDayOrTime }} +
+ + +

+ +

+
+ {{ onlineEntry.timemodified | coreDateDayOrTime }} +
+ + + + + +
+
- - - -

{{ 'addon.mod_glossary.entrypendingapproval' | translate }}

-
-
- - - +
+ +
+
+ +
+ + +
{{ 'core.tag.tags' | translate }}:
+ +
+
+ +
+ + + + +
+
+ + +

{{ 'addon.mod_glossary.entrypendingapproval' | translate }}

+
+
+ + + +
diff --git a/src/core/directives/reading-mode.ts b/src/core/directives/reading-mode.ts index 5c5085d3f..7949eea64 100644 --- a/src/core/directives/reading-mode.ts +++ b/src/core/directives/reading-mode.ts @@ -28,6 +28,7 @@ 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. @@ -48,6 +49,7 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { protected enabled = false; protected contentEl?: HTMLIonContentElement; protected header?: CoreCollapsibleHeaderDirective; + protected logger = CoreLogger.getInstance('CoreReadingModeDirective'); constructor( element: ElementRef, @@ -72,9 +74,14 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { 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"]'); + const buttonsContainer = page?.querySelector('ion-header ion-toolbar ion-buttons[slot="end"]'); - if (!toolbar || toolbar.querySelector('.core-text-viewer-button')) { + if (!buttonsContainer) { + this.logger.warn('The header was not found, or it didn\'t have any ion-buttons on slot end.'); + + return; + } + if (buttonsContainer.querySelector('.core-text-viewer-button')) { return; } @@ -96,7 +103,7 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { 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 = ``; - toolbar.appendChild(button); + buttonsContainer.appendChild(button); button.addEventListener('click', (e: Event) => { if (!this.element.innerHTML) { diff --git a/src/theme/components/reading-mode.scss b/src/theme/components/reading-mode.scss index ed4ef820e..97aa4b5ef 100644 --- a/src/theme/components/reading-mode.scss +++ b/src/theme/components/reading-mode.scss @@ -44,8 +44,10 @@ body.core-reading-mode-enabled { [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); } } From 170817ed7ca4a36a05474381c9f1846bc62cb099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 17 Dec 2024 11:10:44 +0100 Subject: [PATCH 5/5] MOBILE-3063 reading-mode: Find header in splitview if needed --- .../navbar-buttons/navbar-buttons.ts | 39 +------------------ src/core/directives/reading-mode.ts | 37 +++++++++++------- src/core/singletons/dom.ts | 38 ++++++++++++++++++ src/theme/components/reading-mode.scss | 3 +- 4 files changed, 65 insertions(+), 52 deletions(-) diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts index e06a88435..6fc5179f0 100644 --- a/src/core/components/navbar-buttons/navbar-buttons.ts +++ b/src/core/components/navbar-buttons/navbar-buttons.ts @@ -89,7 +89,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { */ async ngOnInit(): Promise { 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 { - await CoreDom.waitToBeInDOM(this.element); - let parentPage: HTMLElement | null = this.element; - - while (parentPage && parentPage.parentElement) { - const content = parentPage.closest('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(':scope > ion-header'); - - if (header && getComputedStyle(header).display !== 'none') { - return header; - } - - // Find using content if any. - header = content?.parentElement?.querySelector(':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. */ diff --git a/src/core/directives/reading-mode.ts b/src/core/directives/reading-mode.ts index 7949eea64..f46a5f124 100644 --- a/src/core/directives/reading-mode.ts +++ b/src/core/directives/reading-mode.ts @@ -47,7 +47,6 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { protected hiddenElements: HTMLElement[] = []; protected renamedStyles: HTMLElement[] = []; protected enabled = false; - protected contentEl?: HTMLIonContentElement; protected header?: CoreCollapsibleHeaderDirective; protected logger = CoreLogger.getInstance('CoreReadingModeDirective'); @@ -64,7 +63,12 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { async ngAfterViewInit(): Promise { await this.viewportPromise; await CoreWait.nextTick(); - this.addTextViewerButton(); + await this.addTextViewerButton(); + + this.enabled = document.body.classList.contains('core-reading-mode-enabled'); + if (this.enabled) { + await this.enterReadingMode(); + } } /** @@ -72,24 +76,26 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { */ protected async addTextViewerButton(): Promise { const page = CoreDom.closest(this.element, '.ion-page'); - this.contentEl = page?.querySelector('ion-content') ?? undefined; + const contentEl = page?.querySelector('ion-content') ?? undefined; - const buttonsContainer = page?.querySelector('ion-header ion-toolbar ion-buttons[slot="end"]'); - - if (!buttonsContainer) { + const header = await CoreDom.findIonHeaderFromElement(this.element); + const buttonsContainer = header?.querySelector('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; } - this.contentEl?.classList.add('core-reading-mode-content'); - - const header = CoreDirectivesRegistry.resolve(page?.querySelector('ion-header'), CoreCollapsibleHeaderDirective); - if (header) { - this.header = header; + const collapsibleHeader = CoreDirectivesRegistry.resolve(header, CoreCollapsibleHeaderDirective); + if (collapsibleHeader) { + this.header = collapsibleHeader; } const label = Translate.instant('core.viewer.enterreadingmode'); @@ -118,7 +124,6 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { } else { this.showReadingSettings(); } - }); } @@ -214,8 +219,14 @@ export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { * @inheritdoc */ ngOnDestroy(): void { - this.disableReadingMode(); 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(); } } diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts index 0a2ddea57..f7cad2467 100644 --- a/src/core/singletons/dom.ts +++ b/src/core/singletons/dom.ts @@ -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 { + await CoreDom.waitToBeInDOM(element); + let parentPage: HTMLElement | null = element; + + while (parentPage && parentPage.parentElement) { + const content = parentPage.closest('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(':scope > ion-header'); + + if (header && getComputedStyle(header).display !== 'none') { + return header; + } + + // Find using content if any. + header = content?.parentElement?.querySelector(':scope > ion-header'); + + if (header && getComputedStyle(header).display !== 'none') { + return header; + } + } + + // Header not found, reject. + throw Error('Header not found.'); + } + } /** diff --git a/src/theme/components/reading-mode.scss b/src/theme/components/reading-mode.scss index 97aa4b5ef..2fec7a7b6 100644 --- a/src/theme/components/reading-mode.scss +++ b/src/theme/components/reading-mode.scss @@ -37,7 +37,8 @@ body.core-reading-mode-enabled { } } - ion-content.core-reading-mode-content { + 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);