Compare commits

...

70 commits

Author SHA1 Message Date
fenris 207ea459ae [mod] overlay:scrollable 2025-10-30 08:32:33 +01:00
fenris 74e95dff54 [mod] focussing 2025-10-29 13:24:41 +01:00
fenris b0bd963145 [mod] Aufräum-Aktionen und kleinere Anpassungen 2025-10-29 11:40:27 +01:00
fenris 58fc2ea97f [res] 2025-10-28 11:44:26 +01:00
fenris 44c70a10bb [task-419]
Co-authored-by: Fenris Wolf <fenris@folksprak.org>
Co-committed-by: Fenris Wolf <fenris@folksprak.org>
2025-10-28 11:41:17 +01:00
fenris 63504f4e70 [mod] Tages-Beschriftung in Wochenansichts-Zellen 2025-10-26 21:46:58 +01:00
fenris fae611bec4 [mod] calendar default access level -> none 2025-10-24 18:55:10 +02:00
fenris b383460f3b [task-426] 2025-10-24 12:38:28 +02:00
fenris 9e24750d4d [mod] queueing ausgelagern 2025-10-24 00:23:53 +02:00
fenris c0abe0a180 [upd] plankton 2025-10-24 00:23:29 +02:00
fenris 2b11b57818 [task-409] 2025-10-23 23:16:11 +02:00
fenris 004b9f3211 [fix] logging:threshold 2025-10-23 21:58:51 +02:00
fenris ccb408b1b0 Merge pull request 'Gruppen-Steuerung' (#3) from task-416 into main
Reviewed-on: #3
2025-10-23 19:16:40 +02:00
fenris 745fdaafa4 [task-416] 2025-10-23 13:19:39 +02:00
fenris 3ca0822cff Merge branch 'main' into task-416 2025-10-23 11:39:20 +02:00
fenris 25b60d4f4f [mod] widget:weekview:senkrechte Ansicht nicht mehr als Standard 2025-10-23 11:39:07 +02:00
fenris f49ff8c519 [task-416] 2025-10-23 11:35:57 +02:00
fenris b3a25c42d7 Merge branch 'temp-20251021' 2025-10-22 00:42:55 +02:00
fenris cb8b48c818 [del] old style sheets 2025-10-22 00:42:34 +02:00
fenris f97c33de1e [int] 2025-10-22 00:41:22 +02:00
fenris 76e2e0560f [int] 2025-10-21 23:06:11 +02:00
fenris 25291b373a [mod] zahlreiche Anpassungen und Verbesserungen 2025-10-20 13:16:00 +02:00
fenris d8d15b03ac [add] Lösch-Bestätigung für Kalender und Termine 2025-10-20 13:14:56 +02:00
fenris 9f0f6808b2 [mod] model:sync_calendars:do not export 2025-10-20 13:13:46 +02:00
fenris 3ca3cabb9e [mod] init:localization 2025-10-20 13:13:12 +02:00
fenris 1d4ee08b06 Merge branch 'task-408' 2025-10-17 00:56:05 +02:00
fenris ede6cd6091 Revert "[task-408] Pages durch Widgets ablösen"
This reverts commit f51cf92907.
2025-10-17 00:56:02 +02:00
fenris b1c8bf5806 [task-408] [mod] Nutzername im Menü-Knopf anzeigen 2025-10-17 00:43:10 +02:00
fenris 1a36af56ae [mod] vieeeele Verbesserungen 2025-10-17 00:10:28 +02:00
fenris f51cf92907 [task-408] Pages durch Widgets ablösen
## Aufgaben

- [\#408](https://vikunja.ramsch.sx/tasks/408)

## Beschreibung

- mit Sync-Funktionen umgesetzt; fühlt sich alles sooo viel besser an

Reviewed-on: #2
Co-authored-by: Fenris Wolf <fenris@folksprak.org>
Co-committed-by: Fenris Wolf <fenris@folksprak.org>
2025-10-14 23:37:02 +02:00
fenris 1990c047c2 [task-408] [mod] loc 2025-10-14 23:34:06 +02:00
fenris c6f3d9fd57 [task-408] [mod] Kalender-Hinzufügen-Seite entfernt 2025-10-14 23:18:30 +02:00
fenris c6d868b243 [task-408] [mod] Quellen-Steuerelement umgestellt 2025-10-14 23:04:31 +02:00
fenris 4612128134 [task-408] [mod] Wochenansich-Steuerelement umgestellt 2025-10-14 22:10:56 +02:00
fenris 7e1f585f34 [upd] plankton 2025-10-14 22:09:57 +02:00
fenris c53837ebfe [upd] plankton 2025-10-14 22:09:49 +02:00
fenris 057b682bdc [task-408] [int] 2025-10-14 15:56:29 +02:00
fenris c97fd37040 [task-408] [int] 2025-10-14 00:16:22 +02:00
fenris dc6abdc2e0 [task-408] Widgets von Plankton 2025-10-13 13:29:19 +02:00
fenris 58e8688bf7 [task-396] 2025-10-13 13:22:53 +02:00
fenris 4766fd7f59 [upd] plankton 2025-10-13 13:22:46 +02:00
fenris 4f830b0790 [add] widget:mode_switcher [mod] page:overview 2025-10-02 17:00:17 +02:00
fenris a49a29da06 [mod] data:localization 2025-10-02 16:59:20 +02:00
fenris c8ce009358 [mod] conf-example 2025-10-02 16:59:00 +02:00
fenris c91c236e3d [upd] plankton 2025-10-02 16:58:44 +02:00
fenris 91a9715fbe [mod] readme 2025-09-26 11:52:32 +02:00
fenris 694d0992b6 [mod] tools:deploy 2025-09-26 11:52:11 +02:00
fenris 93d4c34c3e [fix] index 2025-09-25 17:59:55 +02:00
fenris d15357733a [mod] directory structure 2025-09-25 17:59:48 +02:00
fenris e1d0b17016 [mod] namespace 2025-09-25 17:54:20 +02:00
fenris 9f4227ffbb [mod] readme 2025-09-25 17:38:49 +02:00
fenris 5e932f63fc [task-192]
## Tasks

- [192](https://vikunja.ramsch.sx/tasks/192)

## Zugehörige MRs

- [datamodel](misc/zeitbild-datamodel#1)
- [backend](misc/zeitbild-backend#1)

Reviewed-on: misc/zeitbild-frontend-dali#1
Co-authored-by: Fenris Wolf <fenris@folksprak.org>
Co-committed-by: Fenris Wolf <fenris@folksprak.org>
2025-09-25 17:08:18 +02:00
fenris 3df888ced3 [mov] conf-example 2025-09-25 17:07:37 +02:00
fenris e01db2c3f5 [mod] tools:update-plankton [upd] plankton 2025-09-25 17:07:01 +02:00
fenris 530c0ce116 [mod] date formatting [mod] caldav resource integration 2024-11-01 13:35:49 +01:00
fenris 431f1a1847 [mod] todo 2024-11-01 13:35:43 +01:00
fenris f4811a43bf [upd] plankton 2024-11-01 13:35:06 +01:00
fenris 10e3862fc0 [mod] tools:default directories 2024-10-25 10:30:20 +02:00
fenris 77f43372c9 [mod] overview:hide/show calendars 2024-10-21 23:18:08 +02:00
fenris ad322f7550 [mod] config switch for central europe datetime inputs 2024-10-21 23:17:53 +02:00
fenris 5f6d72f67c [add] todo 2024-10-21 19:19:42 +02:00
fenris 348f6da19c [mod] style 2024-10-21 19:19:33 +02:00
fenris 6c92725d50 [upd] plankton 2024-10-21 19:19:11 +02:00
fenris 61835bc533 [fix] widget:weekview 2024-10-21 07:01:13 +02:00
fenris fda85b38f9 [mod] widget:weekview [mod] page:overview:responsive 2024-10-20 18:26:05 +02:00
fenris 18fcdd881e [mod] widget:listview:bessere Darstellung und Bedienung 2024-10-20 17:26:35 +02:00
fenris 34ec6125c5 [mod] link für events berücksichtigen [mod] Wochentage übersetzen 2024-10-14 19:07:35 +02:00
fenris 5736e1a5a2 [mod] loc:add keys 2024-10-14 19:07:03 +02:00
fenris 93d35f5cc0 Merge branch 'dev-publicity_calendars' 2024-10-12 12:06:38 +02:00
fenris e505103228 [add] license 2024-10-12 12:05:31 +02:00
96 changed files with 16022 additions and 6163 deletions

View file

@ -1,12 +0,0 @@
{
"version": 1,
"backend": {
"scheme": "http",
"host": "localhost",
"port": 7845,
"path": ""
},
"misc": {
"oidc_redirect_uri_template": "http://localhost:8888/#oidc_finish,session_key={{session_key}}"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

13
misc/conf-example.json Normal file
View file

@ -0,0 +1,13 @@
{
"version": 1,
"backend": {
"scheme": "http",
"host": "localhost",
"port": 7845,
"path": ""
},
"misc": {
"oidc_redirect_uri_template": "http://localhost:8888/?action=oidc_finish&session_key={{session_key}}",
"use_central_europe_specific_datetime_inputs": true
}
}

View file

@ -2,7 +2,7 @@
## Beschreibung ## Beschreibung
- Web-Frontend für [`zeitbild`](https://forgejo.linke.sx/zeitbild/backend) - Web-Frontend für [zeitbild](/zeitbild/meta)
## Erstellung ## Erstellung

148
source/base.ts Normal file
View file

@ -0,0 +1,148 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali
{
/**
*/
let _actions_login : Array<((name ?: (null | string)) => Promise<void>)> = [];
/**
*/
let _actions_logout : Array<(() => Promise<void>)> = [];
/**
*/
let _is_logged_in : boolean = false;
/**
*/
export function listen_login(
action : ((name ?: (null | string)) => Promise<void>)
)
: void
{
_actions_login.push(action);
}
/**
*/
export function listen_logout(
action : (() => Promise<void>)
)
: void
{
_actions_logout.push(action);
}
/**
*/
export async function notify_login(
name : (null | string)
)
: Promise<void>
{
_is_logged_in = true;
for (const action of _actions_login)
{
await action(name);
}
}
/**
*/
export async function notify_logout(
)
: Promise<void>
{
_is_logged_in = false;
for (const action of _actions_logout)
{
await action();
}
}
/**
*/
export function is_logged_in(
)
: boolean
{
return _is_logged_in;
}
/**
*/
export async function oidc_finish(
session_key : string
)
: Promise<void>
{
await _dali.backend.set_session_key(session_key);
const status = await _dali.backend.status();
if (! status.logged_in)
{
lib_plankton.log.error(
"dali.oidc_login_failed"
);
await _dali.notify_logout();
return Promise.reject<void>(new Error("oidc login failed"));
}
else
{
await _dali.notify_login(status.name);
return Promise.resolve<void>(undefined);
}
}
/**
*/
export async function logout(
)
: Promise<void>
{
try
{
await _dali.backend.session_end(
);
notify_logout();
}
catch (error)
{
lib_plankton.log.notice(
"dali.logout_failed",
{
"reason": String(error),
}
);
}
}
}

View file

@ -4,17 +4,48 @@
}, },
"tree": { "tree": {
"common.timezone_shift": "Zeitzonen-Verschiebung", "common.timezone_shift": "Zeitzonen-Verschiebung",
"common.timezone_indicator": "ZZ:",
"common.date": "Datum", "common.date": "Datum",
"common.time": "Uhrzeit", "common.time": "Uhrzeit",
"common.weekday.monday": "Mo",
"common.weekday.tuesday": "Di",
"common.weekday.wednesday": "Mi",
"common.weekday.thursday": "Do",
"common.weekday.friday": "Fr",
"common.weekday.saturday": "Sa",
"common.weekday.sunday": "So",
"common.monthname.january": "Jan",
"common.monthname.february": "Feb",
"common.monthname.march": "Mär",
"common.monthname.april": "Apr",
"common.monthname.may": "Mai",
"common.monthname.june": "Jun",
"common.monthname.july": "Jul",
"common.monthname.august": "Aug",
"common.monthname.september": "Sep",
"common.monthname.october": "Okt",
"common.monthname.november": "Nov",
"common.monthname.december": "Dez",
"common.open": "öffnen",
"common.close": "schließen",
"common.edit": "bearbeiten",
"common.show": "zeigen",
"common.hide": "ausblenden",
"common.cancel": "abbrechen",
"common.login": "anmelden",
"common.logout": "abmelden",
"common.confirm_deletion": "sicher?",
"access_level.none": "nichts", "access_level.none": "nichts",
"access_level.view": "nur lesen", "access_level.view": "nur lesen",
"access_level.edit": "lesen und bearbeiten", "access_level.edit": "lesen und bearbeiten",
"access_level.admin": "voll", "access_level.admin": "voll",
"event.event": "Termin", "event.event": "Termin",
"event.name": "Name", "event.name": "Name",
"event.when": "Zeit",
"event.begin": "Anfang", "event.begin": "Anfang",
"event.end": "Ende", "event.end": "Ende",
"event.location": "Ort", "event.location": "Ort",
"event.link": "Netz-Verweis",
"event.description": "Beschreibung", "event.description": "Beschreibung",
"resource.kind": "Art", "resource.kind": "Art",
"resource.kinds.local.title": "lokal", "resource.kinds.local.title": "lokal",
@ -24,34 +55,44 @@
"resource.kinds.caldav.read_only": "nur lesen", "resource.kinds.caldav.read_only": "nur lesen",
"calendar.calendar": "Kalender", "calendar.calendar": "Kalender",
"calendar.name": "Name", "calendar.name": "Name",
"calendar.hue": "Farbton",
"calendar.resource": "Resource", "calendar.resource": "Resource",
"calendar.access.access": "Zugriff", "calendar.access.access": "Zugriff",
"calendar.access.public": "öffentlich", "calendar.access.public": "öffentlich",
"calendar.access.default_level": "Standard", "calendar.access.default_level": "Standard",
"calendar.access.attributed": "Zuweisungen", "calendar.access.attributed_group": "Gruppen-Zuweisungen",
"calendar.access.attributed_user": "Nutzer-Zuweisungen",
"widget.listview.add": "Termin anlegen",
"widget.weekview.controls.year": "Jahr", "widget.weekview.controls.year": "Jahr",
"widget.weekview.controls.week": "Woche", "widget.weekview.controls.week": "Woche",
"widget.weekview.controls.count": "Anzahl", "widget.weekview.controls.count": "Anzahl",
"widget.weekview.controls.vertical": "senkrecht",
"widget.weekview.controls.apply": "Laden", "widget.weekview.controls.apply": "Laden",
"page.login.title": "Anmelden", "widget.calendar_edit.actions.add": "Kalender anlegen",
"page.login.internal.name": "Name", "widget.calendar_edit.actions.change": "ändern",
"page.login.internal.password": "Kennwort", "widget.calendar_edit.actions.remove": "löschen",
"page.login.internal.do": "Anmelden", "widget.event_edit.actions.add": "anlegen",
"page.login.oidc.via": "via {{title}}", "widget.event_edit.actions.change": "ändern",
"page.logout.title": "Abmelden", "widget.event_edit.actions.remove": "löschen",
"page.calendar_add.title": "Kalendar anlegen", "widget.sources.create": "Kalender anlegen",
"page.calendar_add.actions.do": "anlegen", "widget.login.internal.name": "Name",
"page.calendar_edit.title.regular": "Kalendar bearbeiten", "widget.login.internal.password": "Kennwort",
"page.calendar_edit.title.read_only": "Kalendar-Details", "widget.login.internal.do": "Anmelden",
"page.calendar_edit.actions.change": "ändern", "widget.login.oidc.via": "via {{title}}",
"page.calendar_edit.actions.remove": "löschen", "widget.caldav.title": "CalDAV",
"page.event_add.title": "Termin anlegen", "widget.caldav.unavailable": "CalDAV nicht verfügbar",
"page.event_add.actions.do": "anlegen", "widget.caldav.conf.title": "Zugangsdaten",
"page.event_edit.title.regular": "Termin bearbeiten", "widget.caldav.conf.address": "Adresse (URL)",
"page.event_edit.title.read_only": "Termin-Details", "widget.caldav.conf.username": "Nutzername",
"page.event_edit.actions.change": "ändern", "widget.caldav.conf.password": "Kennwort",
"page.event_edit.actions.remove": "löschen", "widget.caldav.conf.setup_hints": "Einrichtungs-Hinweise",
"page.overview.title": "Übersicht", "widget.caldav.conf.token_unset": "es muss zunächst ein Token gesetzt werden",
"page.overview.login_hint": "anmelden um nicht-öffentliche Termine zu sehen" "widget.caldav.set_token.title": "Token setzen",
"widget.caldav.set_token.action.set": "setzen",
"widget.caldav.set_token.action.overwrite": "überschreiben",
"widget.overview.title": "Übersicht",
"widget.overview.login_hint": "anmelden um nicht-öffentliche Termine zu sehen",
"widget.overview.mode.week": "Wochen-Ansicht",
"widget.overview.mode.list": "Listen-Ansicht"
} }
} }

View file

@ -4,17 +4,48 @@
}, },
"tree": { "tree": {
"common.timezone_shift": "Timezone shift", "common.timezone_shift": "Timezone shift",
"common.timezone_indicator": "TZ:",
"common.date": "date", "common.date": "date",
"common.time": "time", "common.time": "time",
"common.weekday.monday": "Mon",
"common.weekday.tuesday": "Tue",
"common.weekday.wednesday": "Wed",
"common.weekday.thursday": "Thu",
"common.weekday.friday": "Fri",
"common.weekday.saturday": "Sat",
"common.weekday.sunday": "Sun",
"common.monthname.january": "jan",
"common.monthname.february": "feb",
"common.monthname.march": "mar",
"common.monthname.april": "apr",
"common.monthname.may": "may",
"common.monthname.june": "jun",
"common.monthname.july": "jul",
"common.monthname.august": "aug",
"common.monthname.september": "sep",
"common.monthname.october": "oct",
"common.monthname.november": "nov",
"common.monthname.december": "dec",
"common.open": "open",
"common.close": "close",
"common.edit": "edit",
"common.show": "show",
"common.hide": "hide",
"common.cancel": "cancel",
"common.login": "login",
"common.logout": "logout",
"common.confirm_deletion": "sure?",
"access_level.none": "none", "access_level.none": "none",
"access_level.view": "read only", "access_level.view": "read only",
"access_level.edit": "read and write", "access_level.edit": "read and write",
"access_level.admin": "full", "access_level.admin": "full",
"event.event": "event", "event.event": "event",
"event.name": "name", "event.name": "name",
"event.when": "time",
"event.begin": "begin", "event.begin": "begin",
"event.end": "end", "event.end": "end",
"event.location": "location", "event.location": "location",
"event.link": "link",
"event.description": "description", "event.description": "description",
"resource.kind": "kind", "resource.kind": "kind",
"resource.kinds.local.title": "local", "resource.kinds.local.title": "local",
@ -22,36 +53,46 @@
"resource.kinds.caldav.title": "CalDAV", "resource.kinds.caldav.title": "CalDAV",
"resource.kinds.caldav.url": "URL", "resource.kinds.caldav.url": "URL",
"resource.kinds.caldav.read_only": "read-only", "resource.kinds.caldav.read_only": "read-only",
"calendar.calendar": "Kalendar", "calendar.calendar": "calendar",
"calendar.name": "name", "calendar.name": "name",
"calendar.hue": "hue",
"calendar.resource": "resource", "calendar.resource": "resource",
"calendar.access.access": "access", "calendar.access.access": "access",
"calendar.access.public": "public", "calendar.access.public": "public",
"calendar.access.default_level": "default", "calendar.access.default_level": "default",
"calendar.access.attributed": "attributed", "calendar.access.attributed_group": "group attributed",
"widget.weekview.controls.year": "Year", "calendar.access.attributed_user": "user attributed",
"widget.weekview.controls.week": "Week", "widget.listview.add": "add event",
"widget.weekview.controls.count": "Count", "widget.weekview.controls.year": "year",
"widget.weekview.controls.week": "week",
"widget.weekview.controls.count": "count",
"widget.weekview.controls.vertical": "vertical",
"widget.weekview.controls.apply": "Load", "widget.weekview.controls.apply": "Load",
"page.login.title": "Login", "widget.calendar_edit.actions.add": "add calendar",
"page.login.internal.name": "name", "widget.calendar_edit.actions.change": "change",
"page.login.internal.password": "password", "widget.calendar_edit.actions.remove": "delete",
"page.login.internal.do": "login", "widget.event_edit.actions.add": "add",
"page.login.oidc.via": "via {{title}}", "widget.event_edit.actions.change": "change",
"page.logout.title": "Logout", "widget.event_edit.actions.remove": "delete",
"page.calendar_add.title": "Add calendar", "widget.sources.create": "create calendar",
"page.calendar_add.actions.do": "anlegen", "widget.login.internal.name": "name",
"page.event_add.title": "Add event", "widget.login.internal.password": "password",
"page.event_add.actions.do": "add", "widget.login.internal.do": "login",
"page.calendar_edit.title.regular": "Edit calendar", "widget.login.oidc.via": "via {{title}}",
"page.calendar_edit.title.read_only": "Calendar details", "widget.caldav.title": "CalDAV",
"page.calendar_edit.actions.change": "change", "widget.caldav.unavailable": "CalDAV not available",
"page.calendar_edit.actions.remove": "delete", "widget.caldav.conf.title": "credentials",
"page.event_edit.title.regular": "Edit event", "widget.caldav.conf.address": "address (URL)",
"page.event_edit.title.read_only": "Event details", "widget.caldav.conf.username": "username",
"page.event_edit.actions.change": "change", "widget.caldav.conf.password": "password",
"page.event_edit.actions.remove": "delete", "widget.caldav.conf.setup_hints": "setup hints",
"page.overview.title": "Overview", "widget.caldav.conf.token_unset": "a token has to be set",
"page.overview.login_hint": "log in to view non-public events" "widget.caldav.set_token.title": "set token",
"widget.caldav.set_token.action.set": "set",
"widget.caldav.set_token.action.overwrite": "overwrite",
"widget.overview.title": "Overview",
"widget.overview.login_hint": "log in to view non-public events",
"widget.overview.mode.week": "week view",
"widget.overview.mode.list": "list view"
} }
} }

394
source/helpers.ts Normal file
View file

