flavortui 1.0.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 (31) hide show
  1. flavortui-1.0.0/.gitignore +12 -0
  2. flavortui-1.0.0/.pylintrc +10 -0
  3. flavortui-1.0.0/LICENSE.txt +18 -0
  4. flavortui-1.0.0/PKG-INFO +77 -0
  5. flavortui-1.0.0/README.md +51 -0
  6. flavortui-1.0.0/TODO.md +21 -0
  7. flavortui-1.0.0/pyproject.toml +69 -0
  8. flavortui-1.0.0/requirements.txt +4 -0
  9. flavortui-1.0.0/screenshots/kitchen.png +0 -0
  10. flavortui-1.0.0/screenshots/with_sidebar.png +0 -0
  11. flavortui-1.0.0/src/flavortui/__about__.py +4 -0
  12. flavortui-1.0.0/src/flavortui/__init__.py +3 -0
  13. flavortui-1.0.0/src/flavortui/__main__.py +4 -0
  14. flavortui-1.0.0/src/flavortui/api/__init__.py +0 -0
  15. flavortui-1.0.0/src/flavortui/api/api.py +75 -0
  16. flavortui-1.0.0/src/flavortui/api/api_key.py +16 -0
  17. flavortui-1.0.0/src/flavortui/api/client.py +168 -0
  18. flavortui-1.0.0/src/flavortui/api/functions.py +14 -0
  19. flavortui-1.0.0/src/flavortui/api/settings.py +24 -0
  20. flavortui-1.0.0/src/flavortui/components/__init__.py +0 -0
  21. flavortui-1.0.0/src/flavortui/components/api_key_input.py +84 -0
  22. flavortui-1.0.0/src/flavortui/components/image_wrapper.py +85 -0
  23. flavortui-1.0.0/src/flavortui/components/popup_modal.py +52 -0
  24. flavortui-1.0.0/src/flavortui/components/sidebar.py +64 -0
  25. flavortui-1.0.0/src/flavortui/main.py +122 -0
  26. flavortui-1.0.0/src/flavortui/views/__init__.py +0 -0
  27. flavortui-1.0.0/src/flavortui/views/explore.py +488 -0
  28. flavortui-1.0.0/src/flavortui/views/kitchen.py +135 -0
  29. flavortui-1.0.0/src/flavortui/views/projects.py +365 -0
  30. flavortui-1.0.0/src/flavortui/views/settings.py +128 -0
  31. flavortui-1.0.0/src/flavortui/views/shop.py +480 -0
