Merge branch 'feature/days-rework' into develop

This commit is contained in:
Jörn-Michael Miehe 2023-09-12 16:57:06 +00:00
commit 3ccd3da28a
21 changed files with 394 additions and 332 deletions

View file

@ -5,76 +5,95 @@ from fastapi import Depends
from PIL import Image, ImageFont from PIL import Image, ImageFont
from .advent_image import _XY, AdventImage from .advent_image import _XY, AdventImage
from .calendar_config import CalendarConfig, get_calendar_config
from .config import Config, get_config from .config import Config, get_config
from .image_helpers import list_images_auto, load_image from .helpers import Random, list_images_auto, load_image, set_len
from .sequence_helpers import Random, set_len, shuffle
from .webdav import WebDAV from .webdav import WebDAV
async def shuffle_solution( async def get_days(
cal_cfg: CalendarConfig = Depends(get_calendar_config),
) -> list[int]:
"""
Alle Tage, für die es ein Türchen gibt
"""
return sorted(set(door.day for door in cal_cfg.doors))
async def get_parts(
cfg: Config = Depends(get_config), cfg: Config = Depends(get_config),
) -> str: days: list[int] = Depends(get_days),
) -> dict[int, str]:
""" """
Lösung: Reihenfolge zufällig bestimmen Lösung auf vorhandene Tage aufteilen
""" """
return "".join(await shuffle(cfg.puzzle.solution)) solution_length = len(cfg.puzzle.solution)
num_days = len(days)
rnd = await Random.get()
solution_days = [
*rnd.shuffled(days * (solution_length // num_days)),
*rnd.sample(days, solution_length % num_days),
]
result: dict[int, str] = {}
for day, letter in zip(solution_days, cfg.puzzle.solution):
result[day] = result.get(day, "")
result[day] += letter
return result
async def shuffle_images_auto( async def get_day_part(
images: list[str] = Depends(list_images_auto),
) -> list[str]:
"""
Bilder: Reihenfolge zufällig bestimmen
"""
ls = set_len(images, 24)
return await shuffle(ls)
async def get_part(
day: int, day: int,
shuffled_solution: str = Depends(shuffle_solution), parts: dict[int, str] = Depends(get_parts),
) -> str: ) -> str:
""" """
Heute angezeigter Teil der Lösung Heute angezeigter Teil der Lösung
""" """
return shuffled_solution[day] return parts[day]
async def get_random( async def get_auto_image_names(
day: int, days: list[int] = Depends(get_days),
) -> Random: images: list[str] = Depends(list_images_auto),
) -> dict[int, str]:
""" """
Tagesabhängige Zufallszahlen Bilder: Reihenfolge zufällig bestimmen
""" """
return await Random.get(day) rnd = await Random.get()
ls = set_len(images, len(days))
return dict(zip(days, rnd.shuffled(ls)))
async def gen_auto_image( async def gen_day_auto_image(
day: int, day: int,
auto_images: list[str] = Depends(shuffle_images_auto),
cfg: Config = Depends(get_config), cfg: Config = Depends(get_config),
rnd: Random = Depends(get_random), auto_image_names: list[str] = Depends(get_auto_image_names),
part: str = Depends(get_part), day_part: str = Depends(get_day_part),
) -> Image.Image: ) -> Image.Image:
""" """
Automatisch generiertes Bild erstellen Automatisch generiertes Bild erstellen
""" """
# Datei existiert garantiert! # Datei existiert garantiert!
img = await load_image(auto_images[day]) img = await load_image(auto_image_names[day])
image = await AdventImage.from_img(img) image = await AdventImage.from_img(img)
rnd = await Random.get(day)
font = ImageFont.truetype( font = ImageFont.truetype(
font=BytesIO(await WebDAV.read_bytes(f"files/{cfg.server.font}")), font=BytesIO(await WebDAV.read_bytes(f"files/{cfg.server.font}")),
size=50, size=50,
) )
# Buchstaben verstecken # Buchstaben verstecken
for letter in part: for letter in day_part:
await image.hide_text( await image.hide_text(
xy=cast(_XY, tuple(rnd.choices(range(30, 470), k=2))), xy=cast(_XY, tuple(rnd.choices(range(30, 470), k=2))),
text=letter, text=letter,
@ -84,12 +103,11 @@ async def gen_auto_image(
return image.img return image.img
async def get_image( async def get_day_image(
day: int, day: int,
auto_images: list[str] = Depends(shuffle_images_auto),
cfg: Config = Depends(get_config), cfg: Config = Depends(get_config),
rnd: Random = Depends(get_random), auto_image_names: list[str] = Depends(get_auto_image_names),
part: str = Depends(get_part), day_part: str = Depends(get_day_part),
) -> Image.Image: ) -> Image.Image:
""" """
Bild für einen Tag abrufen Bild für einen Tag abrufen
@ -105,6 +123,6 @@ async def get_image(
except RuntimeError: except RuntimeError:
# Erstelle automatisch generiertes Bild # Erstelle automatisch generiertes Bild
return await gen_auto_image( return await gen_day_auto_image(
day=day, auto_images=auto_images, cfg=cfg, rnd=rnd, part=part day=day, cfg=cfg, auto_image_names=auto_image_names, day_part=day_part
) )

View file

@ -1,11 +1,35 @@
import itertools
import random
import re import re
from io import BytesIO from io import BytesIO
from typing import Any, Self, Sequence, TypeVar
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from PIL import Image from PIL import Image
from .config import get_config
from .webdav import WebDAV from .webdav import WebDAV
T = TypeVar("T")
class Random(random.Random):
@classmethod
async def get(cls, bonus_salt: Any = "") -> Self:
cfg = await get_config()
return cls(f"{cfg.puzzle.solution}{cfg.puzzle.random_seed}{bonus_salt}")
def shuffled(self, population: Sequence[T]) -> Sequence[T]:
return self.sample(population, k=len(population))
def set_len(seq: Sequence[T], len: int) -> Sequence[T]:
# `seq` unendlich wiederholen
infinite = itertools.cycle(seq)
# Die ersten `length` einträge nehmen
return list(itertools.islice(infinite, len))
async def list_images_auto() -> list[str]: async def list_images_auto() -> list[str]:
""" """

View file

@ -1,28 +0,0 @@
import itertools
import random
from typing import Any, Self, Sequence
from .config import get_config
class Random(random.Random):
@classmethod
async def get(cls, bonus_salt: Any = "") -> Self:
cfg = await get_config()
return cls(f"{cfg.puzzle.solution}{cfg.puzzle.random_seed}{bonus_salt}")
async def shuffle(seq: Sequence, rnd: random.Random | None = None) -> list:
# Zufallsgenerator
rnd = rnd or await Random.get()
# Elemente mischen
return rnd.sample(seq, len(seq))
def set_len(seq: Sequence, length: int) -> list:
# `seq` unendlich wiederholen
infinite = itertools.cycle(seq)
# Die ersten `length` einträge nehmen
return list(itertools.islice(infinite, length))

View file

@ -1,10 +1,8 @@
from fastapi import APIRouter from fastapi import APIRouter
from . import admin, days, general, user from . import admin, images
router = APIRouter(prefix="/api") router = APIRouter(prefix="/api")
router.include_router(admin.router) router.include_router(admin.router)
router.include_router(days.router) router.include_router(images.router)
router.include_router(general.router)
router.include_router(user.router)

View file

@ -50,14 +50,14 @@ async def user_visible_doors() -> int:
return 0 return 0
async def user_can_view_door( async def user_can_view_day(
day: int, day: int,
) -> bool: ) -> bool:
""" """
True iff das Türchen von Tag `day` user-sichtbar ist True iff das Türchen von Tag `day` user-sichtbar ist
""" """
if day < 0: if day < 1:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY) raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY)
return day < await user_visible_doors() return day <= await user_visible_doors()

View file

@ -3,9 +3,9 @@ from datetime import date
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import BaseModel from pydantic import BaseModel
from ..core.calendar_config import CalendarConfig, get_calendar_config from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config
from ..core.config import Config, get_config from ..core.config import Config, get_config
from ..core.depends import shuffle_solution from ..core.depends import get_parts
from ..core.settings import SETTINGS from ..core.settings import SETTINGS
from ._security import require_admin, user_is_admin from ._security import require_admin, user_is_admin
@ -22,7 +22,6 @@ async def is_admin(
class ConfigModel(BaseModel): class ConfigModel(BaseModel):
class __Puzzle(BaseModel): class __Puzzle(BaseModel):
solution: str solution: str
shuffled: str
begin: date begin: date
end: date end: date
closing: date closing: date
@ -57,13 +56,12 @@ async def get_config_model(
_: None = Depends(require_admin), _: None = Depends(require_admin),
cfg: Config = Depends(get_config), cfg: Config = Depends(get_config),
cal_cfg: CalendarConfig = Depends(get_calendar_config), cal_cfg: CalendarConfig = Depends(get_calendar_config),
shuffled_solution: str = Depends(shuffle_solution), parts: dict[int, str] = Depends(get_parts),
) -> ConfigModel: ) -> ConfigModel:
return ConfigModel.model_validate( return ConfigModel.model_validate(
{ {
"puzzle": { "puzzle": {
"solution": cfg.puzzle.solution, "solution": cfg.puzzle.solution,
"shuffled": shuffled_solution,
"begin": date.today(), # TODO "begin": date.today(), # TODO
"end": date.today(), # TODO "end": date.today(), # TODO
"closing": date.today(), # TODO "closing": date.today(), # TODO
@ -85,3 +83,49 @@ async def get_config_model(
}, },
} }
) )
class DayPartModel(BaseModel):
day: int
part: str
@router.get("/day_parts")
async def get_day_parts(
_: None = Depends(require_admin),
parts: dict[int, str] = Depends(get_parts),
) -> list[DayPartModel]:
return [
DayPartModel.model_validate({"day": day, "part": part})
for day, part in sorted(parts.items())
]
@router.get("/doors")
async def get_doors(
_: None = Depends(require_admin),
cal_cfg: CalendarConfig = Depends(get_calendar_config),
) -> DoorsSaved:
"""
Türchen lesen
"""
return cal_cfg.doors
@router.put("/doors")
async def put_doors(
doors: DoorsSaved,
_: None = Depends(require_admin),
cfg: Config = Depends(get_config),
cal_cfg: CalendarConfig = Depends(get_calendar_config),
) -> None:
"""
Türchen ändern
"""
cal_cfg.doors = sorted(
doors,
key=lambda door: door.day,
)
await cal_cfg.change(cfg)

View file

@ -1,71 +0,0 @@
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import StreamingResponse
from PIL import Image
from ..core.config import get_config
from ..core.depends import get_image, get_part, shuffle_solution
from ..core.image_helpers import api_return_image
from ._security import user_can_view_door, user_is_admin, user_visible_doors
router = APIRouter(prefix="/days", tags=["days"])
@router.on_event("startup")
async def startup() -> None:
cfg = await get_config()
print(cfg.puzzle.solution)
shuffled_solution = await shuffle_solution(cfg)
print(shuffled_solution)
@router.get("/date")
async def get_date() -> str:
"""
Aktuelles Server-Datum
"""
return date.today().isoformat()
@router.get("/visible_days")
async def get_visible_days(
visible_doors: int = Depends(user_visible_doors),
) -> int:
"""
Sichtbare Türchen
"""
return visible_doors
@router.get("/part/{day}")
async def get_part_for_day(
part: str = Depends(get_part),
) -> str:
"""
Heutiger Lösungsteil
"""
return part
@router.get(
"/image/{day}",
response_class=StreamingResponse,
)
async def get_image_for_day(
image: Image.Image = Depends(get_image),
can_view: bool = Depends(user_can_view_door),
is_admin: bool = Depends(user_is_admin),
) -> StreamingResponse:
"""
Bild für einen Tag erstellen
"""
if not (can_view or is_admin):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Wie unhöflich!!!")
return await api_return_image(image)

View file

@ -1,50 +0,0 @@
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config
from ..core.config import Config, get_config
from ..core.image_helpers import api_return_image, load_image
router = APIRouter(prefix="/general", tags=["general"])
@router.get(
"/background",
response_class=StreamingResponse,
)
async def get_image_for_day(
cal_cfg: CalendarConfig = Depends(get_calendar_config),
) -> StreamingResponse:
"""
Hintergrundbild laden
"""
return await api_return_image(await load_image(f"files/{cal_cfg.background}"))
@router.get("/doors")
async def get_doors(
cal_cfg: CalendarConfig = Depends(get_calendar_config),
) -> DoorsSaved:
"""
Türchen lesen
"""
return cal_cfg.doors
@router.put("/doors")
async def put_doors(
doors: DoorsSaved,
cfg: Config = Depends(get_config),
cal_cfg: CalendarConfig = Depends(get_calendar_config),
) -> None:
"""
Türchen setzen
"""
cal_cfg.doors = sorted(
doors,
key=lambda door: door.day,
)
await cal_cfg.change(cfg)

View file

@ -0,0 +1,50 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import StreamingResponse
from PIL import Image
from ..core.calendar_config import CalendarConfig, get_calendar_config
from ..core.config import get_config
from ..core.depends import get_day_image
from ..core.helpers import api_return_image, load_image
from ._security import user_can_view_day, user_is_admin
router = APIRouter(prefix="/images", tags=["images"])
@router.on_event("startup")
async def startup() -> None:
cfg = await get_config()
print(cfg.puzzle.solution)
@router.get(
"/background",
response_class=StreamingResponse,
)
async def get_background(
cal_cfg: CalendarConfig = Depends(get_calendar_config),
) -> StreamingResponse:
"""
Hintergrundbild laden
"""
return await api_return_image(await load_image(f"files/{cal_cfg.background}"))
@router.get(
"/{day}",
response_class=StreamingResponse,
)
async def get_image_for_day(
image: Image.Image = Depends(get_day_image),
can_view: bool = Depends(user_can_view_day),
is_admin: bool = Depends(user_is_admin),
) -> StreamingResponse:
"""
Bild für einen Tag erstellen
"""
if not (can_view or is_admin):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Wie unhöflich!!!")
return await api_return_image(image)

View file

@ -1,12 +0,0 @@
from fastapi import APIRouter, Depends
from ._security import require_admin
router = APIRouter(prefix="/user", tags=["user"])
@router.get("/admin")
def check_admin(
_: None = Depends(require_admin),
) -> bool:
return True

View file

@ -3,7 +3,7 @@
<figure> <figure>
<div class="image is-unselectable"> <div class="image is-unselectable">
<img :src="$advent22.api_url('general/background')" /> <img :src="$advent22.api_url('images/background')" />
<ThouCanvas> <ThouCanvas>
<CalendarDoor <CalendarDoor
v-for="(door, index) in doors" v-for="(door, index) in doors"
@ -50,8 +50,8 @@ export default class extends Vue {
multi_modal: MultiModal; multi_modal: MultiModal;
}; };
public door_hover(index: number) { public door_hover(day: number) {
this.figure_caption = `Türchen ${index + 1}`; this.figure_caption = `Türchen ${day}`;
} }
public door_unhover() { public door_unhover() {

View file

@ -1,37 +1,39 @@
<template> <template>
<MultiModal ref="multi_modal" /> <MultiModal ref="multi_modal" />
<BulmaDrawer header="Kalender-Assistent"> <BulmaDrawer
header="Kalender-Assistent"
:ready="is_loaded"
@open="on_open"
refreshable
>
<div class="card-content"> <div class="card-content">
<div class="content"> <div class="content">
<h4>Alle Türchen</h4> <h4>Alle Türchen</h4>
<div class="tags are-medium"> <div class="tags are-medium">
<span <BulmaButton
v-for="index in 24" v-for="(day_part, index) in day_parts"
:key="index" :key="`btn-${index}`"
class="tag button is-primary" class="tag button is-primary"
@click.left="door_click(index - 1)" icon="fa-solid fa-door-open"
> :text="`${day_part.day}`"
<span class="icon"> @click.left="door_click(day_part.day)"
<font-awesome-icon icon="fa-solid fa-door-open" /> />
</span>
<span>{{ index }}</span>
</span>
</div> </div>
<h4>Buchstaben-Zuordnung</h4> <h4>Buchstaben-Zuordnung</h4>
<div class="tags are-medium"> <div class="tags are-medium">
<span class="tag is-info">1: A</span> <span
<span class="tag is-info">2: G</span> v-for="(day_part, index) in day_parts"
<span class="tag is-info">3: F</span> :key="`part-${index}`"
<span class="tag is-info">4: C</span> class="tag is-info"
<span class="tag is-info">5: I</span> >
<span class="tag is-info">6: N</span> {{ day_part.day }}: {{ day_part.part.split("").join(", ") }}
<span class="tag is-info">7: B</span> </span>
<span class="tag is-info"></span>
</div> </div>
<h4>Bilder-Zuordnung</h4> <h4>Bilder-Zuordnung</h4>
<!-- TODO -->
<div class="tags are-medium"> <div class="tags are-medium">
<span class="tag is-primary">1: images_auto/1.jpg</span> <span class="tag is-primary">1: images_auto/1.jpg</span>
<span class="tag is-primary">2: images_manual/1.jpg</span> <span class="tag is-primary">2: images_manual/1.jpg</span>
@ -48,6 +50,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { DayPartModel } from "@/lib/api";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import BulmaButton from "./bulma/Button.vue"; import BulmaButton from "./bulma/Button.vue";
@ -62,15 +65,29 @@ import MultiModal from "./calendar/MultiModal.vue";
}, },
}) })
export default class extends Vue { export default class extends Vue {
public is_loaded = true;
public day_parts: DayPartModel[] = [];
declare $refs: { declare $refs: {
multi_modal: MultiModal; multi_modal: MultiModal;
}; };
public on_open(): void {
this.is_loaded = false;
Promise.all([this.$advent22.api_get<DayPartModel[]>("admin/day_parts")])
.then(([day_parts]) => {
this.day_parts = day_parts;
this.is_loaded = true;
})
.catch(console.log);
}
public door_click(day: number) { public door_click(day: number) {
this.$refs.multi_modal.show_progress(); this.$refs.multi_modal.show_progress();
this.$advent22 this.$advent22
.api_get_blob(`days/image/${day}`) .api_get_blob(`images/${day}`)
.then((data) => this.$refs.multi_modal.show_image(data)) .then((data) => this.$refs.multi_modal.show_image(data))
.catch(() => this.$refs.multi_modal.set_active(false)); .catch(() => this.$refs.multi_modal.set_active(false));
} }

View file

@ -1,5 +1,10 @@
<template> <template>
<BulmaDrawer header="Konfiguration"> <BulmaDrawer
header="Konfiguration"
:ready="is_loaded"
@open="on_open"
refreshable
>
<div class="card-content"> <div class="card-content">
<div class="columns"> <div class="columns">
<div class="column is-one-third"> <div class="column is-one-third">
@ -7,32 +12,46 @@
<h4>Rätsel</h4> <h4>Rätsel</h4>
<dl> <dl>
<dt>Titel</dt> <dt>Titel</dt>
<!-- TODO -->
<dd>Adventskalender 2023</dd> <dd>Adventskalender 2023</dd>
<dt>Lösung</dt> <dt>Lösung</dt>
<dd>{{ admin_config_model.puzzle.solution }}</dd> <dd>{{ config_model.puzzle.solution }}</dd>
<dt>Reihenfolge</dt> <dt>Reihenfolge</dt>
<dd>{{ admin_config_model.puzzle.shuffled }}</dd> <dd>
<template
v-for="(day_part, index) in day_parts"
:key="`part-${index}`"
>
<span>
<template v-if="index > 0"> &ndash; </template>
{{ day_part.day }}:
</span>
<span class="is-family-monospace">{{ day_part.part }}</span>
</template>
</dd>
<dt>Offene Türchen</dt> <dt>Offene Türchen</dt>
<!-- TODO -->
<dd>10</dd> <dd>10</dd>
<dt>Nächstes Türchen in</dt> <dt>Nächstes Türchen in</dt>
<!-- TODO -->
<dd>dd-hh-mm-ss</dd> <dd>dd-hh-mm-ss</dd>
<dt>Erstes Türchen</dt> <dt>Erstes Türchen</dt>
<dd>{{ admin_config_model.puzzle.begin }}</dd> <dd>{{ config_model.puzzle.begin }}</dd>
<dt>Letztes Türchen</dt> <dt>Letztes Türchen</dt>
<dd>{{ admin_config_model.puzzle.end }}</dd> <dd>{{ config_model.puzzle.end }}</dd>
<dt>Rätsel schließt</dt> <dt>Rätsel schließt</dt>
<dd>{{ admin_config_model.puzzle.closing }}</dd> <dd>{{ config_model.puzzle.closing }}</dd>
<dt>Zufalls-Seed</dt> <dt>Zufalls-Seed</dt>
<dd class="is-family-monospace"> <dd class="is-family-monospace">
"{{ admin_config_model.puzzle.seed }}" "{{ config_model.puzzle.seed }}"
</dd> </dd>
</dl> </dl>
</div> </div>
@ -42,35 +61,37 @@
<h4>Kalender</h4> <h4>Kalender</h4>
<dl> <dl>
<dt>Definition</dt> <dt>Definition</dt>
<dd>{{ admin_config_model.calendar.config_file }}</dd> <dd>{{ config_model.calendar.config_file }}</dd>
<dt>Hintergrundbild</dt> <dt>Hintergrundbild</dt>
<dd>{{ admin_config_model.calendar.background }}</dd> <dd>{{ config_model.calendar.background }}</dd>
<dt>Türchen</dt> <dt>Türchen</dt>
<dd> <dd>
<!-- <span>{{ admin_config_model.calendar.doors.join(", ") }}</span> --> <template
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 v-for="(day_part, index) in day_parts"
<span class="tag is-danger ml-2"> :key="`door-${index}`"
<span class="icon"> >
<font-awesome-icon icon="fa-solid fa-bolt" /> <span>
<template v-if="index > 0">, </template>
{{ day_part.day }}
</span> </span>
</span> </template>
</dd> </dd>
</dl> </dl>
<h4>Bilder</h4> <h4>Bilder</h4>
<dl> <dl>
<dt>Größe</dt> <dt>Größe</dt>
<dd>{{ admin_config_model.image.size }} px</dd> <dd>{{ config_model.image.size }} px</dd>
<dt>Rand</dt> <dt>Rand</dt>
<dd>{{ admin_config_model.image.border }} px</dd> <dd>{{ config_model.image.border }} px</dd>
<dt>Schriftarten</dt> <dt>Schriftarten</dt>
<dd <dd
v-for="(font, idx) in admin_config_model.image.fonts" v-for="(font, index) in config_model.image.fonts"
:key="`font-${idx}`" :key="`font-${index}`"
> >
{{ font.file }} (Größe {{ font.size }}) {{ font.file }} (Größe {{ font.size }})
</dd> </dd>
@ -82,32 +103,30 @@
<h4>WebDAV</h4> <h4>WebDAV</h4>
<dl> <dl>
<dt>URL</dt> <dt>URL</dt>
<dd>{{ admin_config_model.webdav.url }}</dd> <dd>{{ config_model.webdav.url }}</dd>
<dt>Zugangsdaten</dt> <dt>Zugangsdaten</dt>
<!-- TODO -->
<dd> <dd>
<span>***</span> <span>***</span>
<span class="tag button is-primary ml-2"> <button class="tag button icon is-primary ml-2">
<span class="icon"> <font-awesome-icon icon="fa-solid fa-eye" />
<font-awesome-icon icon="fa-solid fa-eye" /> </button>
</span>
</span>
</dd> </dd>
<dt>Cache-Dauer</dt> <dt>Cache-Dauer</dt>
<dd>{{ admin_config_model.webdav.cache_ttl }} s</dd> <dd>{{ config_model.webdav.cache_ttl }} s</dd>
<dt>Konfigurationsdatei</dt> <dt>Konfigurationsdatei</dt>
<dd>{{ admin_config_model.webdav.config_file }}</dd> <dd>{{ config_model.webdav.config_file }}</dd>
<dt>UI-Admin</dt> <dt>UI-Admin</dt>
<!-- TODO -->
<dd> <dd>
<span>***</span> <span>***</span>
<span class="tag button is-primary ml-2"> <button class="tag button icon is-primary ml-2">
<span class="icon"> <font-awesome-icon icon="fa-solid fa-eye" />
<font-awesome-icon icon="fa-solid fa-eye" /> </button>
</span>
</span>
</dd> </dd>
</dl> </dl>
</div> </div>
@ -118,45 +137,21 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ConfigModel, DayPartModel } from "@/lib/api";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import BulmaDrawer from "./bulma/Drawer.vue"; import BulmaDrawer from "./bulma/Drawer.vue";
interface ConfigModel {
puzzle: {
solution: string;
shuffled: string;
begin: string;
end: string;
closing: string;
seed: string;
};
calendar: {
config_file: string;
background: string;
};
image: {
size: number;
border: number;
fonts: { file: string; size: number }[];
};
webdav: {
url: string;
cache_ttl: number;
config_file: string;
};
}
@Options({ @Options({
components: { components: {
BulmaDrawer, BulmaDrawer,
}, },
}) })
export default class extends Vue { export default class extends Vue {
public admin_config_model: ConfigModel = { public is_loaded = false;
public config_model: ConfigModel = {
puzzle: { puzzle: {
solution: "ABCDEFGHIJKLMNOPQRSTUVWX", solution: "ABCDEFGHIJKLMNOPQRSTUVWX",
shuffled: "AGFCINBEWLKQMXDURPOSJVHT",
begin: "01.12.2023", begin: "01.12.2023",
end: "24.12.2023", end: "24.12.2023",
closing: "01.04.2024", closing: "01.04.2024",
@ -177,11 +172,20 @@ export default class extends Vue {
config_file: "config.toml", config_file: "config.toml",
}, },
}; };
public day_parts: DayPartModel[] = [];
public mounted(): void { public on_open(): void {
this.$advent22 this.is_loaded = false;
.api_get<ConfigModel>("admin/config_model")
.then((data) => (this.admin_config_model = data)) Promise.all([
this.$advent22.api_get<ConfigModel>("admin/config_model"),
this.$advent22.api_get<DayPartModel[]>("admin/day_parts"),
])
.then(([config_model, day_parts]) => {
this.config_model = config_model;
this.day_parts = day_parts;
this.is_loaded = true;
})
.catch(console.log); .catch(console.log);
} }
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<BulmaDrawer header="Türchen bearbeiten"> <BulmaDrawer header="Türchen bearbeiten" :ready="is_loaded" @open="on_open">
<div class="is-flex is-align-items-center is-justify-content-space-between"> <div class="is-flex is-align-items-center is-justify-content-space-between">
<BulmaButton <BulmaButton
:disabled="current_step === 0" :disabled="current_step === 0"
@ -46,7 +46,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Door, DoorsSaved } from "@/lib/door"; import { DoorsSaved } from "@/lib/api";
import { Door } from "@/lib/door";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import Calendar from "./Calendar.vue"; import Calendar from "./Calendar.vue";
@ -67,6 +68,8 @@ import DoorPlacer from "./editor/DoorPlacer.vue";
}, },
}) })
export default class extends Vue { export default class extends Vue {
public is_loaded = true;
public readonly steps: Step[] = [ public readonly steps: Step[] = [
{ label: "Platzieren", icon: "fa-solid fa-crosshairs" }, { label: "Platzieren", icon: "fa-solid fa-crosshairs" },
{ label: "Ordnen", icon: "fa-solid fa-list-ol" }, { label: "Ordnen", icon: "fa-solid fa-list-ol" },
@ -76,7 +79,7 @@ export default class extends Vue {
public doors: Door[] = []; public doors: Door[] = [];
private load_doors(): Promise<void | DoorsSaved> { private load_doors(): Promise<void | DoorsSaved> {
return this.$advent22.api_get<DoorsSaved>("general/doors").then((data) => { return this.$advent22.api_get<DoorsSaved>("admin/doors").then((data) => {
this.doors.length = 0; this.doors.length = 0;
for (const value of data) { for (const value of data) {
@ -89,14 +92,20 @@ export default class extends Vue {
const data: DoorsSaved = []; const data: DoorsSaved = [];
for (const door of this.doors) { for (const door of this.doors) {
if (door.day === -1) {
continue;
}
data.push(door.save()); data.push(door.save());
} }
return this.$advent22.api_put("general/doors", data); return this.$advent22.api_put("admin/doors", data);
}
public on_open(): void {
this.is_loaded = false;
this.load_doors()
.then(() => (this.is_loaded = true))
.catch(([reason, endpoint]) => {
alert(`Fehler: ${reason} in ${endpoint}`);
});
} }
public mounted(): void { public mounted(): void {

View file

@ -1,11 +1,13 @@
<template> <template>
<div class="card"> <div class="card">
<header <header class="card-header has-background-grey-lighter is-unselectable">
class="card-header has-background-grey-lighter is-unselectable" <p class="card-header-title" @click="toggle">{{ header }}</p>
@click="is_open = !is_open" <p v-if="refreshable" class="card-header-icon px-0">
> <button class="tag button icon is-info" @click="refresh">
<p class="card-header-title">{{ header }}</p> <font-awesome-icon icon="fa-solid fa-arrows-rotate" />
<button class="card-header-icon"> </button>
</p>
<button class="card-header-icon" @click="toggle">
<span class="icon"> <span class="icon">
<font-awesome-icon <font-awesome-icon
:icon="'fa-solid fa-angle-' + (is_open ? 'down' : 'right')" :icon="'fa-solid fa-angle-' + (is_open ? 'down' : 'right')"
@ -14,7 +16,12 @@
</button> </button>
</header> </header>
<slot v-if="is_open" name="default" /> <template v-if="is_open">
<slot v-if="ready" name="default" />
<div v-else class="card-content">
<progress class="progress is-info" max="100" />
</div>
</template>
</div> </div>
</template> </template>
@ -28,11 +35,31 @@ import { Options, Vue } from "vue-class-component";
required: false, required: false,
default: "", default: "",
}, },
ready: Boolean,
refreshable: {
type: Boolean,
required: false,
default: false,
},
}, },
emits: ["open"],
}) })
export default class extends Vue { export default class extends Vue {
public is_open = false;
public header!: string; public header!: string;
public ready!: boolean;
public refreshable!: boolean;
public is_open = false;
public toggle() {
this.is_open = !this.is_open;
if (this.is_open) this.$emit("open");
}
public refresh() {
this.is_open = false;
this.toggle();
}
} }
</script> </script>

View file

@ -25,7 +25,7 @@ export default class extends Vue {
this.$emit("doorClick"); this.$emit("doorClick");
this.$advent22 this.$advent22
.api_get_blob(`days/image/${this.door.day}`) .api_get_blob(`images/${this.door.day}`)
.then((data) => this.$emit("doorSuccess", data)) .then((data) => this.$emit("doorSuccess", data))
.catch(([reason]) => { .catch(([reason]) => {
let msg = "Unbekannter Fehler, bitte wiederholen!"; let msg = "Unbekannter Fehler, bitte wiederholen!";

View file

@ -11,7 +11,7 @@
</ul> </ul>
</div> </div>
<figure class="image is-unselectable"> <figure class="image is-unselectable">
<img :src="$advent22.api_url('general/background')" /> <img :src="$advent22.api_url('images/background')" />
<ThouCanvas> <ThouCanvas>
<PreviewDoor <PreviewDoor
v-for="(_, index) in doors" v-for="(_, index) in doors"

View file

@ -9,7 +9,7 @@
</ul> </ul>
</div> </div>
<figure class="image is-unselectable"> <figure class="image is-unselectable">
<img :src="$advent22.api_url('general/background')" /> <img :src="$advent22.api_url('images/background')" />
<RectangleCanvas <RectangleCanvas
:rectangles="rectangles" :rectangles="rectangles"
@draw="on_draw" @draw="on_draw"

View file

@ -18,12 +18,12 @@
ref="day_input" ref="day_input"
class="input is-large" class="input is-large"
type="number" type="number"
min="-1" :min="MIN_DAY"
placeholder="Tag" placeholder="Tag"
@keydown="on_keydown" @keydown="on_keydown"
/> />
<div v-else class="is-size-1 has-text-weight-bold"> <div v-else-if="door.day > 0" class="is-size-1 has-text-weight-bold">
<template v-if="door.day >= 0">{{ door.day }}</template> {{ door.day }}
</div> </div>
</div> </div>
</foreignObject> </foreignObject>
@ -46,6 +46,7 @@ import SVGRect from "../calendar/SVGRect.vue";
}) })
export default class extends Vue { export default class extends Vue {
public door!: Door; public door!: Door;
public readonly MIN_DAY = Door.MIN_DAY;
public day_str = ""; public day_str = "";
public editing = false; public editing = false;

38
ui/src/lib/api.ts Normal file
View file

@ -0,0 +1,38 @@
export interface ConfigModel {
puzzle: {
solution: string;
begin: string;
end: string;
closing: string;
seed: string;
};
calendar: {
config_file: string;
background: string;
};
image: {
size: number;
border: number;
fonts: { file: string; size: number }[];
};
webdav: {
url: string;
cache_ttl: number;
config_file: string;
};
}
export interface DayPartModel {
day: number;
part: string;
}
export interface DoorSaved {
day: number;
x1: number;
y1: number;
x2: number;
y2: number;
}
export type DoorsSaved = DoorSaved[];

View file

@ -1,23 +1,16 @@
import { DoorSaved } from "./api";
import { Rectangle } from "./rectangle"; import { Rectangle } from "./rectangle";
import { Vector2D } from "./vector2d"; import { Vector2D } from "./vector2d";
export interface DoorSaved {
day: number;
x1: number;
y1: number;
x2: number;
y2: number;
}
export type DoorsSaved = DoorSaved[];
export class Door { export class Door {
private _day = -1; public static readonly MIN_DAY = 1;
private _day = Door.MIN_DAY;
public position: Rectangle; public position: Rectangle;
constructor(position: Rectangle); constructor(position: Rectangle);
constructor(position: Rectangle, day: number); constructor(position: Rectangle, day: number);
constructor(position: Rectangle, day = -1) { constructor(position: Rectangle, day = Door.MIN_DAY) {
this.day = day; this.day = day;
this.position = position; this.position = position;
} }
@ -31,9 +24,9 @@ export class Door {
const result = Number(day); const result = Number(day);
if (isNaN(result)) { if (isNaN(result)) {
this._day = -1; this._day = Door.MIN_DAY;
} else { } else {
this._day = Math.max(Math.floor(result), -1); this._day = Math.max(Math.floor(result), Door.MIN_DAY);
} }
} }