@ -0,0 +1,394 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
/**
*/
namespace _dali.helpers
{
/**
* @todo outsource
*/
function is_touch_device(
)
: boolean
{
return (
("ontouchstart" in window)
||
(navigator.maxTouchPoints > 0)
||
(navigator["msMaxTouchPoints"] > 0)
);
}
/**
*/
var _template_cache : Record<string, string> = {};
/**
*/
export function view_mode_determine(
mode_descriptor : string
)
: _dali.enum_view_mode
{
if (mode_descriptor === "auto")
{
return (
(window.innerWidth >= 1000)
?
_dali.enum_view_mode.week
:
_dali.enum_view_mode.list
);
}
else
{
return view_mode_decode(mode_descriptor);
}
}
/**
*/
export function view_kind_determine(
mode_descriptor : string
)
: _dali.enum_view_kind
{
if (mode_descriptor === "auto")
{
return (
is_touch_device()
?
_dali.enum_view_kind.touch
:
_dali.enum_view_kind.regular
);
}
else
{
return view_kind_decode(mode_descriptor);
}
}
/**
*/
export async function template_coin(
group : string,
name : string,
data : Record<string, string>
) : Promise<string>
{
let content : string;
const key : string = lib_plankton.string.coin(
"{{group}}/{{name}}",
{
"group": group,
"name": name,
}
);
if (! (key in _template_cache)) {
content = (
(
await lib_plankton.file.read(
lib_plankton.string.coin(
"templates/{{group}}/{{name}}.html.tpl",
{
"group": group,
"name": name,
}
)
)
)
.toString()
);
_template_cache[key] = content;
}
else {
content = _template_cache[key];
}
return Promise.resolve<string>(
lib_plankton.string.coin(
content,
data
)
);
}
/**
*/
export async function element_from_template(
group : string,
name : string,
data : Record<string, string>
)
: Promise<HTMLElement>
{
const dom_dummy = document.createElement("div");
dom_dummy.innerHTML = await _dali.helpers.template_coin(
group,
name,
data
);
return (dom_dummy.children[0] as HTMLElement);
}
/**
* @todo outsource
*/
export async function promise_row<type_result>(
members : Array<
() => Promise<type_result>
>
)
: Promise<
Array<
type_result
>
>
{
let results : Array<type_result> = [];
for await (const member of members) {
results.push(await member());
}
return Promise.resolve<Array<type_result>>(results);
}
/**
*/
export function input_access_level(
)
: lib_plankton.zoo_input.interface_input<_dali.enum_access_level>
{
return (
new lib_plankton.zoo_input.class_input_wrapped<
/*("none" | "view" | "edit" | "admin")*/string,
_dali.enum_access_level
>(
new lib_plankton.zoo_input.class_input_selection(
[
{
"value": "none",
"label": lib_plankton.translate.get("access_level.none"),
},
{
"value": "view",
"label": lib_plankton.translate.get("access_level.view")
},
{
"value": "edit",
"label": lib_plankton.translate.get("access_level.edit")
},
{
"value": "admin",
"label": lib_plankton.translate.get("access_level.admin")
},
]
),
(raw) => {
switch (raw) {
case "none": return _dali.enum_access_level.none;
case "view": return _dali.enum_access_level.view;
case "edit": return _dali.enum_access_level.edit;
case "admin": return _dali.enum_access_level.admin;
}
},
(access_level) => {
switch (access_level) {
case _dali.enum_access_level.none: return "none";
case _dali.enum_access_level.view: return "view";
case _dali.enum_access_level.edit: return "edit";
case _dali.enum_access_level.admin: return "admin";
}
},
)
);
}
/**
*/
export function input_attributed_access_group(
groups : Array<{id : _dali.type_group_id; object : _dali.type_group_object;}>
)
: lib_plankton.zoo_input.class_input_hashmap<
_dali.type_group_id,
_dali.enum_access_level
>
{
return (
new lib_plankton.zoo_input.class_input_hashmap<_dali.type_group_id, _dali.enum_access_level>(
// hash_key
(group_id) => group_id.toFixed(0),
// key_input_factory
() => new lib_plankton.zoo_input.class_input_wrapped<string, int>(
new lib_plankton.zoo_input.class_input_selection(
groups
.map(
(group) => ({
"value": group.id.toFixed(0),
"label": group.object.label,
})
)
),
x => parseInt(x),
x => x.toFixed(0)
),
// value_input_factory
() => input_access_level()
)
);
}
/**
*/
export function input_attributed_access_user(
users : Array<{id : _dali.type_user_id; name : string;}>
)
: lib_plankton.zoo_input.class_input_hashmap<
_dali.type_user_id,
_dali.enum_access_level
>
{
return (
new lib_plankton.zoo_input.class_input_hashmap<_dali.type_user_id, _dali.enum_access_level>(
// hash_key
(user_id) => user_id.toFixed(0),
// key_input_factory
() => new lib_plankton.zoo_input.class_input_wrapped<string, int>(
new lib_plankton.zoo_input.class_input_selection(
users
.map(
(user) => ({
"value": user.id.toFixed(0),
"label": user.name,
})
)
),
x => parseInt(x),
x => x.toFixed(0)
),
// value_input_factory
() => input_access_level()
)
);
}
/**
*/
export function datetime_input(
)
: lib_plankton.zoo_input.interface_input<lib_plankton.pit.type_datetime>
{
return (
_dali.conf.get().misc.use_central_europe_specific_datetime_inputs
?
new lib_plankton.zoo_input.class_input_datetime_central_europe(
{
"label_date": lib_plankton.translate.get("common.date"),
"label_time": lib_plankton.translate.get("common.time"),
}
)
:
new lib_plankton.zoo_input.class_input_datetime(
{
"label_timezone_shift": lib_plankton.translate.get("common.timezone_shift"),
"label_date": lib_plankton.translate.get("common.date"),
"label_time": lib_plankton.translate.get("common.time"),
}
)
);
}
/**
*/
export function event_color(
hue : float
)
:
string
{
return lib_plankton.color.output_hex(
lib_plankton.color.make_hsv(
{
"hue": hue,
"saturation": 0.375,
"value": 0.375,
}
),
);
}
/**
*/
export function month_name(
month : int
)
: string
{
const keys : Array<string> = [
"common.monthname.january",
"common.monthname.february",
"common.monthname.march",
"common.monthname.april",
"common.monthname.may",
"common.monthname.june",
"common.monthname.july",
"common.monthname.august",
"common.monthname.september",
"common.monthname.october",
"common.monthname.november",
"common.monthname.december",
];
return lib_plankton.translate.get(keys[month-1]);
}
/**
*/
export function loading(
mode : boolean
)
: void
{
if (! mode)
{
_dali.overlay.toggle({"mode": false});
}
else
{
_dali.overlay.get_content_element().innerHTML = ". . .";
_dali.overlay.toggle({"mode": true});
}
}
}

View file

@ -11,7 +11,7 @@
document.addEventListener( document.addEventListener(
"DOMContentLoaded", "DOMContentLoaded",
() => { () => {
_zeitbild.frontend_web.main() _dali.main()
.then( .then(
() => {} () => {}
) )
@ -20,16 +20,14 @@ document.addEventListener(
) )
} }
); );
</script> </script>
{{templates}}
</head> </head>
<body> <body>
<div id="overlay">
<div id="overlay_content">
</div>
</div>
<header> <header>
<nav>
<ul>
</ul>
</nav>
</header> </header>
<main> <main>
</main> </main>

View file

@ -1,587 +0,0 @@
/**
*/
namespace _zeitbild.frontend_web.backend
{
/**
*/
var _data_chest : (
null
|
lib_plankton.storage.type_chest<string, string, void, string, string>
) = null;
/**
*/
function access_level_encode(
access_level : _zeitbild.frontend_web.type.enum_access_level
) : ("none" | "view" | "edit" | "admin")
{
switch (access_level) {
case _zeitbild.frontend_web.type.enum_access_level.none: return "none";
case _zeitbild.frontend_web.type.enum_access_level.view: return "view";
case _zeitbild.frontend_web.type.enum_access_level.edit: return "edit";
case _zeitbild.frontend_web.type.enum_access_level.admin: return "admin";
}
}
/**
*/
function access_level_decode(
access_level_encoded : ("none" | "view" | "edit" | "admin")
) : _zeitbild.frontend_web.type.enum_access_level
{
switch (access_level_encoded) {
case "none": return _zeitbild.frontend_web.type.enum_access_level.none;
case "view": return _zeitbild.frontend_web.type.enum_access_level.view;
case "edit": return _zeitbild.frontend_web.type.enum_access_level.edit;
case "admin": return _zeitbild.frontend_web.type.enum_access_level.admin;
}
}
/**
*/
export async function init(
) : Promise<void>
{
_data_chest = lib_plankton.storage.localstorage.implementation_chest(
{
"corner": "zeitbild",
}
);
return Promise.resolve<void>(undefined);
}
/**
*/
async function get_session_key(
) : Promise<(null | string)>
{
try {
return (await _data_chest.read("session_key"));
}
catch (error) {
return null;
}
}
/**
*/
export async function is_logged_in(
) : Promise<boolean>
{
return ((await get_session_key()) !== null);
}
/**
*/
async function call(
method : lib_plankton.http.enum_method,
action : string,
input : (null | any)
) : Promise<any>
{
const with_body : boolean = (
[
lib_plankton.http.enum_method.post,
lib_plankton.http.enum_method.put,
lib_plankton.http.enum_method.patch,
].includes(method)
);
const session_key : (null | string) = await get_session_key();
const http_request : lib_plankton.http.type_request = {
"version": "HTTP/2",
"scheme": (
(_zeitbild.frontend_web.conf.get()["backend"]["scheme"] === "http")
?
"http"
:
"https"
),
"host": lib_plankton.string.coin(
"{{host}}:{{port}}",
{
"host": _zeitbild.frontend_web.conf.get()["backend"]["host"],
"port": _zeitbild.frontend_web.conf.get()["backend"]["port"].toFixed(0),
}
),
"path": lib_plankton.string.coin(
"{{base}}{{action}}",
{
"base": _zeitbild.frontend_web.conf.get()["backend"]["path"],
"action": action,
}
),
"method": method,
"query": (
(with_body || (input === null))
?
null
:
("?" + lib_plankton.www_form.encode(input))
),
"headers": Object.assign(
{},
(
(! with_body)
?
{}
:
{"Content-Type": "application/json"}
),
(
(session_key === null)
?
{}
:
{"X-Session-Key": session_key}
)
),
"body": (
((! with_body) || (input === null))
?
null
:
/*Buffer.from*/(lib_plankton.json.encode(input))
),
};
const http_response : lib_plankton.http.type_response = await lib_plankton.http.call(http_request);
if (
! (
(http_response.status_code >= 200)
&&
(http_response.status_code < 300)
)
) {
return Promise.reject<any>(http_response.body.toString());
}
else {
const output : any = lib_plankton.json.decode(http_response.body.toString());
return Promise.resolve<any>(output);
}
}
/**
*/
export async function session_prepare(
input : any
) : Promise<{kind : string; data : any;}>
{
return call(
lib_plankton.http.enum_method.post,
"/session/prepare",
input
);
}
/**
*/
export function set_session_key(
session_key : string
) : Promise<void>
{
return (
_data_chest.write("session_key", session_key)
.then<void>(() => Promise.resolve<void>(undefined))
);
}
/**
*/
export async function session_begin(
name : string,
password : string
) : Promise<void>
{
const session_key : string = await call(
lib_plankton.http.enum_method.post,
"/session/begin",
{
"name": name,
"password": password,
}
);
await _data_chest.write("session_key", session_key);
return Promise.resolve<void>(undefined);
}
/**
*/
export async function session_end(
) : Promise<void>
{
await call(
lib_plankton.http.enum_method.delete,
"/session/end",
null
);
await _data_chest.delete("session_key");
return Promise.resolve<void>(undefined);
}
/**
*/
export async function user_list(
) : Promise<
Array<
{
id : int;
name : string;
}
>
>
{
return call(
lib_plankton.http.enum_method.get,
"/users",
null
);
}
/**
*/
export async function calendar_list(
) : Promise<
Array<
{
id : int;
name : string;
access_level : _zeitbild.frontend_web.type.enum_access_level;
}
>
>
{
return (
call(
lib_plankton.http.enum_method.get,
"/calendar",
null
)
.then(
(entries) => Promise.resolve(
entries
.map(
(entry) => ({
"id": entry.id,
"name": entry.name,
"access_level": access_level_decode(entry.access_level),
})
)
)
)
);
}
/**
*/
export async function calendar_get(
calendar_id : _zeitbild.frontend_web.type.calendar_id
) : Promise<
_zeitbild.frontend_web.type.calendar_object
>
{
return (
call(
lib_plankton.http.enum_method.get,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}",
{
"calendar_id": calendar_id.toFixed(0),
}
),
null
)
.then(
(raw) => Promise.resolve(
{
"name": raw.name,
"access": {
"public": raw.access.public,
"default_level": access_level_decode(raw.access.default_level),
"attributed": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
x => x.toFixed(0),
{
"pairs": (
raw.access.attributed
.map(
(entry) => ({
"key": entry.user_id,
"value": access_level_decode(entry.level),
})
)
),
}
)
),
},
// "resource_id": raw.resource_id
// TODO
"resource": {
"kind": "local",
"data": {
"events": []
}
}
}
)
)
);
}
/**
*/
export async function calendar_add(
calendar_object : _zeitbild.frontend_web.type.calendar_object
) : Promise<
_zeitbild.frontend_web.type.calendar_id
>
{
return call(
lib_plankton.http.enum_method.post,
lib_plankton.string.coin(
"/calendar",
{
}
),
{
"name": calendar_object.name,
"access": {
"public": calendar_object.access.public,
"default_level": access_level_encode(calendar_object.access.default_level),
"attributed": (
lib_plankton.map.dump(calendar_object.access.attributed)
.map(
(pair) => ({
"user_id": pair.key,
"level": access_level_encode(pair.value),
})
)
)
},
"resource": calendar_object.resource,
}
);
}
/**
*/
export async function calendar_change(
calendar_id : _zeitbild.frontend_web.type.calendar_id,
calendar_object : _zeitbild.frontend_web.type.calendar_object
) : Promise<
void
>
{
return call(
lib_plankton.http.enum_method.put,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}",
{
"calendar_id": calendar_id.toFixed(0),
}
),
{
"name": calendar_object.name,
"access": {
"public": calendar_object.access.public,
"default_level": access_level_encode(calendar_object.access.default_level),
"attributed": (
lib_plankton.map.dump(calendar_object.access.attributed)
.map(
(pair) => ({
"user_id": pair.key,
"level": access_level_encode(pair.value),
})
)
)
},
"resource": calendar_object.resource,
}
);
}
/**
*/
export async function calendar_remove(
calendar_id : _zeitbild.frontend_web.type.calendar_id
) : Promise<
void
>
{
return call(
lib_plankton.http.enum_method.delete,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}",
{
"calendar_id": calendar_id.toFixed(0),
}
),
null
);
}
/**
*/
export async function calendar_event_get(
calendar_id : _zeitbild.frontend_web.type.calendar_id,
event_id : _zeitbild.frontend_web.type.local_resource_event_id
) : Promise<_zeitbild.frontend_web.type.event_object>
{
return call(
lib_plankton.http.enum_method.get,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}/event/{{event_id}}",
{
"calendar_id": calendar_id.toFixed(0),
"event_id": event_id.toFixed(0),
}
),
null
);
}
/**
*/
export async function calendar_event_add(
calendar_id : _zeitbild.frontend_web.type.calendar_id,
event_object : _zeitbild.frontend_web.type.event_object
) : Promise<void>
{
return call(
lib_plankton.http.enum_method.post,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}/event",
{
"calendar_id": calendar_id.toFixed(0),
}
),
event_object
);
}
/**
*/
export async function calendar_event_change(
calendar_id : _zeitbild.frontend_web.type.calendar_id,
event_id : _zeitbild.frontend_web.type.local_resource_event_id,
event_object : _zeitbild.frontend_web.type.event_object
) : Promise<void>
{
return call(
lib_plankton.http.enum_method.put,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}/event/{{event_id}}",
{
"calendar_id": calendar_id.toFixed(0),
"event_id": event_id.toFixed(0),
}
),
event_object
);
}
/**
*/
export async function calendar_event_remove(
calendar_id : _zeitbild.frontend_web.type.calendar_id,
event_id : _zeitbild.frontend_web.type.local_resource_event_id
) : Promise<void>
{
return call(
lib_plankton.http.enum_method.delete,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}/event/{{event_id}}",
{
"calendar_id": calendar_id.toFixed(0),
"event_id": event_id.toFixed(0),
}
),
null
);
}
/**
* @todo prevent loops
*/
export async function events(
from_pit : lib_plankton.pit.type_pit,
to_pit : lib_plankton.pit.type_pit,
options : {
calendar_ids ?: (null | Array<_zeitbild.frontend_web.type.calendar_id>);
} = {}
) : Promise<
Array<
{
calendar_id : _zeitbild.frontend_web.type.calendar_id;
calendar_name : string;
access_level : _zeitbild.frontend_web.type.enum_access_level;
event_id : (null | _zeitbild.frontend_web.type.local_resource_event_id);
event_object : _zeitbild.frontend_web.type.event_object;
}
>
>
{
options = Object.assign(
{
"calendar_ids": null,
},
options
);
return (
call(
lib_plankton.http.enum_method.get,
"/events",
Object.assign(
{
"from": from_pit,
"to": to_pit,
},
(
(options.calendar_ids === null)
?
{}
:
{"calendar_ids": options.calendar_ids.join(",")}
)
)
)
.then(
(data) => Promise.resolve(
data
.map(
(entry) => ({
"calendar_id": entry.calendar_id,
"calendar_name": entry.calendar_name,
"access_level": access_level_decode(entry.access_level),
"event_id": entry.event_id,
"event_object": entry.event_object,
})
)
)
)
);
}
}

View file

@ -1,260 +0,0 @@
/**
*/
namespace _zeitbild.frontend_web.helpers
{
/**
*/
var _template_cache : Record<string, string> = {};
/**
*/
export async function template_coin(
group : string,
name : string,
data : Record<string, string>
) : Promise<string>
{
let content : string;
const key : string = lib_plankton.string.coin(
"{{group}}/{{name}}",
{
"group": group,
"name": name,
}
);
if (! (key in _template_cache)) {
content = (
(
await lib_plankton.file.read(
lib_plankton.string.coin(
"templates/{{group}}/{{name}}.html.tpl",
{
"group": group,
"name": name,
}
)
)
)
.toString()
);
_template_cache[key] = content;
}
else {
content = _template_cache[key];
}
return Promise.resolve<string>(
lib_plankton.string.coin(
content,
data
)
);
}
/**
* @todo outsource
*/
export async function promise_row<type_result>(
members : Array<
() => Promise<type_result>
>
) : Promise<
Array<
type_result
>
>
{
let results : Array<type_result> = [];
for await (const member of members) {
results.push(await member());
}
return Promise.resolve<Array<type_result>>(results);
}
/**
* @todo timezone
*/
export function date_format(
date : lib_plankton.pit.type_date
) : string
{
return lib_plankton.string.coin(
"{{year}}-{{month}}-{{day}}",
{
"year": date.year.toFixed(0).padStart(4, "0"),
"month": date.month.toFixed(0).padStart(2, "0"),
"day": date.day.toFixed(0).padStart(2, "0"),
}
);
}
/**
* @todo timezone
*/
export function time_format(
time : lib_plankton.pit.type_time
) : string
{
return lib_plankton.string.coin(
"{{hour}}:{{minute}}",
{
"hour": time.hour.toFixed(0).padStart(2, "0"),
"minute": time.minute.toFixed(0).padStart(2, "0"),
"second": time.second.toFixed(0).padStart(2, "0"),
}
);
}
/**
* @todo timezone
*/
export function datetime_format(
datetime : (null | lib_plankton.pit.type_datetime)
) : string
{
if (datetime === null) {
return "-";
}
else {
return lib_plankton.string.coin(
"{{date}}{{macro_time}}",
{
"date": date_format(datetime.date),
"macro_time": (
(datetime.time === null)
?
""
:
lib_plankton.string.coin(
",{{time}}",
{
"time": time_format(datetime.time),
}
)
),
}
);
}
}
/**
*/
export function timespan_format(
from : lib_plankton.pit.type_datetime,
to : (null | lib_plankton.pit.type_datetime)
) : string
{
let result : string = "";
result += datetime_format(from);
if (to === null) {
// do nothing
}
else {
result += " - ";
if (
(from.date.year === to.date.year)
&&
(from.date.month === to.date.month)
&&
(from.date.day === to.date.day)
) {
// only time
result += time_format(to.time);
}
else {
result += datetime_format(to);
}
}
return result;
}
/**
*/
export function input_access_level(
) : lib_plankton.zoo_input.interface_input<_zeitbild.frontend_web.type.enum_access_level>
{
return (
new lib_plankton.zoo_input.class_input_wrapped<
/*("none" | "view" | "edit" | "admin")*/string,
_zeitbild.frontend_web.type.enum_access_level
>(
new lib_plankton.zoo_input.class_input_selection(
[
{
"value": "none",
"label": lib_plankton.translate.get("access_level.none"),
},
{
"value": "view",
"label": lib_plankton.translate.get("access_level.view")
},
{
"value": "edit",
"label": lib_plankton.translate.get("access_level.edit")
},
{
"value": "admin",
"label": lib_plankton.translate.get("access_level.admin")
},
]
),
(raw) => {
switch (raw) {
case "none": return _zeitbild.frontend_web.type.enum_access_level.none;
case "view": return _zeitbild.frontend_web.type.enum_access_level.view;
case "edit": return _zeitbild.frontend_web.type.enum_access_level.edit;
case "admin": return _zeitbild.frontend_web.type.enum_access_level.admin;
}
},
(access_level) => {
switch (access_level) {
case _zeitbild.frontend_web.type.enum_access_level.none: return "none";
case _zeitbild.frontend_web.type.enum_access_level.view: return "view";
case _zeitbild.frontend_web.type.enum_access_level.edit: return "edit";
case _zeitbild.frontend_web.type.enum_access_level.admin: return "admin";
}
},
)
);
}
/**
*/
export async function input_attributed_access(
) : Promise<lib_plankton.zoo_input.class_input_hashmap<_zeitbild.frontend_web.type.user_id, _zeitbild.frontend_web.type.enum_access_level>>
{
const users : Array<{id : _zeitbild.frontend_web.type.user_id; name : string;}> = await _zeitbild.frontend_web.backend.user_list(
);
return Promise.resolve(
new lib_plankton.zoo_input.class_input_hashmap<_zeitbild.frontend_web.type.user_id, _zeitbild.frontend_web.type.enum_access_level>(
// hash_key
(user_id) => user_id.toFixed(0),
// key_input_factory
() => new lib_plankton.zoo_input.class_input_wrapped<string, int>(
new lib_plankton.zoo_input.class_input_selection(
users
.map(
(user) => ({
"value": user.id.toFixed(0),
"label": user.name,
})
)
),
x => parseInt(x),
x => x.toFixed(0)
),
// value_input_factory
() => input_access_level()
)
);
}
}

View file

@ -1,75 +0,0 @@
/**
*/
namespace _zeitbild.frontend_web
{
/**
*/
export async function main(
) : Promise<void>
{
// conf
await _zeitbild.frontend_web.conf.init(
"conf.json"
);
// init
lib_plankton.log.conf_push(
[
lib_plankton.log.channel_make({"kind": "console", "data": {"threshold": "info"}}),
]
);
await _zeitbild.frontend_web.backend.init(
);
await lib_plankton.translate.initialize(
{
"verbosity": 1,
"packages": [
JSON.parse(await lib_plankton.file.read("data/localization/deu.loc.json")),
JSON.parse(await lib_plankton.file.read("data/localization/eng.loc.json")),
],
"order": ["deu", "eng"],
"autopromote": false,
}
);
lib_plankton.zoo_page.init(
document.querySelector("main"),
{
"fallback": {
"name": "overview",
"parameters": {}
}
}
);
lib_plankton.zoo_page.add_nav_entry(
{"name": "login", "parameters": {}},
{"label": lib_plankton.translate.get("page.login.title")}
);
lib_plankton.zoo_page.add_nav_entry(
{"name": "overview", "parameters": {}},
{"label": lib_plankton.translate.get("page.overview.title")}
);
lib_plankton.zoo_page.add_nav_entry(
{"name": "calendar_add", "parameters": {}},
{"label": lib_plankton.translate.get("page.calendar_add.title")}
);
/*
lib_plankton.zoo_page.add_nav_entry(
{"name": "event_add", "parameters": {}},
{"label": lib_plankton.translate.get("page.event_add.title")}
);
*/
lib_plankton.zoo_page.add_nav_entry(
{"name": "logout", "parameters": {}},
{"label": lib_plankton.translate.get("page.logout.title")}
);
// exec
lib_plankton.zoo_page.start();
return Promise.resolve<void>(undefined);
}
}

View file

@ -1,109 +0,0 @@
/**
*/
namespace _zeitbild.frontend_web.type
{
/**
*/
export enum enum_access_level {
none,
view,
edit,
admin
}
/**
*/
export type user_id = int;
/**
*/
export type user_object = {
name : string;
email_address : (
null
|
string
);
};
/**
*/
export type event_object = {
name : string;
begin : lib_plankton.pit.type_datetime;
end : (
null
|
lib_plankton.pit.type_datetime
);
location : (
null
|
string
);
description : (
null
|
string
);
};
/**
*/
export type local_resource_event_id = int;
/**
*/
export type resource_id = int;
/**
*/
export type resource_object = (
{
kind : "local";
data : {
events : Array<
event_object
>;
};
}
|
{
kind : "caldav";
data : {
read_only : boolean;
url : string;
};
}
);
/**
*/
export type calendar_id = int;
/**
*/
export type calendar_object = {
name : string;
access : {
public : boolean;
default_level : enum_access_level;
attributed : lib_plankton.map.type_map<
user_id,
enum_access_level
>;
};
resource : resource_object;
};
}

View file

@ -1,18 +0,0 @@
namespace _zeitbild
{
/**
* @todo outsource
*/
export abstract class class_widget
{
/**
*/
public abstract load(
target_element : Element
) : Promise<void>;
}
}

221
source/main.ts Normal file
View file

@ -0,0 +1,221 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
/**
*/
namespace _dali
{
/**
*/
export async function main(
)
: Promise<void>
{
// conf
await _dali.conf.init(
"conf.json"
);
// init:logger
lib_plankton.log.set_main_logger(
[
{
"kind": "minlevel",
"data": {
"core": {
"kind": "console",
"data": {
}
},
"threshold": "info"
}
},
]
);
// init:overlay
{
await _dali.overlay.initialize();
_dali.helpers.loading(true);
}
// init:localization
{
const order : Array<string> = ["deu", "eng"];
await lib_plankton.translate.initialize(
{
"verbosity": 1,
"packages": await Promise.all(
order.map(
code => (
Promise.resolve(code)
.then<string>(code => Promise.resolve(lib_plankton.string.coin("data/localization/{{code}}.loc.json", {"code": code})))
.then<string>(lib_plankton.file.read)
.then<any>(content => Promise.resolve(JSON.parse(content)))
)
)
),
"order": order,
"autopromote": false,
}
);
}
// init:backend
await _dali.backend.initialize(
_dali.conf.get()["backend"]
);
// init:model
await _dali.model.initialize(
);
// init:page
lib_plankton.zoo_page.init(
document.querySelector("main"),
{
"fallback": {
"name": "overview",
"parameters": {}
},
}
);
// init:menu
{
const widget_menu : _dali.widgets.class_widget_menu = new _dali.widgets.class_widget_menu(
[
{
"label": lib_plankton.translate.get("common.login"),
"groups": ["logged_out"],
"action": async () => {
_dali.helpers.loading(true);
const widget : lib_plankton.zoo_widget.interface_widget = new _dali.widgets.class_widget_login(
{
"action_cancel": () => {
_dali.overlay.clear();
_dali.overlay.toggle({"mode": false});
},
"action_success": async () => {
_dali.helpers.loading(true);
const status = await _dali.backend.status();
_dali.notify_login(status.name);
// _dali.overlay.clear();
_dali.helpers.loading(false);
},
}
);
await widget.load(_dali.overlay.get_content_element());
// _dali.helpers.loading(false);
_dali.overlay.toggle({"mode": true});
},
},
{
"label": lib_plankton.translate.get("widget.caldav.title"),
"groups": ["logged_in"],
"action": async () => {
_dali.helpers.loading(true);
const widget : lib_plankton.zoo_widget.interface_widget = new _dali.widgets.class_widget_caldav();
await widget.load(_dali.overlay.get_content_element());
// _dali.helpers.loading(false);
_dali.overlay.toggle({"mode": true});
},
},
{
"label": lib_plankton.translate.get("common.logout"),
"groups": ["logged_in"],
"action": async () => {
_dali.helpers.loading(true);
await _dali.logout();
_dali.helpers.loading(false);
},
},
]
);
await widget_menu.load(document.querySelector("header"));
_dali.listen_login(
async (name) => {
widget_menu.set_groups(["logged_in"]);
widget_menu.set_label(name);
}
);
_dali.listen_logout(
async () => {
widget_menu.set_groups(["logged_out"]);
widget_menu.set_label(null);
}
);
}
// process actions
{
let url_search_params : URLSearchParams = new URLSearchParams(window.location.search);
const action : (null | string) = url_search_params.get("action");
switch (action)
{
case "oidc_finish":
{
try
{
await _dali.oidc_finish(url_search_params.get("session_key"));
}
catch (error_)
{
// do nothing
}
document.location = "/";
break;
}
default:
case null:
{
// do nothing
break;
}
}
}
// process status
{
const status = await _dali.backend.status();
lib_plankton.log.info(
"dali.status",
status
);
if (status.logged_in)
{
_dali.notify_login(status.name);
}
else
{
_dali.notify_logout();
}
}
// load overview
{
const widget : lib_plankton.zoo_widget.interface_widget = (
new _dali.widgets.class_widget_overview(
)
);
await widget.load(document.querySelector("main"));
}
_dali.helpers.loading(false);
return Promise.resolve<void>(undefined);
}
}

