From 400bc6840f954a3eaabccff38ca36fc2f45f1ad6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 22 Feb 2021 13:24:06 +0100 Subject: [PATCH 01/10] MOBILE-3708 core: Fix and implement some missing todos --- src/core/components/bs-tooltip/bs-tooltip.ts | 29 +++++++ .../bs-tooltip/core-bs-tooltip.html | 6 ++ src/core/components/components.module.ts | 3 + src/core/components/iframe/iframe.scss | 5 +- src/core/components/iframe/iframe.ts | 1 + .../components/module/core-course-module.html | 4 +- .../course-progress/course-progress.ts | 61 +++++++++++--- .../pages/course-preview/course-preview.ts | 4 +- src/core/features/login/pages/sites/sites.ts | 13 +-- src/core/features/mainmenu/pages/more/more.ts | 5 +- .../question/services/question-helper.ts | 5 +- .../settings/pages/general/general.ts | 2 - .../tag/services/tag-area-delegate.ts | 2 - .../user/pages/profile/profile.page.ts | 5 +- .../features/user/services/user-delegate.ts | 22 ++++- src/core/features/user/services/user.ts | 4 +- .../viewer/components/components.module.ts | 3 + .../viewer/components/image/image.html | 14 ++++ .../viewer/components/image/image.scss | 9 ++ .../features/viewer/components/image/image.ts | 44 ++++++++++ .../features/viewer/pages/iframe/iframe.html | 13 +++ .../viewer/pages/iframe/iframe.module.ts | 38 +++++++++ .../features/viewer/pages/iframe/iframe.ts | 57 +++++++++++++ .../features/viewer/viewer-lazy.module.ts | 28 +++++++ src/core/features/viewer/viewer.module.ts | 10 +++ src/core/services/groups.ts | 8 +- src/core/services/update-manager.ts | 5 +- src/core/services/utils/dom.ts | 84 ++++++++++++++++--- 28 files changed, 425 insertions(+), 59 deletions(-) create mode 100644 src/core/components/bs-tooltip/bs-tooltip.ts create mode 100644 src/core/components/bs-tooltip/core-bs-tooltip.html create mode 100644 src/core/features/viewer/components/image/image.html create mode 100644 src/core/features/viewer/components/image/image.scss create mode 100644 src/core/features/viewer/components/image/image.ts create mode 100644 src/core/features/viewer/pages/iframe/iframe.html create mode 100644 src/core/features/viewer/pages/iframe/iframe.module.ts create mode 100644 src/core/features/viewer/pages/iframe/iframe.ts create mode 100644 src/core/features/viewer/viewer-lazy.module.ts diff --git a/src/core/components/bs-tooltip/bs-tooltip.ts b/src/core/components/bs-tooltip/bs-tooltip.ts new file mode 100644 index 000000000..f3738b099 --- /dev/null +++ b/src/core/components/bs-tooltip/bs-tooltip.ts @@ -0,0 +1,29 @@ +// (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 { Component, Input } from '@angular/core'; + +/** + * Component to display a Bootstrap Tooltip in a popover. + */ +@Component({ + selector: 'core-bs-tooltip', + templateUrl: 'core-bs-tooltip.html', +}) +export class CoreBSTooltipComponent { + + @Input() content = ''; + @Input() html?: boolean; + +} diff --git a/src/core/components/bs-tooltip/core-bs-tooltip.html b/src/core/components/bs-tooltip/core-bs-tooltip.html new file mode 100644 index 000000000..cc5e8e36c --- /dev/null +++ b/src/core/components/bs-tooltip/core-bs-tooltip.html @@ -0,0 +1,6 @@ + + +

+

{{content}}

