diff --git a/scripts/langindex.json b/scripts/langindex.json index 5a800908f..26f2815f3 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2327,5 +2327,7 @@ "core.years": "moodle", "core.yes": "moodle", "core.youreoffline": "local_moodlemobileapp", - "core.youreonline": "local_moodlemobileapp" + "core.youreonline": "local_moodlemobileapp", + "core.zoomin": "local_moodlemobileapp", + "core.zoomout": "local_moodlemobileapp" } diff --git a/src/core/components/swipe-navigation/swipe-navigation.scss b/src/core/components/swipe-navigation/swipe-navigation.scss index 046438a5a..d0dbcaffd 100644 --- a/src/core/components/swipe-navigation/swipe-navigation.scss +++ b/src/core/components/swipe-navigation/swipe-navigation.scss @@ -9,7 +9,7 @@ ion-slide { ::ng-deep { core-loading .core-loading-content { - display: block; + display: block !important; width: 100%; } diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 10c220e72..fdb95c34a 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -235,7 +235,7 @@ export class CoreFormatTextDirective implements OnChanges { button.addEventListener('click', (e: Event) => { e.preventDefault(); e.stopPropagation(); - CoreDomUtils.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId, true); + CoreDomUtils.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId); }); img.parentNode?.appendChild(button); diff --git a/src/core/features/viewer/components/image/image.html b/src/core/features/viewer/components/image/image.html index c55c50d93..c1096c614 100644 --- a/src/core/features/viewer/components/image/image.html +++ b/src/core/features/viewer/components/image/image.html @@ -1,16 +1,29 @@ - - - -

{{ title }}

-
- - - - - -
-
- - - + + + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/src/core/features/viewer/components/image/image.scss b/src/core/features/viewer/components/image/image.scss index c8b0d2f62..9d8036854 100644 --- a/src/core/features/viewer/components/image/image.scss +++ b/src/core/features/viewer/components/image/image.scss @@ -1,9 +1,11 @@ -:host { - .core-zoom-pane { - height: 100%; - - img { - max-width: none; - } - } +ion-slides { + height: 100%; +} + +img { + max-width: 100%; +} + +ion-footer { + background: var(--contrast-background); } diff --git a/src/core/features/viewer/components/image/image.ts b/src/core/features/viewer/components/image/image.ts index ddb52fe3d..09fe88b40 100644 --- a/src/core/features/viewer/components/image/image.ts +++ b/src/core/features/viewer/components/image/image.ts @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit } from '@angular/core'; - +import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { IonSlides } from '@ionic/angular'; import { ModalController, Translate } from '@singletons'; +import { CoreMath } from '@singletons/math'; /** * Modal component to view an image. @@ -24,17 +25,46 @@ import { ModalController, Translate } from '@singletons'; templateUrl: 'image.html', styleUrls: ['image.scss'], }) -export class CoreViewerImageComponent implements OnInit { +export class CoreViewerImageComponent implements OnInit, AfterViewInit { - @Input() title?: string; // Modal title. - @Input() image?: string; // Image URL. + @ViewChild(IonSlides) protected slides?: IonSlides; + + @Input() title = ''; // Modal title. + @Input() image = ''; // Image URL. @Input() component?: string; // Component to use in external-content. @Input() componentId?: string | number; // Component ID to use in external-content. + slidesOpts = { + slidesPerView: 1, + centerInsufficientSlides: true, + centerSlides: true, + zoom: { + maxRatio: 8, + minRatio: 0.5, // User can zoom out to 0.5 only using pinch gesture. + }, + }; + + protected zoomRatio = 1; + + slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + constructor(protected element: ElementRef) { + } + + /** + * @inheritdoc + */ ngOnInit(): void { this.title = this.title || Translate.instant('core.imageviewer'); } + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + this.slidesSwiper = await this.slides?.getSwiper(); + } + /** * Close modal. */ @@ -42,4 +72,33 @@ export class CoreViewerImageComponent implements OnInit { ModalController.dismiss(); } + /** + * Zoom In or Out. + * + * @param zoomIn: True to zoom in, false to zoom out. + */ + zoom(zoomIn = true): void { + const imageElement = this.element.nativeElement.querySelector('img'); + + if (!this.slidesSwiper || !imageElement) { + return; + } + + zoomIn + ? this.zoomRatio *= 2 + : this.zoomRatio /= 2; + + // Using 1 as minimum for manual zoom. + this.zoomRatio = CoreMath.clamp(this.zoomRatio, 1, this.slidesOpts.zoom.maxRatio); + + if (this.zoomRatio > 1) { + this.slidesSwiper.zoom.in(); + + imageElement.style.transform = + 'translate3d(0px, 0px, 0px) scale(' + this.zoomRatio + ')'; + } else { + this.slidesSwiper.zoom.out(); + } + } + } diff --git a/src/core/lang.json b/src/core/lang.json index 793e0da74..ce0aebea4 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -347,5 +347,7 @@ "years": "years", "yes": "Yes", "youreoffline": "You are offline", - "youreonline": "You are back online" + "youreonline": "You are back online", + "zoomin": "Zoom In", + "zoomout": "Zoom Out" } diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 77a664817..d097ee912 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -1787,14 +1787,12 @@ export class CoreDomUtilsProvider { * @param title Title of the page or modal. * @param component Component to link the image to if needed. * @param componentId An ID to use in conjunction with the component. - * @param fullScreen Whether the modal should be full screen. */ async viewImage( image: string, title?: string | null, component?: string, componentId?: string | number, - fullScreen?: boolean, ): Promise { if (!image) { return; @@ -1808,7 +1806,7 @@ export class CoreDomUtilsProvider { component, componentId, }, - cssClass: fullScreen ? 'core-modal-fullscreen' : '', + cssClass: 'core-modal-transparent', }); } diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index d13635f92..0500be86e 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -510,6 +510,27 @@ body.core-iframe-fullscreen ion-router-outlet { height: 100% !important; } +.core-modal-transparent { + + ion-backdrop { + backdrop-filter: blur(8px); + } + + .modal-wrapper { + backdrop-filter: blur(12px); + --background: rgba(10, 10, 10, 0.2); + + ion-content { + --background: transparent !important; + } + position: absolute; + @include position(0 !important, null, null, 0 !important); + display: block; + width: 100% !important; + height: 100% !important; + } +} + .core-modal-force-on-top { z-index: 100000 !important; }