692
source/model.ts Normal file
View file

@ -0,0 +1,692 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.model
{
/**
*/
type type_state = {
groups : Array<
{
id : _dali.type_group_id;
object : _dali.type_group_object;
}
>;
users : Array<
{
id : _dali.type_user_id;
name : string;
}
>;
calendars : (
null
|
lib_plankton.map.type_map<
_dali.type_calendar_id,
{
reduced : _dali.type_calendar_object_reduced;
complete : (null | _dali.type_calendar_object);
}
>
);
events : lib_plankton.map.type_map<
_dali.type_event_key,
_dali.type_event_object_extended
>;
covered_dates : lib_plankton.set.type_set<
lib_plankton.pit.type_date
>;
};
/**
*/
let _state : (null | type_state) = null;
/**
*/
let _listeners_reset : Array<((priviliged ?: boolean) => Promise<void>)> = [];
/**
*/
function make_date_set(
{
"elements": elements = [],
}
:
{
elements ?: Array<lib_plankton.pit.type_date>;
}
=
{
}
)
: lib_plankton.set.type_set<lib_plankton.pit.type_date>
{
return lib_plankton.set.hashset.implementation_set(
lib_plankton.set.hashset.make(
x => lib_plankton.pit.date_format(x),
{
/**
* @todo häääh!?
*/
"elements": elements.filter(x => (x !== null)),
}
)
);
}
/**
*/
function timeframe_to_date_set(
timeframe : {
from : lib_plankton.pit.type_pit;
to : lib_plankton.pit.type_pit;
}
)
: lib_plankton.set.type_set<lib_plankton.pit.type_date>
{
if (! lib_plankton.pit.is_before(timeframe.from, timeframe.to))
{
throw (new Error("invalid timeframe"));
}
else
{
const heuristic : int = Math.ceil((timeframe.to - timeframe.from) / (60 * 60 * 24));
let pit_current : lib_plankton.pit.type_pit = timeframe.from;
return make_date_set(
{
"elements": (
lib_plankton.list.sequence(heuristic)
.map(
(offset) => lib_plankton.call.convey(
timeframe.from,
[
x => lib_plankton.pit.shift_day(x, offset),
x => lib_plankton.pit.to_datetime(x),
x => x.date,
]
)
)
),
}
);
}
}
/**
*/
export async function group_list(
)
: Promise<
Array<
{
id : _dali.type_group_id;
object : _dali.type_group_object;
}
>
>
{
return Promise.resolve(_state.groups);
}
/**
*/
export async function user_list(
)
: Promise<
Array<
{
id : _dali.type_user_id;
name : string;
}
>
>
{
return Promise.resolve(_state.users);
}
/**
*/
async function sync_calendars(
)
: Promise<void>
{
const data = await _dali.backend.calendar_list();
lib_plankton.map.clear(_state.calendars);
data.forEach(
entry => {
_state.calendars.set(
entry.id,
{
"reduced": {
"name": entry.name,
"hue": entry.hue,
"access_level": _dali.access_level_decode(entry.access_level),
},
"complete": null,
}
);
}
);
}
/**
*/
export function calendar_list(
)
: Promise<
Array<
_dali.type_calendar_object_reduced_with_id
>
>
{
return Promise.resolve(
lib_plankton.map.dump(_state.calendars)
.map(
pair => (
{
"id": pair.key,
"name": pair.value.reduced.name,
"hue": pair.value.reduced.hue,
"access_level": pair.value.reduced.access_level,
}
)
)
);
}
/**
*/
export async function calendar_get(
calendar_id : _dali.type_calendar_id
)
: Promise<_dali.type_calendar_object>
{
const value = _state.calendars.get(calendar_id);
if (value.complete === null)
{
const data = await _dali.backend.calendar_get(calendar_id);
const calendar_object : _dali.type_calendar_object = {
"name": data.name,
"hue": data.hue,
"access": {
"public": data.access.public,
"default_level": _dali.access_level_decode(data.access.default_level),
"attributed_group": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
x => x.toFixed(0),
{
"pairs": (
data.access.attributed_group
.map(
(entry) => (
{
"key": entry.group_id,
"value": _dali.access_level_decode(entry.level),
}
)
)
),
}
)
),
"attributed_user": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
x => x.toFixed(0),
{
"pairs": (
data.access.attributed_user
.map(
(entry) => (
{
"key": entry.user_id,
"value": _dali.access_level_decode(entry.level),
}
)
)
),
}
)
),
},
"resource_id": data.resource_id,
};
value.complete = calendar_object;
/**
* @todo set in map?
*/
return calendar_object;
}
else
{
return value.complete;
}
}
/**
*/
export async function calendar_add(
calendar_object : _dali.type_calendar_object
)
: Promise<void>
{
const calendar_id : _dali.type_calendar_id = await _dali.backend.calendar_add(
{
"name": calendar_object.name,
"hue": calendar_object.hue,
"access": {
"public": calendar_object.access.public,
"default_level": _dali.access_level_encode(calendar_object.access.default_level),
"attributed_group": (
lib_plankton.map.dump(calendar_object.access.attributed_group)
.map(
(pair) => (
{
"group_id": pair.key,
"level": _dali.access_level_encode(pair.value),
}
)
)
),
"attributed_user": (
lib_plankton.map.dump(calendar_object.access.attributed_user)
.map(
(pair) => (
{
"user_id": pair.key,
"level": _dali.access_level_encode(pair.value),
}
)
)
),
},
/**
* @todo
*/
"resource": {
"kind": "local",
"data": {
"events": [],
}
},
}
);
const calendar_object_reduced : _dali.type_calendar_object_reduced = {
"name": calendar_object.name,
"hue": calendar_object.hue,
"access_level": _dali.enum_access_level.admin,
};
_state.calendars.set(
calendar_id,
{
"reduced": calendar_object_reduced,
"complete": calendar_object,
}
);
}
/**
*/
export async function calendar_change(
calendar_id : _dali.type_calendar_id,
calendar_object : _dali.type_calendar_object
)
: Promise<void>
{
/*await */ _dali.backend.calendar_change(
calendar_id,
{
"name": calendar_object.name,
"hue": calendar_object.hue,
"access": {
"public": calendar_object.access.public,
"default_level": _dali.access_level_encode(calendar_object.access.default_level),
"attributed_group": (
lib_plankton.map.dump(calendar_object.access.attributed_group)
.map(
(pair) => ({
"group_id": pair.key,
"level": _dali.access_level_encode(pair.value),
})
)
),
"attributed_user": (
lib_plankton.map.dump(calendar_object.access.attributed_user)
.map(
(pair) => ({
"user_id": pair.key,
"level": _dali.access_level_encode(pair.value),
})
)
),
},
}
);
const calendar_object_reduced : _dali.type_calendar_object_reduced = {
"name": calendar_object.name,
"hue": calendar_object.hue,
/**
* @todo
*/
"access_level": _dali.enum_access_level.admin,
};
_state.calendars.set(
calendar_id,
{
"reduced": calendar_object_reduced,
"complete": calendar_object,
}
);
{
lib_plankton.map.clear(_state.events);
lib_plankton.set.clear(_state.covered_dates);
notify_reset();
}
}
/**
*/
export async function calendar_remove(
calendar_id : _dali.type_calendar_id
)
: Promise<void>
{
/*await */ _dali.backend.calendar_remove(calendar_id);
_state.calendars.delete(calendar_id);
{
lib_plankton.map.clear(_state.events);
lib_plankton.set.clear(_state.covered_dates);
notify_reset();
}
}
/**
* @todo do NOT export?
* @todo clear?
* @todo heed calendar_ids
* @todo mutex
* @todo only update outside timeframe
*/
export async function sync_events(
timeframe : {
from : lib_plankton.pit.type_pit;
to : lib_plankton.pit.type_pit;
},
{
"calendar_ids": calendar_ids = null,
}
:
{
calendar_ids ?: (null | Array<_dali.type_calendar_id>);
}
=
{
}
)
: Promise<void>
{
const queried_dates : lib_plankton.set.type_set<lib_plankton.pit.type_date> = timeframe_to_date_set(
timeframe
);
const new_dates : lib_plankton.set.type_set<lib_plankton.pit.type_date> = lib_plankton.set.difference(
() => make_date_set(),
queried_dates,
_state.covered_dates
);
if (lib_plankton.set.empty(new_dates))
{
// do nothing
}
else
{
const result = await _dali.backend.events(
timeframe.from,
timeframe.to,
{
"calendar_ids": calendar_ids,
}
);
result.forEach(
entry => {
_state.events.set(
entry.hash,
{
"key": entry.hash,
"calendar_id": entry.calendar_id,
"calendar_name": entry.calendar_name,
"hue": entry.hue,
"access_level": _dali.access_level_decode(entry.access_level),
"event_id": entry.event_id,
"event_object": entry.event_object
}
);
}
);
_state.covered_dates = lib_plankton.set.union(
() => make_date_set(),
_state.covered_dates,
new_dates
);
}
}
/**
*/
export function event_list(
)
: Promise<Array<_dali.type_event_object_extended>>
{
return Promise.resolve(lib_plankton.map.values(_state.events));
}
/**
*/
export async function event_get(
event_key : _dali.type_event_key
)
: Promise<_dali.type_event_object_extended>
{
return Promise.resolve(_state.events.get(event_key));
}
/**
*/
export async function event_add(
calendar_id : _dali.type_calendar_id,
event_object : _dali.type_event_object
)
: Promise<void>
{
/**
* @todo do NOT wait?
*/
const result = await _dali.backend.calendar_event_add(
calendar_id,
event_object
);
const event_id : _dali.type_local_resource_event_id = result.local_resource_event_id;
const event_key : _dali.type_event_key = result.hash;
const value = _state.calendars.get(calendar_id);
const calendar_object_reduced : _dali.type_calendar_object_reduced = value.reduced;
_state.events.set(
event_key,
{
"key": event_key,
"calendar_id": calendar_id,
"calendar_name": calendar_object_reduced.name,
"hue": calendar_object_reduced.hue,
"access_level": calendar_object_reduced.access_level,
"event_id": event_id,
"event_object": event_object,
}
);
}
/**
*/
export async function event_change(
event_key : _dali.type_event_key,
event_object : _dali.type_event_object
)
: Promise<void>
{
const event_object_extended_old : _dali.type_event_object_extended = _state.events.get(event_key);
const event_object_extended_new : _dali.type_event_object_extended = {
"key": event_object_extended_old.key,
"calendar_id": event_object_extended_old.calendar_id,
"calendar_name": event_object_extended_old.calendar_name,
"hue": event_object_extended_old.hue,
"access_level": event_object_extended_old.access_level,
"event_id": event_object_extended_old.event_id,
"event_object": event_object,
};
_state.events.set(
event_key,
event_object_extended_new
);
/*await */_dali.backend.calendar_event_change(
event_object_extended_old.calendar_id,
event_object_extended_old.event_id,
event_object
);
}
/**
*/
export async function event_remove(
event_key : _dali.type_event_key
)
: Promise<void>
{
const event_object_extended : _dali.type_event_object_extended = _state.events.get(event_key);
_state.events.delete(event_key);
/*await */_dali.backend.calendar_event_remove(
event_object_extended.calendar_id,
event_object_extended.event_id
);
}
/**
*/
export function listen_reset(
action : ((priviliged ?: boolean) => Promise<void>)
)
: void
{
_listeners_reset.push(action);
}
/**
*/
async function notify_reset(
priviliged ?: boolean
)
: Promise<void>
{
for (const action of _listeners_reset)
{
await action(priviliged);
}
}
/**
*/
export async function initialize(
)
: Promise<void>
{
_state = {
"groups": [],
"users": [],
"calendars": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
calendar_id => calendar_id.toFixed(0)
)
),
"events": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
event_key => event_key
)
),
"covered_dates": make_date_set(
),
};
_dali.listen_login(
async () => {
_state.groups = (
(await _dali.backend.group_list())
.map(
entry => (
{
"id": entry.id,
"object": {
"name": entry.name,
"label": entry.label,
}
}
)
)
);
_state.users = await _dali.backend.user_list();
await sync_calendars();
lib_plankton.map.clear(_state.events);
lib_plankton.set.clear(_state.covered_dates);
notify_reset(true);
}
);
_dali.listen_logout(
async () => {
_state.groups = [];
_state.users = [];
await sync_calendars();
lib_plankton.map.clear(_state.events);
lib_plankton.set.clear(_state.covered_dates);
notify_reset(false);
}
);
await sync_calendars();
// await sync_events();
}
}

100
source/overlay.ts Normal file
View file

@ -0,0 +1,100 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.overlay
{
/**
*/
function get_container_element(
)
: HTMLElement
{
return document.querySelector("#overlay");
}
/**
*/
export function get_content_element(
)
: HTMLElement
{
return document.querySelector("#overlay_content");
}
/**
*/
export function clear(
)
: void
{
get_content_element().innerHTML = "";
}
/**
*/
export function toggle(
{
"mode": mode = null,
}
:
{
mode ?: (null | boolean);
}
=
{
}
)
: void
{
get_container_element().classList.toggle("overlay_active", mode ?? undefined);
}
/**
*/
export function initialize(
)
: Promise<void>
{
clear();
const container_element : HTMLElement = get_container_element();
/*
container_element.addEventListener(
"click",
(event) => {
if (event.target == container_element)
{
toggle({"mode": false});
}
else
{
// do nothing
}
}
);
*/
return Promise.resolve<void>(undefined);
}
}

View file

@ -1,168 +0,0 @@
namespace _zeitbild.frontend_web.pages
{
/**
*/
lib_plankton.zoo_page.register(
"calendar_add",
async (parameters, target_element) => {
target_element.innerHTML = "";
target_element.innerHTML = await _zeitbild.frontend_web.helpers.template_coin(
"calendar_add",
"default",
{
"label": lib_plankton.translate.get("page.calendar_add.title")
}
);
const form : lib_plankton.zoo_form.class_form<
_zeitbild.frontend_web.type.calendar_object,
{
name : string;
access : {
public : boolean;
default_level : _zeitbild.frontend_web.type.enum_access_level;
attributed : lib_plankton.map.type_map<
_zeitbild.frontend_web.type.user_id,
_zeitbild.frontend_web.type.enum_access_level
>;
};
resource_kind : string;
}
> = new lib_plankton.zoo_form.class_form<
_zeitbild.frontend_web.type.calendar_object,
{
name : string;
access : {
public : boolean;
default_level : _zeitbild.frontend_web.type.enum_access_level;
attributed : lib_plankton.map.type_map<
_zeitbild.frontend_web.type.user_id,
_zeitbild.frontend_web.type.enum_access_level
>;
};
resource_kind : string;
}
>(
(calendar_object) => ({
"name": calendar_object.name,
"access": calendar_object.access,
"resource_kind": calendar_object.resource.kind,
}),
(raw) => ({
"name": raw.name,
"access": raw.access,
"resource": (() => {
switch (raw.resource_kind) {
case "local": {
return {
"kind": "local",
"data": {
"events": [],
}
};
break;
}
case "caldav": {
return {
"kind": "caldav",
"data": {
"url": "", // TODO
"read_only": true, // TODO
}
};
break;
}
default: {
throw (new Error("invalid resource kind: " + raw.resource_kind));
break;
}
}
}) (),
}),
new lib_plankton.zoo_input.class_input_group<any>(
[
{
"name": "name",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": lib_plankton.translate.get("calendar.name")
},
{
"name": "access",
"input": new lib_plankton.zoo_input.class_input_group(
[
{
"name": "public",
"input": new lib_plankton.zoo_input.class_input_checkbox(),
"label": lib_plankton.translate.get("calendar.access.public"),
},
{
"name": "default_level",
"input": _zeitbild.frontend_web.helpers.input_access_level(),
"label": lib_plankton.translate.get("calendar.access.default_level"),
},
{
"name": "attributed",
"input": await _zeitbild.frontend_web.helpers.input_attributed_access(),
"label": lib_plankton.translate.get("calendar.access.attributed"),
},
]
),
"label": lib_plankton.translate.get("calendar.access.access"),
},
{
"name": "resource_kind",
"input": new lib_plankton.zoo_input.class_input_selection(
[
{
"value": "local",
"label": lib_plankton.translate.get("resource.kinds.local.title")
},
{
"value": "caldav",
"label": lib_plankton.translate.get("resource.kinds.caldav.title")
},
]
),
"label": lib_plankton.translate.get("resource.kind")
},
]
),
[
{
"label": lib_plankton.translate.get("page.calendar_add.actions.do"),
"target": "submit",
"procedure": async (get_value, get_representation) => {
const value : any = await get_value();
try {
await _zeitbild.frontend_web.backend.calendar_add(
value
);
lib_plankton.zoo_page.set(
{
"name": "overview",
"parameters": {}
}
);
}
catch (error) {
// do nothing
/*
lib_plankton.zoo_page.set(
{
"name": "event_add",
"parameters": {
}
}
);
*/
}
}
},
]
);
await form.setup(document.querySelector("#calendar_add_form"));
return Promise.resolve<void>(undefined);
}
);
}

View file

@ -1,5 +0,0 @@
<div id="calendar_add">
<h2>{{label}}</h2>
<div id="calendar_add_form">
</div>
</div>

View file

@ -1,171 +0,0 @@
namespace _zeitbild.frontend_web.pages
{
/**
*/
lib_plankton.zoo_page.register(
"calendar_edit",
async (parameters, target_element) => {
const read_only : boolean = ((parameters["read_only"] ?? "yes") === "yes");
const calendar_id : int = parseInt(parameters["calendar_id"]);
target_element.innerHTML = "";
target_element.innerHTML = await _zeitbild.frontend_web.helpers.template_coin(
"calendar_edit",
"default",
{
"label": lib_plankton.translate.get("page.calendar_edit.title.regular")
}
);
const form : lib_plankton.zoo_form.class_form<
_zeitbild.frontend_web.type.calendar_object,
{
name : string;
access : {
public : boolean;
default_level : _zeitbild.frontend_web.type.enum_access_level;
attributed : lib_plankton.map.type_map<
_zeitbild.frontend_web.type.user_id,
_zeitbild.frontend_web.type.enum_access_level
>;
};
}
> = new lib_plankton.zoo_form.class_form<
_zeitbild.frontend_web.type.calendar_object,
{
name : string;
access : {
public : boolean;
default_level : _zeitbild.frontend_web.type.enum_access_level;
attributed : lib_plankton.map.type_map<
_zeitbild.frontend_web.type.user_id,
_zeitbild.frontend_web.type.enum_access_level
>;
};
}
>(
(calendar_object) => ({
"name": calendar_object.name,
"access": calendar_object.access,
}),
(raw) => ({
"name": raw.name,
"access": raw.access,
"resource": {
"kind": "local",
"data": {
"events": [],
}
},
}),
new lib_plankton.zoo_input.class_input_group<any>(
[
{
"name": "name",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": lib_plankton.translate.get("calendar.name")
},
{
"name": "access",
"input": new lib_plankton.zoo_input.class_input_group(
[
{
"name": "public",
"input": new lib_plankton.zoo_input.class_input_checkbox(),
"label": lib_plankton.translate.get("calendar.access.public"),
},
{
"name": "default_level",
"input": _zeitbild.frontend_web.helpers.input_access_level(),
"label": lib_plankton.translate.get("calendar.access.default_level"),
},
{
"name": "attributed",
"input": await _zeitbild.frontend_web.helpers.input_attributed_access(),
"label": lib_plankton.translate.get("calendar.access.attributed"),
},
]
),
"label": lib_plankton.translate.get("calendar.access.access"),
},
]
),
(
read_only
?
[
]
:
[
{
"label": lib_plankton.translate.get("page.calendar_edit.actions.change"),
"target": "submit",
"procedure": async (get_value, get_representation) => {
const value : any = await get_value();
try {
await _zeitbild.frontend_web.backend.calendar_change(
calendar_id,
value
);
lib_plankton.zoo_page.set(
{
"name": "overview",
"parameters": {}
}
);
}
catch (error) {
// do nothing
/*
lib_plankton.zoo_page.set(
{
"name": "event_add",
"parameters": {
}
}
);
*/
}
}
},
{
"label": lib_plankton.translate.get("page.calendar_edit.actions.remove"),
"target": "submit",
"procedure": async (get_value, get_representation) => {
try {
await _zeitbild.frontend_web.backend.calendar_remove(
calendar_id
);
lib_plankton.zoo_page.set(
{
"name": "overview",
"parameters": {}
}
);
}
catch (error) {
// do nothing
/*
lib_plankton.zoo_page.set(
{
"name": "event_add",
"parameters": {
}
}
);
*/
}
}
},
]
)
);
await form.setup(document.querySelector("#calendar_edit_form"));
const calendar_object : _zeitbild.frontend_web.type.calendar_object = await _zeitbild.frontend_web.backend.calendar_get(
calendar_id
);
await form.input_write(calendar_object);
return Promise.resolve<void>(undefined);
}
);
}

View file

@ -1,5 +0,0 @@
<div id="calendar_edit">
<h2>{{label}}</h2>
<div id="calendar_edit_form">
</div>
</div>

View file

@ -1,234 +0,0 @@
namespace _zeitbild.frontend_web.pages
{
/**
*/
lib_plankton.zoo_page.register(
"event_add",
async (parameters, target_element) => {
const calendar_id : (null | int) = (
("calendar_id" in parameters)
?
parseInt(parameters["calendar_id"])
:
null
);
const year : (null | int) = (
("year" in parameters)
?
parseInt(parameters["year"])
:
null
);
const month : (null | int) = (
("month" in parameters)
?
parseInt(parameters["month"])
:
null
);
const day : (null | int) = (
("day" in parameters)
?
parseInt(parameters["day"])
:
null
);
const date : lib_plankton.pit.type_date = (
(
(year !== null)
&&
(month !== null)
&&
(day !== null)
)
?
{
"year": year,
"month": month,
"day": day,
}
:
lib_plankton.pit.to_datetime(lib_plankton.pit.now()).date
);
target_element.innerHTML = "";
target_element.innerHTML = await _zeitbild.frontend_web.helpers.template_coin(
"event_add",
"default",
{
"label": lib_plankton.translate.get("page.event_add.title")
}
);
const form : lib_plankton.zoo_form.class_form<
{
calendar_id : _zeitbild.frontend_web.type.calendar_id;
event_object : _zeitbild.frontend_web.type.event_object;
},
{
calendar_id : string;
name : string;
begin : lib_plankton.pit.type_datetime;
end : (null | lib_plankton.pit.type_datetime);
location : (null | string);
description : (null | string);
}
> = new lib_plankton.zoo_form.class_form<
{
calendar_id : _zeitbild.frontend_web.type.calendar_id;
event_object : _zeitbild.frontend_web.type.event_object;
},
{
calendar_id : string;
name : string;
begin : lib_plankton.pit.type_datetime;
end : (null | lib_plankton.pit.type_datetime);
location : (null | string);
description : (null | string);
}
>(
(value) => ({
"calendar_id": value.calendar_id.toFixed(0),
"name": value.event_object.name,
"begin": value.event_object.begin,
"end": value.event_object.end,
"location": value.event_object.location,
"description": value.event_object.description,
}),
(representation) => ({
"calendar_id": parseInt(representation.calendar_id),
"event_object": {
"name": representation.name,
"begin": representation.begin,
"end": representation.end,
"location": representation.location,
"description": representation.description,
}
}),
new lib_plankton.zoo_input.class_input_group<any>(
[
{
"name": "calendar_id",
"input": new lib_plankton.zoo_input.class_input_selection(
(
(await _zeitbild.frontend_web.backend.calendar_list())
.filter(
(entry) => (
(entry.access_level === _zeitbild.frontend_web.type.enum_access_level.edit)
||
(entry.access_level === _zeitbild.frontend_web.type.enum_access_level.admin)
)
)
.map(
(entry) => ({
"value": entry.id.toFixed(0),
"label": entry.name,
})
)
)
),
"label": lib_plankton.translate.get("calendar.calendar")
},
{
"name": "name",
"input": new lib_plankton.zoo_input.class_input_text(
),
"label": lib_plankton.translate.get("event.name")
},
{
"name": "begin",
"input": new lib_plankton.zoo_input.class_input_datetime(
{
"label_timezone_shift": lib_plankton.translate.get("common.timezone_shift"),
"label_date": lib_plankton.translate.get("common.date"),
"label_time": lib_plankton.translate.get("common.time"),
}
),
"label": lib_plankton.translate.get("event.begin")
},
{
"name": "end",
"input": new lib_plankton.zoo_input.class_input_soft<lib_plankton.pit.type_datetime>(
new lib_plankton.zoo_input.class_input_datetime(
{
"label_timezone_shift": lib_plankton.translate.get("common.timezone_shift"),
"label_date": lib_plankton.translate.get("common.date"),
"label_time": lib_plankton.translate.get("common.time"),
}
)
),
"label": lib_plankton.translate.get("event.end")
},
{
"name": "location",
"input": new lib_plankton.zoo_input.class_input_soft<string>(
new lib_plankton.zoo_input.class_input_text(
)
),
"label": lib_plankton.translate.get("event.location")
},
{
"name": "description",
"input": new lib_plankton.zoo_input.class_input_soft<string>(
new lib_plankton.zoo_input.class_input_textarea(
)
),
"label": lib_plankton.translate.get("event.description")
},
]
),
[
{
"label": lib_plankton.translate.get("page.event_add.actions.do"),
"target": "submit",
"procedure": async (get_value, get_representation) => {
const value : any = await get_value();
try {
await _zeitbild.frontend_web.backend.calendar_event_add(
value.calendar_id,
value.event_object
);
lib_plankton.zoo_page.set(
{
"name": "overview",
"parameters": {}
}
);
}
catch (error) {
// do nothing
/*
lib_plankton.zoo_page.set(
{
"name": "event_add",
"parameters": {
}
}
);
*/
}
}
},
]
);
await form.setup(document.querySelector("#event_add_form"));
await form.input_write(
{
"calendar_id": (calendar_id ?? 0),
"event_object": {
"name": "",
"begin": {
"timezone_shift": 0,
"date": date,
"time": null
},
"end": null,
"location": null,
"description": null,
}
}
);
return Promise.resolve<void>(undefined);
}
);
}

