Files
2026-09-19 22:50:49 +02:00

135 lines
4.7 KiB
JavaScript

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import St from 'gi://St';
import * as D from './dates.js';
import {escapeMarkup} from './markdown.js';
import {contextMenu, dropdown, openDatePicker, openTimePicker, Popover, scrollBox, textButton} from './widgets.js';
// '17.09. 14:00', '17.09.' oder '' bei allgemeinen Todos.
export function todoWhen(todo) {
if (!todo.date)
return '';
return todo.time ? `${D.formatDayMonth(todo.date)} ${todo.time}` : D.formatDayMonth(todo.date);
}
// Erste Zeile des Textes für Listen.
export function todoTitle(todo) {
return todo.title.split('\n')[0];
}
// Popover am Mauszeiger mit Textfeld sowie Auswahl für Datum und Uhrzeit. Enter speichert.
export function editTodo(store, todo) {
const popover = new Popover();
popover.addLabel(todo.id ? 'Todo bearbeiten' : 'Neues Todo', 'popover-title');
const text = popover.addEntry(todo.title ?? '', 'Was ist zu tun?');
let date = todo.date ?? null;
let time = todo.time ?? null;
const row = new St.BoxLayout({style_class: 'popover-row'});
const dateButton = dropdown('', b => openDatePicker(b, date, picked => {
date = picked;
if (!date)
time = null;
updateLabels();
}), text.clutter_text);
const timeButton = dropdown('', b => openTimePicker(b, time, picked => {
time = picked;
// Eine Uhrzeit ohne Datum meint heute.
if (time && !date)
date = D.today();
updateLabels();
}), text.clutter_text);
const updateLabels = () => {
dateButton.setLabel(date ? D.formatDate(date) : 'Kein Datum');
timeButton.setLabel(time ?? 'Keine Uhrzeit');
};
updateLabels();
row.add_child(dateButton);
row.add_child(timeButton);
popover.add(row);
popover.setSave(() => {
const title = text.get_text().trim();
if (!title)
return 'Text fehlt';
store.saveTodo({...todo, title, date, time});
});
popover.open(text.clutter_text);
return popover;
}
export function todoMenu(store, todo) {
contextMenu([{label: 'Löschen', onSelect: () => store.removeTodo(todo.id)}]);
}
// Gilt überall, wo ein Todo steht: links bearbeiten, Mitte abhaken, rechts Menü.
export function todoClick(store, todo, mouseButton) {
if (mouseButton === 2)
store.toggleTodo(todo.id);
else if (mouseButton === 3)
todoMenu(store, todo);
else
editTodo(store, todo);
}
// Zeile mit Häkchen, Titel und Termin. Links bearbeitet, Mitte hakt ab, rechts öffnet das Menü.
export function todoRow(store, todo) {
const row = new St.BoxLayout({style_class: 'todo-row', x_expand: true});
const check = new St.Button({
style_class: 'todo-check',
can_focus: true,
child: new St.Icon({icon_name: todo.done ? 'checkbox-checked-symbolic' : 'checkbox-symbolic', icon_size: 16}),
});
check.connect('clicked', () => store.toggleTodo(todo.id));
row.add_child(check);
const title = textButton(todoTitle(todo), (_b, mouseButton) => todoClick(store, todo, mouseButton),
todo.done ? 'todo-title done' : 'todo-title');
if (todo.done)
title.child.clutter_text.set_markup(`<s>${escapeMarkup(todoTitle(todo))}</s>`);
row.add_child(title);
const when = todoWhen(todo);
if (when) {
const label = new St.Label({text: when, style_class: 'todo-when', y_align: Clutter.ActorAlign.CENTER});
if (!todo.done && todo.date < D.today())
label.add_style_class_name('overdue');
row.add_child(label);
}
return row;
}
export const TodoPanel = GObject.registerClass(
class TodoPanel extends St.BoxLayout {
_init(store) {
super._init({vertical: true, style_class: 'desktop-panel todos', reactive: true, x_expand: true, y_expand: true});
this._store = store;
const [scroll, list] = scrollBox('todo-scroll');
this._list = list;
this.add_child(scroll);
// Rechtsklick auf die freie Fläche
this.connect('button-press-event', (_actor, event) => {
if (event.get_button() !== Clutter.BUTTON_SECONDARY)
return Clutter.EVENT_PROPAGATE;
contextMenu([{label: 'Erstellen', onSelect: () => editTodo(store, {})}]);
return Clutter.EVENT_STOP;
});
store.onChange(() => this._render());
this._render();
}
_render() {
this._list.destroy_all_children();
const todos = this._store.sortedTodos();
if (!todos.length)
this._list.add_child(new St.Label({text: 'Keine Todos', style_class: 'empty-hint'}));
for (const todo of todos)
this._list.add_child(todoRow(this._store, todo));
}
});