MOBILE-3609 settings: Implement split view
parent
159613bdb5
commit
7f7f282e69
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<ion-content>
|
||||
<ng-content></ng-content>
|
||||
</ion-content>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
<!-- @todo placeholder -->
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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…
Reference in New Issue