View file

@ -1,5 +0,0 @@
<div id="event_add">
<h2>{{label}}</h2>
<div id="event_add_form">
</div>
</div>

View file

@ -1,195 +0,0 @@
namespace _zeitbild.frontend_web.pages
{
/**
*/
lib_plankton.zoo_page.register(
"event_edit",
async (parameters, target_element) => {
const read_only : boolean = ((parameters["read_only"] ?? "yes") === "yes");
const calendar_id : int = parseInt(parameters["calendar_id"]);
const event_id : int = parseInt(parameters["event_id"]);
target_element.innerHTML = "";
target_element.innerHTML = await _zeitbild.frontend_web.helpers.template_coin(
"event_edit",
"default",
{
"label": (
read_only
?
lib_plankton.translate.get("page.event_edit.title.read_only")
:
lib_plankton.translate.get("page.event_edit.title.regular")
)
}
);
const form : lib_plankton.zoo_form.class_form<
_zeitbild.frontend_web.type.event_object,
{
name : string;
begin : lib_plankton.pit.type_datetime;
end : (null | lib_plankton.pit.type_datetime);
location : (null | string);
description : (null | string);
}
> = new lib_plankton.zoo_form.class_form<
_zeitbild.frontend_web.type.event_object,
{
name : string;
begin : lib_plankton.pit.type_datetime;
end : (null | lib_plankton.pit.type_datetime);
location : (null | string);
description : (null | string);
}
>(
(value) => ({
"name": value.name,
"begin": value.begin,
"end": value.end,
"location": value.location,
"description": value.description,
}),
(representation) => ({
"name": representation.name,
"begin": representation.begin,
"end": representation.end,
"location": representation.location,
"description": representation.description,
}),
new lib_plankton.zoo_input.class_input_group<any>(
[
{
"name": "name",
"input": new lib_plankton.zoo_input.class_input_text(
),
"label": lib_plankton.translate.get("event.name")
},
{
"name": "begin",
"input": new lib_plankton.zoo_input.class_input_datetime(
{
"label_timezone_shift": lib_plankton.translate.get("common.timezone_shift"),
"label_date": lib_plankton.translate.get("common.date"),
"label_time": lib_plankton.translate.get("common.time"),
}
),
"label": lib_plankton.translate.get("event.begin")
},
{
"name": "end",
"input": new lib_plankton.zoo_input.class_input_soft<lib_plankton.pit.type_datetime>(
new lib_plankton.zoo_input.class_input_datetime(
{
"label_timezone_shift": lib_plankton.translate.get("common.timezone_shift"),
"label_date": lib_plankton.translate.get("common.date"),
"label_time": lib_plankton.translate.get("common.time"),
}
)
),
"label": lib_plankton.translate.get("event.end")
},
{
"name": "location",
"input": new lib_plankton.zoo_input.class_input_soft<string>(
new lib_plankton.zoo_input.class_input_text(
)
),
"label": lib_plankton.translate.get("event.location")
},
{
"name": "description",
"input": new lib_plankton.zoo_input.class_input_soft<string>(
new lib_plankton.zoo_input.class_input_textarea(
)
),
"label": lib_plankton.translate.get("event.description")
},
]
),
(
read_only
?
[
]
:
[
{
"label": lib_plankton.translate.get("page.event_edit.actions.change"),
"target": "submit",
"procedure": async (get_value, get_representation) => {
const value : any = await get_value();
try {
await _zeitbild.frontend_web.backend.calendar_event_change(
calendar_id,
event_id,
value
);
lib_plankton.zoo_page.set(
{
"name": "overview",
"parameters": {}
}
);
}
catch (error) {
lib_plankton.log.warning("page_event_edit_error", {"error": String(error)});
// do nothing
/*
lib_plankton.zoo_page.set(
{
"name": "event_add",
"parameters": {
}
}
);
*/
}
}
},
{
"label": lib_plankton.translate.get("page.event_edit.actions.remove"),
"target": "submit",
"procedure": async (get_value, get_representation) => {
try {
await _zeitbild.frontend_web.backend.calendar_event_remove(
calendar_id,
event_id
);
lib_plankton.zoo_page.set(
{
"name": "overview",
"parameters": {}
}
);
}
catch (error) {
lib_plankton.log.warning("page_event_edit_error", {"error": String(error)});
// do nothing
/*
lib_plankton.zoo_page.set(
{
"name": "event_add",
"parameters": {
}
}
);
*/
}
}
},
]
)
);
await form.setup(document.querySelector("#event_edit_form"));
const event_object : _zeitbild.frontend_web.type.event_object = await _zeitbild.frontend_web.backend.calendar_event_get(
calendar_id,
event_id
);
await form.input_write(
event_object
);
return Promise.resolve<void>(undefined);
}
);
}

View file

@ -1,5 +0,0 @@
<div id="event_edit">
<h2>{{label}}</h2>
<div id="event_edit_form">
</div>
</div>

View file

@ -1,111 +0,0 @@
namespace _zeitbild.frontend_web.pages
{
/**
*/
lib_plankton.zoo_page.register(
"login",
async (parameters, target_element) => {
target_element.innerHTML = "";
const preparation : {kind : string; data : any;} = await _zeitbild.frontend_web.backend.session_prepare(
{
"oidc_redirect_uri_template": _zeitbild.frontend_web.conf.get()["misc"]["oidc_redirect_uri_template"],
}
);
switch (preparation.kind) {
case "internal": {
target_element.innerHTML = await _zeitbild.frontend_web.helpers.template_coin(
"login",
"default",
{
}
);
const form : lib_plankton.zoo_form.class_form<
{name : string; password : string;},
{name : string; password : string;}
> = new lib_plankton.zoo_form.class_form<
{name : string; password : string;},
{name : string; password : string;}
>(
x => x,
x => x,
new lib_plankton.zoo_input.class_input_group<
{name : string; password : string;}
>(
[
{
"name": "name",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": lib_plankton.translate.get("page.login.internal.name"),
},
{
"name": "password",
"input": new lib_plankton.zoo_input.class_input_password(),
"label": lib_plankton.translate.get("page.login.internal.password"),
},
]
),
[
{
"label": lib_plankton.translate.get("page.login.internal.do"),
"target": "submit",
"procedure": async (get_value, get_representation) => {
const value : any = await get_value();
try {
await _zeitbild.frontend_web.backend.session_begin(
value.name,
value.password
);
lib_plankton.zoo_page.set(
{
"name": "overview",
"parameters": {}
}
);
}
catch (error) {
lib_plankton.zoo_page.set(
{
"name": "login",
"parameters": {
"name": value.name,
}
}
);
}
}
},
]
);
await form.setup(document.querySelector("#login"));
await form.input_write(
{
"name": (parameters.name ?? ""),
"password": "",
}
);
break;
}
case "oidc": {
let element_a : HTMLElement = document.createElement("a");;
element_a.textContent = lib_plankton.string.coin(
lib_plankton.translate.get("page.login.oidc.via"),
{
"title": preparation.data.label,
}
);
element_a.setAttribute("href", preparation.data.url);
target_element.innerHTML = "";
target_element.appendChild(element_a);
break;
}
default: {
break;
}
}
return Promise.resolve<void>(undefined);
}
);
}

View file

@ -1,2 +0,0 @@
<div id="login">
</div>

View file

@ -1,23 +0,0 @@
namespace _zeitbild.frontend_web.pages
{
/**
*/
lib_plankton.zoo_page.register(
"logout",
async (parameters, target_element) => {
target_element.innerHTML = "";
await _zeitbild.frontend_web.backend.session_end(
);
lib_plankton.zoo_page.set(
{
"name": "overview",
"parameters": {
}
}
);
return Promise.resolve<void>(undefined);
}
);
}

View file

@ -1,21 +0,0 @@
namespace _zeitbild.frontend_web.pages
{
/**
*/
lib_plankton.zoo_page.register(
"oidc_finish",
async (parameters, target_element) => {
target_element.innerHTML = "";
await _zeitbild.frontend_web.backend.set_session_key(parameters["session_key"]);
lib_plankton.zoo_page.set(
{
"name": "overview",
"parameters": {}
}
);
return Promise.resolve<void>(undefined);
}
);
}

View file

@ -1,195 +0,0 @@
namespace _zeitbild.frontend_web.pages.overview
{
/**
*/
lib_plankton.zoo_page.register(
"overview",
async (parameters, target_element) => {
// params
const compact : boolean = (
(
parameters["compact"]
??
(
(window.innerWidth >= 800)
?
"no"
:
"yes"
)
)
===
"yes"
);
// exec
target_element.innerHTML = await _zeitbild.frontend_web.helpers.template_coin(
"overview",
"default",
{
}
);
target_element.querySelector("#overview").classList.toggle("compact", compact);
// hint
{
if (! await _zeitbild.frontend_web.backend.is_logged_in()) {
target_element.querySelector("#overview-head").textContent = lib_plankton.translate.get("page.overview.login_hint");
}
else {
// do nothing
}
}
// sources
{
const data : Array<
{
id : _zeitbild.frontend_web.type.calendar_id;
name : string;
access_level : _zeitbild.frontend_web.type.enum_access_level;
}
> = await _zeitbild.frontend_web.backend.calendar_list(
);
const widget_sources = new _zeitbild.frontend_web.widgets.sources.class_widget_sources(
data,
{
"action_select": (entry) => {
switch (entry.access_level) {
case _zeitbild.frontend_web.type.enum_access_level.none: {
throw (new Error("this event should not be visible"));
break;
}
case _zeitbild.frontend_web.type.enum_access_level.edit:
case _zeitbild.frontend_web.type.enum_access_level.view: {
lib_plankton.zoo_page.set(
{
"name": "calendar_edit",
"parameters": {
"read_only": "yes",
"calendar_id": entry.id,
}
}
);
break;
}
case _zeitbild.frontend_web.type.enum_access_level.admin: {
lib_plankton.zoo_page.set(
{
"name": "calendar_edit",
"parameters": {
"read_only": "no",
"calendar_id": entry.id,
}
}
);
break;
}
}
},
}
);
if (compact) {
// do nothing
}
else {
await widget_sources.load(target_element.querySelector("#overview-pane-left"));
}
}
// view
{
const get_entries = (from_pit, to_pit, calendar_ids) => _zeitbild.frontend_web.backend.events(
from_pit,
to_pit,
{
"calendar_ids": calendar_ids,
}
);
const action_select_event = (calendar_id, access_level, event_id) => {
switch (access_level) {
case _zeitbild.frontend_web.type.enum_access_level.none: {
throw (new Error("this event should not be visible"));
break;
}
case _zeitbild.frontend_web.type.enum_access_level.view: {
lib_plankton.zoo_page.set(
{
"name": "event_edit",
"parameters": {
"read_only": "yes",
"calendar_id": calendar_id,
"event_id": event_id,
}
}
);
break;
}
case _zeitbild.frontend_web.type.enum_access_level.edit:
case _zeitbild.frontend_web.type.enum_access_level.admin: {
lib_plankton.zoo_page.set(
{
"name": "event_edit",
"parameters": {
"read_only": "no",
"calendar_id": calendar_id,
"event_id": event_id,
}
}
);
break;
}
}
};
const widget = (
compact
?
(
new _zeitbild.frontend_web.widgets.listview.class_widget_listview(
get_entries,
{
"action_select_event": action_select_event,
"action_add": () => {
lib_plankton.zoo_page.set(
{
"name": "event_add",
"parameters": {
"calendar_id": null,
"year": null,
"month": null,
"day": null,
}
}
);
},
}
)
)
:
(
new _zeitbild.frontend_web.widgets.weekview.class_widget_weekview(
get_entries,
{
"action_select_event": action_select_event,
"action_select_day": (date) => {
lib_plankton.zoo_page.set(
{
"name": "event_add",
"parameters": {
"calendar_id": null,
"year": date.year,
"month": date.month,
"day": date.day,
}
}
);
},
}
)
)
);
await widget.load(target_element.querySelector("#overview-pane-right"));
}
return Promise.resolve<void>(undefined);
},
);
}

View file

@ -1,10 +0,0 @@
<div id="overview">
<div id="overview-head">
</div>
<div id="overview-body">
<div id="overview-pane-left">
</div>
<div id="overview-pane-right">
</div>
</div>
</div>

799
source/resources/backend.ts Normal file
View file

@ -0,0 +1,799 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
/**
*/
namespace _dali.backend
{
/**
*/
type type_conf = {
scheme : string;
host : string;
port : int;
path : string;
};
/**
*/
type type_request = {
method : lib_plankton.http.enum_method;
action : string;
input : (null | any);
};
/**
*/
type type_event_object = {
name : string;
begin : {
timezone_shift: int;
date: {
year: int;
month: int;
day: int;
};
time: (
null
|
{
hour: int;
minute: int;
second: int;
}
);
};
end : (
null
|
{
timezone_shift: int;
date: {
year: int;
month: int;
day: int;
};
time: (
null
|
{
hour: int;
minute: int;
second: int;
}
);
}
);
location : (
null
|
string
);
link : (
null
|
string
);
description : (
null
|
string
);
};
/**
*/
var _conf : type_conf;
/**
*/
var _data_chest : (
null
|
lib_plankton.storage.type_chest<string, string, void, string, string>
) = null;
/**
*/
var _cache : (
null
|
lib_plankton.cache.type_subject<boolean>
);
/**
*/
var _queue : lib_plankton.call.type_queue<type_request, any>;
/**
*/
export async function initialize(
conf : type_conf
)
: Promise<void>
{
_conf = conf;
_data_chest = lib_plankton.storage.localstorage.implementation_chest(
{
"corner": "zeitbild",
}
);
_cache = lib_plankton.cache.make<boolean>(
/*
lib_plankton.storage.memory.implementation_chest(
{
}
)
*/
);
_queue = lib_plankton.call.queue_make<type_request, any>(
call_real
);
return Promise.resolve<void>(undefined);
}
/**
*/
async function get_session_key(
)
: Promise<(null | string)>
{
try
{
return (await _data_chest.read("session_key"));
}
catch (error)
{
return null;
}
}
/**
*/
async function call_real(
request : type_request
)
: Promise<any>
{
const with_body : boolean = (
[
lib_plankton.http.enum_method.post,
lib_plankton.http.enum_method.put,
lib_plankton.http.enum_method.patch,
].includes(request.method)
);
const session_key : (null | string) = await get_session_key();
const http_request : lib_plankton.http.type_request = {
"version": "HTTP/2",
"scheme": (
(_conf.scheme === "http")
?
"http"
:
"https"
),
"host": lib_plankton.string.coin(
"{{host}}:{{port}}",
{
"host": _conf.host,
"port": _conf.port.toFixed(0),
}
),
"path": lib_plankton.string.coin(
"{{base}}{{action}}",
{
"base": _conf.path,
"action": request.action,
}
),
"method": request.method,
"query": (
(with_body || (request.input === null))
?
null
:
("?" + lib_plankton.www_form.encode(request.input))
),
"headers": Object.assign(
{},
(
(! with_body)
?
{}
:
{"Content-Type": "application/json"}
),
(
(session_key === null)
?
{}
:
{"X-Session-Key": session_key}
)
),
"body": (
((! with_body) || (request.input === null))
?
null
:
/*Buffer.from*/(lib_plankton.json.encode(request.input))
),
};
const http_response : lib_plankton.http.type_response = await lib_plankton.http.call(http_request);
if (
! (
(http_response.status_code >= 200)
&&
(http_response.status_code < 300)
)
)
{
return Promise.reject<any>(http_response.body.toString());
}
else
{
const output : any = lib_plankton.json.decode(http_response.body.toString());
return Promise.resolve<any>(output);
}
}
/**
*/
async function call(
method : lib_plankton.http.enum_method,
action : string,
input : (null | any)
)
: Promise<any>
{
const request : type_request = {
"method": method,
"action": action,
"input": input,
};
return (
new Promise<any>(
(resolve, reject) => {
lib_plankton.call.queue_add<type_request, any>(
_queue,
request,
resolve,
reject
);
}
)
);
}
/**
*/
export function status(
)
: Promise<
{
logged_in : boolean;
name : (null | string);
}
>
{
return call(
lib_plankton.http.enum_method.get,
"/session/status",
null
)
}
/**
*/
export async function session_prepare(
input : any
)
: Promise<{kind : string; data : any;}>
{
return call(
lib_plankton.http.enum_method.post,
"/session/prepare",
input
);
}
/**
*/
export function set_session_key(
session_key : string
)
: Promise<void>
{
return (
_data_chest.write("session_key", session_key)
.then<void>(() => Promise.resolve<void>(undefined))
);
}
/**
*/
export async function session_begin(
name : string,
password : string
)
: Promise<void>
{
const session_key : string = await call(
lib_plankton.http.enum_method.post,
"/session/begin",
{
"name": name,
"password": password,
}
);
await _data_chest.write("session_key", session_key);
return Promise.resolve<void>(undefined);
}
/**
*/
export async function session_end(
)
: Promise<void>
{
await call(
lib_plankton.http.enum_method.delete,
"/session/end",
null
);
await _data_chest.delete("session_key");
return Promise.resolve<void>(undefined);
}
/**
*/
export function group_list(
)
: Promise<
Array<
{
id : int;
name : string;
label : string;
}
>
>
{
return call(
lib_plankton.http.enum_method.get,
"/groups",
null
);
}
/**
*/
export function user_list(
)
: Promise<
Array<
{
id : int;
name : string;
}
>
>
{
return call(
lib_plankton.http.enum_method.get,
"/users",
null
);
}
/**
*/
export function user_dav_conf(
)
: Promise<
(
null
|
{
address : string;
username : string;
password : string;
setup_hints : Array<
{
label : string;
link : string;
remark : (null | string);
}
>;
}
)
>
{
return call(
lib_plankton.http.enum_method.get,
"/user_dav_conf",
null
);
}
/**
*/
export function user_dav_token(
)
: Promise<void>
{
return call(
lib_plankton.http.enum_method.patch,
"/user_dav_token",
null
);
}
/**
*/
export async function calendar_list(
)
: Promise<
Array<
{
id : int;
name : string;
hue : float;
access_level : string;
}
>
>
{
return call(
lib_plankton.http.enum_method.get,
"/calendar",
null
);
}
/**
*/
export async function calendar_get(
calendar_id : int
)
: Promise<
{
name : string;
hue : float;
access : {
public : boolean;
default_level : string;
attributed_group : Array<
{
group_id : int;
level : string;
}
>;
attributed_user : Array<
{
user_id : int;
level : string;
}
>;
};
resource_id : int;
}
>
{
return call(
lib_plankton.http.enum_method.get,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}",
{
"calendar_id": calendar_id.toFixed(0),
}
),
null
);
}
/**
*/
export async function calendar_add(
data : {
name : string;
access : {
public : boolean;
default_level : string;
attributed_group : Array<
{
group_id : int;
level : string;
}
>;
attributed_user : Array<
{
user_id : int;
level : string;
}
>;
};
resource : (
{
kind : "local";
data : {
};
}
|
{
kind : "ics_feed";
data : {
url : string;
from_fucked_up_wordpress : boolean;
};
}
);
hue : float;
}
)
: Promise<
int
>
{
return call(
lib_plankton.http.enum_method.post,
lib_plankton.string.coin(
"/calendar",
{
}
),
data
);
}
/**
*/
export async function calendar_change(
id : int,
data : {
name : string;
hue : float;
access : {
public : boolean;
default_level : string;
attributed_group : Array<
{
group_id : int;
level : string;
}
>;
attributed_user : Array<
{
user_id : int;
level : string;
}
>;
};
}
)
: Promise<
void
>
{
return call(
lib_plankton.http.enum_method.put,
lib_plankton.string.coin(
"/calendar/{{id}}",
{
"id": id.toFixed(0),
}
),
data
);
}
/**
*/
export async function calendar_remove(
id : int
)
: Promise<
void
>
{
return call(
lib_plankton.http.enum_method.delete,
lib_plankton.string.coin(
"/calendar/{{id}}",
{
"id": id.toFixed(0),
}
),
null
);
}
/**
*/
export async function calendar_event_get(
calendar_id : int,
event_id : int
)
: Promise<
type_event_object
>
{
return call(
lib_plankton.http.enum_method.get,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}/event/{{event_id}}",
{
"calendar_id": calendar_id.toFixed(0),
"event_id": event_id.toFixed(0),
}
),
null
);
}
/**
*/
export async function calendar_event_add(
calendar_id : int,
event_data : type_event_object
)
: Promise<
{
local_resource_event_id : (null | int);
hash : string;
}
>
{
return call(
lib_plankton.http.enum_method.post,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}/event",
{
"calendar_id": calendar_id.toFixed(0),
}
),
event_data
);
}
/**
* @todo Möglichkeit den Kalender zu ändern
*/
export async function calendar_event_change(
calendar_id : int,
event_id : int,
event_object : type_event_object
)
: Promise<void>
{
return call(
lib_plankton.http.enum_method.put,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}/event/{{event_id}}",
{
"calendar_id": calendar_id.toFixed(0),
"event_id": event_id.toFixed(0),
}
),
event_object
);
}
/**
*/
export async function calendar_event_remove(
calendar_id : int,
event_id : int
)
: Promise<void>
{
return call(
lib_plankton.http.enum_method.delete,
lib_plankton.string.coin(
"/calendar/{{calendar_id}}/event/{{event_id}}",
{
"calendar_id": calendar_id.toFixed(0),
"event_id": event_id.toFixed(0),
}
),
null
);
}
/**
* @todo prevent loops
*/
export async function events(
from_timestamp : int,
to_timestamp : int,
{
"calendar_ids": calendar_ids = null,
}
:
{
calendar_ids ?: (null | Array<int>);
}
=
{
}
)
: Promise<
Array<
{
hash : string;
calendar_id : int;
calendar_name : string;
hue : float;
access_level : string;
event_id : (null | int);
event_object : type_event_object;
}
>
>
{
return call(
lib_plankton.http.enum_method.get,
"/events",
Object.assign(
{
"from": from_timestamp,
"to": to_timestamp,
},
(
(calendar_ids === null)
?
{}
:
{"calendar_ids": calendar_ids.join(",")}
)
)
);
}
}

View file

