first part of refactoring

- use `ImageData` and `Loading` types for image transfer
-  consolidate library directory
- remove `advent22` plugin in favor of static `API` class
This commit is contained in:
Jörn-Michael Miehe 2024-08-26 14:14:07 +00:00
parent e3e19a0572
commit 109dec73d2
14 changed files with 113 additions and 72 deletions

View file

@ -7,7 +7,18 @@
</section> </section>
<section class="section px-3"> <section class="section px-3">
<div class="container"> <progress
v-if="store.background_image === 'loading'"
class="progress is-primary"
max="100"
/>
<div
v-else-if="store.background_image === 'error'"
class="notification is-danger"
>
Hintergrundbild konnte nicht geladen werden
</div>
<div v-else class="container">
<AdminView v-if="store.is_admin" /> <AdminView v-if="store.is_admin" />
<UserView v-else /> <UserView v-else />
</div> </div>

View file

@ -15,6 +15,7 @@ import { Credentials } from "@/lib/model";
import { advent22Store } from "@/lib/store"; import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { APIError } from "@/lib/api_error";
import BulmaButton from "./bulma/Button.vue"; import BulmaButton from "./bulma/Button.vue";
import LoginModal from "./LoginModal.vue"; import LoginModal from "./LoginModal.vue";
@ -44,7 +45,7 @@ export default class extends Vue {
this.store this.store
.login(creds) .login(creds)
.catch(this.store.alert_user_error) .catch((error) => APIError.alert(error))
.finally(() => (this.is_busy = false)); .finally(() => (this.is_busy = false));
} }

View file

@ -29,14 +29,14 @@
<figure> <figure>
<div class="image is-unselectable"> <div class="image is-unselectable">
<img :src="store.background_image" /> <img :src="_ensure_loaded(store.background_image).data_url" />
<ThouCanvas> <ThouCanvas>
<CalendarDoor <CalendarDoor
v-for="(door, index) in doors" v-for="(door, index) in doors"
:key="`door-${index}`" :key="`door-${index}`"
:door="door" :door="door"
:visible="store.is_touch_device" :visible="store.is_touch_device"
:title="$advent22.name_door(door.day)" :title="_name_door(door.day)"
@click="door_click(door.day)" @click="door_click(door.day)"
style="cursor: pointer" style="cursor: pointer"
/> />
@ -46,6 +46,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { API } from "@/lib/api";
import { APIError } from "@/lib/api_error";
import { ensure_loaded, Loading, name_door } from "@/lib/helpers";
import { ImageData } from "@/lib/model";
import { Door } from "@/lib/rects/door"; import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store"; import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
@ -96,26 +100,32 @@ export default class extends Vue {
}); });
} }
public door_click(day: number) { public async door_click(day: number) {
if (this.toast_timeout !== undefined) clearTimeout(this.toast_timeout); if (this.toast_timeout !== undefined) clearTimeout(this.toast_timeout);
this.toast?.hide(); this.toast?.hide();
if (this.multi_modal === undefined) return; if (this.multi_modal === undefined) return;
this.multi_modal.show_progress(); this.multi_modal.show_progress();
this.$advent22 try {
.api_get_blob(`user/image_${day}`) const day_image = await API.request<ImageData>(`user/image_${day}`);
.then((image_src) => { this.multi_modal!.show_image(day_image.data_url, name_door(day));
this.multi_modal!.show_image(image_src, this.$advent22.name_door(day)); } catch (error) {
}) APIError.alert(error);
.catch((error) => {
this.store.alert_user_error(error);
this.multi_modal!.hide(); this.multi_modal!.hide();
}); }
} }
public beforeUnmount(): void { public beforeUnmount(): void {
this.toast?.hide(); this.toast?.hide();
} }
public _ensure_loaded<T>(o: Loading<T>): T {
return ensure_loaded(o);
}
public _name_door(day: number): string {
return name_door(day);
}
} }
</script> </script>

View file

@ -1,5 +1,4 @@
<template> <template>
<template v-if="store.is_initialized === true">
<Calendar :doors="store.user_doors" /> <Calendar :doors="store.user_doors" />
<hr /> <hr />
<div class="content" v-html="store.site_config.content" /> <div class="content" v-html="store.site_config.content" />
@ -18,8 +17,6 @@
<CountDown :until="store.next_door_target" /> <CountDown :until="store.next_door_target" />
</template> </template>
</div> </div>
</template>
<progress v-else class="progress is-primary" max="100" />
</template> </template>
<script lang="ts"> <script lang="ts">

View file

