Compare commits

...

7 commits

11 changed files with 134 additions and 70 deletions

View file

@ -1,7 +1,5 @@
# MUSS # MUSS
- api: Config-Liste von Extra-Türchen (kein Buchstabe, nur manuelles Bild)
- api: Config-Option "Überspringe leere Türchen" (standard ja)
# KANN # KANN
@ -21,3 +19,5 @@
- `alert` durch bulma Komponente(n) ersetzen - `alert` durch bulma Komponente(n) ersetzen
- api: admin Login case sensitivity (username "admin" == "AdMiN") - api: admin Login case sensitivity (username "admin" == "AdMiN")
- api: `config.solution` - whitespace="IGNORE"->"REMOVE" umbenennen, +Sonderzeichen - api: `config.solution` - whitespace="IGNORE"->"REMOVE" umbenennen, +Sonderzeichen
- api: Config-Option "Überspringe leere Türchen" (standard ja)
- api: Config-Liste von Extra-Türchen (kein Buchstabe, nur manuelles Bild)

View file

@ -43,6 +43,12 @@ class Puzzle(BaseModel):
# Kalender so viele Tage nach der letzten Türöffnung schließen # Kalender so viele Tage nach der letzten Türöffnung schließen
close_after: int = 90 close_after: int = 90
# Tage, für die kein Buchstabe vorgesehen wird
extra_days: set[int] = set()
# Türchen ohne Buchstabe überspringen
skip_empty: bool = True
class Image(BaseModel): class Image(BaseModel):
# Quadrat, Seitenlänge in px # Quadrat, Seitenlänge in px

View file

@ -1,5 +1,4 @@
import re import re
from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date from datetime import date
from io import BytesIO from io import BytesIO
@ -34,23 +33,6 @@ async def get_all_sorted_days(
return sorted(set(door.day for door in cal_cfg.doors)) return sorted(set(door.day for door in cal_cfg.doors))
async def get_all_event_dates(
cfg: Config = Depends(get_config),
days: list[int] = Depends(get_all_sorted_days),
) -> EventDates:
"""
Aktueller Kalender-Zeitraum
"""
return EventDates(
today=date.today(),
begin_month=cfg.puzzle.begin_month,
begin_day=cfg.puzzle.begin_day,
events=days,
close_after=cfg.puzzle.close_after,
)
async def get_all_parts( async def get_all_parts(
cfg: Config = Depends(get_config), cfg: Config = Depends(get_config),
days: list[int] = Depends(get_all_sorted_days), days: list[int] = Depends(get_all_sorted_days),
@ -59,6 +41,11 @@ async def get_all_parts(
Lösung auf vorhandene Tage aufteilen Lösung auf vorhandene Tage aufteilen
""" """
# noch keine Buchstaben verteilt
result = {day: "" for day in days}
# extra-Tage ausfiltern
days = [day for day in days if day not in cfg.puzzle.extra_days]
solution_length = len(cfg.solution.clean) solution_length = len(cfg.solution.clean)
num_days = len(days) num_days = len(days)
@ -72,15 +59,33 @@ async def get_all_parts(
*rnd.sample(days, solution_length % num_days), *rnd.sample(days, solution_length % num_days),
] ]
result: defaultdict[int, str] = defaultdict(str)
for day, letter in zip(solution_days, cfg.solution.clean): for day, letter in zip(solution_days, cfg.solution.clean):
result[day] += letter result[day] += letter
result |= {missed_day: "" for missed_day in set(days) - set(result.keys())}
return result return result
async def get_all_event_dates(
cfg: Config = Depends(get_config),
days: list[int] = Depends(get_all_sorted_days),
parts: dict[int, str] = Depends(get_all_parts),
) -> EventDates:
"""
Aktueller Kalender-Zeitraum
"""
if cfg.puzzle.skip_empty:
days = [day for day in days if parts[day] != "" or day in cfg.puzzle.extra_days]
return EventDates(
today=date.today(),
begin_month=cfg.puzzle.begin_month,
begin_day=cfg.puzzle.begin_day,
events=days,
close_after=cfg.puzzle.close_after,
)
async def get_all_auto_image_names( async def get_all_auto_image_names(
days: list[int] = Depends(get_all_sorted_days), days: list[int] = Depends(get_all_sorted_days),
images: list[str] = Depends(list_images_auto), images: list[str] = Depends(list_images_auto),

View file

@ -46,7 +46,7 @@ async def user_visible_days(
event_dates: EventDates = Depends(get_all_event_dates), event_dates: EventDates = Depends(get_all_event_dates),
) -> list[int]: ) -> list[int]:
""" """
Anzahl der user-sichtbaren Türchen User-sichtbare Türchen
""" """
today = date.today() today = date.today()

View file

@ -41,6 +41,8 @@ class AdminConfigModel(BaseModel):
last: date last: date
end: date end: date
seed: str seed: str
extra_days: list[int]
skip_empty: bool
class __Calendar(BaseModel): class __Calendar(BaseModel):
config_file: str config_file: str
@ -92,6 +94,8 @@ async def get_config_model(
"last": event_dates.last, "last": event_dates.last,
"end": event_dates.end, "end": event_dates.end,
"seed": cfg.random_seed, "seed": cfg.random_seed,
"extra_days": sorted(cfg.puzzle.extra_days),
"skip_empty": cfg.puzzle.skip_empty,
}, },
"calendar": { "calendar": {
"config_file": cfg.calendar, "config_file": cfg.calendar,

View file

@ -89,6 +89,7 @@ export default class extends Vue {
this.store.when_initialized(() => { this.store.when_initialized(() => {
this.toast_timeout = setTimeout(() => { this.toast_timeout = setTimeout(() => {
if (this.store.user_doors.length === 0) return; if (this.store.user_doors.length === 0) return;
if (this.store.is_touch_device) return;
this.toast!.show({ duration: 600000, type: "is-warning" }); this.toast!.show({ duration: 600000, type: "is-warning" });
}, 10e3); }, 10e3);

View file

@ -68,6 +68,23 @@
<dd class="is-family-monospace"> <dd class="is-family-monospace">
"{{ admin_config_model.puzzle.seed }}" "{{ admin_config_model.puzzle.seed }}"
</dd> </dd>
<dt>Extra-Tage</dt>
<dd>
<template
v-for="(day, index) in admin_config_model.puzzle.extra_days"
:key="`extra_day-${index}`"
>
<span>
<template v-if="index > 0">, </template>
{{ day }}
</span>
</template>
</dd>
<dt>Leere Türchen</dt>
<dd v-if="admin_config_model.puzzle.skip_empty">Überspringen</dd>
<dd v-else>Anzeigen</dd>
</dl> </dl>
</div> </div>
</div> </div>
@ -200,6 +217,8 @@ export default class extends Vue {
last: "2023-12-24", last: "2023-12-24",
end: "2024-04-01", end: "2024-04-01",
seed: "", seed: "",
extra_days: [],
skip_empty: true,
}, },
calendar: { calendar: {
config_file: "lorem ipsum", config_file: "lorem ipsum",
@ -229,17 +248,20 @@ export default class extends Vue {
public fmt_puzzle_date(name: keyof AdminConfigModel["puzzle"]): string { public fmt_puzzle_date(name: keyof AdminConfigModel["puzzle"]): string {
const iso_date = this.admin_config_model.puzzle[name]; const iso_date = this.admin_config_model.puzzle[name];
if (iso_date === null) return "-"; if (!(typeof iso_date == "string")) return "-";
return DateTime.fromISO(iso_date).toLocaleString(DateTime.DATE_SHORT); return DateTime.fromISO(iso_date).toLocaleString(DateTime.DATE_SHORT);
} }
public on_open(ready: () => void, fail: () => void): void { public on_open(ready: () => void, fail: () => void): void {
Promise.all([ Promise.all([
this.store.update(),
this.$advent22.api_get<AdminConfigModel>("admin/config_model"), this.$advent22.api_get<AdminConfigModel>("admin/config_model"),
this.$advent22.api_get<DoorSaved[]>("admin/doors"), this.$advent22.api_get<DoorSaved[]>("admin/doors"),
]) ])
.then(([admin_config_model, doors]) => { .then(([store_update, admin_config_model, doors]) => {
store_update; // discard value
this.admin_config_model = admin_config_model; this.admin_config_model = admin_config_model;
this.doors = doors; this.doors = doors;

View file

@ -4,7 +4,12 @@
:visible="store.is_touch_device || force_visible" :visible="store.is_touch_device || force_visible"
:rectangle="door.position" :rectangle="door.position"
> >
<div class="has-text-danger">{{ door.day }}</div> <div
class="has-text-danger"
style="text-shadow: 0 0 10px white, 0 0 20px white"
>
{{ door.day }}
</div>
</SVGRect> </SVGRect>
</template> </template>

View file

@ -8,7 +8,7 @@
> >
<div <div
xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml"
:class="`px-4 is-flex is-align-items-center is-justify-content-center is-size-1 has-text-weight-bold ${extra_classes}`" :class="`px-2 is-flex is-align-items-center is-justify-content-center is-size-2 has-text-weight-bold ${extra_classes}`"
style="height: inherit" style="height: inherit"
:title="title" :title="title"
> >
@ -72,7 +72,7 @@ foreignObject > div {
&.visible, &.visible,
&:hover { &:hover {
border-width: 3px; border-width: 2px;
border-style: solid; border-style: solid;
@each $name, $color in $advent22-colors { @each $name, $color in $advent22-colors {

View file

@ -12,6 +12,8 @@ export interface AdminConfigModel {
last: string; last: string;
end: string; end: string;
seed: string; seed: string;
extra_days: number[];
skip_empty: boolean;
}; };
calendar: { calendar: {
config_file: string; config_file: string;

View file

@ -60,53 +60,72 @@ export const advent22Store = defineStore({
actions: { actions: {
init(): void { init(): void {
this.update_is_admin(); this.update()
.then(() => {
this.advent22
.api_get_blob("user/favicon")
.then((favicon_src) => {
const link: HTMLLinkElement =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
link.rel = "shortcut icon";
link.type = "image/x-icon";
link.href = favicon_src;
if (link.parentElement === null)
document.getElementsByTagName("head")[0].appendChild(link);
})
.catch(() => {});
Promise.all([
this.advent22.api_get<SiteConfigModel>("user/site_config"),
this.advent22.api_get_blob("user/background_image"),
this.advent22.api_get<DoorSaved[]>("user/doors"),
this.advent22.api_get<number | null>("user/next_door"),
])
.then(([site_config, background_image, user_doors, next_door]) => {
document.title = site_config.title;
if (site_config.subtitle !== "")
document.title += " " + site_config.subtitle;
this.site_config = site_config;
this.calendar_background_image = background_image;
this.user_doors.length = 0;
for (const door_saved of user_doors) {
this.user_doors.push(Door.load(door_saved));
}
if (next_door !== null)
this.next_door_target = Date.now() + next_door;
this.is_initialized = true; this.is_initialized = true;
for (const callback of this.on_initialized) callback(); for (const callback of this.on_initialized) callback();
}) })
.catch(this.advent22.alert_user_error); .catch(this.advent22.alert_user_error);
}, },
update(): Promise<void> {
return new Promise((resolve, reject) => {
this.advent22
.api_get_blob("user/favicon")
.then((favicon_src) => {
const link: HTMLLinkElement =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
link.rel = "shortcut icon";
link.type = "image/x-icon";
link.href = favicon_src;
if (link.parentElement === null)
document.getElementsByTagName("head")[0].appendChild(link);
})
.catch(() => {});
Promise.all([
this.update_is_admin(),
this.advent22.api_get<SiteConfigModel>("user/site_config"),
this.advent22.api_get_blob("user/background_image"),
this.advent22.api_get<DoorSaved[]>("user/doors"),
this.advent22.api_get<number | null>("user/next_door"),
])
.then(
([
is_admin,
site_config,
background_image,
user_doors,
next_door,
]) => {
is_admin; // discard value
document.title = site_config.title;
if (site_config.subtitle !== "")
document.title += " " + site_config.subtitle;
this.site_config = site_config;
this.calendar_background_image = background_image;
this.user_doors.length = 0;
for (const door_saved of user_doors) {
this.user_doors.push(Door.load(door_saved));
}
if (next_door !== null)
this.next_door_target = Date.now() + next_door;
resolve();
},
)
.catch(reject);
});
},
when_initialized(callback: () => void): void { when_initialized(callback: () => void): void {
if (this.is_initialized) { if (this.is_initialized) {
callback(); callback();