From 94485919a772b1ed72e4db9bc05acb6284cd6d20 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Tue, 10 Sep 2024 01:11:58 +0200 Subject: [PATCH] [mod] --- data/example.kal.json | 18 ++ source/helpers.ts | 53 +++++ source/logic.ts | 538 +++++++++++++++++++++++++++++++++++------- source/main.ts | 83 +++++-- tools/build | 9 - 5 files changed, 588 insertions(+), 113 deletions(-) diff --git a/data/example.kal.json b/data/example.kal.json index 6f2599d..6649e6b 100644 --- a/data/example.kal.json +++ b/data/example.kal.json @@ -129,6 +129,24 @@ }, { "id": 4, + "object": { + "kind": "caldav", + "data": { + "source_url": "https://export.kalender.digital/ics/0/3e10dae66950379d4cc8/gesamterkalender.ics?past_months=3&future_months=36" + } + } + }, + { + "id": 5, + "object": { + "kind": "caldav", + "data": { + "source_url": "https://roydfalk:uywvui66svLHFMUp6LndxNO9ZASNrY9vmUDcAPPNIr7PWmjAMpEQce0JiA9AAeUH@vikunja.linke.sx/dav/projects/4/" + } + } + }, + { + "id": 6, "object": { "kind": "collection", "data": { diff --git a/source/helpers.ts b/source/helpers.ts index f4f802f..865a97d 100644 --- a/source/helpers.ts +++ b/source/helpers.ts @@ -459,3 +459,56 @@ function pit_from_year_and_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, + } + ) + }; +} diff --git a/source/logic.ts b/source/logic.ts index efcfc15..b59d396 100644 --- a/source/logic.ts +++ b/source/logic.ts @@ -44,6 +44,7 @@ type type_calendar_id = int; /** + * @todo bei "collection" Kreise vermeiden */ type type_calendar_object = ( { @@ -69,6 +70,13 @@ type type_calendar_object = ( >; } } + | + { + kind : "caldav"; + data : { + source_url : string; + } + } ); @@ -106,12 +114,35 @@ function calendar_list( return ( data.calendars .map( - (calendar_entry) => ({ - "key": calendar_entry.id, - "preview": { - "name": calendar_entry.object.data.name, + (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)", + } + }; + } } - }) + } ) ); } @@ -141,16 +172,18 @@ function calendar_read( /** */ -function calendar_gather_events( +async function calendar_gather_events( data : type_datamodel, calendar_id : type_calendar_id, from_pit : type_pit, to_pit : type_pit -) : Array< - { - calendar_id : type_calendar_id; - event : type_event; - } +) : Promise< + Array< + { + calendar_id : type_calendar_id; + event : type_event; + } + > > { const calendar_object : type_calendar_object = calendar_read( @@ -159,7 +192,7 @@ function calendar_gather_events( ); switch (calendar_object.kind) { case "concrete": { - return ( + return Promise.resolve( calendar_object.data.events .filter( (event) => pit_is_between( @@ -176,18 +209,90 @@ function calendar_gather_events( } case "collection": { return ( - calendar_object.data.sources - .map( - (source_calendar_id) => calendar_gather_events( - data, - source_calendar_id, - from_pit, - to_pit + Promise.all( + calendar_object.data.sources + .map( + (source_calendar_id) => calendar_gather_events( + data, + source_calendar_id, + from_pit, + to_pit + ) ) ) - .reduce( - (x, y) => x.concat(y), - [] + .then( + (sources) => Promise.resolve( + sources + .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/1.1", + "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, + }; + 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": ical_dt_to_own_datetime(vevent.dtstart), + "end": ( + (vevent.dtend !== undefined) + ? + ical_dt_to_own_datetime(vevent.dtend) + : + null + ), + "description": ( + (vevent.description !== undefined) + ? + vevent.description + : + null + ), + } + : + null + ) + ) + .filter( + (event) => (event !== null) + ) + .map( + (event) => ({"calendar_id": calendar_id, "event": event}) ) ); break; @@ -197,41 +302,67 @@ function calendar_gather_events( /** + * @todo kein "while" */ -function calendar_view_table( +async function calendar_view_table_data( data : type_datamodel, calendar_id : type_calendar_id, - from : { - year : int; - week : int; - }, - to : { - year : int; - week : int; - }, options : { + from ?: { + year : int; + week : int; + }, + to ?: { + year : int; + week : int; + }, timezone_shift ?: int; } = {} -) : Array< - { - week : int; - data : Array< - { - pit : type_pit; - entries : Array< - { - calendar_id : type_calendar_id; - event : type_event; - } - >; - today : boolean; - } - >; - } +) : Promise< + Array< + { + week : int; + data : Array< + { + pit : type_pit; + entries : Array< + { + calendar_id : type_calendar_id; + event : type_event; + } + >; + today : boolean; + } + >; + } + > > { + const now_pit : type_pit = pit_now(); options = Object.assign( { + "from": lib_plankton.call.convey( + now_pit, + [ + (x : type_pit) => pit_shift_week(x, -1), + pit_to_date_object, + (x : Date) => ({ + "year": x.getFullYear(), + "week": date_object_get_week_of_year(x), + }) + ] + ), + "to": lib_plankton.call.convey( + now_pit, + [ + (x : type_pit) => pit_shift_week(x, +4), + pit_to_date_object, + (x : Date) => ({ + "year": x.getFullYear(), + "week": date_object_get_week_of_year(x), + }) + ] + ), "timezone_shift": 0, }, options @@ -243,20 +374,19 @@ function calendar_view_table( ); */ const from_pit : type_pit = pit_from_year_and_week( - from.year, - from.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 : type_pit = pit_from_year_and_week( - to.year, - to.week, + (options.to as {year : int; week : int}).year, + (options.to as {year : int; week : int}).week, { "timezone_shift": (options.timezone_shift as int), } ); - const now_pit : type_pit = pit_now(); // prepare const entries : Array< @@ -264,7 +394,7 @@ function calendar_view_table( calendar_id : type_calendar_id; event : type_event; } - > = calendar_gather_events( + > = await calendar_gather_events( data, calendar_id, from_pit, @@ -312,7 +442,18 @@ function calendar_view_table( } ); if (day % 7 === 0) { - result.push({"week": (from.week + Math.floor(day / 7)), "data": row}); + result.push( + { + "week": ( + (options.from as {year : int; week : int}).week + + + Math.floor(day / 7) + - + 1 // TODO + ), + "data": row + } + ); row = []; } else { @@ -337,7 +478,12 @@ function calendar_view_table( const week : int = Math.floor(Math.floor(distance_days) / 7); const day : int = (Math.floor(distance_days) % 7); - result[week].data[day].entries.push(entry); + if ((week >= 0) && (week < result.length)) { + result[week].data[day].entries.push(entry); + } + else { + // do nothing + } } ) ); @@ -358,35 +504,29 @@ function calendar_view_table( } } - return result; + return Promise.resolve(result); } /** */ -function calendar_view_table_html( +async function calendar_view_table_html( data : type_datamodel, calendar_id : type_calendar_id, - from : { - year : int; - week : int; - }, - to : { - year : int; - week : int; - }, options : { + from ?: { + year : int; + week : int; + }; + to ?: { + year : int; + week : int; + }; timezone_shift ?: int; } = {} -) : string +) : Promise { - options = Object.assign( - { - "timezone_shift": 0, - }, - options - ); - const rows : Array< + const stuff : Array< { week : int; data : Array< @@ -402,16 +542,12 @@ function calendar_view_table_html( } >; } - > = calendar_view_table( + > = await calendar_view_table_data( data, calendar_id, - from, - to, - { - "timezone_shift": options.timezone_shift, - } + options ); - return ( + return Promise.resolve( new lib_plankton.xml.class_node_complex( "div", { @@ -433,7 +569,7 @@ function calendar_view_table_html( + ".calendar-cell-week {width: 5.5%;}\n" + - ".calendar-cell-regular {width: 13.5%;}\n" + ".calendar-cell-regular {width: 13.5%; height: 150px;}\n" + ".calendar-cell-today {background-color: #333;}\n" + @@ -441,7 +577,7 @@ function calendar_view_table_html( + ".calendar-events {margin: 0; padding: 0; list-style-type: none;}\n" + - ".calendar-event_entry {margin: 4px; padding: 4px; border-radius: 2px; font-size: 0.75em;}\n" + ".calendar-event_entry {margin: 4px; padding: 4px; border-radius: 2px; font-size: 0.75em; color: #FFF; font-weight: bold; cursor: pointer;}\n" ) ] ), @@ -493,7 +629,7 @@ function calendar_view_table_html( { }, ( - rows + stuff .map( (row) => ( new lib_plankton.xml.class_node_complex( @@ -594,6 +730,112 @@ function calendar_view_table_html( ), } ), + "title": ( + lib_plankton.string.coin( + "[{{calendar_name}}] {{event_name}}\n", + { + "calendar_name": entry.calendar_id.toFixed(0), + "event_name": entry.event.name, + } + ) + + + "--\n" + + + ( + (entry.event.begin.time !== null) + ? + lib_plankton.string.coin( + "{{label}}: {{value}}\n", + { + "label": "Anfang", // TODO: translate + "value": lib_plankton.string.coin( + "{{hour}}:{{minute}}", + { + "hour": entry.event.begin.time.hour.toFixed(0).padStart(2, "0"), + "minute": entry.event.begin.time.minute.toFixed(0).padStart(2, "0"), + } + ), // TODO: outsource + } + ) + : + "" + ) + + + ( + (entry.event.end !== null) + ? + lib_plankton.string.coin( + "{{label}}: {{value}}\n", + { + "label": "Ende", // TODO: translate + "value": ( + [ + ( + ( + (entry.event.end.date.year !== entry.event.begin.date.year) + || + (entry.event.end.date.month !== entry.event.begin.date.month) + || + (entry.event.end.date.day !== entry.event.begin.date.day) + ) + ? + lib_plankton.string.coin( + "{{year}}-{{month}}-{{day}}", + { + "year": entry.event.end.date.year.toFixed(0).padStart(4, "0"), + "month": entry.event.end.date.month.toFixed(0).padStart(2, "0"), + "day": entry.event.end.date.month.toFixed(0).padStart(2, "0"), + } + ) + : + null + ), + ( + (entry.event.end.time !== null) + ? + lib_plankton.string.coin( + "{{hour}}:{{minute}}", + { + "hour": entry.event.end.time.hour.toFixed(0).padStart(2, "0"), + "minute": entry.event.end.time.minute.toFixed(0).padStart(2, "0"), + } + ) + : + null + ), + ] + .filter(x => (x !== null)) + .join(",") + ), + } + ) + : + "" + ) + + + ( + (entry.event.description !== null) + ? + ( + "--\n" + + + lib_plankton.string.coin( + "{{description}}\n", + { + "description": ( + (entry.event.description !== null) + ? + entry.event.description + : + "?" + ), + } + ) + ) + : + "" + ) + ), }, [ new lib_plankton.xml.class_node_text(entry.event.name), @@ -620,3 +862,133 @@ function calendar_view_table_html( ).compile() ); } + + +/** + */ +async function calendar_view_list_data( + data : type_datamodel, + calendar_id : type_calendar_id, + options : { + from ?: type_pit; + to ?: type_pit; + timezone_shift ?: int; + } = {} +) : Promise< + Array< + { + calendar_id : type_calendar_id; + event : type_event; + } + > +> +{ + const now_pit : type_pit = pit_now(); + options = Object.assign( + { + "from": lib_plankton.call.convey( + now_pit, + [ + (x : type_pit) => pit_shift_day(x, -1), + ] + ), + "to": lib_plankton.call.convey( + now_pit, + [ + (x : type_pit) => pit_shift_week(x, +4), + ] + ), + "timezone_shift": 0, + }, + options + ); + + const entries : Array< + { + calendar_id : type_calendar_id; + event : type_event; + } + > = await calendar_gather_events( + data, + calendar_id, + (options.from as type_pit), + (options.to as type_pit) + ); + // TODO: optimize + entries.sort( + (entry_1, entry_2) => (pit_from_datetime(entry_1.event.begin) - pit_from_datetime(entry_2.event.begin)) + ); + + return Promise.resolve(entries); +} + + +/** + */ +async function calendar_view_list_html( + data : type_datamodel, + calendar_id : type_calendar_id, + options : { + from ?: type_pit; + to ?: type_pit; + timezone_shift ?: int; + } = {} +) : Promise +{ + const stuff : Array< + { + calendar_id : type_calendar_id; + event : type_event; + } + > = await calendar_view_list_data( + data, + calendar_id, + options + ); + return Promise.resolve( + 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() + ); +} + diff --git a/source/main.ts b/source/main.ts index b9e2099..00560bc 100644 --- a/source/main.ts +++ b/source/main.ts @@ -8,7 +8,7 @@ async function main( { const arg_handler : lib_plankton.args.class_handler = new lib_plankton.args.class_handler({ "data_path": lib_plankton.args.class_argument.volatile({ - "indicators_long": ["data_path"], + "indicators_long": ["data-path"], "indicators_short": ["d"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, @@ -17,14 +17,32 @@ async function main( "name": "data-path", }), "timezone_shift": lib_plankton.args.class_argument.volatile({ - "indicators_long": ["timezone_shift"], + "indicators_long": ["timezone-shift"], "indicators_short": ["t"], - "type": lib_plankton.args.enum_type.string, + "type": lib_plankton.args.enum_type.integer, "mode": lib_plankton.args.enum_mode.replace, "default": 0, // "info": null, "name": "timezone-shift", }), + "calendar_id": lib_plankton.args.class_argument.volatile({ + "indicators_long": ["calendar"], + "indicators_short": ["c"], + "type": lib_plankton.args.enum_type.integer, + "mode": lib_plankton.args.enum_mode.replace, + "default": 1, + // "info": null, + "name": "calendar_id", + }), + "view_mode": lib_plankton.args.class_argument.volatile({ + "indicators_long": ["view-moode"], + "indicators_short": ["m"], + "type": lib_plankton.args.enum_type.string, + "mode": lib_plankton.args.enum_mode.replace, + "default": "table", + // "info": null, + "name": "view_mode", + }), "help": lib_plankton.args.class_argument.volatile({ "indicators_long": ["help"], "indicators_short": ["h"], @@ -37,6 +55,14 @@ async function main( }); const args : Record = arg_handler.read(lib_plankton.args.enum_environment.cli, args_raw.join(" ")); + // 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": "stdout", "data": {"threshold": "info"}}), + ] + ); + // exec if (args["help"]) { process.stdout.write( @@ -67,6 +93,10 @@ async function main( "object": ( ((calendar_object_raw) => { switch (calendar_object_raw["kind"]) { + default: { + return calendar_object_raw; + break; + } case "concrete": { return { "kind": "concrete", @@ -94,10 +124,6 @@ async function main( }; break; } - case "collection": { - return calendar_object_raw; - break; - } } }) (calendar_entry_raw["object"]) ), @@ -108,21 +134,34 @@ async function main( ), ] ); - const output : string = calendar_view_table_html( - data, - 4, - { - "year": 2024, - "week": 35 - }, - { - "year": 2024, - "week": 41 - }, - { - "timezone_shift": parseInt(args["timezone_shift"]), + let output : string; + switch (args["view_mode"]) { + default: { + output = ""; + throw (new Error("invalid view mode")); + break; } - ); + case "table": { + output = await calendar_view_table_html( + data, + args.calendar_id, + { + "timezone_shift": args["timezone_shift"], + } + ); + break; + } + case "list": { + output = await calendar_view_list_html( + data, + args.calendar_id, + { + "timezone_shift": args["timezone_shift"], + } + ); + break; + } + } process.stdout.write(output); } return Promise.resolve(undefined); @@ -164,7 +203,9 @@ process.stderr.write( .then( () => {} ) + /* .catch( (error) => {process.stderr.write(String(error) + "\n");} ) + */ ); diff --git a/tools/build b/tools/build index cdc38e6..1f4e770 100755 --- a/tools/build +++ b/tools/build @@ -8,13 +8,6 @@ import argparse as _argparse def main(): ## args argument_parser = _argparse.ArgumentParser() - argument_parser.add_argument( - "-t", - "--tests", - action = "store_true", - default = False, - help = "whether to also build the test routines", - ) argument_parser.add_argument( "-o", "--output-directory", @@ -28,8 +21,6 @@ def main(): ## exec targets = [] targets.append("default") - if args.tests: - targets.append("test") _os.system( "make dir_build=%s --file=tools/makefile %s" % (