diff --git a/.changeset/orange-knives-happen.md b/.changeset/orange-knives-happen.md new file mode 100644 index 00000000..b7ca4504 --- /dev/null +++ b/.changeset/orange-knives-happen.md @@ -0,0 +1,5 @@ +--- +'@openstapps/app': minor +--- + +Add directions to inPlace and place list items diff --git a/.changeset/proud-cameras-fail.md b/.changeset/proud-cameras-fail.md new file mode 100644 index 00000000..5b4f3d10 --- /dev/null +++ b/.changeset/proud-cameras-fail.md @@ -0,0 +1,8 @@ +--- +'@openstapps/app': minor +--- + +Adjust map button and item behavior on different screen sizes + +- Small screens will show the item without margins below the map actions +- Large screens will show the list item on the left side diff --git a/.changeset/thick-mails-peel.md b/.changeset/thick-mails-peel.md new file mode 100644 index 00000000..61e59d65 --- /dev/null +++ b/.changeset/thick-mails-peel.md @@ -0,0 +1,5 @@ +--- +'@openstapps/app': minor +--- + +Map items are now native list items diff --git a/frontend/app/.eslintrc.json b/frontend/app/.eslintrc.json index 4f31912f..29a37c94 100644 --- a/frontend/app/.eslintrc.json +++ b/frontend/app/.eslintrc.json @@ -8,7 +8,7 @@ "parserOptions": { "ecmaVersion": 2020, "sourceType": "module", - "project": ["tsconfig.json", "tsconfig.spec.json", "e2e/tsconfig.e2e.json"], + "project": ["tsconfig.json", "tsconfig.spec.json", "cypress/tsconfig.json"], "createDefaultProgram": true }, "extends": [ diff --git a/frontend/app/package.json b/frontend/app/package.json index f94ff0c8..3d39457b 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -63,6 +63,7 @@ "@awesome-cordova-plugins/core": "5.45.0", "@capacitor/app": "4.1.1", "@capacitor/browser": "4.1.0", + "@capacitor/clipboard": "4.1.0", "@capacitor/core": "4.6.1", "@capacitor/device": "4.1.0", "@capacitor/dialog": "4.1.0", @@ -87,6 +88,7 @@ "@openstapps/collection-utils": "workspace:*", "@openstapps/core": "workspace:*", "@transistorsoft/capacitor-background-fetch": "1.0.2", + "@types/dom-view-transitions": "1.0.1", "capacitor-secure-storage-plugin": "0.8.1", "cordova-plugin-calendar": "5.1.6", "deepmerge": "4.3.1", diff --git a/frontend/app/src/app/modules/data/chips/action-chip-list.component.ts b/frontend/app/src/app/modules/data/chips/action-chip-list.component.ts index 9a039dcc..667f631d 100644 --- a/frontend/app/src/app/modules/data/chips/action-chip-list.component.ts +++ b/frontend/app/src/app/modules/data/chips/action-chip-list.component.ts @@ -42,6 +42,7 @@ export class ActionChipListComponent { event: item.type === SCThingType.AcademicEvent || (item.type === SCThingType.DateSeries && (item as SCDateSeries).dates.length > 0), + navigate: ('inPlace' in item && item.inPlace && 'geo' in item.inPlace) || 'geo' in item, }; } diff --git a/frontend/app/src/app/modules/data/chips/action-chip-list.html b/frontend/app/src/app/modules/data/chips/action-chip-list.html index 42c80b7e..fe44b8c9 100644 --- a/frontend/app/src/app/modules/data/chips/action-chip-list.html +++ b/frontend/app/src/app/modules/data/chips/action-chip-list.html @@ -14,5 +14,6 @@ --> + diff --git a/frontend/app/src/app/modules/data/chips/data/navigate-action-chip.component.ts b/frontend/app/src/app/modules/data/chips/data/navigate-action-chip.component.ts new file mode 100644 index 00000000..017f057e --- /dev/null +++ b/frontend/app/src/app/modules/data/chips/data/navigate-action-chip.component.ts @@ -0,0 +1,35 @@ +/* + * 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 . + */ +import {Component, Input} from '@angular/core'; +import {SCPlaceWithoutReferences, SCThings} from '@openstapps/core'; + +@Component({ + selector: 'stapps-navigate-action-chip', + templateUrl: 'navigate-action-chip.html', + styleUrls: ['navigate-action-chip.scss'], +}) +export class NavigateActionChipComponent { + place: SCPlaceWithoutReferences; + + @Input({required: true}) set item(value: SCThings) { + if ('geo' in value) { + this.place = value; + } else if ('inPlace' in value && value.inPlace && 'geo' in value.inPlace) { + this.place = value.inPlace; + } else { + console.error('Invalid place', value); + } + } +} diff --git a/frontend/app/src/app/modules/map/page/modals/map-single.html b/frontend/app/src/app/modules/data/chips/data/navigate-action-chip.html similarity index 55% rename from frontend/app/src/app/modules/map/page/modals/map-single.html rename to frontend/app/src/app/modules/data/chips/data/navigate-action-chip.html index 5fe7c588..d99454a4 100644 --- a/frontend/app/src/app/modules/map/page/modals/map-single.html +++ b/frontend/app/src/app/modules/data/chips/data/navigate-action-chip.html @@ -1,5 +1,5 @@ - - - - {{ 'map.modals.single.TITLE' | translate }} - - {{ 'app.ui.CLOSE' | translate }} - - - - - - + + + {{'map.directions.TITLE' | translate}} + diff --git a/frontend/app/src/app/modules/map/page/modals/map-single-modal.component.ts b/frontend/app/src/app/modules/data/chips/data/navigate-action-chip.scss similarity index 55% rename from frontend/app/src/app/modules/map/page/modals/map-single-modal.component.ts rename to frontend/app/src/app/modules/data/chips/data/navigate-action-chip.scss index 4852d64e..bc381711 100644 --- a/frontend/app/src/app/modules/map/page/modals/map-single-modal.component.ts +++ b/frontend/app/src/app/modules/data/chips/data/navigate-action-chip.scss @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2021 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,20 +12,3 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, Input} from '@angular/core'; -import {SCPlace} from '@openstapps/core'; -import {ModalController} from '@ionic/angular'; - -@Component({ - selector: 'app-map-single-modal', - templateUrl: './map-single.html', - styleUrls: ['./map-single.scss'], -}) -export class MapSingleModalComponent { - /** - * The item to be shown - */ - @Input() item: SCPlace; - - constructor(readonly modalController: ModalController) {} -} diff --git a/frontend/app/src/app/modules/data/data.module.ts b/frontend/app/src/app/modules/data/data.module.ts index 8f04c248..bdfc0b23 100644 --- a/frontend/app/src/app/modules/data/data.module.ts +++ b/frontend/app/src/app/modules/data/data.module.ts @@ -102,6 +102,8 @@ import {StappsRatingComponent} from './elements/rating.component'; import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.component'; import {SkeletonListComponent} from './list/skeleton-list.component'; import {CertificationsInDetailComponent} from './elements/certifications-in-detail.component'; +import {GeoNavigationDirective} from '../map/geo-navigation.directive'; +import {NavigateActionChipComponent} from './chips/data/navigate-action-chip.component'; /** * Module for handling data @@ -110,6 +112,7 @@ import {CertificationsInDetailComponent} from './elements/certifications-in-deta declarations: [ ActionChipListComponent, AddEventActionChipComponent, + NavigateActionChipComponent, EditEventSelectionComponent, AddressDetailComponent, CatalogDetailContentComponent, @@ -194,6 +197,7 @@ import {CertificationsInDetailComponent} from './elements/certifications-in-deta TranslateModule.forChild(), ThingTranslateModule.forChild(), UtilModule, + GeoNavigationDirective, ], providers: [ CoordinatedSearchProvider, diff --git a/frontend/app/src/app/modules/data/types/date-series/date-series-detail-content.html b/frontend/app/src/app/modules/data/types/date-series/date-series-detail-content.html index 4a7632ad..a9b31a4b 100644 --- a/frontend/app/src/app/modules/data/types/date-series/date-series-detail-content.html +++ b/frontend/app/src/app/modules/data/types/date-series/date-series-detail-content.html @@ -63,3 +63,4 @@ + diff --git a/frontend/app/src/app/modules/data/types/place/place-detail-content.html b/frontend/app/src/app/modules/data/types/place/place-detail-content.html index 793a2ed4..811980ce 100644 --- a/frontend/app/src/app/modules/data/types/place/place-detail-content.html +++ b/frontend/app/src/app/modules/data/types/place/place-detail-content.html @@ -38,9 +38,5 @@ - + diff --git a/frontend/app/src/app/modules/data/types/place/place-detail-content.scss b/frontend/app/src/app/modules/data/types/place/place-detail-content.scss index e817313f..bc381711 100644 --- a/frontend/app/src/app/modules/data/types/place/place-detail-content.scss +++ b/frontend/app/src/app/modules/data/types/place/place-detail-content.scss @@ -12,10 +12,3 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -.map-widget { - position: relative; - width: auto; - height: 300px; - min-height: 300px; -} diff --git a/frontend/app/src/app/modules/map/geo-navigation.directive.ts b/frontend/app/src/app/modules/map/geo-navigation.directive.ts new file mode 100644 index 00000000..e12eabdd --- /dev/null +++ b/frontend/app/src/app/modules/map/geo-navigation.directive.ts @@ -0,0 +1,92 @@ +import {Directive, HostListener, Input} from '@angular/core'; +import {SCPlaceWithoutReferences, SCThings, SCThingWithoutReferences} from '@openstapps/core'; +import {Device} from '@capacitor/device'; +import {ActionSheetController, ActionSheetOptions, ToastController} from '@ionic/angular'; +import {TranslateService} from '@ngx-translate/core'; +import {ThingTranslateService} from '../../translation/thing-translate.service'; +import {Clipboard} from '@capacitor/clipboard'; +import {PositionService} from './position.service'; + +/** + * A button that provides navigation options to the user via an action sheet + * @example + * + * + * {{'map.directions.TITLE' | translate}} + * + */ +@Directive({ + selector: '[geoNavigation]', + standalone: true, +}) +export class GeoNavigationDirective { + @Input({required: true}) geoNavigation: SCThingWithoutReferences & + Pick; + + constructor( + private actionSheetController: ActionSheetController, + private translateService: TranslateService, + private thingTranslate: ThingTranslateService, + private toastController: ToastController, + private positionService: PositionService, + ) {} + + @HostListener('click', ['$event']) + async presentActionSheet(event: Event) { + event.stopPropagation(); + const {operatingSystem} = await Device.getInfo(); + const [lon, lat] = this.geoNavigation.geo.point.coordinates; + + const supportedMapProviders = + operatingSystem === 'mac' || operatingSystem === 'ios' + ? ['OSM_ROUTING', 'APPLE_MAPS', 'GOOGLE_MAPS'] + : ['OSM_ROUTING', 'GOOGLE_MAPS']; + const address = this.geoNavigation.address + ? this.translateService.instant( + 'map.directions.ADDRESS', + this.thingTranslate.get(this.geoNavigation as SCThings, 'address'), + ) + : `${lat}, ${lon}`; + + const options: ActionSheetOptions = { + header: this.translateService.instant('map.directions.TITLE_LONG', { + name: this.thingTranslate.get(this.geoNavigation as SCThings, 'name'), + }), + subHeader: address, + buttons: [ + { + text: this.translateService.instant('map.directions.COPY_ADDRESS'), + role: 'selected', + handler: async () => { + await Clipboard.write({string: address}); + this.toastController + .create({ + message: this.translateService.instant('map.directions.ADDRESS_COPIED'), + duration: 500, + }) + .then(toast => toast.present()); + }, + }, + ...supportedMapProviders.map(provider => ({ + text: this.translateService.instant(`map.directions.${provider}.TITLE`), + handler: () => { + const url: string = this.translateService.instant(`map.directions.${provider}.URL`, { + lat, + lon, + posLat: this.positionService.position?.latitude ?? 0, + posLon: this.positionService.position?.longitude ?? 0, + }); + window.open(url.replace(/&?\w+=0,0/, ''), '_blank', 'noreferrer'); + }, + })), + { + text: this.translateService.instant('abort'), + role: 'cancel', + }, + ], + }; + + const actionSheet = await this.actionSheetController.create(options); + await actionSheet.present(); + } +} diff --git a/frontend/app/src/app/modules/map/item/map-item.component.html b/frontend/app/src/app/modules/map/item/map-item.component.html deleted file mode 100644 index ffdb98bd..00000000 --- a/frontend/app/src/app/modules/map/item/map-item.component.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - {{ $any(item).inPlace.name }}, - {{ address.streetAddress }}, {{ address.addressLocality }} - - - {{ 'map.page.buttons.MORE' | translate }} - - diff --git a/frontend/app/src/app/modules/map/item/map-item.component.scss b/frontend/app/src/app/modules/map/item/map-item.component.scss deleted file mode 100644 index b33ba8e3..00000000 --- a/frontend/app/src/app/modules/map/item/map-item.component.scss +++ /dev/null @@ -1,72 +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 . - */ - -@import '../../../../theme/util/mixins'; - -:host { - display: block; - max-width: 100%; - - ion-card { - overflow: visible; - padding: 0; - - ion-card-header { - padding: 0; - border-bottom: var(--border-width-default) solid var(--border-color-default); - - stapps-data-list-item { - --ion-margin: 0; - - &::ng-deep ion-item { - --padding-start: 0; - --padding-end: 0; - - ion-label { - white-space: break-spaces; - } - } - } - - .close { - --padding-top: 0; - --padding-bottom: 0; - --padding-start: 0; - --padding-end: 0; - - position: absolute; - z-index: 1; - top: -15px; - right: -15px; - - ion-icon { - width: 30px; - height: 30px; - } - } - } - - ion-card-content { - display: flex; - flex-direction: row; - padding: var(--spacing-md); - - .show-more-button { - margin-left: auto; - text-transform: uppercase; - } - } - } -} diff --git a/frontend/app/src/app/modules/map/item/map-item.component.ts b/frontend/app/src/app/modules/map/item/map-item.component.ts deleted file mode 100644 index 856339ab..00000000 --- a/frontend/app/src/app/modules/map/item/map-item.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2021 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 . - */ -import {Component, EventEmitter, Input, Output} from '@angular/core'; -import {SCPlace} from '@openstapps/core'; -import {IonRouterOutlet} from '@ionic/angular'; - -@Component({ - selector: 'stapps-map-item', - templateUrl: './map-item.component.html', - styleUrls: ['./map-item.component.scss'], -}) -export class MapItemComponent { - /** - * An item to show - */ - @Input() item: SCPlace; - - // eslint-disable-next-line @angular-eslint/no-output-on-prefix - @Output() onClose = new EventEmitter(); - - constructor(readonly routerOutlet: IonRouterOutlet) {} - - /** - * Action when edit is clicked - */ - onCloseClick() { - this.onClose.emit(); - } -} diff --git a/frontend/app/src/app/modules/map/map.module.ts b/frontend/app/src/app/modules/map/map.module.ts index 5eaf5cd4..6d631630 100644 --- a/frontend/app/src/app/modules/map/map.module.ts +++ b/frontend/app/src/app/modules/map/map.module.ts @@ -29,12 +29,11 @@ import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider'; import {MenuModule} from '../menu/menu.module'; import {MapProvider} from './map.provider'; import {MapPageComponent} from './page/map-page.component'; -import {MapListModalComponent} from './page/modals/map-list-modal.component'; -import {MapSingleModalComponent} from './page/modals/map-single-modal.component'; -import {MapItemComponent} from './item/map-item.component'; +import {MapListModalComponent} from './page/map-list-modal.component'; import {NgModule} from '@angular/core'; import {UtilModule} from '../../util/util.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; +import {GeoNavigationDirective} from './geo-navigation.directive'; /** * Initializes the default area to show in advance (before components are initialized) @@ -56,7 +55,7 @@ const mapRoutes: Routes = [ * Module containing map related stuff */ @NgModule({ - declarations: [MapPageComponent, MapListModalComponent, MapSingleModalComponent, MapItemComponent], + declarations: [MapPageComponent, MapListModalComponent], exports: [], imports: [ CommonModule, @@ -71,6 +70,8 @@ const mapRoutes: Routes = [ FormsModule, ThingTranslateModule, UtilModule, + GeoNavigationDirective, + GeoNavigationDirective, ], providers: [Geolocation, MapProvider, DataProvider, DataFacetsProvider, StAppsWebHttpClient], }) diff --git a/frontend/app/src/app/modules/map/page/modals/map-list-modal.component.ts b/frontend/app/src/app/modules/map/page/map-list-modal.component.ts similarity index 95% rename from frontend/app/src/app/modules/map/page/modals/map-list-modal.component.ts rename to frontend/app/src/app/modules/map/page/map-list-modal.component.ts index 32cc9ff4..d7a89a84 100644 --- a/frontend/app/src/app/modules/map/page/modals/map-list-modal.component.ts +++ b/frontend/app/src/app/modules/map/page/map-list-modal.component.ts @@ -14,7 +14,7 @@ */ import {Component, Input, OnInit} from '@angular/core'; import {SCSearchBooleanFilter, SCPlace, SCSearchFilter} from '@openstapps/core'; -import {MapProvider} from '../../map.provider'; +import {MapProvider} from '../map.provider'; import {ModalController} from '@ionic/angular'; import {LatLngBounds} from 'leaflet'; @@ -23,8 +23,8 @@ import {LatLngBounds} from 'leaflet'; */ @Component({ selector: 'map-list-modal', - templateUrl: 'map-list.html', - styleUrls: ['map-list.scss'], + templateUrl: 'map-list-modal.html', + styleUrls: ['map-list-modal.scss'], }) export class MapListModalComponent implements OnInit { /** diff --git a/frontend/app/src/app/modules/map/page/modals/map-list.html b/frontend/app/src/app/modules/map/page/map-list-modal.html similarity index 100% rename from frontend/app/src/app/modules/map/page/modals/map-list.html rename to frontend/app/src/app/modules/map/page/map-list-modal.html diff --git a/frontend/app/src/app/modules/map/page/modals/map-list.scss b/frontend/app/src/app/modules/map/page/map-list-modal.scss similarity index 100% rename from frontend/app/src/app/modules/map/page/modals/map-list.scss rename to frontend/app/src/app/modules/map/page/map-list-modal.scss diff --git a/frontend/app/src/app/modules/map/page/map-page.component.ts b/frontend/app/src/app/modules/map/page/map-page.component.ts index 1dedb91a..dcc68c7d 100644 --- a/frontend/app/src/app/modules/map/page/map-page.component.ts +++ b/frontend/app/src/app/modules/map/page/map-page.component.ts @@ -29,6 +29,7 @@ import {Geolocation, PermissionStatus} from '@capacitor/geolocation'; import {Capacitor} from '@capacitor/core'; import {pauseWhen} from '../../../util/pause-when'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {startViewTransition} from '../../../util/view-transition'; /** * The main page of the map @@ -100,7 +101,17 @@ export class MapPageComponent implements OnInit { /** * Options of the leaflet map */ - options: MapOptions; + options: MapOptions = { + center: geoJSON(this.mapProvider.defaultPolygon).getBounds().getCenter(), + layers: [ + tileLayer('https://osm.server.uni-frankfurt.de/tiles/roads/x={x}&y={y}&z={z}', { + attribution: '© OpenStreetMap contributors', + maxZoom: this.MAX_ZOOM, + }), + ], + zoom: this.DEFAULT_ZOOM, + zoomControl: false, + }; /** * Position of the user on the map @@ -134,20 +145,7 @@ export class MapPageComponent implements OnInit { private dataRoutingService: DataRoutingService, private positionService: PositionService, readonly routerOutlet: IonRouterOutlet, - ) { - // initialize the options - this.options = { - center: geoJSON(this.mapProvider.defaultPolygon).getBounds().getCenter(), - layers: [ - tileLayer('https://osm.server.uni-frankfurt.de/tiles/roads/x={x}&y={y}&z={z}', { - attribution: '© OpenStreetMap contributors', - maxZoom: this.MAX_ZOOM, - }), - ], - zoom: this.DEFAULT_ZOOM, - zoomControl: false, - }; - } + ) {} ngOnInit() { this.dataRoutingService @@ -305,6 +303,7 @@ export class MapPageComponent implements OnInit { */ async onMapReady(map: Map) { this.map = map; + this.map.attributionControl.setPosition('topright'); const interval = window.setInterval(() => MapProvider.invalidateWhenRendered(map, this.mapContainer, interval), ); @@ -384,10 +383,12 @@ export class MapPageComponent implements OnInit { * Resets the map = fetch all the items based on the filters (and go to component's base location) */ async resetView() { - this.location.go('/map'); - await this.fetchAndUpdateItems(this.items.length > 0); + startViewTransition(async () => { + this.location.go('/map'); + await this.fetchAndUpdateItems(this.items.length > 0); - this.ref.detectChanges(); + this.ref.detectChanges(); + }); } /** @@ -414,14 +415,16 @@ export class MapPageComponent implements OnInit { * @param uid Uuid of the place */ async showItem(uid: SCUuid) { - const response = await this.mapProvider.searchPlace(uid); - this.items = response.data as SCPlace[]; - this.distance = this.positionService.getDistance(this.items[0].geo.point); - this.addToMap(this.items, true); - this.ref.detectChanges(); - const url = this.router.createUrlTree(['/map', uid]).toString(); - this.location.go(url); - // center the selected place - this.focus(geoJSON(this.items[0].geo.point).getBounds().getCenter()); + startViewTransition(async () => { + const response = await this.mapProvider.searchPlace(uid); + this.items = response.data as SCPlace[]; + this.distance = this.positionService.getDistance(this.items[0].geo.point); + this.addToMap(this.items, true); + this.ref.detectChanges(); + const url = this.router.createUrlTree(['/map', uid]).toString(); + this.location.go(url); + // center the selected place + this.focus(geoJSON(this.items[0].geo.point).getBounds().getCenter()); + }); } } diff --git a/frontend/app/src/app/modules/map/page/map-page.html b/frontend/app/src/app/modules/map/page/map-page.html index 52b9aceb..a7b71a5b 100644 --- a/frontend/app/src/app/modules/map/page/map-page.html +++ b/frontend/app/src/app/modules/map/page/map-page.html @@ -43,7 +43,7 @@ - +
-
+
  {{ 'map.page.buttons.SHOW_LIST' | translate }} - +
- -
-
- -   {{ 'map.page.buttons.SHOW_LIST' | translate }} - - - - - - - - - - + + + + + +
diff --git a/frontend/app/src/app/modules/map/page/map-page.scss b/frontend/app/src/app/modules/map/page/map-page.scss index 01563e90..e9b6ce47 100644 --- a/frontend/app/src/app/modules/map/page/map-page.scss +++ b/frontend/app/src/app/modules/map/page/map-page.scss @@ -12,113 +12,87 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +@import '../../../../theme/util/mixins'; -ion-content { - // fixes the unexpected issue that the content is not fullscreen (behind the header) - position: absolute; +$bottom-offset: 7px; // no idea what happened here - div.map-container { - position: fixed; - width: 100%; - height: 100%; - } - - & > div { - overflow: hidden; - } +.map-container { + width: 100%; + height: 100%; } ion-toolbar:first-of-type { padding: 0 var(--spacing-md) var(--spacing-xs); } -div.map-buttons { +.floating-content { + position: fixed; + z-index: 1000; + right: 0; + bottom: 0; + left: 0; + + display: flex; + flex-flow: row-reverse wrap; + align-items: flex-end; + justify-content: space-between; +} + +.map-buttons { + display: flex; + justify-content: flex-end; + ion-button { // important for iOS // TODO: find an option that is better suited for the iOS theme --box-shadow: var(--map-box-shadow); align-self: flex-end; - margin: 4px; + margin: var(--spacing-md); + + &.location-button { + view-transition-name: location-button; + } } } -::ng-deep { - .stapps-location { - ion-icon { - width: 100%; - height: 100%; - color: #fd435c; - } - } +.map-item { + position: relative; + max-width: 550px; + margin: var(--spacing-md); - .stapps-device-location { - ion-icon { - width: 100%; - height: 100%; - color: #4387fd; - } - } - - div.floating-content { - position: fixed; - z-index: 1000; + .close { + position: absolute; + top: 0; right: 0; - bottom: 0; - left: 0; + } - display: block; - justify-content: center; + ::ng-deep ion-item { + margin: 0; + } +} +@include ion-md-down { + .md { + ion-content { + --padding-bottom: $bottom-offset; + } + + .floating-content { + bottom: $bottom-offset; + } + } + + .map-buttons ion-button { + margin: var(--spacing-sm); + } + + .map-item { width: 100%; - padding: 0 var(--spacing-md) 8vh; + max-width: unset; + margin: 0; - ion-card { - margin: 0; - } - - div.map-buttons { - display: flex; - justify-content: flex-end; - } - - stapps-map-item { - position: center; - justify-self: center; - width: 550px; - margin: var(--spacing-sm) auto; - } - } -} - -div.floating-buttons { - position: absolute; - z-index: 1000; - right: 10px; - bottom: 15px; -} - -div.map-buttons.above { - display: none; - min-width: 70%; -} - -@media (width <= 667px) { - div.map-buttons.above { - display: flex; - } - - div.floating-content { - justify-content: normal; - padding: 0 var(--spacing-md) var(--spacing-lg); - - stapps-map-item { - display: grid; - width: 100%; - } - } - - div.map-buttons.floating-buttons { - display: none; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } } diff --git a/frontend/app/src/app/modules/map/page/modals/map-single.scss b/frontend/app/src/app/modules/map/page/modals/map-single.scss deleted file mode 100644 index 4cef8a0c..00000000 --- a/frontend/app/src/app/modules/map/page/modals/map-single.scss +++ /dev/null @@ -1,4 +0,0 @@ -:host { - display: flex; - flex-direction: column; -} diff --git a/frontend/app/src/app/modules/map/widget/map-widget.component.ts b/frontend/app/src/app/modules/map/widget/map-widget.component.ts index 7f522634..74477408 100644 --- a/frontend/app/src/app/modules/map/widget/map-widget.component.ts +++ b/frontend/app/src/app/modules/map/widget/map-widget.component.ts @@ -12,9 +12,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; +import {Component, ElementRef, HostBinding, Input, OnInit, ViewChild} from '@angular/core'; import {Router} from '@angular/router'; -import {SCPlace} from '@openstapps/core'; +import {SCPlaceWithoutReferences, SCThingWithoutReferences} from '@openstapps/core'; import {geoJSON, Map, MapOptions, tileLayer} from 'leaflet'; import {MapProvider} from '../map.provider'; @@ -27,6 +27,8 @@ import {MapProvider} from '../map.provider'; templateUrl: './map-widget.html', }) export class MapWidgetComponent implements OnInit { + @HostBinding('class.expand-when-space') expandWhenSpace = true; + /** * A leaflet map showed */ @@ -45,7 +47,7 @@ export class MapWidgetComponent implements OnInit { /** * A place to show on the map */ - @Input() place: SCPlace; + @Input() place: SCThingWithoutReferences & Pick; /** * Indicates if the expand button should be visible diff --git a/frontend/app/src/app/modules/map/widget/map-widget.html b/frontend/app/src/app/modules/map/widget/map-widget.html index c0bb910b..5077d6d2 100644 --- a/frontend/app/src/app/modules/map/widget/map-widget.html +++ b/frontend/app/src/app/modules/map/widget/map-widget.html @@ -21,6 +21,10 @@ [leafletOptions]="options" >
+ + + {{'map.directions.TITLE' | translate}} + diff --git a/frontend/app/src/app/modules/map/widget/map-widget.scss b/frontend/app/src/app/modules/map/widget/map-widget.scss index abd4ae60..7cbe7ef2 100644 --- a/frontend/app/src/app/modules/map/widget/map-widget.scss +++ b/frontend/app/src/app/modules/map/widget/map-widget.scss @@ -12,6 +12,12 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +:host { + position: relative; + width: auto; + height: 300px; + min-height: 300px; +} div.map-container { pointer-events: none; diff --git a/frontend/app/src/app/translation/i18n.spec.ts b/frontend/app/src/app/translation/i18n.spec.ts index a6f96bd8..4672a619 100644 --- a/frontend/app/src/app/translation/i18n.spec.ts +++ b/frontend/app/src/app/translation/i18n.spec.ts @@ -17,23 +17,31 @@ import english from '../../assets/i18n/en.json'; import german from '../../assets/i18n/de.json'; -const exceptions = new Set([ - 'login', - 'ok', - 'protein', - 'feedback', - 'name', - 'status', - 'issn', - 'ejournal', - 'backup', - 'export', - 'dashboard', - 'home', - 'email', - 'logins', - 'https://www.swffm.de/essen-trinken/uebersicht/umweltscore', -]); +const exceptions = new Set( + [ + 'login', + 'ok', + 'protein', + 'feedback', + 'name', + 'status', + 'issn', + 'ejournal', + 'backup', + 'export', + 'dashboard', + 'home', + 'email', + 'logins', + 'google maps', + 'apple maps', + 'openstreetmaps routing', + 'https://www.swffm.de/essen-trinken/uebersicht/umweltscore', + 'https://www.google.com/maps/dir/?api=1&destination={{lat}},{{lon}}&origin={{posLat}},{{posLon}}', + 'https://maps.apple.com/?daddr={{lat}},{{lon}}&saddr={{posLat}},{{posLon}}', + 'https://www.openstreetmap.org/directions?from={{posLat}},{{posLon}}&to={{lat}},{{lon}}#map=15/{{lat}}/{{lon}}', + ].map(it => it.toLowerCase()), +); const languages = [ ['english', english], diff --git a/frontend/app/src/app/util/view-transition.ts b/frontend/app/src/app/util/view-transition.ts new file mode 100644 index 00000000..afea72f1 --- /dev/null +++ b/frontend/app/src/app/util/view-transition.ts @@ -0,0 +1,14 @@ +/** + * Performs a view transition + * + * This is a progressive enhancement for (as of right now) Chromium-based browsers + * - Firefox position: [positive](https://mozilla.github.io/standards-positions/#view-transitions) + * - WebKit position: [support](https://github.com/WebKit/standards-positions/issues/48#issuecomment-1679760489) + */ +export function startViewTransition(runner: () => Promise) { + if ('startViewTransition' in document) { + document.startViewTransition(runner); + } else { + void runner(); + } +} diff --git a/frontend/app/src/assets/i18n/de.json b/frontend/app/src/assets/i18n/de.json index 50bf50f4..bbd07cb6 100644 --- a/frontend/app/src/assets/i18n/de.json +++ b/frontend/app/src/assets/i18n/de.json @@ -244,6 +244,25 @@ } }, "map": { + "directions": { + "TITLE": "Anbindung", + "TITLE_LONG": "Anbindung nach {{name}}", + "ADDRESS": "{{streetAddress}}, {{postalCode}} {{addressLocality}}", + "COPY_ADDRESS": "Adresse kopieren", + "ADDRESS_COPIED": "Adresse wurde kopiert", + "GOOGLE_MAPS": { + "TITLE": "Google Maps", + "URL": "https://www.google.com/maps/dir/?api=1&destination={{lat}},{{lon}}&origin={{posLat}},{{posLon}}" + }, + "APPLE_MAPS": { + "TITLE": "Apple Maps", + "URL": "https://maps.apple.com/?daddr={{lat}},{{lon}}&saddr={{posLat}},{{posLon}}" + }, + "OSM_ROUTING": { + "TITLE": "OpenStreetMaps Routing", + "URL": "https://routing.openstreetmap.de/?loc={{posLat}},{{posLon}}&loc={{lat}},{{lon}}&srv=2&hl=de" + } + }, "page": { "TITLE": "Karte", "search_bar": { diff --git a/frontend/app/src/assets/i18n/en.json b/frontend/app/src/assets/i18n/en.json index 21b4f93a..3b6350ff 100644 --- a/frontend/app/src/assets/i18n/en.json +++ b/frontend/app/src/assets/i18n/en.json @@ -244,6 +244,25 @@ } }, "map": { + "directions": { + "TITLE": "Directions", + "TITLE_LONG": "Directions to {{name}}", + "ADDRESS": "{{streetAddress}}, {{addressLocality}} {{postalCode}}", + "COPY_ADDRESS": "Copy Address", + "ADDRESS_COPIED": "Address copied", + "GOOGLE_MAPS": { + "TITLE": "Google Maps", + "URL": "https://www.google.com/maps/dir/?api=1&destination={{lat}},{{lon}}&origin={{posLat}},{{posLon}}" + }, + "APPLE_MAPS": { + "TITLE": "Apple Maps", + "URL": "https://maps.apple.com/?daddr={{lat}},{{lon}}&saddr={{posLat}},{{posLon}}" + }, + "OSM_ROUTING": { + "TITLE": "OpenStreetMaps Routing", + "URL": "https://routing.openstreetmap.de/?loc={{posLat}},{{posLon}}&loc={{lat}},{{lon}}&srv=2&hl=en" + } + }, "page": { "TITLE": "Map", "search_bar": { diff --git a/frontend/app/src/assets/icons.min.woff2 b/frontend/app/src/assets/icons.min.woff2 index 3cc1beeb..433d5e16 100644 Binary files a/frontend/app/src/assets/icons.min.woff2 and b/frontend/app/src/assets/icons.min.woff2 differ diff --git a/frontend/app/src/global.scss b/frontend/app/src/global.scss index aaaebff5..5848a75f 100644 --- a/frontend/app/src/global.scss +++ b/frontend/app/src/global.scss @@ -131,6 +131,18 @@ ion-alert { } } +.stapps-location ion-icon { + width: 100%; + height: 100%; + color: #fd435c; +} + +.stapps-device-location ion-icon { + width: 100%; + height: 100%; + color: #4387fd; +} + .add-event-popover { --width: fit-content; --max-width: 95%; diff --git a/frontend/app/tsconfig.spec.json b/frontend/app/tsconfig.spec.json index 59fb1e3c..4e9b391c 100644 --- a/frontend/app/tsconfig.spec.json +++ b/frontend/app/tsconfig.spec.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/spec", - "types": ["jasmine", "node"], + "types": ["jasmine", "node", "dom-view-transitions"], "paths": { "@capacitor/*": ["__mocks__/@capacitor/*"] } diff --git a/images/app-cypress/package.json b/images/app-cypress/package.json index 2fe83fc2..e065a398 100644 --- a/images/app-cypress/package.json +++ b/images/app-cypress/package.json @@ -5,8 +5,7 @@ "type": "module", "license": "GPL-3.0-only", "author": "Rainer Killinger ", - "contributors": [ - ], + "contributors": [], "files": [ "Dockerfile", "CHANGELOG.md" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca87e354..8dedc830 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -752,6 +752,9 @@ importers: '@capacitor/browser': specifier: 4.1.0 version: 4.1.0(@capacitor/core@4.6.1) + '@capacitor/clipboard': + specifier: 4.1.0 + version: 4.1.0(@capacitor/core@4.6.1) '@capacitor/core': specifier: 4.6.1 version: 4.6.1 @@ -824,6 +827,9 @@ importers: '@transistorsoft/capacitor-background-fetch': specifier: 1.0.2 version: 1.0.2(@capacitor/core@4.6.1) + '@types/dom-view-transitions': + specifier: 1.0.1 + version: 1.0.1 capacitor-secure-storage-plugin: specifier: 0.8.1 version: 0.8.1(@capacitor/core@4.6.1) @@ -1089,6 +1095,8 @@ importers: images/app-builder: {} + images/app-cypress: {} + images/node-base: {} images/node-builder: {} @@ -4910,6 +4918,14 @@ packages: - supports-color dev: true + /@capacitor/clipboard@4.1.0(@capacitor/core@4.6.1): + resolution: {integrity: sha512-lfUwDqZces3mQcBOyfxpBCsRWWSfLuPzekA1N3RaMgYVhD6/rdzFnzfRiksj1hm4It+lnULK0y+N5nxVnTt+0Q==} + peerDependencies: + '@capacitor/core': ^4.0.0 + dependencies: + '@capacitor/core': 4.6.1 + dev: false + /@capacitor/core@4.6.1: resolution: {integrity: sha512-7A2IV9E8umgu9u0fChUTjQJq+Jp25GJZMmWxoQN/nVx/1rcpFJ4m1xo3NPBoIRs+aV7FR+BM17mPrnkKlA8N2g==} dependencies: @@ -6735,6 +6751,10 @@ packages: '@types/node': 18.15.3 dev: false + /@types/dom-view-transitions@1.0.1: + resolution: {integrity: sha512-A9S1ijj/4MX06I1W/6on8lhaYyq1Ir7gaOvfllW1o4RzVWW88HAeqX0pUx9VgOLnNpdiGeUW2CTkg18p5LWIrA==} + dev: false + /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: