voidcli 1.0.0__tar.gz → 1.2.0__tar.gz
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.
- {voidcli-1.0.0 → voidcli-1.2.0}/PKG-INFO +2 -2
- {voidcli-1.0.0 → voidcli-1.2.0}/README.md +1 -1
- {voidcli-1.0.0 → voidcli-1.2.0}/pyproject.toml +1 -1
- {voidcli-1.0.0 → voidcli-1.2.0}/void/controllers/activity.py +17 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/main.py +11 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/models/activity.py +25 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/views/collection.py +21 -3
- voidcli-1.2.0/void/views/note_modal.py +73 -0
- voidcli-1.2.0/void/views/suspended.py +109 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/.gitignore +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/LICENSE +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/__init__.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/controllers/__init__.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/controllers/category.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/controllers/note.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/database/__init__.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/database/connection.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/database/init_tables.sql +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/models/__init__.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/models/category.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/models/note.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/views/__init__.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/views/activity.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/views/day_note.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/views/note.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/views/note_form.py +0 -0
- {voidcli-1.0.0 → voidcli-1.2.0}/void/views/welcome.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: voidcli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Project-URL: Repository, https://github.com/NelsonUrrutia/void
|
|
5
5
|
Author-email: Nelson Urrutia <nelurrutia15@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -44,7 +44,7 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
# **V**ital **O**ffline **I**nformation **D**iary
|
|
45
45
|
|
|
46
46
|
---
|
|
47
|
-

|
|
48
48
|
|
|
49
49
|
> **VOID** is an offline diary to be used in the terminal.
|
|
50
50
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# **V**ital **O**ffline **I**nformation **D**iary
|
|
13
13
|
|
|
14
14
|
---
|
|
15
|
-