+
+
diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts index 02fc1268d..4e37a8a76 100644 --- a/src/core/components/components.module.ts +++ b/src/core/components/components.module.ts @@ -51,6 +51,7 @@ import { CorePipesModule } from '@pipes/pipes.module'; import { CoreAttachmentsComponent } from './attachments/attachments'; import { CoreFilesComponent } from './files/files'; import { CoreLocalFileComponent } from './local-file/local-file'; +import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip'; @NgModule({ declarations: [ @@ -84,6 +85,7 @@ import { CoreLocalFileComponent } from './local-file/local-file'; CoreAttachmentsComponent, CoreFilesComponent, CoreLocalFileComponent, + CoreBSTooltipComponent, ], imports: [ CommonModule, @@ -124,6 +126,7 @@ import { CoreLocalFileComponent } from './local-file/local-file'; CoreAttachmentsComponent, CoreFilesComponent, CoreLocalFileComponent, + CoreBSTooltipComponent, ], }) export class CoreComponentsModule {} diff --git a/src/core/components/iframe/iframe.scss b/src/core/components/iframe/iframe.scss index d99e54aae..67a48e598 100644 --- a/src/core/components/iframe/iframe.scss +++ b/src/core/components/iframe/iframe.scss @@ -1,5 +1,6 @@ -ion-app.app-root core-iframe { +@import "~theme/globals"; +:host { > div { max-width: 100%; max-height: 100%; @@ -8,7 +9,7 @@ ion-app.app-root core-iframe { border: 0; display: block; max-width: 100%; - background-color: $gray-light; + background-color: var(--gray-light); } .core-loading-container { diff --git a/src/core/components/iframe/iframe.ts b/src/core/components/iframe/iframe.ts index 3269f7629..70ffdffcc 100644 --- a/src/core/components/iframe/iframe.ts +++ b/src/core/components/iframe/iframe.ts @@ -27,6 +27,7 @@ import { CoreLogger } from '@singletons/logger'; @Component({ selector: 'core-iframe', templateUrl: 'core-iframe.html', + styleUrls: ['iframe.scss'], }) export class CoreIframeComponent implements OnChanges { diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html index cab6e929c..cf25c9990 100644 --- a/src/core/features/course/components/module/core-course-module.html +++ b/src/core/features/course/components/module/core-course-module.html @@ -22,10 +22,10 @@
- + { e.preventDefault(); @@ -247,22 +246,62 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { * Hide/Unhide the course from the course list. * * @param hide True to hide and false to show. - * @todo CoreUser */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected setCourseHidden(hide: boolean): void { - return; + protected async setCourseHidden(hide: boolean): Promise { + this.showSpinner = true; + + // We should use null to unset the preference. + try { + await CoreUser.updateUserPreference( + 'block_myoverview_hidden_course_' + this.course.id, + hide ? '1' : undefined, + ); + + this.course.hidden = hide; + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + courseId: this.course.id, + course: this.course, + action: CoreCoursesProvider.ACTION_STATE_CHANGED, + state: CoreCoursesProvider.STATE_HIDDEN, + value: hide, + }, CoreSites.getCurrentSiteId()); + + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.showErrorModalDefault(error, 'Error changing course visibility.'); + } + } finally { + this.showSpinner = false; + } } /** * Favourite/Unfavourite the course from the course list. * * @param favourite True to favourite and false to unfavourite. - * @todo CoreUser */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected setCourseFavourite(favourite: boolean): void { - return; + protected async setCourseFavourite(favourite: boolean): Promise { + this.showSpinner = true; + + try { + await CoreCourses.setFavouriteCourse(this.course.id, favourite); + + this.course.isfavourite = favourite; + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + courseId: this.course.id, + course: this.course, + action: CoreCoursesProvider.ACTION_STATE_CHANGED, + state: CoreCoursesProvider.STATE_FAVOURITE, + value: favourite, + }, CoreSites.getCurrentSiteId()); + + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.showErrorModalDefault(error, 'Error changing course favourite attribute.'); + } + } finally { + this.showSpinner = false; + } } /** diff --git a/src/core/features/courses/pages/course-preview/course-preview.ts b/src/core/features/courses/pages/course-preview/course-preview.ts index 1bad9c1ee..0b95eb3bc 100644 --- a/src/core/features/courses/pages/course-preview/course-preview.ts +++ b/src/core/features/courses/pages/course-preview/course-preview.ts @@ -450,11 +450,11 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy { * Prefetch the course. */ prefetchCourse(): void { - /* @todo CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course).catch((error) => { + CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course!).catch((error) => { if (!this.pageDestroyed) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); } - });*/ + }); } /** diff --git a/src/core/features/login/pages/sites/sites.ts b/src/core/features/login/pages/sites/sites.ts index 3bf3222c7..7c5b778c5 100644 --- a/src/core/features/login/pages/sites/sites.ts +++ b/src/core/features/login/pages/sites/sites.ts @@ -20,6 +20,8 @@ import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; import { CoreLogger } from '@singletons/logger'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreNavigator } from '@services/navigator'; +import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; +import { CoreFilter } from '@features/filter/services/filter'; /** * Page that displays a "splash screen" while the app is being initialized. @@ -48,13 +50,12 @@ export class CoreLoginSitesPage implements OnInit { const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]); // Remove protocol from the url to show more url text. - this.sites = sites.map((site) => { + this.sites = await Promise.all(sites.map(async (site) => { site.siteUrl = site.siteUrl.replace(/^https?:\/\//, ''); - site.badge = 0; - // @todo: getSiteCounter. + site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0; return site; - }); + })); this.showDelete = false; } @@ -76,9 +77,9 @@ export class CoreLoginSitesPage implements OnInit { async deleteSite(e: Event, site: CoreSiteBasicInfo): Promise { e.stopPropagation(); - const siteName = site.siteName || ''; + let siteName = site.siteName || ''; - // @todo: Format text: siteName. + siteName = await CoreFilter.formatText(siteName, { clean: true, singleLine: true, filter: false }, [], site.id); try { await CoreDomUtils.showDeleteConfirm('core.login.confirmdeletesite', { sitename: siteName }); diff --git a/src/core/features/mainmenu/pages/more/more.ts b/src/core/features/mainmenu/pages/more/more.ts index 3c72b8885..506855dca 100644 --- a/src/core/features/mainmenu/pages/more/more.ts +++ b/src/core/features/mainmenu/pages/more/more.ts @@ -146,10 +146,7 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { * @param item Item to open. */ openItem(item: CoreMainMenuCustomItem): void { - // @todo CoreNavigator.navigateToSitePath('CoreViewerIframePage', {title: item.label, url: item.url}); - - // eslint-disable-next-line no-console - console.error('openItem not implemented', item); + CoreNavigator.navigateToSitePath('viewer/iframe', { params: { title: item.label, url: item.url } }); } /** diff --git a/src/core/features/question/services/question-helper.ts b/src/core/features/question/services/question-helper.ts index 253069ab5..40f87a862 100644 --- a/src/core/features/question/services/question-helper.ts +++ b/src/core/features/question/services/question-helper.ts @@ -819,7 +819,6 @@ export class CoreQuestionHelperProvider { if (span.innerHTML) { // There's a hidden feedback. Mark the icon as tappable. // The click listener is only added if treatCorrectnessIconsClicks is called. - // @todo: Check if another attribute needs to be used now instead of tappable. icon.setAttribute('tappable', ''); } }); @@ -843,9 +842,7 @@ export class CoreQuestionHelperProvider { contextInstanceId?: number, courseId?: number, ): void { - - // @todo: Check if another attribute needs to be used now instead of tappable. - const icons = Array.from(element.querySelectorAll('i.icon.questioncorrectnessicon[tappable]')); + const icons = Array.from(element.querySelectorAll('ion-icon.questioncorrectnessicon[tappable]')); const title = Translate.instant('core.question.feedback'); icons.forEach((icon) => { diff --git a/src/core/features/settings/pages/general/general.ts b/src/core/features/settings/pages/general/general.ts index 5c248c56e..9f9f8aebf 100644 --- a/src/core/features/settings/pages/general/general.ts +++ b/src/core/features/settings/pages/general/general.ts @@ -153,8 +153,6 @@ export class CoreSettingsGeneralPage { /** * Called when the analytics setting is enabled or disabled. - * - * @todo */ async analyticsEnabledChanged(): Promise { await CorePushNotifications.enableAnalytics(this.analyticsEnabled); diff --git a/src/core/features/tag/services/tag-area-delegate.ts b/src/core/features/tag/services/tag-area-delegate.ts index 0ef6d446f..e2f0e464d 100644 --- a/src/core/features/tag/services/tag-area-delegate.ts +++ b/src/core/features/tag/services/tag-area-delegate.ts @@ -37,7 +37,6 @@ export interface CoreTagAreaHandler extends CoreDelegateHandler { * Get the component to use to display items. * * @return The component (or promise resolved with component) to use, undefined if not found. - * @todo, check return types. */ getComponent(): Type | Promise>; } @@ -85,7 +84,6 @@ export class CoreTagAreaDelegateService extends CoreDelegate * @param component Component name. * @param itemType Item type. * @return The component (or promise resolved with component) to use, undefined if not found. - * @todo, check return types. */ async getComponent(component: string, itemType: string): Promise | undefined> { const type = component + '/' + itemType; diff --git a/src/core/features/user/pages/profile/profile.page.ts b/src/core/features/user/pages/profile/profile.page.ts index 37764fec0..10d326490 100644 --- a/src/core/features/user/pages/profile/profile.page.ts +++ b/src/core/features/user/pages/profile/profile.page.ts @@ -35,6 +35,7 @@ import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuplo import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreUtils } from '@services/utils/utils'; import { CoreNavigator } from '@services/navigator'; +import { CoreCourses } from '@features/courses/services/courses'; @Component({ selector: 'page-core-user-profile', @@ -239,8 +240,8 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { async refreshUser(event?: CustomEvent): Promise { await CoreUtils.ignoreErrors(Promise.all([ CoreUser.invalidateUserCache(this.userId), - // @todo this.coursesProvider.invalidateUserNavigationOptions(), - // this.coursesProvider.invalidateUserAdministrationOptions() + CoreCourses.invalidateUserNavigationOptions(), + CoreCourses.invalidateUserAdministrationOptions(), ])); await this.fetchUser(); diff --git a/src/core/features/user/services/user-delegate.ts b/src/core/features/user/services/user-delegate.ts index 00eb18b9a..ae44378b4 100644 --- a/src/core/features/user/services/user-delegate.ts +++ b/src/core/features/user/services/user-delegate.ts @@ -20,7 +20,8 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreEvents } from '@singletons/events'; import { CoreUserProfile } from './user'; import { makeSingleton } from '@singletons'; -import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; +import { CoreCourses, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; +import { CoreSites } from '@services/sites'; /** * Interface that all user profile handlers must implement. @@ -241,9 +242,22 @@ export class CoreUserDelegateService extends CoreDelegate { - // @todo: Get Course admin/nav options. - let navOptions; - let admOptions; + let navOptions: CoreCourseUserAdminOrNavOptionIndexed | undefined; + let admOptions: CoreCourseUserAdminOrNavOptionIndexed | undefined; + + if (CoreCourses.canGetAdminAndNavOptions()) { + // Get course options. + const courses = await CoreCourses.getUserCourses(true); + const courseIds = courses.map((course) => course.id); + + const options = await CoreCourses.getCoursesAdminAndNavOptions(courseIds); + + // For backwards compatibility we don't modify the courseId. + const courseIdForOptions = courseId || CoreSites.getCurrentSiteHomeId(); + + navOptions = options.navOptions[courseIdForOptions]; + admOptions = options.admOptions[courseIdForOptions]; + } const userData = this.userHandlers[user.id]; userData.handlers = []; diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 19d8312b8..1439f184a 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -759,7 +759,7 @@ export class CoreUserProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved if success. */ - updateUserPreference(name: string, value: string, userId?: number, siteId?: string): Promise { + updateUserPreference(name: string, value: string | undefined, userId?: number, siteId?: string): Promise { const preferences = [ { type: name, @@ -780,7 +780,7 @@ export class CoreUserProvider { * @return Promise resolved if success. */ async updateUserPreferences( - preferences: { type: string; value: string }[], + preferences: { type: string; value: string | undefined }[], disableNotifications?: boolean, userId?: number, siteId?: string, diff --git a/src/core/features/viewer/components/components.module.ts b/src/core/features/viewer/components/components.module.ts index 5b1af2909..cb1428d05 100644 --- a/src/core/features/viewer/components/components.module.ts +++ b/src/core/features/viewer/components/components.module.ts @@ -15,16 +15,19 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; +import { CoreViewerImageComponent } from './image/image'; import { CoreViewerTextComponent } from './text/text'; @NgModule({ declarations: [ + CoreViewerImageComponent, CoreViewerTextComponent, ], imports: [ CoreSharedModule, ], exports: [ + CoreViewerImageComponent, CoreViewerTextComponent, ], }) diff --git a/src/core/features/viewer/components/image/image.html b/src/core/features/viewer/components/image/image.html new file mode 100644 index 000000000..d293205fb --- /dev/null +++ b/src/core/features/viewer/components/image/image.html @@ -0,0 +1,14 @@ + + + {{ title }} + + + + + + + + + + + diff --git a/src/core/features/viewer/components/image/image.scss b/src/core/features/viewer/components/image/image.scss new file mode 100644 index 000000000..c8b0d2f62 --- /dev/null +++ b/src/core/features/viewer/components/image/image.scss @@ -0,0 +1,9 @@ +:host { + .core-zoom-pane { + height: 100%; + + img { + max-width: none; + } + } +} diff --git a/src/core/features/viewer/components/image/image.ts b/src/core/features/viewer/components/image/image.ts new file mode 100644 index 000000000..ed6a64f0b --- /dev/null +++ b/src/core/features/viewer/components/image/image.ts @@ -0,0 +1,44 @@ +// (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 { Component, Input, OnInit } from '@angular/core'; + +import { ModalController, Translate } from '@singletons'; + +/** + * Modal component to view an image. + */ +@Component({ + selector: 'core-viewer-image', + templateUrl: 'image.html', +}) +export class CoreViewerImageComponent implements OnInit { + + @Input() title?: string; // Modal title. + @Input() image?: string; // Image URL. + @Input() component?: string; // Component to use in external-content. + @Input() componentId?: string | number; // Component ID to use in external-content. + + ngOnInit(): void { + this.title = this.title || Translate.instant('core.imageviewer'); + } + + /** + * Close modal. + */ + closeModal(): void { + ModalController.dismiss(); + } + +} diff --git a/src/core/features/viewer/pages/iframe/iframe.html b/src/core/features/viewer/pages/iframe/iframe.html new file mode 100644 index 000000000..f7bd2fecb --- /dev/null +++ b/src/core/features/viewer/pages/iframe/iframe.html @@ -0,0 +1,13 @@ + + + + + + {{ title }} + + + + + + + diff --git a/src/core/features/viewer/pages/iframe/iframe.module.ts b/src/core/features/viewer/pages/iframe/iframe.module.ts new file mode 100644 index 000000000..9a8e7380c --- /dev/null +++ b/src/core/features/viewer/pages/iframe/iframe.module.ts @@ -0,0 +1,38 @@ +// (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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreViewerIframePage } from './iframe'; + +const routes: Routes = [ + { + path: '', + component: CoreViewerIframePage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + declarations: [ + CoreViewerIframePage, + ], + exports: [RouterModule], +}) +export class CoreViewerIframePageModule {} diff --git a/src/core/features/viewer/pages/iframe/iframe.ts b/src/core/features/viewer/pages/iframe/iframe.ts new file mode 100644 index 000000000..fca24169a --- /dev/null +++ b/src/core/features/viewer/pages/iframe/iframe.ts @@ -0,0 +1,57 @@ +// (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 { Component, OnInit } from '@angular/core'; +import { CoreNavigator } from '@services/navigator'; + +import { CoreSites } from '@services/sites'; + +/** + * Page to display a URL in an iframe. + */ +@Component({ + selector: 'core-viewer-iframe', + templateUrl: 'iframe.html', +}) +export class CoreViewerIframePage implements OnInit { + + title?: string; // Page title. + url?: string; // Iframe URL. + /* Whether the URL should be open with auto-login. Accepts the following values: + "yes" -> Always auto-login. + "no" -> Never auto-login. + "check" -> Auto-login only if it points to the current site. Default value. */ + autoLogin?: string; + finalUrl?: string; + + async ngOnInit(): Promise { + this.title = CoreNavigator.getRouteParam('title'); + this.url = CoreNavigator.getRouteParam('url'); + this.autoLogin = CoreNavigator.getRouteParam('autoLogin') || 'check'; + + if (!this.url) { + return; + } + + const currentSite = CoreSites.getCurrentSite(); + + if (currentSite && (this.autoLogin == 'yes' || (this.autoLogin == 'check' && currentSite.containsUrl(this.url)))) { + // Format the URL to add auto-login. + this.finalUrl = await currentSite.getAutoLoginUrl(this.url, false); + } else { + this.finalUrl = this.url; + } + } + +} diff --git a/src/core/features/viewer/viewer-lazy.module.ts b/src/core/features/viewer/viewer-lazy.module.ts new file mode 100644 index 000000000..99649b6af --- /dev/null +++ b/src/core/features/viewer/viewer-lazy.module.ts @@ -0,0 +1,28 @@ +// (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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: 'iframe', + loadChildren: () => import('./pages/iframe/iframe.module').then( m => m.CoreViewerIframePageModule), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], +}) +export class CoreViewerLazyModule {} diff --git a/src/core/features/viewer/viewer.module.ts b/src/core/features/viewer/viewer.module.ts index ae9dc3816..bfa5806c6 100644 --- a/src/core/features/viewer/viewer.module.ts +++ b/src/core/features/viewer/viewer.module.ts @@ -13,11 +13,21 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreViewerComponentsModule } from './components/components.module'; +const routes: Routes = [ + { + path: 'viewer', + loadChildren: () => import('./viewer-lazy.module').then(m => m.CoreViewerLazyModule), + }, +]; + @NgModule({ imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), CoreViewerComponentsModule, ], }) diff --git a/src/core/services/groups.ts b/src/core/services/groups.ts index e5b19bd01..cedb82af9 100644 --- a/src/core/services/groups.ts +++ b/src/core/services/groups.ts @@ -19,6 +19,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons'; import { CoreWSExternalWarning } from '@services/ws'; +import { CoreCourses } from '@features/courses/services/courses'; const ROOT_CACHE_KEY = 'mmGroups:'; @@ -242,8 +243,11 @@ export class CoreGroupsProvider { return this.getUserGroupsInCourse(0, siteId); } - // @todo Get courses. - return []; + const courses = await CoreCourses.getUserCourses(false, siteId); + + courses.push({ id: site.getSiteHomeId() }); // Add site home. + + return this.getUserGroups(courses, siteId); } /** diff --git a/src/core/services/update-manager.ts b/src/core/services/update-manager.ts index 5d11fd4fc..cf608e954 100644 --- a/src/core/services/update-manager.ts +++ b/src/core/services/update-manager.ts @@ -18,6 +18,7 @@ import { CoreConfig } from '@services/config'; import { CoreConstants } from '@/core/constants'; import { CoreLogger } from '@singletons/logger'; import { makeSingleton } from '@singletons'; +import { CoreH5P } from '@features/h5p/services/h5p'; const VERSION_APPLIED = 'version_applied'; @@ -42,13 +43,13 @@ export class CoreUpdateManagerProvider { * @return Promise resolved when the update process finishes. */ async load(): Promise { - const promises = []; + const promises: Promise[] = []; const versionCode = CoreConstants.CONFIG.versioncode; const versionApplied = await CoreConfig.get(VERSION_APPLIED, 0); if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) { - // @todo: H5P update. + promises.push(CoreH5P.h5pPlayer.deleteAllContentIndexes()); } try { diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 6212a6678..52b82602b 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -31,11 +31,20 @@ import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreError } from '@classes/errors/error'; import { CoreSilentError } from '@classes/errors/silenterror'; - -import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons'; +import { + makeSingleton, + Translate, + AlertController, + LoadingController, + ToastController, + PopoverController, + ModalController, +} from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreFileSizeSum } from '@services/plugin-file-delegate'; import { CoreNetworkError } from '@classes/errors/network-error'; +import { CoreBSTooltipComponent } from '@components/bs-tooltip/bs-tooltip'; +import { CoreViewerImageComponent } from '@features/viewer/components/image/image'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -810,8 +819,18 @@ export class CoreDomUtilsProvider { el.setAttribute('data-original-title', content); el.setAttribute('title', ''); - el.addEventListener('click', () => { - // @todo + el.addEventListener('click', async (ev: Event) => { + const html = el.getAttribute('data-html'); + + const popover = await PopoverController.create({ + component: CoreBSTooltipComponent, + componentProps: { + content, + html: html === 'true', + }, + event: ev, + }); + await popover.present(); }); }); } @@ -1554,12 +1573,32 @@ export class CoreDomUtilsProvider { * @param message Modal message. * @param buttons Buttons to pass to the modal. * @param placeholder Placeholder of the input element if any. - * @return Promise resolved when modal presented. + * @return Promise resolved with the entered text if any. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - showTextareaPrompt(title: string, message: string, buttons: (string | unknown)[], placeholder?: string): Promise { - // @todo - return Promise.resolve(); + async showTextareaPrompt( + title: string, + message: string, + buttons: AlertButton[], + placeholder?: string, + ): Promise { + const alert = await AlertController.create({ + header: title, + message, + inputs: [ + { + name: 'textarea-prompt', + type: 'textarea', + placeholder: placeholder, + }, + ], + buttons, + }); + + await alert.present(); + + const result = await alert.onWillDismiss(); + + return result.data?.values?.['textarea-prompt']; } /** @@ -1671,9 +1710,30 @@ export class CoreDomUtilsProvider { * @param componentId An ID to use in conjunction with the component. * @param fullScreen Whether the modal should be full screen. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - viewImage(image: string, title?: string | null, component?: string, componentId?: string | number, fullScreen?: boolean): void { - // @todo + async viewImage( + image: string, + title?: string | null, + component?: string, + componentId?: string | number, + fullScreen?: boolean, + ): Promise { + if (!image) { + return; + } + + const modal = await ModalController.create({ + component: CoreViewerImageComponent, + componentProps: { + title, + image, + component, + componentId, + }, + cssClass: fullScreen ? 'core-modal-fullscreen' : '', + }); + + + await modal.present(); } /** From 54cf0c58a058f42cc68f725f523800c6f7ae3ec1 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Feb 2021 08:23:40 +0100 Subject: [PATCH 02/10] MOBILE-3708 h5p: Fix DB site schema --- src/core/features/h5p/services/database/h5p.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/features/h5p/services/database/h5p.ts b/src/core/features/h5p/services/database/h5p.ts index 0564cdb75..f6605d529 100644 --- a/src/core/features/h5p/services/database/h5p.ts +++ b/src/core/features/h5p/services/database/h5p.ts @@ -26,7 +26,7 @@ export const CONTENTS_LIBRARIES_TABLE_NAME = 'h5p_contents_libraries'; // Which export const LIBRARIES_CACHEDASSETS_TABLE_NAME = 'h5p_libraries_cachedassets'; // H5P cached library assets. export const SITE_SCHEMA: CoreSiteSchema = { name: 'CoreH5PProvider', - version: 1, + version: 2, canBeCleared: [ CONTENT_TABLE_NAME, LIBRARIES_TABLE_NAME, From 9d3f4d5a2983ba0579a202b0d81c4a2b96d3e63f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Feb 2021 12:09:51 +0100 Subject: [PATCH 03/10] MOBILE-3708 settings: Add split view in site preferences --- src/addons/messages/messages-lazy.module.ts | 6 - src/addons/messages/messages.module.ts | 10 +- .../messages/services/handlers/settings.ts | 5 +- .../notifications-lazy.module.ts | 5 - .../notifications/notifications.module.ts | 10 +- .../services/handlers/settings.ts | 5 +- .../settings/pages/site/site-routing.ts | 33 +++++ .../features/settings/pages/site/site.html | 126 +++++++++--------- .../settings/pages/site/site.module.ts | 43 ++++-- src/core/features/settings/pages/site/site.ts | 91 ++++++++----- 10 files changed, 208 insertions(+), 126 deletions(-) create mode 100644 src/core/features/settings/pages/site/site-routing.ts diff --git a/src/addons/messages/messages-lazy.module.ts b/src/addons/messages/messages-lazy.module.ts index 35e2a2431..c606a6ae0 100644 --- a/src/addons/messages/messages-lazy.module.ts +++ b/src/addons/messages/messages-lazy.module.ts @@ -16,7 +16,6 @@ import { Injector, NgModule } from '@angular/core'; import { Route, RouterModule, ROUTES, Routes } from '@angular/router'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; -import { AddonMessagesSettingsHandlerService } from './services/handlers/settings'; export const AddonMessagesDiscussionRoute: Route = { path: 'discussion', @@ -46,11 +45,6 @@ function buildRoutes(injector: Injector): Routes { loadChildren: () => import('./pages/search/search.module') .then(m => m.AddonMessagesSearchPageModule), }, - { - path: AddonMessagesSettingsHandlerService.PAGE_NAME, - loadChildren: () => import('./pages/settings/settings.module') - .then(m => m.AddonMessagesSettingsPageModule), - }, { path: 'contacts', // 3.6 or greater. loadChildren: () => import('./pages/contacts/contacts.module') diff --git a/src/addons/messages/messages.module.ts b/src/addons/messages/messages.module.ts index 33b1124a0..d097a4363 100644 --- a/src/addons/messages/messages.module.ts +++ b/src/addons/messages/messages.module.ts @@ -22,7 +22,7 @@ import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-deleg import { AddonMessagesMainMenuHandler, AddonMessagesMainMenuHandlerService } from './services/handlers/mainmenu'; import { CoreCronDelegate } from '@services/cron'; import { CoreSettingsDelegate } from '@features/settings/services/settings-delegate'; -import { AddonMessagesSettingsHandler } from './services/handlers/settings'; +import { AddonMessagesSettingsHandler, AddonMessagesSettingsHandlerService } from './services/handlers/settings'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { AddonMessagesIndexLinkHandler } from './services/handlers/index-link'; @@ -35,6 +35,7 @@ import { AddonMessagesSendMessageUserHandler } from './services/handlers/user-se import { Network, NgZone } from '@singletons'; import { AddonMessagesSync } from './services/messages-sync'; import { AddonMessagesSyncCronHandler } from './services/handlers/sync-cron'; +import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing'; const mainMenuChildrenRoutes: Routes = [ { @@ -42,11 +43,18 @@ const mainMenuChildrenRoutes: Routes = [ loadChildren: () => import('./messages-lazy.module').then(m => m.AddonMessagesLazyModule), }, ]; +const preferencesRoutes: Routes = [ + { + path: AddonMessagesSettingsHandlerService.PAGE_NAME, + loadChildren: () => import('./pages/settings/settings.module').then(m => m.AddonMessagesSettingsPageModule), + }, +]; @NgModule({ imports: [ CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }), CoreMainMenuTabRoutingModule.forChild( mainMenuChildrenRoutes), + CoreSitePreferencesRoutingModule.forChild(preferencesRoutes), ], providers: [ { diff --git a/src/addons/messages/services/handlers/settings.ts b/src/addons/messages/services/handlers/settings.ts index d6e625767..40f29b1e2 100644 --- a/src/addons/messages/services/handlers/settings.ts +++ b/src/addons/messages/services/handlers/settings.ts @@ -16,7 +16,6 @@ import { Injectable } from '@angular/core'; import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; import { makeSingleton } from '@singletons'; import { AddonMessages } from '../messages'; -import { AddonMessagesMainMenuHandlerService } from './mainmenu'; /** * Message settings handler. @@ -24,7 +23,7 @@ import { AddonMessagesMainMenuHandlerService } from './mainmenu'; @Injectable({ providedIn: 'root' }) export class AddonMessagesSettingsHandlerService implements CoreSettingsHandler { - static readonly PAGE_NAME = 'settings'; + static readonly PAGE_NAME = 'messages'; name = 'AddonMessages'; priority = 600; @@ -49,7 +48,7 @@ export class AddonMessagesSettingsHandlerService implements CoreSettingsHandler return { icon: 'fas-comments', title: 'addon.messages.messages', - page: AddonMessagesMainMenuHandlerService.PAGE_NAME + '/' + AddonMessagesSettingsHandlerService.PAGE_NAME, + page: AddonMessagesSettingsHandlerService.PAGE_NAME, class: 'addon-messages-settings-handler', }; } diff --git a/src/addons/notifications/notifications-lazy.module.ts b/src/addons/notifications/notifications-lazy.module.ts index 556467262..890e8df7c 100644 --- a/src/addons/notifications/notifications-lazy.module.ts +++ b/src/addons/notifications/notifications-lazy.module.ts @@ -16,7 +16,6 @@ import { Injector, NgModule } from '@angular/core'; import { RouterModule, ROUTES, Routes } from '@angular/router'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; -import { AddonNotificationsSettingsHandlerService } from './services/handlers/settings'; function buildRoutes(injector: Injector): Routes { return [ @@ -24,10 +23,6 @@ function buildRoutes(injector: Injector): Routes { path: 'list', loadChildren: () => import('./pages/list/list.module').then(m => m.AddonNotificationsListPageModule), }, - { - path: AddonNotificationsSettingsHandlerService.PAGE_NAME, - loadChildren: () => import('./pages/settings/settings.module').then(m => m.AddonNotificationsSettingsPageModule), - }, ...buildTabMainRoutes(injector, { redirectTo: 'list', pathMatch: 'full', diff --git a/src/addons/notifications/notifications.module.ts b/src/addons/notifications/notifications.module.ts index 34217e31a..01abc8f39 100644 --- a/src/addons/notifications/notifications.module.ts +++ b/src/addons/notifications/notifications.module.ts @@ -24,7 +24,8 @@ import { CoreSettingsDelegate } from '@features/settings/services/settings-deleg import { AddonNotificationsMainMenuHandler, AddonNotificationsMainMenuHandlerService } from './services/handlers/mainmenu'; import { AddonNotificationsCronHandler } from './services/handlers/cron'; import { AddonNotificationsPushClickHandler } from './services/handlers/push-click'; -import { AddonNotificationsSettingsHandler } from './services/handlers/settings'; +import { AddonNotificationsSettingsHandler, AddonNotificationsSettingsHandlerService } from './services/handlers/settings'; +import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing'; const routes: Routes = [ { @@ -32,11 +33,18 @@ const routes: Routes = [ loadChildren: () => import('@/addons/notifications/notifications-lazy.module').then(m => m.AddonNotificationsLazyModule), }, ]; +const preferencesRoutes: Routes = [ + { + path: AddonNotificationsSettingsHandlerService.PAGE_NAME, + loadChildren: () => import('./pages/settings/settings.module').then(m => m.AddonNotificationsSettingsPageModule), + }, +]; @NgModule({ imports: [ CoreMainMenuRoutingModule.forChild({ children: routes }), CoreMainMenuTabRoutingModule.forChild(routes), + CoreSitePreferencesRoutingModule.forChild(preferencesRoutes), ], exports: [CoreMainMenuRoutingModule], providers: [ diff --git a/src/addons/notifications/services/handlers/settings.ts b/src/addons/notifications/services/handlers/settings.ts index 6bea1480d..a67c415f4 100644 --- a/src/addons/notifications/services/handlers/settings.ts +++ b/src/addons/notifications/services/handlers/settings.ts @@ -18,7 +18,6 @@ import { CoreLocalNotifications } from '@services/local-notifications'; import { makeSingleton } from '@singletons'; import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; import { AddonNotifications } from '../notifications'; -import { AddonNotificationsMainMenuHandlerService } from './mainmenu'; /** * Notifications settings handler. @@ -26,7 +25,7 @@ import { AddonNotificationsMainMenuHandlerService } from './mainmenu'; @Injectable({ providedIn: 'root' }) export class AddonNotificationsSettingsHandlerService implements CoreSettingsHandler { - static readonly PAGE_NAME = 'settings'; + static readonly PAGE_NAME = 'notifications'; name = 'AddonNotifications'; priority = 500; @@ -50,7 +49,7 @@ export class AddonNotificationsSettingsHandlerService implements CoreSettingsHan return { icon: 'fas-bell', title: 'addon.notifications.notifications', - page: AddonNotificationsMainMenuHandlerService.PAGE_NAME + '/' + AddonNotificationsSettingsHandlerService.PAGE_NAME, + page: AddonNotificationsSettingsHandlerService.PAGE_NAME, class: 'addon-notifications-settings-handler', }; } diff --git a/src/core/features/settings/pages/site/site-routing.ts b/src/core/features/settings/pages/site/site-routing.ts new file mode 100644 index 000000000..367a3b92f --- /dev/null +++ b/src/core/features/settings/pages/site/site-routing.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 { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core'; + +import { ModuleRoutesConfig } from '@/app/app-routing.module'; + +export const SITE_PREFERENCES_ROUTES = new InjectionToken('SITE_PREFERENCES_ROUTES'); + +@NgModule() +export class CoreSitePreferencesRoutingModule { + + static forChild(routes: ModuleRoutesConfig): ModuleWithProviders { + return { + ngModule: CoreSitePreferencesRoutingModule, + providers: [ + { provide: SITE_PREFERENCES_ROUTES, multi: true, useValue: routes }, + ], + }; + } + +} diff --git a/src/core/features/settings/pages/site/site.html b/src/core/features/settings/pages/site/site.html index c4d9e4a08..6ee4b674d 100644 --- a/src/core/features/settings/pages/site/site.html +++ b/src/core/features/settings/pages/site/site.html @@ -9,72 +9,74 @@ - - - - - - - -

{{siteInfo!.fullname}}

-

- -

-

{{ siteUrl }}

-
-
- - - - -

{{ 'core.sharedfiles.sharedfiles' | translate }}

-
- {{ iosSharedFiles }} -
- - - - - -

{{ handler.title | translate}}

-
-
- - - + + + + + + + -

{{ 'core.settings.spaceusage' | translate }} -

-

{{ spaceUsage.spaceUsage | coreBytesToSize }}

+

{{siteInfo!.fullname}}

+

+ +

+

{{ siteUrl }}

- - -
- + + + + + + + +

{{ handler.title | translate}}

- - - -
-
-
-
+ + + + +

{{ 'core.settings.spaceusage' | translate }} +

+

{{ spaceUsage.spaceUsage | coreBytesToSize }}

+
+ + + +
+ + +

{{ 'core.settings.synchronizenow' | translate }} + +

+
+ + + + +
+
+ + +
diff --git a/src/core/features/settings/pages/site/site.module.ts b/src/core/features/settings/pages/site/site.module.ts index 0e6c8a1b2..e6aa8aae0 100644 --- a/src/core/features/settings/pages/site/site.module.ts +++ b/src/core/features/settings/pages/site/site.module.ts @@ -12,26 +12,51 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { Injector, NgModule } from '@angular/core'; +import { RouterModule, ROUTES, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreSitePreferencesPage } from './site'; +import { conditionalRoutes, resolveModuleRoutes } from '@/app/app-routing.module'; +import { SITE_PREFERENCES_ROUTES } from './site-routing'; +import { CoreScreen } from '@services/screen'; -const routes: Routes = [ - { - path: '', - component: CoreSitePreferencesPage, - }, -]; +function buildRoutes(injector: Injector): Routes { + const routes = resolveModuleRoutes(injector, SITE_PREFERENCES_ROUTES); + + const mobileRoutes: Routes = [ + { + path: '', + component: CoreSitePreferencesPage, + }, + ...routes.siblings, + + ]; + + const tabletRoutes: Routes = [ + { + path: '', + component: CoreSitePreferencesPage, + children: routes.siblings, + }, + ]; + + return [ + ...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile), + ...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet), + ]; +} @NgModule({ + providers: [ + { provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] }, + ], declarations: [ CoreSitePreferencesPage, ], imports: [ - RouterModule.forChild(routes), CoreSharedModule, ], + exports: [RouterModule], }) export class CoreSitePreferencesPageModule {} diff --git a/src/core/features/settings/pages/site/site.ts b/src/core/features/settings/pages/site/site.ts index bfed21cdc..c2d1421aa 100644 --- a/src/core/features/settings/pages/site/site.ts +++ b/src/core/features/settings/pages/site/site.ts @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Params } from '@angular/router'; +import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { ActivatedRouteSnapshot, Params } from '@angular/router'; import { IonRefresher } from '@ionic/angular'; -import { CoreSettingsDelegate, CoreSettingsHandlerData } from '../../services/settings-delegate'; +import { CoreSettingsDelegate, CoreSettingsHandlerToDisplay } from '../../services/settings-delegate'; import { CoreEventObserver, CoreEvents, CoreEventSiteUpdatedData } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; @@ -26,7 +26,8 @@ import { CoreApp } from '@services/app'; import { CoreSiteInfo } from '@classes/site'; import { Translate } from '@singletons'; import { CoreNavigator } from '@services/navigator'; -import { CoreScreen } from '@services/screen'; +import { CorePageItemsListManager } from '@classes/page-items-list-manager'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** * Page that displays the list of site settings pages. @@ -35,12 +36,13 @@ import { CoreScreen } from '@services/screen'; selector: 'page-core-site-preferences', templateUrl: 'site.html', }) -export class CoreSitePreferencesPage implements OnInit, OnDestroy { +export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { + + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + + handlers: CoreSettingsSitePreferencesManager; isIOS: boolean; - selectedPage?: string; - - handlers: CoreSettingsHandlerData[] = []; siteId: string; siteInfo?: CoreSiteInfo; siteName?: string; @@ -50,7 +52,6 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy { spaceUsage: 0, }; - loaded = false; iosSharedFiles = 0; protected sitesObserver: CoreEventObserver; protected isDestroyed = false; @@ -59,6 +60,7 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy { this.isIOS = CoreApp.isIOS(); this.siteId = CoreSites.getCurrentSiteId(); + this.handlers = new CoreSettingsSitePreferencesManager(CoreSitePreferencesPage); this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, (data: CoreEventSiteUpdatedData) => { if (data.siteId == this.siteId) { @@ -70,30 +72,29 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy { /** * View loaded. */ - ngOnInit(): void { - // @todo this.selectedPage = route.snapshot.paramMap.get('page') || undefined; + async ngAfterViewInit(): Promise { + const pageToOpen = CoreNavigator.getRouteParam('page'); - this.fetchData().finally(() => { - this.loaded = true; + try { + await this.fetchData(); + } finally { - if (this.selectedPage) { - this.openHandler(this.selectedPage); - } else if (CoreScreen.isTablet) { - if (this.isIOS) { - // @todo - // this.openHandler('CoreSharedFilesListPage', { manage: true, siteId: this.siteId, hideSitePicker: true }); - } else if (this.handlers.length > 0) { - this.openHandler(this.handlers[0].page, this.handlers[0].params); - } + const handler = pageToOpen ? this.handlers.items.find(handler => handler.page == pageToOpen) : undefined; + + if (handler) { + this.handlers.select(handler); + this.handlers.watchSplitViewOutlet(this.splitView); + } else { + this.handlers.start(this.splitView); } - }); + } } /** * Fetch Data. */ protected async fetchData(): Promise { - this.handlers = CoreSettingsDelegate.getHandlers(); + this.handlers.setItems(CoreSettingsDelegate.getHandlers()); const currentSite = CoreSites.getCurrentSite(); this.siteInfo = currentSite!.getInfo(); @@ -170,18 +171,6 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy { } } - /** - * Open a handler. - * - * @param page Page to open. - * @param params Params of the page to open. - */ - openHandler(page: string, params?: Params): void { - this.selectedPage = page; - // this.splitviewCtrl.push(page, params); - CoreNavigator.navigateToSitePath(page, { params }); - } - /** * Show information about space usage actions. */ @@ -211,3 +200,33 @@ export class CoreSitePreferencesPage implements OnInit, OnDestroy { } } + +/** + * Helper class to manage sections. + */ +class CoreSettingsSitePreferencesManager extends CorePageItemsListManager { + + /** + * @inheritdoc + */ + protected getItemPath(handler: CoreSettingsHandlerToDisplay): string { + return handler.page; + } + + /** + * @inheritdoc + */ + protected getItemQueryParams(handler: CoreSettingsHandlerToDisplay): Params { + return handler.params || {}; + } + + /** + * @inheritdoc + */ + protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null { + // @todo: routeConfig doesn't have a path after refreshing the app. + // route.component is null too, and route.parent.url is empty. + return route.parent?.routeConfig?.path ?? null; + } + +} From e427ce656871c572371d5ea2798e6044fbfa038f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Feb 2021 14:29:59 +0100 Subject: [PATCH 04/10] MOBILE-3708 core: Remove todos related to split view navigation --- src/addons/calendar/pages/event/event.page.ts | 2 -- src/addons/notifications/pages/settings/settings.ts | 5 +---- src/core/components/iframe/iframe.ts | 2 -- src/core/components/infinite-loading/infinite-loading.ts | 4 ++-- src/core/components/navbar-buttons/navbar-buttons.ts | 1 - src/core/components/user-avatar/user-avatar.ts | 1 - src/core/directives/format-text.ts | 2 -- src/core/directives/link.ts | 2 -- src/core/directives/user-link.ts | 1 - .../block/components/only-title-block/only-title-block.ts | 1 - src/core/features/course/components/tag-area/tag-area.ts | 2 -- src/core/features/tag/components/list/list.ts | 1 - src/core/features/user/pages/profile/profile.page.ts | 4 +--- 13 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/addons/calendar/pages/event/event.page.ts b/src/addons/calendar/pages/event/event.page.ts index 43dc8a502..f260b272e 100644 --- a/src/addons/calendar/pages/event/event.page.ts +++ b/src/addons/calendar/pages/event/event.page.ts @@ -434,8 +434,6 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { * Open the page to edit the event. */ openEdit(): void { - // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. - // @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; CoreNavigator.navigateToSitePath('/calendar/edit', { params: { eventId: this.eventId } }); } diff --git a/src/addons/notifications/pages/settings/settings.ts b/src/addons/notifications/pages/settings/settings.ts index 12211e72c..6b430204d 100644 --- a/src/addons/notifications/pages/settings/settings.ts +++ b/src/addons/notifications/pages/settings/settings.ts @@ -38,7 +38,6 @@ import { AddonNotificationsPreferencesProcessorFormatted, } from '@addons/notifications/services/notifications-helper'; import { CoreNavigator } from '@services/navigator'; -// import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** * Page that displays notifications settings. @@ -61,7 +60,7 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy { protected updateTimeout?: number; - constructor() { // @todo @Optional() protected svComponent: CoreSplitViewComponent, + constructor() { this.notifPrefsEnabled = AddonNotifications.isNotificationPreferencesEnabled(); this.canChangeSound = CoreLocalNotifications.canDisableSound(); } @@ -196,8 +195,6 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy { * @param handlerData */ openExtraPreferences(handlerData: AddonMessageOutputHandlerData): void { - // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. - // @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; CoreNavigator.navigateToSitePath(handlerData.page, { params: handlerData.pageParams }); } diff --git a/src/core/components/iframe/iframe.ts b/src/core/components/iframe/iframe.ts index 70ffdffcc..bc4c7a656 100644 --- a/src/core/components/iframe/iframe.ts +++ b/src/core/components/iframe/iframe.ts @@ -75,8 +75,6 @@ export class CoreIframeComponent implements OnChanges { // Show loading only with external URLs. this.loading = !this.src || !CoreUrlUtils.isLocalFileUrl(this.src); - // @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; - // CoreIframeUtils.treatFrame(iframe, false, this.navCtrl); CoreIframeUtils.treatFrame(iframe, false); iframe.addEventListener('load', () => { diff --git a/src/core/components/infinite-loading/infinite-loading.ts b/src/core/components/infinite-loading/infinite-loading.ts index 69addc88d..169908475 100644 --- a/src/core/components/infinite-loading/infinite-loading.ts +++ b/src/core/components/infinite-loading/infinite-loading.ts @@ -132,9 +132,9 @@ export class CoreInfiniteLoadingComponent implements OnChanges { * Get the height of the element. * * @return Height. - * @todo erase is not needed: I'm depreacating it because if not needed or getBoundingClientRect has the same result, it should + * @todo erase if not needed: I'm depreacating it because if not needed or getBoundingClientRect has the same result, it should * be erased, also with getElementHeight - * @deprecated + * @deprecated since 3.9.5 */ getHeight(): number { // return this.element.nativeElement.getBoundingClientRect().height; diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts index f24c05a47..7bac66635 100644 --- a/src/core/components/navbar-buttons/navbar-buttons.ts +++ b/src/core/components/navbar-buttons/navbar-buttons.ts @@ -124,7 +124,6 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { * If both button containers have a context menu, merge them into a single one. * * @param buttonsContainer The container where the buttons will be moved. - * @todo */ protected mergeContextMenus(buttonsContainer: HTMLElement): void { // Check if both button containers have a context menu. diff --git a/src/core/components/user-avatar/user-avatar.ts b/src/core/components/user-avatar/user-avatar.ts index a89f204d5..b53ef95fe 100644 --- a/src/core/components/user-avatar/user-avatar.ts +++ b/src/core/components/user-avatar/user-avatar.ts @@ -137,7 +137,6 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { event.preventDefault(); event.stopPropagation(); - // @todo Decide which navCtrl to use. If this component is inside a split view, use the split view's master nav. CoreNavigator.navigateToSitePath('user', { params: { userId: this.userId, diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 9997a4d3e..8ad9dd230 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -471,8 +471,6 @@ export class CoreFormatTextDirective implements OnChanges { */ protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise { const canTreatVimeo = site?.isVersionGreaterEqualThan(['3.3.4', '3.4']) || false; - // @todo this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; - // @todo: Pass navCtrl to all treateFrame calls? const images = Array.from(div.querySelectorAll('img')); const anchors = Array.from(div.querySelectorAll('a')); diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index cdebc6524..0271143bc 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -56,8 +56,6 @@ export class CoreLinkDirective implements OnInit { ngOnInit(): void { this.inApp = typeof this.inApp == 'undefined' ? this.inApp : CoreUtils.isTrueOrOne(this.inApp); - // @todo: Handle split view? - this.element.addEventListener('click', async (event) => { if (event.defaultPrevented) { return; // Link already treated, stop. diff --git a/src/core/directives/user-link.ts b/src/core/directives/user-link.ts index f7261e1dc..7fff5949d 100644 --- a/src/core/directives/user-link.ts +++ b/src/core/directives/user-link.ts @@ -49,7 +49,6 @@ export class CoreUserLinkDirective implements OnInit { event.preventDefault(); event.stopPropagation(); - // @todo If this directive is inside a split view, use the split view's master nav. CoreNavigator.navigateToSitePath('user', { params: CoreObject.withoutEmpty({ userId: this.userId, diff --git a/src/core/features/block/components/only-title-block/only-title-block.ts b/src/core/features/block/components/only-title-block/only-title-block.ts index 4e777f6c0..a0be90b78 100644 --- a/src/core/features/block/components/only-title-block/only-title-block.ts +++ b/src/core/features/block/components/only-title-block/only-title-block.ts @@ -43,7 +43,6 @@ export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implemen * Go to the block page. */ gotoBlock(): void { - // @todo test that this is working properly. CoreNavigator.navigateToSitePath(this.link!, { params: this.linkParams }); } diff --git a/src/core/features/course/components/tag-area/tag-area.ts b/src/core/features/course/components/tag-area/tag-area.ts index 9f841771f..5c5f28c18 100644 --- a/src/core/features/course/components/tag-area/tag-area.ts +++ b/src/core/features/course/components/tag-area/tag-area.ts @@ -34,8 +34,6 @@ export class CoreCourseTagAreaComponent { * @param courseId The course to open. */ openCourse(courseId: number): void { - // @todo If this component is inside a split view, use the master nav to open it. - // const navCtrl = this.splitviewCtrl ? this.splitviewCtrl.getMasterNav() : this.navCtrl; CoreCourseHelper.getAndOpenCourse(courseId); } diff --git a/src/core/features/tag/components/list/list.ts b/src/core/features/tag/components/list/list.ts index dfcdcb931..286d8916c 100644 --- a/src/core/features/tag/components/list/list.ts +++ b/src/core/features/tag/components/list/list.ts @@ -40,7 +40,6 @@ export class CoreTagListComponent { fromContextId: tag.taginstancecontextid, }; - // @todo: Check split view to navigate on the outlet if any. CoreNavigator.navigateToSitePath('/tag/index', { params, preferCurrentTab: false }); } diff --git a/src/core/features/user/pages/profile/profile.page.ts b/src/core/features/user/pages/profile/profile.page.ts index 10d326490..7487b764a 100644 --- a/src/core/features/user/pages/profile/profile.page.ts +++ b/src/core/features/user/pages/profile/profile.page.ts @@ -261,8 +261,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { * Open the page with the user details. */ openUserDetails(): void { - // @todo: Navigate out of split view if this page is in the right pane. - CoreNavigator.navigate('../about', { + CoreNavigator.navigateToSitePath('user/about', { params: { courseId: this.courseId, userId: this.userId, @@ -277,7 +276,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { * @param handler Handler that was clicked. */ handlerClicked(event: Event, handler: CoreUserProfileHandlerData): void { - // @todo: Pass the right navCtrl if this page is in the right pane of split view. handler.action(event, this.user!, this.courseId); } From 81bf906fefc8dbd59e94c4ee722dba20cd16e4e0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Feb 2021 15:29:17 +0100 Subject: [PATCH 05/10] MOBILE-3708 core: Translate month names in datetime --- src/addons/calendar/pages/event/event.html | 2 +- src/addons/calendar/pages/event/event.page.ts | 4 ++ src/addons/calendar/pages/index/index.page.ts | 8 ++-- .../calendar/services/calendar-helper.ts | 2 +- .../addon-user-profile-field-datetime.html | 2 +- .../datetime/component/datetime.ts | 4 ++ .../settings/pages/site/site.module.ts | 4 +- src/core/services/lang.ts | 38 ++++++++++++++++++- 8 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/addons/calendar/pages/event/event.html b/src/addons/calendar/pages/event/event.html index 4a6a3802a..06771880f 100644 --- a/src/addons/calendar/pages/event/event.html +++ b/src/addons/calendar/pages/event/event.html @@ -161,7 +161,7 @@ diff --git a/src/addons/calendar/pages/event/event.page.ts b/src/addons/calendar/pages/event/event.page.ts index f260b272e..e1501f7ca 100644 --- a/src/addons/calendar/pages/event/event.page.ts +++ b/src/addons/calendar/pages/event/event.page.ts @@ -46,6 +46,7 @@ import { AddonCalendarReminderDBRecord } from '../../services/database/calendar' import { ActivatedRoute } from '@angular/router'; import { CoreScreen } from '@services/screen'; import { CoreConstants } from '@/core/constants'; +import { CoreLang } from '@services/lang'; /** * Page that displays a single calendar event. @@ -87,6 +88,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { isOnline = false; syncIcon = CoreConstants.ICON_LOADING; // Sync icon. isSplitViewOn = false; + monthNames?: string[]; constructor( @Optional() protected svComponent: CoreSplitViewComponent, @@ -137,6 +139,8 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { protected async asyncConstructor(): Promise { if (this.notificationsEnabled) { + this.monthNames = CoreLang.getMonthNames(); + this.reminders = await AddonCalendar.getEventReminders(this.eventId); this.defaultTime = await AddonCalendar.getDefaultNotificationTime() * 60; diff --git a/src/addons/calendar/pages/index/index.page.ts b/src/addons/calendar/pages/index/index.page.ts index 1e90f5ccc..7588b2b45 100644 --- a/src/addons/calendar/pages/index/index.page.ts +++ b/src/addons/calendar/pages/index/index.page.ts @@ -74,7 +74,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { loadUpcoming = false; filter: AddonCalendarFilter = { filtered: false, - courseId: -1, + courseId: undefined, categoryId: undefined, course: true, group: true, @@ -149,7 +149,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.filter = filterData; // Course viewed has changed, check if the user can create events for this course calendar. - this.canCreate = await AddonCalendarHelper.canEditEvents(this.filter['courseId']); + this.canCreate = await AddonCalendarHelper.canEditEvents(this.filter.courseId); }, ); @@ -170,12 +170,12 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.route.queryParams.subscribe(() => { this.eventId = CoreNavigator.getRouteNumberParam('eventId'); - this.filter.courseId = CoreNavigator.getRouteNumberParam('courseId') || -1; + this.filter.courseId = CoreNavigator.getRouteNumberParam('courseId'); this.year = CoreNavigator.getRouteNumberParam('year'); this.month = CoreNavigator.getRouteNumberParam('month'); this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming'); this.showCalendar = !this.loadUpcoming; - this.filter.filtered = this.filter.courseId > 0; + this.filter.filtered = !!this.filter.courseId; if (this.eventId) { // There is an event to load, open the event in a new state. diff --git a/src/addons/calendar/services/calendar-helper.ts b/src/addons/calendar/services/calendar-helper.ts index 881f133e1..8012c5bc2 100644 --- a/src/addons/calendar/services/calendar-helper.ts +++ b/src/addons/calendar/services/calendar-helper.ts @@ -721,7 +721,7 @@ export const AddonCalendarHelper = makeSingleton(AddonCalendarHelperProvider); */ export type AddonCalendarFilter = { filtered: boolean; // If filter enabled (some filters applied). - courseId: number; // Course Id to filter. + courseId: number | undefined; // Course Id to filter. categoryId?: number; // Category Id to filter. course: boolean; // Filter to show course events. group: boolean; // Filter to show group events. diff --git a/src/addons/userprofilefield/datetime/component/addon-user-profile-field-datetime.html b/src/addons/userprofilefield/datetime/component/addon-user-profile-field-datetime.html index fb8f85417..aa541feba 100644 --- a/src/addons/userprofilefield/datetime/component/addon-user-profile-field-datetime.html +++ b/src/addons/userprofilefield/datetime/component/addon-user-profile-field-datetime.html @@ -12,7 +12,7 @@ {{ field.name }} + [max]="max" [min]="min" [monthNames]="monthNames"> diff --git a/src/addons/userprofilefield/datetime/component/datetime.ts b/src/addons/userprofilefield/datetime/component/datetime.ts index b1c581be2..8f350740c 100644 --- a/src/addons/userprofilefield/datetime/component/datetime.ts +++ b/src/addons/userprofilefield/datetime/component/datetime.ts @@ -21,6 +21,7 @@ import { AuthEmailSignupProfileField } from '@features/login/services/login-help import { CoreUserProfileField } from '@features/user/services/user'; import { Translate } from '@singletons'; import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component'; +import { CoreLang } from '@services/lang'; /** * Directive to render a datetime user profile field. @@ -35,6 +36,7 @@ export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileField min?: number; max?: number; valueNumber = 0; + monthNames?: string[]; /** * Init the data when the field is meant to be displayed without editing. @@ -53,6 +55,8 @@ export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileField protected initForEdit(field: AuthEmailSignupProfileField): void { super.initForEdit(field); + this.monthNames = CoreLang.getMonthNames(); + // Check if it's only date or it has time too. const hasTime = CoreUtils.isTrueOrOne(field.param3); diff --git a/src/core/features/settings/pages/site/site.module.ts b/src/core/features/settings/pages/site/site.module.ts index e6aa8aae0..a160cc2fb 100644 --- a/src/core/features/settings/pages/site/site.module.ts +++ b/src/core/features/settings/pages/site/site.module.ts @@ -42,8 +42,8 @@ function buildRoutes(injector: Injector): Routes { ]; return [ - ...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile), - ...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet), + ...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile), + ...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet), ]; } diff --git a/src/core/services/lang.ts b/src/core/services/lang.ts index 81fdcd15a..89b987416 100644 --- a/src/core/services/lang.ts +++ b/src/core/services/lang.ts @@ -156,8 +156,6 @@ export class CoreLangProvider { // Use british english when parent english is loaded. moment.locale(language == 'en' ? 'en-gb' : language); - // @todo: Set data for ion-datetime. - this.currentLanguage = language; try { @@ -275,6 +273,42 @@ export class CoreLangProvider { return this.fallbackLanguage; } + /** + * Get translated month names. + * + * @return Translated month names. + */ + getMonthNames(): string[] { + return moment.months().map(this.capitalize.bind(this)); + } + + /** + * Get translated month short names. + * + * @return Translated month short names. + */ + getMonthShortNames(): string[] { + return moment.monthsShort().map(this.capitalize.bind(this)); + } + + /** + * Get translated day names. + * + * @return Translated day names. + */ + getDayNames(): string[] { + return moment.weekdays().map(this.capitalize.bind(this)); + } + + /** + * Get translated day short names. + * + * @return Translated day short names. + */ + getDayShortNames(): string[] { + return moment.weekdaysShort().map(this.capitalize.bind(this)); + } + /** * Get the full list of translations for a certain language. * From 3d80e5740237cbbe02c4452316cb21517f6f3baa Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 25 Feb 2021 12:16:23 +0100 Subject: [PATCH 06/10] MOBILE-3708 android: Handle back button in tabs --- src/app/app.component.ts | 2 + src/core/classes/tabs.ts | 41 +++++++------ .../features/mainmenu/pages/menu/menu.html | 3 +- src/core/features/mainmenu/pages/menu/menu.ts | 59 ++++++++++++++++++- src/core/services/app.ts | 43 +++----------- 5 files changed, 92 insertions(+), 56 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5d6154614..71bd22964 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -115,6 +115,8 @@ export class AppComponent implements OnInit, AfterViewInit { }); this.onPlatformReady(); + + // @todo: Quit app with back button. How to tell if we're at root level? } /** diff --git a/src/core/classes/tabs.ts b/src/core/classes/tabs.ts index 10e252956..8cc91af13 100644 --- a/src/core/classes/tabs.ts +++ b/src/core/classes/tabs.ts @@ -25,9 +25,9 @@ import { ElementRef, } from '@angular/core'; import { IonSlides } from '@ionic/angular'; +import { BackButtonEvent } from '@ionic/core'; import { Subscription } from 'rxjs'; -import { CoreApp } from '@services/app'; import { Platform, Translate } from '@singletons'; import { CoreSettingsHelper } from '@features/settings/services/settings-helper'; @@ -81,7 +81,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft protected selectHistory: string[] = []; protected firstSelectedTab?: string; // ID of the first selected tab to control history. - protected unregisterBackButtonAction: any; + protected backButtonFunction: (event: BackButtonEvent) => void; protected languageChangedSubscription?: Subscription; protected isInTransition = false; // Weather Slides is in transition. protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -91,6 +91,7 @@ export class CoreTabsBaseComponent implements OnInit, Aft constructor( protected element: ElementRef, ) { + this.backButtonFunction = this.backButtonClicked.bind(this); } /** @@ -171,43 +172,47 @@ export class CoreTabsBaseComponent implements OnInit, Aft this.calculateSlides(); - this.registerBackButtonAction(); + document.addEventListener('ionBackButton', this.backButtonFunction); } /** - * Register back button action. + * Back button clicked. + * + * @param event Event. */ - protected registerBackButtonAction(): void { - this.unregisterBackButtonAction = CoreApp.registerBackButtonAction(() => { - // The previous page in history is not the last one, we need the previous one. + protected backButtonClicked(event: BackButtonEvent): void { + event.detail.register(40, (processNextHandler: () => void) => { if (this.selectHistory.length > 1) { - const tabIndex = this.selectHistory[this.selectHistory.length - 2]; + // The previous page in history is not the last one, we need the previous one. + const previousTabId = this.selectHistory[this.selectHistory.length - 2]; // Remove curent and previous tabs from history. - this.selectHistory = this.selectHistory.filter((tabId) => this.selected != tabId && tabIndex != tabId); + this.selectHistory = this.selectHistory.filter((tabId) => this.selected != tabId && previousTabId != tabId); - this.selectTab(tabIndex); + this.selectTab(previousTabId); - return true; - } else if (this.selected != this.firstSelectedTab) { + return; + } + + if (this.firstSelectedTab && this.selected != this.firstSelectedTab) { // All history is gone but we are not in the first selected tab. this.selectHistory = []; - this.selectTab(this.firstSelectedTab!); + this.selectTab(this.firstSelectedTab); - return true; + return; } - return false; - }, 750); + processNextHandler(); + }); } /** * User left the page that contains the component. */ ionViewDidLeave(): void { - // Unregister the custom back button action for this page - this.unregisterBackButtonAction && this.unregisterBackButtonAction(); + // Unregister the custom back button action for this component. + document.removeEventListener('ionBackButton', this.backButtonFunction); this.isCurrentView = false; } diff --git a/src/core/features/mainmenu/pages/menu/menu.html b/src/core/features/mainmenu/pages/menu/menu.html index 916230a83..8042716f7 100644 --- a/src/core/features/mainmenu/pages/menu/menu.html +++ b/src/core/features/mainmenu/pages/menu/menu.html @@ -1,4 +1,5 @@ - + diff --git a/src/core/features/mainmenu/pages/menu/menu.ts b/src/core/features/mainmenu/pages/menu/menu.ts index 7dfca5ad7..9dd61a115 100644 --- a/src/core/features/mainmenu/pages/menu/menu.ts +++ b/src/core/features/mainmenu/pages/menu/menu.ts @@ -15,6 +15,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { IonTabs } from '@ionic/angular'; +import { BackButtonEvent } from '@ionic/core'; import { Subscription } from 'rxjs'; import { CoreApp } from '@services/app'; @@ -50,6 +51,11 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { protected pendingRedirect?: CoreRedirectPayload; protected urlToOpen?: string; protected keyboardObserver?: CoreEventObserver; + protected resizeFunction: () => void; + protected backButtonFunction: (event: BackButtonEvent) => void; + protected selectHistory: string[] = []; + protected selectedTab?: string; + protected firstSelectedTab?: string; @ViewChild('mainTabs') mainTabs?: IonTabs; @@ -57,7 +63,10 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { protected route: ActivatedRoute, protected changeDetector: ChangeDetectorRef, protected router: Router, - ) {} + ) { + this.resizeFunction = this.initHandlers.bind(this); + this.backButtonFunction = this.backButtonClicked.bind(this); + } /** * Initialize the component. @@ -100,7 +109,8 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { } }); - window.addEventListener('resize', this.initHandlers.bind(this)); + window.addEventListener('resize', this.resizeFunction); + document.addEventListener('ionBackButton', this.backButtonFunction); if (CoreApp.isIOS()) { // In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done. @@ -209,7 +219,8 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { ngOnDestroy(): void { this.subscription?.unsubscribe(); this.redirectObs?.off(); - window.removeEventListener('resize', this.initHandlers.bind(this)); + window.removeEventListener('resize', this.resizeFunction); + document.removeEventListener('ionBackButton', this.backButtonFunction); this.keyboardObserver?.off(); } @@ -262,4 +273,46 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { } } + /** + * Selected tab has changed. + * + * @param event Event. + */ + tabChanged(event: {tab: string}): void { + this.selectedTab = event.tab; + this.firstSelectedTab = this.firstSelectedTab ?? event.tab; + this.selectHistory.push(event.tab); + } + + /** + * Back button clicked. + * + * @param event Event. + */ + protected backButtonClicked(event: BackButtonEvent): void { + event.detail.register(20, (processNextHandler: () => void) => { + if (this.selectHistory.length > 1) { + // The previous page in history is not the last one, we need the previous one. + const previousTab = this.selectHistory[this.selectHistory.length - 2]; + + // Remove curent and previous tabs from history. + this.selectHistory = this.selectHistory.filter((tab) => this.selectedTab != tab && previousTab != tab); + + this.mainTabs?.select(previousTab); + + return; + } + + if (this.firstSelectedTab && this.selectedTab != this.firstSelectedTab) { + // All history is gone but we are not in the first selected tab. + this.selectHistory = []; + this.mainTabs?.select(this.firstSelectedTab); + + return; + } + + processNextHandler(); + }); + } + } diff --git a/src/core/services/app.ts b/src/core/services/app.ts index 808976e5a..abc92e0b4 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -56,7 +56,6 @@ export class CoreAppProvider { protected isKeyboardShown = false; protected keyboardOpening = false; protected keyboardClosing = false; - protected backActions: {callback: () => boolean; priority: number}[] = []; protected forceOffline = false; protected redirect?: CoreRedirectData; @@ -68,11 +67,6 @@ export class CoreAppProvider { this.schemaVersionsManager = new Promise(resolve => this.resolveSchemaVersionsManager = resolve); this.db = CoreDB.getDB(DBNAME); this.logger = CoreLogger.getInstance('CoreAppProvider'); - - // @todo - // this.platform.registerBackButtonAction(() => { - // this.backButtonAction(); - // }, 100); } /** @@ -592,37 +586,18 @@ export class CoreAppProvider { } /** - * The back button event is triggered when the user presses the native - * platform's back button, also referred to as the "hardware" back button. - * This event is only used within Cordova apps running on Android and - * Windows platforms. This event is not fired on iOS since iOS doesn't come - * with a hardware back button in the same sense an Android or Windows device - * does. + * Register a back button action. + * This function is deprecated and no longer works. You should now use Ionic events directly, please see: + * https://ionicframework.com/docs/developing/hardware-back-button * - * Registering a hardware back button action and setting a priority allows - * apps to control which action should be called when the hardware back - * button is pressed. This method decides which of the registered back button - * actions has the highest priority and should be called. - * - * @param callback Called when the back button is pressed, if this registered action has the highest priority. - * @param priority Set the priority for this action. All actions sorted by priority will be executed since one of - * them returns true. - * - Priorities higher or equal than 1000 will go before closing modals - * - Priorities lower than 500 will only be executed if you are in the first state of the app (before exit). + * @param callback Called when the back button is pressed. + * @param priority Priority. * @return A function that, when called, will unregister the back button action. + * @deprecated since 3.9.5 */ - registerBackButtonAction(callback: () => boolean, priority: number = 0): () => boolean { - const action = { callback, priority }; - - this.backActions.push(action); - - this.backActions.sort((a, b) => b.priority - a.priority); - - return (): boolean => { - const index = this.backActions.indexOf(action); - - return index >= 0 && !!this.backActions.splice(index, 1); - }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + registerBackButtonAction(callback: () => boolean, priority = 0): () => boolean { + return () => false; } /** From 65d097b4985b81add7c46b26507c8709003f814b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 25 Feb 2021 13:21:54 +0100 Subject: [PATCH 07/10] MOBILE-3708 grades: Complete grades types --- .../grades/pages/course/course.page.ts | 5 +- .../features/grades/services/grades-helper.ts | 147 ++++++++++-------- src/core/features/grades/services/grades.ts | 101 ++++++------ 3 files changed, 127 insertions(+), 126 deletions(-) diff --git a/src/core/features/grades/pages/course/course.page.ts b/src/core/features/grades/pages/course/course.page.ts index 7b3dac6df..f1126acd3 100644 --- a/src/core/features/grades/pages/course/course.page.ts +++ b/src/core/features/grades/pages/course/course.page.ts @@ -22,7 +22,6 @@ import { CoreGradesFormattedTable, CoreGradesFormattedTableColumn, CoreGradesFormattedTableRow, - CoreGradesFormattedTableRowFilled, CoreGradesHelper, } from '@features/grades/services/grades-helper'; import { CoreSites } from '@services/sites'; @@ -203,3 +202,7 @@ class CoreGradesCourseManager extends CorePageItemsListManager & { + id: number; +}; diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts index 08105547e..15bf71d7e 100644 --- a/src/core/features/grades/services/grades-helper.ts +++ b/src/core/features/grades/services/grades-helper.ts @@ -23,6 +23,8 @@ import { CoreGradesGradeItem, CoreGradesGradeOverview, CoreGradesTable, + CoreGradesTableColumn, + CoreGradesTableItemNameColumn, CoreGradesTableRow, } from '@features/grades/services/grades'; import { CoreTextUtils } from '@services/utils/text'; @@ -56,27 +58,31 @@ export class CoreGradesHelperProvider { rowclass: '', }; for (const name in tableRow) { - if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) { - let content = String(tableRow[name].content); + const column: CoreGradesTableColumn = tableRow[name]; - if (name == 'itemname') { - this.setRowIcon(row, content); - row.link = this.getModuleLink(content); - row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; - row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; - - content = content.replace(/<\/span>/gi, '\n'); - content = CoreTextUtils.cleanTags(content); - } else { - content = CoreTextUtils.replaceNewLines(content, '
'); - } - - if (content == ' ') { - content = ''; - } - - row[name] = content.trim(); + if (column.content === undefined || column.content === null) { + continue; } + + let content = String(column.content); + + if (name == 'itemname') { + this.setRowIcon(row, content); + row.link = this.getModuleLink(content); + row.rowclass += column.class.indexOf('hidden') >= 0 ? ' hidden' : ''; + row.rowclass += column.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; + + content = content.replace(/<\/span>/gi, '\n'); + content = CoreTextUtils.cleanTags(content); + } else { + content = CoreTextUtils.replaceNewLines(content, '
'); + } + + if (content == ' ') { + content = ''; + } + + row[name] = content.trim(); } return row; @@ -88,35 +94,41 @@ export class CoreGradesHelperProvider { * @param tableRow JSON object representing row of grades table data. * @return Formatted row object. */ - protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable { - const row: CoreGradesFormattedRowForTable = {}; + protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedTableRow { + const row: CoreGradesFormattedTableRow = {}; for (let name in tableRow) { - if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) { - let content = String(tableRow[name].content); + const column: CoreGradesTableColumn = tableRow[name]; - if (name == 'itemname') { - row.id = parseInt(tableRow[name]!.id.split('_')[1], 10); - row.colspan = tableRow[name]!.colspan; - row.rowspan = (tableRow.leader && tableRow.leader.rowspan) || 1; - - this.setRowIcon(row, content); - row.rowclass = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even'; - row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; - row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; - - content = content.replace(/<\/span>/gi, '\n'); - content = CoreTextUtils.cleanTags(content); - name = 'gradeitem'; - } else { - content = CoreTextUtils.replaceNewLines(content, '
'); - } - - if (content == ' ') { - content = ''; - } - - row[name] = content.trim(); + if (column.content === undefined || column.content === null) { + continue; } + + let content = String(column.content); + + if (name == 'itemname') { + const itemNameColumn = column; + + row.id = parseInt(itemNameColumn.id.split('_')[1], 10); + row.colspan = itemNameColumn.colspan; + row.rowspan = tableRow.leader?.rowspan || 1; + + this.setRowIcon(row, content); + row.rowclass = itemNameColumn.class.indexOf('leveleven') < 0 ? 'odd' : 'even'; + row.rowclass += itemNameColumn.class.indexOf('hidden') >= 0 ? ' hidden' : ''; + row.rowclass += itemNameColumn.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; + + content = content.replace(/<\/span>/gi, '\n'); + content = CoreTextUtils.cleanTags(content); + name = 'gradeitem'; + } else { + content = CoreTextUtils.replaceNewLines(content, '
'); + } + + if (content == ' ') { + content = ''; + } + + row[name] = content.trim(); } return row; @@ -147,9 +159,9 @@ export class CoreGradesHelperProvider { */ formatGradesTable(table: CoreGradesTable): CoreGradesFormattedTable { const maxDepth = table.maxdepth; - const formatted = { - columns: [] as any[], - rows: [] as any[], + const formatted: CoreGradesFormattedTable = { + columns: [], + rows: [], }; // Columns, in order. @@ -185,7 +197,7 @@ export class CoreGradesHelperProvider { } for (const colName in columns) { - if (typeof normalRow[colName] != 'undefined') { + if (normalRow && typeof normalRow[colName] != 'undefined') { formatted.columns.push({ name: colName, colspan: colName == 'gradeitem' ? maxDepth : 1, @@ -561,10 +573,7 @@ export class CoreGradesHelperProvider { * @param text HTML where the image will be rendered. * @return Row object with the image. */ - protected setRowIcon( - row: CoreGradesFormattedRowForTable | CoreGradesFormattedRow, - text: string, - ): CoreGradesFormattedRowForTable { + protected setRowIcon(row: T, text: string): T { text = text.replace('%2F', '/').replace('%2f', '/'); if (text.indexOf('/agg_mean') > -1) { @@ -683,10 +692,6 @@ export class CoreGradesHelperProvider { export const CoreGradesHelper = makeSingleton(CoreGradesHelperProvider); -// @todo formatted data types. -export type CoreGradesFormattedRowForTable = any; -export type CoreGradesFormattedTableColumn = any; - export type CoreGradesFormattedItem = CoreGradesGradeItem & { weight?: string; // Weight. grade?: string; // The grade formatted. @@ -696,15 +701,13 @@ export type CoreGradesFormattedItem = CoreGradesGradeItem & { average?: string; // Grade average. }; -export type CoreGradesFormattedRow = { +export type CoreGradesFormattedRowCommonData = { icon?: string; - link?: string | false; rowclass?: string; itemtype?: string; image?: string; itemmodule?: string; rowspan?: number; - itemname?: string; // The item returned data. weight?: string; // Weight column. grade?: string; // Grade column. range?: string;// Range column. @@ -716,20 +719,26 @@ export type CoreGradesFormattedRow = { contributiontocoursetotal?: string; // Contributiontocoursetotal column. }; -export type CoreGradesFormattedTableRow = CoreGradesFormattedTableRowFilled | CoreGradesFormattedTableRowEmpty; +export type CoreGradesFormattedRow = CoreGradesFormattedRowCommonData & { + link?: string | false; + itemname?: string; // The item returned data. +}; + export type CoreGradesFormattedTable = { columns: CoreGradesFormattedTableColumn[]; rows: CoreGradesFormattedTableRow[]; }; -export type CoreGradesFormattedTableRowFilled = { - // @todo complete types. - id: number; - itemtype: 'category' | 'leader'; - grade: unknown; - percentage: unknown; + +export type CoreGradesFormattedTableRow = CoreGradesFormattedRowCommonData & { + id?: number; + colspan?: number; + gradeitem?: string; // The item returned data. }; -type CoreGradesFormattedTableRowEmpty ={ - // + +export type CoreGradesFormattedTableColumn = { + name: string; + colspan: number; + hiddenPhone: boolean; }; /** diff --git a/src/core/features/grades/services/grades.ts b/src/core/features/grades/services/grades.ts index 74c680a9d..1cf28d739 100644 --- a/src/core/features/grades/services/grades.ts +++ b/src/core/features/grades/services/grades.ts @@ -515,64 +515,53 @@ export type CoreGradesTable = { * Grade table data item. */ export type CoreGradesTableRow = { - itemname?: { - class: string; // Class. - colspan: number; // Col span. - content: string; // Cell content. - celltype: string; // Cell type. - id: string; // Id. - }; // The item returned data. - leader?: { - class: string; // Class. - rowspan: number; // Row span. - }; // The item returned data. - weight?: { - class: string; // Class. - content: string; // Cell content. - headers: string; // Headers. - }; // Weight column. - grade?: { - class: string; // Class. - content: string; // Cell content. - headers: string; // Headers. - }; // Grade column. - range?: { - class: string; // Class. - content: string; // Cell content. - headers: string; // Headers. - }; // Range column. - percentage?: { - class: string; // Class. - content: string; // Cell content. - headers: string; // Headers. - }; // Percentage column. - lettergrade?: { - class: string; // Class. - content: string; // Cell content. - headers: string; // Headers. - }; // Lettergrade column. - rank?: { - class: string; // Class. - content: string; // Cell content. - headers: string; // Headers. - }; // Rank column. - average?: { - class: string; // Class. - content: string; // Cell content. - headers: string; // Headers. - }; // Average column. - feedback?: { - class: string; // Class. - content: string; // Cell content. - headers: string; // Headers. - }; // Feedback column. - contributiontocoursetotal?: { - class: string; // Class. - content: string; // Cell content. - headers: string; // Headers. - }; // Contributiontocoursetotal column. + itemname?: CoreGradesTableItemNameColumn; // The item returned data. + leader?: CoreGradesTableLeaderColumn; // The item returned data. + weight?: CoreGradesTableCommonColumn; // Weight column. + grade?: CoreGradesTableCommonColumn; // Grade column. + range?: CoreGradesTableCommonColumn; // Range column. + percentage?: CoreGradesTableCommonColumn; // Percentage column. + lettergrade?: CoreGradesTableCommonColumn; // Lettergrade column. + rank?: CoreGradesTableCommonColumn; // Rank column. + average?: CoreGradesTableCommonColumn; // Average column. + feedback?: CoreGradesTableCommonColumn; // Feedback column. + contributiontocoursetotal?: CoreGradesTableCommonColumn; // Contributiontocoursetotal column. }; +/** + * Grade table common column data. + */ +export type CoreGradesTableCommonColumn = { + class: string; // Class. + content: string; // Cell content. + headers: string; // Headers. +}; + +/** + * Grade table item name column. + */ +export type CoreGradesTableItemNameColumn = { + class: string; // Class. + colspan: number; // Col span. + content: string; // Cell content. + celltype: string; // Cell type. + id: string; // Id. +}; + +/** + * Grade table leader column. + */ +export type CoreGradesTableLeaderColumn = { + class: string; // Class. + rowspan: number; // Row span. + content: undefined; // The WS doesn't return this data, but we declare it to make it coherent with the other columns. +}; + +/** + * Grade table column. + */ +export type CoreGradesTableColumn = CoreGradesTableCommonColumn | CoreGradesTableItemNameColumn | CoreGradesTableLeaderColumn; + /** * Grade overview data. */ From 200b01eef6b1e343dd833e36c48513ad53b40258 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 25 Feb 2021 14:44:06 +0100 Subject: [PATCH 08/10] MOBILE-3708 core: Always remove empty params in navigator --- src/addons/badges/pages/user-badges/user-badges.ts | 5 ++--- src/addons/badges/services/handlers/user.ts | 3 +-- .../assign/pages/submission-list/submission-list.page.ts | 5 ++--- src/core/directives/user-link.ts | 6 ++---- src/core/features/grades/pages/course/course.page.ts | 3 +-- src/core/features/login/services/login-helper.ts | 3 +-- src/core/guards/redirect.ts | 9 ++++----- src/core/services/navigator.ts | 2 +- 8 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/addons/badges/pages/user-badges/user-badges.ts b/src/addons/badges/pages/user-badges/user-badges.ts index 13b36bb52..79c621e77 100644 --- a/src/addons/badges/pages/user-badges/user-badges.ts +++ b/src/addons/badges/pages/user-badges/user-badges.ts @@ -22,7 +22,6 @@ import { CoreUtils } from '@services/utils/utils'; import { CorePageItemsListManager } from '@classes/page-items-list-manager'; import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { CoreObject } from '@singletons/object'; /** * Page that displays the list of calendar events. @@ -125,10 +124,10 @@ class AddonBadgesUserBadgesManager extends CorePageItemsListManager Date: Thu, 25 Feb 2021 15:38:06 +0100 Subject: [PATCH 09/10] MOBILE-3708 format-text: Fix magnifying glass --- src/core/directives/format-text.ts | 4 +- .../viewer/components/image/image.html | 4 +- .../features/viewer/components/image/image.ts | 1 + src/theme/components/format-text.scss | 38 +++++++++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 8ad9dd230..9b3d9cb96 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -208,8 +208,8 @@ export class CoreFormatTextDirective implements OnChanges { anchor.classList.add('core-image-viewer-icon'); anchor.setAttribute('aria-label', label); - // @todo Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed. - anchor.innerHTML = ''; + // Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed. + anchor.innerHTML = ''; anchor.addEventListener('click', (e: Event) => { e.preventDefault(); diff --git a/src/core/features/viewer/components/image/image.html b/src/core/features/viewer/components/image/image.html index d293205fb..e911e9d56 100644 --- a/src/core/features/viewer/components/image/image.html +++ b/src/core/features/viewer/components/image/image.html @@ -8,7 +8,7 @@ - - + + diff --git a/src/core/features/viewer/components/image/image.ts b/src/core/features/viewer/components/image/image.ts index ed6a64f0b..ddb52fe3d 100644 --- a/src/core/features/viewer/components/image/image.ts +++ b/src/core/features/viewer/components/image/image.ts @@ -22,6 +22,7 @@ import { ModalController, Translate } from '@singletons'; @Component({ selector: 'core-viewer-image', templateUrl: 'image.html', + styleUrls: ['image.scss'], }) export class CoreViewerImageComponent implements OnInit { diff --git a/src/theme/components/format-text.scss b/src/theme/components/format-text.scss index d8a8c498a..e94b4814e 100644 --- a/src/theme/components/format-text.scss +++ b/src/theme/components/format-text.scss @@ -1,5 +1,7 @@ /** Format Text - Show more styles. */ /** Styles of elements inside the directive should be placed in format-text.scss */ +@import "~theme/globals"; + core-format-text { user-select: text; word-break: break-word; @@ -78,4 +80,40 @@ core-format-text { } } } + + .core-adapted-img-container { + position: relative; + display: inline-block; + width: 100%; + } + + .core-image-viewer-icon { + position: absolute; + @include position(null, 10px, 10px, null); + color: var(--black); + border-radius: 5px; + background-color: rgba(255, 255, 255, .5); + text-align: center; + cursor: pointer; + + width: 32px; + height: 32px; + max-width: 32px; + line-height: 32px; + font-size: 24px; + + ion-icon { + margin-top: 3px; + } + + &:hover { + opacity: .7; + } + } +} + +body.dark { + core-format-text .core-image-viewer-icon { + background-color: rgba(0, 0, 0, .5); + } } From 2c625850bcb3a505662e454afab009359ef370bf Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 26 Feb 2021 08:41:00 +0100 Subject: [PATCH 10/10] MOBILE-3708 iframe: Display warning in offline --- src/core/services/utils/iframe.ts | 90 +++++++++++++++++++++++++------ src/core/singletons/index.ts | 2 +- src/theme/theme.base.scss | 10 +++- 3 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index 587759265..f14a84b34 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -25,10 +25,11 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; -import { makeSingleton, Network, Platform, NgZone } from '@singletons'; +import { makeSingleton, Network, Platform, NgZone, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreUrl } from '@singletons/url'; import { CoreWindow } from '@singletons/window'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; /** * Possible types of frame elements. @@ -74,21 +75,7 @@ export class CoreIframeUtilsProvider { } // The frame has an online URL but the app is offline. Show a warning, or a link if the URL can be opened in the app. - const div = document.createElement('div'); - - div.setAttribute('text-center', ''); - div.setAttribute('padding', ''); - div.classList.add('core-iframe-offline-warning'); - - // @todo Handle link - - // Add a class to specify that the iframe is hidden. - element.classList.add('core-iframe-offline-disabled'); - - if (isSubframe) { - // We cannot apply CSS styles in subframes, just hide the iframe. - element.style.display = 'none'; - } + this.addOfflineWarning(element, src, isSubframe); // If the network changes, check it again. const subscription = Network.onConnect().subscribe(() => { @@ -124,6 +111,77 @@ export class CoreIframeUtilsProvider { return false; } + /** + * Add an offline warning message. + * + * @param element The frame to check (iframe, embed, ...). + * @param src Frame src. + * @param isSubframe Whether it's a frame inside another frame. + * @return Promise resolved when done. + */ + protected async addOfflineWarning(element: HTMLElement, src: string, isSubframe?: boolean): Promise { + const site = CoreSites.getCurrentSite(); + const username = site ? site.getInfo()?.username : undefined; + + const div = document.createElement('div'); + div.classList.add('core-iframe-offline-warning', 'ion-padding', 'ion-text-center'); + + // Add a class to specify that the iframe is hidden. + element.classList.add('core-iframe-offline-disabled'); + if (isSubframe) { + // We cannot apply CSS styles in subframes, just hide the iframe. + element.style.display = 'none'; + } + + const canHandleLink = await CoreContentLinksHelper.canHandleLink(src, undefined, username); + + if (!canHandleLink) { + // @todo: The not connected icon isn't seen due to the div's height. Also, it's quite big. + div.innerHTML = (isSubframe ? '' : '
') + + '

' + Translate.instant('core.networkerroriframemsg') + '

'; + + element.parentElement?.insertBefore(div, element); + + return; + } + + let link: HTMLElement | undefined; + + if (isSubframe) { + // Ionic styles are not available in subframes, adding some minimal inline styles. + link = document.createElement('a'); + link.style.display = 'block'; + link.style.padding = '1em'; + link.style.fontWeight = '500'; + link.style.textAlign = 'center'; + link.style.textTransform = 'uppercase'; + link.style.cursor = 'pointer'; + } else { + link = document.createElement('ion-button'); + link.setAttribute('expand', 'block'); + link.setAttribute('size', 'default'); + link.classList.add( + 'button', + 'button-block', + 'button-default', + 'button-solid', + 'ion-activatable', + 'ion-focusable', + ); + } + + link.innerHTML = Translate.instant('core.viewembeddedcontent'); + + link.onclick = (event: Event): void => { + CoreContentLinksHelper.handleLink(src, username); + event.preventDefault(); + }; + + div.appendChild(link); + + element.parentElement?.insertBefore(div, element); + } + /** * Given an element, return the content window and document. * Please notice that the element should be an iframe, embed or similar. diff --git a/src/core/singletons/index.ts b/src/core/singletons/index.ts index 7cf4d1e73..6999ef29c 100644 --- a/src/core/singletons/index.ts +++ b/src/core/singletons/index.ts @@ -252,4 +252,4 @@ export const NavController = makeSingleton(NavControllerService); export const Router = makeSingleton(RouterService, ['routerState', 'url']); // Convert external libraries injectables. -export const Translate = makeSingleton(TranslateService, ['onLangChange']); +export const Translate = makeSingleton(TranslateService, ['onLangChange', 'translations']); diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index a8c693a6f..43dfa088c 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -95,7 +95,8 @@ ion-button.button-small ion-icon.faicon[slot] { } // Ionic alert. -ion-alert.core-alert-network-error .alert-head { +ion-alert.core-alert-network-error .alert-head, +div.core-iframe-network-error { position: relative; content: " "; background: url("/assets/fonts/font-awesome/solid/wifi.svg") no-repeat 50% 50%; @@ -113,7 +114,8 @@ ion-alert.core-alert-network-error .alert-head { mask: url("/assets/fonts/font-awesome/solid/exclamation-triangle.svg") no-repeat 50% 50%; } } -[dir=rtl] ion-alert.core-alert-network-error .alert-head::after { +[dir=rtl] ion-alert.core-alert-network-error .alert-head::after, +[dir=rtl] div.core-iframe-network-error::after { right: unset; left: -15%; } @@ -442,3 +444,7 @@ ion-button.core-button-select { .core-monospaced { font-family: Andale Mono,Monaco,Courier New,DejaVu Sans Mono,monospace; } + +.core-iframe-offline-disabled { + display: none !important; +}