Performance and code improvements

This commit is contained in:
itskovacs 2025-08-06 21:11:46 +02:00
parent 6703a197fb
commit 9d79ddf1e0
2 changed files with 354 additions and 283 deletions

View File

@ -1,5 +1,5 @@
import { AfterViewInit, Component } from "@angular/core"; import { AfterViewInit, Component, OnInit } from "@angular/core";
import { combineLatest, debounceTime, tap } from "rxjs"; import { combineLatest, debounceTime, take, tap } from "rxjs";
import { Place, Category } from "../../types/poi"; import { Place, Category } from "../../types/poi";
import { ApiService } from "../../services/api.service"; import { ApiService } from "../../services/api.service";
import { PlaceBoxComponent } from "../../shared/place-box/place-box.component"; import { PlaceBoxComponent } from "../../shared/place-box/place-box.component";
@ -40,6 +40,7 @@ import { SelectItemGroup } from "primeng/api";
import { YesNoModalComponent } from "../../modals/yes-no-modal/yes-no-modal.component"; import { YesNoModalComponent } from "../../modals/yes-no-modal/yes-no-modal.component";
import { CategoryCreateModalComponent } from "../../modals/category-create-modal/category-create-modal.component"; import { CategoryCreateModalComponent } from "../../modals/category-create-modal/category-create-modal.component";
import { AuthService } from "../../services/auth.service"; import { AuthService } from "../../services/auth.service";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
export interface ContextMenuItem { export interface ContextMenuItem {
text: string; text: string;
@ -76,36 +77,37 @@ export interface MarkerOptions extends L.MarkerOptions {
templateUrl: "./dashboard.component.html", templateUrl: "./dashboard.component.html",
styleUrls: ["./dashboard.component.scss"], styleUrls: ["./dashboard.component.scss"],
}) })
export class DashboardComponent implements AfterViewInit { export class DashboardComponent implements OnInit, AfterViewInit {
searchInput = new FormControl(""); searchInput = new FormControl("", { nonNullable: true });
info: Info | undefined; info?: Info;
isLowNet: boolean = false; isLowNet = false;
isDarkMode: boolean = false; isDarkMode = false;
isGpxInPlaceMode: boolean = false; isGpxInPlaceMode = false;
viewSettings = false; viewSettings = false;
viewFilters = false; viewFilters = false;
viewMarkersList = false; viewMarkersList = false;
viewMarkersListSearch = false; viewMarkersListSearch = false;
settingsForm: FormGroup;
hoveredElements: HTMLElement[] = [];
map: any; settingsForm: FormGroup;
mapDisplayedTrace: L.Polyline[] = []; hoveredElement?: HTMLElement;
settings: Settings | undefined;
currencySigns: { c: string; s: string }[] = []; map?: L.Map;
markerClusterGroup?: L.MarkerClusterGroup;
gpxLayerGroup?: L.LayerGroup;
settings?: Settings;
currencySigns = UtilsService.currencySigns();
doNotDisplayOptions: SelectItemGroup[] = []; doNotDisplayOptions: SelectItemGroup[] = [];
markerClusterGroup: L.MarkerClusterGroup | undefined;
places: Place[] = []; places: Place[] = [];
visiblePlaces: Place[] = []; visiblePlaces: Place[] = [];
selectedPlace: Place | undefined; selectedPlace?: Place;
categories: Category[] = []; categories: Category[] = [];
filter_display_visited: boolean = false; filter_display_visited = false;
filter_display_favorite_only: boolean = false; filter_display_favorite_only = false;
filter_dog_only: boolean = false; filter_dog_only = false;
activeCategories: Set<string> = new Set(); activeCategories = new Set<string>();
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
@ -115,7 +117,7 @@ export class DashboardComponent implements AfterViewInit {
private router: Router, private router: Router,
private fb: FormBuilder, private fb: FormBuilder,
) { ) {
this.currencySigns = this.utilsService.currencySigns(); this.currencySigns = UtilsService.currencySigns();
this.settingsForm = this.fb.group({ this.settingsForm = this.fb.group({
map_lat: [ map_lat: [
@ -143,40 +145,21 @@ export class DashboardComponent implements AfterViewInit {
tile_layer: ["", Validators.required], tile_layer: ["", Validators.required],
}); });
this.apiService.getInfo().subscribe({ // HACK: Subscribe in constructor for takeUntilDestroyed
next: (info) => (this.info = info), this.searchInput.valueChanges
}); .pipe(debounceTime(200), takeUntilDestroyed())
.subscribe({
this.searchInput.valueChanges.pipe(debounceTime(200)).subscribe({ next: () => this.setVisibleMarkers(),
next: () => this.setVisibleMarkers(), });
});
} }
logout() { ngOnInit(): void {
this.authService.logout(); this.apiService
} .getInfo()
.pipe(take(1))
closePlaceBox() { .subscribe({
this.selectedPlace = undefined; next: (info) => (this.info = info),
} });
toGithub() {
this.utilsService.toGithubTRIP();
}
check_update() {
this.apiService.checkVersion().subscribe({
next: (remote_version) => {
if (!remote_version)
this.utilsService.toast(
"success",
"Latest version",
"You're running the latest version of TRIP",
);
if (this.info && remote_version != this.info?.version)
this.info.update = remote_version;
},
});
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -186,6 +169,7 @@ export class DashboardComponent implements AfterViewInit {
settings: this.apiService.getSettings(), settings: this.apiService.getSettings(),
}) })
.pipe( .pipe(
take(1),
tap(({ categories, places, settings }) => { tap(({ categories, places, settings }) => {
this.settings = settings; this.settings = settings;
this.initMap(); this.initMap();
@ -199,7 +183,7 @@ export class DashboardComponent implements AfterViewInit {
if (this.isDarkMode) this.utilsService.toggleDarkMode(); if (this.isDarkMode) this.utilsService.toggleDarkMode();
this.resetFilters(); this.resetFilters();
this.places.push(...places); this.places = [...places];
this.updateMarkersAndClusters(); //Not optimized as I could do it on the forEach, but it allows me to modify only one function instead of multiple places this.updateMarkersAndClusters(); //Not optimized as I could do it on the forEach, but it allows me to modify only one function instead of multiple places
}), }),
) )
@ -209,7 +193,7 @@ export class DashboardComponent implements AfterViewInit {
initMap(): void { initMap(): void {
if (!this.settings) return; if (!this.settings) return;
let contentMenuItems = [ const contentMenuItems = [
{ {
text: "Add Point of Interest", text: "Add Point of Interest",
icon: "add-location.png", icon: "add-location.png",
@ -220,26 +204,27 @@ export class DashboardComponent implements AfterViewInit {
]; ];
this.map = createMap(contentMenuItems, this.settings?.tile_layer); this.map = createMap(contentMenuItems, this.settings?.tile_layer);
this.map.setView(L.latLng(this.settings.map_lat, this.settings.map_lng)); this.map.setView(L.latLng(this.settings.map_lat, this.settings.map_lng));
this.map.on("moveend zoomend", () => { this.map.on("moveend zoomend", () => this.setVisibleMarkers());
this.setVisibleMarkers();
});
this.markerClusterGroup = createClusterGroup().addTo(this.map); this.markerClusterGroup = createClusterGroup().addTo(this.map);
} }
setVisibleMarkers() { setVisibleMarkers() {
if (!this.viewMarkersList || !this.map) return;
const bounds = this.map.getBounds(); const bounds = this.map.getBounds();
this.visiblePlaces = this.filteredPlaces
.filter((p) => bounds.contains([p.lat, p.lng])) this.visiblePlaces = this.filteredPlaces.filter((p) =>
.filter((p) => { bounds.contains([p.lat, p.lng]),
const v = this.searchInput.value; );
if (v)
return ( const searchValue = this.searchInput.value?.toLowerCase() ?? "";
p.name.toLowerCase().includes(v) || if (searchValue)
p.description?.toLowerCase().includes(v) this.visiblePlaces.filter(
); (p) =>
return true; p.name.toLowerCase().includes(searchValue) ||
}) p.description?.toLowerCase().includes(searchValue),
.sort((a, b) => a.name.localeCompare(b.name)); );
this.visiblePlaces.sort((a, b) => a.name.localeCompare(b.name));
} }
resetFilters() { resetFilters() {
@ -260,42 +245,14 @@ export class DashboardComponent implements AfterViewInit {
if (this.viewMarkersList) this.setVisibleMarkers(); if (this.viewMarkersList) this.setVisibleMarkers();
} }
toggleLowNet() {
this.apiService.putSettings({ mode_low_network: this.isLowNet }).subscribe({
next: (_) => {
setTimeout(() => {
this.updateMarkersAndClusters();
}, 100);
},
});
}
toggleDarkMode() {
this.apiService.putSettings({ mode_dark: this.isDarkMode }).subscribe({
next: (_) => {
this.utilsService.toggleDarkMode();
},
});
}
toggleGpxInPlace() {
this.apiService
.putSettings({ mode_gpx_in_place: this.isGpxInPlaceMode })
.subscribe({
next: (_) => {
this.updateMarkersAndClusters();
},
});
}
get filteredPlaces(): Place[] { get filteredPlaces(): Place[] {
return this.places.filter((p) => { return this.places.filter(
if (!this.filter_display_visited && p.visited) return false; (p) =>
if (this.filter_display_favorite_only && !p.favorite) return false; (this.filter_display_visited || !p.visited) &&
if (this.filter_dog_only && !p.allowdog) return false; (!this.filter_display_favorite_only || p.favorite) &&
if (!this.activeCategories.has(p.category.name)) return false; (!this.filter_dog_only || p.allowdog) &&
return true; this.activeCategories.has(p.category.name),
}); );
} }
updateMarkersAndClusters(): void { updateMarkersAndClusters(): void {
@ -308,7 +265,7 @@ export class DashboardComponent implements AfterViewInit {
} }
placeToMarker(place: Place): L.Marker { placeToMarker(place: Place): L.Marker {
let marker = placeToMarker( const marker = placeToMarker(
place, place,
this.isLowNet, this.isLowNet,
place.visited, place.visited,
@ -316,24 +273,23 @@ export class DashboardComponent implements AfterViewInit {
); );
marker marker
.on("click", (e) => { .on("click", (e) => {
this.selectedPlace = place; this.selectedPlace = { ...place };
let toView = { ...e.latlng }; let toView = { ...e.latlng };
if ("ontouchstart" in window) toView.lat = toView.lat - 0.0175; if ("ontouchstart" in window) toView.lat = toView.lat - 0.0175;
marker.closeTooltip(); marker.closeTooltip();
this.map.setView(toView); this.map?.setView(toView);
}) })
.on("contextmenu", () => { .on("contextmenu", () => {
this.map.contextmenu.hide(); if (this.map && (this.map as any).contextmenu)
(this.map as any).contextmenu.hide();
}); });
return marker; return marker;
} }
addPlaceModal(e?: any): void { addPlaceModal(e?: any): void {
let opts = {}; const opts = e ? { data: { place: e.latlng } } : {};
if (e) opts = { data: { place: e.latlng } };
const modal: DynamicDialogRef = this.dialogService.open( const modal: DynamicDialogRef = this.dialogService.open(
PlaceCreateModalComponent, PlaceCreateModalComponent,
{ {
@ -351,19 +307,23 @@ export class DashboardComponent implements AfterViewInit {
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (place: Place | null) => { next: (place: Place | null) => {
if (!place) return; if (!place) return;
this.apiService.postPlace(place).subscribe({ this.apiService
next: (place: Place) => { .postPlace(place)
this.places.push(place); .pipe(take(1))
this.places.sort((a, b) => a.name.localeCompare(b.name)); .subscribe({
setTimeout(() => { next: (place: Place) => {
this.updateMarkersAndClusters(); this.places = [...this.places, place].sort((a, b) =>
}, 10); a.name.localeCompare(b.name),
}, );
}); setTimeout(() => {
this.updateMarkersAndClusters();
}, 10);
},
});
}, },
}); });
} }
@ -385,41 +345,38 @@ export class DashboardComponent implements AfterViewInit {
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (places: string | null) => { next: (places: string | null) => {
if (!places) return; if (!places) return;
let parsedPlaces = []; let parsedPlaces = [];
try { try {
parsedPlaces = JSON.parse(places); parsedPlaces = JSON.parse(places);
if (!Array.isArray(parsedPlaces)) return; if (!Array.isArray(parsedPlaces)) throw new Error();
} catch (err) { } catch (err) {
this.utilsService.toast("error", "Error", "Content looks invalid"); this.utilsService.toast("error", "Error", "Content looks invalid");
return; return;
} }
this.apiService.postPlaces(parsedPlaces).subscribe((places) => { this.apiService
places.forEach((p) => this.places.push(p)); .postPlaces(parsedPlaces)
this.places.sort((a, b) => a.name.localeCompare(b.name)); .pipe(take(1))
setTimeout(() => { .subscribe((places) => {
this.updateMarkersAndClusters(); this.places = [...this.places, ...places].sort((a, b) =>
}, 10); a.name.localeCompare(b.name),
}); );
setTimeout(() => {
this.updateMarkersAndClusters();
}, 10);
});
}, },
}); });
} }
gotoPlace(p: Place) {
this.map.flyTo([p.lat, p.lng]);
}
gotoTrips() {
this.router.navigateByUrl("/trips");
}
resetHoverPlace() { resetHoverPlace() {
this.hoveredElements.forEach((elem) => elem.classList.remove("listHover")); if (!this.hoveredElement) return;
this.hoveredElements = []; this.hoveredElement.classList.remove("listHover");
this.hoveredElement = undefined;
} }
hoverPlace(p: Place) { hoverPlace(p: Place) {
@ -431,14 +388,14 @@ export class DashboardComponent implements AfterViewInit {
}); });
if (!marker) return; if (!marker) return;
let markerElement = marker.getElement() as HTMLElement; // search for Marker. If 'null', is inside Cluster const markerElement = marker.getElement() as HTMLElement; // search for Marker. If 'null', is inside Cluster
if (markerElement) { if (markerElement) {
// marker, not clustered // marker, not clustered
markerElement.classList.add("listHover"); markerElement.classList.add("listHover");
this.hoveredElements.push(markerElement); this.hoveredElement = markerElement;
} else { } else {
// marker , clustered // marker is clustered
const parentCluster = (this.markerClusterGroup as any).getVisibleParent( const parentCluster = (this.markerClusterGroup as any).getVisibleParent(
marker, marker,
); );
@ -446,7 +403,7 @@ export class DashboardComponent implements AfterViewInit {
const clusterEl = parentCluster.getElement(); const clusterEl = parentCluster.getElement();
if (clusterEl) { if (clusterEl) {
clusterEl.classList.add("listHover"); clusterEl.classList.add("listHover");
this.hoveredElements.push(clusterEl); this.hoveredElement = clusterEl;
} }
} }
} }
@ -454,13 +411,19 @@ export class DashboardComponent implements AfterViewInit {
favoritePlace() { favoritePlace() {
if (!this.selectedPlace) return; if (!this.selectedPlace) return;
const favoriteBool = !this.selectedPlace.favorite;
let favoriteBool = !this.selectedPlace.favorite;
this.apiService this.apiService
.putPlace(this.selectedPlace.id, { favorite: favoriteBool }) .putPlace(this.selectedPlace.id, { favorite: favoriteBool })
.pipe(take(1))
.subscribe({ .subscribe({
next: () => { next: () => {
this.selectedPlace!.favorite = favoriteBool; const idx = this.places.findIndex(
(p) => p.id === this.selectedPlace!.id,
);
if (idx !== -1)
this.places[idx] = { ...this.places[idx], favorite: favoriteBool };
this.selectedPlace = { ...this.places[idx] };
this.updateMarkersAndClusters(); this.updateMarkersAndClusters();
}, },
}); });
@ -468,13 +431,19 @@ export class DashboardComponent implements AfterViewInit {
visitPlace() { visitPlace() {
if (!this.selectedPlace) return; if (!this.selectedPlace) return;
const visitedBool = !this.selectedPlace.visited;
let visitedBool = !this.selectedPlace.visited;
this.apiService this.apiService
.putPlace(this.selectedPlace.id, { visited: visitedBool }) .putPlace(this.selectedPlace.id, { visited: visitedBool })
.pipe(take(1))
.subscribe({ .subscribe({
next: () => { next: () => {
this.selectedPlace!.visited = visitedBool; const idx = this.places.findIndex(
(p) => p.id === this.selectedPlace!.id,
);
if (idx !== -1)
this.places[idx] = { ...this.places[idx], visited: visitedBool };
this.selectedPlace = { ...this.places[idx] };
this.updateMarkersAndClusters(); this.updateMarkersAndClusters();
}, },
}); });
@ -496,13 +465,15 @@ export class DashboardComponent implements AfterViewInit {
modal.onClose.subscribe({ modal.onClose.subscribe({
next: (bool) => { next: (bool) => {
if (bool) if (!bool) return;
this.apiService.deletePlace(this.selectedPlace!.id).subscribe({ this.apiService
.deletePlace(this.selectedPlace!.id)
.pipe(take(1))
.subscribe({
next: () => { next: () => {
let index = this.places.findIndex( this.places = this.places.filter(
(p) => p.id == this.selectedPlace!.id, (p) => p.id !== this.selectedPlace!.id,
); );
if (index > -1) this.places.splice(index, 1);
this.closePlaceBox(); this.closePlaceBox();
this.updateMarkersAndClusters(); this.updateMarkersAndClusters();
if (this.viewMarkersList) this.setVisibleMarkers(); if (this.viewMarkersList) this.setVisibleMarkers();
@ -536,74 +507,80 @@ export class DashboardComponent implements AfterViewInit {
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (place: Place | null) => { next: (place: Place | null) => {
if (!place) return; if (!place) return;
this.apiService.putPlace(place.id, place).subscribe({ this.apiService
next: (place: Place) => { .putPlace(place.id, place)
let index = this.places.findIndex((p) => p.id == place.id); .pipe(take(1))
if (index > -1) this.places.splice(index, 1, place); .subscribe({
this.places.sort((a, b) => a.name.localeCompare(b.name)); next: (place: Place) => {
this.selectedPlace = place; const places = [...this.places];
setTimeout(() => { const idx = places.findIndex((p) => p.id == place.id);
this.updateMarkersAndClusters(); if (idx > -1) places.splice(idx, 1, place);
}, 10); places.sort((a, b) => a.name.localeCompare(b.name));
if (this.viewMarkersList) this.setVisibleMarkers(); this.places = places;
}, this.selectedPlace = { ...place };
}); setTimeout(() => {
this.updateMarkersAndClusters();
}, 10);
if (this.viewMarkersList) this.setVisibleMarkers();
},
});
}, },
}); });
} }
displayGPXOnMap(gpx: string) { displayGPXOnMap(gpx: string) {
if (!this.map) return;
if (!this.gpxLayerGroup)
this.gpxLayerGroup = L.layerGroup().addTo(this.map);
this.gpxLayerGroup.clearLayers();
try { try {
// HINT: For now, delete traces everytime we display a GPX const gpxPolyline = gpxToPolyline(gpx);
// TODO: Handle multiple polygons and handle Click events
this.mapDisplayedTrace.forEach((p) => this.map.removeLayer(p));
this.mapDisplayedTrace = [];
const gpxPolyline = gpxToPolyline(gpx).addTo(this.map);
gpxPolyline.on("click", () => { gpxPolyline.on("click", () => {
this.map.removeLayer(gpxPolyline); this.gpxLayerGroup?.removeLayer(gpxPolyline);
}); });
this.gpxLayerGroup?.addLayer(gpxPolyline);
this.mapDisplayedTrace.push(gpxPolyline); this.map.fitBounds(gpxPolyline.getBounds(), { padding: [20, 20] });
} catch { } catch {
this.utilsService.toast("error", "Error", "Couldn't parse GPX data"); this.utilsService.toast("error", "Error", "Couldn't parse GPX data");
return;
} }
} }
getPlaceGPX() { getPlaceGPX() {
if (!this.selectedPlace) return; if (!this.selectedPlace) return;
this.apiService.getPlaceGPX(this.selectedPlace.id).subscribe({ this.apiService
next: (p) => { .getPlaceGPX(this.selectedPlace.id)
if (!p.gpx) { .pipe(take(1))
this.utilsService.toast( .subscribe({
"error", next: (p) => {
"Error", if (!p.gpx) {
"Couldn't retrieve GPX data", this.utilsService.toast(
); "error",
return; "Error",
} "Couldn't retrieve GPX data",
this.displayGPXOnMap(p.gpx); );
}, return;
}); }
this.displayGPXOnMap(p.gpx);
},
});
} }
toggleSettings() { toggleSettings() {
this.viewSettings = !this.viewSettings; this.viewSettings = !this.viewSettings;
if (this.viewSettings && this.settings) { if (!this.viewSettings || !this.settings) return;
this.settingsForm.reset();
this.settingsForm.patchValue(this.settings); this.settingsForm.reset(this.settings);
this.doNotDisplayOptions = [ this.doNotDisplayOptions = [
{ {
label: "Categories", label: "Categories",
items: this.categories.map((c) => ({ label: c.name, value: c.name })), items: this.categories.map((c) => ({ label: c.name, value: c.name })),
}, },
]; ];
}
} }
toggleFilters() { toggleFilters() {
@ -611,70 +588,82 @@ export class DashboardComponent implements AfterViewInit {
} }
toggleMarkersList() { toggleMarkersList() {
this.searchInput.setValue("");
this.viewMarkersListSearch = false;
this.viewMarkersList = !this.viewMarkersList; this.viewMarkersList = !this.viewMarkersList;
this.viewMarkersListSearch = false;
this.searchInput.setValue("");
if (this.viewMarkersList) this.setVisibleMarkers(); if (this.viewMarkersList) this.setVisibleMarkers();
} }
toggleMarkersListSearch() { toggleMarkersListSearch() {
this.searchInput.setValue("");
this.viewMarkersListSearch = !this.viewMarkersListSearch; this.viewMarkersListSearch = !this.viewMarkersListSearch;
if (this.viewMarkersListSearch) this.searchInput.setValue("");
} }
setMapCenterToCurrent() { setMapCenterToCurrent() {
let latlng: L.LatLng = this.map.getCenter(); const latlng = this.map?.getCenter();
if (!latlng) return;
this.settingsForm.patchValue({ map_lat: latlng.lat, map_lng: latlng.lng }); this.settingsForm.patchValue({ map_lat: latlng.lat, map_lng: latlng.lng });
this.settingsForm.markAsDirty(); this.settingsForm.markAsDirty();
} }
importData(e: any): void { importData(event: Event): void {
const formdata = new FormData(); const input = event.target as HTMLInputElement;
if (e.target.files[0]) { if (!input.files?.length) return;
formdata.append("file", e.target.files[0]);
this.apiService.settingsUserImport(formdata).subscribe({ const formdata = new FormData();
formdata.append("file", input.files[0]);
this.apiService
.settingsUserImport(formdata)
.pipe(take(1))
.subscribe({
next: (places) => { next: (places) => {
places.forEach((p) => this.places.push(p)); this.places = [...this.places, ...places].sort((a, b) =>
this.places.sort((a, b) => a.name.localeCompare(b.name)); a.name.localeCompare(b.name),
);
setTimeout(() => { setTimeout(() => {
this.updateMarkersAndClusters(); this.updateMarkersAndClusters();
}, 10); }, 10);
this.viewSettings = false; this.viewSettings = false;
}, },
}); });
}
} }
exportData(): void { exportData(): void {
this.apiService.settingsUserExport().subscribe((resp: Object) => { this.apiService
let _datablob = new Blob([JSON.stringify(resp, null, 2)], { .settingsUserExport()
type: "text/json", .pipe(take(1))
.subscribe((resp: Object) => {
const dataBlob = new Blob([JSON.stringify(resp, null, 2)], {
type: "application/json",
});
const downloadURL = URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.href = downloadURL;
link.download = `TRIP_backup_${new Date().toISOString().split("T")[0]}.json`;
link.click();
link.remove();
URL.revokeObjectURL(downloadURL);
}); });
var downloadURL = URL.createObjectURL(_datablob);
var link = document.createElement("a");
link.href = downloadURL;
link.download =
"TRIP_backup_" + new Date().toISOString().split("T")[0] + ".json";
link.click();
link.remove();
});
} }
updateSettings() { updateSettings() {
this.apiService.putSettings(this.settingsForm.value).subscribe({ this.apiService
next: (settings) => { .putSettings(this.settingsForm.value)
const refreshMap = this.settings!.tile_layer != settings.tile_layer; .pipe(take(1))
this.settings = settings; .subscribe({
if (refreshMap) { next: (settings) => {
this.map.remove(); const refreshMap = this.settings?.tile_layer != settings.tile_layer;
this.initMap(); this.settings = settings;
this.updateMarkersAndClusters(); if (refreshMap) {
} this.map?.remove();
this.resetFilters(); this.initMap();
this.toggleSettings(); this.updateMarkersAndClusters();
}, }
}); this.resetFilters();
this.toggleSettings();
},
});
} }
editCategory(c: Category) { editCategory(c: Category) {
@ -695,30 +684,32 @@ export class DashboardComponent implements AfterViewInit {
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (category: Category | null) => { next: (category: Category | null) => {
if (!category) return; if (!category) return;
this.apiService.putCategory(c.id, category).subscribe({ this.apiService
next: (category) => { .putCategory(c.id, category)
const index = this.categories.findIndex( .pipe(take(1))
(categ) => categ.id == c.id, .subscribe({
); next: (updated) => {
if (index > -1) { this.categories = this.categories.map((cat) =>
this.categories.splice(index, 1, category); cat.id === updated.id ? updated : cat,
);
this.activeCategories = new Set( this.activeCategories = new Set(
this.categories.map((c) => c.name), this.categories.map((c) => c.name),
); );
this.places = this.places.map((p) => { this.places = this.places.map((p) => {
if (p.category.id == category.id) return { ...p, category }; if (p.category.id == updated.id)
return { ...p, category: updated };
return p; return p;
}); });
setTimeout(() => { setTimeout(() => {
this.updateMarkersAndClusters(); this.updateMarkersAndClusters();
}, 100); }, 100);
} },
}, });
});
}, },
}); });
} }
@ -740,19 +731,22 @@ export class DashboardComponent implements AfterViewInit {
}, },
); );
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (category: Category | null) => { next: (category: Category | null) => {
if (!category) return; if (!category) return;
this.apiService.postCategory(category).subscribe({ this.apiService
next: (category: Category) => { .postCategory(category)
this.categories.push(category); .pipe(take(1))
this.categories.sort((categoryA: Category, categoryB: Category) => .subscribe({
categoryA.name.localeCompare(categoryB.name), next: (category: Category) => {
); this.categories.push(category);
this.activeCategories.add(category.name); this.categories.sort((categoryA: Category, categoryB: Category) =>
}, categoryA.name.localeCompare(categoryB.name),
}); );
this.activeCategories.add(category.name);
},
});
}, },
}); });
} }
@ -769,23 +763,95 @@ export class DashboardComponent implements AfterViewInit {
data: "Delete this category ?", data: "Delete this category ?",
}); });
modal.onClose.subscribe({ modal.onClose.pipe(take(1)).subscribe({
next: (bool) => { next: (bool) => {
if (bool) if (bool)
this.apiService.deleteCategory(c_id).subscribe({ this.apiService
next: () => { .deleteCategory(c_id)
const index = this.categories.findIndex( .pipe(take(1))
(categ) => categ.id == c_id, .subscribe({
); next: () => {
if (index > -1) { this.categories = this.categories.filter((c) => c.id !== c_id);
this.categories.splice(index, 1);
this.activeCategories = new Set( this.activeCategories = new Set(
this.categories.map((c) => c.name), this.categories.map((c) => c.name),
); );
} },
}, });
});
}, },
}); });
} }
gotoPlace(p: Place) {
this.map?.flyTo([p.lat, p.lng]);
}
gotoTrips() {
this.router.navigateByUrl("/trips");
}
logout() {
this.authService.logout();
}
closePlaceBox() {
this.selectedPlace = undefined;
}
toGithub() {
this.utilsService.toGithubTRIP();
}
check_update() {
this.apiService
.checkVersion()
.pipe(take(1))
.subscribe({
next: (remote_version) => {
if (!remote_version)
this.utilsService.toast(
"success",
"Latest version",
"You're running the latest version of TRIP",
);
if (this.info && remote_version != this.info?.version)
this.info.update = remote_version;
},
});
}
toggleLowNet() {
this.apiService
.putSettings({ mode_low_network: this.isLowNet })
.pipe(take(1))
.subscribe({
next: (_) => {
setTimeout(() => {
this.updateMarkersAndClusters();
}, 100);
},
});
}
toggleDarkMode() {
this.apiService
.putSettings({ mode_dark: this.isDarkMode })
.pipe(take(1))
.subscribe({
next: (_) => {
this.utilsService.toggleDarkMode();
},
});
}
toggleGpxInPlace() {
this.apiService
.putSettings({ mode_gpx_in_place: this.isGpxInPlaceMode })
.pipe(take(1))
.subscribe({
next: (_) => {
this.updateMarkersAndClusters();
},
});
}
} }

View File

@ -4,6 +4,8 @@ import { TripStatus } from "../types/trip";
import { ApiService } from "./api.service"; import { ApiService } from "./api.service";
import { map } from "rxjs"; import { map } from "rxjs";
type ToastSeverity = "info" | "warn" | "error" | "success";
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
}) })
@ -11,6 +13,13 @@ export class UtilsService {
private apiService = inject(ApiService); private apiService = inject(ApiService);
currency$ = this.apiService.settings$.pipe(map((s) => s?.currency ?? "€")); currency$ = this.apiService.settings$.pipe(map((s) => s?.currency ?? "€"));
readonly statuses: TripStatus[] = [
{ label: "pending", color: "#3258A8" },
{ label: "booked", color: "#007A30" },
{ label: "constraint", color: "#FFB900" },
{ label: "optional", color: "#625A84" },
];
constructor(private ngMessageService: MessageService) {} constructor(private ngMessageService: MessageService) {}
toGithubTRIP() { toGithubTRIP() {
@ -22,16 +31,12 @@ export class UtilsService {
element?.classList.toggle("dark"); element?.classList.toggle("dark");
} }
get statuses(): TripStatus[] { toast(
return [ severity: ToastSeverity = "info",
{ label: "pending", color: "#3258A8" }, summary = "Info",
{ label: "booked", color: "#007A30" }, detail = "",
{ label: "constraint", color: "#FFB900" }, life = 3000,
{ label: "optional", color: "#625A84" }, ): void {
];
}
toast(severity = "info", summary = "Info", detail = "", life = 3000): void {
this.ngMessageService.add({ this.ngMessageService.add({
severity, severity,
summary, summary,
@ -40,7 +45,7 @@ export class UtilsService {
}); });
} }
parseGoogleMapsUrl(url: string): [string, string] { parseGoogleMapsUrl(url: string): [place: string, latlng: string] {
// Look /place/<place>/ and !3d<lat> and !4d<lng> // Look /place/<place>/ and !3d<lat> and !4d<lng>
const placeMatch = url.match(/\/place\/([^\/]+)/); const placeMatch = url.match(/\/place\/([^\/]+)/);
const latMatch = url.match(/!3d([\d\-.]+)/); const latMatch = url.match(/!3d([\d\-.]+)/);
@ -52,12 +57,12 @@ export class UtilsService {
return ["", ""]; return ["", ""];
} }
let place = decodeURIComponent(placeMatch[1].replace(/\+/g, " ").trim()); const place = decodeURIComponent(placeMatch[1].replace(/\+/g, " ").trim());
let latlng = `${latMatch[1]},${lngMatch[1]}`; const latlng = `${latMatch[1]},${lngMatch[1]}`;
return [place, latlng]; return [place, latlng];
} }
currencySigns(): { c: string; s: string }[] { static currencySigns(): { c: string; s: string }[] {
return [ return [
{ c: "EUR", s: "€" }, { c: "EUR", s: "€" },
{ c: "GBP", s: "£" }, { c: "GBP", s: "£" },