@ -1,5 +1,25 @@
/*
This file is part of »dali«.
namespace _zeitbild.frontend_web.conf Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.conf
{ {
/** /**
@ -52,6 +72,17 @@ namespace _zeitbild.frontend_web.conf
"type": "string", "type": "string",
"default": "http://localhost:8888/#oidc_finish,session_key={{session_key}}" "default": "http://localhost:8888/#oidc_finish,session_key={{session_key}}"
}, },
"use_central_europe_specific_datetime_inputs": {
"nullable": false,
"type": "boolean",
"default": false
},
"weekview_cell_day_format": {
"nullable": false,
"type": "string",
"default": "d.b",
"description": "available placeholders: Y,m,b,d,W,w (as in UNIX command 'date')"
},
}, },
"required": [ "required": [
], ],
@ -68,7 +99,25 @@ namespace _zeitbild.frontend_web.conf
/** /**
*/ */
var _data : (null | any) = null; type type_data = {
version : string;
backend : {
scheme : string;
host : string;
port : int;
path : string;
};
misc : {
oidc_redirect_uri_template : string;
use_central_europe_specific_datetime_inputs : string;
weekview_cell_day_format : string;
};
};
/**
*/
var _data : (null | type_data) = null;
/** /**
@ -83,9 +132,10 @@ namespace _zeitbild.frontend_web.conf
/** /**
*/ */
export function get( export function get(
) : any ) : type_data
{ {
if (_data === null) { if (_data === null)
{
throw (new Error("conf not loaded yet")); throw (new Error("conf not loaded yet"));
} }
else { else {
@ -100,10 +150,7 @@ namespace _zeitbild.frontend_web.conf
path : string path : string
) : Promise<void> ) : Promise<void>
{ {
_data = await lib_plankton.conf.load( _data = ((await lib_plankton.conf.load(_schema, path)) as type_data);
_schema,
path
);
return Promise.resolve<void>(undefined); return Promise.resolve<void>(undefined);
} }

View file

@ -1,73 +0,0 @@
.tableview-sources-entry:not(.tableview-sources-entry-active)
{
filter: saturate(0);
}
.calendar-cell
{
border: 1px solid hsl(0,0%,37.5%);
padding: 8px;
vertical-align: top;
}
.calendar-cell-day
{
width: 13.5%;
}
.calendar-cell-week
{
width: 5.5%;
}
.calendar-cell-regular
{
width: 13.5%;
height: 120px;
cursor: copy;
}
.calendar-cell-today
{
outline: 2px solid hsl(0, 0%, 100%);
}
.calendar-day
{
font-size: 0.75em;
cursor: help;
}
.calendar-events
{
margin: 0; padding: 0;
list-style-type: none;
}
.calendar-event_entry
{
margin: 4px;
padding: 4px;
border-radius: 2px;
font-size: 0.75em;
color: #FFF;
font-weight: bold;
}
.calendar-event_entry.access_level-none
,
.calendar-event_entry.access_level-view
{
/*
cursor: default;
*/
cursor: pointer;
}
.calendar-event_entry.access_level-edit
,
.calendar-event_entry.access_level-admin
{
cursor: pointer;
}

View file

@ -1,4 +0,0 @@
.plankton_input_group_field[rel="resource_kind"]
{
display: none;
}

View file

@ -1,65 +1,60 @@
:root
{
--hue: 150;
}
html html
{ {
background-color: hsl(0, 0%, 12.5%); background-color: hsl(var(--hue), 0%, 12.5%);
color: hsl(0, 0%, 100%); color: hsl(var(--hue), 0%, 100%);
font-family: sans-serif; font-family: sans-serif;
} }
header header
{ {
background-color: hsl(0, 0%, 25%);
/* /*
background-color: hsl(0, 0%, 25%);
border-bottom: 2px solid #888; border-bottom: 2px solid #888;
padding-bottom: 16px; padding-bottom: 16px;
*/ */
margin-bottom: 16px; margin-bottom: 16px;
} }
nav > ul
{
list-style-type: none;
margin: 0;
padding: 0;
}
nav > ul > li
{
display: inline-block;
margin: 8px;
padding: 8px;
}
a a
{ {
padding: 8px;
text-decoration: none; text-decoration: none;
color: hsl(0, 0%, 87.5%); color: hsl(var(--hue), 0%, 87.5%);
} }
a:hover a:hover
{ {
color: hsl(0, 0%, 100%); color: hsl(var(--hue), 0%, 100%);
border-bottom: 2px solid hsl(0, 0%, 100%); border-bottom: 2px solid hsl(0, 0%, 100%);
transition: 1s ease color; transition: 1s ease color;
} }
input,select
{
padding: 4px;
}
button button
{ {
padding: 8px; padding: 8px 12px;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
background-color: hsl(var(--hue), 0%, 6.125%);
border: 1px solid hsl(var(--hue), 0%, 6.125%);
color: hsl(0, 0%, 87.5%);
margin: 4px;
border-radius: 4px;
} }
input,select,button input,select,textarea
{ {
background-color: hsl(0, 0%, 0%); background-color: hsl(0, 0%, 25%);
border: 1px solid hsl(0, 0%, 25%);
color: hsl(0, 0%, 100%); color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 75%); padding: 4px;
margin: 4px; margin: 4px;
border-radius: 2px; border-radius: 4px;
/*
font-family: monospace;
*/
} }

33
source/style/overlay.css Normal file
View file

@ -0,0 +1,33 @@
#overlay
{
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: hsla(var(--hue), 0%, 0%, 0.75);
z-index: 2;
overflow: auto;
}
#overlay_content
{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
padding: 32px;
background-color: hsl(0, 0%, 12.5%);
color: hsl(0, 0%, 100%);
}
#overlay:not(.overlay_active)
{
display: none;
}

View file

@ -1,60 +0,0 @@
#calendar_add .plankton_input_group_field[rel="attributed"] > .plankton_input_list > .plankton_input_list_elements > .plankton_input_list_element
{
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#calendar_add .plankton_input_group_field[rel="attributed"] > .plankton_input_list > .plankton_input_list_elements > .plankton_input_list_element > .plankton_input_list_element_input > .plankton_input_group
{
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#event_add .plankton_input_group_field[rel="begin"] > .plankton_input_group
,
#event_add .plankton_input_group_field[rel="end"] > .plankton_input_soft_container > .plankton_input_soft_core_wrapper > .plankton_input_group
,
#event_edit .plankton_input_group_field[rel="begin"] > .plankton_input_group
,
#event_edit .plankton_input_group_field[rel="end"] > .plankton_input_soft_container > .plankton_input_soft_core_wrapper > .plankton_input_group
{
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#overview-head
{
text-align: center;
font-weight: bold;
margin-bottom: 12px;
}
#overview-body
{
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#overview-body:not(.compact) #overview-pane-left
{
flex-basis: 12.5%;
}
#overview-body:not(.compact) #overview-pane-right
{
flex-basis: 87.5%;
}
#overview-body.compact #overview-pane-left
{
flex-basis: 0%;
}
#overview-body.compact #overview-pane-right
{
flex-basis: 100%;
}

View file

@ -1,26 +1,33 @@
.plankton_input_group_field { .plankton_input_group_field
{
margin-bottom: 8px; margin-bottom: 8px;
} }
.plankton_input_group_field_label { .plankton_input_group_field_label
{
display: block; display: block;
font-weight: bold; font-weight: bold;
font-size: 0.8em; font-size: 0.8em;
text-transform: uppercase;
} }
.plankton_input_soft_container > * { .plankton_input_soft_container > *
{
display: inline-block; display: inline-block;
} }
.plankton_input_soft_setter { .plankton_input_soft_setter
{
margin-right: 8px; margin-right: 8px;
} }
.plankton_input_soft_inactive { .plankton_input_soft_inactive
{
display: none !important; display: none !important;
} }
.plankton_input_group { .plankton_input_group
{
margin-left: 24px; margin-left: 24px;
} }
@ -33,3 +40,19 @@
{ {
display: inline-block; display: inline-block;
} }
.plankton_input_group_field textarea
{
min-width: 350px;
min-height: 200px;
}
.plankton_input_soft_setter
{
vertical-align: top;
}
.plankton_input_password_exhibit
{
display: none;
}

334
source/types.ts Normal file
View file

@ -0,0 +1,334 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
/**
*/
namespace _dali
{
/**
*/
export enum enum_access_level
{
none,
view,
edit,
admin
}
/**
*/
export type type_group_id = int;
/**
*/
export type type_group_object = {
name : string;
label : string;
};
/**
*/
export type type_user_id = int;
/**
*/
export type type_user_object = {
name : string;
label : string;
email_address : (
null
|
string
);
};
/**
* @todo deprecate?
*/
export type type_local_resource_event_id = int;
/**
* info: das ist nicht deckungsgleich mit der Event-ID aus dem Backend; hiermit werden sowohl lokale als auch
* extern eingebundene Events kodiert
*
* @example "local:1234"
* @example "ics~2345"
*/
export type type_event_key = string;
/**
*/
export type type_event_object = {
name : string;
begin : lib_plankton.pit.type_datetime;
end : (
null
|
lib_plankton.pit.type_datetime
);
location : (
null
|
string
);
link : (
null
|
string
);
description : (
null
|
string
);
};
/**
*/
export type type_event_entry = {
id : (null | type_local_resource_event_id);
key : type_event_key;
object : type_event_object;
};
/**
*/
export type type_event_object_extended = {
key : type_event_key;
calendar_id : type_calendar_id;
calendar_name : string;
hue : float;
access_level : enum_access_level;
event_id : (null | type_local_resource_event_id);
event_object : type_event_object;
};
/**
*/
export type type_resource_id = int;
/**
*/
export type type_resource_object = (
{
kind : "local";
data : {
events : Array<
type_event_object
>;
};
}
|
{
kind : "caldav";
data : {
read_only : boolean;
url : string;
};
}
);
/**
*/
export type type_calendar_id = int;
/**
*/
export type type_calendar_object = {
name : string;
hue : float;
access : {
public : boolean;
default_level : enum_access_level;
attributed_group : lib_plankton.map.type_map<
type_group_id,
enum_access_level
>;
attributed_user : lib_plankton.map.type_map<
type_user_id,
enum_access_level
>;
};
resource_id : type_resource_id;
};
/**
*/
export type type_calendar_object_reduced = {
name : string;
hue : float;
access_level : enum_access_level;
};
/**
*/
export type type_calendar_object_reduced_with_id = {
id : type_calendar_id;
name : string;
hue : float;
access_level : enum_access_level;
};
/**
*/
export function access_level_encode(
access_level : _dali.enum_access_level
)
: ("none" | "view" | "edit" | "admin")
{
switch (access_level)
{
case _dali.enum_access_level.none: return "none";
case _dali.enum_access_level.view: return "view";
case _dali.enum_access_level.edit: return "edit";
case _dali.enum_access_level.admin: return "admin";
}
}
/**
*/
export function access_level_decode(
representation : /*("none" | "view" | "edit" | "admin")*/string
)
: _dali.enum_access_level
{
switch (representation)
{
case "none": return _dali.enum_access_level.none;
case "view": return _dali.enum_access_level.view;
case "edit": return _dali.enum_access_level.edit;
case "admin": return _dali.enum_access_level.admin;
default: throw (new Error("invalid access level representation: " + representation));
}
}
/**
*/
export enum enum_view_mode
{
week,
list,
}
/**
*/
export function view_mode_encode(
mode : _dali.enum_view_mode
)
: string
{
switch (mode)
{
case _dali.enum_view_mode.week: {return "week"; break;}
case _dali.enum_view_mode.list: {return "list"; break;}
default: {throw (new Error("invalid mode"));}
}
}
/**
*/
export function view_mode_decode(
view_mode_encoded : string
)
: _dali.enum_view_mode
{
const map : Record<string, _dali.enum_view_mode> = {
"week": _dali.enum_view_mode.week,
"list": _dali.enum_view_mode.list,
};
if (! (view_mode_encoded in map))
{
throw (new Error("invalid mode: " + view_mode_encoded));
}
else
{
return map[view_mode_encoded];
}
}
/**
*/
export enum enum_view_kind
{
regular,
touch,
}
/**
*/
export function view_kind_encode(
kind : _dali.enum_view_kind
)
: string
{
switch (kind)
{
case _dali.enum_view_kind.regular: {return "regular"; break;}
case _dali.enum_view_kind.touch: {return "touch"; break;}
default: {throw (new Error("invalid kind"));}
}
}
/**
*/
export function view_kind_decode(
view_kind_encoded : string
)
: _dali.enum_view_kind
{
const map : Record<string, _dali.enum_view_kind> = {
"regular": _dali.enum_view_kind.regular,
"touch": _dali.enum_view_kind.touch,
};
if (! (view_kind_encoded in map))
{
throw (new Error("invalid kind: " + view_kind_encoded));
}
else
{
return map[view_kind_encoded];
}
}
}

View file

@ -0,0 +1,173 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
export class class_widget_caldav
{
/**
*/
public constructor(
)
{
}
/**
*/
public async load(
target_element : HTMLElement
)
: Promise<void>
{
target_element.innerHTML = "";
const conf = await _dali.backend.user_dav_conf();
target_element.innerHTML = await _dali.helpers.template_coin(
"widget-caldav",
"main",
{
"title": lib_plankton.translate.get("widget.caldav.title"),
"content": (
(conf === null)
?
await _dali.helpers.template_coin(
"widget-caldav",
"unavailable",
{
"text": lib_plankton.translate.get("widget.caldav.unavailable"),
}
)
:
await _dali.helpers.template_coin(
"widget-caldav",
"available",
{
"conf_title": lib_plankton.translate.get("widget.caldav.conf.title"),
"conf_content": (
(conf.password === null)
?
await _dali.helpers.template_coin(
"widget-caldav",
"conf-token_unset",
{
"text": lib_plankton.translate.get("widget.caldav.conf.token_unset")
}
)
:
await _dali.helpers.template_coin(
"widget-caldav",
"conf-token_set",
{
"address_label": lib_plankton.translate.get("widget.caldav.conf.address"),
"address_value": conf.address,
"username_label": lib_plankton.translate.get("widget.caldav.conf.username"),
"username_value": conf.username,
"password_label": lib_plankton.translate.get("widget.caldav.conf.password"),
"password_value": conf.password,
"setup_hints_label": lib_plankton.translate.get("widget.caldav.conf.setup_hints"),
"setup_hint_entries": (
await lib_plankton.call.promise_condense<string, unknown>(
conf.setup_hints
.map(
entry => () => _dali.helpers.template_coin(
"widget-caldav",
"conf-setup_hint_entry",
{
"text": entry.label,
"href": entry.link,
"remark": (
(entry.remark === null)
?
""
:
lib_plankton.string.coin(
" — {{content}}",
{
"content": entry.remark,
}
)
)
}
)
)
)
).join("")
}
)
),
"set_token_title": lib_plankton.translate.get("widget.caldav.set_token.title"),
"set_token_action": (
(conf.password === null)
?
lib_plankton.translate.get("widget.caldav.set_token.action.set")
:
lib_plankton.translate.get("widget.caldav.set_token.action.overwrite")
),
}
)
),
"close": lib_plankton.translate.get("common.close"),
}
);
// logic
{
// set token
{
if (conf === null)
{
// do nothing
}
else
{
target_element.querySelector(".widget-caldav-set_token > button").addEventListener(
"click",
async () => {
await _dali.backend.user_dav_token();
await this.load(target_element);
}
);
}
}
// close
{
target_element.querySelector(".widget-caldav-close").addEventListener(
"click",
() => {
_dali.overlay.toggle({"mode": false});
}
);
}
}
// init
{
(target_element.querySelector(".widget-caldav-close") as HTMLElement).focus();
}
return Promise.resolve<void>(undefined);
}
}
}

View file

@ -0,0 +1,22 @@
.widget-caldav-conf-section
{
margin-bottom: 16px;
}
.widget-caldav-conf-section-label
{
margin-left: 16px;
display: block;
font-weight: bold;
text-transform: capitalize;
}
.widget-caldav-conf-section-value
{
margin-left: 32px;
}
.widget-caldav-conf-section-value-regular
{
font-family: monospace;
}

View file

@ -0,0 +1,8 @@
<div class="widget-caldav-conf">
<h3>{{conf_title}}</h3>
{{conf_content}}
</div>
<div class="widget-caldav-set_token">
<h3>{{set_token_title}}</h3>
<button>{{set_token_action}}</button>
</div>

View file

@ -0,0 +1,3 @@
<li class="widget-caldav-conf-setup_hints-entry">
<a target="_blank" href="{{href}}">{{text}}</a>{{remark}}
</li>

View file

@ -0,0 +1,18 @@
<div class="widget-caldav-conf-section" class="widget-caldav-conf-address">
<span class="widget-caldav-conf-section-label">{{address_label}}</span>
<span class="widget-caldav-conf-section-value widget-caldav-conf-section-value-regular">{{address_value}}</span>
</div>
<div class="widget-caldav-conf-section" class="widget-caldav-conf-username">
<span class="widget-caldav-conf-section-label">{{username_label}}</span>
<span class="widget-caldav-conf-section-value widget-caldav-conf-section-value-regular">{{username_value}}</span>
</div>
<div class="widget-caldav-conf-section" class="widget-caldav-conf-password">
<span class="widget-caldav-conf-section-label">{{password_label}}</span>
<span class="widget-caldav-conf-section-value widget-caldav-conf-section-value-regular">{{password_value}}</span>
</div>
<div class="widget-caldav-conf-section" class="widget-caldav-conf-setup_hints">
<span class="widget-caldav-conf-section-label">{{setup_hints_label}}</span>
<ul>
{{setup_hint_entries}}
</ul>
</div>

View file

@ -0,0 +1,4 @@
<div class="widget-caldav-conf-info">
({{text}})
</div>

View file

@ -0,0 +1,8 @@
<div class="widget-caldav">
<h2>{{title}}</h2>
<div class="widget-caldav-content">
{{content}}
</div>
<hr/>
<button class="widget-caldav-close">{{close}}</button>
</div>

View file

@ -0,0 +1,3 @@
<div class="widget-caldav-info">
{{text}}
</div>

View file

@ -0,0 +1,263 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
export class class_widget_calendar_edit
implements lib_plankton.zoo_widget.interface_widget
{
/**
*/
private groups : Array<{id : _dali.type_group_id; object : _dali.type_group_object;}>;
/**
*/
private users : Array<{id : _dali.type_user_id; name : string;}>;
/**
*/
private read_only : boolean;
/**
*/
private action_cancel ?: (null | (() => void));
/**
*/
private action_add ?: (null | ((value : _dali.type_calendar_object) => void));
/**
*/
private action_change ?: (null | ((value : _dali.type_calendar_object) => void));
/**
*/
private action_remove ?: (null | ((value : _dali.type_calendar_object) => void));
/**
*/
private initial_value : _dali.type_calendar_object;
/**
*/
public constructor(
groups : Array<{id : _dali.type_group_id; object : _dali.type_group_object;}>,
users : Array<{id : _dali.type_user_id; name : string;}>,
initial_value : _dali.type_calendar_object,
{
"read_only": read_only = false,
"action_cancel": action_cancel = null,
"action_add": action_add = null,
"action_change": action_change = null,
"action_remove": action_remove = null,
}
:
{
read_only ?: boolean;
action_cancel ?: (null | (() => void));
action_add ?: (null | ((value : _dali.type_calendar_object) => void))
action_change ?: (null | ((value : _dali.type_calendar_object) => void));
action_remove ?: (null | ((value : _dali.type_calendar_object) => void));
}
=
{
}
)
{
this.groups = groups;
this.users = users;
this.initial_value = initial_value;
this.read_only = read_only;
this.action_cancel = action_cancel;
this.action_add = action_add;
this.action_change = action_change;
this.action_remove = action_remove;
}
/**
* [implementation]
*/
public async load(
target_element : HTMLElement
) : Promise<void>
{
const dom_root = await _dali.helpers.element_from_template(
"widget-calendar_edit",
"main",
{
}
);
const form : lib_plankton.zoo_form.class_form<
_dali.type_calendar_object,
_dali.type_calendar_object
> = new lib_plankton.zoo_form.class_form<
_dali.type_calendar_object,
_dali.type_calendar_object
>(
(value) => value,
(raw) => raw,
new lib_plankton.zoo_input.class_input_group<any>(
[
{
"name": "name",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": lib_plankton.translate.get("calendar.name")
},
{
"name": "hue",
"input": new lib_plankton.zoo_input.class_input_hue(
),
"label": lib_plankton.translate.get("calendar.hue"),
},
{
"name": "access",
"input": new lib_plankton.zoo_input.class_input_group(
[
{
"name": "public",
"input": new lib_plankton.zoo_input.class_input_checkbox(),
"label": lib_plankton.translate.get("calendar.access.public"),
},
{
"name": "default_level",
"input": _dali.helpers.input_access_level(),
"label": lib_plankton.translate.get("calendar.access.default_level"),
},
{
"name": "attributed_group",
"input": _dali.helpers.input_attributed_access_group(this.groups),
"label": lib_plankton.translate.get("calendar.access.attributed_group"),
},
{
"name": "attributed_user",
"input": _dali.helpers.input_attributed_access_user(this.users),
"label": lib_plankton.translate.get("calendar.access.attributed_user"),
},
]
),
"label": lib_plankton.translate.get("calendar.access.access"),
},
{
"name": "resource",
"input": new lib_plankton.zoo_input.class_input_hidden(
),
"label": lib_plankton.translate.get("calendar.resource"),
},
]
),
(
[]
// add
.concat(
((! this.read_only) && (! (this.action_add === null)))
?
[
{
"label": lib_plankton.translate.get("widget.calendar_edit.actions.add"),
"procedure": async (get_value, get_representation) => {
const value : _dali.type_calendar_object = await get_value();
this.action_add(value);
}
},
]
:
[]
)
// change
.concat(
((! this.read_only) && (! (this.action_change === null)))
?
[
{
"label": lib_plankton.translate.get("widget.calendar_edit.actions.change"),
"procedure": async (get_value, get_representation) => {
const value : _dali.type_calendar_object = await get_value();
this.action_change(value);
}
},
]
:
[]
)
// remove
.concat(
((! this.read_only) && (! (this.action_remove === null)))
?
[
{
"label": lib_plankton.translate.get("widget.calendar_edit.actions.remove"),
"procedure": async (get_value, get_representation) => {
if (! window.confirm(lib_plankton.translate.get("common.confirm_deletion")))
{
// do nothing
}
else
{
const value : _dali.type_calendar_object = await get_value();
this.action_remove(value);
}
}
},
]
:
[]
)
// cancel
.concat(
(! (this.action_cancel === null))
?
[
{
"label": lib_plankton.translate.get("common.cancel"),
"procedure": async () => {
this.action_cancel();
}
},
]
:
[]
)
)
);
await form.setup(dom_root);
await form.input_lock(this.read_only);
await form.input_write(this.initial_value);
target_element.appendChild(dom_root);
form.input_focus();
}
}
}

View file

@ -0,0 +1,4 @@
.widget-calendar_edit .plankton_input_group_field[rel="resource"]
{
display: none;
}

View file

@ -0,0 +1,2 @@
<div class="widget-calendar_edit">
</div>

View file

@ -0,0 +1,336 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
export type type_value = {
calendar_id : (null | _dali.type_calendar_id);
event_name : string;
event_begin : lib_plankton.pit.type_datetime;
event_end : (null | lib_plankton.pit.type_datetime);
event_location : (null | string);
event_link : (null | string);
event_description : (null | string);
};
/**
*/
export type type_representation = {
calendar_id : string;
event_name : string;
event_begin : lib_plankton.pit.type_datetime;
event_end : (null | lib_plankton.pit.type_datetime);
event_location : (null | string);
event_link : (null | string);
event_description : (null | string);
};
/**
*/
export class class_widget_event_edit
implements lib_plankton.zoo_widget.interface_widget
{
/**
* [data]
*/
private available_calendars : Array<
{
id : int;
name : string;
hue : float;
access_level : _dali.enum_access_level;
}
>;
/**
* [data]
*/
private read_only : boolean;
/**
* [data]
*/
private initial_value : type_value;
/**
* [hook]
*/
private action_cancel ?: (null | (() => void));
/**
* [hook]
*/
private action_add ?: (null | ((value : type_value) => void));
/**
* [hook]
*/
private action_change ?: (null | ((value : type_value) => void));
/**
* [hook]
*/
private action_remove ?: (null | ((value : type_value) => void));
/**
*/
public constructor(
available_calendars : Array<
{
id : int;
name : string;
hue : float;
access_level : _dali.enum_access_level;
}
>,
initial_value : type_value,
{
"read_only": read_only = false,
"action_cancel": action_cancel = null,
"action_add": action_add = null,
"action_change": action_change = null,
"action_remove": action_remove = null,
}
:
{
read_only ?: boolean;
action_cancel ?: (null | (() => void));
action_add ?: (null | ((value : type_value) => void));
action_change ?: (null | ((value : type_value) => void));
action_remove ?: (null | ((value : type_value) => void));
}
=
{
}
)
{
// data
this.read_only = read_only;
this.available_calendars = available_calendars;
this.initial_value = initial_value;
// hooks
this.action_cancel = action_cancel;
this.action_add = action_add;
this.action_change = action_change;
this.action_remove = action_remove;
}
/**
* [implementation]
*/
public async load(
target_element : HTMLElement
) : Promise<void>
{
const dom_root = await _dali.helpers.element_from_template(
"widget-event_edit",
"main",
{
}
);
const form : lib_plankton.zoo_form.class_form<
type_value,
type_representation
> = new lib_plankton.zoo_form.class_form<
type_value,
type_representation
>(
(value) => ({
"calendar_id": (value.calendar_id ?? this.available_calendars[0].id).toFixed(0),
"event_name": value.event_name,
"event_begin": value.event_begin,
"event_end": value.event_end,
"event_location": value.event_location,
"event_link": value.event_link,
"event_description": value.event_description,
}),
(representation) => ({
"calendar_id": parseInt(representation.calendar_id),
"event_name": representation.event_name,
"event_begin": representation.event_begin,
"event_end": representation.event_end,
"event_location": representation.event_location,
"event_link": representation.event_link,
"event_description": representation.event_description,
}),
new lib_plankton.zoo_input.class_input_group<any>(
[
{
"name": "calendar_id",
"input": new lib_plankton.zoo_input.class_input_selection(
(
this.available_calendars
.map(
(entry) => ({
"value": entry.id.toFixed(0),
"label": entry.name,
})
)
)
),
"label": lib_plankton.translate.get("calendar.calendar")
},
{
"name": "event_name",
"input": new lib_plankton.zoo_input.class_input_text(
),
"label": lib_plankton.translate.get("event.name")
},
{
"name": "event_begin",
"input": _dali.helpers.datetime_input(
),
"label": lib_plankton.translate.get("event.begin")
},
{
"name": "event_end",
"input": new lib_plankton.zoo_input.class_input_soft<lib_plankton.pit.type_datetime>(
_dali.helpers.datetime_input(
)
),
"label": lib_plankton.translate.get("event.end")
},
{
"name": "event_location",
"input": new lib_plankton.zoo_input.class_input_soft<string>(
new lib_plankton.zoo_input.class_input_text(
)
),
"label": lib_plankton.translate.get("event.location")
},
{
"name": "event_link",
"input": new lib_plankton.zoo_input.class_input_soft<string>(
new lib_plankton.zoo_input.class_input_text(
)
),
"label": lib_plankton.translate.get("event.link")
},
{
"name": "event_description",
"input": new lib_plankton.zoo_input.class_input_soft<string>(
new lib_plankton.zoo_input.class_input_textarea(
)
),
"label": lib_plankton.translate.get("event.description")
},
]
),
(
[]
// add
.concat(
((! this.read_only) && (! (this.action_add === null)))
?
[
{
"label": lib_plankton.translate.get("widget.event_edit.actions.add"),
"procedure": async (get_value, get_representation) => {
const value : type_value = await get_value();
this.action_add(value);
}
},
]
:
[]
)
// change
.concat(
((! this.read_only) && (! (this.action_change === null)))
?
[
{
"label": lib_plankton.translate.get("widget.event_edit.actions.change"),
"procedure": async (get_value, get_representation) => {
const value : type_value = await get_value();
this.action_change(value);
}
},
]
:
[]
)
// remove
.concat(
((! this.read_only) && (! (this.action_remove === null)))
?
[
{
"label": lib_plankton.translate.get("widget.event_edit.actions.remove"),
"procedure": async (get_value, get_representation) => {
if (! window.confirm(lib_plankton.translate.get("common.confirm_deletion")))
{
// do nothing
}
else
{
const value : type_value = await get_value();
this.action_remove(value);
}
}
},
]
:
[]
)
// cancel
.concat(
(! (this.action_cancel === null))
?
[
{
"label": lib_plankton.translate.get("common.cancel"),
"procedure": async () => {
this.action_cancel();
}
},
]
:
[]
)
)
);
await form.setup(dom_root);
await form.input_lock(this.read_only);
await form.input_write(this.initial_value);
target_element.appendChild(dom_root);
form.input_focus();
}
}
}

View file

@ -0,0 +1,2 @@
<div class="widget-event_edit">
</div>

View file

@ -1,47 +1,56 @@
namespace _zeitbild.frontend_web.widgets.listview /*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{ {
/**
*/
type type_entry = {
calendar_id : _zeitbild.frontend_web.type.calendar_id;
calendar_name : string;
access_level : _zeitbild.frontend_web.type.enum_access_level;
event_id : (null | _zeitbild.frontend_web.type.local_resource_event_id);
event_object : _zeitbild.frontend_web.type.event_object;
};
/** /**
*/ */
type type_get_entries = ( type type_get_entries = (
( (
from_pit : lib_plankton.pit.type_pit, from_pit : lib_plankton.pit.type_pit,
to_pit : lib_plankton.pit.type_pit, to_pit : lib_plankton.pit.type_pit,
calendar_ids : Array<_zeitbild.frontend_web.type.calendar_id> calendar_ids : Array<_dali.type_calendar_id>
) )
=> =>
Promise<Array<type_entry>> Promise<Array<_dali.type_event_object_extended>>
); );
/** /**
*/ */
export class class_widget_listview extends _zeitbild.class_widget export class class_widget_listview implements lib_plankton.zoo_widget.interface_widget
{ {
/** /**
* [dependency]
*/ */
private get_entries : type_get_entries; private get_entries : type_get_entries;
/** /**
* [hook]
*/ */
private action_select_event : ( private action_select : (
( (
calendar_id : _zeitbild.frontend_web.type.calendar_id, event_key : _dali.type_event_key
access_level : _zeitbild.frontend_web.type.enum_access_level,
event_id : _zeitbild.frontend_web.type.local_resource_event_id
) )
=> =>
void void
@ -49,6 +58,7 @@ namespace _zeitbild.frontend_web.widgets.listview
/** /**
* [hook]
*/ */
private action_add : ( private action_add : (
( (
@ -58,16 +68,32 @@ namespace _zeitbild.frontend_web.widgets.listview
); );
/**
*/
private include_passed : boolean;
/**
* [state]
*/
private container : (null | Element);
/** /**
*/ */
public constructor( public constructor(
get_entries : type_get_entries, get_entries : type_get_entries,
options : { {
action_select_event ?: ( "include_passed": include_passed = false,
"action_select": action_select = ((event_key) => {}),
"action_add": action_add = (() => {}),
}
:
{
include_passed ?: boolean;
action_select ?: (
( (
calendar_id : _zeitbild.frontend_web.type.calendar_id, event_key : _dali.type_event_key
access_level : _zeitbild.frontend_web.type.enum_access_level,
event_id : _zeitbild.frontend_web.type.local_resource_event_id
) )
=> =>
void void
@ -78,20 +104,197 @@ namespace _zeitbild.frontend_web.widgets.listview
=> =>
void void
); );
} = {} }
=
{
}
) )
{ {
options = Object.assign( // dependencies
{
"action_select_event": (calendar_id, access_level, event_id) => {},
"action_select_add": () => {},
},
options
);
super();
this.get_entries = get_entries; this.get_entries = get_entries;
this.action_select_event = options.action_select_event;
this.action_add = options.action_add; // hooks
this.action_select = action_select;
this.action_add = action_add;
// state
this.include_passed = include_passed;
this.container = null;
}
/**
*/
public toggle_visibility(
calendar_id : _dali.type_calendar_id,
{
"mode": mode = null,
}
:
{
mode ?: (null | boolean);
}
=
{
}
)
: void
{
this.container.querySelectorAll(".listview-entry").forEach(
(element) => {
const rel : string = element.getAttribute("rel");
const parts : Array<string> = rel.split("/");
const calendar_id_ : _dali.type_calendar_id = parseInt(parts[0]);
if (! (calendar_id === calendar_id_))
{
// do nothing
}
else
{
element.classList.toggle(
"listview-entry-hidden",
((mode !== null) ? (! mode) : undefined)
);
}
}
);
}
/**
*/
public async update_entries(
)
: Promise<void>
{
// structure
{
const now_pit : lib_plankton.pit.type_pit = lib_plankton.pit.now();
const from_pit : lib_plankton.pit.type_pit = now_pit;
const to_pit : lib_plankton.pit.type_pit = lib_plankton.pit.shift_week(now_pit, +4);
const entries : Array<_dali.type_event_object_extended> = await this.get_entries(
from_pit,
to_pit,
null
);
entries.sort(
(x, y) => (
lib_plankton.pit.from_datetime(x.event_object.begin)
-
lib_plankton.pit.from_datetime(y.event_object.begin)
)
);
const dom_list : HTMLElement = this.container.querySelector(".listview-entries");
dom_list.innerHTML = (
(
await _dali.helpers.promise_row<string>(
entries
.filter(
(entry) => (
this.include_passed
?
true
:
lib_plankton.pit.is_after(
lib_plankton.pit.from_datetime(entry.event_object.begin),
now_pit
)
)
)
.map(
(entry) => () => _dali.helpers.template_coin(
"widget-listview",
"entry",
{
"name_value": entry.event_object.name,
"calendar_value": entry.calendar_name,
"when_value": lib_plankton.pit.timespan_format(
entry.event_object.begin,
entry.event_object.end,
{
"timezone_indicator": lib_plankton.translate.get("common.timezone_indicator"),
"adjust_to_ce": true,
"show_timezone": false,
}
),
"location_label": lib_plankton.translate.get("event.location"),
"location_extra_classes": (
(entry.event_object.location === null)
?
" listview-entry-field-empty"
:
""
),
"location_value": (
(entry.event_object.location === null)
?
"?"
:
entry.event_object.location
),
"link_label": lib_plankton.translate.get("event.link"),
"link_extra_classes": (
(entry.event_object.link === null)
?
" listview-entry-field-empty"
:
""
),
"link_value": (
(entry.event_object.link === null)
?
"?"
:
entry.event_object.link
),
"link_action": lib_plankton.translate.get("common.open"),
"description_label": lib_plankton.translate.get("event.description"),
"description_extra_classes": (
(entry.event_object.description === null)
?
" listview-entry-field-empty"
:
""
),
"description_value": (
(entry.event_object.description === null)
?
"?"
:
entry.event_object.description
),
"raw": JSON.stringify(entry),
"color": _dali.helpers.event_color(entry.hue),
"rel": entry.key,
},
)
)
)
)
.join("")
);
}
// listeners
{
this.container.querySelectorAll(".listview-entry").forEach(
(element) => {
element.addEventListener(
"click",
(event) => {
if ((event.target as Element).nodeName === "A")
{
// do nothing
}
else
{
const event_key : string = element.getAttribute("rel");
this.action_select(event_key);
}
}
);
}
);
}
} }
@ -100,136 +303,44 @@ namespace _zeitbild.frontend_web.widgets.listview
*/ */
public async load( public async load(
target_element : Element target_element : Element
) : Promise<void> )
: Promise<void>
{ {
const now_pit : lib_plankton.pit.type_pit = lib_plankton.pit.now(); this.container = target_element;
const from_pit : lib_plankton.pit.type_pit = now_pit;
const to_pit : lib_plankton.pit.type_pit = lib_plankton.pit.shift_week(now_pit, +4);
const entries : Array<type_entry> = await this.get_entries(
from_pit,
to_pit,
null
);
entries.sort(
(x, y) => (
lib_plankton.pit.from_datetime(x.event_object.begin)
-
lib_plankton.pit.from_datetime(y.event_object.begin)
)
);
// view // view
{ {
target_element.innerHTML = await _zeitbild.frontend_web.helpers.template_coin( target_element.innerHTML = await _dali.helpers.template_coin(
"widget-listview", "widget-listview",
"main", "main",
{ {
"entries": ( "add_href": "",
( "add_label": lib_plankton.translate.get("widget.listview.add"),
await _zeitbild.frontend_web.helpers.promise_row<string>( "add_extra_classes": (
entries (! await _dali.is_logged_in())
.map( ?
(entry) => () => _zeitbild.frontend_web.helpers.template_coin( " listview-add-hidden"
"widget-listview", :
"entry", ""
{
"name": entry.event_object.name,
"calendar": entry.calendar_name,
"when": _zeitbild.frontend_web.helpers.timespan_format(entry.event_object.begin, entry.event_object.end),
"label_location": lib_plankton.translate.get("event.location"),
"location": (
(entry.event_object.location === null)
?
"?"
:
entry.event_object.location
),
"label_description": lib_plankton.translate.get("event.description"),
"description": (
(entry.event_object.description === null)
?
"?"
:
entry.event_object.description
),
"raw": JSON.stringify(entry),
"color": lib_plankton.color.output_hex(
lib_plankton.color.give_generic(
(entry.calendar_id - 1),
{
"saturation": 0.375,
"value": 0.375,
}
),
),
"rel": lib_plankton.string.coin(
"{{calendar_id}}/{{event_id}}/{{access_level}}",
{
"calendar_id": entry.calendar_id.toFixed(0),
"event_id": (
(! (entry.event_id === null))
?
entry.event_id.toFixed(0)
:
"-"
),
"access_level": (() => {
switch (entry.access_level) {
case _zeitbild.frontend_web.type.enum_access_level.none: return "none";
case _zeitbild.frontend_web.type.enum_access_level.view: return "view";
case _zeitbild.frontend_web.type.enum_access_level.edit: return "edit";
case _zeitbild.frontend_web.type.enum_access_level.admin: return "admin";
}
}) (),
}
),
},
)
)
)
)
.join("")
), ),
"entries": "",
} }
); );
} }
// control // control
{ {
target_element.querySelectorAll(".listview-entry").forEach( target_element.querySelector(".listview-add").addEventListener(
(element) => { "click",
element.addEventListener( (event) => {
"click", event.preventDefault();
() => { this.action_add();
const rel : string = element.getAttribute("rel");
const parts : Array<string> = rel.split("/");
const calendar_id : _zeitbild.frontend_web.type.calendar_id = parseInt(parts[0]);
const event_id : (null | _zeitbild.frontend_web.type.local_resource_event_id) = (
parts[1] === "-"
?
null
:
parseInt(parts[1])
);
const access_level : _zeitbild.frontend_web.type.enum_access_level = (() => {
switch (parts[2]) {
case "none": return _zeitbild.frontend_web.type.enum_access_level.none;
case "view": return _zeitbild.frontend_web.type.enum_access_level.view;
case "edit": return _zeitbild.frontend_web.type.enum_access_level.edit;
case "admin": return _zeitbild.frontend_web.type.enum_access_level.admin;
}
}) ();
this.action_select_event(
calendar_id,
access_level,
event_id,
);
}
);
} }
); );
} }
await this.update_entries();
return Promise.resolve<void>(undefined); return Promise.resolve<void>(undefined);
} }

View file

@ -1,33 +1,11 @@
.sources .listview-add
{ {
margin: 0; margin-left: 12px;
padding: 0;
list-style-type: none;
font-size: 0.75em;
} }
.sources-entry .listview-add-hidden
{ {
margin: 8px; display: none;
padding: 4px;
cursor: pointer;
}
.weekview-controls
{
margin-bottom: 12px;
text-align: center;
}
.weekview-control
{
margin: 0 12px;
}
.weekview-table table
{
width: 100%;
border-collapse: collapse;
} }
.listview-entries .listview-entries
@ -46,6 +24,11 @@
cursor: pointer; cursor: pointer;
} }
.listview-entry-hidden
{
display: none;
}
.listview-entry-name .listview-entry-name
{ {
font-weight: bold; font-weight: bold;
@ -67,6 +50,10 @@
content: "] "; content: "] ";
} }
.listview-entry-field-empty {
display: none;
}
.listview-entry-field:not(:last-child) .listview-entry-field:not(:last-child)
{ {
margin-bottom: 16px; margin-bottom: 16px;

View file

@ -1,16 +1,20 @@
<li class="listview-entry" style="background-color: {{color}}" rel="{{rel}}"> <li class="listview-entry" style="background-color: {{color}}" rel="{{rel}}">
<span class="listview-entry-name">{{name}}</span> (<span class="listview-entry-calendar">{{calendar}}</span>) <span class="listview-entry-name">{{name_value}}</span> (<span class="listview-entry-calendar">{{calendar_value}}</span>)
<div class="listview-entry-field listview-entry-when"> <div class="listview-entry-field listview-entry-when">
<span class="listview-entry-value">{{when}}</span> <span class="listview-entry-value">{{when_value}}</span>
</div> </div>
<div class="listview-entry-field listview-entry-location"> <div class="listview-entry-field listview-entry-location{{location_extra_classes}}">
<span class="listview-entry-label">{{label_location}}</span> <span class="listview-entry-label">{{location_label}}</span>
<span class="listview-entry-value">{{location}}</span> <span class="listview-entry-value">{{location_value}}</span>
</div>
<div class="listview-entry-field listview-entry-link{{link_extra_classes}}">
<span class="listview-entry-label">{{link_label}}</span>
<a class="listview-entry-value" href="{{link_value}}" target="_blank">{{link_action}}</a>
</div> </div>
<!-- <!--
<div class="listview-entry-field listview-entry-description"> <div class="listview-entry-field listview-entry-description{{description_extra_classes}}">
<span class="listview-entry-label">{{label_description}}</span> <span class="listview-entry-label">{{description_label}}</span>
<span class="listview-entry-value">{{description}}</span> <span class="listview-entry-value">{{description_value}}</span>
</div> </div>
<div class="listview-entry-raw">{{raw}}</div> <div class="listview-entry-raw">{{raw}}</div>
--> -->

View file

@ -1,4 +1,5 @@
<div class="listview"> <div class="listview">
<a class="listview-add{{add_extra_classes}}" href="{{add_href}}">{{add_label}}</a>
<ul class="listview-entries"> <ul class="listview-entries">
{{entries}} {{entries}}
</ul> </ul>

View file

@ -0,0 +1,126 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
export class class_widget_login implements lib_plankton.zoo_widget.interface_widget
{
/**
* [data]
*/
private initial_name : string;
/**
* [hook]
*/
private action_cancel : (null | (() => void));
/**
* [hook]
*/
private action_success : (null | (() => void));
/**
*/
public constructor(
{
"initial_name": initial_name = "",
"action_cancel": action_cancel = null,
"action_success": action_success = null,
}
:
{
initial_name ?: string;
action_cancel ?: (null | (() => void));
action_success ?: (null | (() => void));
}
=
{
}
)
{
this.initial_name = initial_name;
this.action_cancel = action_cancel;
this.action_success = action_success;
}
/**
* [implementation]
*/
public async load(
target_element : HTMLElement
)
: Promise<void>
{
const preparation : {kind : string; data : any;} = await _dali.backend.session_prepare(
{
"oidc_redirect_uri_template": _dali.conf.get()["misc"]["oidc_redirect_uri_template"],
}
);
switch (preparation.kind)
{
case "internal":
{
const sub_widget : lib_plankton.zoo_widget.interface_widget = (
new class_widget_login_internal(
preparation.data,
{
"initial_name": this.initial_name,
"action_cancel": this.action_cancel,
"action_success": this.action_success,
}
)
);
await sub_widget.load(target_element);
break;
}
case "oidc":
{
const sub_widget : lib_plankton.zoo_widget.interface_widget = (
new class_widget_login_oidc(
preparation.data,
{
"action_cancel": this.action_cancel,
"action_success": this.action_success,
}
)
);
await sub_widget.load(target_element);
break;
}
default:
{
throw (new Error("unhandled login kind: " + preparation.kind));
break;
}
}
}
}
}

View file

@ -0,0 +1,165 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
export class class_widget_login_internal
implements lib_plankton.zoo_widget.interface_widget
{
/**
*/
private preparation_data : any;
/**
*/
private initial_name : (null | string);
/**
* [hook]
*/
private action_cancel : (null | (() => void));
/**
* [hook]
*/
private action_success : (null | (() => void));
/**
*/
public constructor(
preparation_data : any,
{
"initial_name": initial_name = null,
"action_cancel": action_cancel = null,
"action_success": action_success = null,
}
:
{
initial_name ?: (null | string);
action_cancel ?: (null | (() => void));
action_success ?: (null | (() => void));
}
=
{
}
)
{
this.preparation_data = preparation_data;
this.initial_name = initial_name;
this.action_cancel = action_cancel;
this.action_success = action_success;
}
/**
* [implementation]
*/
public async load(
target_element : HTMLElement
)
: Promise<void>
{
const form : lib_plankton.zoo_form.class_form<
{name : string; password : string;},
{name : string; password : string;}
> = new lib_plankton.zoo_form.class_form<
{name : string; password : string;},
{name : string; password : string;}
>(
x => x,
x => x,
new lib_plankton.zoo_input.class_input_group<
{name : string; password : string;}
>(
[
{
"name": "name",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": lib_plankton.translate.get("widget.login.internal.name"),
},
{
"name": "password",
"input": new lib_plankton.zoo_input.class_input_password(),
"label": lib_plankton.translate.get("widget.login.internal.password"),
},
]
),
(
[]
.concat(
[
{
"label": lib_plankton.translate.get("widget.login.internal.do"),
"procedure": async (get_value, get_representation) => {
const value : any = await get_value();
try
{
await _dali.backend.session_begin(
value.name,
value.password
);
if (this.action_success !== null) this.action_success();
}
catch (error)
{
// todo
}
}
}
]
)
.concat(
(this.action_cancel === null)
?
[]
:
[
{
"label": lib_plankton.translate.get("common.cancel"),
"procedure": () => {
this.action_cancel();
}
}
]
)
)
);
target_element.innerHTML = "";
await form.setup(target_element);
await form.input_write(
{
"name": this.initial_name,
"password": "",
}
);
form.input_focus();
}
}
}

View file

@ -0,0 +1,110 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
export class class_widget_login_oidc
implements lib_plankton.zoo_widget.interface_widget
{
/**
*/
private preparation_data : any;
/**
* [hook]
*/
private action_cancel : (null | (() => void));
/**
* [hook]
*/
private action_success : (null | (() => void));
/**
*/
public constructor(
preparation_data : any,
{
"action_cancel": action_cancel = null,
"action_success": action_success = null,
}
:
{
action_cancel ?: (null | (() => void));
action_success ?: (null | (() => void));
}
=
{
}
)
{
this.preparation_data = preparation_data;
this.action_cancel = action_cancel;
this.action_success = action_success;
}
/**
* [implementation]
*/
public async load(
target_element : HTMLElement
)
: Promise<void>
{
// structure
{
target_element.innerHTML = await _dali.helpers.template_coin(
"widget-login_oidc",
"main",
{
"label": lib_plankton.string.coin(
lib_plankton.translate.get("widget.login.oidc.via"),
{
"title": this.preparation_data.label,
}
),
"href": this.preparation_data.url,
"cancel": lib_plankton.translate.get("common.cancel"),
}
);
}
// controls
{
target_element.querySelector(".widget-login_oidc-cancel").addEventListener(
"click",
() => {
this.action_cancel();
}
);
}
}
}
}

View file

@ -0,0 +1,5 @@
.widget-login_oidc-cancel
{
display: block;
margin-top: 32px;
}

View file

@ -0,0 +1,4 @@
<div class="widget-login_oidc">
<a class="widget-login_oidc-link" href="{{href}}">{{label}}</a>
<button class="widget-login_oidc-cancel">{{cancel}}</button>
</div>

View file

@ -0,0 +1,214 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
type type_entry_data = {
label : string;
groups : Array<string>;
action : (() => void);
}
/**
*/
export class class_widget_menu implements lib_plankton.zoo_widget.interface_widget
{
/**
*/
private entries : Array<type_entry_data>;
/**
*/
private initial_groups : Array<string>;
/**
*/
private label : (null | string);
/**
*/
private container : (null | HTMLElement);
/**
*/
public constructor(
entries : Array<type_entry_data>,
{
"initial_groups": initial_groups = [],
"initial_label": initial_label = "",
}
:
{
initial_groups ?: Array<string>;
initial_label ?: string;
}
=
{
}
)
{
this.entries = entries;
this.initial_groups = initial_groups;
this.label = initial_label;
this.container = null;
}
/**
*/
public set_groups(
groups : Array<string>
)
: void
{
this.entries.forEach(
(entry, index) => {
const active : boolean = groups.some(group => entry.groups.includes(group));
const rel : string = index.toFixed(0);
const dom_entry = this.container.querySelector(".widget-menu-entry[rel=\"" + rel + "\"]");
dom_entry.classList.toggle("widget-menu-entry-hidden", (! active));
}
);
}
/**
*/
public set_label(
label : (null | string)
)
: void
{
this.label = label;
this.container.querySelector(".widget-menu-button").innerHTML = (
(this.label === null)
?
("[" + "=" + "]")
:
("[" + this.label + "]")
);
}
/**
*/
private toggle_collapsed(
{
"mode": mode = null,
}
:
{
mode ?: (null | boolean);
}
=
{
}
)
: void
{
this.container.classList.toggle("widget-menu-collapsed", mode ?? undefined);
}
/**
* [implementation]
*/
public async load(
target_element : Element
)
: Promise<void>
{
// structure
this.container = await _dali.helpers.element_from_template(
"widget-menu",
"main",
{
"entries": (
(
await lib_plankton.call.promise_condense(
this.entries.map(
(entry, index) => () => _dali.helpers.template_coin(
"widget-menu",
"entry",
{
"label": entry.label,
"rel": index.toFixed(0),
}
)
)
)
)
.join("")
),
}
);
// logic
{
// collapser
{
this.container.querySelector(".widget-menu-button").addEventListener(
"click",
() => {
this.toggle_collapsed();
}
);
}
// entries
{
this.container.querySelectorAll(".widget-menu-entry").forEach(
dom_entry => {
dom_entry.addEventListener(
"click",
() => {
const index : int = parseInt(dom_entry.getAttribute("rel"));
const entry : type_entry_data = this.entries[index];
this.toggle_collapsed({"mode": true});
entry.action();
}
);
}
);
}
}
// init
{
this.toggle_collapsed({"mode": true});
this.set_groups(this.initial_groups);
this.set_label(null);
}
// finish
target_element.appendChild(this.container);
}
}
}

View file

@ -0,0 +1,75 @@
.widget-menu
{
margin-left: auto;
margin-right: 8px;
width: fit-content;
}
.widget-menu.widget-menu-collapsed > .widget-menu-platform
{
display: none;
}
.widget-menu-button
{
text-transform: initial;
}
.widget-menu-entry
{
cursor: pointer;
}
.widget-menu-platform
{
background-color: hsl(var(--hue), 0%, 25%);
border-radius: 2px;
border: 1px solid hsl(var(--hue), 0%, 0%);
padding: 8px;
/*
min-width: 200px;
*/
}
.widget-menu-platform:not(.widget-menu-platform-collapsed)
{
position: fixed;
top: 50px;
right: 20px;
z-index: 2;
}
.widget-menu-entries
{
padding: 0;
margin: 0;
list-style-type: none;
}
.widget-menu-entry
{
margin: 12px 8px;
text-transform: capitalize;
background-color: hsl(var(--hue), 0%, 25%);
color: hsl(var(--hue), 0%, 100%);
}
.widget-menu-entry:not(:hover) > span
{
border-bottom: 2px solid hsl(0, 0%, 25%);
}
.widget-menu-entry:hover > span
{
border-bottom: 2px solid hsl(0, 0%, 100%);
transition: 1s ease color;
}
.widget-menu-entry.widget-menu-entry-hidden
{
display: none;
}

View file

@ -0,0 +1,3 @@
<li class="widget-menu-entry" rel="{{rel}}">
<span>{{label}}</span>
</li>

View file

@ -0,0 +1,8 @@
<div class="widget-menu">
<button class="widget-menu-button"></button>
<div class="widget-menu-platform">
<ul class="widget-menu-entries">
{{entries}}
</ul>
</div>
</div>

View file

@ -0,0 +1,139 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
type type_option = {
mode : _dali.enum_view_mode;
label : string,
};
/**
*/
export class class_widget_mode_switcher implements lib_plankton.zoo_widget.interface_widget
{
/**
*/
private options : Array<type_option>;
/**
*/
private initial_selection : (null | _dali.enum_view_mode);
/**
*/
private action_change : (null | ((mode : _dali.enum_view_mode) => void));
/**
*/
public constructor(
options : Array<type_option>,
{
"initial_selection": initial_selection = null,
"action_change": action_change = null,
}
:
{
initial_selection ?: (null | _dali.enum_view_mode),
action_change ?: (null | ((mode : _dali.enum_view_mode) => void))
}
=
{
}
)
{
this.options = options;
this.initial_selection = initial_selection;
this.action_change = action_change;
}
/**
* [implementation]
*/
public async load(
target_element : Element
)
: Promise<void>
{
target_element.innerHTML = await _dali.helpers.template_coin(
"widget-mode_switcher",
"main",
{
"options": (await lib_plankton.call.promise_condense(
this.options.map(
option => () => _dali.helpers.template_coin(
"widget-mode_switcher",
"option",
{
"rel": _dali.view_mode_encode(option.mode),
"value": _dali.view_mode_encode(option.mode),
"label": option.label,
}
)
)
)).join(""),
}
);
// initial selection
{
if (this.initial_selection !== null)
{
const selector : string = lib_plankton.string.coin(
".widget-mode_switcher-option[rel=\"{{rel}}\"] > input",
{
"rel": _dali.view_mode_encode(this.initial_selection)
}
);
(target_element.querySelector(selector) as HTMLInputElement).checked = true;
}
}
// set listeners
{
if (this.action_change !== null)
{
target_element.querySelectorAll(".widget-mode_switcher-option").forEach(
element => {
const view_mode_encoded : string = element.getAttribute("rel");
const mode : _dali.enum_view_mode = _dali.view_mode_decode(view_mode_encoded);
element.querySelector("input").addEventListener(
"change",
(event) => {
this.action_change(mode);
}
);
}
);
}
}
return Promise.resolve<void>(undefined);
}
}
}

View file

@ -0,0 +1,4 @@
.widget-mode_switcher-option
{
margin-left: 16px;
}

View file

@ -0,0 +1,3 @@
<div class="widget-mode_switcher">
{{options}}
</div>

View file

@ -0,0 +1,4 @@
<label class="widget-mode_switcher-option" rel="{{rel}}">
<input type="radio" name="mode" value="{{value}}"/>
<span>{{label}}</span>
</label>

View file

@ -0,0 +1,278 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
type type_get_entries = (
(
from_pit : lib_plankton.pit.type_pit,
to_pit : lib_plankton.pit.type_pit,
calendar_ids : Array<_dali.type_calendar_id>
)
=>
Promise<Array<_dali.type_event_object_extended>>
);
/**
*/
type type_create_event = (
(
stuff ?: {
date ?: lib_plankton.pit.type_date;
}
)
=>
Promise<void>
);
/**
*/
type type_edit_event = (
(
event_key : _dali.type_event_key
)
=>
Promise<void>
);
/**
*/
export class class_widget_multiview
implements lib_plankton.zoo_widget.interface_widget
{
/**
*/
private initial_view_mode : _dali.enum_view_mode;
/**
*/
private weekview_initial_vertical : boolean;
/**
*/
private get_entries : type_get_entries;
/**
*/
private action_create_event : type_create_event;
/**
*/
private action_edit_event : type_edit_event;
/**
*/
private dom_context : (null | HTMLElement);
/**
*/
private widget_mode_switcher : _dali.widgets.class_widget_mode_switcher;
/**
*/
private widget_weekview : _dali.widgets.class_widget_weekview;
/**
*/
private widget_listview : _dali.widgets.class_widget_listview;
/**
*/
public constructor(
get_entries : type_get_entries,
{
"initial_view_mode": initial_view_mode = _dali.enum_view_mode.week,
"weekview_initial_vertical": weekview_initial_vertical = false,
"action_create_event": action_create_event = ((stuff) => Promise.resolve<void>(undefined)),
"action_edit_event": action_edit_event = ((event_key) => Promise.resolve<void>(undefined)),
}
:
{
initial_view_mode ?: _dali.enum_view_mode;
weekview_initial_vertical ?: boolean;
action_create_event ?: type_create_event;
action_edit_event ?: type_edit_event;
}
=
{
}
)
{
this.get_entries = get_entries;
this.initial_view_mode = initial_view_mode;
this.weekview_initial_vertical = weekview_initial_vertical;
this.action_create_event = action_create_event;
this.action_edit_event = action_edit_event;
this.dom_context = null;
}
/**
*/
public toggle_calendar_visibilty(
calendar_id : _dali.type_calendar_id,
{
"mode": mode = null,
}
:
{
mode ?: (null | boolean);
}
=
{
}
)
: void
{
this.widget_weekview.toggle_visibility(calendar_id, {"mode": mode});
this.widget_listview.toggle_visibility(calendar_id, {"mode": mode});
}
/**
*/
private set_view_mode
(
view_mode : _dali.enum_view_mode
)
: void
{
this.dom_context.setAttribute("rel", _dali.view_mode_encode(view_mode));
}
/**
*/
public async update_entries(
)
:
Promise<void>
{
await Promise.all(
[
this.widget_weekview.update_entries(),
this.widget_listview.update_entries(),
]
);
}
/**
* [implementation]
*/
public async load(
target_element : Element
)
: Promise<void>
{
this.dom_context = (target_element as HTMLElement);
this.dom_context.classList.add("widget-multiview");
// mode switcher
{
this.widget_mode_switcher = (
new _dali.widgets.class_widget_mode_switcher(
[
{
"mode": _dali.enum_view_mode.week,
/**
* @todo as dependency
*/
"label": lib_plankton.translate.get("widget.overview.mode.week"),
},
{
"mode": _dali.enum_view_mode.list,
/**
* @todo as dependency
*/
"label": lib_plankton.translate.get("widget.overview.mode.list"),
},
],
{
"initial_selection": this.initial_view_mode,
"action_change": (view_mode) => {
this.set_view_mode(view_mode);
}
}
)
);
let dom_wrapper = document.createElement("div");
dom_wrapper.classList.add("widget-multiview-mode_switcher");
await this.widget_mode_switcher.load(dom_wrapper);
this.dom_context.appendChild(dom_wrapper);
}
// weekview
{
this.widget_weekview = (
new _dali.widgets.class_widget_weekview(
this.get_entries,
{
"action_select_event": (event_key) => this.action_edit_event(event_key),
"action_select_day": (date) => this.action_create_event({"date": date}),
"vertical": this.weekview_initial_vertical,
}
)
);
let dom_wrapper = document.createElement("div");
dom_wrapper.classList.add("widget-multiview-weekview");
await this.widget_weekview.load(dom_wrapper);
this.dom_context.appendChild(dom_wrapper);
}
// listview
{
this.widget_listview = (
new _dali.widgets.class_widget_listview(
this.get_entries,
{
"action_select": (event_key) => this.action_edit_event(event_key),
"action_add": () => this.action_create_event(),
}
)
);
let dom_wrapper = document.createElement("div");
dom_wrapper.classList.add("widget-multiview-listview");
await this.widget_listview.load(dom_wrapper);
this.dom_context.appendChild(dom_wrapper);
}
this.set_view_mode(this.initial_view_mode);
return Promise.resolve<void>(undefined);
}
}
}

View file

@ -0,0 +1,10 @@
.widget-multiview-mode_switcher
{
margin-bottom: 12px;
}
.widget-multiview[rel="week"] > .widget-multiview-weekview {}
.widget-multiview[rel="week"] > .widget-multiview-listview {display: none;}
.widget-multiview[rel="list"] > .widget-multiview-weekview {display: none;}
.widget-multiview[rel="list"] > .widget-multiview-listview {}

View file

@ -0,0 +1,648 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
type type_action_create_calendar = (
(
)
=>
Promise<void>
);
/**
*/
type type_action_edit_calendar = (
(
type_calendar_object_reduced_with_id
)
=>
Promise<void>
);
/**
*/
type type_action_create_event = (
(
stuff ?: {
date ?: lib_plankton.pit.type_date;
}
)
=>
Promise<void>
);
/**
*/
type type_action_edit_event = (
(
event_key : _dali.type_event_key
)
=>
Promise<void>
);
/**
*/
export class class_widget_overview
implements lib_plankton.zoo_widget.interface_widget
{
/**
*/
private widget_sources : _dali.widgets.class_widget_sources;
/**
*/
private widget_multiview : _dali.widgets.class_widget_multiview;
/**
*/
private action_create_calendar : type_action_create_calendar;
/**
*/
private action_edit_calendar : type_action_edit_calendar;
/**
*/
private action_create_event : type_action_create_event;
/**
*/
private action_edit_event : type_action_edit_event;
/**
*/
public constructor(
)
{
this.action_create_calendar = async (
) => {
const widget = new _dali.widgets.class_widget_calendar_edit(
await _dali.model.group_list(),
await _dali.model.user_list(),
{
"name": "",
"hue": lib_plankton.random.generate_unit(),
"access": {
"public": false,
"default_level": _dali.enum_access_level.none,
"attributed_group": lib_plankton.map.hashmap.implementation_map<
_dali.type_group_id,
_dali.enum_access_level
>(
lib_plankton.map.hashmap.make<
_dali.type_group_id,
_dali.enum_access_level
>(
group_id => group_id.toFixed(0),
)
),
"attributed_user": lib_plankton.map.hashmap.implementation_map<
_dali.type_user_id,
_dali.enum_access_level
>(
lib_plankton.map.hashmap.make<
_dali.type_user_id,
_dali.enum_access_level
>(
user_id => user_id.toFixed(0),
)
),
},
/**
* @todo
*/
"resource_id": 0,
},
{
"read_only": false,
"action_cancel": () => {
_dali.overlay.toggle({"mode": false});
},
"action_add": (calendar_object) => {
_dali.model.calendar_add(
calendar_object
)
.then(
() => {
this.update_sources_and_entries();
_dali.overlay.toggle({"mode": false});
}
)
.catch(
(reason) => {
lib_plankton.log.warning(
"dali.overview.calendar_add_error",
{"reason": String(reason)}
);
}
);
},
}
);
_dali.overlay.clear();
_dali.overlay.toggle({"mode": true});
await widget.load(_dali.overlay.get_content_element());
};
this.action_edit_calendar = async (
calendar_object_reduced_with_id
) => {
const read_only : boolean = (() => {
switch (calendar_object_reduced_with_id.access_level)
{
case _dali.enum_access_level.none:
{
throw (new Error("this event should not be visible"));
break;
}
case _dali.enum_access_level.edit:
case _dali.enum_access_level.view:
{
return true;
break;
}
case _dali.enum_access_level.admin:
{
return false;
break;
}
}
}) ();
if (read_only)
{
lib_plankton.log.notice(
"dali.overview.may_not_edit_calendar",
{
"calendar_id": calendar_object_reduced_with_id.id,
}
);
}
else
{
const calendar_id : _dali.type_calendar_id = calendar_object_reduced_with_id.id;
const calendar_object : _dali.type_calendar_object = await _dali.model.calendar_get(
calendar_id
);
const widget = new _dali.widgets.class_widget_calendar_edit(
await _dali.model.group_list(),
await _dali.model.user_list(),
calendar_object,
{
"read_only": read_only,
"action_cancel": () => {
_dali.overlay.toggle({"mode": false});
},
"action_change": (data) => {
_dali.model.calendar_change(
calendar_id,
data
)
.then(
() => {
this.update_sources_and_entries();
_dali.overlay.toggle({"mode": false});
}
);
},
"action_remove": (data) => {
_dali.model.calendar_remove(
calendar_id
)
.then(
() => {
this.update_sources_and_entries();
_dali.overlay.toggle({"mode": false});
}
);
},
}
);
_dali.overlay.clear();
_dali.overlay.toggle({"mode": true});
await widget.load(_dali.overlay.get_content_element());
}
}
/**
* @todo unterschiedliches Verhalten bei Anmeldung?
*/
this.action_create_event = async (
{
"date": date = lib_plankton.pit.to_datetime(lib_plankton.pit.now()).date,
}
:
{
date ?: lib_plankton.pit.type_date;
}
=
{
}
) => {
if (! _dali.is_logged_in())
{
// do nothing
}
else
{
const widget = new _dali.widgets.class_widget_event_edit(
(await this.get_available_calendars()),
{
"calendar_id": null,
"event_name": "",
"event_begin": lib_plankton.call.convey(
date,
[
x => ({
"timezone_shift": 0,
"date": date,
"time": {"hour": 12, "minute": 0, "second": 0}
}),
lib_plankton.pit.from_datetime,
x => lib_plankton.pit.shift_hour(x, 0),
lib_plankton.pit.to_datetime,
]
),
"event_end": lib_plankton.call.convey(
date,
[
x => ({
"timezone_shift": 0,
"date": date,
"time": {"hour": 12, "minute": 0, "second": 0}
}),
lib_plankton.pit.from_datetime,
x => lib_plankton.pit.shift_hour(x, +1),
lib_plankton.pit.to_datetime,
]
),
"event_location": null,
"event_link": null,
"event_description": null,
},
{
"read_only": false,
"action_cancel": () => {
_dali.overlay.toggle({"mode": false});
},
"action_add": (data) => {
_dali.model.event_add(
data.calendar_id,
{
"name": data.event_name,
"begin": data.event_begin,
"end": data.event_end,
"location": data.event_location,
"link": data.event_link,
"description": data.event_description,
}
)
.then(
() => {
this.update_entries();
_dali.overlay.toggle({"mode": false});
}
)
.catch(
(reason) => {
// todo
}
);
},
}
);
_dali.overlay.clear();
_dali.overlay.toggle({"mode": true});
await widget.load(_dali.overlay.get_content_element());
}
}
/**
*/
this.action_edit_event = async (
event_key
) => {
const event_object_extended : _dali.type_event_object_extended = await _dali.model.event_get(event_key);
const read_only : boolean = (() => {
switch (event_object_extended.access_level)
{
case _dali.enum_access_level.none:
{
throw (new Error("this event should not be visible"));
break;
}
case _dali.enum_access_level.view:
{
return true;
break;
}
case _dali.enum_access_level.edit:
case _dali.enum_access_level.admin:
{
return false;
break;
}
}
}) ();
const widget = new _dali.widgets.class_widget_event_edit(
(await this.get_available_calendars()),
{
"calendar_id": event_object_extended.calendar_id,
"event_name": event_object_extended.event_object.name,
"event_begin": event_object_extended.event_object.begin,
"event_end": event_object_extended.event_object.end,
"event_location": event_object_extended.event_object.location,
"event_link": event_object_extended.event_object.link,
"event_description": event_object_extended.event_object.description,
},
{
"read_only": read_only,
"action_cancel": () => {
_dali.overlay.toggle({"mode": false});
},
"action_change": (data) => {
_dali.model.event_change(
event_key,
{
"name": data.event_name,
"begin": data.event_begin,
"end": data.event_end,
"location": data.event_location,
"link": data.event_link,
"description": data.event_description,
}
)
.then(
() => {
this.update_entries();
_dali.overlay.toggle({"mode": false});
}
)
.catch(
(reason) => {
lib_plankton.log.warning(
"dali.overview.event_change.error",
{"reason": String(reason)}
);
}
);
},
"action_remove": () => {
_dali.model.event_remove(
event_key
)
.then(
() => {
this.update_entries();
_dali.overlay.toggle({"mode": false});
}
)
.catch(
(reason) => {
lib_plankton.log.warning(
"dali.overview.event_remove_error",
{"reason": String(reason)}
);
}
);
},
}
);
_dali.overlay.clear();
_dali.overlay.toggle({"mode": true});
await widget.load(_dali.overlay.get_content_element());
}
}
/**
*/
private async get_available_calendars(
)
: Promise<
Array<
_dali.type_calendar_object_reduced_with_id
>
>
{
return (
(await _dali.model.calendar_list())
/*
.filter(
(entry) => (
(entry.access_level === _dali.enum_access_level.edit)
||
(entry.access_level === _dali.enum_access_level.admin)
)
)
*/
);
}
/**
*/
private async get_entries(
from_pit : lib_plankton.pit.type_pit,
to_pit : lib_plankton.pit.type_pit,
calendar_ids : Array<_dali.type_calendar_id>
)
: Promise<Array<_dali.type_event_object_extended>>
{
/**
* @todo do NOT wait?
*/
await _dali.model.sync_events(
{
"from": from_pit,
"to": to_pit,
},
{
"calendar_ids": calendar_ids,
}
);
/**
* @todo filter
*/
return _dali.model.event_list();
}
/**
*/
private async update_sources_and_entries(
{
"priviliged": priviliged = null,
}
:
{
priviliged ?: (null | boolean);
}
=
{
}
)
: Promise<void>
{
await this.widget_sources.update({"priviliged": priviliged});
await this.widget_multiview.update_entries();
}
/**
* @todo use priviliged?
*/
private async update_entries(
{
"priviliged": priviliged = null,
}
:
{
priviliged ?: (null | boolean);
}
=
{
}
)
: Promise<void>
{
await this.widget_multiview.update_entries();
}
/**
* [implementation]
*/
public async load(
target_element : HTMLElement
)
: Promise<void>
{
const view_mode : _dali.enum_view_mode = _dali.helpers.view_mode_determine("auto");
const view_kind : _dali.enum_view_kind = _dali.helpers.view_kind_determine("auto");
this.widget_sources = (
new _dali.widgets.class_widget_sources(
_dali.model.calendar_list,
{
"initial_priviliged": _dali.is_logged_in(),
"action_add": this.action_create_calendar,
"action_select": this.action_edit_calendar,
"action_toggle": (entry, mode) => {
this.widget_multiview.toggle_calendar_visibilty(entry.id, {"mode": mode});
},
}
)
);
this.widget_multiview = (
new _dali.widgets.class_widget_multiview(
this.get_entries,
{
"initial_view_mode": view_mode,
"weekview_initial_vertical": (view_kind === _dali.enum_view_kind.touch),
"action_create_event": this.action_create_event,
"action_edit_event": this.action_edit_event,
}
)
);
target_element.innerHTML = await _dali.helpers.template_coin(
"widget-overview",
"main",
{
}
);
// head
{
// hint
{
const dom_hint = target_element.querySelector(".widget-overview-hint");
dom_hint.textContent = lib_plankton.translate.get("widget.overview.login_hint");
dom_hint.classList.toggle("widget-overview-hint-hidden", _dali.is_logged_in());
}
}
// body
{
let widget_slider : lib_plankton.zoo_widget.interface_widget;
switch (view_kind)
{
case _dali.enum_view_kind.regular:
{
widget_slider = new lib_plankton.zoo_widget.class_slider(
[
new lib_plankton.zoo_widget.class_bunch(
[
this.widget_sources,
this.widget_multiview,
]
),
],
{
"threshold": 100,
"initial_index": 1,
}
);
break;
}
case _dali.enum_view_kind.touch:
{
widget_slider = new lib_plankton.zoo_widget.class_slider(
[
this.widget_sources,
this.widget_multiview,
],
{
"threshold": 100,
"initial_index": 2,
}
);
break;
}
}
await widget_slider.load(target_element.querySelector(".widget-overview-body"));
}
_dali.model.listen_reset(
async (priviliged) => {
this.update_sources_and_entries({"priviliged": priviliged});
target_element.querySelector(".widget-overview-hint").classList.toggle("widget-overview-hint-hidden", priviliged);
}
);
return Promise.resolve<void>(undefined);
}
}
}

View file

@ -0,0 +1,59 @@
.widget-overview-head
{
padding-bottom: 12px;
margin-bottom: 12px;
border-bottom: 1px solid;
}
.widget-overview-hint
{
text-align: center;
font-weight: bold;
}
.widget-overview-hint.widget-overview-hint-hidden
{
display: none;
}
.widget-overview-body
{
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.widget-overview-pane-left
{
flex-basis: 12.5%;
flex-grow: 0;
flex-shrink: 1;
}
.widget-overview-pane-right
{
flex-basis: 87.5%;
flex-grow: 1;
flex-shrink: 1;
}
.widget-overview-body .widget-slider-slide .widget-bunch
{
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.widget-overview-body .widget-slider-slide .widget-bunch-element:nth-child(1)
{
flex-grow: 0;
flex-shrink: 1;
flex-basis: 12.5%;
}
.widget-overview-body .widget-slider-slide .widget-bunch-element:nth-child(2)
{
flex-grow: 1;
flex-shrink: 1;
flex-basis: 87.5%;
}

View file

@ -0,0 +1,8 @@
<div class="widget-overview">
<div class="widget-overview-head">
<div class="widget-overview-hint">
</div>
</div>
<div class="widget-overview-body">
</div>
</div>

View file

@ -1,61 +1,255 @@
namespace _zeitbild.frontend_web.widgets.sources /*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{ {
/** /**
*/ */
type type_entry = { type type_entry = {
id : _zeitbild.frontend_web.type.calendar_id; id : _dali.type_calendar_id;
name : string; name : string;
access_level : _zeitbild.frontend_web.type.enum_access_level; hue : float;
access_level : _dali.enum_access_level;
}; };
/** /**
*/ */
export class class_widget_sources extends _zeitbild.class_widget export class class_widget_sources implements lib_plankton.zoo_widget.interface_widget
{ {
/** /**
* [dependency]
*/ */
private keys : Array<string>; private get_entries : (() => Promise<Array<type_entry>>);
/** /**
* [hook]
*/ */
private data : Record<string, type_entry>; private action_toggle : ((entry : type_entry, mode : boolean) => void);
/** /**
* [hook]
*/ */
private action_select : ((entry : type_entry) => void); private action_select : ((entry : type_entry) => void);
/**
* [hook]
*/
private action_add : (() => void);
/**
* [state]
*/
private priviliged : boolean;
/**
* [state]
*/
private container : (null | Element);
/** /**
*/ */
public constructor( public constructor(
entries : Array<type_entry>, get_entries : (() => Promise<Array<type_entry>>),
options : { {
"action_select": action_select = ((calendar_id) => {}),
"action_toggle": action_toggle = ((calendar_id, mode) => {}),
"action_add": action_add = (() => {}),
"initial_priviliged": initial_priviliged = false,
}
:
{
action_select ?: ((entry : type_entry) => void); action_select ?: ((entry : type_entry) => void);
} = {} action_toggle ?: ((entry : type_entry, mode : boolean) => void);
action_add ?: (() => void);
initial_priviliged ?: boolean;
}
=
{
}
) )
{ {
options = Object.assign( // dependencies
{ this.get_entries = get_entries;
"action_select": (calendar_id) => {},
}, // hooks
options this.action_select = action_select;
this.action_toggle = action_toggle;
this.action_add = action_add;
// state
this.priviliged = initial_priviliged;
this.container = null;
}
/**
*/
private static id_encode(
id : _dali.type_calendar_id
)
: string
{
return id.toFixed(0);
}
/**
*/
private static id_decode(
representation : string
)
: _dali.type_calendar_id
{
return parseInt(representation);
}
/**
*/
public async update(
{
"priviliged": priviliged = null,
}
:
{
priviliged ?: boolean;
}
=
{
}
)
: Promise<void>
{
if (priviliged === null)
{
// do nothing
}
else
{
this.priviliged = priviliged;
}
const data : lib_plankton.map.type_map<_dali.type_calendar_id, type_entry> = lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make(
calendar_id => class_widget_sources.id_encode(calendar_id),
{
"pairs": (
(await this.get_entries())
.map(
entry => (
{
"key": entry.id,
"value": entry,
}
)
)
),
}
)
); );
super(); // structure
this.keys = []; {
this.data = {}; this.container.innerHTML = await _dali.helpers.template_coin(
entries.forEach( "widget-sources",
(entry) => { "main",
const key : string = entry.id.toFixed(0); {
this.keys.push(key); "label_create": lib_plankton.translate.get("widget.sources.create"),
this.data[key] = entry; "entries": (
} (
); await _dali.helpers.promise_row<string>(
this.action_select = options.action_select; lib_plankton.map.dump(data)
.map(
(pair) => () => {
return _dali.helpers.template_coin(
"widget-sources",
"entry",
{
"name": pair.value.name,
// "access_level": entry.access_level, // TODO
"color": _dali.helpers.event_color(pair.value.hue),
"rel": class_widget_sources.id_encode(pair.key),
}
);
}
)
)
)
.join("")
),
}
);
this.container.querySelector(".sources").classList.toggle("sources-priviliged", this.priviliged);
}
// listeners
{
this.container.querySelector(".sources-create").addEventListener(
"click",
(event) => {
event.preventDefault();
this.action_add();
}
);
this.container.querySelectorAll(".sources-entry-visibility").forEach(
(element) => {
element.addEventListener(
"change",
() => {
const mode : boolean = (element as HTMLInputElement).checked;
const key_encoded : string = element.parentElement.getAttribute("rel");
const calendar_id : _dali.type_calendar_id = class_widget_sources.id_decode(key_encoded);
const entry : type_entry = data.get(calendar_id);
element.parentElement.classList.toggle("sources-entry-hidden", (! mode));
this.action_toggle(entry, mode);
}
);
}
);
this.container.querySelectorAll(".sources-entry").forEach(
(element) => {
element.addEventListener(
"click",
(event) => {
if ((event.target as Element).classList.contains("sources-entry-visibility"))
{
// do nothing
}
else
{
const key_encoded : string = element.getAttribute("rel");
const calendar_id : _dali.type_calendar_id = class_widget_sources.id_decode(key_encoded);
const entry : type_entry = data.get(calendar_id);
this.action_select(entry);
}
}
);
}
);
}
} }
@ -64,59 +258,11 @@ namespace _zeitbild.frontend_web.widgets.sources
*/ */
public async load( public async load(
target_element : Element target_element : Element
) : Promise<void> )
: Promise<void>
{ {
target_element.innerHTML = await _zeitbild.frontend_web.helpers.template_coin( this.container = target_element;
"widget-sources", await this.update();
"main",
{
"entries": (
(
await _zeitbild.frontend_web.helpers.promise_row<string>(
this.keys
.map(
(key) => () => {
const entry : type_entry = this.data[key];
return _zeitbild.frontend_web.helpers.template_coin(
"widget-sources",
"entry",
{
"name": entry.name,
// "access_level": entry.access_level, // TODO
// TODO centralize
"color": lib_plankton.color.output_hex(
lib_plankton.color.give_generic(
(entry.id - 1),
{
"saturation": 0.375,
"value": 0.375,
}
),
),
"rel": key,
}
);
}
)
)
)
.join("")
),
}
);
target_element.querySelectorAll(".sources-entry").forEach(
(element) => {
element.addEventListener(
"click",
(event) => {
const key : string = element.getAttribute("rel");
const entry : type_entry = this.data[key];
this.action_select(entry);
}
);
}
);
return Promise.resolve<void>(undefined);
} }
} }

View file

@ -0,0 +1,36 @@
.sources
{
font-size: 0.75em;
}
.sources:not(.sources-priviliged) > .sources-create
{
display: none;
}
.sources-create
{
margin-left: 8px;
}
.sources-entries
{
margin: 0;
padding: 0;
list-style-type: none;
}
.sources-entry
{
margin: 8px;
padding: 4px;
cursor: pointer;
}
/*
.sources-entry-hidden
{
filter: saturate(0);
}
*/

View file

@ -1 +1,4 @@
<li class="sources-entry" style="background-color: {{color}}" rel="{{rel}}">{{name}}</li> <li class="sources-entry" style="background-color: {{color}}" rel="{{rel}}">
<input class="sources-entry-visibility" type="checkbox" checked="checked"/>
<span class="sources-entry-name">{{name}}</span>
</li>

View file

@ -1,3 +1,6 @@
<ul class="sources"> <div class="sources">
{{entries}} <ul class="sources-entries">
</ul> {{entries}}
</ul>
<a class="sources-create" href="">{{label_create}}</a>
</div>

View file

@ -0,0 +1,167 @@
/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets
{
/**
*/
export class class_widget_special_number_input
implements lib_plankton.zoo_widget.interface_widget
{
/**
*/
private action_change : ((int) => Promise<void>);
/**
*/
private label : (null | string);
/**
*/
private minimum : (null | int);
/**
*/
private maximum : (null | int);
/**
*/
private value : int;
/**
*/
public constructor(
{
"label": label = null,
"minimum": minimum = null,
"maximum": maximum = null,
"initial_value": initial_value = 0,
"action_change": action_change = ((value) => Promise.resolve<void>(undefined)),
}
:
{
label ?: (null | string);
minimum ?: (null | int);
maximum ?: (null | int);
initial_value ?: int;
action_change ?: ((int) => Promise<void>);
}
=
{
}
)
{
this.label = label;
this.minimum = minimum;
this.maximum = maximum;
this.value = initial_value;
this.action_change = action_change;
}
/**
* [implementation]
*/
public async load(
target_element : HTMLElement
)
: Promise<void>
{
const dom_root = await _dali.helpers.element_from_template(
"widget-special_number_input",
"main",
{
"label": this.label,
}
);
const dom_input : HTMLInputElement = (dom_root.querySelector(".widget-special_number_input-input") as HTMLInputElement);
// listeners
{
dom_input.addEventListener(
"change",
() => {
const value : int = parseInt(dom_input.value);
if (
((this.minimum === null) || (value >= this.minimum))
&&
((this.maximum === null) || (value <= this.maximum))
)
{
this.value = value;
this.action_change(this.value);
}
else
{
// do nothing
}
}
);
dom_root.querySelector(".widget-special_number_input-prev").addEventListener(
"click",
() => {
if ((this.minimum === null) || (this.value > this.minimum))
{
this.value -= 1;
dom_input.value = this.value.toFixed(0);
this.action_change(this.value);
}
else
{
// do nothing
}
}
);
dom_root.querySelector(".widget-special_number_input-next").addEventListener(
"click",
() => {
if ((this.maximum === null) || (this.value < this.maximum))
{
this.value += 1;
dom_input.value = this.value.toFixed(0);
this.action_change(this.value);
}
else
{
// do nothing
}
}
);
}
// content
{
dom_input.value = this.value.toFixed(0);
}
target_element.appendChild(dom_root);
}
}
}

View file

@ -0,0 +1,34 @@
.widget-special_number_input-controls
{
white-space: nowrap;
}
.widget-special_number_input-controls > *
{
display: inline-block;
}
.widget-special_number_input-button
{
padding: 4px 4px;
margin: 0;
cursor: pointer;
}
.widget-special_number_input-label
{
display: block;
font-size: 0.75em;
/*
text-transform: uppercase;
*/
}
.widget-special_number_input-input
{
max-width: 40px;
margin: 0;
text-align: center;
}

View file

@ -0,0 +1,8 @@
<div class="widget-special_number_input">
<label class="widget-special_number_input-label">{{label}}</label>
<div class="widget-special_number_input-controls">
<div class="widget-special_number_input-button widget-special_number_input-prev">&#x25C2;</div>
<input type="text" class="widget-special_number_input-input" pattern="-?[0-9]+"/>
<div class="widget-special_number_input-button widget-special_number_input-next">&#x25B8;</div>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,119 @@
.weekview-controls
{
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: right;
margin-bottom: 12px;
text-align: center;
}
.weekview-controls > *
{
flex-basis: 0;
flex-grow: 0;
flex-shrink: 1;
margin: 0 0 0 16px;
}
.weekview-control-label
{
display: block;
font-size: 0.75em;
/*
text-transform: uppercase;
*/
}
.weekview-table table
{
width: 100%;
border-collapse: collapse;
}
.weekview-cell
{
border: 1px solid hsl(0,0%,37.5%);
padding: 8px;
vertical-align: top;
}
.weekview-cell-day
{
/* todo */
width: 13.5%;
}
.weekview-cell-week
{
/* todo */
width: 5.5%;
}
.weekview-cell-regular
{
/* todo */
width: 13.5%;
height: 100px;
cursor: copy;
}
.weekview-cell-today
{
outline: 2px solid hsl(0, 0%, 100%);
}
.weekview-cell-hidden
{
display: none;
}
.weekview-day
{
font-size: 0.75em;
}
.weekview-events
{
margin: 0;
padding: 0;
list-style-type: none;
}
.weekview-event_entry
{
margin: 4px;
padding: 4px;
border-radius: 2px;
font-size: 0.75em;
color: #FFF;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
min-width: 75px;
word-wrap: anywhere;
}
.weekview-event_entry.access_level-none
,
.weekview-event_entry.access_level-view
{
/*
cursor: default;
*/
cursor: pointer;
}
.weekview-event_entry.access_level-edit
,
.weekview-event_entry.access_level-admin
{
cursor: pointer;
}

View file

@ -0,0 +1,6 @@
<div class="weekview-control weekview-control-vertical">
<label class="weekview-control-label">{{label}}</label>
<div class="weekview-control-input">
<input type="checkbox"/>
</div>
</div>

View file

@ -1,32 +1,9 @@
<div class="weekview"> <div class="weekview">
<div class="weekview-controls"> <div class="weekview-controls">
<label class="weekview-control weekview-control-year">
<span>{{label_control_year}}</span>
<input type="number"/>
</label>
<label class="weekview-control weekview-control-week">
<span>{{label_control_week}}</span>
<input type="number"/>
</label>
<label class="weekview-control weekview-control-count">
<span>{{label_control_count}}</span>
<input type="number"/>
</label>
<input type="submit" class="weekview-control weekview-control-apply" value="{{label_control_apply}}"/>
</div> </div>
<div class="weekview-table"> <div class="weekview-table">
<table> <table>
<thead> <thead>
<tr>
<th class="calendar-cell"></th>
<th class="calendar-cell calendar-cell-day">Mo</th>
<th class="calendar-cell calendar-cell-day">Di</th>
<th class="calendar-cell calendar-cell-day">Mi</th>
<th class="calendar-cell calendar-cell-day">Do</th>
<th class="calendar-cell calendar-cell-day">Fr</th>
<th class="calendar-cell calendar-cell-day">Sa</th>
<th class="calendar-cell calendar-cell-day">So</th>
</tr>
</thead> </thead>
<tbody> <tbody>
</tbody> </tbody>

View file

@ -1,3 +1,3 @@
<li class="calendar-event_entry{{additional_classes}}" style="background-color: {{color}};" title="{{title}}" rel="{{rel}}"> <li class="weekview-event_entry{{additional_classes}}" style="background-color: {{color}};" rel="{{rel}}">
{{name}} {{name}}
</li> </li>

View file

@ -1,8 +1,8 @@
<td class="calendar-cell calendar-cell-regular{{extra_classes}}" rel="{{rel}}"> <td class="weekview-cell weekview-cell-regular{{extra_classes}}" rel="{{rel}}">
<span class="calendar-day" title="{{title}}"> <span class="weekview-day">
{{day}} {{day}}
</span> </span>
<ul class="calendar-events"> <ul class="weekview-events">
{{entries}} {{entries}}
</ul> </ul>
</td> </td>

View file

@ -1,5 +1,5 @@
<tr> <tr>
<th class="calendar-cell calendar-cell-week"> <th class="weekview-cell weekview-cell-week">
{{week}} {{week}}
</th> </th>
{{cells}} {{cells}}

2
todo.md Normal file
View file

@ -0,0 +1,2 @@
- unterschiedliche Ansichten für Betrachten und Bearbeiten von Terminen und Kalendern
- zu verwendende Zeitzone nicht fest auf mitteleuropäische setzen

View file

@ -12,7 +12,7 @@ def main():
"-o", "-o",
"--output-directory", "--output-directory",
type = str, type = str,
default = "/tmp/zeitbild-frontend-web", default = "/tmp/dali",
metavar = "<output-directory>", metavar = "<output-directory>",
help = "output directory", help = "output directory",
) )
@ -24,16 +24,24 @@ def main():
metavar = "<conf-path>", metavar = "<conf-path>",
help = "conf path", help = "conf path",
) )
argument_parser.add_argument(
"-f",
"--force",
action = 'store_true',
default = False,
help = "force",
)
args = argument_parser.parse_args() args = argument_parser.parse_args()
## exec ## exec
targets = [] targets = []
targets.append("default") targets.append("default")
_os.system( _os.system(
"make dir_build=%s --file=tools/makefile %s" "make dir_build=%s --file=tools/makefile %s%s"
% ( % (
args.output_directory, args.output_directory,
" ".join(targets), " ".join(targets),
(" --always-make" if args.force else ""),
) )
) )
if True: if True:

