voidcli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- void/__init__.py +1 -0
- void/controllers/__init__.py +1 -0
- void/controllers/activity.py +44 -0
- void/controllers/category.py +30 -0
- void/controllers/note.py +47 -0
- void/database/__init__.py +16 -0
- void/database/connection.py +59 -0
- void/database/init_tables.sql +47 -0
- void/main.py +153 -0
- void/models/__init__.py +1 -0
- void/models/activity.py +68 -0
- void/models/category.py +40 -0
- void/models/note.py +64 -0
- void/views/__init__.py +1 -0
- void/views/activity.py +247 -0
- void/views/collection.py +52 -0
- void/views/day_note.py +101 -0
- void/views/note.py +47 -0
- void/views/note_form.py +144 -0
- void/views/welcome.py +72 -0
- voidcli-0.1.0.dist-info/METADATA +77 -0
- voidcli-0.1.0.dist-info/RECORD +25 -0
- voidcli-0.1.0.dist-info/WHEEL +4 -0
- voidcli-0.1.0.dist-info/entry_points.txt +2 -0
- voidcli-0.1.0.dist-info/licenses/LICENSE +21 -0
void/views/activity.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
from textual import on
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual.containers import Horizontal, Vertical
|
|
6
|
+
from textual.message import Message
|
|
7
|
+
from textual.widgets import Button, DataTable, Input, Label, Select, Static
|
|
8
|
+
|
|
9
|
+
from void.controllers.activity import ActivityController
|
|
10
|
+
from void.controllers.category import CategoryController
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ActivitiesView(Static):
|
|
14
|
+
|
|
15
|
+
class Changed(Message):
|
|
16
|
+
"""Posted when the activity list changes, so NoteForm can re-render."""
|
|
17
|
+
|
|
18
|
+
DEFAULT_CSS = """
|
|
19
|
+
#category_module{
|
|
20
|
+
height: 1fr;
|
|
21
|
+
padding: 0 2 0 0;
|
|
22
|
+
width: 30%;
|
|
23
|
+
border-right: solid $primary;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#activity_module{
|
|
27
|
+
padding: 0 0 0 2;
|
|
28
|
+
height: 1fr;
|
|
29
|
+
width: 70%;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.field_label{
|
|
33
|
+
margin: 1 0 0 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#add_category{
|
|
37
|
+
margin: 1 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#activity_form{
|
|
41
|
+
height: auto;
|
|
42
|
+
margin: 0 0 1 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#activity_form_cta_container{
|
|
46
|
+
height: auto;
|
|
47
|
+
margin: 1 0 0 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#activity_form_cta_container Button{
|
|
51
|
+
margin: 0 1 0 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#activities_table{
|
|
55
|
+
height: 1fr;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#activity_id{
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
def __init__(self) -> None:
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.ctrl = ActivityController()
|
|
68
|
+
self.categoriesCtrl = CategoryController()
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
def compose(self) -> ComposeResult:
|
|
72
|
+
with Vertical(classes="header"):
|
|
73
|
+
yield Label("VOID ACTIVITIES", classes="module_title")
|
|
74
|
+
yield Label(self.counter_text(), id="counter")
|
|
75
|
+
with Horizontal(classes="main_container"):
|
|
76
|
+
with Vertical(id="category_module"):
|
|
77
|
+
yield Label("CATEGORIES", classes="module_title")
|
|
78
|
+
yield Label("New Category", classes="field_label")
|
|
79
|
+
yield Input(placeholder="Add a new category", id="new_category")
|
|
80
|
+
yield Button(label="ADD", flat=True, id="add_category")
|
|
81
|
+
yield DataTable(id="categories_table")
|
|
82
|
+
with Vertical(id="activity_module"):
|
|
83
|
+
with Vertical(id="activity_form"):
|
|
84
|
+
yield Label("ACTIVITIES", classes="module_title")
|
|
85
|
+
yield Label("Activity", classes="field_label")
|
|
86
|
+
yield Input(placeholder="Add a new activity", id="new_activity")
|
|
87
|
+
yield Label("Select a category", classes="field_label")
|
|
88
|
+
yield Select([], type_to_search=True, id="category_select")
|
|
89
|
+
yield Input(id="activity_id", compact=True)
|
|
90
|
+
with Horizontal(id="activity_form_cta_container"):
|
|
91
|
+
yield Button(label="ADD", flat=True, id="add_activity", variant="success")
|
|
92
|
+
yield Button(label="UPDATE", flat=True, id="update_activity",variant="warning", disabled=True)
|
|
93
|
+
yield Button(label="SUSPEND", flat=True, id="suspend_activity", variant="error", disabled=True)
|
|
94
|
+
yield Button(label="CLEAR", flat=True, id="clear_activity_form", variant="default")
|
|
95
|
+
yield DataTable(id="activities_table", cursor_type="row")
|
|
96
|
+
|
|
97
|
+
def on_mount(self) -> None:
|
|
98
|
+
# Category inputs
|
|
99
|
+
self.add_category_input = self.query_one("#new_category", Input)
|
|
100
|
+
self.add_category_btn = self.query_one("#add_category", Button)
|
|
101
|
+
self.categories_table = self.query_one("#categories_table", DataTable)
|
|
102
|
+
|
|
103
|
+
# Activity inputs
|
|
104
|
+
self.add_activity_input = self.query_one("#new_activity", Input)
|
|
105
|
+
self.category_selector = self.query_one("#category_select", Select)
|
|
106
|
+
self.activity_id = self.query_one("#activity_id", Input)
|
|
107
|
+
self.add_activity_btn = self.query_one("#add_activity", Button)
|
|
108
|
+
self.update_activity_btn = self.query_one("#update_activity", Button)
|
|
109
|
+
self.suspend_activity_btn = self.query_one("#suspend_activity", Button)
|
|
110
|
+
self.clear_activity_form_btn = self.query_one("#clear_activity_form", Button)
|
|
111
|
+
self.activities_table = self.query_one("#activities_table", DataTable)
|
|
112
|
+
|
|
113
|
+
self.init_activities_table()
|
|
114
|
+
self.init_categories_selector()
|
|
115
|
+
self.init_categories_table()
|
|
116
|
+
|
|
117
|
+
def counter_text(self) -> str:
|
|
118
|
+
activities = len(self.ctrl.get_activities())
|
|
119
|
+
categories = len(self.categoriesCtrl.get_categories())
|
|
120
|
+
return f"{activities} activities in {categories} categories"
|
|
121
|
+
|
|
122
|
+
def update_counter(self) -> None:
|
|
123
|
+
self.query_one("#counter", Label).update(self.counter_text())
|
|
124
|
+
|
|
125
|
+
def init_activities_table(self):
|
|
126
|
+
activities = self.ctrl.get_activities()
|
|
127
|
+
self.activities_table.add_columns("ID","ACTIVITY", "CATEGORY")
|
|
128
|
+
self.activities_table.add_rows(activities)
|
|
129
|
+
|
|
130
|
+
def init_categories_selector(self):
|
|
131
|
+
self.category_selector.clear()
|
|
132
|
+
self.category_selector.set_options(self.categoriesCtrl.get_categories_for_activities())
|
|
133
|
+
|
|
134
|
+
def init_categories_table(self):
|
|
135
|
+
categories = self.categoriesCtrl.get_categories()
|
|
136
|
+
self.categories_table.add_columns("CATEGORY")
|
|
137
|
+
self.categories_table.add_rows(categories)
|
|
138
|
+
|
|
139
|
+
def update_activities(self):
|
|
140
|
+
activities = self.ctrl.get_activities()
|
|
141
|
+
self.activities_table.clear()
|
|
142
|
+
self.activities_table.add_rows(activities)
|
|
143
|
+
self.update_counter()
|
|
144
|
+
|
|
145
|
+
def update_categories(self):
|
|
146
|
+
categories = self.categoriesCtrl.get_categories()
|
|
147
|
+
self.categories_table.clear()
|
|
148
|
+
self.categories_table.add_rows(categories)
|
|
149
|
+
self.init_categories_selector()
|
|
150
|
+
self.update_counter()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@on(Button.Pressed, "#clear_activity_form")
|
|
154
|
+
def on_clear_activity_form(self) -> None:
|
|
155
|
+
self.clear_activity_form()
|
|
156
|
+
|
|
157
|
+
@on(Button.Pressed, "#add_activity")
|
|
158
|
+
def on_add_activity(self) -> None:
|
|
159
|
+
activity = self.add_activity_input.value
|
|
160
|
+
category_id = self.category_selector.value
|
|
161
|
+
|
|
162
|
+
if not activity:
|
|
163
|
+
self.notify("Activity must be set", severity="error")
|
|
164
|
+
return
|
|
165
|
+
if not category_id:
|
|
166
|
+
self.notify("Category must be set", severity="error")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if not self.ctrl.create_activity(activity, category_id):
|
|
170
|
+
self.notify(f"'{activity}' already exists in that category", severity="error")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
self.add_activity_input.value = ""
|
|
174
|
+
self.category_selector.clear()
|
|
175
|
+
self.update_activities()
|
|
176
|
+
self.post_message(self.Changed())
|
|
177
|
+
|
|
178
|
+
@on(Button.Pressed, "#update_activity")
|
|
179
|
+
def on_update_activity(self) -> None:
|
|
180
|
+
activity = self.add_activity_input.value
|
|
181
|
+
activity_id = self.activity_id.value
|
|
182
|
+
category_id = self.category_selector.value
|
|
183
|
+
|
|
184
|
+
if not activity:
|
|
185
|
+
self.notify("Activity must be set", severity="error")
|
|
186
|
+
return
|
|
187
|
+
if not category_id:
|
|
188
|
+
self.notify("Category must be set", severity="error")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if not self.ctrl.update_activity(activity_id, activity, category_id):
|
|
192
|
+
self.notify(f"'{activity}' already exists in that category", severity="error")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
self.update_activities()
|
|
196
|
+
self.clear_activity_form()
|
|
197
|
+
self.post_message(self.Changed())
|
|
198
|
+
|
|
199
|
+
@on(Button.Pressed, "#suspend_activity")
|
|
200
|
+
def on_suspend_activity(self) -> None:
|
|
201
|
+
activity_id = self.activity_id.value
|
|
202
|
+
self.ctrl.suspend_activity(activity_id)
|
|
203
|
+
self.clear_activity_form()
|
|
204
|
+
self.update_activities()
|
|
205
|
+
self.post_message(self.Changed())
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@on(Button.Pressed, "#add_category")
|
|
210
|
+
def on_add_category(self) -> None:
|
|
211
|
+
category = self.add_category_input.value
|
|
212
|
+
if not category:
|
|
213
|
+
self.notify("Category must be set", severity="error")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
if not self.categoriesCtrl.create_category(category):
|
|
217
|
+
self.notify(f"Category '{category}' already exists", severity="error")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
self.add_category_input.value = ""
|
|
221
|
+
self.update_categories()
|
|
222
|
+
|
|
223
|
+
@on(DataTable.RowSelected, "#activities_table")
|
|
224
|
+
def on_activity_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
225
|
+
row = event.data_table.get_row(event.row_key)
|
|
226
|
+
ac_id = row[0]
|
|
227
|
+
ac_name = row[1]
|
|
228
|
+
ac_category = row[2]
|
|
229
|
+
|
|
230
|
+
self.add_activity_input.value = ac_name
|
|
231
|
+
self.activity_id.value = str(ac_id)
|
|
232
|
+
|
|
233
|
+
categories = self.categoriesCtrl.get_categories_for_activities()
|
|
234
|
+
category_id = next((cid for name, cid in categories if name == ac_category), Select.BLANK)
|
|
235
|
+
self.category_selector.value = category_id
|
|
236
|
+
|
|
237
|
+
self.update_activity_btn.disabled = False
|
|
238
|
+
self.add_activity_btn.disabled = True
|
|
239
|
+
self.suspend_activity_btn.disabled = False
|
|
240
|
+
|
|
241
|
+
def clear_activity_form(self):
|
|
242
|
+
self.add_activity_input.value = ""
|
|
243
|
+
self.activity_id.value = ""
|
|
244
|
+
self.category_selector.clear()
|
|
245
|
+
self.update_activity_btn.disabled = True
|
|
246
|
+
self.add_activity_btn.disabled = False
|
|
247
|
+
self.suspend_activity_btn.disabled = True
|
void/views/collection.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from itertools import groupby
|
|
3
|
+
from typing import override
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Grid, Vertical, VerticalScroll
|
|
7
|
+
from textual.widgets import Label, Static
|
|
8
|
+
|
|
9
|
+
from void.controllers.note import NoteController
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CollectionView(Static):
|
|
13
|
+
|
|
14
|
+
DEFAULT_CSS = """
|
|
15
|
+
.note_grid{
|
|
16
|
+
grid-size: 3;
|
|
17
|
+
grid-gutter: 1;
|
|
18
|
+
}
|
|
19
|
+
.note_card_date{
|
|
20
|
+
color: $text-muted;
|
|
21
|
+
padding: 0 0 1 0;
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.ctrl = NoteController()
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
def compose(self) -> ComposeResult:
|
|
32
|
+
notes = self.ctrl.get_notes()
|
|
33
|
+
with Vertical(classes="header"):
|
|
34
|
+
yield Label("VOID COLLECTION", classes="module_title")
|
|
35
|
+
with VerticalScroll(classes="main_container"):
|
|
36
|
+
if not notes:
|
|
37
|
+
yield Label("No VOID NOTE saved yet.")
|
|
38
|
+
return
|
|
39
|
+
with Grid(classes="card_grid note_grid"):
|
|
40
|
+
for note_date, rows in groupby(notes, key=lambda row: row["note_date"]):
|
|
41
|
+
with Vertical(classes="note_card"):
|
|
42
|
+
yield Label(self.format_date(note_date), classes="note_card_date")
|
|
43
|
+
for row in rows:
|
|
44
|
+
with Vertical(classes="entry_card"):
|
|
45
|
+
yield Label(row["activity"], classes="entry_title")
|
|
46
|
+
if row["notes"]:
|
|
47
|
+
yield Label(row["notes"], classes="entry_note")
|
|
48
|
+
else:
|
|
49
|
+
yield Label("No notes written.", classes="entry_empty")
|
|
50
|
+
|
|
51
|
+
def format_date(self, date_str):
|
|
52
|
+
return date.fromisoformat(date_str).strftime("%B %d, %Y").upper()
|
void/views/day_note.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from datetime import date
|
|
3
|
+
from itertools import groupby
|
|
4
|
+
from typing import override
|
|
5
|
+
|
|
6
|
+
from textual import on
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import Center, Horizontal, Vertical, VerticalScroll
|
|
9
|
+
from textual.widgets import Button, Label, Static, TabbedContent
|
|
10
|
+
|
|
11
|
+
from void.controllers.note import NoteController
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DayNote(Static):
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
.day_logged{
|
|
18
|
+
width: 2fr;
|
|
19
|
+
}
|
|
20
|
+
.day_pending{
|
|
21
|
+
width: 1fr;
|
|
22
|
+
padding: 0 0 0 2;
|
|
23
|
+
}
|
|
24
|
+
.day_pending Label{
|
|
25
|
+
width: 100%;
|
|
26
|
+
}
|
|
27
|
+
.pending_category{
|
|
28
|
+
color: $text-muted;
|
|
29
|
+
padding: 1 0 0 3;
|
|
30
|
+
}
|
|
31
|
+
.pending_item{
|
|
32
|
+
color: $text-disabled;
|
|
33
|
+
padding: 0 0 0 2;
|
|
34
|
+
border-left: thick $primary;
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.ctrl = NoteController()
|
|
42
|
+
|
|
43
|
+
self.today = date.today() # noqa: DTZ011
|
|
44
|
+
self.date_str = self.today.strftime("%B %d, %Y").upper()
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
def compose(self) -> ComposeResult:
|
|
48
|
+
day_note_data = self.ctrl.get_day_note(self.today.isoformat())
|
|
49
|
+
activities = self.ctrl.get_activities()
|
|
50
|
+
per_category = Counter(category for _, _, category in activities)
|
|
51
|
+
|
|
52
|
+
logged_ids = {row["activity_id"] for row in day_note_data}
|
|
53
|
+
pending = sorted(
|
|
54
|
+
(category, name)
|
|
55
|
+
for activity_id, name, category in activities
|
|
56
|
+
if activity_id not in logged_ids
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
with Vertical(classes="header"):
|
|
60
|
+
yield Label(self.date_str, classes="module_title")
|
|
61
|
+
yield Label(self.counter_text(len(day_note_data), len(activities)), id="counter")
|
|
62
|
+
|
|
63
|
+
if not day_note_data:
|
|
64
|
+
with Vertical(classes="state_screen"), Vertical(classes="state_card"):
|
|
65
|
+
yield Label("NOTHING LOGGED TODAY", classes="state_title")
|
|
66
|
+
yield Label("Write today's note to fill the void.", classes="state_hint")
|
|
67
|
+
with Center():
|
|
68
|
+
yield Button("WRITE VOID NOTE", id="go_to_note", variant="primary", flat=True)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
with Horizontal(classes="main_container"):
|
|
72
|
+
with VerticalScroll(classes="day_logged"):
|
|
73
|
+
for category, group in groupby(day_note_data, key=lambda row: row["category"]):
|
|
74
|
+
rows = list(group)
|
|
75
|
+
yield Label(
|
|
76
|
+
f"── {category.upper()} · {len(rows)} of {per_category[category]} ",
|
|
77
|
+
classes="section_title",
|
|
78
|
+
)
|
|
79
|
+
for row in rows:
|
|
80
|
+
with Vertical(classes="entry_card"):
|
|
81
|
+
yield Label(row["activity"], classes="entry_title")
|
|
82
|
+
if row["notes"]:
|
|
83
|
+
yield Label(row["notes"], classes="entry_note")
|
|
84
|
+
else:
|
|
85
|
+
yield Label("No notes written.", classes="entry_empty")
|
|
86
|
+
|
|
87
|
+
if pending:
|
|
88
|
+
with VerticalScroll(classes="day_pending"):
|
|
89
|
+
yield Label(f"── NOT LOGGED · {len(pending)} ", classes="section_title")
|
|
90
|
+
for category, group in groupby(pending, key=lambda item: item[0]):
|
|
91
|
+
yield Label(category.upper(), classes="pending_category")
|
|
92
|
+
for _, name in group:
|
|
93
|
+
yield Label(name, classes="pending_item")
|
|
94
|
+
|
|
95
|
+
def counter_text(self, logged: int, total: int) -> str:
|
|
96
|
+
activities = "activity" if total == 1 else "activities"
|
|
97
|
+
return f"{logged} of {total} {activities} logged"
|
|
98
|
+
|
|
99
|
+
@on(Button.Pressed, "#go_to_note")
|
|
100
|
+
def on_go_to_note(self) -> None:
|
|
101
|
+
self.app.query_one(TabbedContent).active = "void_note"
|
void/views/note.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from typing import override
|
|
3
|
+
|
|
4
|
+
from textual import on
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Center, Vertical
|
|
7
|
+
from textual.widgets import Button, Label, Static, TabbedContent
|
|
8
|
+
|
|
9
|
+
from void.controllers.note import NoteController
|
|
10
|
+
from void.views.note_form import NoteForm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NoteView(Static):
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@override
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.ctrl = NoteController()
|
|
20
|
+
self.today = date.today() # noqa: DTZ011
|
|
21
|
+
self.date_str = self.today.strftime("%B %d, %Y").upper()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
def compose(self) -> ComposeResult:
|
|
26
|
+
created_note_of_the_day = self.ctrl.get_day_note(self.today.isoformat())
|
|
27
|
+
if created_note_of_the_day:
|
|
28
|
+
with Vertical(classes="state_screen"), Vertical(classes="state_card state_card_success"):
|
|
29
|
+
yield Label(self.date_str, classes="state_muted")
|
|
30
|
+
yield Label("VOID NOTE SAVED", classes="state_title state_success")
|
|
31
|
+
yield Label(self.hint_text(len(created_note_of_the_day)), classes="state_hint")
|
|
32
|
+
with Center():
|
|
33
|
+
yield Button("SEE VOID DAY", id="go_to_day_note", variant="success", flat=True)
|
|
34
|
+
else:
|
|
35
|
+
yield NoteForm()
|
|
36
|
+
|
|
37
|
+
def hint_text(self, logged: int) -> str:
|
|
38
|
+
activities = "activity" if logged == 1 else "activities"
|
|
39
|
+
return f"{logged} {activities} logged today"
|
|
40
|
+
|
|
41
|
+
@on(Button.Pressed, "#go_to_day_note")
|
|
42
|
+
def on_go_to_day_note(self) -> None:
|
|
43
|
+
self.app.query_one(TabbedContent).active = "void_day_note"
|
|
44
|
+
|
|
45
|
+
@on(NoteForm.Saved)
|
|
46
|
+
async def on_note_saved(self) -> None:
|
|
47
|
+
await self.recompose()
|
void/views/note_form.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from typing import override
|
|
3
|
+
|
|
4
|
+
from textual import on
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.containers import Grid, Horizontal, Vertical, VerticalScroll
|
|
8
|
+
from textual.message import Message
|
|
9
|
+
from textual.widgets import Button, Checkbox, Input, Label, Static, TextArea
|
|
10
|
+
|
|
11
|
+
from void.controllers.note import NoteController
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NoteForm(Static):
|
|
15
|
+
|
|
16
|
+
class Saved(Message):
|
|
17
|
+
"""Posted once the day's note is stored, so NoteView can swap to DayNote."""
|
|
18
|
+
|
|
19
|
+
BINDINGS = [Binding("ctrl+s", "save", "SAVE VOID NOTE", priority=True)]
|
|
20
|
+
|
|
21
|
+
DEFAULT_CSS = """
|
|
22
|
+
NoteForm{
|
|
23
|
+
layout: vertical;
|
|
24
|
+
}
|
|
25
|
+
.actions{
|
|
26
|
+
dock: bottom;
|
|
27
|
+
height: auto;
|
|
28
|
+
padding: 0 2 1 2;
|
|
29
|
+
align-horizontal: right;
|
|
30
|
+
}
|
|
31
|
+
#date_str{
|
|
32
|
+
display: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.category_block{
|
|
36
|
+
height: auto;
|
|
37
|
+
padding: 0 0 1 0;
|
|
38
|
+
}
|
|
39
|
+
.category_title{
|
|
40
|
+
text-style: bold;
|
|
41
|
+
color: $accent;
|
|
42
|
+
padding: 0 0 1 0;
|
|
43
|
+
}
|
|
44
|
+
.activity_grid{
|
|
45
|
+
grid-size: 2;
|
|
46
|
+
grid-gutter: 1 2;
|
|
47
|
+
}
|
|
48
|
+
.activity_card{
|
|
49
|
+
height: auto;
|
|
50
|
+
padding: 1 1;
|
|
51
|
+
}
|
|
52
|
+
.activity_card Checkbox{
|
|
53
|
+
width: 100%;
|
|
54
|
+
}
|
|
55
|
+
.activity_card TextArea{
|
|
56
|
+
height: 6;
|
|
57
|
+
}
|
|
58
|
+
.activity_handle{
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
super().__init__()
|
|
66
|
+
self.ctrl = NoteController()
|
|
67
|
+
self.activities_by_category_data = self.ctrl.get_activities_by_category()
|
|
68
|
+
self.activity_total = self.ctrl.get_activity_total()
|
|
69
|
+
|
|
70
|
+
self.today = date.today() # noqa: DTZ011
|
|
71
|
+
self.date_str = self.today.strftime("%B %d, %Y").upper()
|
|
72
|
+
self.date_iso = self.today.isoformat()
|
|
73
|
+
|
|
74
|
+
day_note_data = self.ctrl.get_day_note(self.today.isoformat())
|
|
75
|
+
|
|
76
|
+
self.disable_add_note_btn = False
|
|
77
|
+
if day_note_data:
|
|
78
|
+
self.disable_add_note_btn = True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
def compose(self) -> ComposeResult:
|
|
84
|
+
with Vertical(classes="header"):
|
|
85
|
+
yield Label(self.date_str, classes="module_title")
|
|
86
|
+
yield Label(self.counter_text(0), id="counter")
|
|
87
|
+
yield Input(value=self.date_iso, id="date_str")
|
|
88
|
+
with Horizontal(classes="actions"):
|
|
89
|
+
yield Button("SAVE VOID NOTE", id="save_note_btn", variant="success", flat=True, disabled=self.disable_add_note_btn)
|
|
90
|
+
with VerticalScroll(classes="main_container"):
|
|
91
|
+
for _, category, activities in self.activities_by_category_data:
|
|
92
|
+
if activities:
|
|
93
|
+
with Vertical(classes="category_block"):
|
|
94
|
+
yield Label(f"── {category} ", classes="category_title")
|
|
95
|
+
with Grid(classes="card_grid activity_grid"):
|
|
96
|
+
for id, name in activities:
|
|
97
|
+
handle = f"{id}::{self.handleize(name)}"
|
|
98
|
+
with Vertical(classes="activity_card"):
|
|
99
|
+
yield Checkbox(label=name)
|
|
100
|
+
yield Input(value=handle, classes="activity_handle")
|
|
101
|
+
yield TextArea()
|
|
102
|
+
|
|
103
|
+
@on(Checkbox.Changed)
|
|
104
|
+
def on_activity_toggled(self) -> None:
|
|
105
|
+
self.query_one("#counter", Label).update(self.counter_text(self.count_check_elements()))
|
|
106
|
+
|
|
107
|
+
def counter_text(self, checked: int) -> str:
|
|
108
|
+
return f"{checked} of {self.activity_total} activities logged"
|
|
109
|
+
|
|
110
|
+
def action_save(self) -> None:
|
|
111
|
+
if not self.disable_add_note_btn:
|
|
112
|
+
self.on_save_note()
|
|
113
|
+
|
|
114
|
+
@on(Button.Pressed, "#save_note_btn")
|
|
115
|
+
def on_save_note(self) -> None:
|
|
116
|
+
if self.count_check_elements() == 0:
|
|
117
|
+
self.notify("Please check at least one activity to save", severity="warning")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
checked_activities = []
|
|
121
|
+
for checkbox in self.query(Checkbox):
|
|
122
|
+
if checkbox.value:
|
|
123
|
+
parent_container = checkbox.query_ancestor(Vertical)
|
|
124
|
+
handle = parent_container.query_one(Input).value
|
|
125
|
+
note = parent_container.query_one(TextArea).text
|
|
126
|
+
checked_activities.append((handle, note))
|
|
127
|
+
|
|
128
|
+
self.save_note(checked_activities)
|
|
129
|
+
|
|
130
|
+
def save_note(self, activities):
|
|
131
|
+
date_str = self.query_one("#date_str", Input).value.strip()
|
|
132
|
+
self.ctrl.save_note(date_str, activities)
|
|
133
|
+
self.notify("VOID NOTE successfully saved", severity="information")
|
|
134
|
+
self.post_message(self.Saved())
|
|
135
|
+
|
|
136
|
+
def count_check_elements(self) -> int:
|
|
137
|
+
counter = 0
|
|
138
|
+
for checkbox in self.query(Checkbox):
|
|
139
|
+
if checkbox.value:
|
|
140
|
+
counter += 1
|
|
141
|
+
return counter
|
|
142
|
+
|
|
143
|
+
def handleize(self, string_to_handle):
|
|
144
|
+
return string_to_handle.lower().replace("'","").replace(" ", "-")
|
void/views/welcome.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Vertical
|
|
5
|
+
from textual.events import Key
|
|
6
|
+
from textual.screen import Screen
|
|
7
|
+
from textual.widgets import Label, Rule
|
|
8
|
+
|
|
9
|
+
LOGO = """\
|
|
10
|
+
█████ █████ ███████ █████ ██████████
|
|
11
|
+
░░███ ░░███ ███░░░░░███ ░░███ ░░███░░░░███
|
|
12
|
+
░███ ░███ ███ ░░███ ░███ ░███ ░░███
|
|
13
|
+
░███ ░███ ░███ ░███ ░███ ░███ ░███
|
|
14
|
+
░░███ ███ ░███ ░███ ░███ ░███ ░███
|
|
15
|
+
░░░█████░ ░░███ ███ ░███ ░███ ███
|
|
16
|
+
░░███ ░░░███████░ █████ ██████████
|
|
17
|
+
░░░ ░░░░░░░ ░░░░░ ░░░░░░░░░░\
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
AUTO_DISMISS = 1.75
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WelcomeScreen(Screen[None]):
|
|
25
|
+
|
|
26
|
+
DEFAULT_CSS = """
|
|
27
|
+
.welcome_card{
|
|
28
|
+
width: 59;
|
|
29
|
+
max-width: 100%;
|
|
30
|
+
height: auto;
|
|
31
|
+
padding: 2 4;
|
|
32
|
+
border: round $primary;
|
|
33
|
+
}
|
|
34
|
+
.welcome_card Label{
|
|
35
|
+
width: 100%;
|
|
36
|
+
text-align: center;
|
|
37
|
+
}
|
|
38
|
+
.welcome_logo{
|
|
39
|
+
color: $primary;
|
|
40
|
+
text-style: bold;
|
|
41
|
+
}
|
|
42
|
+
.welcome_card Rule{
|
|
43
|
+
color: $primary;
|
|
44
|
+
}
|
|
45
|
+
.welcome_tagline{
|
|
46
|
+
padding: 0 0 1 0;
|
|
47
|
+
color: $text-muted;
|
|
48
|
+
}
|
|
49
|
+
.welcome_hint{
|
|
50
|
+
color: $accent;
|
|
51
|
+
text-style: italic;
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
def compose(self) -> ComposeResult:
|
|
57
|
+
with Vertical(classes="state_screen"), Vertical(classes="welcome_card"):
|
|
58
|
+
yield Label(LOGO, classes="welcome_logo")
|
|
59
|
+
yield Rule(line_style="heavy")
|
|
60
|
+
yield Label("VITAL OFFLINE INFORMATION DIARY", classes="welcome_tagline")
|
|
61
|
+
yield Label("press any key to skip", classes="welcome_hint")
|
|
62
|
+
|
|
63
|
+
def on_mount(self) -> None:
|
|
64
|
+
self.set_timer(AUTO_DISMISS, self.close)
|
|
65
|
+
|
|
66
|
+
def on_key(self, _: Key) -> None:
|
|
67
|
+
self.close()
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
# returns None on purpose: awaiting dismiss() from a handler is an error
|
|
71
|
+
if self.app.screen is self:
|
|
72
|
+
self.dismiss()
|