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.
Files changed (27) hide show
  1. {voidcli-1.0.0 → voidcli-1.2.0}/PKG-INFO +2 -2
  2. {voidcli-1.0.0 → voidcli-1.2.0}/README.md +1 -1
  3. {voidcli-1.0.0 → voidcli-1.2.0}/pyproject.toml +1 -1
  4. {voidcli-1.0.0 → voidcli-1.2.0}/void/controllers/activity.py +17 -0
  5. {voidcli-1.0.0 → voidcli-1.2.0}/void/main.py +11 -0
  6. {voidcli-1.0.0 → voidcli-1.2.0}/void/models/activity.py +25 -0
  7. {voidcli-1.0.0 → voidcli-1.2.0}/void/views/collection.py +21 -3
  8. voidcli-1.2.0/void/views/note_modal.py +73 -0
  9. voidcli-1.2.0/void/views/suspended.py +109 -0
  10. {voidcli-1.0.0 → voidcli-1.2.0}/.gitignore +0 -0
  11. {voidcli-1.0.0 → voidcli-1.2.0}/LICENSE +0 -0
  12. {voidcli-1.0.0 → voidcli-1.2.0}/void/__init__.py +0 -0
  13. {voidcli-1.0.0 → voidcli-1.2.0}/void/controllers/__init__.py +0 -0
  14. {voidcli-1.0.0 → voidcli-1.2.0}/void/controllers/category.py +0 -0
  15. {voidcli-1.0.0 → voidcli-1.2.0}/void/controllers/note.py +0 -0
  16. {voidcli-1.0.0 → voidcli-1.2.0}/void/database/__init__.py +0 -0
  17. {voidcli-1.0.0 → voidcli-1.2.0}/void/database/connection.py +0 -0
  18. {voidcli-1.0.0 → voidcli-1.2.0}/void/database/init_tables.sql +0 -0
  19. {voidcli-1.0.0 → voidcli-1.2.0}/void/models/__init__.py +0 -0
  20. {voidcli-1.0.0 → voidcli-1.2.0}/void/models/category.py +0 -0
  21. {voidcli-1.0.0 → voidcli-1.2.0}/void/models/note.py +0 -0
  22. {voidcli-1.0.0 → voidcli-1.2.0}/void/views/__init__.py +0 -0
  23. {voidcli-1.0.0 → voidcli-1.2.0}/void/views/activity.py +0 -0
  24. {voidcli-1.0.0 → voidcli-1.2.0}/void/views/day_note.py +0 -0
  25. {voidcli-1.0.0 → voidcli-1.2.0}/void/views/note.py +0 -0
  26. {voidcli-1.0.0 → voidcli-1.2.0}/void/views/note_form.py +0 -0
  27. {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.0.0
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
- ![VOID: wellcome screen, void activities, void collection, void day](https://github.com/NelsonUrrutia/void/blob/main/assets/void-banner.png)
47
+ ![VOID: welcome screen, void activities, void collection, void day](https://raw.githubusercontent.com/NelsonUrrutia/void/main/assets/void-banner.png)
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
- ![VOID: wellcome screen, void activities, void collection, void day](https://github.com/NelsonUrrutia/void/blob/main/assets/void-banner.png)
15
+ ![VOID: welcome screen, void activities, void collection, void day](https://raw.githubusercontent.com/NelsonUrrutia/void/main/assets/void-banner.png)
16
16
 
17
17
  > **VOID** is an offline diary to be used in the terminal.
18
18
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "voidcli"
7
- version = "1.0.0"
7
+ version = "1.2.0"
8
8
  description = ""
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -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 groupby(notes, key=lambda row: row["note_date"]):
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