This commit is contained in:
Fenris Wolf 2024-09-11 17:24:20 +02:00
parent 49b0da5f5b
commit 12697e2a7a
24 changed files with 5970 additions and 2171 deletions

5
conf/example.json Normal file
View file

@ -0,0 +1,5 @@
{
"view_mode": "table",
"calendar_id": 6,
"timezone_shift": 0
}

View file

@ -3,93 +3,119 @@
{
"id": 1,
"object": {
"name": "Anton"
"name": "christian.frass"
}
},
{
"id": 2,
"object": {
"name": "Berta"
"name": "andre.weichert"
}
},
{
"id": 3,
"object": {
"name": "Caesar"
"name": "steffen.doegnitz"
}
},
{
"id": 4,
"object": {
"name": "frank.dietrich"
}
},
{
"id": 5,
"object": {
"name": "michael.berger"
}
},
{
"id": 6,
"object": {
"name": "roland.schroeder"
}
},
{
"id": 7,
"object": {
"name": "rene.hahn"
}
},
{
"id": 8,
"object": {
"name": "max.meierhof"
}
},
{
"id": 9,
"object": {
"name": "klaus.kleba"
}
},
{
"id": 10,
"object": {
"name": "tim.detzner"
}
}
],
"calendars": [
{
"id": 1,
"id": 5,
"object": {
"kind": "concrete",
"data": {
"name": "Garten",
"name": "BV",
"private": false,
"hue": 0.0000000000000000,
"users": [
{
"id": 1,
"role": "editor"
}
],
"events": [
{
"name": "Unkraut jähten",
"name": "9. Bundesparteitag | 1. Sitzung | Tag 1",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 12},
"date": {"year": 2024, "month": 10, "day": 18},
"time": {"hour": 10, "minute": 0, "second": 0}
},
"end": null,
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 10, "day": 18},
"time": {"hour": 18, "minute": 0, "second": 0}
},
"location": "Halle",
"description": null
},
{
"name": "Kartoffeln ernten",
"name": "9. Bundesparteitag | 1. Sitzung | Tag 2",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 24},
"time": {"hour": 17, "minute": 0, "second": 0}
"date": {"year": 2024, "month": 10, "day": 19},
"time": {"hour": 10, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 24},
"time": {"hour": 20, "minute": 0, "second": 0}
"date": {"year": 2024, "month": 10, "day": 19},
"time": {"hour": 18, "minute": 0, "second": 0}
},
"location": "Halle",
"description": null
}
]
}
}
},
{
"id": 2,
"object": {
"kind": "concrete",
"data": {
"name": "Kochen",
"users": [
{
"id": 1,
"role": "editor"
},
{
"id": 2,
"role": "viewer"
}
],
"events": [
{
"name": "Krautnudeln",
"name": "9. Bundesparteitag | 1. Sitzung | Tag 3",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 21},
"time": {"hour": 17, "minute": 0, "second": 0}
"date": {"year": 2024, "month": 10, "day": 20},
"time": {"hour": 10, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 21},
"time": {"hour": 19, "minute": 0, "second": 0}
"date": {"year": 2024, "month": 10, "day": 20},
"time": {"hour": 18, "minute": 0, "second": 0}
},
"location": "Halle",
"description": null
}
]
@ -101,26 +127,70 @@
"object": {
"kind": "concrete",
"data": {
"name": "Band",
"name": "LV Sachsen",
"private": false,
"hue": 0.6180339887498949,
"users": [
{
"id": 2,
"id": 9,
"role": "editor"
}
],
"events": [
{
"name": "Auftritt im Park",
"name": "Sören Pellmann zu den Landtagswahlen im Osten",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 21},
"time": {"hour": 20, "minute": 0, "second": 0}
"date": {"year": 2024, "month": 9, "day": 11},
"time": {"hour": 18, "minute": 0, "second": 0}
},
"end": null,
"location": "online: https://v2202002113208108062.supersrv.de/b/har-jbu-lxy-rx1",
"description": null
},
{
"name": "Erinnern versammeln. Praktiken für die Zukünfte einer Gesellschaft der Vielen.",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 13},
"time": {"hour": 17, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 21},
"date": {"year": 2024, "month": 9, "day": 15},
"time": null
},
"location": "Weltecho, Annaberger Straße 24, 09111 Chemnitz",
"description": null
},
{
"name": "Parteikonvent zur Auswertung des Wahljahres",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 14},
"time": {"hour": 10, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 14},
"time": {"hour": 16, "minute": 30, "second": 0}
},
"location": "Veranstaltungs- und Kulturforum STADTPARK | Hammertal 3 | 09669 Frankenberg/Sachsen",
"description": null
},
{
"name": "Ist die extreme Rechte noch zu stoppen?",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 19},
"time": {"hour": 19, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 19},
"time": {"hour": 21, "minute": 0, "second": 0}
},
"location": "online: https://www.dielinke-sachsen.de/termine/?termin_ort=digital-internet-stream",
"description": null
}
]
@ -130,18 +200,134 @@
{
"id": 4,
"object": {
"kind": "caldav",
"kind": "concrete",
"data": {
"source_url": "https://export.kalender.digital/ics/0/3e10dae66950379d4cc8/gesamterkalender.ics?past_months=3&future_months=36"
"name": "KV Zwickau",
"private": false,
"hue": 0.4721359549995796,
"events": [
{
"name": "Vorstands-Sitzung",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 5},
"time": {"hour": 18, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 5},
"time": {"hour": 21, "minute": 0, "second": 0}
},
"location": "Zwickau, Innere Schneeberger Straße 17",
"description": null
}
]
}
}
},
{
"id": 5,
"id": 1,
"object": {
"kind": "caldav",
"kind": "concrete",
"data": {
"source_url": "https://roydfalk:uywvui66svLHFMUp6LndxNO9ZASNrY9vmUDcAPPNIr7PWmjAMpEQce0JiA9AAeUH@vikunja.linke.sx/dav/projects/4/"
"name": "OV Glauchau",
"private": false,
"users": [
{
"id": 1,
"role": "editor"
},
{
"id": 5,
"role": "editor"
},
{
"id": 6,
"role": "editor"
}
],
"hue": 0.09016994374947451,
"events": [
{
"name": "Kinderspieletag",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 8},
"time": {"hour": 12, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 8},
"time": {"hour": 16, "minute": 0, "second": 0}
},
"location": null,
"description": null
},
{
"name": "Mitglieder-Sitzung",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 19},
"time": {"hour": 17, "minute": 30, "second": 0}
},
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 19},
"time": {"hour": 19, "minute": 0, "second": 0}
},
"location": null,
"description": null
}
]
}
}
},
{
"id": 2,
"object": {
"kind": "concrete",
"data": {
"name": "OV Zwickau",
"private": false,
"hue": 0.09016994374947451,
"users": [
{
"id": 7,
"role": "editor"
},
{
"id": 8,
"role": "viewer"
}
],
"events": [
{
"name": "4ter Christopher Street Day",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 8, "day": 31},
"time": {"hour": 10, "minute": 0, "second": 0}
},
"end": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 8, "day": 31},
"time": {"hour": 17, "minute": 0, "second": 0}
},
"location": "Zwickau",
"description": null
},
{
"name": "Regionaltreffen Westsachsen: Schule ohne Rassismus Schule mit Courage",
"begin": {
"timezone_shift": 2,
"date": {"year": 2024, "month": 9, "day": 19},
"time": {"hour": 9, "minute": 0, "second": 0}
},
"end": null,
"location": null,
"description": null
}
]
}
}
},
@ -154,10 +340,24 @@
"sources": [
1,
2,
3
3,
4,
5
]
}
}
},
{
"id": 7,
"object": {
"kind": "caldav",
"data": {
"name": "Lixer",
"private": false,
"read_only": true,
"source_url": "https://export.kalender.digital/ics/0/3e10dae66950379d4cc8/gesamterkalender.ics?past_months=3&future_months=36"
}
}
}
]
}

