wip
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
soraefir
2025-03-05 00:09:38 +01:00
parent 5d04c8777b
commit e94aad1d96
20 changed files with 163 additions and 149 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,4 +1,4 @@
export const throttle = (func: () => void, wait: number) => { export const throttle = (func: (...args: any[]) => any, wait: number) => {
var lastTime = 0; var lastTime = 0;
var timeoutId: ReturnType<typeof setTimeout> | undefined; var timeoutId: ReturnType<typeof setTimeout> | undefined;
var lastArgs: any[]; var lastArgs: any[];
@ -54,7 +54,7 @@ export const save = (id: string, v: journey) =>
export const query_nominatim = ( export const query_nominatim = (
q: string, q: string,
bb: any, bb: any,
f: (v: string) => Boolean = () => true f: (v: NominatimResult) => Boolean = () => true
) => { ) => {
if (q.length == 0) return Promise.resolve([]) if (q.length == 0) return Promise.resolve([])
let url = new URL("/api/place/" + q, window.location.origin); let url = new URL("/api/place/" + q, window.location.origin);

View File

@ -1,8 +1,8 @@
import { getGeoLine } from "./types/geom"; import { getGeoLine } from "../types/geom";
import * as api from "../api";
const filter_existing = function (tpe: "nominatim" | "travel", leg: leg, r: geoloc[]) {
export const filter_existing = function (tpe: "nominatim" | "travel", leg: leg, r: geoloc[]) {
switch (tpe) { switch (tpe) {
case 'nominatim': case 'nominatim':
return r.filter(e => { return r.filter(e => {
@ -20,7 +20,7 @@ export const filter_existing = function (tpe: "nominatim" | "travel", leg: leg,
} }
} }
export const process_results = function (tpe: "nominatim" | "travel", r: geoloc[]) { const process_results = function (tpe: "nominatim" | "travel", r: geoloc[]) {
switch (tpe) { switch (tpe) {
case 'nominatim': case 'nominatim':
return r.map((rr) => { return r.map((rr) => {
@ -39,3 +39,23 @@ export const process_results = function (tpe: "nominatim" | "travel", r: geoloc[
}); });
} }
} }
var _search_set_results: (...arg: any[]) => any;
export const set_search_set_results = function (f: (...arg: any[]) => any) {
_search_set_results = f;
}
export const search_nominatim = api.throttle(
(f: string, q: string, bb: [[number, number], [number, number]], leg: leg) =>
api.query_nominatim(q, bb, api.get_filter(f)).catch((_err) => console.log(_err)).then((r) => {
r = process_results('nominatim', r)
r = filter_existing('nominatim', leg, r)
_search_set_results(r)
}), 1000);
export const search_flight = api.throttle(
(f: string, q: string, leg: leg) =>
api.query_flight(q).then((r) => {
r = process_results('travel', r)
r = filter_existing('travel', leg, r)
_search_set_results(r)
}), 2000)

49
src/client/helper/nav.ts Normal file
View File

@ -0,0 +1,49 @@
import journey_wrapper from "../types/wrapper";
var nav = {
scrollInterval: 0,
scrollDir: 'none',
};
const sideScroll = function (element: Element, direction: 'left' | 'right' | 'none', speed: number, step: number) {
nav.scrollDir = direction
if (direction == 'none') return;
nav.scrollInterval = setInterval(() => {
element.scrollLeft += (direction == 'left') ? -step : step;
}, speed);
}
export const focus_leg = function (journey: journey_wrapper, idx: number | null = null) {
const c = document.querySelector('.scroll-content.nav-leg')!!
console.log(idx, c, journey)
const item = c.children[(idx != null ? idx : journey.sel_leg) + 1];
c.scrollLeft = (item as any).offsetLeft + ((item as any).offsetWidth / 2) - (c as any).offsetWidth / 2
}
export const focus_day = function (journey: journey_wrapper, idx: number | null = null) {
const c = document.querySelector('.scroll-content.nav-day')!!
console.log(idx, c, journey)
const item = c.children[(idx != null ? idx : journey.sel_day) + 1];
c.scrollLeft = (item as any).offsetLeft + ((item as any).offsetWidth / 2) - (c as any).offsetWidth / 2;
//focus_leg(journey) // We dont render both navs anymore
}
export const nav_mousemove = function (e: PointerEvent) {
if (e.pointerType != 'mouse') return;
const c = (e.target as any).closest('.scroll-content') || (e.target as any).firstChild;
const left = e.pageX - c.getBoundingClientRect().left;
const newDir =
left < c.offsetWidth * 0.1 ? 'left' :
(left > c.offsetWidth * 0.9 ? 'right' : 'none')
if (!nav.scrollInterval || nav.scrollDir != newDir) {
if (nav.scrollInterval) clearInterval(nav.scrollInterval)
sideScroll(c, newDir, 25, 10);
}
}
export const nav_mouseleave = function (_e: PointerEvent) {
clearInterval(nav.scrollInterval);
nav.scrollDir = 'none'
nav.scrollInterval = 0
}

View File

@ -2,8 +2,10 @@
import * as api from "./api"; import * as api from "./api";
import journey_wrapper from "./types/wrapper"; import journey_wrapper from "./types/wrapper";
import { migrator } from "./types/migration"; import { migrator } from "./types/migration";
import { journey_add_place, journey_del_place } from "./journey_helper"; import { journey_add_place, journey_del_place } from "./helper/journey";
import { filter_existing, process_results } from "./api_helper"; import { search_nominatim, search_flight } from "./helper/api";
import { focus_day, focus_leg, nav_mouseleave, nav_mousemove } from "./helper/nav";
import { set_search_set_results } from "./helper/api";
Vue.component("l-map", window.Vue2Leaflet.LMap); Vue.component("l-map", window.Vue2Leaflet.LMap);
Vue.component("l-tile-layer", window.Vue2Leaflet.LTileLayer); Vue.component("l-tile-layer", window.Vue2Leaflet.LTileLayer);
@ -23,20 +25,16 @@ const app = new Vue({
query: { query: {
type: "", query: "", res: [], load: false, sub: false, note: false, drawer: false, addmarker: false, type: "", query: "", res: [], load: false, sub: false, note: false, drawer: false, addmarker: false,
}, },
nav: {
scrollInterval: null,
scrollDir: null,
useDay: false, useDay: false,
},
toast: { toast: {
show: true, show: false,
title: 'Confirm deletion', title: '',
desc: 'Are you sure ? This is irreversible...', desc: '',
special: '', special: '',
acceptText: 'delete', acceptText: '',
cancelText: 'cancel', cancelText: '',
func: () => { },
}, },
impexp: "",
lang: { lang: {
format: "ddd D MMM", format: "ddd D MMM",
formatLocale: { formatLocale: {
@ -74,11 +72,10 @@ const app = new Vue({
return `<i class="fa fa-${api.icon_type(item) || "star"} fa-2x ${classes}" style="${styling}; color:${fcolor || "white"}; text-align:center; align-content:center;"></i>`; return `<i class="fa fa-${api.icon_type(item) || "star"} fa-2x ${classes}" style="${styling}; color:${fcolor || "white"}; text-align:center; align-content:center;"></i>`;
}, },
import_data: function (v) {
import_data: function () {
this.journey.data = Object.assign( this.journey.data = Object.assign(
{}, {},
JSON.parse(this.impexp.toDecoded()), JSON.parse(v.toDecoded()),
); );
this.journey.data.main.forEach((e) => { this.journey.data.main.forEach((e) => {
if (e.date_range) { if (e.date_range) {
@ -87,10 +84,26 @@ const app = new Vue({
} }
}); });
}, },
export_data: function () {
this.impexp = JSON.stringify(this.journey.data).toEncoded(); toast_reset: function () {
this.toast.show = false;
this.toast.title = '';
this.toast.desc = ''
this.toast.special = '';
this.toast.acceptText = ''
this.toast.cancelText = ''
this.toast.func = () => { }
}, },
toast_impexp: function (is_import) {
this.toast.show = true;
this.toast.title = is_import ? 'Import' : 'Export';
this.toast.desc = ''
this.toast.special = JSON.stringify(this.journey.data).toEncoded();
this.toast.acceptText = is_import ? 'load' : ''
this.toast.cancelText = 'cancel'
this.toast.func = () => { this.import_data(this.toast.special); this.toast_reset(); }
},
search_set_results(r) { search_set_results(r) {
this.query.load = false; this.query.load = false;
@ -114,21 +127,6 @@ const app = new Vue({
setTimeout(() => this.$refs.map.mapObject.invalidateSize(), 500); setTimeout(() => this.$refs.map.mapObject.invalidateSize(), 500);
}, },
search_nominatim: function (f) {
return (q) => api.query_nominatim(q, this.compute_bb(), api.get_filter(f)).catch((_err) => []).then((r) => {
r = process_results('nominatim', r)
r = filter_existing('nominatim', this.journey.leg_get(), r)
return this.search_set_results(r)
});
},
search_travel: function (f) {
return (q) => api.query_flight(q).then((r) => {
r = process_results('travel', r)
r = filter_existing('travel', this.journey.leg_get(), r)
return this.search_set_results(r)
});
},
drawer_hover_item: function (item) { drawer_hover_item: function (item) {
if (item) { if (item) {
this.map_override.active = true this.map_override.active = true
@ -143,88 +141,46 @@ const app = new Vue({
}, },
drawer_click_item: function (item) { drawer_click_item: function (item) {
const tpe = this.query.type;
this.search_set_clear(item ? true : false); this.search_set_clear(item ? true : false);
this.drawer_hover_item(); this.drawer_hover_item();
if (item) { if (item) {
item.step = -1; item.step = -1;
journey_add_place(this.journey, tpe, item) journey_add_place(this.journey, this.query.type, item)
} }
}, },
place_delete(tpe, idx) { search_active: function (_e) {
journey_del_place(this.journey, tpe, idx) const tpe = this.query.type;
}, const query = this.query.query;
switch (tpe) {
case 'hotel':
case 'restaurant': ;
case 'place':
case 'other':
return search_nominatim(tpe, query, this.compute_bb(), this.journey.leg_get())
search_active: function (q) { case 'flight':
const txt = q.target.value return search_flight(tpe, query, this.journey.leg_get());
switch (this.query.type) {
case 'hotel': return this.search_hotel(txt);
case 'restaurant': return this.search_restaurant(txt);
case 'place': return this.search_place(txt);
case 'other': return this.search_other(txt);
case 'flight': return this.search_flight(txt);
} }
}, },
search_enable: function (f) { search_enable: function (f) {
const is_notes = f == 'notes'; const is_notes = f == 'notes';
this.search_set_active(is_notes, f) this.search_set_active(is_notes, f)
const query_in = document.getElementById(is_notes ? 'query_note' : 'query_input') setTimeout(() => document.getElementById(is_notes ? 'query_note' : 'query_input').focus(), 500);
setTimeout(() => query_in.focus(), 500); if (!is_notes) this.search_active()
if (!is_notes) this.search_active({ target: query_in })
}, },
sideScroll: function (element, direction, speed, step) { refreshTextAreaHeight: function (e) {
this.nav.scrollDir = direction e.target.style['height'] = 'auto';
if (direction == 'none') return; e.target.style['height'] = e.target.scrollHeight + 'px';
this.nav.scrollInterval = setInterval(() => { e.target.style['max-height'] = "100%";
element.scrollLeft += (direction == 'left') ? -step : step;
}, speed);
},
focus_leg(idx) {
const c = document.querySelector('.scroll-content.nav-leg')
const item = c.children[(idx != undefined ? idx : this.journey.sel_leg) + 1];
c.scrollLeft = item.offsetLeft + (item.offsetWidth / 2) - c.offsetWidth / 2
},
focus_day(idx) {
const c = document.querySelector('.scroll-content.nav-day')
const item = c.children[(idx != undefined ? idx : this.journey.sel_day) + 1];
c.scrollLeft = item.offsetLeft + (item.offsetWidth / 2) - c.offsetWidth / 2;
this.focus_leg()
},
nav_mousemove(e) {
if (e.pointerType != 'mouse') return;
const c = e.target.closest('.scroll-content') || e.target.firstChild;
const left = e.pageX - c.getBoundingClientRect().left;
const newDir =
left < c.offsetWidth * 0.1 ? 'left' :
(left > c.offsetWidth * 0.9 ? 'right' : 'none')
if (!this.nav.scrollInterval || this.nav.scrollDir != newDir) {
if (this.nav.scrollInterval) clearInterval(this.nav.scrollInterval)
this.sideScroll(c, newDir, 25, 10);
}
},
nav_mouseleave(e) {
clearInterval(this.nav.scrollInterval);
this.nav.scrollDir = 'none'
this.nav.scrollInterval = null
},
refreshTextAreaHeight(event) {
event.target.style['height'] = 'auto';
event.target.style['height'] = event.target.scrollHeight + 'px';
event.target.style['max-height'] = "100%";
}, },
onMapClick(e) { onMapClick(e) {
if (this.query.addmarker) { if (this.query.addmarker) {
const newMarker = { const newMarker = {
latlon: [e.latlng.lat, e.latlng.lng], latlon: [e.latlng.lat, e.latlng.lng],
lat: e.latlng.lat,
lon: e.latlng.lng,
sname: this.query.query, sname: this.query.query,
type: this.query.type, type: this.query.type,
}; };
@ -233,34 +189,31 @@ const app = new Vue({
}, },
}, },
created: function () { created: function () {
this.search_hotel = api.throttle(this.search_nominatim("hotel"), 1000) set_search_set_results(this.search_set_results);
this.search_restaurant = api.throttle(this.search_nominatim("restaurant"), 1000) this.nav_mouseleave = nav_mouseleave;
this.search_place = api.throttle(this.search_nominatim("place"), 1000) this.nav_mousemove = nav_mousemove;
this.search_flight = api.throttle(this.search_travel("flight"), 2000) this.focus_day = focus_day;
this.focus_leg = focus_leg;
this.place_delete = (tpe, idx) => journey_del_place(this.journey, tpe, idx);
this.save_data = api.throttle(() => { this.save_data = api.throttle(() => {
this.impexp = JSON.stringify(this.journey.data).toEncoded();
api.save(this.journey.id, this.journey.data); api.save(this.journey.id, this.journey.data);
}, 1000); }, 1000);
this.pressed_prev = api.throttle(() => { this.pressed_prev = api.throttle(() => {
if (this.nav.useDay) this.journey.day_prev(); if (this.useDay) this.journey.day_prev();
else this.journey.leg_prev(); else this.journey.leg_prev();
}, 200) }, 250)
this.pressed_next = api.throttle(() => { this.pressed_next = api.throttle(() => {
if (this.nav.useDay) this.journey.day_next(); if (this.useDay) this.journey.day_next();
else this.journey.leg_next(); else this.journey.leg_next();
}, 200) }, 250)
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
switch (e.key) { switch (e.key) {
case "ArrowLeft": case "ArrowLeft": return this.pressed_prev()
this.pressed_prev() case "ArrowRight": return this.pressed_next()
break; default: return console.log(e.key);
case "ArrowRight":
this.pressed_next()
break;
default:
console.log(e.key);
} }
}); });
@ -271,8 +224,7 @@ const app = new Vue({
watch: { watch: {
journey: { journey: {
handler: function (ndata, odata) { handler: function (ndata, odata) {
if (this.edit_active) if (this.edit_active) this.save_data();
this.save_data();
}, },
deep: true, deep: true,
}, },

View File

@ -1,3 +1,7 @@
* {
box-sizing: border-box;
}
html, html,
body, body,
body, body,

View File

@ -1,13 +0,0 @@
.impexp
.container-medium.section
.aligner
.input.col-sm-4
input(v-model="impexp", type="text")
.col-sm-2
button.button.button--primary.button--mobileFull(
v-on:click="import_data"
) Import
.col-sm-2
button.button.button--primary.button--mobileFull(
v-on:click="export_data"
) Export

View File

@ -42,7 +42,7 @@ div(v-else-if="['flight'].indexOf(query.type)>=0")
div( v-html="generate_icon('plane', 'var(--dark)')") div( v-html="generate_icon('plane', 'var(--dark)')")
.col-10() .col-10()
| {{ item.from }} => {{item.to}} | {{ item.from }} => {{item.to}}
bg-dark.divider( .bg-dark.divider(
:key="'qdiv'+idx" style="height:1px" ) :key="'qdiv'+idx" style="height:1px" )
div(v-else) div(v-else)
template() template()

View File

@ -1,7 +1,7 @@
.row .row
.mr-auto.col-auto .mr-auto.col-auto
.list-group.text-dark .list-group.text-dark
.list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.day_prev(); focus_day();") .list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.day_prev(); focus_day(journey);")
i.fas.fa-angle-left i.fas.fa-angle-left
.col-8.col-sm-8.col-md-10.scroll-handler( .col-8.col-sm-8.col-md-10.scroll-handler(
@pointerleave="nav_mouseleave" @pointerleave="nav_mouseleave"
@ -37,7 +37,7 @@
.ml-auto.col-auto .ml-auto.col-auto
.list-group.text-dark .list-group.text-dark
.list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.day_next(); focus_day();") .list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.day_next(); focus_day(journey);")
i.fas.fa-angle-right i.fas.fa-angle-right
.row .row
.col-8.col-sm-6.col-md-4 .col-8.col-sm-6.col-md-4

View File

@ -1,7 +1,7 @@
.row .row
.mr-auto.col-auto .mr-auto.col-auto
.list-group.text-dark .list-group.text-dark
.list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.leg_prev(); focus_leg();") .list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.leg_prev(); focus_leg(journey);")
i.fas.fa-angle-left i.fas.fa-angle-left
.col-8.col-sm-8.col-md-10.scroll-handler( .col-8.col-sm-8.col-md-10.scroll-handler(
@pointerleave="nav_mouseleave" @pointerleave="nav_mouseleave"
@ -26,14 +26,14 @@
.text {{ element.title || "Leg "+idx}} .text {{ element.title || "Leg "+idx}}
i.fa.fa-times.text-small.fright( i.fa.fa-times.text-small.fright(
style="top: 2px; right: 2px; position: absolute", style="top: 2px; right: 2px; position: absolute",
@click="journey.rm_leg(idx); focus_leg();" @click="journey.rm_leg(idx); focus_leg(journey);"
) )
.list-group-item.bg-white.text-white.show.placeholder-right( .list-group-item.bg-white.text-white.show.placeholder-right(
:class="journey.sel_leg < journey.data.main.length-1? '': 'bg-dark'" :class="journey.sel_leg < journey.data.main.length-1? '': 'bg-dark'"
slot="footer" slot="footer"
) )
.text {{'⋅'}} .text {{'⋅'}}
.list-group-item.bg-white.text-dark.add(@click="journey.add_leg(); focus_leg();" .list-group-item.bg-white.text-dark.add(@click="journey.add_leg(); focus_leg(journey);"
slot="footer" slot="footer"
:class="journey.sel_leg >= journey.data.main.length-1? 'show': ''") :class="journey.sel_leg >= journey.data.main.length-1? 'show': ''")
div div
@ -41,7 +41,7 @@
.ml-auto.col-auto .ml-auto.col-auto
.list-group.text-dark .list-group.text-dark
.list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.leg_next(); focus_leg();") .list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.leg_next(); focus_leg(journey);")
i.fas.fa-angle-right i.fas.fa-angle-right
.row.text-center .row.text-center
.col-6.col-sm-6.col-md-5.mr-auto .col-6.col-sm-6.col-md-5.mr-auto

View File

@ -6,12 +6,12 @@ include smallbar.pug
.col-6.col-sm-4.col-md-3 .col-6.col-sm-4.col-md-3
.input.text-big .input.text-big
input.text-center(v-model="journey.data.name" placeholder="My Journey" type="text") input.text-center(v-model="journey.data.name" placeholder="My Journey" type="text")
.col-6.col-sm-4.col-md-3( v-if="nav.useDay" ) .col-6.col-sm-4.col-md-3( v-if="useDay" )
.input.text-small .input.text-small
input.text-center(v-model="journey.leg_get().title" placeholder="Leg" type="text") input.text-center(v-model="journey.leg_get().title" placeholder="Leg" type="text")
//- input.small(type="text", :placeholder="journey.date_tot() + ' (' + journey.tot_len() + ')'" ) //- input.small(type="text", :placeholder="journey.date_tot() + ' (' + journey.tot_len() + ')'" )
template(v-if="nav.useDay") template(v-if="useDay")
include leg/top_day.pug include leg/top_day.pug
template(v-else) template(v-else)
include leg/top_leg.pug include leg/top_leg.pug
@ -23,5 +23,3 @@ include smallbar.pug
include leg/drawer.pug include leg/drawer.pug
.h-100(:class="{ 'w-100': query.note, 'w-0': !query.note }") .h-100(:class="{ 'w-100': query.note, 'w-0': !query.note }")
include leg/drawer-notes.pug include leg/drawer-notes.pug
//- include impexp.pug

View File

@ -1,7 +1,7 @@
l-marker( l-marker(
v-if="map_override.active", v-if="map_override.active",
v-for="(el, idx) in map_override.elements" v-for="(el, idx) in map_override.elements"
key="'ovr'+idx" :key="'ovr'+idx"
:lat-lng="el" :lat-lng="el"
) )
l-icon(v-html="generate_marker('plus', 'darkgreen')") l-icon(v-html="generate_marker('plus', 'darkgreen')")

View File

@ -14,17 +14,17 @@
.tooltip-text Editor .tooltip-text Editor
.col-1 .col-1
.col-1 .col-1
a.text-white.tooltip(v-on:click.prevent="import_data") a.text-white.tooltip(v-on:click.prevent="toast_impexp(true)")
i.fas.fa-file-import i.fas.fa-file-import
.tooltip-text Import .tooltip-text Import
.col-1 .col-1
a.text-white.tooltip(v-on:click.prevent="export_data") a.text-white.tooltip(v-on:click.prevent="toast_impexp(false)")
i.fas.fa-file-export i.fas.fa-file-export
.tooltip-text Export .tooltip-text Export
.col-auto.ml-auto .col-auto.ml-auto
.switch.legday-switch .switch.legday-switch
input(type="checkbox" v-model="nav.useDay") input(type="checkbox" v-model="useDay")
.rocker .rocker
.rocker-left LEG .rocker-left LEG
.rocker-right DAY .rocker-right DAY

View File

@ -3,8 +3,12 @@ template
.popup.bg-white(@click.stop) .popup.bg-white(@click.stop)
.row .row
.col-auto.text-huge {{ toast.title }} .col-auto.text-huge {{ toast.title }}
.row .row(v-if="toast.desc")
.col-auto.text-medium {{ toast.desc }} .col-auto.text-medium {{ toast.desc }}
.row(v-if="toast.special")
.col-auto.text-medium
.input
input(v-model="toast.special")
.row.align .row.align
button.button.bg-white(@click="toast-accept" v-if="toast.acceptText") {{ toast.acceptText }} button.button.bg-white(@click="toast.func" v-if="toast.acceptText") {{ toast.acceptText }}
button.button.bg-white(@click="toast.show=false;" v-if="toast.cancelText") {{ toast.cancelText }} button.button.bg-white(@click="toast.show=false;" v-if="toast.cancelText") {{ toast.cancelText }}