View file

@ -19,16 +19,16 @@ def main():
"--target-directory", "--target-directory",
type = str, type = str,
dest = "target_directory", dest = "target_directory",
default = "/opt/zbwf", default = "/opt/dali",
metavar = "<target-directory>", metavar = "<target-directory>",
help = "directory on the target system, where the files shall be put; default: /opt/zbwf", help = "directory on the target system, where the files shall be put; default: /opt/dali",
) )
argument_parser.add_argument( argument_parser.add_argument(
"-b", "-b",
"--build-directory", "--build-directory",
type = str, type = str,
dest = "build_directory", dest = "build_directory",
default = "build", default = "/tmp/dali",
metavar = "<build-directory>", metavar = "<build-directory>",
help = "directory to where the build was put", help = "directory to where the build was put",
) )

View file

@ -31,14 +31,6 @@ def main(
dest = "index_template_path", dest = "index_template_path",
type = str, type = str,
) )
argument_parser.add_argument(
"-t",
"--template",
dest = "template_paths",
type = str,
action = "append",
default = []
)
args = argument_parser.parse_args() args = argument_parser.parse_args()
## exec ## exec
@ -46,12 +38,6 @@ def main(
string_coin( string_coin(
file_read(args.index_template_path), file_read(args.index_template_path),
{ {
"templates": "".join(
map(
file_read,
args.template_paths
)
),
} }
) )
) )

