forked from CIT/Vmeda.Online
		
	Merge pull request #2661 from NoelDeMartin/MOBILE-3609
MOBILE-3609: Implement split view in settings
This commit is contained in:
		
						commit
						87aa244ad7
					
				@ -102,6 +102,12 @@ const appConfig = {
 | 
			
		||||
                selector: 'property',
 | 
			
		||||
                format: ['camelCase'],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                selector: 'property',
 | 
			
		||||
                modifiers: ['private'],
 | 
			
		||||
                format: ['camelCase'],
 | 
			
		||||
                leadingUnderscore: 'allow',
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        '@typescript-eslint/no-empty-function': 'error',
 | 
			
		||||
        '@typescript-eslint/no-empty-interface': 'off',
 | 
			
		||||
@ -201,7 +207,6 @@ const appConfig = {
 | 
			
		||||
        'no-new-wrappers': 'error',
 | 
			
		||||
        'no-sequences': 'error',
 | 
			
		||||
        'no-trailing-spaces': 'error',
 | 
			
		||||
        'no-underscore-dangle': 'error',
 | 
			
		||||
        'no-unused-labels': 'error',
 | 
			
		||||
        'no-var': 'error',
 | 
			
		||||
        'object-curly-spacing': ['error', 'always'],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										38
									
								
								.vscode/moodle.code-snippets
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								.vscode/moodle.code-snippets
									
									
									
									
										vendored
									
									
								
							@ -1,4 +1,42 @@
 | 
			
		||||
{
 | 
			
		||||
    "[Moodle] Component class": {
 | 
			
		||||
        "scope": "typescript",
 | 
			
		||||
        "prefix": "macomponent",
 | 
			
		||||
        "body": [
 | 
			
		||||
            "import { Component } from '@angular/core';",
 | 
			
		||||
            "",
 | 
			
		||||
            "@Component({",
 | 
			
		||||
            "    selector: '$2${TM_FILENAME_BASE}',",
 | 
			
		||||
            "    templateUrl: '${TM_FILENAME_BASE}.html',",
 | 
			
		||||
            "})",
 | 
			
		||||
            "export class ${1:${TM_FILENAME_BASE}}Component {",
 | 
			
		||||
            "",
 | 
			
		||||
            "    $0",
 | 
			
		||||
            "",
 | 
			
		||||
            "}",
 | 
			
		||||
            ""
 | 
			
		||||
        ],
 | 
			
		||||
        "description": "[Moodle] Create a Component class"
 | 
			
		||||
    },
 | 
			
		||||
    "[Moodle] Page class": {
 | 
			
		||||
        "scope": "typescript",
 | 
			
		||||
        "prefix": "mapage",
 | 
			
		||||
        "body": [
 | 
			
		||||
            "import { Component } from '@angular/core';",
 | 
			
		||||
            "",
 | 
			
		||||
            "@Component({",
 | 
			
		||||
            "    selector: 'page-$2${TM_FILENAME_BASE}',",
 | 
			
		||||
            "    templateUrl: '${TM_FILENAME_BASE}.html',",
 | 
			
		||||
            "})",
 | 
			
		||||
            "export class ${1:${TM_FILENAME_BASE}}Page {",
 | 
			
		||||
            "",
 | 
			
		||||
            "    $0",
 | 
			
		||||
            "",
 | 
			
		||||
            "}",
 | 
			
		||||
            ""
 | 
			
		||||
        ],
 | 
			
		||||
        "description": "[Moodle] Create a Page class"
 | 
			
		||||
    },
 | 
			
		||||
    "[Moodle] Service Singleton": {
 | 
			
		||||
        "scope": "typescript",
 | 
			
		||||
        "prefix": "massingleton",
 | 
			
		||||
 | 
			
		||||
@ -13,17 +13,105 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { InjectionToken, Injector, ModuleWithProviders, NgModule } from '@angular/core';
 | 
			
		||||
import { PreloadAllModules, RouterModule, ROUTES, Routes } from '@angular/router';
 | 
			
		||||
import {
 | 
			
		||||
    PreloadAllModules,
 | 
			
		||||
    RouterModule,
 | 
			
		||||
    Route,
 | 
			
		||||
    Routes,
 | 
			
		||||
    ROUTES,
 | 
			
		||||
    UrlMatcher,
 | 
			
		||||
    UrlMatchResult,
 | 
			
		||||
    UrlSegment,
 | 
			
		||||
    UrlSegmentGroup,
 | 
			
		||||
} from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { CoreArray } from '@singletons/array';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Build app routes.
 | 
			
		||||
 *
 | 
			
		||||
 * @param injector Module injector.
 | 
			
		||||
 * @return App routes.
 | 
			
		||||
 */
 | 
			
		||||
function buildAppRoutes(injector: Injector): Routes {
 | 
			
		||||
    return CoreArray.flatten(injector.get<Routes[]>(APP_ROUTES, []));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a url matcher that will only match when a given condition is met.
 | 
			
		||||
 *
 | 
			
		||||
 * @param condition Condition.
 | 
			
		||||
 * @return Conditional url matcher.
 | 
			
		||||
 */
 | 
			
		||||
function buildConditionalUrlMatcher(condition: () => boolean): UrlMatcher {
 | 
			
		||||
    // Create a matcher based on Angular's default matcher.
 | 
			
		||||
    // see https://github.com/angular/angular/blob/10.0.x/packages/router/src/shared.ts#L127
 | 
			
		||||
    return (segments: UrlSegment[], segmentGroup: UrlSegmentGroup, route: Route): UrlMatchResult | null => {
 | 
			
		||||
        // If the condition isn't met, the route will never match.
 | 
			
		||||
        if (!condition()) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { path, pathMatch } = route as { path: string; pathMatch?: 'full' };
 | 
			
		||||
        const posParams: Record<string, UrlSegment> = {};
 | 
			
		||||
        const isFullMatch = pathMatch === 'full';
 | 
			
		||||
        const parts = path.split('/');
 | 
			
		||||
 | 
			
		||||
        // The actual URL is shorter than the config, no match.
 | 
			
		||||
        if (parts.length > segments.length) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The config is longer than the actual URL but we are looking for a full match, return null.
 | 
			
		||||
        if (isFullMatch && (segmentGroup.hasChildren() || parts.length < segments.length)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check each config part against the actual URL.
 | 
			
		||||
        for (let index = 0; index < parts.length; index++) {
 | 
			
		||||
            const part = parts[index];
 | 
			
		||||
            const segment = segments[index];
 | 
			
		||||
            const isParameter = part.startsWith(':');
 | 
			
		||||
 | 
			
		||||
            if (isParameter) {
 | 
			
		||||
                posParams[part.substring(1)] = segment;
 | 
			
		||||
            } else if (part !== segment.path) {
 | 
			
		||||
                // The actual URL part does not match the config, no match.
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Return consumed segments with params.
 | 
			
		||||
        return { consumed: segments.slice(0, parts.length), posParams };
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ModuleRoutes = { children: Routes; siblings: Routes };
 | 
			
		||||
export type ModuleRoutesConfig = Routes | Partial<ModuleRoutes>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Configure routes so that they'll only match when a given condition is met.
 | 
			
		||||
 *
 | 
			
		||||
 * @param routes Routes.
 | 
			
		||||
 * @param condition Condition to determine if routes should be activated or not.
 | 
			
		||||
 * @return Conditional routes.
 | 
			
		||||
 */
 | 
			
		||||
export function conditionalRoutes(routes: Routes, condition: () => boolean): Routes {
 | 
			
		||||
    const conditionalMatcher = buildConditionalUrlMatcher(condition);
 | 
			
		||||
 | 
			
		||||
    return routes.map(route => ({
 | 
			
		||||
        ...route,
 | 
			
		||||
        matcher: conditionalMatcher,
 | 
			
		||||
    }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Resolve module routes.
 | 
			
		||||
 *
 | 
			
		||||
 * @param injector Module injector.
 | 
			
		||||
 * @param token Routes injection token.
 | 
			
		||||
 * @return Routes.
 | 
			
		||||
 */
 | 
			
		||||
export function resolveModuleRoutes(injector: Injector, token: InjectionToken<ModuleRoutesConfig[]>): ModuleRoutes {
 | 
			
		||||
    const configs = injector.get(token, []);
 | 
			
		||||
    const routes = configs.map(config => {
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ import { CoreMarkRequiredComponent } from './mark-required/mark-required';
 | 
			
		||||
import { CoreRecaptchaComponent } from './recaptcha/recaptcha';
 | 
			
		||||
import { CoreRecaptchaModalComponent } from './recaptcha/recaptcha-modal';
 | 
			
		||||
import { CoreShowPasswordComponent } from './show-password/show-password';
 | 
			
		||||
import { CoreSplitViewComponent } from './split-view/split-view';
 | 
			
		||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
 | 
			
		||||
import { CoreTabsComponent } from './tabs/tabs';
 | 
			
		||||
import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
 | 
			
		||||
@ -55,6 +56,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
 | 
			
		||||
        CoreRecaptchaComponent,
 | 
			
		||||
        CoreRecaptchaModalComponent,
 | 
			
		||||
        CoreShowPasswordComponent,
 | 
			
		||||
        CoreSplitViewComponent,
 | 
			
		||||
        CoreEmptyBoxComponent,
 | 
			
		||||
        CoreTabsComponent,
 | 
			
		||||
        CoreInfiniteLoadingComponent,
 | 
			
		||||
@ -85,6 +87,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
 | 
			
		||||
        CoreRecaptchaComponent,
 | 
			
		||||
        CoreRecaptchaModalComponent,
 | 
			
		||||
        CoreShowPasswordComponent,
 | 
			
		||||
        CoreSplitViewComponent,
 | 
			
		||||
        CoreEmptyBoxComponent,
 | 
			
		||||
        CoreTabsComponent,
 | 
			
		||||
        CoreInfiniteLoadingComponent,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								src/core/components/split-view/split-view.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/core/components/split-view/split-view.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ng-content></ng-content>
 | 
			
		||||
</ion-content>
 | 
			
		||||
<ion-router-outlet></ion-router-outlet>
 | 
			
		||||
<!-- @todo placeholder -->
 | 
			
		||||
							
								
								
									
										86
									
								
								src/core/components/split-view/split-view.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/core/components/split-view/split-view.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,86 @@
 | 
			
		||||
@import "~theme/breakpoints";
 | 
			
		||||
 | 
			
		||||
// @todo RTL layout
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    --side-width: 100%;
 | 
			
		||||
    --side-min-width: 270px;
 | 
			
		||||
    --side-max-width: 28%;
 | 
			
		||||
 | 
			
		||||
    top: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    inset: 0;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    flex-wrap: nowrap;
 | 
			
		||||
    contain: strict;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(ion-app.md) {
 | 
			
		||||
    --border: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, .13))));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(ion-app.ios) {
 | 
			
		||||
    --border: .55px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-250, #c8c7cc)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ion-content,
 | 
			
		||||
ion-router-outlet {
 | 
			
		||||
    top: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    box-shadow: none !important;
 | 
			
		||||
    z-index: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ion-content {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    order: -1;
 | 
			
		||||
    border-left: unset;
 | 
			
		||||
    border-right: unset;
 | 
			
		||||
    border-inline-start: 0;
 | 
			
		||||
    border-inline-end: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ion-router-outlet {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    display: none;
 | 
			
		||||
 | 
			
		||||
    ::ng-deep ion-header {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host(.outlet-activated) {
 | 
			
		||||
 | 
			
		||||
    ion-router-outlet {
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ion-content {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: $breakpoint-tablet) {
 | 
			
		||||
 | 
			
		||||
    ion-content {
 | 
			
		||||
        border-inline-end: var(--border);
 | 
			
		||||
        min-width: var(--side-min-width);
 | 
			
		||||
        max-width: var(--side-max-width);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    :host(.outlet-activated) ion-content {
 | 
			
		||||
        display: flex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								src/core/components/split-view/split-view.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/core/components/split-view/split-view.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { AfterViewInit, Component, HostBinding, OnDestroy, ViewChild } from '@angular/core';
 | 
			
		||||
import { IonRouterOutlet } from '@ionic/angular';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'core-split-view',
 | 
			
		||||
    templateUrl: 'split-view.html',
 | 
			
		||||
    styleUrls: ['split-view.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(IonRouterOutlet) outlet!: IonRouterOutlet;
 | 
			
		||||
    @HostBinding('class.outlet-activated') outletActivated = false;
 | 
			
		||||
 | 
			
		||||
    private subscriptions?: Subscription[];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngAfterViewInit(): void {
 | 
			
		||||
        this.outletActivated = this.outlet.isActivated;
 | 
			
		||||
        this.subscriptions = [
 | 
			
		||||
            this.outlet.activateEvents.subscribe(() => this.outletActivated = true),
 | 
			
		||||
            this.outlet.deactivateEvents.subscribe(() => this.outletActivated = false),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.subscriptions?.forEach(subscription => subscription.unsubscribe());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								src/core/features/settings/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/core/features/settings/constants.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
			
		||||
// (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.
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Settings section.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreSettingsSection = {
 | 
			
		||||
    name: string;
 | 
			
		||||
    path: string;
 | 
			
		||||
    icon: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Settings constants.
 | 
			
		||||
 */
 | 
			
		||||
export class CoreSettingsConstants {
 | 
			
		||||
 | 
			
		||||
    static readonly SECTIONS: CoreSettingsSection[] = [
 | 
			
		||||
        {
 | 
			
		||||
            name: 'general',
 | 
			
		||||
            path: 'general',
 | 
			
		||||
            icon: 'fas-wrench',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'spaceusage',
 | 
			
		||||
            path: 'spaceusage',
 | 
			
		||||
            icon: 'fas-tasks',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'synchronization',
 | 
			
		||||
            path: 'sync',
 | 
			
		||||
            icon: 'fas-sync-alt',
 | 
			
		||||
        },
 | 
			
		||||
        // @TODO sharedfiles
 | 
			
		||||
        {
 | 
			
		||||
            name: 'about',
 | 
			
		||||
            path: 'about',
 | 
			
		||||
            icon: 'fas-id-card',
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            {{ 'core.settings.appsettings' | translate}}
 | 
			
		||||
        </ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-item button (click)="openSettings('general')" [class.core-split-item-selected]="'general' == selectedPage" detail>
 | 
			
		||||
        <ion-icon name="fas-wrench" slot="start"></ion-icon>
 | 
			
		||||
        <ion-label>{{ 'core.settings.general' | translate }}</ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <ion-item button (click)="openSettings('spaceusage')" [class.core-split-item-selected]="'spaceusage' == selectedPage"
 | 
			
		||||
        detail>
 | 
			
		||||
        <ion-icon name="fas-tasks" slot="start"></ion-icon>
 | 
			
		||||
        <ion-label>{{ 'core.settings.spaceusage' | translate }}</ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <ion-item button (click)="openSettings('sync')" [class.core-split-item-selected]="'sync' == selectedPage" detail>
 | 
			
		||||
        <ion-icon name="fas-sync-alt" slot="start"></ion-icon>
 | 
			
		||||
        <ion-label>{{ 'core.settings.synchronization' | translate }}</ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <ion-item button *ngIf="isIOS" (click)="openSettings('sharedfiles', {manage: true})"
 | 
			
		||||
        [class.core-split-item-selected]="'sharedfiles' == selectedPage" detail>
 | 
			
		||||
        <ion-icon name="fas-folder" slot="start"></ion-icon>
 | 
			
		||||
        <ion-label>{{ 'core.sharedfiles.sharedfiles' | translate }}</ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <ion-item button (click)="openSettings('about')" [class.core-split-item-selected]="'about' == selectedPage" detail>
 | 
			
		||||
        <ion-icon name="fas-id-card" slot="start"></ion-icon>
 | 
			
		||||
        <ion-label>{{ 'core.settings.about' | translate }}</ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
</ion-content>
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
// (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 { CommonModule } from '@angular/common';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { IonicModule } from '@ionic/angular';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreSettingsAppPage } from './app';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: CoreSettingsAppPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        TranslateModule.forChild(),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        CoreSettingsAppPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class CoreSettingsAppPageModule {}
 | 
			
		||||
@ -1,68 +0,0 @@
 | 
			
		||||
// (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 { CoreApp } from '@services/app';
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, Params, Router } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * App settings menu page.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-core-app-settings',
 | 
			
		||||
    templateUrl: 'app.html',
 | 
			
		||||
})
 | 
			
		||||
export class CoreSettingsAppPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    // @ViewChild(CoreSplitViewComponent) splitviewCtrl?: CoreSplitViewComponent;
 | 
			
		||||
 | 
			
		||||
    isIOS: boolean;
 | 
			
		||||
    selectedPage?: string;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
        protected router: Router, // Will be removed when splitview is implemented
 | 
			
		||||
    ) {
 | 
			
		||||
        this.isIOS = CoreApp.instance.isIOS();
 | 
			
		||||
        this.selectedPage = route.snapshot.paramMap.get('page') || undefined;
 | 
			
		||||
 | 
			
		||||
        if (this.selectedPage) {
 | 
			
		||||
            this.openSettings(this.selectedPage);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View loaded.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        if (this.selectedPage) {
 | 
			
		||||
            this.openSettings(this.selectedPage);
 | 
			
		||||
        } /* else if (this.splitviewCtrl!.isOn()) {
 | 
			
		||||
            this.openSettings('general');
 | 
			
		||||
        }*/
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open a settings page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page Page to open.
 | 
			
		||||
     * @param params Params of the page to open.
 | 
			
		||||
     */
 | 
			
		||||
    openSettings(page: string, params?: Params): void {
 | 
			
		||||
        this.selectedPage = page;
 | 
			
		||||
        // this.splitviewCtrl!.push(page, params);
 | 
			
		||||
        this.router.navigate([page], { relativeTo: this.route, queryParams: params });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/core/features/settings/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/core/features/settings/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>{{ 'core.settings.appsettings' | translate }}</ion-title>
 | 
			
		||||
        <ion-buttons slot="end"></ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-split-view>
 | 
			
		||||
        <ion-list>
 | 
			
		||||
            <ion-item
 | 
			
		||||
                *ngFor="let section of sections"
 | 
			
		||||
                [class.core-selected-item]="section.name === activeSection"
 | 
			
		||||
                button
 | 
			
		||||
                detail
 | 
			
		||||
                (click)="openSection(section)"
 | 
			
		||||
            >
 | 
			
		||||
                <ion-icon [name]="section.icon" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'core.settings.' + section.name | translate }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
    </core-split-view>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										94
									
								
								src/core/features/settings/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/core/features/settings/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
			
		||||
// (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, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreScreen } from '@services/screen';
 | 
			
		||||
import { CoreSettingsConstants, CoreSettingsSection } from '@features/settings/constants';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-core-settings-index',
 | 
			
		||||
    templateUrl: 'index.html',
 | 
			
		||||
})
 | 
			
		||||
export class CoreSettingsIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    sections = CoreSettingsConstants.SECTIONS;
 | 
			
		||||
    activeSection?: string;
 | 
			
		||||
    layoutSubscription?: Subscription;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveSection());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ionViewWillEnter(): void {
 | 
			
		||||
        this.updateActiveSection();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.layoutSubscription?.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open a section page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param section Section to open.
 | 
			
		||||
     */
 | 
			
		||||
    openSection(section: CoreSettingsSection): void {
 | 
			
		||||
        const path = this.activeSection ? `../${section.path}` : section.path;
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.instance.navigate(path);
 | 
			
		||||
 | 
			
		||||
        this.updateActiveSection(section.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update active section.
 | 
			
		||||
     *
 | 
			
		||||
     * @param activeSection Active section.
 | 
			
		||||
     */
 | 
			
		||||
    private updateActiveSection(activeSection?: string): void {
 | 
			
		||||
        if (CoreScreen.instance.isMobile) {
 | 
			
		||||
            delete this.activeSection;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.activeSection = activeSection ?? this.guessActiveSection();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Guess active section looking at the current route.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Active section.
 | 
			
		||||
     */
 | 
			
		||||
    private guessActiveSection(): string | undefined {
 | 
			
		||||
        const activeSection = this.sections.find(
 | 
			
		||||
            section => CoreNavigator.instance.isCurrent(`**/settings/${section.path}`),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return activeSection?.name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -35,8 +35,8 @@
 | 
			
		||||
                <h2>
 | 
			
		||||
                    <core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text>
 | 
			
		||||
                </h2>
 | 
			
		||||
            <p>{{ site.fullName }}</p>
 | 
			
		||||
            <p>{{ site.siteUrl }}</p>
 | 
			
		||||
                <p>{{ site.fullName }}</p>
 | 
			
		||||
                <p>{{ site.siteUrl }}</p>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
            <ion-button fill="clear" slot="end" *ngIf="!isSynchronizing(site.id)" (click)="synchronize(site.id)"
 | 
			
		||||
                [title]="site.siteName" [attr.aria-label]="'core.settings.synchronizenow' | translate">
 | 
			
		||||
 | 
			
		||||
@ -12,14 +12,19 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
import { IonicModule } from '@ionic/angular';
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { TranslateModule } from '@ngx-translate/core';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: 'about',
 | 
			
		||||
        loadChildren: () => import('./pages/about/about.module').then(m => m.CoreSettingsAboutPageModule),
 | 
			
		||||
    },
 | 
			
		||||
import { conditionalRoutes } from '@/app/app-routing.module';
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreScreen } from '@services/screen';
 | 
			
		||||
 | 
			
		||||
import { CoreSettingsIndexPage } from './pages/index';
 | 
			
		||||
 | 
			
		||||
const sectionRoutes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: 'general',
 | 
			
		||||
        loadChildren: () => import('./pages/general/general.module').then(m => m.CoreSettingsGeneralPageModule),
 | 
			
		||||
@ -34,13 +39,43 @@ const routes: Routes = [
 | 
			
		||||
            import('./pages/synchronization/synchronization.module')
 | 
			
		||||
                .then(m => m.CoreSettingsSynchronizationPageModule),
 | 
			
		||||
    },
 | 
			
		||||
    // @todo sharedfiles
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        loadChildren: () => import('./pages/app/app.module').then(m => m.CoreSettingsAppPageModule),
 | 
			
		||||
        path: 'about',
 | 
			
		||||
        loadChildren: () => import('./pages/about/about.module').then(m => m.CoreSettingsAboutPageModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        matcher: segments => {
 | 
			
		||||
            const matches = CoreScreen.instance.isMobile ? segments.length === 0 : true;
 | 
			
		||||
 | 
			
		||||
            return matches ? { consumed: [] } : null;
 | 
			
		||||
        },
 | 
			
		||||
        component: CoreSettingsIndexPage,
 | 
			
		||||
        children: conditionalRoutes([
 | 
			
		||||
            {
 | 
			
		||||
                path: '',
 | 
			
		||||
                pathMatch: 'full',
 | 
			
		||||
                redirectTo: 'general',
 | 
			
		||||
            },
 | 
			
		||||
            ...sectionRoutes,
 | 
			
		||||
        ], () => !CoreScreen.instance.isMobile),
 | 
			
		||||
    },
 | 
			
		||||
    ...conditionalRoutes(sectionRoutes, () => CoreScreen.instance.isMobile),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [RouterModule.forChild(routes)],
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        IonicModule,
 | 
			
		||||
        TranslateModule,
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        CoreSettingsIndexPage,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class CoreSettingsLazyModule {}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								src/core/initializers/watch-screen-viewport.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/core/initializers/watch-screen-viewport.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
// (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 { CoreScreen } from '@services/screen';
 | 
			
		||||
 | 
			
		||||
export default function(): void {
 | 
			
		||||
    CoreScreen.instance.watchViewport();
 | 
			
		||||
}
 | 
			
		||||
@ -25,6 +25,7 @@ import { CoreObject } from '@singletons/object';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreUrlUtils } from '@services/utils/url';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { makeSingleton, NavController, Router } from '@singletons';
 | 
			
		||||
 | 
			
		||||
const DEFAULT_MAIN_MENU_TAB = CoreMainMenuHomeHandlerService.PAGE_NAME;
 | 
			
		||||
@ -55,11 +56,11 @@ export class CoreNavigatorService {
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether the active route is using the given path.
 | 
			
		||||
     *
 | 
			
		||||
     * @param path Path.
 | 
			
		||||
     * @param path Path, can be a glob pattern.
 | 
			
		||||
     * @return Whether the active route is using the given path.
 | 
			
		||||
     */
 | 
			
		||||
    isCurrent(path: string): boolean {
 | 
			
		||||
        return this.getCurrentPath() === path;
 | 
			
		||||
        return CoreTextUtils.instance.matchesGlob(this.getCurrentPath(), path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -195,7 +196,7 @@ export class CoreNavigatorService {
 | 
			
		||||
    protected getCurrentRoute(route?: ActivatedRoute): ActivatedRoute {
 | 
			
		||||
        route = route ?? Router.instance.routerState.root;
 | 
			
		||||
 | 
			
		||||
        return route.children.length === 0 ? route : this.getCurrentRoute(route.children[0]);
 | 
			
		||||
        return route.firstChild ? this.getCurrentRoute(route.firstChild) : route;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										143
									
								
								src/core/services/screen.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/core/services/screen.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,143 @@
 | 
			
		||||
// (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 { Injectable } from '@angular/core';
 | 
			
		||||
import { BehaviorSubject, Observable } from 'rxjs';
 | 
			
		||||
import { distinctUntilChanged, map } from 'rxjs/operators';
 | 
			
		||||
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Screen breakpoints.
 | 
			
		||||
 *
 | 
			
		||||
 * @see https://ionicframework.com/docs/layout/grid#default-breakpoints
 | 
			
		||||
 */
 | 
			
		||||
enum Breakpoint {
 | 
			
		||||
    ExtraSmall = 'xs',
 | 
			
		||||
    Small = 'sm',
 | 
			
		||||
    Medium = 'md',
 | 
			
		||||
    Large = 'lg',
 | 
			
		||||
    ExtraLarge = 'xl',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const BREAKPOINT_NAMES = Object.values(Breakpoint);
 | 
			
		||||
const BREAKPOINT_WIDTHS: Record<Breakpoint, number> = {
 | 
			
		||||
    [Breakpoint.ExtraSmall]: 0,
 | 
			
		||||
    [Breakpoint.Small]: 576,
 | 
			
		||||
    [Breakpoint.Medium]: 768,
 | 
			
		||||
    [Breakpoint.Large]: 992,
 | 
			
		||||
    [Breakpoint.ExtraLarge]: 1200,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Screen layouts.
 | 
			
		||||
 */
 | 
			
		||||
export enum CoreScreenLayout {
 | 
			
		||||
    Mobile = 'mobile',
 | 
			
		||||
    Tablet = 'tablet',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manage application screen.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class CoreScreenService {
 | 
			
		||||
 | 
			
		||||
    protected breakpointsSubject: BehaviorSubject<Record<Breakpoint, boolean>>;
 | 
			
		||||
    private _layoutObservable: Observable<CoreScreenLayout>;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.breakpointsSubject = new BehaviorSubject(BREAKPOINT_NAMES.reduce((breakpoints, breakpoint) => ({
 | 
			
		||||
            ...breakpoints,
 | 
			
		||||
            [breakpoint]: false,
 | 
			
		||||
        }), {} as Record<Breakpoint, boolean>));
 | 
			
		||||
 | 
			
		||||
        this._layoutObservable = this.breakpointsObservable.pipe(
 | 
			
		||||
            map(this.calculateLayout.bind(this)),
 | 
			
		||||
            distinctUntilChanged<CoreScreenLayout>(),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get breakpoints(): Record<Breakpoint, boolean> {
 | 
			
		||||
        return this.breakpointsSubject.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get breakpointsObservable(): Observable<Record<Breakpoint, boolean>> {
 | 
			
		||||
        return this.breakpointsSubject.asObservable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get layout(): CoreScreenLayout {
 | 
			
		||||
        return this.calculateLayout(this.breakpointsSubject.value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get layoutObservable(): Observable<CoreScreenLayout> {
 | 
			
		||||
        return this._layoutObservable;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get isMobile(): boolean {
 | 
			
		||||
        return this.layout === CoreScreenLayout.Mobile;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get isTablet(): boolean {
 | 
			
		||||
        return this.layout === CoreScreenLayout.Tablet;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Watch viewport changes.
 | 
			
		||||
     */
 | 
			
		||||
    watchViewport(): void {
 | 
			
		||||
        for (const breakpoint of BREAKPOINT_NAMES) {
 | 
			
		||||
            const width = BREAKPOINT_WIDTHS[breakpoint];
 | 
			
		||||
            const mediaQuery = window.matchMedia(`(min-width: ${width}px)`);
 | 
			
		||||
 | 
			
		||||
            this.updateBreakpointVisibility(breakpoint, mediaQuery.matches);
 | 
			
		||||
 | 
			
		||||
            mediaQuery.onchange = (({ matches }) => this.updateBreakpointVisibility(breakpoint, matches));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update breakpoint visibility.
 | 
			
		||||
     *
 | 
			
		||||
     * @param breakpoint Breakpoint.
 | 
			
		||||
     * @param visible Visible.
 | 
			
		||||
     */
 | 
			
		||||
    protected updateBreakpointVisibility(breakpoint: Breakpoint, visible: boolean): void {
 | 
			
		||||
        if (this.breakpoints[breakpoint] === visible) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.breakpointsSubject.next({
 | 
			
		||||
            ...this.breakpoints,
 | 
			
		||||
            [breakpoint]: visible,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calculate the layout given the current breakpoints.
 | 
			
		||||
     *
 | 
			
		||||
     * @param breakpoints Breakpoints visibility.
 | 
			
		||||
     * @return Active layout.
 | 
			
		||||
     */
 | 
			
		||||
    protected calculateLayout(breakpoints: Record<Breakpoint, boolean>): CoreScreenLayout {
 | 
			
		||||
        if (breakpoints[Breakpoint.Large]) {
 | 
			
		||||
            return CoreScreenLayout.Tablet;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return CoreScreenLayout.Mobile;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class CoreScreen extends makeSingleton(CoreScreenService) {}
 | 
			
		||||
@ -19,6 +19,7 @@ import { mock, mockSingleton } from '@/testing/utils';
 | 
			
		||||
import { CoreNavigatorService } from '@services/navigator';
 | 
			
		||||
import { CoreUtils, CoreUtilsProvider } from '@services/utils/utils';
 | 
			
		||||
import { CoreUrlUtils, CoreUrlUtilsProvider } from '@services/utils/url';
 | 
			
		||||
import { CoreTextUtils, CoreTextUtilsProvider } from '@services/utils/text';
 | 
			
		||||
import { NavController, Router } from '@singletons';
 | 
			
		||||
import { ActivatedRoute, RouterState } from '@angular/router';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
@ -43,6 +44,7 @@ describe('CoreNavigator', () => {
 | 
			
		||||
        mockSingleton(Router, router);
 | 
			
		||||
        mockSingleton(CoreUtils, new CoreUtilsProvider(mock()));
 | 
			
		||||
        mockSingleton(CoreUrlUtils, new CoreUrlUtilsProvider());
 | 
			
		||||
        mockSingleton(CoreTextUtils, new CoreTextUtilsProvider(mock()));
 | 
			
		||||
        mockSingleton(CoreSites, { getCurrentSiteId: () => 42, isLoggedIn: () => true });
 | 
			
		||||
        mockSingleton(CoreMainMenu, { isMainMenuTab: path => Promise.resolve(currentMainMenuHandlers.includes(path)) });
 | 
			
		||||
    });
 | 
			
		||||
@ -73,9 +75,9 @@ describe('CoreNavigator', () => {
 | 
			
		||||
 | 
			
		||||
    it('navigates to relative paths', async () => {
 | 
			
		||||
        // Arrange.
 | 
			
		||||
        const mainOutletRoute = { routeConfig: { path: 'foo' }, children: [] };
 | 
			
		||||
        const primaryOutletRoute = { routeConfig: { path: 'main' }, children: [mainOutletRoute] };
 | 
			
		||||
        const rootRoute = { children: [primaryOutletRoute] };
 | 
			
		||||
        const mainOutletRoute = { routeConfig: { path: 'foo' } };
 | 
			
		||||
        const primaryOutletRoute = { routeConfig: { path: 'main' }, firstChild: mainOutletRoute };
 | 
			
		||||
        const rootRoute = { firstChild: primaryOutletRoute };
 | 
			
		||||
 | 
			
		||||
        router.routerState = { root: rootRoute as unknown as ActivatedRoute };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -78,4 +78,19 @@ describe('CoreTextUtilsProvider', () => {
 | 
			
		||||
        expect(CoreApp.instance.isAndroid).toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('matches glob patterns', () => {
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar', '/foo/bar')).toBe(true);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar', '/foo/bar/')).toBe(false);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo', '/foo/*')).toBe(false);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/', '/foo/*')).toBe(true);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar', '/foo/*')).toBe(true);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar/', '/foo/*')).toBe(false);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar/baz', '/foo/*')).toBe(false);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar/baz', '/foo/**')).toBe(true);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar/baz/', '/foo/**')).toBe(true);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar/baz', '**/baz')).toBe(true);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar/baz', '**/bar')).toBe(false);
 | 
			
		||||
        expect(textUtils.matchesGlob('/foo/bar/baz', '/foo/ba?/ba?')).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -644,6 +644,29 @@ export class CoreTextUtilsProvider {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether the given text matches a glob pattern.
 | 
			
		||||
     *
 | 
			
		||||
     * @param text Text to match against.
 | 
			
		||||
     * @param pattern Glob pattern.
 | 
			
		||||
     * @return Whether the pattern matches.
 | 
			
		||||
     */
 | 
			
		||||
    matchesGlob(text: string, pattern: string): boolean {
 | 
			
		||||
        pattern = pattern
 | 
			
		||||
            .replace(/\*\*/g, '%RECURSIVE_MATCH%')
 | 
			
		||||
            .replace(/\*/g, '%LOCAL_MATCH%')
 | 
			
		||||
            .replace(/\?/g, '%CHARACTER_MATCH%');
 | 
			
		||||
 | 
			
		||||
        pattern = this.escapeForRegex(pattern);
 | 
			
		||||
 | 
			
		||||
        pattern = pattern
 | 
			
		||||
            .replace(/%RECURSIVE_MATCH%/g, '.*')
 | 
			
		||||
            .replace(/%LOCAL_MATCH%/g, '[^/]*')
 | 
			
		||||
            .replace(/%CHARACTER_MATCH%/g, '[^/]');
 | 
			
		||||
 | 
			
		||||
        return new RegExp(`^${pattern}$`).test(text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Same as Javascript's JSON.parse, but it will handle errors.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								src/theme/breakpoints.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/theme/breakpoints.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Layout Breakpoints
 | 
			
		||||
 *
 | 
			
		||||
 * https://ionicframework.com/docs/layout/grid#default-breakpoints
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
$breakpoint-xs: 0px;
 | 
			
		||||
$breakpoint-sm: 576px;
 | 
			
		||||
$breakpoint-md: 768px;
 | 
			
		||||
$breakpoint-lg: 992px;
 | 
			
		||||
$breakpoint-xl: 1200px;
 | 
			
		||||
 | 
			
		||||
$breakpoint-tablet: $breakpoint-lg;
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user