@@ -0,0 +1,12 @@
1
+ # python stuff
2
+ venv/
3
+ __pycache__/
4
+
5
+ # api cache
6
+ .cache/
7
+
8
+ # might use this at some point (its for debugging 😱)
9
+ debug.log
10
+
11
+ # I hate macos
12
+ .DS_Store
@@ -0,0 +1,10 @@
1
+ [MASTER]
2
+ init-hook='import os, sys; sys.path.insert(0, os.getcwd())'
3
+ ignore=venv,.venv,__pycache__
4
+
5
+ [MESSAGES CONTROL]
6
+ disable=
7
+ duplicate-code,
8
+ missing-module-docstring,
9
+ missing-class-docstring,
10
+ missing-function-docstring
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present BookCatKid <99609593+BookCatKid@users.noreply.github.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: flavortui
3
+ Version: 1.0.0
4
+ Summary: Feature-rich terminal UI for Flavortown: devlogs, projects, shop, and more.
5
+ Project-URL: Documentation, https://github.com/BookCatKid/flavortui#readme
6
+ Project-URL: Issues, https://github.com/BookCatKid/flavortui/issues
7
+ Project-URL: Source, https://github.com/BookCatKid/flavortui
8
+ Author-email: BookCatKid <99609593+BookCatKid@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE.txt
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: keyring
21
+ Requires-Dist: platformdirs
22
+ Requires-Dist: requests
23
+ Requires-Dist: textual
24
+ Requires-Dist: textual-image[textual]
25
+ Description-Content-Type: text/markdown
26
+
27
+ # FlavorTUI
28
+
29
+ [![PyPI - Version](https://img.shields.io/pypi/v/flavortui.svg)](https://pypi.org/project/flavortui)
30
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flavortui.svg)](https://pypi.org/project/flavortui)
31
+
32
+ FlavorTUI is a feature-rich terminal user interface (TUI) for Flavortown. With FlavorTUI, you can browse and create devlogs, manage your projects, explore the shop, and access other Flavortown features, all from an interactive terminal interface.
33
+
34
+ It is built using the `textual` library, which provides (imo) a great terminal UI experience. The TUI is of course written in Python 🥰. This is my first time creating a TUI so I hope its good :) Depending on your terminal, the ui might look different. It all depends on how well your terminal supports different things.
35
+
36
+ Your API key is stored "securely" using the `keyring` library, so you don't have to worry about it being exposed in your terminal history or config files.
37
+
38
+ Additionaly, your settings are store using the `platformdirs` `user_config_dir` function. Your settings are probobly stored in `~/Library/Application Support/flavortui` on macOS, `C:\Users\<user>\AppData\Local\flavortui` on Windows (`%localappdata%`), and ~/.config/flavortui (or $XDG_CONFIG_HOME) on Linux.
39
+
40
+ <div>
41
+ <img src="screenshots/kitchen.png" alt="Kitchen Screenshot" style="width:49%;">
42
+ <img src="screenshots/with_sidebar.png" alt="Shop Screenshot" style="width:49%;">
43
+ </div>
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install flavortui
49
+ ```
50
+
51
+ ## Local Development
52
+
53
+ ```bash
54
+ python -m venv venv
55
+ source venv/bin/activate # On Windows: venv\Scripts\activate
56
+ pip install -e .
57
+ ```
58
+
59
+ Run with either:
60
+
61
+ ```bash
62
+ flavortui
63
+ ```
64
+
65
+ or:
66
+
67
+ ```bash
68
+ python -m flavortui
69
+ ```
70
+
71
+ ## API
72
+
73
+ Flavortown API docs can be found [here](https://flavortown.hackclub.com/api/v1/docs).
74
+
75
+ ## License
76
+
77
+ `flavortui` is distributed under the terms of the MIT license.
@@ -0,0 +1,51 @@
1
+ # FlavorTUI
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/flavortui.svg)](https://pypi.org/project/flavortui)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flavortui.svg)](https://pypi.org/project/flavortui)
5
+
6
+ FlavorTUI is a feature-rich terminal user interface (TUI) for Flavortown. With FlavorTUI, you can browse and create devlogs, manage your projects, explore the shop, and access other Flavortown features, all from an interactive terminal interface.
7
+
8
+ It is built using the `textual` library, which provides (imo) a great terminal UI experience. The TUI is of course written in Python 🥰. This is my first time creating a TUI so I hope its good :) Depending on your terminal, the ui might look different. It all depends on how well your terminal supports different things.
9
+
10
+ Your API key is stored "securely" using the `keyring` library, so you don't have to worry about it being exposed in your terminal history or config files.
11
+
12
+ Additionaly, your settings are store using the `platformdirs` `user_config_dir` function. Your settings are probobly stored in `~/Library/Application Support/flavortui` on macOS, `C:\Users\<user>\AppData\Local\flavortui` on Windows (`%localappdata%`), and ~/.config/flavortui (or $XDG_CONFIG_HOME) on Linux.
13
+
14
+ <div>
15
+ <img src="screenshots/kitchen.png" alt="Kitchen Screenshot" style="width:49%;">
16
+ <img src="screenshots/with_sidebar.png" alt="Shop Screenshot" style="width:49%;">
17
+ </div>
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install flavortui
23
+ ```
24
+
25
+ ## Local Development
26
+
27
+ ```bash
28
+ python -m venv venv
29
+ source venv/bin/activate # On Windows: venv\Scripts\activate
30
+ pip install -e .
31
+ ```
32
+
33
+ Run with either:
34
+
35
+ ```bash
36
+ flavortui
37
+ ```
38
+
39
+ or:
40
+
41
+ ```bash
42
+ python -m flavortui
43
+ ```
44
+
45
+ ## API
46
+
47
+ Flavortown API docs can be found [here](https://flavortown.hackclub.com/api/v1/docs).
48
+
49
+ ## License
50
+
51
+ `flavortui` is distributed under the terms of the MIT license.
@@ -0,0 +1,21 @@
1
+ # TODO
2
+
3
+ - performance kinda sucks (bruh)
4
+ - offline mode
5
+ - working keyboard support (oops)
6
+
7
+ - video showing off the app
8
+ - polish for ship
9
+
10
+ - refresh button?
11
+
12
+ duh:
13
+
14
+ - projects page
15
+ - explore page
16
+ - settings page
17
+ - shop page
18
+
19
+
20
+ - name of project creator + picture?
21
+ - demo, readme, repo urls
@@ -0,0 +1,69 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "flavortui"
7
+ dynamic = ["version"]
8
+ description = 'Feature-rich terminal UI for Flavortown: devlogs, projects, shop, and more.'
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ keywords = []
13
+ authors = [
14
+ { name = "BookCatKid", email = "99609593+BookCatKid@users.noreply.github.com" },
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Programming Language :: Python",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: Implementation :: CPython",
24
+ "Programming Language :: Python :: Implementation :: PyPy",
25
+ ]
26
+ dependencies = [
27
+ "textual",
28
+ "textual-image[textual]",
29
+ "requests",
30
+ "platformdirs",
31
+ "keyring",
32
+ ]
33
+
34
+ [project.scripts]
35
+ flavortui = "flavortui.main:main"
36
+
37
+ [project.urls]
38
+ Documentation = "https://github.com/BookCatKid/flavortui#readme"
39
+ Issues = "https://github.com/BookCatKid/flavortui/issues"
40
+ Source = "https://github.com/BookCatKid/flavortui"
41
+
42
+ [tool.hatch.version]
43
+ path = "src/flavortui/__about__.py"
44
+
45
+ [tool.hatch.envs.types]
46
+ extra-dependencies = [
47
+ "mypy>=1.0.0",
48
+ ]
49
+ [tool.hatch.envs.types.scripts]
50
+ check = "mypy --install-types --non-interactive {args:src/flavortui tests}"
51
+
52
+ [tool.coverage.run]
53
+ source_pkgs = ["flavortui", "tests"]
54
+ branch = true
55
+ parallel = true
56
+ omit = [
57
+ "src/flavortui/__about__.py",
58
+ ]
59
+
60
+ [tool.coverage.paths]
61
+ flavortui = ["src/flavortui", "*/flavortui/src/flavortui"]
62
+ tests = ["tests", "*/flavortui/tests"]
63
+
64
+ [tool.coverage.report]
65
+ exclude_lines = [
66
+ "no cov",
67
+ "if __name__ == .__main__.:",
68
+ "if TYPE_CHECKING:",
69
+ ]
@@ -0,0 +1,4 @@
1
+ textual
2
+ keyring
3
+ requests
4
+ textual-image[textual]
Binary file
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2026-present BookCatKid <99609593+BookCatKid@users.noreply.github.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "1.0.0"
@@ -0,0 +1,3 @@
1
+ from .__about__ import __version__
2
+
3
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from flavortui.main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
File without changes
@@ -0,0 +1,75 @@
1
+ from urllib.parse import urlencode
2
+
3
+ from flavortui.api.client import get_client
4
+
5
+ # This file is surprisingly simple!
6
+
7
+ # simple helper func
8
+
9
+
10
+ def fetch_endpoint(endpoint, api_key):
11
+ return get_client(api_key).fetch_endpoint(endpoint)
12
+
13
+
14
+ # User stuff
15
+
16
+
17
+ def get_user(api_key, user_id="me"):
18
+ status_code, response = fetch_endpoint(f"users/{user_id}", api_key)
19
+ return status_code, response
20
+
21
+
22
+ def get_users(api_key, query="", page=1):
23
+ params = urlencode({"page": page, "query": query})
24
+ return fetch_endpoint(f"users?{params}", api_key)
25
+
26
+
27
+ # Store stuff
28
+
29
+
30
+ def get_store(api_key):
31
+ return fetch_endpoint("store", api_key)
32
+
33
+
34
+ # don't think I'll ever use this but ok?
35
+ def get_store_item(api_key, item_id):
36
+ return fetch_endpoint(f"store/{item_id}", api_key)
37
+
38
+
39
+ # Projects
40
+
41
+
42
+ def get_project(api_key, project_id):
43
+ return fetch_endpoint(f"projects/{project_id}", api_key)
44
+
45
+
46
+ def get_projects_for_user(api_key, user_id="me"):
47
+ user = get_user(api_key, user_id)
48
+ projects = []
49
+ for project_id in user[1]["project_ids"]:
50
+ projects.append(get_project(api_key, project_id))
51
+ return projects
52
+
53
+
54
+ def get_projects(api_key, page=1, query=""):
55
+ params = urlencode({"page": page, "query": query})
56
+ return fetch_endpoint(f"projects?{params}", api_key)
57
+
58
+
59
+ # Devlogs
60
+
61
+
62
+ def get_project_devlogs(api_key, project_id):
63
+ return fetch_endpoint(f"projects/{project_id}/devlogs", api_key)
64
+
65
+
66
+ def get_devlogs(api_key, page=1):
67
+ params = urlencode({"page": page})
68
+ return fetch_endpoint(f"devlogs?{params}", api_key)
69
+
70
+
71
+ # Check api key
72
+
73
+
74
+ def check_api_key(api_key):
75
+ return get_user(api_key, "me")[0] == 200
@@ -0,0 +1,16 @@
1
+ import keyring
2
+
3
+ SERVICE = "flavortown-cli"
4
+ ACCOUNT = "default"
5
+
6
+
7
+ def get_api_key():
8
+ return keyring.get_password(SERVICE, ACCOUNT)
9
+
10
+
11
+ def save_api_key(key: str):
12
+ keyring.set_password(SERVICE, ACCOUNT, key)
13
+
14
+
15
+ def delete_api_key():
16
+ keyring.delete_password(SERVICE, ACCOUNT)
@@ -0,0 +1,168 @@
1
+ import hashlib
2
+ import json
3
+ import os
4
+ import threading
5
+ import time
6
+ import urllib.parse
7
+
8
+ import requests
9
+
10
+ CACHE_DIR = ".cache"
11
+ API_TIMEOUT = (3.05, 10)
12
+ IMAGE_TIMEOUT = (3.05, 20)
13
+
14
+
15
+ class OfflineError(Exception):
16
+ pass
17
+
18
+
19
+ class ApiClient:
20
+ def __init__(self, api_key, settings=None):
21
+ self.api_key = api_key
22
+ self.settings = settings or {}
23
+ self.base_url = "https://flavortown.hackclub.com/api/v1"
24
+ self.headers = {
25
+ "Authorization": f"Bearer {api_key}",
26
+ "X-Flavortown-Ext-16596": "true",
27
+ }
28
+ self.is_offline = False
29
+
30
+ self._ensure_cache_dir()
31
+
32
+ self.rate_limits = {
33
+ # In seconds, how long we should wait between requests.
34
+ # In general we go for double the "required" time.
35
+ "projects": 24,
36
+ "store": 24,
37
+ "users_list": 24,
38
+ "projects_id": 4,
39
+ "store_id": 4,
40
+ "default": 2,
41
+ }
42
+
43
+ def _ensure_cache_dir(self):
44
+ os.makedirs(CACHE_DIR, exist_ok=True)
45
+
46
+ def _get_cache_file(self, endpoint, file_format):
47
+ hashed_key = hashlib.sha256(endpoint.encode()).hexdigest()
48
+ return os.path.join(CACHE_DIR, f"{hashed_key}.{file_format}")
49
+
50
+ def _save_to_cache(self, endpoint, response, status_code):
51
+ self._ensure_cache_dir()
52
+ file_name = self._get_cache_file(endpoint, "json")
53
+ with open(file_name, "w", encoding="utf-8") as file:
54
+ json.dump(
55
+ {
56
+ "timestamp": time.time(),
57
+ "data": response,
58
+ "status_code": status_code,
59
+ },
60
+ file,
61
+ )
62
+
63
+ def _load_from_cache(self, endpoint):
64
+ file_name = self._get_cache_file(endpoint, "json")
65
+ if os.path.exists(file_name):
66
+ with open(file_name, "r", encoding="utf-8") as f:
67
+ return json.load(f)
68
+ return None
69
+
70
+ def _get_endpoint_rate_limit(self, endpoint):
71
+ parts = endpoint.split("/")
72
+ base = parts[0]
73
+
74
+ if base == "users":
75
+ if len(parts) == 1:
76
+ return self.rate_limits["users_list"]
77
+ return self.rate_limits["default"]
78
+
79
+ if base == "store":
80
+ if len(parts) == 1:
81
+ return self.rate_limits["store"]
82
+ return self.rate_limits["store_id"]
83
+
84
+ if base == "projects":
85
+ if len(parts) == 1:
86
+ return self.rate_limits["projects"]
87
+ return self.rate_limits["projects_id"]
88
+
89
+ # add other bases here later
90
+
91
+ return self.rate_limits["default"]
92
+
93
+ def _revalidate(self, endpoint):
94
+ try:
95
+ response = requests.get(
96
+ f"{self.base_url}/{endpoint}",
97
+ headers=self.headers,
98
+ timeout=API_TIMEOUT,
99
+ )
100
+ self._save_to_cache(endpoint, response.json(), response.status_code)
101
+ except Exception:
102
+ pass
103
+
104
+ def fetch_endpoint(self, endpoint):
105
+ cached_file = self._load_from_cache(endpoint)
106
+ caching_strategy = self.settings.get("caching_strategy", "timed")
107
+ if cached_file:
108
+ # default caching
109
+ if (
110
+ time.time() - cached_file["timestamp"]
111
+ < self._get_endpoint_rate_limit(endpoint)
112
+ and caching_strategy == "timed"
113
+ ):
114
+ return cached_file["status_code"], cached_file["data"]
115
+ # extended caching
116
+ if (
117
+ time.time() - cached_file["timestamp"]
118
+ < self._get_endpoint_rate_limit(endpoint) * 15
119
+ and caching_strategy == "extended"
120
+ ):
121
+ return cached_file["status_code"], cached_file["data"]
122
+ # swr caching
123
+ if caching_strategy == "swr":
124
+ if time.time() - cached_file[
125
+ "timestamp"
126
+ ] >= self._get_endpoint_rate_limit(endpoint):
127
+ threading.Thread(
128
+ target=self._revalidate, args=(endpoint,), daemon=True
129
+ ).start()
130
+ return cached_file["status_code"], cached_file["data"]
131
+
132
+ url = f"{self.base_url}/{endpoint}"
133
+
134
+ try:
135
+ response = requests.get(url, headers=self.headers, timeout=API_TIMEOUT)
136
+ except requests.ConnectionError as e:
137
+ self.is_offline = True
138
+ if cached_file:
139
+ return cached_file["status_code"], cached_file["data"]
140
+ raise OfflineError("Could not connect to the Flavortown server.") from e
141
+ self.is_offline = False
142
+ data = response.json()
143
+ self._save_to_cache(endpoint, data, response.status_code)
144
+ return response.status_code, data
145
+
146
+ def fetch_image(self, url):
147
+ ext = os.path.splitext(urllib.parse.urlparse(url).path)[1].lstrip(".") or "png"
148
+ if not os.path.exists(self._get_cache_file(url, ext)):
149
+ try:
150
+ response = requests.get(url, timeout=IMAGE_TIMEOUT)
151
+ response.raise_for_status()
152
+ except requests.ConnectionError:
153
+ raise OfflineError("Could not connect to the Flavortown server.")
154
+ with open(self._get_cache_file(url, ext), "wb") as f:
155
+ f.write(response.content)
156
+ return self._get_cache_file(url, ext)
157
+
158
+
159
+ _GLOBAL_CLIENT = None
160
+
161
+
162
+ def get_client(api_key, settings=None):
163
+ global _GLOBAL_CLIENT
164
+ if not _GLOBAL_CLIENT or _GLOBAL_CLIENT.api_key != api_key:
165
+ _GLOBAL_CLIENT = ApiClient(api_key, settings)
166
+ elif settings is not None:
167
+ _GLOBAL_CLIENT.settings = settings
168
+ return _GLOBAL_CLIENT
@@ -0,0 +1,14 @@
1
+ from datetime import datetime, timezone
2
+
3
+
4
+ def format_seconds(seconds):
5
+ try:
6
+ hours = seconds // 3600
7
+ minutes = (seconds % 3600) // 60
8
+ return f"{hours}h {minutes}m"
9
+ except Exception:
10
+ return "0h 0m"
11
+
12
+
13
+ def get_days_ago(utc_time_string):
14
+ return (datetime.now(timezone.utc) - datetime.fromisoformat(utc_time_string)).days
@@ -0,0 +1,24 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from platformdirs import user_config_dir
5
+
6
+ SETTINGS_PATH = Path(user_config_dir("flavortui")) / "settings.json"
7
+ DEFAULTS = {"image_mode": "auto", "caching_strategy": "swr"}
8
+
9
+
10
+ def load_settings() -> dict:
11
+ if SETTINGS_PATH.exists():
12
+ try:
13
+ with open(SETTINGS_PATH, encoding="utf-8") as f:
14
+ saved = json.load(f)
15
+ return {**DEFAULTS, **saved}
16
+ except (json.JSONDecodeError, OSError):
17
+ pass
18
+ return dict(DEFAULTS)
19
+
20
+
21
+ def save_settings(settings: dict) -> None:
22
+ SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
23
+ with open(SETTINGS_PATH, "w", encoding="utf-8") as f:
24
+ json.dump(settings, f, indent=2)
File without changes
@@ -0,0 +1,84 @@
1
+ from textual.app import ComposeResult
2
+ from textual.widgets import Button, Input, Static
3
+
4
+ from flavortui.api.api import check_api_key
5
+ from flavortui.api.api_key import delete_api_key, get_api_key, save_api_key
6
+ from flavortui.components.popup_modal import PopupModal
7
+
8
+
9
+ class ApiKeyInput(PopupModal):
10
+ DEFAULT_CSS = """
11
+ #dialog {
12
+ height: auto;
13
+ max-height: 90%;
14
+ padding: 2 4;
15
+ }
16
+
17
+ #dialog-content {
18
+ height: auto;
19
+ align-horizontal: center;
20
+ }
21
+
22
+ #title {
23
+ text-align: center;
24
+ width: 100%;
25
+ margin-bottom: 1;
26
+ }
27
+
28
+ #subtitle {
29
+ text-align: center;
30
+ width: 100%;
31
+ margin-bottom: 1;
32
+ }
33
+ """
34
+
35
+ def __init__(self, callback_function, **kwargs):
36
+ super().__init__(**kwargs)
37
+ self._callback_function = callback_function
38
+
39
+ def compose_content(self) -> ComposeResult:
40
+ yield Static("🔒 Flavortown Api Key Required", id="title")
41
+ yield Static("Please enter your api key to continue.", id="subtitle")
42
+ yield Input(password=True, placeholder="Api Key")
43
+
44
+ def compose_footer(self) -> ComposeResult:
45
+ return [
46
+ Button("Save API Key", id="save_key", variant="success"),
47
+ Button("Print API Key", id="print_key", variant="primary"),
48
+ Button("Delete API Key", id="delete_key", variant="error"),
49
+ Button("Close", id="close", variant="default"),
50
+ ]
51
+
52
+ def on_button_pressed(self, event: Button.Pressed):
53
+ if event.button.id == "save_key":
54
+ self._save_key()
55
+ elif event.button.id == "print_key":
56
+ key = get_api_key()
57
+ if key:
58
+ self.notify(f"API Key: {key}", timeout=5.0)
59
+ else:
60
+ self.notify("No API key saved.", timeout=3.0)
61
+ elif event.button.id == "delete_key":
62
+ try:
63
+ delete_api_key()
64
+ self.notify("API key deleted successfully.", timeout=2.0)
65
+ except Exception as e:
66
+ self.notify(f"Error deleting API key: {e}", timeout=2.0)
67
+ elif event.button.id == "close":
68
+ self.app.pop_screen()
69
+
70
+ def _save_key(self) -> None:
71
+ value = self.query_one(Input).value
72
+ if not value.strip():
73
+ self.notify("API key cannot be empty.", timeout=2.0)
74
+ return
75
+ if not check_api_key(value):
76
+ self.notify("Invalid API key. Please check and try again.", timeout=4.0)
77
+ return
78
+ try:
79
+ save_api_key(value)
80
+ self.notify("Api key saved successfully!", timeout=2.0)
81
+ self.app.pop_screen()
82
+ self._callback_function()
83
+ except Exception as e:
84
+ self.notify(f"Error saving api key: {e}", timeout=2.0)