21
doc/konzept.md Normal file
View file

@ -0,0 +1,21 @@
- Kalender sollen unabhängig von Nutzern bestehen können
- einem Kalender können beliebig viele Nutzer zugeordnet werden, die jeweils bestimmte Berechtigungen haben (z.B. als Rollen "admin", "editor", "viewer", …)
- Berechtigungen:
- Kalender anlegen
- Kalender-Stammdaten ändern
- Kalender-Einträge lesen
- Kalender-Einträge erstellen
- Kalender-Einträge ändern
- Kalender-Einträge entfernen
- Kalender sind für gewöhnlichen öffentlich
- es gibt verschiedene Arten von Kalendern:
- konkret
- enthält Veranstaltungen
- Sammlung
- enthält selbst keine Veranstaltungen
- verweist auf beliebig viele andere Kalender (Kreise vermeiden)
- extern
- über CalDAV
- sollte read-only- und read/write-Modus haben
- nach dem Anmelden sieht man eine Liste mit öffentlichen Kalendern, gruppiert nach jewiliger Rolle
- Entwurfsname: "zeitbild"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,521 +0,0 @@
/**
*/
function date_object_get_week_of_year(
date : Date
) : int
{
let date_ : Date = new Date(date.getTime());
date_.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
date_.setDate(date_.getDate() + 3 - (date_.getDay() + 6) % 7);
// January 4 is always in week 1.
let week1 : Date = new Date(date_.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return (
1
+
Math.round(
(
((date_.getTime() - week1.getTime()) / 86400000)
-
3
+
(week1.getDay() + 6) % 7
)
/
7
)
);
}
/**
* @todo unite with type_datetimeobject?
*/
type type_datetime = {
timezone_shift : int;
date : type_date;
time : (
null
|
type_time
);
};
/**
* @todo timezone_shift
*/
function datetime_from_date_object(
date : Date,
options : {
timezone_shift ?: int;
} = {}
) : type_datetime
{
options = Object.assign(
{
"timezone_shift": 0,
},
options
);
const date_ : Date = lib_plankton.call.convey(
date,
[
(x : Date) => x.getTime(),
(x : int) => (x + (((options.timezone_shift as int) * (60 * 60)) * 1000)),
(x : int) => new Date(x),
]
);
const iso_string : string = date_.toISOString();
return {
"timezone_shift": (options.timezone_shift as int),
"date": {
"year": parseInt(iso_string.slice(0, 4)),
"month": parseInt(iso_string.slice(5, 7)),
"day": parseInt(iso_string.slice(8, 10)),
},
"time": {
"hour": parseInt(iso_string.slice(11, 13)),
"minute": parseInt(iso_string.slice(14, 16)),
"second": parseInt(iso_string.slice(17, 19)),
},
};
}
/**
* @todo negative shift?
*/
function datetime_to_date_object(
datetime : type_datetime
) : Date
{
const iso_string : string = lib_plankton.string.coin(
"{{year}}-{{month}}-{{day}}T{{hour}}:{{minute}}:{{second}}.000+{{shift}}",
{
"year": datetime.date.year.toFixed(0).padStart(4, "0"),
"month": datetime.date.month.toFixed(0).padStart(2, "0"),
"day": datetime.date.day.toFixed(0).padStart(2, "0"),
"hour": ((datetime.time !== null) ? datetime.time.hour : 0).toFixed(0).padStart(2, "0"),
"minute": ((datetime.time !== null) ? datetime.time.minute : 0).toFixed(0).padStart(2, "0"),
"second": ((datetime.time !== null) ? datetime.time.second : 0).toFixed(0).padStart(2, "0"),
"shift": (datetime.timezone_shift.toFixed(0).padStart(2, "0") + ":00"),
}
);
return (new Date(iso_string));
}
/**
*/
type type_pit = int;
/**
*/
function pit_to_date_object(
pit : type_pit
) : Date
{
return (new Date(pit * 1000));
}
/**
*/
function pit_from_date_object(
date_object : Date
) : type_pit
{
return Math.round(date_object.getTime() / 1000);
}
/**
*/
function pit_now(
) : type_pit
{
return pit_from_date_object(new Date(Date.now()));
}
/**
* @todo timezone
*/
function pit_to_datetime(
pit : type_pit,
options : {
timezone_shift ?: int
} = {}
) : type_datetime
{
options = Object.assign(
{
"timezone_shift": 0,
},
options
);
const date_object : Date = pit_to_date_object(pit);
return datetime_from_date_object(
date_object,
{
"timezone_shift": (options.timezone_shift as int),
}
);
}
/**
*/
function pit_from_datetime(
datetime : type_datetime
) : type_pit
{
return lib_plankton.call.convey(
datetime,
[
datetime_to_date_object,
pit_from_date_object,
]
);
}
/**
*/
function pit_is_before(
pit : type_pit,
reference : type_pit
) : boolean
{
return (pit < reference);
}
/**
*/
function pit_is_after(
pit : type_pit,
reference : type_pit
) : boolean
{
return (pit > reference);
}
/**
*/
function pit_is_between(
pit : type_pit,
reference_left : type_pit,
reference_right : type_pit
) : boolean
{
return (
pit_is_after(pit, reference_left)
&&
pit_is_before(pit, reference_right)
);
}
/**
*/
function pit_shift_hour(
pit : type_pit,
increment : int
) : type_pit
{
return (pit + (60 * 60 * increment));
}
/**
*/
function pit_shift_day(
pit : type_pit,
increment : int
) : type_pit
{
return (pit + (60 * 60 * 24 * increment));
}
/**
*/
function pit_shift_week(
pit : type_pit,
increment : int
) : type_pit
{
return (pit + (60 * 60 * 24 * 7 * increment));
}
/**
*/
function pit_shift_year(
pit : type_pit,
increment : int
) : type_pit
{
return (pit + (60 * 60 * 24 * 365 * increment));
}
/**
*/
function pit_trunc_minute(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": datetime_input.date.month,
"day": datetime_input.date.day,
},
"time": {
"hour": (
(datetime_input.time === null)
?
0
:
datetime_input.time.hour
),
"minute": (
(datetime_input.time === null)
?
0
:
datetime_input.time.minute
),
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
*/
function pit_trunc_hour(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": datetime_input.date.month,
"day": datetime_input.date.day,
},
"time": {
"hour": (
(datetime_input.time === null)
?
0
:
datetime_input.time.hour
),
"minute": 0,
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
*/
function pit_trunc_day(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": datetime_input.date.month,
"day": datetime_input.date.day,
},
"time": {
"hour": 0,
"minute": 0,
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
*/
function pit_trunc_week(
pit : type_pit
) : type_pit
{
const date_object : Date = pit_to_date_object(pit);
return lib_plankton.call.convey(
date_object.getDay(),
[
(x : int) => ((x === 0) ? 7 : x),
(x : int) => (x - 1),
(x : int) => pit_shift_day(pit, (-x)),
pit_trunc_day
]
);
}
/**
*/
function pit_trunc_month(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": datetime_input.date.month,
"day": 1,
},
"time": {
"hour": 0,
"minute": 0,
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
*/
function pit_trunc_year(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": 1,
"day": 1,
},
"time": {
"hour": 0,
"minute": 0,
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
* @param year year according to specified timezone shift
* @param week week according to specified timezone shift
* @return the begin of the week (monday, 00:00)
*/
function pit_from_year_and_week(
year : int,
week : int,
options : {
timezone_shift ?: int;
} = {}
) : type_pit
{
options = Object.assign(
{
"timezone_shift": 0,
},
options
);
return lib_plankton.call.convey(
{
"timezone_shift": (options.timezone_shift as int),
"date": {
"year": year,
"month": 1,
"day": 1,
},
"time": {
"hour": 0,
"minute": 0,
"second": 0
}
},
[
pit_from_datetime,
(x : type_pit) => pit_shift_week(x, (week - 1)),
pit_trunc_week,
]
);
}
/**
* @todo timezone
*/
function ical_datetime_to_own_datetime(
ical_datetime : lib_plankton.ical.type_datetime
) : type_datetime
{
return {
"timezone_shift": 0,
"date": {
"year": ical_datetime.date.year,
"month": ical_datetime.date.month,
"day": ical_datetime.date.day,
},
"time": (
(ical_datetime.time === null)
?
null
:
{
"hour": ical_datetime.time.hour,
"minute": ical_datetime.time.minute,
"second": ical_datetime.time.second,
}
)
};
}
/**
* @todo timezone
*/
function ical_dt_to_own_datetime(
ical_dt: lib_plankton.ical.type_dt
) : type_datetime
{
return {
"timezone_shift": 0,
"date": ical_dt.value.date,
"time": (
(ical_dt.value.time === null)
?
null
:
{
"hour": ical_dt.value.time.hour,
"minute": ical_dt.value.time.minute,
"second": ical_dt.value.time.second,
}
)
};
}

26
source/index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script type="text/javascript" src="logic.js">
</script>
<script type="text/javascript">
document.addEventListener(
"DOMContentLoaded",
() => {
_zeitbild.frontend.main()
.then(
() => {}
)
.catch(
(error) => {console.error(error);}
)
}
);
</script>
</head>
<body>
</body>
</html>

File diff suppressed because it is too large Load diff

339
source/logic/backend.ts Normal file
View file

@ -0,0 +1,339 @@
/**
*/
namespace _zeitbild.frontend.resources.backend
{
/**
*/
var _data : _zeitbild.frontend.type_datamodel;
/**
*/
export async function init(
) : Promise<void>
{
const path : string = "data.json";
if (_data === undefined) {
_data = lib_plankton.call.convey(
await lib_plankton.file.read(path),
[
lib_plankton.json.decode,
(data_raw : any) => (
({
"users": data_raw["users"],
"calendars": (
data_raw["calendars"]
.map(
(calendar_entry_raw : any) => ({
"id": calendar_entry_raw["id"],
"object": (
((calendar_object_raw) => {
switch (calendar_object_raw["kind"]) {
default: {
return calendar_object_raw;
break;
}
case "concrete": {
return {
"kind": "concrete",
"data": {
"name": calendar_object_raw["data"]["name"],
"private": (
calendar_object_raw["data"]["private"]
??
false
),
"users": calendar_object_raw["data"]["users"],
"events": (
calendar_object_raw["data"]["events"]
.map(
(event_raw : any) => ({
"name": event_raw["name"],
"begin": event_raw["begin"],
"end": (
(
(
event_raw["end"]
??
null
)
===
null
)
?
null
:
event_raw["end"]
),
"location": (
event_raw["location"]
??
null
),
"description": (
event_raw["description"]
??
null
),
})
)
),
},
};
break;
}
}
}) (calendar_entry_raw["object"])
),
})
)
),
}) as type_datamodel
),
]
);
}
else {
// do nothing
}
return Promise.resolve<void>(undefined);
}
/**
*/
export async function calendar_list(
) : Promise<
Array<
{
key : type_calendar_id;
preview : {
name : string;
}
}
>
>
{
await init();
return Promise.resolve(
_data.calendars
.map(
(calendar_entry) => {
switch (calendar_entry.object.kind) {
case "concrete": {
return {
"key": calendar_entry.id,
"preview": {
"name": calendar_entry.object.data.name,
}
};
break;
}
case "collection": {
return {
"key": calendar_entry.id,
"preview": {
"name": calendar_entry.object.data.name,
}
};
}
case "caldav": {
return {
"key": calendar_entry.id,
"preview": {
"name": "(imported)",
}
};
}
}
}
)
);
}
/**
*/
export async function calendar_read(
calendar_id : type_calendar_id
) : Promise<type_calendar_object>
{
await init();
const hits = (
_data.calendars
.filter(
(calendar_entry) => (calendar_entry.id === calendar_id)
)
);
if (hits.length <= 0) {
return Promise.reject<type_calendar_object>(new Error("not found"));
}
else {
return Promise.resolve<type_calendar_object>(hits[0].object);
}
}
/**
* @todo prevent loops
*/
export async function calendar_gather_events(
calendar_id : type_calendar_id,
from_pit : _zeitbild.frontend.helpers.type_pit,
to_pit : _zeitbild.frontend.helpers.type_pit
) : Promise<
Array<
{
calendar_id : type_calendar_id;
calendar_name : string;
event : type_event;
}
>
>
{
await init();
const calendar_object : type_calendar_object = await calendar_read(
calendar_id
);
lib_plankton.log.info(
"calendar_gather_events",
{
"calendar_id": calendar_id,
}
);
switch (calendar_object.kind) {
case "concrete": {
return Promise.resolve(
calendar_object.data.events
.filter(
(event) => _zeitbild.frontend.helpers.pit_is_between(
_zeitbild.frontend.helpers.pit_from_datetime(event.begin),
from_pit,
to_pit
)
)
.map(
(event) => ({
"calendar_id": calendar_id,
"calendar_name": calendar_object.data.name,
"event": event
})
)
);
break;
}
case "collection": {
return (
Promise.all(
calendar_object.data.sources
.map(
(source_calendar_id) => calendar_gather_events(
source_calendar_id,
from_pit,
to_pit
)
)
)
.then(
(entries) => Promise.resolve(
entries
.reduce(
(x, y) => x.concat(y),
[]
)
)
)
);
break;
}
case "caldav": {
const url : lib_plankton.url.type_url = lib_plankton.url.decode(
calendar_object.data.source_url
);
const http_request : lib_plankton.http.type_request = {
"version": "HTTP/2",
"scheme": ((url.scheme === "https") ? "https" : "http"),
"host": url.host,
"path": (url.path ?? "/"),
"query": url.query,
"method": lib_plankton.http.enum_method.get,
"headers": {},
"body": null,
};
// TODO: cache?
const http_response : lib_plankton.http.type_response = await lib_plankton.http.call(
http_request,
{
}
);
const vcalendar : lib_plankton.ical.type_vcalendar = lib_plankton.ical.ics_decode(
http_response.body.toString(),
{
}
);
return Promise.resolve(
vcalendar.vevents
.map(
(vevent : lib_plankton.ical.type_vevent) => (
(vevent.dtstart !== undefined)
?
{
"name": (
(vevent.summary !== undefined)
?
vevent.summary
:
"???"
),
"begin": _zeitbild.frontend.helpers.ical_dt_to_own_datetime(vevent.dtstart),
"end": (
(vevent.dtend !== undefined)
?
_zeitbild.frontend.helpers.ical_dt_to_own_datetime(vevent.dtend)
:
null
),
"location": (
(vevent.location !== undefined)
?
vevent.location
:
null
),
"description": (
(vevent.description !== undefined)
?
vevent.description
:
null
),
}
:
null
)
)
.filter(
(event) => (event !== null)
)
.filter(
(event) => _zeitbild.frontend.helpers.pit_is_between(
_zeitbild.frontend.helpers.pit_from_datetime(event.begin),
from_pit,
to_pit
)
)
.map(
(event) => ({
"calendar_id": calendar_id,
"calendar_name": calendar_object.data.name,
"event": event,
})
)
);
break;
}
}
}
}

View file

@ -58,7 +58,8 @@ async function main(
// init
lib_plankton.log.conf_push(
[
lib_plankton.log.channel_make({"kind": "file", "data": {"threshold": "debug", "path": "/tmp/kalender.log"}}),
// lib_plankton.log.channel_make({"kind": "file", "data": {"threshold": "debug", "path": "/tmp/kalender.log"}}),
lib_plankton.log.channel_make({"kind": "file", "data": {"threshold": "info", "path": "/dev/stderr"}}),
// lib_plankton.log.channel_make({"kind": "stdout", "data": {"threshold": "info"}}),
]
);
@ -134,15 +135,15 @@ async function main(
),
]
);
let output : string;
let content : string;
switch (args["view_mode"]) {
default: {
output = "";
content = "";
throw (new Error("invalid view mode"));
break;
}
case "table": {
output = await calendar_view_table_html(
content = await calendar_view_table_html(
data,
args.calendar_id,
{
@ -152,7 +153,7 @@ async function main(
break;
}
case "list": {
output = await calendar_view_list_html(
content = await calendar_view_list_html(
data,
args.calendar_id,
{
@ -162,6 +163,12 @@ async function main(
break;
}
}
const output : string = template_coin(
"main",
{
"content": content,
}
);
process.stdout.write(output);
}
return Promise.resolve<void>(undefined);

590
source/logic/helpers.ts Normal file
View file

@ -0,0 +1,590 @@
/**
*/
namespace _zeitbild.frontend.helpers
{
/**
*/
var _template_cache : Record<string, string> = {};
/**
* @todo caching
*/
export async function template_coin(
name : string,
data : Record<string, string>
) : Promise<string>
{
let content : string;
if (! (name in _template_cache)) {
content = (
(
await lib_plankton.file.read(
lib_plankton.string.coin(
"templates/{{name}}.html.tpl",
{
"name": name,
}
)
)
)
.toString()
);
_template_cache[name] = content;
}
else {
content = _template_cache[name];
}
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);
}
/**
*/
export function date_object_get_week_of_year(
date : Date
) : int
{
let date_ : Date = new Date(date.getTime());
date_.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
date_.setDate(date_.getDate() + 3 - (date_.getDay() + 6) % 7);
// January 4 is always in week 1.
let week1 : Date = new Date(date_.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return (
1
+
Math.round(
(
((date_.getTime() - week1.getTime()) / 86400000)
-
3
+
(week1.getDay() + 6) % 7
)
/
7
)
);
}
/**
* @todo unite with type_datetimeobject?
*/
export type type_datetime = {
timezone_shift : int;
date : type_date;
time : (
null
|
type_time
);
};
/**
* @todo timezone_shift
*/
function datetime_from_date_object(
date : Date,
options : {
timezone_shift ?: int;
} = {}
) : type_datetime
{
options = Object.assign(
{
"timezone_shift": 0,
},
options
);
const date_ : Date = lib_plankton.call.convey(
date,
[
(x : Date) => x.getTime(),
(x : int) => (x + (((options.timezone_shift as int) * (60 * 60)) * 1000)),
(x : int) => new Date(x),
]
);
const iso_string : string = date_.toISOString();
return {
"timezone_shift": (options.timezone_shift as int),
"date": {
"year": parseInt(iso_string.slice(0, 4)),
"month": parseInt(iso_string.slice(5, 7)),
"day": parseInt(iso_string.slice(8, 10)),
},
"time": {
"hour": parseInt(iso_string.slice(11, 13)),
"minute": parseInt(iso_string.slice(14, 16)),
"second": parseInt(iso_string.slice(17, 19)),
},
};
}
/**
* @todo negative shift?
*/
function datetime_to_date_object(
datetime : type_datetime
) : Date
{
const iso_string : string = lib_plankton.string.coin(
"{{year}}-{{month}}-{{day}}T{{hour}}:{{minute}}:{{second}}.000+{{shift}}",
{
"year": datetime.date.year.toFixed(0).padStart(4, "0"),
"month": datetime.date.month.toFixed(0).padStart(2, "0"),
"day": datetime.date.day.toFixed(0).padStart(2, "0"),
"hour": ((datetime.time !== null) ? datetime.time.hour : 0).toFixed(0).padStart(2, "0"),
"minute": ((datetime.time !== null) ? datetime.time.minute : 0).toFixed(0).padStart(2, "0"),
"second": ((datetime.time !== null) ? datetime.time.second : 0).toFixed(0).padStart(2, "0"),
"shift": (datetime.timezone_shift.toFixed(0).padStart(2, "0") + ":00"),
}
);
return (new Date(iso_string));
}
/**
*/
export type type_pit = int;
/**
*/
export function pit_to_date_object(
pit : type_pit
) : Date
{
return (new Date(pit * 1000));
}
/**
*/
function pit_from_date_object(
date_object : Date
) : type_pit
{
return Math.round(date_object.getTime() / 1000);
}
/**
*/
export function pit_now(
) : type_pit
{
return pit_from_date_object(new Date(Date.now()));
}
/**
* @todo timezone
*/
export function pit_to_datetime(
pit : type_pit,
options : {
timezone_shift ?: int
} = {}
) : type_datetime
{
options = Object.assign(
{
"timezone_shift": 0,
},
options
);
const date_object : Date = pit_to_date_object(pit);
return datetime_from_date_object(
date_object,
{
"timezone_shift": (options.timezone_shift as int),
}
);
}
/**
*/
export function pit_from_datetime(
datetime : type_datetime
) : type_pit
{
return lib_plankton.call.convey(
datetime,
[
datetime_to_date_object,
pit_from_date_object,
]
);
}
/**
*/
export function pit_is_before(
pit : type_pit,
reference : type_pit
) : boolean
{
return (pit < reference);
}
/**
*/
function pit_is_after(
pit : type_pit,
reference : type_pit
) : boolean
{
return (pit > reference);
}
/**
*/
export function pit_is_between(
pit : type_pit,
reference_left : type_pit,
reference_right : type_pit
) : boolean
{
return (
pit_is_after(pit, reference_left)
&&
pit_is_before(pit, reference_right)
);
}
/**
*/
function pit_shift_hour(
pit : type_pit,
increment : int
) : type_pit
{
return (pit + (60 * 60 * increment));
}
/**
*/
export function pit_shift_day(
pit : type_pit,
increment : int
) : type_pit
{
return (pit + (60 * 60 * 24 * increment));
}
/**
*/
export function pit_shift_week(
pit : type_pit,
increment : int
) : type_pit
{
return (pit + (60 * 60 * 24 * 7 * increment));
}
/**
*/
function pit_shift_year(
pit : type_pit,
increment : int
) : type_pit
{
return (pit + (60 * 60 * 24 * 365 * increment));
}
/**
*/
function pit_trunc_minute(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": datetime_input.date.month,
"day": datetime_input.date.day,
},
"time": {
"hour": (
(datetime_input.time === null)
?
0
:
datetime_input.time.hour
),
"minute": (
(datetime_input.time === null)
?
0
:
datetime_input.time.minute
),
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
*/
function pit_trunc_hour(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": datetime_input.date.month,
"day": datetime_input.date.day,
},
"time": {
"hour": (
(datetime_input.time === null)
?
0
:
datetime_input.time.hour
),
"minute": 0,
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
*/
function pit_trunc_day(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": datetime_input.date.month,
"day": datetime_input.date.day,
},
"time": {
"hour": 0,
"minute": 0,
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
*/
export function pit_trunc_week(
pit : type_pit
) : type_pit
{
const date_object : Date = pit_to_date_object(pit);
return lib_plankton.call.convey(
date_object.getDay(),
[
(x : int) => ((x === 0) ? 7 : x),
(x : int) => (x - 1),
(x : int) => pit_shift_day(pit, (-x)),
pit_trunc_day
]
);
}
/**
*/
function pit_trunc_month(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": datetime_input.date.month,
"day": 1,
},
"time": {
"hour": 0,
"minute": 0,
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
*/
function pit_trunc_year(
pit : type_pit
) : type_pit
{
const datetime_input : type_datetime = pit_to_datetime(pit);
const datetime_output : type_datetime = {
"timezone_shift": 0,
"date": {
"year": datetime_input.date.year,
"month": 1,
"day": 1,
},
"time": {
"hour": 0,
"minute": 0,
"second": 0,
},
};
return pit_from_datetime(datetime_output);
}
/**
* @param year year according to specified timezone shift
* @param week week according to specified timezone shift
* @return the begin of the week (monday, 00:00)
*/
export function pit_from_year_and_week(
year : int,
week : int,
options : {
timezone_shift ?: int;
} = {}
) : type_pit
{
options = Object.assign(
{
"timezone_shift": 0,
},
options
);
return lib_plankton.call.convey(
{
"timezone_shift": (options.timezone_shift as int),
"date": {
"year": year,
"month": 1,
"day": 1,
},
"time": {
"hour": 0,
"minute": 0,
"second": 0
}
},
[
pit_from_datetime,
(x : type_pit) => pit_shift_week(x, (week - 1)),
pit_trunc_week,
]
);
}
/**
* @todo timezone
*/
function ical_datetime_to_own_datetime(
ical_datetime : lib_plankton.ical.type_datetime
) : type_datetime
{
return {
"timezone_shift": 0,
"date": {
"year": ical_datetime.date.year,
"month": ical_datetime.date.month,
"day": ical_datetime.date.day,
},
"time": (
(ical_datetime.time === null)
?
null
:
{
"hour": ical_datetime.time.hour,
"minute": ical_datetime.time.minute,
"second": ical_datetime.time.second,
}
)
};
}
/**
* @todo timezone
*/
export function ical_dt_to_own_datetime(
ical_dt: lib_plankton.ical.type_dt
) : type_datetime
{
return {
"timezone_shift": 0,
"date": ical_dt.value.date,
"time": (
(ical_dt.value.time === null)
?
null
:
{
"hour": ical_dt.value.time.hour,
"minute": ical_dt.value.time.minute,
"second": ical_dt.value.time.second,
}
)
};
}
}

70
source/logic/main.ts Normal file
View file

@ -0,0 +1,70 @@
/**
*/
namespace _zeitbild.frontend
{
/**
*/
type type_conf = {
view_mode : string;
calendar_id : int;
timezone_shift : int;
};
/**
*/
export async function main(
) : Promise<void>
{
// init
lib_plankton.log.conf_push(
[
lib_plankton.log.channel_make({"kind": "console", "data": {"threshold": "info"}}),
]
);
// conf
const conf : type_conf = lib_plankton.json.decode(await lib_plankton.file.read("conf.json"));
// exec
let content : string;
switch (conf.view_mode) {
default: {
content = "";
throw (new Error("invalid view mode"));
break;
}
case "table": {
content = await _zeitbild.frontend.view.calendar_view_table_html(
conf.calendar_id,
{
"from": {
"year": 2024,
"week": 35
},
"to": {
"year": 2024,
"week": 43
},
"timezone_shift": conf.timezone_shift,
}
);
break;
}
case "list": {
content = await _zeitbild.frontend.view.calendar_view_list_html(
conf.calendar_id,
{
"timezone_shift": conf.timezone_shift,
}
);
break;
}
}
(document.querySelector("body") as HTMLBodyElement).innerHTML = content;
return Promise.resolve<void>(undefined);
}
}

115
source/logic/types.ts Normal file
View file

@ -0,0 +1,115 @@
/**
*/
namespace _zeitbild.frontend
{
/**
*/
type type_role = (
"editor"
|
"viewer"
);
/**
*/
type type_user_id = int;
/**
*/
type type_user_object = {
name : string;
};
/**
*/
export type type_event = {
name : string;
begin : _zeitbild.frontend.helpers.type_datetime;
end : (
null
|
_zeitbild.frontend.helpers.type_datetime
);
location : (
null
|
string
);
description : (
null
|
string
);
};
/**
*/
export type type_calendar_id = int;
/**
* @todo bei "collection" Kreise vermeiden
*/
export type type_calendar_object = (
{
kind : "concrete";
data : {
name : string;
private : boolean;
users : Array<
{
id : type_user_id;
role : type_role;
}
>;
events : Array<type_event>;
};
}
|
{
kind : "collection";
data : {
name : string;
private : boolean;
sources : Array<
type_calendar_id
>;
}
}
|
{
kind : "caldav";
data : {
name : string;
private : boolean;
read_only : boolean;
source_url : string;
}
}
);
/**
*/
export type type_datamodel = {
users : Array<
{
id : type_user_id;
object : type_user_object;
}
>;
calendars : Array<
{
id : type_calendar_id;
object : type_calendar_object;
}
>;
};
}

695
source/logic/view.ts Normal file
View file

@ -0,0 +1,695 @@
/**
*/
namespace _zeitbild.frontend.view
{
/**
*/
function event_generate_tooltip(
calendar_name : string,
event : type_event
) : string
{
return (
lib_plankton.string.coin(
"[{{calendar_name}}] {{event_name}}\n",
{
"calendar_name": calendar_name,
"event_name": event.name,
}
)
+
"--\n"
+
(
(event.begin.time !== null)
?
lib_plankton.string.coin(
"{{label}}: {{value}}\n",
{
"label": "Anfang", // TODO: translate
"value": lib_plankton.string.coin(
"{{hour}}:{{minute}}",
{
"hour": event.begin.time.hour.toFixed(0).padStart(2, "0"),
"minute": event.begin.time.minute.toFixed(0).padStart(2, "0"),
}
), // TODO: outsource
}
)
:
""
)
+
(
(event.end !== null)
?
lib_plankton.string.coin(
"{{label}}: {{value}}\n",
{
"label": "Ende", // TODO: translate
"value": (
[
(
(
(event.end.date.year !== event.begin.date.year)
||
(event.end.date.month !== event.begin.date.month)
||
(event.end.date.day !== event.begin.date.day)
)
?
lib_plankton.string.coin(
"{{year}}-{{month}}-{{day}}",
{
"year": event.end.date.year.toFixed(0).padStart(4, "0"),
"month": event.end.date.month.toFixed(0).padStart(2, "0"),
"day": event.end.date.month.toFixed(0).padStart(2, "0"),
}
)
:
null
),
(
(event.end.time !== null)
?
lib_plankton.string.coin(
"{{hour}}:{{minute}}",
{
"hour": event.end.time.hour.toFixed(0).padStart(2, "0"),
"minute": event.end.time.minute.toFixed(0).padStart(2, "0"),
}
)
:
null
),
]
.filter(x => (x !== null))
.join(",")
),
}
)
:
""
)
+
(
(event.location !== null)
?
(
lib_plankton.string.coin(
"{{label}}: {{value}}\n",
{
"label": "Ort", // TODO
"value": event.location,
}
)
)
:
""
)
+
(
(event.description !== null)
?
(
"--\n"
+
lib_plankton.string.coin(
"{{description}}\n",
{
"description": event.description,
}
)
)
:
""
)
);
}
/**
* @todo kein "while"
*/
async function calendar_view_table_data(
calendar_id : type_calendar_id,
options : {
from ?: {
year : int;
week : int;
},
to ?: {
year : int;
week : int;
},
timezone_shift ?: int;
} = {}
) : Promise<
{
sources : lib_plankton.structures.type_hashmap<
type_calendar_id,
{
name : string;
}
>;
rows : Array<
{
week : int;
data : Array<
{
pit : _zeitbild.frontend.helpers.type_pit;
entries : Array<
{
calendar_id : type_calendar_id;
event : type_event;
}
>;
today : boolean;
}
>;
}
>
}
>
{
const now_pit : _zeitbild.frontend.helpers.type_pit = _zeitbild.frontend.helpers.pit_now();
options = Object.assign(
{
"from": lib_plankton.call.convey(
now_pit,
[
(x : _zeitbild.frontend.helpers.type_pit) => _zeitbild.frontend.helpers.pit_shift_week(x, -1),
_zeitbild.frontend.helpers.pit_to_date_object,
(x : Date) => ({
"year": x.getFullYear(),
"week": _zeitbild.frontend.helpers.date_object_get_week_of_year(x),
})
]
),
"to": lib_plankton.call.convey(
now_pit,
[
(x : _zeitbild.frontend.helpers.type_pit) => _zeitbild.frontend.helpers.pit_shift_week(x, +4),
_zeitbild.frontend.helpers.pit_to_date_object,
(x : Date) => ({
"year": x.getFullYear(),
"week": _zeitbild.frontend.helpers.date_object_get_week_of_year(x),
})
]
),
"timezone_shift": 0,
},
options
);
/*
const calendar_object : type_calendar_object = calendar_read(
data,
calendar_id
);
*/
const from_pit : _zeitbild.frontend.helpers.type_pit = _zeitbild.frontend.helpers.pit_from_year_and_week(
(options.from as {year : int; week : int}).year,
(options.from as {year : int; week : int}).week,
{
"timezone_shift": (options.timezone_shift as int),
}
);
const to_pit : _zeitbild.frontend.helpers.type_pit = _zeitbild.frontend.helpers.pit_from_year_and_week(
(options.to as {year : int; week : int}).year,
(options.to as {year : int; week : int}).week,
{
"timezone_shift": (options.timezone_shift as int),
}
);
// prepare
const entries : Array<
{
calendar_id : type_calendar_id;
calendar_name : string;
event : type_event;
}
> = await _zeitbild.frontend.resources.backend.calendar_gather_events(
calendar_id,
from_pit,
to_pit
);
let result : {
sources : lib_plankton.structures.type_hashmap<
type_calendar_id,
{
name : string;
}
>;
rows : Array<
{
week : int;
data : Array<
{
pit : _zeitbild.frontend.helpers.type_pit;
entries : Array<
{
calendar_id : type_calendar_id;
event : type_event;
}
>;
today : boolean;
}
>;
}
>;
} = {
"sources": lib_plankton.structures.hashmap_construct(
x => x.toFixed(0),
(
entries
.map(
(entry) => (
{
"key": entry.calendar_id,
"value": {
"name": entry.calendar_name,
}
}
)
)
)
),
"rows": [],
};
let row : Array<
{
pit : _zeitbild.frontend.helpers.type_pit;
entries : Array<
{
calendar_id : type_calendar_id;
event : type_event;
}
>;
today : boolean;
}
> = [];
let day : int = 0;
while (true) {
const pit_current : _zeitbild.frontend.helpers.type_pit = _zeitbild.frontend.helpers.pit_shift_day(
from_pit,
day
);
if (
_zeitbild.frontend.helpers.pit_is_before(
pit_current,
to_pit
)
) {
day += 1;
row.push(
{
"pit": pit_current,
"entries": [],
"today": false, // TODO
}
);
if (day % 7 === 0) {
result.rows.push(
{
"week": (
(options.from as {year : int; week : int}).week
+
Math.floor(day / 7)
-
1 // TODO
),
"data": row
}
);
row = [];
}
else {
// do nothing
}
}
else {
break;
}
}
// fill
{
// events
(
entries
.forEach(
(entry) => {
const distance_seconds : int = (
_zeitbild.frontend.helpers.pit_from_datetime(entry.event.begin)
-
from_pit
);
const distance_days : int = (distance_seconds / (60 * 60 * 24));
const week : int = Math.floor(Math.floor(distance_days) / 7);
const day : int = (Math.floor(distance_days) % 7);
if ((week >= 0) && (week < result.rows.length)) {
result.rows[week].data[day].entries.push(entry);
}
else {
// do nothing
}
}
)
);
// today
{
const distance_seconds : int = (
now_pit
-
from_pit
);
const distance_days : int = (distance_seconds / (60 * 60 * 24));
const week : int = Math.floor(Math.floor(distance_days) / 7);
const day : int = (Math.floor(distance_days) % 7);
if ((week >= 0) && (week < result.rows.length)) {
result.rows[week].data[day].today = true;
}
else {
// do nothing
}
}
}
return Promise.resolve(result);
}
/**
*/
export async function calendar_view_table_html(
calendar_id : type_calendar_id,
options : {
from ?: {
year : int;
week : int;
};
to ?: {
year : int;
week : int;
};
timezone_shift ?: int;
} = {}
) : Promise<string>
{
const stuff : {
sources : lib_plankton.structures.type_hashmap<
type_calendar_id,
{
name : string;
}
>;
rows : Array<
{
week : int;
data : Array<
{
pit : _zeitbild.frontend.helpers.type_pit;
entries : Array<
{
calendar_id : type_calendar_id;
event : type_event;
}
>;
today : boolean;
}
>;
}
>;
} = await calendar_view_table_data(
calendar_id,
options
);
const sources : lib_plankton.structures.type_hashmap<
type_calendar_id,
{
name : string;
color : lib_plankton.color.type_color;
}
> = lib_plankton.structures.hashmap_construct(
(x => x.toFixed(0)),
lib_plankton.structures.hashmap_dump(
stuff.sources
)
.map(
(pair) => ({
"key": pair.key,
"value": {
"name": pair.value.name,
"color": lib_plankton.color.give_generic(
(pair.key + 0.2),
{
"saturation": 0.375,
"value": 0.375,
}
),
}
})
)
);
return _zeitbild.frontend.helpers.template_coin(
"tableview",
{
"sources": (
await _zeitbild.frontend.helpers.promise_row<string>(
lib_plankton.structures.hashmap_dump(sources)
.map(
({"key": calendar_id, "value": data}) => async () => _zeitbild.frontend.helpers.template_coin(
"tableview-sources-entry",
{
"name": data.name,
"color": lib_plankton.color.output_hex(data.color),
}
)
)
)
).join(""),
"rows": (
await _zeitbild.frontend.helpers.promise_row<string>(
stuff.rows
.map(
(row) => async () => _zeitbild.frontend.helpers.template_coin(
"tableview-row",
{
"week": row.week.toFixed(0).padStart(2, "0"),
"cells": (
await _zeitbild.frontend.helpers.promise_row<string>(
row.data
.map(
(cell) => async () => _zeitbild.frontend.helpers.template_coin(
"tableview-cell",
{
"extra_classes": (
[""]
.concat(cell.today ? ["calendar-cell-today"] : [])
.join(" ")
),
"title": lib_plankton.call.convey(
cell.pit,
[
_zeitbild.frontend.helpers.pit_to_datetime,
(x : _zeitbild.frontend.helpers.type_datetime) => lib_plankton.string.coin(
"{{year}}-{{month}}-{{day}}",
{
"year": x.date.year.toFixed(0).padStart(4, "0"),
"month": x.date.month.toFixed(0).padStart(2, "0"),
"day": x.date.day.toFixed(0).padStart(2, "0"),
}
),
]
),
"day": lib_plankton.call.convey(
cell.pit,
[
_zeitbild.frontend.helpers.pit_to_datetime,
(x : _zeitbild.frontend.helpers.type_datetime) => lib_plankton.string.coin(
"{{day}}",
{
"year": x.date.year.toFixed(0).padStart(4, "0"),
"month": x.date.month.toFixed(0).padStart(2, "0"),
"day": x.date.day.toFixed(0).padStart(2, "0"),
}
),
]
),
"entries": (
await _zeitbild.frontend.helpers.promise_row<string>(
cell.entries
.map(
(entry) => () => _zeitbild.frontend.helpers.template_coin(
"tableview-cell-entry",
{
"color": lib_plankton.color.output_hex(
lib_plankton.structures.hashmap_get(
sources,
entry.calendar_id
).color
),
"title": event_generate_tooltip(
lib_plankton.structures.hashmap_get(
sources,
entry.calendar_id
).name,
entry.event
),
"name": entry.event.name,
}
)
)
)
).join(""),
}
)
)
)
).join(""),
}
)
)
)
).join(""),
}
);
}
/**
*/
async function calendar_view_list_data(
calendar_id : type_calendar_id,
options : {
from ?: _zeitbild.frontend.helpers.type_pit;
to ?: _zeitbild.frontend.helpers.type_pit;
timezone_shift ?: int;
} = {}
) : Promise<
Array<
{
calendar_id : type_calendar_id;
event : type_event;
}
>
>
{
const now_pit : _zeitbild.frontend.helpers.type_pit = _zeitbild.frontend.helpers.pit_now();
options = Object.assign(
{
"from": lib_plankton.call.convey(
now_pit,
[
(x : _zeitbild.frontend.helpers.type_pit) => _zeitbild.frontend.helpers.pit_shift_day(x, -1),
]
),
"to": lib_plankton.call.convey(
now_pit,
[
(x : _zeitbild.frontend.helpers.type_pit) => _zeitbild.frontend.helpers.pit_shift_week(x, +4),
]
),
"timezone_shift": 0,
},
options
);
const entries : Array<
{
calendar_id : type_calendar_id;
event : type_event;
}
> = await _zeitbild.frontend.resources.backend.calendar_gather_events(
calendar_id,
(options.from as _zeitbild.frontend.helpers.type_pit),
(options.to as _zeitbild.frontend.helpers.type_pit)
);
// TODO: optimize
entries.sort(
(entry_1, entry_2) => (
_zeitbild.frontend.helpers.pit_from_datetime(entry_1.event.begin)
-
_zeitbild.frontend.helpers.pit_from_datetime(entry_2.event.begin)
)
);
return Promise.resolve(entries);
}
/**
*/
export async function calendar_view_list_html(
calendar_id : type_calendar_id,
options : {
from ?: _zeitbild.frontend.helpers.type_pit;
to ?: _zeitbild.frontend.helpers.type_pit;
timezone_shift ?: int;
} = {}
) : Promise<string>
{
const stuff : Array<
{
calendar_id : type_calendar_id;
event : type_event;
}
> = await calendar_view_list_data(
calendar_id,
options
);
return Promise.resolve<string>(
new lib_plankton.xml.class_node_complex(
"div",
{
"class": "list",
},
[
new lib_plankton.xml.class_node_complex(
"style",
{},
[
new lib_plankton.xml.class_node_text(
"html {background-color: #111; color: #FFF; font-family: sans-serif;}\n"
+
"table {width: 100%; border-collapse: collapse;}\n"
)
]
),
new lib_plankton.xml.class_node_complex(
"ul",
{
"class": "list-events",
},
(
stuff
.map(
(entry) => (
new lib_plankton.xml.class_node_complex(
"li",
{
"class": "list-event_entry",
},
[
new lib_plankton.xml.class_node_text(
JSON.stringify(entry)
),
]
)
)
)
)
),
]
).compile()
);
}
}

78
source/style/main.css Normal file
View file

@ -0,0 +1,78 @@
html {
background-color: #111;
color: #FFF;
font-family: sans-serif;
}
.calendar {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.calendar-pane-left {
flex-basis: 12.5%;
}
.calendar-pane-right {
flex-basis: 87.5%;
}
.tableview-sources {
margin: 0;
padding: 0;
list-style-type: none;
}
.tableview-sources-entry {
margin: 8px;
padding: 4px;
}
.calendar table {
width: 100%;
border-collapse: collapse;
}
.calendar-cell {
border: 1px solid #888;
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;
}
.calendar-cell-today {
outline: 4px solid #FFF;
}
.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;
cursor: pointer;
}

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" type="text/css" href="style.css"/>
</head>
<body>
</body>
</html>

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
<li class="tableview-sources-entry" style="background-color: {{color}}">{{name}}</li>

View file

@ -0,0 +1,27 @@
<div class="calendar">
<div class="calendar-pane calendar-pane-left">
<ul class="tableview-sources">
{{sources}}
</ul>
</div>
<div class="calendar-pane calendar-pane-right">
<table>
<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>
<tbody>
{{rows}}
</tbody>
</table>
</div>
</div>

View file

@ -16,6 +16,22 @@ def main():
metavar = "<output-directory>",
help = "output directory",
)
argument_parser.add_argument(
"-c",
"--conf-path",
type = str,
default = None,
metavar = "<conf-path>",
help = "conf path",
)
argument_parser.add_argument(
"-d",
"--data-path",
type = str,
default = None,
metavar = "<data-path>",
help = "data path",
)
args = argument_parser.parse_args()
## exec
@ -28,6 +44,28 @@ def main():
" ".join(targets),
)
)
if True:
if (args.conf_path is None):
pass
else:
_os.system(
"cp %s %s/conf.json"
% (
args.conf_path,
args.output_directory,
)
)
if True:
if (args.data_path is None):
pass
else:
_os.system(
"cp %s %s/data.json"
% (
args.data_path,
args.output_directory,
)
)
_sys.stdout.write("%s\n" % args.output_directory)

View file

@ -6,37 +6,58 @@ dir_temp := /tmp/kalender-temp
dir_build := build
dir_tools := tools
cmd_log := echo "--"
cmd_cat := cat
cmd_chmod := chmod
cmd_mkdir := mkdir -p
cmd_cp := cp
cmd_log := echo "--"
cmd_mkdir := mkdir -p
cmd_tsc := ${dir_tools}/typescript/node_modules/.bin/tsc
## rules
.PHONY: default
default: ${dir_build}/kalender
default: index templates style logic
${dir_temp}/kalender-unlinked.js: \
.PHONY: index
index: ${dir_build}/index.html
${dir_build}/index.html: \
${dir_source}/index.html
@ ${cmd_log} "index …"
@ ${cmd_cp} -u -v $^ $@
.PHONY: templates
templates: \
$(wildcard ${dir_source}/templates/*)
@ ${cmd_log} "templates …"
@ ${cmd_mkdir} ${dir_build}/templates
@ ${cmd_cp} -r -u -v ${dir_source}/templates/* ${dir_build}/templates/
.PHONY: style
style: \
$(wildcard ${dir_source}/style/*)
@ ${cmd_log} "style …"
@ ${cmd_mkdir} ${dir_build}
@ ${cmd_cat} ${dir_source}/style/* > ${dir_build}/style.css
.PHONY: logic
logic: ${dir_build}/logic.js
${dir_temp}/logic-unlinked.js: \
${dir_lib}/plankton/plankton.d.ts \
${dir_source}/helpers.ts \
${dir_source}/logic.ts \
${dir_source}/main.ts
@ ${cmd_log} "compile …"
${dir_source}/logic/helpers.ts \
${dir_source}/logic/types.ts \
${dir_source}/logic/backend.ts \
${dir_source}/logic/view.ts \
${dir_source}/logic/main.ts
@ ${cmd_log} "logic | compile …"
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_tsc} --lib es2020 --strict $^ --outFile $@
@ ${cmd_tsc} --lib dom,es2020 --strict $^ --outFile $@
${dir_temp}/head.js:
@ ${cmd_mkdir} $(dir $@)
@ echo "#!/usr/bin/env node" > $@
${dir_build}/kalender: \
${dir_temp}/head.js \
${dir_build}/logic.js: \
${dir_lib}/plankton/plankton.js \
${dir_temp}/kalender-unlinked.js
@ ${cmd_log} "link …"
${dir_temp}/logic-unlinked.js
@ ${cmd_log} "logic | link …"
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_cat} $^ > $@
@ ${cmd_chmod} +x $@

View file

@ -8,6 +8,7 @@ modules=""
modules="${modules} base"
modules="${modules} call"
modules="${modules} file"
modules="${modules} structures"
modules="${modules} json"
modules="${modules} args"
modules="${modules} string"
@ -23,5 +24,5 @@ modules="${modules} url"
mkdir -p ${dir}
cd ${dir}
ptk bundle node ${modules}
ptk bundle web ${modules}
cd - > /dev/null