|
|
16
16
|
|
|
17
17
|
> **VOID** is an offline diary to be used in the terminal.
|
|
18
18
|
|
|
@@ -42,3 +42,20 @@ class ActivityController:
|
|
|
42
42
|
def suspend_activity(self, activity_id):
|
|
43
43
|
with Activity() as ac:
|
|
44
44
|
ac.suspend_activity(activity_id)
|
|
45
|
+
|
|
46
|
+
def get_suspended_activities(self) -> list[tuple]:
|
|
47
|
+
with Activity() as ac:
|
|
48
|
+
raw_activities = ac.get_suspended_activities()
|
|
49
|
+
activities = []
|
|
50
|
+
for item in raw_activities:
|
|
51
|
+
activities.append((item['id'], item['activity'], item['category']))
|
|
52
|
+
return activities
|
|
53
|
+
|
|
54
|
+
def restore_activity(self, activity_id) -> bool:
|
|
55
|
+
"""Return False when an active activity already uses that name in its category."""
|
|
56
|
+
with Activity() as ac:
|
|
57
|
+
try:
|
|
58
|
+
ac.restore_activity(activity_id)
|
|
59
|
+
except sqlite3.IntegrityError:
|
|
60
|
+
return False
|
|
61
|
+
return True
|
|
@@ -8,6 +8,7 @@ from void.views.collection import CollectionView
|
|
|
8
8
|
from void.views.day_note import DayNote
|
|
9
9
|
from void.views.note import NoteView
|
|
10
10
|
from void.views.note_form import NoteForm
|
|
11
|
+
from void.views.suspended import SuspendedView
|
|
11
12
|
from void.views.welcome import WelcomeScreen
|
|
12
13
|
|
|
13
14
|
|
|
@@ -159,6 +160,8 @@ class VoidApp(App):
|
|
|
159
160
|
with TabPane("VOID ACTIVITIES", id="void_activities"):
|
|
160
161
|
yield ActivitiesView()
|
|
161
162
|
|
|
163
|
+
with TabPane("VOID SUSPENDED", id="void_suspended"):
|
|
164
|
+
yield SuspendedView()
|
|
162
165
|
|
|
163
166
|
yield Footer()
|
|
164
167
|
|
|
@@ -171,6 +174,14 @@ class VoidApp(App):
|
|
|
171
174
|
@on(ActivitiesView.Changed)
|
|
172
175
|
async def on_activities_changed(self) -> None:
|
|
173
176
|
await self.query_one(NoteView).recompose()
|
|
177
|
+
await self.query_one(DayNote).recompose()
|
|
178
|
+
self.query_one(SuspendedView).update_suspended()
|
|
179
|
+
|
|
180
|
+
@on(SuspendedView.Restored)
|
|
181
|
+
async def on_activity_restored(self) -> None:
|
|
182
|
+
await self.query_one(NoteView).recompose()
|
|
183
|
+
await self.query_one(DayNote).recompose()
|
|
184
|
+
self.query_one(ActivitiesView).update_activities()
|
|
174
185
|
|
|
175
186
|
def on_mount(self) -> None:
|
|
176
187
|
self.title = "VOID"
|
|
@@ -66,3 +66,28 @@ class Activity:
|
|
|
66
66
|
WHERE id = ?
|
|
67
67
|
"""
|
|
68
68
|
db.execute(sql, data)
|
|
69
|
+
|
|
70
|
+
def get_suspended_activities(self):
|
|
71
|
+
with Connection() as db:
|
|
72
|
+
sql = """
|
|
73
|
+
SELECT
|
|
74
|
+
activities.id as "id",
|
|
75
|
+
activities.name as "activity",
|
|
76
|
+
categories.name as "category"
|
|
77
|
+
FROM activities
|
|
78
|
+
INNER JOIN
|
|
79
|
+
categories on categories.id = activities.category_id
|
|
80
|
+
WHERE activities.is_active = 0
|
|
81
|
+
ORDER BY categories.name, activities.name"""
|
|
82
|
+
result = db.querying(sql)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
def restore_activity(self, activity_id):
|
|
86
|
+
with Connection() as db:
|
|
87
|
+
data = (activity_id,)
|
|
88
|
+
sql = """
|
|
89
|
+
UPDATE activities
|
|
90
|
+
SET is_active = 1
|
|
91
|
+
WHERE id = ?
|
|
92
|
+
"""
|
|
93
|
+
db.execute(sql, data)
|
|
@@ -2,11 +2,13 @@ from datetime import date
|
|
|
2
2
|
from itertools import groupby
|
|
3
3
|
from typing import override
|
|
4
4
|
|
|
5
|
+
from textual import on
|
|
5
6
|
from textual.app import ComposeResult
|
|
6
|
-
from textual.containers import Grid, Vertical, VerticalScroll
|
|
7
|
-
from textual.widgets import Label, Static
|
|
7
|
+
from textual.containers import Grid, Right, Vertical, VerticalScroll
|
|
8
|
+
from textual.widgets import Button, Label, Static
|
|
8
9
|
|
|
9
10
|
from void.controllers.note import NoteController
|
|
11
|
+
from void.views.note_modal import NoteModal
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class CollectionView(Static):
|
|
@@ -16,16 +18,24 @@ class CollectionView(Static):
|
|
|
16
18
|
grid-size: 3;
|
|
17
19
|
grid-gutter: 1;
|
|
18
20
|
}
|
|
21
|
+
.note_card_actions{
|
|
22
|
+
height: auto;
|
|
23
|
+
margin: 0 0 1 0;
|
|
24
|
+
}
|
|
19
25
|
"""
|
|
20
26
|
|
|
21
27
|
@override
|
|
22
28
|
def __init__(self) -> None:
|
|
23
29
|
super().__init__()
|
|
24
30
|
self.ctrl = NoteController()
|
|
31
|
+
self.notes_by_date: dict[str, list] = {}
|
|
25
32
|
|
|
26
33
|
@override
|
|
27
34
|
def compose(self) -> ComposeResult:
|
|
28
35
|
notes = self.ctrl.get_notes()
|
|
36
|
+
self.notes_by_date = {
|
|
37
|
+
note_date: list(rows) for note_date, rows in groupby(notes, key=lambda row: row["note_date"])
|
|
38
|
+
}
|
|
29
39
|
with Vertical(classes="header"):
|
|
30
40
|
yield Label("VOID COLLECTION", classes="module_title")
|
|
31
41
|
with VerticalScroll(classes="main_container"):
|
|
@@ -33,7 +43,7 @@ class CollectionView(Static):
|
|
|
33
43
|
yield Label("No VOID NOTE saved yet.")
|
|
34
44
|
return
|
|
35
45
|
with Grid(classes="card_grid note_grid"):
|
|
36
|
-
for note_date, rows in
|
|
46
|
+
for note_date, rows in self.notes_by_date.items():
|
|
37
47
|
with Vertical(classes="note_card") as card:
|
|
38
48
|
card.border_title = self.format_date(note_date)
|
|
39
49
|
for row in rows:
|
|
@@ -43,6 +53,14 @@ class CollectionView(Static):
|
|
|
43
53
|
yield Label(row["notes"], classes="entry_note")
|
|
44
54
|
else:
|
|
45
55
|
yield Label("No notes written.", classes="entry_empty")
|
|
56
|
+
with Right(classes="note_card_actions"):
|
|
57
|
+
yield Button("EXPAND", name=note_date, classes="open_note", flat=True)
|
|
46
58
|
|
|
47
59
|
def format_date(self, date_str):
|
|
48
60
|
return date.fromisoformat(date_str).strftime("%B %d, %Y").upper()
|
|
61
|
+
|
|
62
|
+
@on(Button.Pressed, ".open_note")
|
|
63
|
+
def on_open_note(self, event: Button.Pressed) -> None:
|
|
64
|
+
note_date = event.button.name
|
|
65
|
+
if note_date in self.notes_by_date:
|
|
66
|
+
self.app.push_screen(NoteModal(self.format_date(note_date), self.notes_by_date[note_date]))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from itertools import groupby
|
|
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 Center, Vertical, VerticalScroll
|
|
8
|
+
from textual.screen import ModalScreen
|
|
9
|
+
from textual.widgets import Button, Label
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NoteModal(ModalScreen[None]):
|
|
13
|
+
|
|
14
|
+
BINDINGS = [Binding("escape", "close", "Close")]
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
NoteModal{
|
|
18
|
+
align: center middle;
|
|
19
|
+
}
|
|
20
|
+
.modal_card{
|
|
21
|
+
width: 90%;
|
|
22
|
+
height: 90%;
|
|
23
|
+
padding: 1 4;
|
|
24
|
+
border: round $primary;
|
|
25
|
+
border-title-color: $accent;
|
|
26
|
+
border-title-style: bold;
|
|
27
|
+
background: $background;
|
|
28
|
+
}
|
|
29
|
+
.modal_card .section_title{
|
|
30
|
+
padding: 1 0 1 0;
|
|
31
|
+
}
|
|
32
|
+
.modal_card .entry_card{
|
|
33
|
+
padding: 1 0 1 2;
|
|
34
|
+
}
|
|
35
|
+
.modal_body{
|
|
36
|
+
height: 1fr;
|
|
37
|
+
}
|
|
38
|
+
.modal_footer{
|
|
39
|
+
height: auto;
|
|
40
|
+
padding: 1 0 0 0;
|
|
41
|
+
}
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
def __init__(self, title: str, rows: list) -> None:
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.note_title = title
|
|
48
|
+
self.rows = sorted(rows, key=lambda row: (row["category"], row["activity"]))
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
def compose(self) -> ComposeResult:
|
|
52
|
+
with Vertical(classes="modal_card") as card:
|
|
53
|
+
card.border_title = self.note_title
|
|
54
|
+
with VerticalScroll(classes="modal_body"):
|
|
55
|
+
for category, group in groupby(self.rows, key=lambda row: row["category"]):
|
|
56
|
+
entries = list(group)
|
|
57
|
+
yield Label(f"── {category.upper()} · {len(entries)} ", classes="section_title")
|
|
58
|
+
for row in entries:
|
|
59
|
+
with Vertical(classes="entry_card"):
|
|
60
|
+
yield Label(row["activity"], classes="entry_title")
|
|
61
|
+
if row["notes"]:
|
|
62
|
+
yield Label(row["notes"], classes="entry_note")
|
|
63
|
+
else:
|
|
64
|
+
yield Label("No notes written.", classes="entry_empty")
|
|
65
|
+
with Center(classes="modal_footer"):
|
|
66
|
+
yield Button("CLOSE", id="close_note_modal", variant="primary", flat=True)
|
|
67
|
+
|
|
68
|
+
@on(Button.Pressed, "#close_note_modal")
|
|
69
|
+
def on_close_pressed(self) -> None:
|
|
70
|
+
self.action_close()
|
|
71
|
+
|
|
72
|
+
def action_close(self) -> None:
|
|
73
|
+
self.dismiss()
|
|
@@ -0,0 +1,109 @@
|
|
|
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, Label, Static
|
|
8
|
+
|
|
9
|
+
from void.controllers.activity import ActivityController
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SuspendedView(Static):
|
|
13
|
+
|
|
14
|
+
class Restored(Message):
|
|
15
|
+
"""Posted when an activity is restored, so the other views can re-render."""
|
|
16
|
+
|
|
17
|
+
DEFAULT_CSS = """
|
|
18
|
+
#suspended_module{
|
|
19
|
+
height: 1fr;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#selected_activity{
|
|
23
|
+
color: $text-muted;
|
|
24
|
+
margin: 1 0 0 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#suspended_cta_container{
|
|
28
|
+
height: auto;
|
|
29
|
+
margin: 1 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#suspended_cta_container Button{
|
|
33
|
+
margin: 0 1 0 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#suspended_table{
|
|
37
|
+
height: 1fr;
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.ctrl = ActivityController()
|
|
45
|
+
self.selected_id: int | None = None
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
def compose(self) -> ComposeResult:
|
|
49
|
+
with Vertical(classes="header"):
|
|
50
|
+
yield Label("VOID SUSPENDED", classes="module_title")
|
|
51
|
+
yield Label(self.counter_text(), id="counter")
|
|
52
|
+
with Vertical(classes="main_container", id="suspended_module"):
|
|
53
|
+
yield Label("SUSPENDED ACTIVITIES", classes="module_title")
|
|
54
|
+
yield Label(self.selected_text(), id="selected_activity")
|
|
55
|
+
with Horizontal(id="suspended_cta_container"):
|
|
56
|
+
yield Button(label="RESTORE", flat=True, id="restore_activity", variant="success", disabled=True)
|
|
57
|
+
yield Button(label="CLEAR", flat=True, id="clear_selection", variant="default")
|
|
58
|
+
yield DataTable(id="suspended_table", cursor_type="row")
|
|
59
|
+
|
|
60
|
+
def on_mount(self) -> None:
|
|
61
|
+
self.restore_btn = self.query_one("#restore_activity", Button)
|
|
62
|
+
self.suspended_table = self.query_one("#suspended_table", DataTable)
|
|
63
|
+
self.suspended_table.add_columns("ID", "ACTIVITY", "CATEGORY")
|
|
64
|
+
self.suspended_table.add_rows(self.ctrl.get_suspended_activities())
|
|
65
|
+
|
|
66
|
+
def counter_text(self) -> str:
|
|
67
|
+
total = len(self.ctrl.get_suspended_activities())
|
|
68
|
+
activities = "activity" if total == 1 else "activities"
|
|
69
|
+
return f"{total} suspended {activities}"
|
|
70
|
+
|
|
71
|
+
def selected_text(self, name: str | None = None) -> str:
|
|
72
|
+
if name is None:
|
|
73
|
+
return "Select an activity to restore it."
|
|
74
|
+
return f"Selected: {name}"
|
|
75
|
+
|
|
76
|
+
def update_suspended(self) -> None:
|
|
77
|
+
self.suspended_table.clear()
|
|
78
|
+
self.suspended_table.add_rows(self.ctrl.get_suspended_activities())
|
|
79
|
+
self.query_one("#counter", Label).update(self.counter_text())
|
|
80
|
+
self.clear_selection()
|
|
81
|
+
|
|
82
|
+
@on(DataTable.RowSelected, "#suspended_table")
|
|
83
|
+
def on_suspended_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
84
|
+
row = event.data_table.get_row(event.row_key)
|
|
85
|
+
self.selected_id = row[0]
|
|
86
|
+
self.query_one("#selected_activity", Label).update(self.selected_text(f"{row[1]} ({row[2]})"))
|
|
87
|
+
self.restore_btn.disabled = False
|
|
88
|
+
|
|
89
|
+
@on(Button.Pressed, "#clear_selection")
|
|
90
|
+
def on_clear_selection(self) -> None:
|
|
91
|
+
self.clear_selection()
|
|
92
|
+
|
|
93
|
+
@on(Button.Pressed, "#restore_activity")
|
|
94
|
+
def on_restore_activity(self) -> None:
|
|
95
|
+
if self.selected_id is None:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if not self.ctrl.restore_activity(self.selected_id):
|
|
99
|
+
self.notify("An active activity with that name already exists in its category", severity="error")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
self.notify("Activity restored", severity="information")
|
|
103
|
+
self.update_suspended()
|
|
104
|
+
self.post_message(self.Restored())
|
|
105
|
+
|
|
106
|
+
def clear_selection(self) -> None:
|
|
107
|
+
self.selected_id = None
|
|
108
|
+
self.query_one("#selected_activity", Label).update(self.selected_text())
|
|
109
|
+
self.restore_btn.disabled = True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|