135 lines
4.7 KiB
JavaScript
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));
|
|
}
|
|
});
|