View file

@ -26,114 +26,123 @@ ${dir_build}/index.html: \
${dir_source}/index.html.tpl ${dir_source}/index.html.tpl
@ ${cmd_log} "index …" @ ${cmd_log} "index …"
@ ${cmd_mkdir} $(dir $@) @ ${cmd_mkdir} $(dir $@)
@ tools/make-index \ @ tools/make-index ${dir_source}/index.html.tpl > $@
${dir_source}/index.html.tpl \
> $@
.PHONY: templates .PHONY: templates
templates: \ templates: \
templates-widgets-special_number_input \
templates-widgets-login_oidc \
templates-widgets-menu \
templates-widgets-sources \ templates-widgets-sources \
templates-widgets-calendar_edit \
templates-widgets-event_edit \
templates-widgets-caldav \
templates-widgets-listview \ templates-widgets-listview \
templates-widgets-weekview \ templates-widgets-weekview \
templates-pages-calendar_add \ templates-widgets-overview \
templates-pages-calendar_edit \ templates-widgets-mode_switcher
templates-pages-event_add \
templates-pages-event_edit \ .PHONY: templates-widgets-special_number_input
templates-pages-overview \ templates-widgets-special_number_input: \
templates-pages-login $(wildcard ${dir_source}/widgets/special_number_input/templates/*)
@ ${cmd_log} "templates:widget:special_number_input …"
@ ${cmd_mkdir} ${dir_build}/templates/widget-special_number_input
@ ${cmd_cp} -r -u -v ${dir_source}/widgets/special_number_input/templates/* ${dir_build}/templates/widget-special_number_input/
.PHONY: templates-widgets-login_oidc
templates-widgets-login_oidc: \
$(wildcard ${dir_source}/widgets/login_oidc/templates/*)
@ ${cmd_log} "templates:widget:login_oidc …"
@ ${cmd_mkdir} ${dir_build}/templates/widget-login_oidc
@ ${cmd_cp} -r -u -v ${dir_source}/widgets/login_oidc/templates/* ${dir_build}/templates/widget-login_oidc/
.PHONY: templates-widgets-menu
templates-widgets-menu: \
$(wildcard ${dir_source}/widgets/menu/templates/*)
@ ${cmd_log} "templates:widget:menu …"
@ ${cmd_mkdir} ${dir_build}/templates/widget-menu
@ ${cmd_cp} -r -u -v ${dir_source}/widgets/menu/templates/* ${dir_build}/templates/widget-menu/
.PHONY: templates-widgets-sources .PHONY: templates-widgets-sources
templates-widgets-sources: \ templates-widgets-sources: \
$(wildcard ${dir_source}/widgets/sources/templates/*) $(wildcard ${dir_source}/widgets/sources/templates/*)
@ ${cmd_log} "templates:widgets:sources …" @ ${cmd_log} "templates:widget:sources …"
@ ${cmd_mkdir} ${dir_build}/templates/widget-sources @ ${cmd_mkdir} ${dir_build}/templates/widget-sources
@ ${cmd_cp} -r -u -v ${dir_source}/widgets/sources/templates/* ${dir_build}/templates/widget-sources/ @ ${cmd_cp} -r -u -v ${dir_source}/widgets/sources/templates/* ${dir_build}/templates/widget-sources/
.PHONY: templates-widgets-calendar_edit
templates-widgets-calendar_edit: \
$(wildcard ${dir_source}/widgets/calendar_edit/templates/*)
@ ${cmd_log} "templates:widget:calendar_edit …"
@ ${cmd_mkdir} ${dir_build}/templates/widget-calendar_edit
@ ${cmd_cp} -r -u -v ${dir_source}/widgets/calendar_edit/templates/* ${dir_build}/templates/widget-calendar_edit/
.PHONY: templates-widgets-event_edit
templates-widgets-event_edit: \
$(wildcard ${dir_source}/widgets/event_edit/templates/*)
@ ${cmd_log} "templates:widget:event_edit …"
@ ${cmd_mkdir} ${dir_build}/templates/widget-event_edit
@ ${cmd_cp} -r -u -v ${dir_source}/widgets/event_edit/templates/* ${dir_build}/templates/widget-event_edit/
.PHONY: templates-widgets-caldav
templates-widgets-caldav: \
$(wildcard ${dir_source}/widgets/caldav/templates/*)
@ ${cmd_log} "templates:widget:caldav …"
@ ${cmd_mkdir} ${dir_build}/templates/widget-caldav
@ ${cmd_cp} -r -u -v ${dir_source}/widgets/caldav/templates/* ${dir_build}/templates/widget-caldav/
.PHONY: templates-widgets-listview .PHONY: templates-widgets-listview
templates-widgets-listview: \ templates-widgets-listview: \
$(wildcard ${dir_source}/widgets/listview/templates/*) $(wildcard ${dir_source}/widgets/listview/templates/*)
@ ${cmd_log} "templates:widgets:listview …" @ ${cmd_log} "templates:widget:listview …"
@ ${cmd_mkdir} ${dir_build}/templates/widget-listview @ ${cmd_mkdir} ${dir_build}/templates/widget-listview
@ ${cmd_cp} -r -u -v ${dir_source}/widgets/listview/templates/* ${dir_build}/templates/widget-listview/ @ ${cmd_cp} -r -u -v ${dir_source}/widgets/listview/templates/* ${dir_build}/templates/widget-listview/
.PHONY: templates-widgets-weekview .PHONY: templates-widgets-weekview
templates-widgets-weekview: \ templates-widgets-weekview: \
$(wildcard ${dir_source}/widgets/weekview/templates/*) $(wildcard ${dir_source}/widgets/weekview/templates/*)
@ ${cmd_log} "templates:widgets:weekview …" @ ${cmd_log} "templates:widget:weekview …"
@ ${cmd_mkdir} ${dir_build}/templates/widget-weekview @ ${cmd_mkdir} ${dir_build}/templates/widget-weekview
@ ${cmd_cp} -r -u -v ${dir_source}/widgets/weekview/templates/* ${dir_build}/templates/widget-weekview/ @ ${cmd_cp} -r -u -v ${dir_source}/widgets/weekview/templates/* ${dir_build}/templates/widget-weekview/
.PHONY: templates-pages-calendar_add .PHONY: templates-widgets-overview
templates-pages-calendar_add: \ templates-widgets-overview: \
$(wildcard ${dir_source}/pages/calendar_add/templates/*) $(wildcard ${dir_source}/widgets/overview/templates/*)
@ ${cmd_log} "templates:calendar_add …" @ ${cmd_log} "templates:widget:overview …"
@ ${cmd_mkdir} ${dir_build}/templates/calendar_add @ ${cmd_mkdir} ${dir_build}/templates/widget-overview
@ ${cmd_cp} -r -u -v ${dir_source}/pages/calendar_add/templates/* ${dir_build}/templates/calendar_add/ @ ${cmd_cp} -r -u -v ${dir_source}/widgets/overview/templates/* ${dir_build}/templates/widget-overview/
.PHONY: templates-pages-calendar_edit .PHONY: templates-widgets-mode_switcher
templates-pages-calendar_edit: \ templates-widgets-mode_switcher: \
$(wildcard ${dir_source}/pages/calendar_edit/templates/*) $(wildcard ${dir_source}/widgets/mode_switcher/templates/*)
@ ${cmd_log} "templates:calendar_edit …" @ ${cmd_log} "templates:widget:mode_switcher …"
@ ${cmd_mkdir} ${dir_build}/templates/calendar_edit @ ${cmd_mkdir} ${dir_build}/templates/widget-mode_switcher
@ ${cmd_cp} -r -u -v ${dir_source}/pages/calendar_edit/templates/* ${dir_build}/templates/calendar_edit/ @ ${cmd_cp} -r -u -v ${dir_source}/widgets/mode_switcher/templates/* ${dir_build}/templates/widget-mode_switcher/
.PHONY: templates-pages-event_add
templates-pages-event_add: \
$(wildcard ${dir_source}/pages/event_add/templates/*)
@ ${cmd_log} "templates:event_add …"
@ ${cmd_mkdir} ${dir_build}/templates/event_add
@ ${cmd_cp} -r -u -v ${dir_source}/pages/event_add/templates/* ${dir_build}/templates/event_add/
.PHONY: templates-pages-event_edit
templates-pages-event_edit: \
$(wildcard ${dir_source}/pages/event_edit/templates/*)
@ ${cmd_log} "templates:event_edit …"
@ ${cmd_mkdir} ${dir_build}/templates/event_edit
@ ${cmd_cp} -r -u -v ${dir_source}/pages/event_edit/templates/* ${dir_build}/templates/event_edit/
.PHONY: templates-pages-overview
templates-pages-overview: \
$(wildcard ${dir_source}/pages/overview/templates/*)
@ ${cmd_log} "templates:overview …"
@ ${cmd_mkdir} ${dir_build}/templates/overview
@ ${cmd_cp} -r -u -v ${dir_source}/pages/overview/templates/* ${dir_build}/templates/overview/
.PHONY: templates-pages-login
templates-pages-login: \
$(wildcard ${dir_source}/pages/login/templates/*)
@ ${cmd_log} "templates:login …"
@ ${cmd_mkdir} ${dir_build}/templates/login
@ ${cmd_cp} -r -u -v ${dir_source}/pages/login/templates/* ${dir_build}/templates/login/
.PHONY: style .PHONY: style
style: \ style: ${dir_build}/style.css
$(wildcard ${dir_source}/style/*)
${dir_build}/style.css: \
$(wildcard ${dir_source}/style/*.css) \
$(wildcard ${dir_source}/widgets/*/style.css)
@ ${cmd_log} "style …" @ ${cmd_log} "style …"
@ ${cmd_mkdir} ${dir_build} @ ${cmd_mkdir} ${dir_build}
@ ${cmd_cat} ${dir_source}/style/* > ${dir_build}/style.css @ ${cmd_cat} $^ > $@
.PHONY: logic .PHONY: logic
logic: ${dir_build}/logic.js logic: ${dir_build}/logic.js
${dir_temp}/logic-unlinked.js: \ ${dir_temp}/logic-unlinked.js: \
${dir_lib}/plankton/plankton.d.ts \ ${dir_lib}/plankton/plankton.d.ts \
${dir_source}/logic/helpers.ts \ ${dir_source}/resources/conf.ts \
${dir_source}/logic/widget.ts \ ${dir_source}/resources/backend.ts \
${dir_source}/logic/conf.ts \ ${dir_source}/base.ts \
${dir_source}/logic/types.ts \ ${dir_source}/types.ts \
${dir_source}/logic/backend.ts \ ${dir_source}/model.ts \
${dir_source}/widgets/sources/logic.ts \ ${dir_source}/overlay.ts \
${dir_source}/widgets/listview/logic.ts \ ${dir_source}/helpers.ts \
${dir_source}/widgets/weekview/logic.ts \ $(wildcard ${dir_source}/widgets/*/logic.ts) \
${dir_source}/pages/login/logic.ts \ ${dir_source}/main.ts
${dir_source}/pages/logout/logic.ts \
${dir_source}/pages/oidc_finish/logic.ts \
${dir_source}/pages/calendar_add/logic.ts \
${dir_source}/pages/calendar_edit/logic.ts \
${dir_source}/pages/event_add/logic.ts \
${dir_source}/pages/event_edit/logic.ts \
${dir_source}/pages/overview/logic.ts \
${dir_source}/logic/main.ts
@ ${cmd_log} "logic | compile …" @ ${cmd_log} "logic | compile …"
@ ${cmd_mkdir} $(dir $@) @ ${cmd_mkdir} $(dir $@)
@ ${cmd_tsc} --lib dom,es2020 $^ --outFile $@ # --strict @ ${cmd_tsc} --lib dom,es2020 $^ --outFile $@ # --strict

