diff --git a/.gitignore b/.gitignore index 69133a7..e44cb4d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,7 @@ env/ *.db *.sqlite *.sqlite3 -db/*.json +data/db/*.json # Flask instance/ diff --git a/api/image_api.py b/api/image_api.py index a23f285..579e02d 100644 --- a/api/image_api.py +++ b/api/image_api.py @@ -1,15 +1,15 @@ import os -import uuid -from io import BytesIO +from PIL import Image as PILImage, UnidentifiedImageError from flask import Blueprint, request, jsonify, send_file +from tinydb import Query +from config.paths import get_user_image_dir + from db.db import image_db from models.image import Image -from tinydb import Query -from PIL import Image as PILImage, UnidentifiedImageError image_api = Blueprint('image_api', __name__) -UPLOAD_FOLDER = './resources/images' +UPLOAD_FOLDER = get_user_image_dir("user123") ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png'} IMAGE_TYPE_PROFILE = 1 IMAGE_TYPE_ICON = 2 @@ -61,7 +61,7 @@ def upload(): format_extension_map = {'JPEG': '.jpg', 'PNG': '.png'} extension = format_extension_map.get(original_format, '.png') - image_record = Image(extension=extension, permanent=perm, type=image_type) + image_record = Image(extension=extension, permanent=perm, type=image_type, user="user123") filename = image_record.id + extension filepath = os.path.abspath(os.path.join(UPLOAD_FOLDER, filename)) @@ -83,11 +83,11 @@ def upload(): @image_api.route('/image/request/', methods=['GET']) def request_image(id): ImageQuery = Query() - image = image_db.get(ImageQuery.id == id) + image: Image = Image.from_dict(image_db.get(ImageQuery.id == id)) if not image: return jsonify({'error': 'Image not found'}), 404 - filename = f"{image['id']}{image['extension']}" - filepath = os.path.abspath(os.path.join(UPLOAD_FOLDER, filename)) + filename = f"{image.id}{image.extension}" + filepath = os.path.abspath(os.path.join(get_user_image_dir(image.user), filename)) if not os.path.exists(filepath): return jsonify({'error': 'File not found'}), 404 return send_file(filepath) diff --git a/config/paths.py b/config/paths.py new file mode 100644 index 0000000..a5c354b --- /dev/null +++ b/config/paths.py @@ -0,0 +1,27 @@ +# python +# file: config/paths.py +import os + +# Constant directory names +DATA_DIR_NAME = 'data' +TEST_DATA_DIR_NAME = 'test_data' + +# Project root (two levels up from this file) +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +def get_database_dir(db_env: str | None = None) -> str: + """ + Return the absolute base directory path for the given DB env. + db_env: 'prod' uses `data/db`, anything else uses `test_data/db`. + """ + env = (db_env or os.environ.get('DB_ENV', 'prod')).lower() + base_name = DATA_DIR_NAME if env == 'prod' else TEST_DATA_DIR_NAME + return os.path.join(PROJECT_ROOT, base_name, 'db') + +def get_user_image_dir(username: str | None) -> str: + """ + Return the absolute directory path for storing images for a specific user. + """ + if username: + return os.path.join(PROJECT_ROOT, DATA_DIR_NAME, 'images', username) + return os.path.join(PROJECT_ROOT, 'resources', 'images') diff --git a/db/db.py b/db/db.py index 535ef1e..703b204 100644 --- a/db/db.py +++ b/db/db.py @@ -1,18 +1,10 @@ # python import os +from config.paths import get_database_dir import threading from tinydb import TinyDB -DB_ENV = os.environ.get('DB_ENV', 'prod') - - -project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -if DB_ENV == 'prod': - base_dir = os.path.join(project_root, 'data/db') -else: - base_dir = os.path.join(project_root, 'test_data/db') - +base_dir = get_database_dir() os.makedirs(base_dir, exist_ok=True) @@ -95,7 +87,7 @@ reward_db = LockedTable(_reward_db) image_db = LockedTable(_image_db) pending_reward_db = LockedTable(_pending_rewards_db) -if DB_ENV == 'test': +if os.environ.get('DB_ENV', 'prod') == 'test': child_db.truncate() task_db.truncate() reward_db.truncate() diff --git a/db/default.py b/db/default.py index 4bafb2c..4f17d38 100644 --- a/db/default.py +++ b/db/default.py @@ -1,14 +1,14 @@ # python # File: db/debug.py -import random -from models.child import Child -from models.task import Task -from models.reward import Reward -from models.image import Image from tinydb import Query -from db.db import child_db, task_db, reward_db, image_db + from api.image_api import IMAGE_TYPE_ICON, IMAGE_TYPE_PROFILE +from db.db import task_db, reward_db, image_db +from models.image import Image +from models.reward import Reward +from models.task import Task + def populate_default_data(): # Create tasks @@ -86,19 +86,33 @@ def initializeImages(): """Initialize the image database with default images if empty.""" if len(image_db.all()) == 0: image_defs = [ - ('computer-game', IMAGE_TYPE_ICON, '.png', True), - ('ice-cream', IMAGE_TYPE_ICON, '.png', True), - ('meal', IMAGE_TYPE_ICON, '.png', True), - ('playground', IMAGE_TYPE_ICON, '.png', True), - ('tablet', IMAGE_TYPE_ICON, '.png', True), ('boy01', IMAGE_TYPE_PROFILE, '.png', True), - ('girl01', IMAGE_TYPE_PROFILE, '.png', True), - ('girl02', IMAGE_TYPE_PROFILE, '.png', True), ('boy02', IMAGE_TYPE_PROFILE, '.png', True), ('boy03', IMAGE_TYPE_PROFILE, '.png', True), - ('girl03', IMAGE_TYPE_PROFILE, '.png', True), ('boy04', IMAGE_TYPE_PROFILE, '.png', True), + ('broom', IMAGE_TYPE_ICON, '.png', True), + ('computer-game', IMAGE_TYPE_ICON, '.png', True), + ('fighting', IMAGE_TYPE_ICON, '.png', True), + ('games-with-dad', IMAGE_TYPE_ICON, '.png', True), + ('girl01', IMAGE_TYPE_PROFILE, '.png', True), + ('girl02', IMAGE_TYPE_PROFILE, '.png', True), + ('girl03', IMAGE_TYPE_PROFILE, '.png', True), ('girl04', IMAGE_TYPE_PROFILE, '.png', True), + ('good', IMAGE_TYPE_ICON, '.png', True), + ('homework', IMAGE_TYPE_ICON, '.png', True), + ('ice-cream', IMAGE_TYPE_ICON, '.png', True), + ('ignore', IMAGE_TYPE_ICON, '.png', True), + ('lying', IMAGE_TYPE_ICON, '.png', True), + ('make-the-bed', IMAGE_TYPE_ICON, '.png', True), + ('meal', IMAGE_TYPE_ICON, '.png', True), + ('money', IMAGE_TYPE_ICON, '.png', True), + ('playground', IMAGE_TYPE_ICON, '.png', True), + ('tablet', IMAGE_TYPE_ICON, '.png', True), + ('toilet', IMAGE_TYPE_ICON, '.png', True), + ('trash-can', IMAGE_TYPE_ICON, '.png', True), + ('tv', IMAGE_TYPE_ICON, '.png', True), + ('vacuum', IMAGE_TYPE_ICON, '.png', True), + ('yelling', IMAGE_TYPE_ICON, '.png', True), ] for _id, _type, ext, perm in image_defs: img = Image(type=_type, extension=ext, permanent=perm) diff --git a/main.py b/main.py index 90efff0..f048433 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ -import sys, logging - +import sys, logging, os +from config.paths import get_user_image_dir from flask import Flask, request from flask_cors import CORS @@ -55,7 +55,8 @@ def start_background_threads(): broadcaster.daemon = True broadcaster.start() -# Initialize background workers on server start +# TODO: implement users +os.makedirs(get_user_image_dir("user123"), exist_ok=True) initializeImages() start_background_threads() diff --git a/models/image.py b/models/image.py index 76b56b7..15e69f0 100644 --- a/models/image.py +++ b/models/image.py @@ -7,6 +7,7 @@ class Image(BaseModel): type: int extension: str permanent: bool = False + user: str | None = None @classmethod def from_dict(cls, d: dict): @@ -17,7 +18,8 @@ class Image(BaseModel): permanent=d.get('permanent', False), id=d.get('id'), created_at=d.get('created_at'), - updated_at=d.get('updated_at') + updated_at=d.get('updated_at'), + user=d.get('user') ) def to_dict(self): @@ -25,6 +27,7 @@ class Image(BaseModel): base.update({ 'type': self.type, 'permanent': self.permanent, - 'extension': self.extension + 'extension': self.extension, + 'user': self.user }) return base diff --git a/resources/images/broom.png b/resources/images/broom.png new file mode 100644 index 0000000..22cf4e6 Binary files /dev/null and b/resources/images/broom.png differ diff --git a/resources/images/fighting.png b/resources/images/fighting.png new file mode 100644 index 0000000..3264914 Binary files /dev/null and b/resources/images/fighting.png differ diff --git a/resources/images/games-with-dad.png b/resources/images/games-with-dad.png new file mode 100644 index 0000000..85a50fa Binary files /dev/null and b/resources/images/games-with-dad.png differ diff --git a/resources/images/good.png b/resources/images/good.png new file mode 100644 index 0000000..bc9654b Binary files /dev/null and b/resources/images/good.png differ diff --git a/resources/images/homework.png b/resources/images/homework.png new file mode 100644 index 0000000..16dfe0e Binary files /dev/null and b/resources/images/homework.png differ diff --git a/resources/images/ignore.png b/resources/images/ignore.png new file mode 100644 index 0000000..a82db33 Binary files /dev/null and b/resources/images/ignore.png differ diff --git a/resources/images/lying.png b/resources/images/lying.png new file mode 100644 index 0000000..b9197da Binary files /dev/null and b/resources/images/lying.png differ diff --git a/resources/images/make-the-bed.png b/resources/images/make-the-bed.png new file mode 100644 index 0000000..e63653c Binary files /dev/null and b/resources/images/make-the-bed.png differ diff --git a/resources/images/money.png b/resources/images/money.png new file mode 100644 index 0000000..7482c3d Binary files /dev/null and b/resources/images/money.png differ diff --git a/resources/images/toilet.png b/resources/images/toilet.png new file mode 100644 index 0000000..8cf8125 Binary files /dev/null and b/resources/images/toilet.png differ diff --git a/resources/images/trash-can.png b/resources/images/trash-can.png new file mode 100644 index 0000000..5bbcebb Binary files /dev/null and b/resources/images/trash-can.png differ diff --git a/resources/images/tv.png b/resources/images/tv.png new file mode 100644 index 0000000..3feba3c Binary files /dev/null and b/resources/images/tv.png differ diff --git a/resources/images/vacuum.png b/resources/images/vacuum.png new file mode 100644 index 0000000..060e0c8 Binary files /dev/null and b/resources/images/vacuum.png differ diff --git a/resources/images/yelling.png b/resources/images/yelling.png new file mode 100644 index 0000000..7a2a8c8 Binary files /dev/null and b/resources/images/yelling.png differ diff --git a/web/vue-app/src/layout/ParentLayout.vue b/web/vue-app/src/layout/ParentLayout.vue index ea359b6..446d196 100644 --- a/web/vue-app/src/layout/ParentLayout.vue +++ b/web/vue-app/src/layout/ParentLayout.vue @@ -159,7 +159,7 @@ const showBack = computed( /* top bar holds title and logout button */ .topbar { display: grid; - grid-template-columns: 76px 1fr 76px; + grid-template-columns: 46px 1fr 46px; align-items: center; padding: 5px 5px; } @@ -235,7 +235,11 @@ const showBack = computed( @media (max-width: 480px) { .back-btn { padding: 0.45rem 0.75rem; - font-size: 0.95rem; + font-size: 0.6rem; + height: 100%; + } + .login-btn { + height: 100%; } } @@ -250,4 +254,12 @@ const showBack = computed( color: #667eea; font-weight: 600; } + +@media (max-width: 480px) { + .login-btn button { + padding: 0.45rem 0.75rem; + font-size: 0.6rem; + height: 100%; + } +}