Compare commits

..

7 commits

11 changed files with 134 additions and 70 deletions

View file

@ -1,7 +1,5 @@
# MUSS
- api: Config-Liste von Extra-Türchen (kein Buchstabe, nur manuelles Bild)
- api: Config-Option "Überspringe leere Türchen" (standard ja)
# KANN
@ -21,3 +19,5 @@
- `alert` durch bulma Komponente(n) ersetzen
- api: admin Login case sensitivity (username "admin" == "AdMiN")
- 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
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):
# Quadrat, Seitenlänge in px

View file

@ -1,5 +1,4 @@
import re
from collections import defaultdict
from dataclasses import dataclass
from datetime import date
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))
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(
cfg: Config = Depends(get_config),
days: list[int] = Depends(get_all_sorted_days),
@ -59,6 +41,11 @@ async def get_all_parts(
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)
num_days = len(days)
@ -72,15 +59,33 @@ async def get_all_parts(
*rnd.sample(days, solution_length % num_days),
]
result: defaultdict[int, str] = defaultdict(str)
for day, letter in zip(solution_days, cfg.solution.clean):
result[day] += letter
result |= {missed_day: "" for missed_day in set(days) - set(result.keys())}
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(
days: list[int] = Depends(get_all_sorted_days),
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),
) -> list[int]:
"""
Anzahl der user-sichtbaren Türchen
User-sichtbare Türchen
"""
today = date.today()

View file

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

View file

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

View file

@ -68,6 +68,23 @@
<dd class="is-family-monospace">
"{{ admin_config_model.puzzle.seed }}"
</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>
</div>
</div>
@ -200,6 +217,8 @@ export default class extends Vue {
last: "2023-12-24",
end: "2024-04-01",
seed: "",
extra_days: [],
skip_empty: true,
},
calendar: {
config_file: "lorem ipsum",
@ -229,17 +248,20 @@ export default class extends Vue {
public fmt_puzzle_date(name: keyof AdminConfigModel["puzzle"]): string {
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);
}
public on_open(ready: () => void, fail: () => void): void {
Promise.all([
this.store.update(),
this.$advent22.api_get<AdminConfigModel>("admin/config_model"),
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.doors = doors;

View file

@ -4,7 +4,12 @@
:visible="store.is_touch_device || force_visible"
: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>
</template>

View file

@ -8,7 +8,7 @@
>
<div
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"
:title="title"
>
@ -72,7 +72,7 @@ foreignObject > div {
&.visible,
&:hover {
border-width: 3px;
border-width: 2px;
border-style: solid;
@each $name, $color in $advent22-colors {

View file

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

View file

@ -60,53 +60,72 @@ export const advent22Store = defineStore({
actions: {
init(): void {
this.update_is_admin();
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.update()
.then(() => {
this.is_initialized = true;
for (const callback of this.on_initialized) callback();
})
.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 {
if (this.is_initialized) {
callback();