@ -46,8 +46,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { objForEach } from "@/lib/helpers"; import { API } from "@/lib/api";
import { NumStrDict } from "@/lib/model"; import { name_door, objForEach } from "@/lib/helpers";
import { ImageData, NumStrDict } from "@/lib/model";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import MultiModal from "../MultiModal.vue"; import MultiModal from "../MultiModal.vue";
@ -77,8 +78,8 @@ export default class extends Vue {
public on_open(ready: () => void, fail: () => void): void { public on_open(ready: () => void, fail: () => void): void {
Promise.all([ Promise.all([
this.$advent22.api_get<NumStrDict>("admin/day_parts"), API.request<NumStrDict>("admin/day_parts"),
this.$advent22.api_get<NumStrDict>("admin/day_image_names"), API.request<NumStrDict>("admin/day_image_names"),
]) ])
.then(([day_parts, day_image_names]) => { .then(([day_parts, day_image_names]) => {
const _ensure_day_in_data = (day: number) => { const _ensure_day_in_data = (day: number) => {
@ -102,16 +103,16 @@ export default class extends Vue {
.catch(fail); .catch(fail);
} }
public door_click(day: number) { public async door_click(day: number) {
if (this.multi_modal === undefined) return; if (this.multi_modal === undefined) return;
this.multi_modal.show_progress(); this.multi_modal.show_progress();
this.$advent22 try {
.api_get_blob(`user/image_${day}`) const day_image = await API.request<ImageData>(`user/image_${day}`);
.then((image_src) => this.multi_modal!.show_image(day_image.data_url, name_door(day));
this.multi_modal!.show_image(image_src, this.$advent22.name_door(day)), } catch (error) {
) this.multi_modal!.hide();
.catch(() => this.multi_modal!.hide()); }
} }
} }
</script> </script>

View file

@ -189,6 +189,7 @@ import { advent22Store } from "@/lib/store";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { API } from "@/lib/api";
import BulmaDrawer from "../bulma/Drawer.vue"; import BulmaDrawer from "../bulma/Drawer.vue";
import BulmaSecret from "../bulma/Secret.vue"; import BulmaSecret from "../bulma/Secret.vue";
import CountDown from "../CountDown.vue"; import CountDown from "../CountDown.vue";
@ -256,8 +257,8 @@ export default class extends Vue {
public on_open(ready: () => void, fail: () => void): void { public on_open(ready: () => void, fail: () => void): void {
Promise.all([ Promise.all([
this.store.update(), this.store.update(),
this.$advent22.api_get<AdminConfigModel>("admin/config_model"), API.request<AdminConfigModel>("admin/config_model"),
this.$advent22.api_get<DoorSaved[]>("admin/doors"), API.request<DoorSaved[]>("admin/doors"),
]) ])
.then(([store_update, admin_config_model, doors]) => { .then(([store_update, admin_config_model, doors]) => {
store_update; // discard value store_update; // discard value
@ -271,15 +272,13 @@ export default class extends Vue {
} }
public load_dav_credentials(): void { public load_dav_credentials(): void {
this.$advent22 API.request<Credentials>("admin/dav_credentials")
.api_get<Credentials>("admin/dav_credentials")
.then((creds) => (this.dav_credentials = creds)) .then((creds) => (this.dav_credentials = creds))
.catch(() => {}); .catch(() => {});
} }
public load_ui_credentials(): void { public load_ui_credentials(): void {
this.$advent22 API.request<Credentials>("admin/ui_credentials")
.api_get<Credentials>("admin/ui_credentials")
.then((creds) => (this.ui_credentials = creds)) .then((creds) => (this.ui_credentials = creds))
.catch(() => {}); .catch(() => {});
} }

View file

@ -74,6 +74,8 @@ import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store"; import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { API } from "@/lib/api";
import { APIError } from "@/lib/api_error";
import { toast } from "bulma-toast"; import { toast } from "bulma-toast";
import Calendar from "../Calendar.vue"; import Calendar from "../Calendar.vue";
import BulmaBreadcrumbs, { Step } from "../bulma/Breadcrumbs.vue"; import BulmaBreadcrumbs, { Step } from "../bulma/Breadcrumbs.vue";
@ -107,8 +109,7 @@ export default class extends Vue {
private load_doors(): Promise<void> { private load_doors(): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.$advent22 API.request<DoorSaved[]>("admin/doors")
.api_get<DoorSaved[]>("admin/doors")
.then((data) => { .then((data) => {
this.doors.length = 0; this.doors.length = 0;
@ -119,7 +120,7 @@ export default class extends Vue {
resolve(); resolve();
}) })
.catch((error) => { .catch((error) => {
this.store.alert_user_error(error); APIError.alert(error);
reject(); reject();
}); });
}); });
@ -133,11 +134,10 @@ export default class extends Vue {
data.push(door.save()); data.push(door.save());
} }
this.$advent22 API.request<void>({ endpoint: "admin/doors", method: "PUT", data: data })
.api_put("admin/doors", data)
.then(resolve) .then(resolve)
.catch((error) => { .catch((error) => {
this.store.alert_user_error(error); APIError.alert(error);
reject(); reject();
}); });
}); });

View file

@ -1,10 +1,10 @@
<template> <template>
<foreignObject <foreignObject
:x="Math.round(store.calendar_aspect_ratio * rectangle.left)" :x="Math.round(aspect_ratio * rectangle.left)"
:y="rectangle.top" :y="rectangle.top"
:width="Math.round(store.calendar_aspect_ratio * rectangle.width)" :width="Math.round(aspect_ratio * rectangle.width)"
:height="rectangle.height" :height="rectangle.height"
:style="`transform: scaleX(${1 / store.calendar_aspect_ratio})`" :style="`transform: scaleX(${1 / aspect_ratio})`"
> >
<div <div
xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml"
@ -18,6 +18,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { loading_success } from "@/lib/helpers";
import { Rectangle } from "@/lib/rects/rectangle"; import { Rectangle } from "@/lib/rects/rectangle";
import { advent22Store } from "@/lib/store"; import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
@ -59,6 +60,14 @@ export default class extends Vue {
return result; return result;
} }
public get aspect_ratio(): number {
if (!loading_success(this.store.background_image)) return 1;
return (
this.store.background_image.height / this.store.background_image.width
);
}
} }
</script> </script>

View file

@ -57,15 +57,6 @@ function mouse_event_validator(event: object, point: object): boolean {
export default class extends Vue { export default class extends Vue {
public readonly store = advent22Store(); public readonly store = advent22Store();
public mounted(): void {
new ResizeObserver(([first, ...rest]) => {
if (rest.length > 0)
console.warn(`Unexpected ${rest.length} extra entries!`);
this.store.set_calendar_aspect_ratio(first.contentRect);
}).observe(this.$el);
}
public transform_mouse_event(event: MouseEvent) { public transform_mouse_event(event: MouseEvent) {
const point = get_event_thous(event); const point = get_event_thous(event);
this.$emit(event.type, event, point); this.$emit(event.type, event, point);

View file

@ -11,7 +11,7 @@
</ul> </ul>
</div> </div>
<figure class="image is-unselectable"> <figure class="image is-unselectable">
<img :src="store.background_image" /> <img :src="_ensure_loaded(store.background_image).data_url" />
<ThouCanvas> <ThouCanvas>
<PreviewDoor <PreviewDoor
v-for="(door, index) in doors" v-for="(door, index) in doors"
@ -24,6 +24,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ensure_loaded, Loading } from "@/lib/helpers";
import { Door } from "@/lib/rects/door"; import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store"; import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
@ -43,5 +44,9 @@ import PreviewDoor from "./PreviewDoor.vue";
export default class extends Vue { export default class extends Vue {
public doors!: Door[]; public doors!: Door[];
public readonly store = advent22Store(); public readonly store = advent22Store();
public _ensure_loaded<T>(o: Loading<T>): T {
return ensure_loaded(o);
}
} }
</script> </script>

View file

@ -9,13 +9,14 @@
</ul> </ul>
</div> </div>
<figure class="image is-unselectable"> <figure class="image is-unselectable">
<img :src="store.background_image" /> <img :src="_ensure_loaded(store.background_image).data_url" />
<DoorCanvas :doors="doors" /> <DoorCanvas :doors="doors" />
</figure> </figure>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ensure_loaded, Loading } from "@/lib/helpers";
import { Door } from "@/lib/rects/door"; import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store"; import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
@ -33,5 +34,9 @@ import DoorCanvas from "./DoorCanvas.vue";
export default class extends Vue { export default class extends Vue {
public doors!: Door[]; public doors!: Door[];
public readonly store = advent22Store(); public readonly store = advent22Store();
public _ensure_loaded<T>(o: Loading<T>): T {
return ensure_loaded(o);
}
} }
</script> </script>

View file

@ -70,4 +70,8 @@ export class APIError extends Error {
type: "is-danger", type: "is-danger",
}); });
} }
public static alert(error: unknown) {
new APIError(error, "").alert();
}
} }

