refactor: simplify favorites and mensa dashboard sections

This commit is contained in:
Thea Schöbl
2023-03-22 15:33:07 +00:00
committed by Rainer Killinger
parent 47565e51b0
commit 23bd5a431c
27 changed files with 172 additions and 472 deletions

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Observable} from 'rxjs';
/**
*
*/
export function fromMutationObserver(
target: Node,
options?: MutationObserverInit,
): Observable<MutationRecord[]> {
return new Observable(subscriber => {
const observer = new MutationObserver(mutations => {
subscriber.next(mutations);
});
observer.observe(target, options);
return () => {
observer.disconnect();
};
});
}

View File

@@ -50,6 +50,9 @@
ion-content {
--background: var(--ion-color-light);
--padding-bottom: var(--spacing-xl);
&::part(inner-scroll) {
scrollbar-gutter: stable;
}
}
.schedule {

View File

@@ -23,7 +23,6 @@ import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module';
import {SettingsProvider} from '../settings/settings.provider';
import {DashboardComponent} from './dashboard.component';
import {EditModalComponent} from './edit-modal/edit-modal.component';
import {SearchSectionComponent} from './sections/search-section/search-section.component';
import {NewsSectionComponent} from './sections/news-section/news-section.component';
import {MensaSectionComponent} from './sections/mensa-section/mensa-section.component';
@@ -46,7 +45,6 @@ const catalogRoutes: Routes = [
*/
@NgModule({
declarations: [
EditModalComponent,
SearchSectionComponent,
NewsSectionComponent,
MensaSectionComponent,
@@ -69,6 +67,5 @@ const catalogRoutes: Routes = [
NewsModule,
],
providers: [SettingsProvider, TranslatePipe],
exports: [EditModalComponent],
})
export class DashboardModule {}

View File

@@ -1,52 +0,0 @@
<!--
~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-header>
<ion-toolbar mode="ios">
<ion-title>{{ 'modal.settings' | translate | titlecase }}</ion-title>
<ion-button fill="clear" slot="start" (click)="dismissModal()">
{{ 'modal.DISMISS_CANCEL' | translate }}
</ion-button>
<ion-button fill="clear" slot="end" (click)="onSaveClick()">
<ion-label>{{ 'modal.DISMISS_CONFIRM' | translate }}</ion-label>
</ion-button>
</ion-toolbar>
</ion-header>
<ion-content>
<ng-container [ngSwitch]="true">
<ion-reorder-group
*ngSwitchCase="type === types.CHECKBOXES"
disabled="false"
(ionItemReorder)="doReorder($event)"
>
<!-- Default reorder icon, end aligned items -->
<ion-item *ngFor="let item of items">
<ion-reorder slot="start"></ion-reorder>
<ion-label>{{ item.labelLocalized }}</ion-label>
<ion-toggle slot="end" [checked]="item.active" [(ngModel)]="item.active"></ion-toggle>
</ion-item>
</ion-reorder-group>
<ion-radio-group *ngSwitchCase="type === types.RADIOBOXES" [(ngModel)]="selectedValue">
<ion-list-header>
<ion-label>{{ 'dashboard.canteens.choose_favorite' | translate }}</ion-label>
</ion-list-header>
<ion-item *ngFor="let item of items">
<ion-label>{{ item.labelLocalized }}</ion-label>
<ion-radio slot="end" [value]="item.id"></ion-radio>
</ion-item>
</ion-radio-group>
</ng-container>
</ion-content>

View File

@@ -1,3 +0,0 @@
:host {
--width: 100vw;
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {IonReorderGroup, ModalController} from '@ionic/angular';
import {ItemReorderEventDetail} from '@ionic/core';
import {EditModalItem, EditModalTypeEnum} from './edit-modal-type.enum';
/**
* Shows a modal window to sort and enable/disable menu items
*/
@Component({
selector: 'stapps-dashboard-edit-modal',
templateUrl: 'edit-modal.component.html',
styleUrls: ['edit-modal.component.scss'],
})
export class EditModalComponent implements OnInit {
@ViewChild(IonReorderGroup) reorderGroup: IonReorderGroup;
@Input() type: EditModalTypeEnum = EditModalTypeEnum.CHECKBOXES;
@Input() items: EditModalItem[];
@Input() selectedValue: string;
reorderedItems: EditModalItem[];
types = EditModalTypeEnum;
constructor(public modalController: ModalController) {}
ngOnInit() {
this.reorderedItems = this.items;
}
ionViewWillLeave() {
this.dismissModal();
}
doReorder(event: CustomEvent<ItemReorderEventDetail>) {
this.reorderedItems = event.detail.complete(this.reorderedItems);
}
onSaveClick() {
this.modalController.dismiss({
items: this.reorderedItems,
selectedValue: this.selectedValue,
});
}
dismissModal() {
this.modalController.dismiss();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 StApps
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
@@ -12,14 +12,8 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {animate, style, transition, trigger} from '@angular/animations';
export enum EditModalTypeEnum {
CHECKBOXES,
RADIOBOXES,
}
export interface EditModalItem {
id: unknown;
labelLocalized: string;
active: boolean;
}
export const fadeAnimation = trigger('fade', [
transition(':enter', [style({opacity: '0'}), animate(250, style({opacity: '1'}))]),
]);

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCBuildingCategories, SCThings, SCThingWithCategories} from '@openstapps/core';
const mensaCategories = new Set<SCBuildingCategories>(['canteen', 'cafe', 'student canteen', 'restaurant']);
/**
*
*/
export function isMensaThing(item: SCThings): boolean {
return (
(item as SCThingWithCategories<string, never>).categories?.some(category =>
mensaCategories.has(category as never),
) || false
);
}

View File

@@ -17,9 +17,9 @@
<ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/favorites']">
<ion-icon slot="icon-only" name="search" size="24"></ion-icon>
</ion-button>
<simple-swiper *ngIf="(items | async)?.length; else noItems">
<simple-swiper *ngIf="items | async as items; else noItems" @fade>
<stapps-data-list-item
*ngFor="let item of items | async"
*ngFor="let item of items"
[hideThumbnail]="true"
[favoriteButton]="false"
[item]="item"

View File

@@ -12,22 +12,11 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {AlertController, AnimationController} from '@ionic/angular';
import {combineLatest} from 'rxjs';
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
import {NGXLogger} from 'ngx-logger';
import {SCThings} from '@openstapps/core';
import {DataProvider} from '../../../data/data.provider';
import {DataRoutingService} from '../../../data/data-routing.service';
import {SearchPageComponent} from '../../../data/list/search-page.component';
import {PositionService} from '../../../map/position.service';
import {SettingsProvider} from '../../../settings/settings.provider';
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {filter, map} from 'rxjs/operators';
import {FavoritesService} from '../../../favorites/favorites.service';
import {ContextMenuService} from '../../../menu/context/context-menu.service';
import {ConfigProvider} from '../../../config/config.provider';
import {fadeAnimation} from '../../fade.animation';
import {isMensaThing} from '../../mensa-filters';
/**
* Shows a section with meals of the chosen mensa
@@ -36,95 +25,14 @@ import {ConfigProvider} from '../../../config/config.provider';
selector: 'stapps-favorites-section',
templateUrl: 'favorites-section.component.html',
styleUrls: ['favorites-section.component.scss'],
animations: [fadeAnimation],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FavoritesSectionComponent extends SearchPageComponent implements OnInit {
constructor(
protected readonly alertController: AlertController,
protected dataProvider: DataProvider,
protected readonly contextMenuService: ContextMenuService,
protected readonly settingsProvider: SettingsProvider,
protected readonly logger: NGXLogger,
protected dataRoutingService: DataRoutingService,
protected router: Router,
route: ActivatedRoute,
positionService: PositionService,
private favoritesService: FavoritesService,
configProvider: ConfigProvider,
animationController: AnimationController,
) {
super(
alertController,
dataProvider,
contextMenuService,
settingsProvider,
logger,
dataRoutingService,
router,
route,
positionService,
configProvider,
animationController,
);
}
export class FavoritesSectionComponent {
items = this.favoritesService.favoriteThings$.pipe(
map(favorites => favorites.filter(it => !isMensaThing(it))),
filter(favorites => favorites.length > 0),
);
async initialize() {
this.subscriptions.push(
combineLatest([
this.queryTextChanged.pipe(
debounceTime(this.searchQueryDueTime),
distinctUntilChanged(),
startWith(this.queryText),
),
this.favoritesService.favoritesChanged$,
]).subscribe(async () => {
await this.fetchAndUpdateItems();
this.queryChanged.next();
}),
);
}
/**
* Fetches/updates the favorites (search page component's method override)
*/
async fetchAndUpdateItems() {
this.favoritesService
.search(this.queryText, this.filterQuery, this.sortQuery)
.pipe(take(1))
.subscribe(result => {
this.items = new Promise(resolve => {
resolve(result.data && result.data.filter(item => !this.isMensaThing(item)));
});
});
}
/**
* Helper function as 'typeof' is not accessible in HTML
*
* @param item TODO
*/
isMensaThing(item: SCThings): boolean {
return (
this.hasCategories(item) &&
((item.categories as string[]).includes('canteen') ||
(item.categories as string[]).includes('cafe') ||
(item.categories as string[]).includes('student canteen') ||
(item.categories as string[]).includes('restaurant'))
);
}
/**
* TODO
*
* @param item TODO
*/
hasCategories(item: SCThings): item is SCThings & {categories: string[]} {
return typeof (item as {categories: string[]}).categories !== 'undefined';
}
/**
* Emit event that an item was selected
*/
notifySelect(item: SCThings) {
this.dataRoutingService.emitChildEvent(item);
}
constructor(private favoritesService: FavoritesService) {}
}

View File

@@ -12,11 +12,11 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {SCDish, SCPlace, SCThings} from '@openstapps/core';
import {PlaceMensaService} from '../../../data/types/place/special/mensa/place-mensa-service';
import {animate, style, transition, trigger} from '@angular/animations';
import moment from 'moment';
import {fadeAnimation} from '../../fade.animation';
/**
* Shows a section with meals of the chosen mensa
@@ -25,11 +25,8 @@ import moment from 'moment';
selector: 'stapps-mensa-section-content',
templateUrl: 'mensa-section-content.component.html',
styleUrls: ['mensa-section-content.component.scss'],
animations: [
trigger('fade', [
transition(':enter', [style({opacity: '0'}), animate('500ms ease', style({opacity: '1'}))]),
]),
],
animations: [fadeAnimation],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MensaSectionContentComponent {
/**

View File

@@ -30,7 +30,7 @@
<ion-item class="nothing-selected" lines="none">
<ion-label class="ion-text-wrap">
{{ 'dashboard.canteens.no_favorite_prefix' | translate }}
<a (click)="onSectionEdit()">{{ 'dashboard.canteens.no_favorite_link' | translate }}</a>
<a [routerLink]="['/canteen']">{{ 'dashboard.canteens.no_favorite_link' | translate }}</a>
{{ 'dashboard.canteens.no_favorite_suffix' | translate }}
</ion-label>
</ion-item>

View File

@@ -12,23 +12,11 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {AlertController, AnimationController, ModalController} from '@ionic/angular';
import {combineLatest, Subscription} from 'rxjs';
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
import {NGXLogger} from 'ngx-logger';
import {SCThings} from '@openstapps/core';
import {DataProvider} from '../../../data/data.provider';
import {DataRoutingService} from '../../../data/data-routing.service';
import {FoodDataListComponent} from '../../../data/list/food-data-list.component';
import {PositionService} from '../../../map/position.service';
import {SettingsProvider} from '../../../settings/settings.provider';
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {map} from 'rxjs/operators';
import {FavoritesService} from '../../../favorites/favorites.service';
import {ContextMenuService} from '../../../menu/context/context-menu.service';
import {ConfigProvider} from '../../../config/config.provider';
import {animate, style, transition, trigger} from '@angular/animations';
import {fadeAnimation} from '../../fade.animation';
import {isMensaThing} from '../../mensa-filters';
/**
* Shows a section with meals of the chosen mensa
@@ -37,107 +25,11 @@ import {animate, style, transition, trigger} from '@angular/animations';
selector: 'stapps-mensa-section',
templateUrl: 'mensa-section.component.html',
styleUrls: ['mensa-section.component.scss'],
animations: [
trigger('fade', [transition(':enter', [style({opacity: '0'}), animate(250, style({opacity: '1'}))])]),
],
animations: [fadeAnimation],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MensaSectionComponent extends FoodDataListComponent {
sub: Subscription;
export class MensaSectionComponent {
items = this.favoritesService.favoriteThings$.pipe(map(favorites => favorites.filter(isMensaThing)));
constructor(
protected readonly alertController: AlertController,
protected dataProvider: DataProvider,
protected readonly contextMenuService: ContextMenuService,
protected readonly settingsProvider: SettingsProvider,
protected readonly logger: NGXLogger,
protected dataRoutingService: DataRoutingService,
protected router: Router,
route: ActivatedRoute,
protected positionService: PositionService,
public modalController: ModalController,
protected favoritesService: FavoritesService,
configProvider: ConfigProvider,
animationController: AnimationController,
) {
super(
alertController,
dataProvider,
contextMenuService,
settingsProvider,
logger,
dataRoutingService,
router,
route,
positionService,
configProvider,
animationController,
);
}
async initialize() {
super.initialize();
this.subscriptions.push(
combineLatest([
this.queryTextChanged.pipe(
debounceTime(this.searchQueryDueTime),
distinctUntilChanged(),
startWith(this.queryText),
),
this.favoritesService.favoritesChanged$,
]).subscribe(async query => {
this.queryText = query[0];
this.from = 0;
if (typeof this.filterQuery !== 'undefined' || this.queryText?.length > 0 || this.showDefaultData) {
await this.fetchAndUpdateItems();
this.queryChanged.next();
}
}),
);
}
/**
* Fetches/updates the favorites (search page component's method override)
*/
async fetchAndUpdateItems() {
this.favoritesService
.search(this.queryText, this.filterQuery, this.sortQuery)
.pipe(take(1))
.subscribe(result => {
this.items = new Promise(resolve => {
resolve(result.data && result.data.filter(item => this.isMensaThing(item)));
});
});
}
/**
* Helper function as 'typeof' is not accessible in HTML
*
* @param item TODO
*/
isMensaThing(item: SCThings): boolean {
return (
this.hasCategories(item) &&
((item.categories as string[]).includes('canteen') ||
(item.categories as string[]).includes('cafe') ||
(item.categories as string[]).includes('student canteen') ||
(item.categories as string[]).includes('restaurant'))
);
}
/**
* TODO
*
* @param item TODO
*/
hasCategories(item: SCThings): item is SCThings & {categories: string[]} {
return typeof (item as {categories: string[]}).categories !== 'undefined';
}
/**
* Action when user clicked edit to this section
*/
onSectionEdit() {
void this.router.navigate(['/canteen']);
}
constructor(protected favoritesService: FavoritesService) {}
}

View File

@@ -18,7 +18,6 @@ simple-swiper {
}
.more-news {
width: 128px;
font-size: var(--font-size-xl);
--color: var(--ion-color-medium-tint);

View File

@@ -12,11 +12,9 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component} from '@angular/core';
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {NewsProvider} from '../../../news/news.provider';
import {SCMessage} from '@openstapps/core';
import {animate, style, transition, trigger} from '@angular/animations';
import {Router} from '@angular/router';
import {fadeAnimation} from '../../fade.animation';
/**
* Shows a section with news
@@ -25,21 +23,14 @@ import {Router} from '@angular/router';
selector: 'stapps-news-section',
templateUrl: 'news-section.component.html',
styleUrls: ['news-section.component.scss'],
animations: [
trigger('fade', [
transition(':enter', [
style({opacity: '0', transform: 'translateX(100px)'}),
animate('250ms ease', style({opacity: '1', transform: 'translateX(0)'})),
]),
]),
],
animations: [fadeAnimation],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NewsSectionComponent {
news: Promise<SCMessage[]>;
news = this.newsProvider
.getCurrentFilters()
// eslint-disable-next-line unicorn/prefer-top-level-await,unicorn/consistent-function-scoping
.then(filters => this.newsProvider.getList(5, 0, filters));
constructor(readonly newsProvider: NewsProvider, readonly router: Router) {
this.news = this.newsProvider
.getCurrentFilters()
.then(filters => this.newsProvider.getList(5, 0, filters));
}
constructor(readonly newsProvider: NewsProvider) {}
}

View File

@@ -15,7 +15,7 @@
<stapps-section title="{{ 'dashboard.navigation.item.search' | translate }}">
<ion-searchbar
[routerLink]="'/search'"
[routerLink]="['/search']"
[routerAnimation]="routeTransition"
class="stapps-searchbar ion-activatable ripple-parent"
>

View File

@@ -13,10 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component} from '@angular/core';
import {Router} from '@angular/router';
import {Capacitor} from '@capacitor/core';
import {Keyboard} from '@capacitor/keyboard';
import {AnimationBuilder, AnimationController} from '@ionic/angular';
import {AnimationController} from '@ionic/angular';
import {homePageSearchTransition} from './search-route-transition';
/**
@@ -28,29 +25,7 @@ import {homePageSearchTransition} from './search-route-transition';
styleUrls: ['search-section.component.scss'],
})
export class SearchSectionComponent {
searchTerm = '';
routeTransition = homePageSearchTransition(this.animationController);
routeTransition: AnimationBuilder;
constructor(private router: Router, private animationController: AnimationController) {
this.routeTransition = homePageSearchTransition(this.animationController);
}
/**
* User submits search
*/
onSubmitSearch() {
this.router
.navigate(['/search'], {queryParams: {query: this.searchTerm}})
.then(() => this.hideKeyboard());
}
/**
* Hides keyboard in native app environments
*/
hideKeyboard() {
if (Capacitor.isNativePlatform()) {
Keyboard.hide();
}
}
constructor(private animationController: AnimationController) {}
}

View File

@@ -51,6 +51,8 @@ export class FavoritesService {
// using debounce time 0 allows change detection to run through async suspension
favoritesChanged$ = this.favorites.pipe(debounceTime(0));
favoriteThings$ = this.favoritesChanged$.pipe(map(favorite => [...favorite.values()].map(it => it.data)));
static getDataFromFavorites(items: SCFavorite[]) {
return items.map(item => item.data);
}

View File

@@ -28,6 +28,7 @@
*ngFor="let link of item.links"
[routerLink]="link.link"
[disabled]="link.needsAuth && !isLoggedIn"
[detail]="false"
>
<div>
<ion-icon [name]="link.icon" size="36" color="dark"></ion-icon>

View File

@@ -12,6 +12,8 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
@use 'sass:math';
$width: 108px;
simple-swiper {
@@ -22,7 +24,7 @@ simple-swiper {
@each $i in 7, 6, 5, 4, 3, 2, 1 {
$max: #{($width + 8px) * $i};
@container (inline-size < #{$max}) {
--swiper-slide-width: #{100cqi / $i};
--swiper-slide-width: #{math.div(100cqi, $i)};
}
}
}

View File

@@ -1,24 +1,25 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, HostBinding, Input} from '@angular/core';
import {ChangeDetectionStrategy, Component, HostBinding, Input} from '@angular/core';
@Component({
selector: 'stapps-icon',
templateUrl: 'icon.html',
styleUrls: ['icon.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IconComponent {
@HostBinding('style.--size')

View File

@@ -25,14 +25,24 @@
</ng-template>
</ion-col>
<ng-container *ngIf="swiper">
<ng-container *ngIf="swiper | async as swiper">
<ion-col size="auto" class="swiper-button">
<ion-button fill="clear" color="medium" (click)="slidePrev()" [disabled]="false">
<ion-button
fill="clear"
color="medium"
(click)="swiper.scrollBy({left: -swiper.offsetWidth, behavior: 'smooth'})"
[disabled]="false"
>
<ion-icon size="24" slot="icon-only" name="chevron_left"></ion-icon>
</ion-button>
</ion-col>
<ion-col size="auto" class="swiper-button">
<ion-button fill="clear" color="medium" (click)="slideNext()" [disabled]="false">
<ion-button
fill="clear"
color="medium"
(click)="swiper.scrollBy({left: swiper.offsetWidth, behavior: 'smooth'})"
[disabled]="false"
>
<ion-icon size="24" slot="icon-only" name="chevron_right"></ion-icon>
</ion-button>
</ion-col>

View File

@@ -55,6 +55,7 @@ ion-col {
}
:host {
transition: height 250ms ease;
display: block;
padding: var(--spacing-sm) var(--spacing-md) var(--spacing-sm);
--swiper-scroll-padding: var(--spacing-md);

View File

@@ -12,8 +12,11 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AfterContentInit, Component, Input, OnDestroy, ViewContainerRef} from '@angular/core';
import {AfterContentInit, ChangeDetectionStrategy, Component, Input, ViewContainerRef} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {fromMutationObserver} from '../_helpers/rxjs/mutation-observer';
import {mergeMap, ReplaySubject, takeLast} from 'rxjs';
import {distinctUntilChanged, filter, map, startWith} from 'rxjs/operators';
/**
* Shows a horizontal list of action chips
@@ -22,50 +25,34 @@ import {SCThings} from '@openstapps/core';
selector: 'stapps-section',
templateUrl: 'section.component.html',
styleUrls: ['section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SectionComponent implements AfterContentInit, OnDestroy {
export class SectionComponent implements AfterContentInit {
@Input() title = '';
@Input() item?: SCThings;
mutationObserver: MutationObserver;
nativeElement = new ReplaySubject<HTMLElement>(1);
swiper?: HTMLElement;
swiper = this.nativeElement.pipe(
takeLast(1),
mergeMap(element =>
fromMutationObserver(element, {
childList: true,
subtree: true,
}).pipe(
startWith([]),
map(() => element.querySelector('simple-swiper') as HTMLElement),
distinctUntilChanged(),
filter(element => !!element),
),
),
);
constructor(readonly viewContainerRef: ViewContainerRef) {}
ngAfterContentInit() {
this.mutationObserver = new MutationObserver(() => {
const simpleSwiper = this.viewContainerRef.element.nativeElement.querySelector('simple-swiper');
if (!simpleSwiper) return;
this.swiper = simpleSwiper;
});
this.mutationObserver.observe(this.viewContainerRef.element.nativeElement, {
childList: true,
subtree: true,
});
}
slideNext() {
if (this.swiper) {
this.swiper.scrollBy({
left: this.swiper.offsetWidth,
behavior: 'smooth',
});
}
}
slidePrev() {
if (this.swiper) {
this.swiper.scrollBy({
left: -this.swiper.offsetWidth,
behavior: 'smooth',
});
}
}
ngOnDestroy() {
this.mutationObserver.disconnect();
this.nativeElement.next(this.viewContainerRef.element.nativeElement);
this.nativeElement.complete();
}
}

View File

@@ -13,15 +13,12 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, ContentChildren, ElementRef, ViewContainerRef} from '@angular/core';
import {ChangeDetectionStrategy, Component} from '@angular/core';
@Component({
selector: 'simple-swiper',
templateUrl: 'simple-swiper.html',
styleUrls: ['simple-swiper.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SimpleSwiperComponent {
constructor(readonly viewContainerRef: ViewContainerRef) {}
@ContentChildren('*') children: ElementRef<unknown>;
}
export class SimpleSwiperComponent {}

View File

@@ -29,7 +29,7 @@
gap: var(--swiper-gap, 0);
&::ng-deep > *:not(ion-button) {
&::ng-deep > * {
contain: layout;
scroll-snap-align: start;
scroll-margin-inline: var(--swiper-scroll-padding, 0);

View File

@@ -30,9 +30,10 @@ import {ThingTranslateModule} from '../translation/thing-translate.module';
import {SimpleSwiperComponent} from './simple-swiper.component';
import {SearchbarAutofocusDirective} from './searchbar-autofocus.directive';
import {SectionComponent} from './section.component';
import {RouterModule} from '@angular/router';
@NgModule({
imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild()],
imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild(), RouterModule],
declarations: [
ElementSizeChangeDirective,
ArrayLastPipe,