View file

@ -2,7 +2,7 @@
## consts ## consts
dir=lib/plankton dir=$(pwd)/lib/plankton
modules="" modules=""
modules="${modules} base" modules="${modules} base"
@ -12,9 +12,12 @@ modules="${modules} storage"
modules="${modules} file" modules="${modules} file"
modules="${modules} json" modules="${modules} json"
modules="${modules} string" modules="${modules} string"
modules="${modules} random"
modules="${modules} map" modules="${modules} map"
modules="${modules} set"
modules="${modules} cache"
modules="${modules} color" modules="${modules} color"
modules="${modules} xml" # modules="${modules} xml"
modules="${modules} map" modules="${modules} map"
modules="${modules} http" modules="${modules} http"
modules="${modules} log" modules="${modules} log"
@ -22,6 +25,7 @@ modules="${modules} url"
modules="${modules} pit" modules="${modules} pit"
modules="${modules} www_form" modules="${modules} www_form"
modules="${modules} translate" modules="${modules} translate"
modules="${modules} zoo-widget"
modules="${modules} zoo-page" modules="${modules} zoo-page"
modules="${modules} zoo-form" modules="${modules} zoo-form"
modules="${modules} zoo-input" modules="${modules} zoo-input"
@ -29,6 +33,14 @@ modules="${modules} zoo-input"
## exec ## exec
mkdir -p ${dir}
mkdir /tmp/sandbox -p
cd /tmp/sandbox
ptk fetch web ${modules}
schwamm --include=/tmp/sandbox/plankton.swm.json --output=dump:logic-decl > ${dir}/plankton.d.ts
schwamm --include=/tmp/sandbox/plankton.swm.json --output=dump:logic-impl > ${dir}/plankton.js
exit
mkdir -p ${dir} mkdir -p ${dir}
cd ${dir} cd ${dir}
ptk bundle web ${modules} ptk bundle web ${modules}