View file

@ -20,6 +20,12 @@ export function loading_success<T>(o: Loading<T>): o is T {
return true; return true;
} }
export function ensure_loaded<T>(o: Loading<T>): T {
if (!loading_success(o)) throw "";
return o;
}
export function handle_error(error: unknown) { export function handle_error(error: unknown) {
if (error instanceof APIError) { if (error instanceof APIError) {
error.alert(); error.alert();
@ -27,3 +33,7 @@ export function handle_error(error: unknown) {
console.error(error); console.error(error);
} }
} }
export function name_door(day: number): string {
return `Türchen ${day}`;
}

View file

@ -1,5 +1,4 @@
import { advent22Store } from "@/lib/store"; import { advent22Store } from "@/lib/store";
import { Advent22Plugin } from "@/plugins/advent22";
import { FontAwesomePlugin } from "@/plugins/fontawesome"; import { FontAwesomePlugin } from "@/plugins/fontawesome";
import * as bulmaToast from "bulma-toast"; import * as bulmaToast from "bulma-toast";
import { createPinia } from "pinia"; import { createPinia } from "pinia";
@ -10,10 +9,9 @@ import "@/main.scss";
const app = createApp(App); const app = createApp(App);
app.use(Advent22Plugin);
app.use(FontAwesomePlugin); app.use(FontAwesomePlugin);
app.use(createPinia()); app.use(createPinia());
advent22Store().init(); advent22Store().init();
app.mount("#app"); app.mount("#app");