voidcli 0.1.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-0.1.0/.gitignore +4 -0
- voidcli-0.1.0/LICENSE +21 -0
- voidcli-0.1.0/PKG-INFO +77 -0
- voidcli-0.1.0/README.md +45 -0
- voidcli-0.1.0/pyproject.toml +28 -0
- voidcli-0.1.0/void/__init__.py +1 -0
- voidcli-0.1.0/void/controllers/__init__.py +1 -0
- voidcli-0.1.0/void/controllers/activity.py +44 -0
- voidcli-0.1.0/void/controllers/category.py +30 -0
- voidcli-0.1.0/void/controllers/note.py +47 -0
- voidcli-0.1.0/void/database/__init__.py +16 -0
- voidcli-0.1.0/void/database/connection.py +59 -0
- voidcli-0.1.0/void/database/init_tables.sql +47 -0
- voidcli-0.1.0/void/main.py +153 -0
- voidcli-0.1.0/void/models/__init__.py +1 -0
- voidcli-0.1.0/void/models/activity.py +68 -0
- voidcli-0.1.0/void/models/category.py +40 -0
- voidcli-0.1.0/void/models/note.py +64 -0
- voidcli-0.1.0/void/views/__init__.py +1 -0
- voidcli-0.1.0/void/views/activity.py +247 -0
- voidcli-0.1.0/void/views/collection.py +52 -0
- voidcli-0.1.0/void/views/day_note.py +101 -0
- voidcli-0.1.0/void/views/note.py +47 -0
- voidcli-0.1.0/void/views/note_form.py +144 -0
- voidcli-0.1.0/void/views/welcome.py +72 -0
voidcli-0.1.0/.gitignore
ADDED
voidcli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Nelson Urrutia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
voidcli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: voidcli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Project-URL: Repository, https://github.com/NelsonUrrutia/void
|
|
5
|
+
Author-email: Nelson Urrutia <nelurrutia15@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) Nelson Urrutia
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: textual
|
|
30
|
+
Requires-Dist: textual-dev
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
█████ █████ ███████ █████ ██████████
|
|
35
|
+
░░███ ░░███ ███░░░░░███ ░░███ ░░███░░░░███
|
|
36
|
+
░███ ░███ ███ ░░███ ░███ ░███ ░░███
|
|
37
|
+
░███ ░███ ░███ ░███ ░███ ░███ ░███
|
|
38
|
+
░░███ ███ ░███ ░███ ░███ ░███ ░███
|
|
39
|
+
░░░█████░ ░░███ ███ ░███ ░███ ███
|
|
40
|
+
░░███ ░░░███████░ █████ ██████████
|
|
41
|
+
░░░ ░░░░░░░ ░░░░░ ░░░░░░░░░░
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
# **V**ital **O**ffline **I**nformation **D**iary
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
> **VOID** is an offline diary to be used in the terminal.
|
|
49
|
+
|
|
50
|
+
I made VOID with these goals in mind:
|
|
51
|
+
|
|
52
|
+
1. Make something useful for me, and feel accomplished delivering a usable piece of
|
|
53
|
+
software - at least for myself.
|
|
54
|
+
2. Learn Python and SQLite. I wanted to do something different from my regular job and
|
|
55
|
+
grow my skills - starting with tooling, and now this more UI-heavy piece backed by
|
|
56
|
+
a database.
|
|
57
|
+
3. Practice and get involved in the full life cycle of a project: identify a need or
|
|
58
|
+
something to improve, then design and develop a solution.
|
|
59
|
+
|
|
60
|
+
It took me a year to complete this project - this is the third and final repo - I pitched the idea on August 10th, 2025.
|
|
61
|
+
Life, doubts, and failures get in the way of personal projects. However, looking back
|
|
62
|
+
at my notes, I learned a lot, and a lot of myself changed along the way.
|
|
63
|
+
|
|
64
|
+
The project is in its final form, and I can proudly say it's finished.
|
|
65
|
+
I'll keep improving it, but the core things I wanted to do are delivered.
|
|
66
|
+
|
|
67
|
+
I made it public so there's proof on the Internet of my work.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### The pitch
|
|
72
|
+
|
|
73
|
+
> Track your life, thoughts, and growth without distractions. VOID is a focused personal logging tool built for the terminal. No cloud sync, no notifications, no feature bloat. Just you, your thoughts, and a clean interface to document what truly matters in your life.
|
|
74
|
+
|
|
75
|
+
### How to install
|
|
76
|
+
|
|
77
|
+
The project is available on PyPI, so you can install it with `pipx install voidcli`.
|
voidcli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
```
|
|
2
|
+
█████ █████ ███████ █████ ██████████
|
|
3
|
+
░░███ ░░███ ███░░░░░███ ░░███ ░░███░░░░███
|
|
4
|
+
░███ ░███ ███ ░░███ ░███ ░███ ░░███
|
|
5
|
+
░███ ░███ ░███ ░███ ░███ ░███ ░███
|
|
6
|
+
░░███ ███ ░███ ░███ ░███ ░███ ░███
|
|
7
|
+
░░░█████░ ░░███ ███ ░███ ░███ ███
|
|
8
|
+
░░███ ░░░███████░ █████ ██████████
|
|
9
|
+
░░░ ░░░░░░░ ░░░░░ ░░░░░░░░░░
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
# **V**ital **O**ffline **I**nformation **D**iary
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
> **VOID** is an offline diary to be used in the terminal.
|
|
17
|
+
|
|
18
|
+
I made VOID with these goals in mind:
|
|
19
|
+
|
|
20
|
+
1. Make something useful for me, and feel accomplished delivering a usable piece of
|
|
21
|
+
software - at least for myself.
|
|
22
|
+
2. Learn Python and SQLite. I wanted to do something different from my regular job and
|
|
23
|
+
grow my skills - starting with tooling, and now this more UI-heavy piece backed by
|
|
24
|
+
a database.
|
|
25
|
+
3. Practice and get involved in the full life cycle of a project: identify a need or
|
|
26
|
+
something to improve, then design and develop a solution.
|
|
27
|
+
|
|
28
|
+
It took me a year to complete this project - this is the third and final repo - I pitched the idea on August 10th, 2025.
|
|
29
|
+
Life, doubts, and failures get in the way of personal projects. However, looking back
|
|
30
|
+
at my notes, I learned a lot, and a lot of myself changed along the way.
|
|
31
|
+
|
|
32
|
+
The project is in its final form, and I can proudly say it's finished.
|
|
33
|
+
I'll keep improving it, but the core things I wanted to do are delivered.
|
|
34
|
+
|
|
35
|
+
I made it public so there's proof on the Internet of my work.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### The pitch
|
|
40
|
+
|
|
41
|
+
> Track your life, thoughts, and growth without distractions. VOID is a focused personal logging tool built for the terminal. No cloud sync, no notifications, no feature bloat. Just you, your thoughts, and a clean interface to document what truly matters in your life.
|
|
42
|
+
|
|
43
|
+
### How to install
|
|
44
|
+
|
|
45
|
+
The project is available on PyPI, so you can install it with `pipx install voidcli`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "voidcli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = ""
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = ["textual", "textual-dev"]
|
|
13
|
+
authors = [{ name = "Nelson Urrutia", email = "nelurrutia15@gmail.com" }]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Repository = "https://github.com/NelsonUrrutia/void"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
void = "void.main:run"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.sdist]
|
|
22
|
+
include = ["/void", "/LICENSE", "/README.md", "/pyproject.toml"]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["void"]
|
|
26
|
+
|
|
27
|
+
[tool.basedpyright]
|
|
28
|
+
typeCheckingMode = "basic"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""" VOID APP SRC CODE """
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Controllers module"""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
|
|
3
|
+
from void.models.activity import Activity
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ActivityController:
|
|
7
|
+
|
|
8
|
+
def get_activities(self) -> list[tuple]:
|
|
9
|
+
with Activity() as ac:
|
|
10
|
+
raw_activities = ac.get_active_activities()
|
|
11
|
+
activities = []
|
|
12
|
+
for item in raw_activities:
|
|
13
|
+
activities.append((item['id'], item['activity'], item['category']))
|
|
14
|
+
return activities
|
|
15
|
+
|
|
16
|
+
def get_activities_by_category(self, category_id):
|
|
17
|
+
with Activity() as ac:
|
|
18
|
+
raw_activities = ac.get_activities_by_category(category_id)
|
|
19
|
+
activities = []
|
|
20
|
+
for item in raw_activities:
|
|
21
|
+
activities.append((item["id"], item['name']))
|
|
22
|
+
return activities
|
|
23
|
+
|
|
24
|
+
def create_activity(self, activity, category_id) -> bool:
|
|
25
|
+
"""Return False when the name is already used in that category."""
|
|
26
|
+
with Activity() as ac:
|
|
27
|
+
try:
|
|
28
|
+
ac.create_activity(activity, category_id)
|
|
29
|
+
except sqlite3.IntegrityError:
|
|
30
|
+
return False
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
def update_activity(self, activity_id, activity, category_id) -> bool:
|
|
34
|
+
"""Return False when the name is already used in that category."""
|
|
35
|
+
with Activity() as ac:
|
|
36
|
+
try:
|
|
37
|
+
ac.update_activity(activity_id, activity, category_id)
|
|
38
|
+
except sqlite3.IntegrityError:
|
|
39
|
+
return False
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
def suspend_activity(self, activity_id):
|
|
43
|
+
with Activity() as ac:
|
|
44
|
+
ac.suspend_activity(activity_id)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
|
|
3
|
+
from void.models.category import Category
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CategoryController:
|
|
7
|
+
def get_categories_for_activities(self) -> list[tuple]:
|
|
8
|
+
with Category() as ca:
|
|
9
|
+
raw_categories = ca.get_active_categories_for_activities()
|
|
10
|
+
categories = []
|
|
11
|
+
for item in raw_categories:
|
|
12
|
+
categories.append((item['category'], item['id']))
|
|
13
|
+
return categories
|
|
14
|
+
|
|
15
|
+
def get_categories(self) -> list[tuple]:
|
|
16
|
+
with Category() as ca:
|
|
17
|
+
raw_categories = ca.get_active_categories()
|
|
18
|
+
categories = []
|
|
19
|
+
for item in raw_categories:
|
|
20
|
+
categories.append((item['category'],))
|
|
21
|
+
return categories
|
|
22
|
+
|
|
23
|
+
def create_category(self, category) -> bool:
|
|
24
|
+
"""Return False when a category with that name already exists."""
|
|
25
|
+
with Category() as ca:
|
|
26
|
+
try:
|
|
27
|
+
ca.create_category(category)
|
|
28
|
+
except sqlite3.IntegrityError:
|
|
29
|
+
return False
|
|
30
|
+
return True
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from void.controllers.activity import ActivityController
|
|
2
|
+
from void.controllers.category import CategoryController
|
|
3
|
+
from void.models.note import Note
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NoteController:
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.category_ctrl = CategoryController()
|
|
10
|
+
self.activity_ctrl = ActivityController()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_activities_by_category(self):
|
|
14
|
+
active_categories = self.category_ctrl.get_categories_for_activities()
|
|
15
|
+
categories_and_activities = []
|
|
16
|
+
for cat in active_categories:
|
|
17
|
+
category, id = cat
|
|
18
|
+
activities = self.activity_ctrl.get_activities_by_category(category_id=id)
|
|
19
|
+
data = (id, category, activities)
|
|
20
|
+
categories_and_activities.append(data)
|
|
21
|
+
return categories_and_activities
|
|
22
|
+
|
|
23
|
+
def get_activities(self):
|
|
24
|
+
"""Every active activity as (id, name, category)."""
|
|
25
|
+
return self.activity_ctrl.get_activities()
|
|
26
|
+
|
|
27
|
+
def get_activity_total(self):
|
|
28
|
+
return len(self.get_activities())
|
|
29
|
+
|
|
30
|
+
def save_note(self, date_str, activities):
|
|
31
|
+
with Note() as note:
|
|
32
|
+
note_id = note.save_note(date_str)
|
|
33
|
+
for activity in activities:
|
|
34
|
+
activity_info, activity_note = activity
|
|
35
|
+
self.save_note_details(note_id, activity_info, activity_note)
|
|
36
|
+
|
|
37
|
+
def save_note_details(self, note_id, activity_info, activity_note):
|
|
38
|
+
with Note() as note:
|
|
39
|
+
note.save_note_detail(note_id, activity_info, activity_note)
|
|
40
|
+
|
|
41
|
+
def get_day_note(self, date_str):
|
|
42
|
+
with Note() as note:
|
|
43
|
+
return note.get_day_note(date_str)
|
|
44
|
+
|
|
45
|
+
def get_notes(self):
|
|
46
|
+
with Note() as note:
|
|
47
|
+
return note.get_notes()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Database module """
|
|
2
|
+
|
|
3
|
+
from void.database.connection import DB_PATH, Connection
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def initialize_database():
|
|
7
|
+
"""Create void.db and its schema the first time the app runs.
|
|
8
|
+
|
|
9
|
+
Does nothing once void.db already exists, so it's safe to call on
|
|
10
|
+
every startup.
|
|
11
|
+
"""
|
|
12
|
+
if DB_PATH.exists():
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
with Connection() as db:
|
|
16
|
+
db.create_tables()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
DB_PATH = Path(__file__).parent / "void.db"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Connection:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
"""Connect to void.db and configure results as name-accessible rows."""
|
|
10
|
+
self.db_path = DB_PATH
|
|
11
|
+
self.tables_script = Path(__file__).parent / "init_tables.sql"
|
|
12
|
+
self.test_data_script = Path(__file__).parent / "test_data.sql"
|
|
13
|
+
|
|
14
|
+
self.connection = sqlite3.connect(self.db_path)
|
|
15
|
+
self.connection.row_factory = sqlite3.Row
|
|
16
|
+
self.cursor = self.connection.cursor()
|
|
17
|
+
|
|
18
|
+
def __enter__(self):
|
|
19
|
+
"""Allow `with Connection() as db:`; returns this instance."""
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
23
|
+
"""Close the connection when the `with` block ends, even on error."""
|
|
24
|
+
self.close()
|
|
25
|
+
|
|
26
|
+
def create_tables(self):
|
|
27
|
+
"""Run init_tables.sql to create the schema. Safe to call more than
|
|
28
|
+
once, every statement uses IF NOT EXISTS."""
|
|
29
|
+
with open(self.tables_script) as f:
|
|
30
|
+
sql = f.read()
|
|
31
|
+
self.cursor.executescript(sql)
|
|
32
|
+
self.connection.commit()
|
|
33
|
+
|
|
34
|
+
def load_test_data(self):
|
|
35
|
+
"""Run test_data.sql to populate sample rows."""
|
|
36
|
+
with open(self.test_data_script) as f:
|
|
37
|
+
sql = f.read()
|
|
38
|
+
self.cursor.executescript(sql)
|
|
39
|
+
self.connection.commit()
|
|
40
|
+
|
|
41
|
+
def querying(self, sql, params=()):
|
|
42
|
+
"""Run a parameterized SELECT and return the matching rows.
|
|
43
|
+
|
|
44
|
+
params fills any `?` placeholders in sql, keeping values out of the
|
|
45
|
+
SQL string itself (avoids SQL injection).
|
|
46
|
+
"""
|
|
47
|
+
self.cursor.execute(sql, params)
|
|
48
|
+
return self.cursor.fetchall()
|
|
49
|
+
|
|
50
|
+
def execute(self, sql, params=()):
|
|
51
|
+
"""Run a parameterized INSERT/UPDATE/DELETE, commit, and return the
|
|
52
|
+
id of the inserted row (lastrowid; meaningless for UPDATE/DELETE)."""
|
|
53
|
+
self.cursor.execute(sql, params)
|
|
54
|
+
self.connection.commit()
|
|
55
|
+
return self.cursor.lastrowid
|
|
56
|
+
|
|
57
|
+
def close(self):
|
|
58
|
+
"""Close the database connection."""
|
|
59
|
+
self.connection.close()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
PRAGMA foreign_keys = ON;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS categories(
|
|
4
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5
|
+
name TEXT NOT NULL,
|
|
6
|
+
is_active INTEGER DEFAULT 1,
|
|
7
|
+
date_created TEXT DEFAULT (datetime('now'))
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ux_categories_name
|
|
11
|
+
ON categories(name) WHERE is_active = 1;
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS void_note(
|
|
14
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
15
|
+
note_date TEXT NOT NULL,
|
|
16
|
+
is_active INTEGER DEFAULT 1,
|
|
17
|
+
date_created TEXT DEFAULT (datetime('now'))
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ux_void_note_note_date
|
|
21
|
+
ON void_note(note_date) WHERE is_active = 1;
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS activities(
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
name TEXT NOT NULL,
|
|
26
|
+
is_active INTEGER DEFAULT 1,
|
|
27
|
+
date_created TEXT DEFAULT (datetime('now')),
|
|
28
|
+
category_id INTEGER NOT NULL,
|
|
29
|
+
FOREIGN KEY (category_id)
|
|
30
|
+
REFERENCES categories (id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ux_activities_name_category
|
|
34
|
+
ON activities(name, category_id) WHERE is_active = 1;
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS void_note_details (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
notes TEXT,
|
|
39
|
+
is_active INTEGER DEFAULT 1,
|
|
40
|
+
date_created TEXT DEFAULT (datetime('now')),
|
|
41
|
+
void_note_id INTEGER NOT NULL,
|
|
42
|
+
activity_id INTEGER NOT NULL,
|
|
43
|
+
FOREIGN KEY (void_note_id)
|
|
44
|
+
REFERENCES void_note (id),
|
|
45
|
+
FOREIGN KEY (activity_id)
|
|
46
|
+
REFERENCES activities(id)
|
|
47
|
+
);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from textual import on
|
|
2
|
+
from textual.app import App, ComposeResult
|
|
3
|
+
from textual.widgets import Footer, Header, TabbedContent, TabPane
|
|
4
|
+
|
|
5
|
+
from void.database import initialize_database
|
|
6
|
+
from void.views.activity import ActivitiesView
|
|
7
|
+
from void.views.collection import CollectionView
|
|
8
|
+
from void.views.day_note import DayNote
|
|
9
|
+
from void.views.note import NoteView
|
|
10
|
+
from void.views.note_form import NoteForm
|
|
11
|
+
from void.views.welcome import WelcomeScreen
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VoidApp(App):
|
|
15
|
+
|
|
16
|
+
CSS = """
|
|
17
|
+
/* LAYOUT */
|
|
18
|
+
.header{
|
|
19
|
+
dock: top;
|
|
20
|
+
height: auto;
|
|
21
|
+
padding: 1 2 0 2;
|
|
22
|
+
border-bottom: solid $primary;
|
|
23
|
+
}
|
|
24
|
+
.main_container{
|
|
25
|
+
padding: 1 2;
|
|
26
|
+
}
|
|
27
|
+
.card_grid{
|
|
28
|
+
grid-columns: 1fr;
|
|
29
|
+
height: auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* TITLES */
|
|
33
|
+
.module_title{
|
|
34
|
+
text-style: bold;
|
|
35
|
+
}
|
|
36
|
+
.section_title{
|
|
37
|
+
text-style: bold;
|
|
38
|
+
color: $text-muted;
|
|
39
|
+
}
|
|
40
|
+
#counter{
|
|
41
|
+
color: $text-muted;
|
|
42
|
+
}
|
|
43
|
+
.empty_state{
|
|
44
|
+
color: $text-muted;
|
|
45
|
+
padding: 1 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* CARDS */
|
|
49
|
+
.note_card{
|
|
50
|
+
height: auto;
|
|
51
|
+
padding: 0 2;
|
|
52
|
+
border: round $primary;
|
|
53
|
+
}
|
|
54
|
+
.note_card Label{
|
|
55
|
+
width: 100%;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* LOGGED ACTIVITY ENTRY */
|
|
59
|
+
.entry_card{
|
|
60
|
+
height: auto;
|
|
61
|
+
margin: 0 0 1 0;
|
|
62
|
+
padding: 0 0 0 2;
|
|
63
|
+
border-left: thick $primary;
|
|
64
|
+
}
|
|
65
|
+
.entry_card Label{
|
|
66
|
+
width: 100%;
|
|
67
|
+
}
|
|
68
|
+
.entry_title{
|
|
69
|
+
text-style: bold;
|
|
70
|
+
}
|
|
71
|
+
.entry_note{
|
|
72
|
+
color: $text-muted;
|
|
73
|
+
}
|
|
74
|
+
.entry_empty{
|
|
75
|
+
color: $text-muted;
|
|
76
|
+
text-style: italic;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* CENTERED STATE SCREENS */
|
|
80
|
+
.state_screen{
|
|
81
|
+
height: 1fr;
|
|
82
|
+
align: center middle;
|
|
83
|
+
}
|
|
84
|
+
.state_card{
|
|
85
|
+
width: 60;
|
|
86
|
+
max-width: 100%;
|
|
87
|
+
height: auto;
|
|
88
|
+
padding: 1 2;
|
|
89
|
+
border: round $panel;
|
|
90
|
+
}
|
|
91
|
+
.state_card Label{
|
|
92
|
+
width: 100%;
|
|
93
|
+
text-align: center;
|
|
94
|
+
}
|
|
95
|
+
.state_card_success{
|
|
96
|
+
border: round $success;
|
|
97
|
+
}
|
|
98
|
+
.state_title{
|
|
99
|
+
text-style: bold;
|
|
100
|
+
}
|
|
101
|
+
.state_success{
|
|
102
|
+
color: $success;
|
|
103
|
+
}
|
|
104
|
+
.state_muted{
|
|
105
|
+
color: $text-muted;
|
|
106
|
+
}
|
|
107
|
+
.state_hint{
|
|
108
|
+
color: $text-muted;
|
|
109
|
+
padding: 0 0 1 0;
|
|
110
|
+
}
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def compose(self) -> ComposeResult:
|
|
114
|
+
yield Header(icon="🧠")
|
|
115
|
+
|
|
116
|
+
with TabbedContent(initial="void_note"):
|
|
117
|
+
|
|
118
|
+
with TabPane("VOID NOTE", id="void_note"):
|
|
119
|
+
yield NoteView()
|
|
120
|
+
|
|
121
|
+
with TabPane("VOID DAY", id="void_day_note"):
|
|
122
|
+
yield DayNote()
|
|
123
|
+
|
|
124
|
+
with TabPane("VOID COLLECTION", id="void_collection"):
|
|
125
|
+
yield CollectionView()
|
|
126
|
+
|
|
127
|
+
with TabPane("VOID ACTIVITIES", id="void_activities"):
|
|
128
|
+
yield ActivitiesView()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
yield Footer()
|
|
132
|
+
|
|
133
|
+
@on(NoteForm.Saved)
|
|
134
|
+
async def on_note_saved(self) -> None:
|
|
135
|
+
await self.query_one(DayNote).recompose()
|
|
136
|
+
await self.query_one(CollectionView).recompose()
|
|
137
|
+
self.query_one(TabbedContent).active = "void_day_note"
|
|
138
|
+
|
|
139
|
+
@on(ActivitiesView.Changed)
|
|
140
|
+
async def on_activities_changed(self) -> None:
|
|
141
|
+
await self.query_one(NoteView).recompose()
|
|
142
|
+
|
|
143
|
+
def on_mount(self) -> None:
|
|
144
|
+
self.title = "VOID"
|
|
145
|
+
self.sub_title = "Vital Offline Information Diary"
|
|
146
|
+
self.push_screen(WelcomeScreen())
|
|
147
|
+
|
|
148
|
+
def run() -> None:
|
|
149
|
+
initialize_database()
|
|
150
|
+
VoidApp().run()
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Models module"""
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from void.database.connection import Connection
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Activity:
|
|
5
|
+
|
|
6
|
+
def __enter__(self):
|
|
7
|
+
"""Allow `with Activity() as activity:`; returns this instance."""
|
|
8
|
+
return self
|
|
9
|
+
|
|
10
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
11
|
+
"""Close the model when the `with` block ends, even on error."""
|
|
12
|
+
|
|
13
|
+
def get_active_activities(self):
|
|
14
|
+
with Connection() as db:
|
|
15
|
+
sql = """
|
|
16
|
+
SELECT
|
|
17
|
+
activities.id as "id",
|
|
18
|
+
activities.name as "activity",
|
|
19
|
+
categories.name as "category"
|
|
20
|
+
FROM activities
|
|
21
|
+
INNER JOIN
|
|
22
|
+
categories on categories.id = activities.category_id
|
|
23
|
+
WHERE activities.is_active == 1
|
|
24
|
+
AND
|
|
25
|
+
categories.is_active == 1;"""
|
|
26
|
+
result = db.querying(sql)
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
def get_activities_by_category(self, category_id):
|
|
30
|
+
with Connection() as db:
|
|
31
|
+
sql = """
|
|
32
|
+
SELECT
|
|
33
|
+
activities.id as "id",
|
|
34
|
+
activities.name as "name"
|
|
35
|
+
FROM activities
|
|
36
|
+
WHERE
|
|
37
|
+
activities.category_id = ?
|
|
38
|
+
AND
|
|
39
|
+
activities.is_active = 1
|
|
40
|
+
"""
|
|
41
|
+
result = db.querying(sql, (category_id,))
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
def create_activity(self, activity, category_id):
|
|
45
|
+
with Connection() as db:
|
|
46
|
+
data = (activity, category_id)
|
|
47
|
+
sql = "INSERT INTO activities (name, category_id) VALUES(?, ?)"
|
|
48
|
+
db.execute(sql, data)
|
|
49
|
+
|
|
50
|
+
def update_activity(self, activity_id, activity, category_id):
|
|
51
|
+
with Connection() as db:
|
|
52
|
+
data = (activity, category_id, activity_id)
|
|
53
|
+
sql = """
|
|
54
|
+
UPDATE activities
|
|
55
|
+
SET name = ?, category_id = ?
|
|
56
|
+
WHERE id = ?
|
|
57
|
+
"""
|
|
58
|
+
db.execute(sql, data)
|
|
59
|
+
|
|
60
|
+
def suspend_activity(self, activity_id):
|
|
61
|
+
with Connection() as db:
|
|
62
|
+
data = (activity_id,)
|
|
63
|
+
sql = """
|
|
64
|
+
UPDATE activities
|
|
65
|
+
SET is_active = 0
|
|
66
|
+
WHERE id = ?
|
|
67
|
+
"""
|
|
68
|
+
db.execute(sql, data)
|