diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..39d8b1a Binary files /dev/null and b/.DS_Store differ diff --git a/Client/__init__.py b/Client/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Client/__init__.py @@ -0,0 +1 @@ + diff --git a/Client/app.py b/Client/app.py index 9d5b407..96c4d73 100644 --- a/Client/app.py +++ b/Client/app.py @@ -1,131 +1,355 @@ -from flask import Flask, render_template, request, jsonify, redirect, url_for from tcp_client import TCPClient + +from flask import Flask, render_template, request, jsonify, redirect, url_for, session, flash + import time import base64 +import os +from database import Database from comment_manager import CommentManager from image_manager import ImageManager app = Flask(__name__) +app.secret_key = os.urandom(24) -# Create a global client instance tcp_client = TCPClient(server_host='localhost', server_port=5001) -#initialize the managers + comment_manager = CommentManager() image_manager = ImageManager() upload_images = [] +db = Database() + -#login page @app.route('/') def login(): + # Check if user is already logged in + if 'user_id' in session: + return redirect(url_for('index')) return render_template('login.html') -# Process login +@app.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + email = request.form.get('email', None) + + if not username or not password: + return render_template('register.html', error="Username and password are required") + + success, result = db.create_user(username, password, email) + + if success: + print(f"User registered: {username}") + tcp_client.send_request("REGISTER", { + "username": username, + "user_id": result + }) + + flash("Registration successful! Please log in.") + return redirect(url_for('login')) + else: + return render_template('register.html', error=result) + + return render_template('register.html') + @app.route('/process_login', methods=['POST']) def process_login(): username = request.form.get('username') password = request.form.get('password') - # Use TCP client to authenticate - success, response = tcp_client.send_request("LOGIN", { - "username": username, - "password": password - }) + if not username or not password: + return render_template('login.html', error="Username and password are required") + + success, user_id = db.authenticate_user(username, password) if success: + session['user_id'] = user_id + session['username'] = username + + tcp_client.send_request("LOGIN", { + "username": username, + "user_id": user_id + }) + + print(f"User logged in: {username}") return redirect(url_for('index')) else: - return render_template('login.html', error="Login failed") + return render_template('login.html', error="Invalid username or password") + +@app.route('/logout') +def logout(): + session.pop('user_id', None) + session.pop('username', None) -#home page + return redirect(url_for('login')) + @app.route('/home') def index(): - # Use TCP client to fetch images - # success, response = tcp_client.send_request("GET_IMAGES", {}) - images = image_manager.get_images() - return render_template('index.html', images=images) + + if 'user_id' not in session: + return redirect(url_for('login')) + + images = db.get_all_images() + + user = db.get_user(session['user_id']) + + for image in images: + image['is_saved'] = db.is_image_saved_by_user(session['user_id'], image['id']) + + categories = [] + for image in images: + if image['category'] and image['category'] not in categories: + categories.append(image['category']) + + return render_template('index.html', images=images, user=user, categories=categories) + + +#prev home route +# #home page +# @app.route('/home') +# def index(): +# # Use TCP client to fetch images +# # success, response = tcp_client.send_request("GET_IMAGES", {}) +# images = image_manager.get_images() +# return render_template('index.html', images=images) -#saved page @app.route('/saved') def saved(): - # Use TCP client to fetch saved images - # success, response = tcp_client.send_request("GET_SAVED_IMAGES", {"username": username}) - username = "Andy" - images = image_manager.get_saved_images(username) - return render_template('saved-section.html', images=images, username=username) + + if 'user_id' not in session: + return redirect(url_for('login')) + + images = db.get_saved_images(session['user_id']) + + + user = db.get_user(session['user_id']) + + categories = [] + for image in images: + if image['category'] and image['category'] not in categories: + categories.append(image['category']) + + return render_template('saved-section.html', images=images, username=user['username'], categories=categories) -#when clicking the saved button on an image, copy it to the saved section @app.route('/save_image', methods=['POST']) def save_image(): + + if 'user_id' not in session: + return jsonify({"success": False, "message": "You must be logged in to save images"}) + image_id = request.json.get('image_id') - username = "Andy" - - image = image_manager.get_image_by_id(image_id) - if image: - image_manager.save_image_for_user(image, username) + + success = db.save_image_for_user(session['user_id'], image_id) + + if success: + + tcp_client.send_request("SAVE_IMAGE", { + "user_id": session['user_id'], + "image_id": image_id + }) + return jsonify({"success": True, "message": "Image saved to your vault!!"}) + else: + return jsonify({"success": False, "message": "Image already saved or couldn't be saved"}) + - return jsonify({"success": False, "message": "Couldn't save this image. Please try again later"}) +@app.route('/unsave_image', methods=['POST']) +def unsave_image(): + + if 'user_id' not in session: + return jsonify({"success": False, "message": "You must be logged in to unsave images"}) + + image_id = request.json.get('image_id') + + success = db.unsave_image_for_user(session['user_id'], image_id) + + if success: + + tcp_client.send_request("UNSAVE_IMAGE", { + "user_id": session['user_id'], + "image_id": image_id + }) + + return jsonify({"success": True, "message": "Image removed from your saved posts"}) + else: + return jsonify({"success": False, "message": "Failed to remove image"}) -#profile page @app.route('/profile') def profile(): - username = "Andy" - return render_template('profile.html', username=username) + # Check if user is logged in + if 'user_id' not in session: + return redirect(url_for('login')) + + user = db.get_user(session['user_id']) + + # Get counts for user stats + saved_count = db.get_saved_count(session['user_id']) + uploaded_count = db.get_uploaded_count(session['user_id']) + comment_count = db.get_comment_count(session['user_id']) + + return render_template('profile.html', + user=user, + saved_count=saved_count, + uploaded_count=uploaded_count, + comment_count=comment_count) + +# ======= +# # Use TCP client to fetch saved images +# # success, response = tcp_client.send_request("GET_SAVED_IMAGES", {"username": username}) +# username = "Andy" +# images = image_manager.get_saved_images(username) +# return render_template('saved-section.html', images=images, username=username) + +# #when clicking the saved button on an image, copy it to the saved section +# @app.route('/save_image', methods=['POST']) +# def save_image(): +# image_id = request.json.get('image_id') +# username = "Andy" + +# image = image_manager.get_image_by_id(image_id) +# if image: +# image_manager.save_image_for_user(image, username) +# return jsonify({"success": True, "message": "Image saved to your vault!!"}) + +# return jsonify({"success": False, "message": "Couldn't save this image. Please try again later"}) + +# #profile page +# @app.route('/profile') +# def profile(): +# username = "Andy" +# return render_template('profile.html', username=username) +# >>>>>>> db-to-master-merge-check + -#image uploads @app.route('/upload', methods=['POST']) def upload_image(): + + if 'user_id' not in session: + return redirect(url_for('login')) + if 'image' not in request.files: - return 'no file (flask side)', 400 - #gets info from form + return 'No file provided', 400 + image = request.files['image'] caption = request.form.get('caption') tags = request.form.get('tags') - image_data, error = image_manager.upload_image(image, caption, tags) - if error: - return error, 400 + if image.filename == '': + return 'No selected file', 400 + + # Save the file to uploads folder + image_content = image.read() + image_filename = image.filename + upload_folder = './static/uploads' + + if not os.path.exists(upload_folder): + os.makedirs(upload_folder) + + image_path = os.path.join(upload_folder, image_filename) + + with open(image_path, 'wb') as f: + f.write(image_content) + + # Encode image to base64 + base64_image = base64.b64encode(image_content).decode('utf-8') + + + image_url = f"./static/uploads/{image_filename}" + image_id = db.upload_image(image_url, caption, tags, session['user_id'], base64_image) + + image_data = { + "id": image_id, + "url": image_url, + "caption": caption, + "category": tags, + "user_id": session['user_id'], + "image": base64_image + } success, response = tcp_client.send_request("UPLOAD_IMAGE", image_data) + +# ======= +# image_data, error = image_manager.upload_image(image, caption, tags) +# if error: +# return error, 400 + +# success, response = tcp_client.send_request("UPLOAD_IMAGE", image_data) +# >>>>>>> db-to-master-merge-check + if success: return redirect(url_for('index')) else: - return jsonify({'status': 'error', 'message': response}) + return redirect(url_for('index')) #we are just going to ignore this error #jsonify({'status': 'error', 'message': response}) -#comment handleing routes -#to get the comments for a specific post @app.route('/api/comments/') def get_comments_for_img(image_id): - image_comments = comment_manager.get_comments(image_id) - return jsonify({"success": True, "comments": image_comments}) + comments = db.get_comments(image_id) + + return jsonify({"success": True, "comments": comments}) -#save a newly written comment + #save a newly written comment @app.route('/api/comments', methods=['POST']) def save_comment(): + + if 'user_id' not in session: + return jsonify({"success": False, "message": "You must be logged in to comment"}) + data = request.json if not data or 'imageId' not in data or 'text' not in data: - return jsonify({"success": False, "message": "fill out all required fields"}) + return jsonify({"success": False, "message": "Fill out all required fields"}) - image_id = data['imageId'] - comment_text = data['text'] +# ======= +# #comment handleing routes +# #to get the comments for a specific post +# @app.route('/api/comments/') +# def get_comments_for_img(image_id): +# image_comments = comment_manager.get_comments(image_id) +# return jsonify({"success": True, "comments": image_comments}) + +# #save a newly written comment +# @app.route('/api/comments', methods=['POST']) +# def save_comment(): +# data = request.json +# if not data or 'imageId' not in data or 'text' not in data: +# return jsonify({"success": False, "message": "fill out all required fields"}) + +# image_id = data['imageId'] +# comment_text = data['text'] - result = comment_manager.save_comment(image_id, comment_text) +# result = comment_manager.save_comment(image_id, comment_text) - return jsonify(result) +# return jsonify(result) +# >>>>>>> db-to-master-merge-check + + image_id = data['imageId'] + comment_text = data['text'] -# API endpoint to check TCP connection + success = db.add_comment(image_id, comment_text, session['user_id']) + + if success: + + tcp_client.send_request("ADD_COMMENT", { + "user_id": session['user_id'], + "image_id": image_id, + "text": comment_text + }) + + return jsonify({"success": True, "message": "Comment posted!!"}) + else: + return jsonify({"success": False, "message": "Failed to post comment"}) + @app.route('/api/check_connection') def check_connection(): if not tcp_client.connected: if not tcp_client.connect(): return jsonify({"status": "disconnected", "message": "Failed to connect to server"}) - # Test the connection with a simple ping success, response = tcp_client.send_request("PING", {"timestamp": time.time()}) if success: @@ -141,7 +365,7 @@ def check_connection(): }) if __name__ == '__main__': - # Connect to the server + print("\n") print("#" * 70) print("CONNECTING TO TCP SERVER ON PORT 5001") @@ -157,5 +381,4 @@ def check_connection(): print("#" * 70) print("\n") - # Run Flask app app.run(debug=True, port=5002) \ No newline at end of file diff --git a/Client/database.py b/Client/database.py new file mode 100644 index 0000000..5e46c38 --- /dev/null +++ b/Client/database.py @@ -0,0 +1,548 @@ +import sqlite3 +import os +import hashlib +import json +from datetime import datetime +import base64 + +class Database: + def __init__(self, db_name='preppersdb.sqlite'): + self.db_name = db_name + self.connection = None + self.cursor = None + self.init_db() + + def connect(self): + """Connect to the SQLite database""" + self.connection = sqlite3.connect(self.db_name) + self.connection.row_factory = sqlite3.Row + self.cursor = self.connection.cursor() + + def close(self): + """Close the database connection""" + if self.connection: + self.connection.close() + self.connection = None + self.cursor = None + + def commit(self): + """Commit changes to the database""" + if self.connection: + self.connection.commit() + + def log(self, message): + """Simple logging function""" + print(f"[Database] {message}") + + def init_db(self): + """Initialize the database tables if they don't exist""" + self.connect() + + # Database tables for all the commands and functions that will be used + tables = [ + '''CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + email TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )''', + '''CREATE TABLE IF NOT EXISTS images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + url TEXT NOT NULL, + caption TEXT, + category TEXT, + user_id INTEGER, + is_default INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + image_data TEXT, + classification_label TEXT, + classification_confidence REAL, + FOREIGN KEY (user_id) REFERENCES users (id) + )''', + '''CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + image_id INTEGER NOT NULL, + user_id INTEGER, + text TEXT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (image_id) REFERENCES images (id), + FOREIGN KEY (user_id) REFERENCES users (id) + )''', + '''CREATE TABLE IF NOT EXISTS saved_images ( + user_id INTEGER NOT NULL, + image_id INTEGER NOT NULL, + saved_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, image_id), + FOREIGN KEY (user_id) REFERENCES users (id), + FOREIGN KEY (image_id) REFERENCES images (id) + )''', + '''CREATE TABLE IF NOT EXISTS classification_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + image_id INTEGER NOT NULL, + label TEXT NOT NULL, + confidence REAL NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (image_id) REFERENCES images (id) + )''' + ] + + for table in tables: + self.cursor.execute(table) + + try: + self.cursor.execute("ALTER TABLE images ADD COLUMN classification_label TEXT") + self.log("INFO - Added classification_label column to images table") + except sqlite3.OperationalError: + + pass + + try: + self.cursor.execute("ALTER TABLE images ADD COLUMN classification_confidence REAL") + self.log("INFO - Added classification_confidence column to images table") + except sqlite3.OperationalError: + pass + + self._create_default_user() + + self.cursor.execute("SELECT COUNT(*) FROM images WHERE is_default = 1") + if self.cursor.fetchone()[0] == 0: + self._insert_default_data() + + # Import saved images + self._import_default_saved_images() + + self.commit() + self.close() + + + #ACCOUNT ANDY IS USED FOR TESTING PURPOSE AND IS HARDOCDED INTO DATABSE SO TEH DEFAULT IMAGES CAN BE USED passowrd is the passwrod + + + def _create_default_user(self): + """Create a default 'Andy' user if it doesn't exist""" + self.cursor.execute("SELECT id FROM users WHERE username = 'Andy'") + user = self.cursor.fetchone() + + if not user: + password_hash = hashlib.sha256('password'.encode()).hexdigest() + + self.cursor.execute( + 'INSERT INTO users (username, password_hash, email) VALUES (?, ?, ?)', + ('Andy', password_hash, 'andy@preppers.app') + ) + + self.log("INFO - Created default user 'Andy' with password 'password'") + else: + self.log("INFO - Default user 'Andy' already exists") + + def _insert_default_data(self): + """Insert default images into the database""" + # Default images + default_images = [ + + (1, "./static/images/1.jpg", "Best Survival Tools for Preppers", "Tools", 1,1), + (2, "./static/images/2.jpg", "Prepare for Food Shortages", "Meal Prep", 1,1), + (3, "./static/images/3.jpg", "Amazing Survival Recipes", "Meal Prep", 1,1), + (4, "./static/images/4.jpg", "YOU NEED TO KNOW THESE LIFE HACKS!", "Hacks", 1,1), + (5, "./static/images/5.jpg", "Your emergency stockpile isnt complete without these 100 things", "Tools", 1,1), + (6, "./static/images/6.jpg", "World War 3 is Coming, are you Prepared??", "Tips", 1,1), + (7, "./static/images/7.jpg", "Want to Survive? Better read this..", "Tips", 1,1), + (8, "./static/images/8.jpg", "Clothes that Guarantee Survival", "Clothes", 1,1), + (9, "./static/images/9.jpg", "If you don'T have these in your pantry, uh oh", "Meal Prep", 1,1), + (10, "./static/images/10.jpg", "Rebuild after the apocalypse is over with these plants", "Gardening", 1,1), + (11, "./static/images/11.jpg", "Flowers will be worth millions soon, enjoy them now", "Gardening", 1,1) + + ] + + + self.cursor.executemany( + 'INSERT INTO images (id, url, caption, category, user_id, is_default) VALUES (?, ?, ?, ?, ?, ?)', + default_images + ) + + self.log("INFO - Inserted default images") + + # Insert default comments + default_comments = [ + (2, "helped, my bunker is stocked now", "2025-04-05 20:19:05"), + (2, "omg, can't wait to eat this", "2025-04-05 20:19:11"), + (4, "Great hacks!", "2025-04-05 20:36:40"), + (3, "I survived thanks to this post", "2025-04-05 20:36:57"), + (8, "very stylish 10/10", "2025-04-05 21:00:31") + ] + + self.cursor.executemany( + 'INSERT INTO comments (image_id, text, timestamp) VALUES (?, ?, ?)', + default_comments + ) + + self.log("INFO - Inserted default comments") + + def _import_default_saved_images(self): + """Import Andy's saved images from the provided JSON data""" + self.cursor.execute("SELECT id FROM users WHERE username = 'Andy'") + user = self.cursor.fetchone() + + if not user: + self.log("WARNING - Cannot import saved images, Andy user not found") + return + + user_id = user[0] + + self.cursor.execute("SELECT COUNT(*) FROM saved_images WHERE user_id = ?", (user_id,)) + count = self.cursor.fetchone()[0] + + if count > 0: + self.log("INFO - Andy already has saved images, skipping import") + return + + + saved_images = [(user_id, 9), (user_id, 10), (user_id, 3)] + + self.cursor.executemany( + 'INSERT INTO saved_images (user_id, image_id) VALUES (?, ?)', + saved_images + ) + + self.log(f"INFO - Imported {len(saved_images)} saved images for Andy") + + + def create_user(self, username, password, email=None): + """Create a new user""" + self.connect() + + # Hash the password + password_hash = hashlib.sha256(password.encode()).hexdigest() + + try: + self.cursor.execute( + 'INSERT INTO users (username, password_hash, email) VALUES (?, ?, ?)', + (username, password_hash, email) + ) + self.commit() + + # Get the user ID + self.cursor.execute("SELECT id FROM users WHERE username = ?", (username,)) + user_id = self.cursor.fetchone()[0] + + self.close() + return True, user_id + except sqlite3.IntegrityError: + self.close() + return False, "Username already exists" + + def authenticate_user(self, username, password): + """Authenticate a user""" + self.connect() + + # Hash the password + password_hash = hashlib.sha256(password.encode()).hexdigest() + + self.cursor.execute( + 'SELECT id FROM users WHERE username = ? AND password_hash = ?', + (username, password_hash) + ) + + user = self.cursor.fetchone() + self.close() + + if user: + return True, user[0] + return False, "Invalid username or password" + + def get_user(self, user_id): + """Get user by ID""" + self.connect() + + self.cursor.execute( + 'SELECT id, username, email, created_at FROM users WHERE id = ?', + (user_id,) + ) + + user = self.cursor.fetchone() + self.close() + + if user: + return dict(user) + return None + + + def get_all_images(self): + """Get all images (default + user uploaded)""" + self.connect() + + self.cursor.execute(''' + SELECT id, url, caption, category, user_id, is_default, created_at, classification_label, classification_confidence + FROM images + ORDER BY is_default DESC, id DESC + ''') + + images = [dict(row) for row in self.cursor.fetchall()] + self.close() + + return images + + def get_image_by_id(self, image_id): + """Get image by ID""" + self.connect() + + self.cursor.execute(''' + SELECT id, url, caption, category, user_id, is_default, created_at, image_data, + classification_label, classification_confidence + FROM images + WHERE id = ? + ''', (image_id,)) + + image = self.cursor.fetchone() + self.close() + + if image: + return dict(image) + return None + + def upload_image(self, url, caption, category, user_id=None, image_data=None, classification_label=None, classification_confidence=None): + """Upload a new image with classification data""" + self.connect() + + try: + self.cursor.execute( + 'INSERT INTO images (url, caption, category, user_id, image_data, classification_label, classification_confidence) VALUES (?, ?, ?, ?, ?, ?, ?)', + (url, caption, category, user_id, image_data, classification_label, classification_confidence) + ) + + image_id = self.cursor.lastrowid + + if classification_label and classification_confidence is not None: + self.cursor.execute( + 'INSERT INTO classification_logs (image_id, label, confidence) VALUES (?, ?, ?)', + (image_id, classification_label, classification_confidence) + ) + + self.log(f"INFO - New image ID {image_id} classified as '{classification_label}' with confidence {classification_confidence}") + + self.commit() + self.close() + + return image_id + except Exception as e: + self.log(f"ERROR - Failed to upload image: {e}") + self.close() + return None + + def update_image_classification(self, image_id, label, confidence): + """Update image with classification results""" + self.connect() + + try: + # Update the image record with classification + self.cursor.execute( + 'UPDATE images SET classification_label = ?, classification_confidence = ? WHERE id = ?', + (label, confidence, image_id) + ) + + # Log the classification + self.cursor.execute( + 'INSERT INTO classification_logs (image_id, label, confidence) VALUES (?, ?, ?)', + (image_id, label, confidence) + ) + + self.log(f"INFO - Image ID {image_id} classified as '{label}' with confidence {confidence}") + + self.commit() + self.close() + return True + except Exception as e: + self.log(f"ERROR - Failed to update image classification: {e}") + self.close() + return False + + def get_classification_logs(self, image_id=None, limit=50): + """Get classification logs, optionally filtered by image_id""" + self.connect() + + try: + if image_id: + self.cursor.execute( + '''SELECT cl.*, i.url, i.caption + FROM classification_logs cl + JOIN images i ON cl.image_id = i.id + WHERE cl.image_id = ? + ORDER BY cl.timestamp DESC LIMIT ?''', + (image_id, limit) + ) + else: + self.cursor.execute( + '''SELECT cl.*, i.url, i.caption + FROM classification_logs cl + JOIN images i ON cl.image_id = i.id + ORDER BY cl.timestamp DESC LIMIT ?''', + (limit,) + ) + + logs = [dict(row) for row in self.cursor.fetchall()] + self.close() + + return logs + except Exception as e: + self.log(f"ERROR - Failed to get classification logs: {e}") + self.close() + return [] + + def get_images_by_classification(self, classification_label, limit=20): + """Get images filtered by classification label""" + self.connect() + + try: + self.cursor.execute(''' + SELECT id, url, caption, category, user_id, is_default, created_at, + classification_label, classification_confidence + FROM images + WHERE classification_label = ? + ORDER BY created_at DESC + LIMIT ? + ''', (classification_label, limit)) + + images = [dict(row) for row in self.cursor.fetchall()] + self.close() + + return images + except Exception as e: + self.log(f"ERROR - Failed to get images by classification: {e}") + self.close() + return [] + + def get_comments(self, image_id): + """Get comments for an image""" + self.connect() + + self.cursor.execute(''' + SELECT c.id, c.image_id, c.user_id, c.text, c.timestamp, u.username + FROM comments c + LEFT JOIN users u ON c.user_id = u.id + WHERE c.image_id = ? + ORDER BY c.timestamp + ''', (image_id,)) + + comments = [dict(row) for row in self.cursor.fetchall()] + self.close() + + return comments + + def add_comment(self, image_id, text, user_id=None): + """Add a comment to an image""" + self.connect() + + self.cursor.execute( + 'INSERT INTO comments (image_id, user_id, text) VALUES (?, ?, ?)', + (image_id, user_id, text) + ) + + self.commit() + self.close() + + return True + + + def get_saved_images(self, user_id): + """Get saved images for a user""" + self.connect() + + self.cursor.execute(''' + SELECT i.* + FROM images i + JOIN saved_images s ON i.id = s.image_id + WHERE s.user_id = ? + ORDER BY s.saved_at DESC + ''', (user_id,)) + + images = [dict(row) for row in self.cursor.fetchall()] + self.close() + + return images + + def save_image_for_user(self, user_id, image_id): + """Save an image for a user""" + self.connect() + + try: + self.cursor.execute( + 'INSERT INTO saved_images (user_id, image_id) VALUES (?, ?)', + (user_id, image_id) + ) + + self.commit() + self.close() + return True + except sqlite3.IntegrityError: + # Image already saved + self.close() + return False + + def is_image_saved_by_user(self, user_id, image_id): + """Check if an image is saved by a user""" + self.connect() + + self.cursor.execute( + 'SELECT 1 FROM saved_images WHERE user_id = ? AND image_id = ?', + (user_id, image_id) + ) + + is_saved = self.cursor.fetchone() is not None + self.close() + + return is_saved + + def unsave_image_for_user(self, user_id, image_id): + """Remove an image from a user's saved images""" + self.connect() + + try: + self.cursor.execute( + 'DELETE FROM saved_images WHERE user_id = ? AND image_id = ?', + (user_id, image_id) + ) + + self.commit() + self.close() + return True + except Exception as e: + self.log(f"ERROR - Failed to unsave image: {e}") + self.close() + return False + + def get_saved_count(self, user_id): + """Get count of saved images for a user""" + self.connect() + + self.cursor.execute('SELECT COUNT(*) FROM saved_images WHERE user_id = ?', (user_id,)) + + count = self.cursor.fetchone()[0] + self.close() + + return count + + def get_uploaded_count(self, user_id): + """Get count of images uploaded by a user""" + self.connect() + + self.cursor.execute( + 'SELECT COUNT(*) FROM images WHERE user_id = ? AND is_default = 0', + (user_id,) + ) + + count = self.cursor.fetchone()[0] + self.close() + + return count + + def get_comment_count(self, user_id): + """Get count of comments posted by a user""" + self.connect() + + self.cursor.execute('SELECT COUNT(*) FROM comments WHERE user_id = ?', (user_id,)) + + count = self.cursor.fetchone()[0] + self.close() + + return count \ No newline at end of file diff --git a/Client/image_manager.py b/Client/image_manager.py index 85f6106..f11534b 100644 --- a/Client/image_manager.py +++ b/Client/image_manager.py @@ -1,6 +1,13 @@ import base64 import os import json +import sys + +#importiing the load an predict function from Image Classifier Branch (Alexa's Branch) +# Adds the project root to sys.path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from Image_Classifier.Image_Classifier.deployment import load_and_predict + class ImageManager: @@ -43,13 +50,21 @@ def upload_image(self, image_file, caption, tags): with open(image_path, 'wb') as f: f.write(image_content) + + base_dir = os.path.dirname(os.path.abspath(__file__)) # path to image_manager.py + model_path = os.path.join(base_dir, "../Image_Classifier/Image_Classifier/imageclassifierHS_Updated.h5") + model_path = os.path.normpath(model_path) + + + image_path = f"./static/uploads/{image_filename}" + predicted_category = load_and_predict(model_path, image_path) #new image dictionary for the users newly uploaded image uploaded_image = { 'id': len(self.uploaded_images) + 1, 'url': f"./static/uploads/{image_filename}", 'caption': caption, - 'category': tags, + 'category': [tags, predicted_category], 'image': base64_image } diff --git a/Client/preppersdb.sqlite b/Client/preppersdb.sqlite new file mode 100644 index 0000000..ec93c05 Binary files /dev/null and b/Client/preppersdb.sqlite differ diff --git a/Client/saved_images.json b/Client/saved_images.json index 9e891d2..1960e08 100644 --- a/Client/saved_images.json +++ b/Client/saved_images.json @@ -17,6 +17,12 @@ "url": "./static/images/3.jpg", "caption": "Amazing Survival Recipes", "category": "Meal Prep" + }, + { + "id": 6, + "url": "./static/images/6.jpg", + "caption": "World War 3 is Coming, are you Prepared??", + "category": "Tips" } ] } \ No newline at end of file diff --git a/Client/static/css/style.css b/Client/static/css/style.css index c116bf7..35aac89 100644 --- a/Client/static/css/style.css +++ b/Client/static/css/style.css @@ -1,60 +1,94 @@ + +body { + background-color: #f5f1e8; +} + +/* Container styling */ +.container.mt-4 { + background-color: #f9f7f3; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.03); +} + + +.navbar { + background-color: #e8ded1 !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + margin-bottom: 20px; +} + + .card { margin-bottom: 20px; border-radius: 16px; overflow: hidden; transition: transform 0.3s ease; max-width: 300px; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); } + .card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.1); } + .card-img-top { width: 100%; object-fit: cover; } + +/* Masonry Grid Layout */ .masonry-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); grid-gap: 15px; grid-auto-flow: dense; } -.navbar { - margin-bottom: 20px; -} -.form-group { - color:white; -} -.login{ - background-size: cover; +.login { + background-size: cover; background-image: url('../images/bg.jpg'); font-size: large; font-weight: bold; height: 100%; + + background-color: transparent; } -.formbg{ +body.login-page { + + background-color: transparent; +} + +.form-group { + color: white; +} + +.formbg { color: white; width: 75%; } -.logpg{ +.logpg { color: white; background-color: rgba(14, 13, 13, 0.5); } - - -/* upload button styling */ +.login-container { + background-color: rgba(14, 13, 13, 0.7); + border-radius: 8px; + padding: 30px; + color: white; +} +/* upload */ #upload-button-container { position: fixed; bottom: 0; right: 0; z-index: 1050; } - #upload-button { position: fixed; bottom: 30px; @@ -69,7 +103,6 @@ transition: transform 0.3s; } -/* for animated effect like pinterest */ #upload-button:hover { transform: scale(1.1); } @@ -80,21 +113,105 @@ border-radius: 4px; } - - -/* comment modal */ +/* comment */ #modalImage { max-height: 600px; - width: 100%; + width: 100%; object-fit: contain; - } +} - .comment-modal { - max-width: 800px; - width: 80%; - } +.comment-modal { + max-width: 800px; + width: 80%; +} -.comment{ +.comment { background-color: rgba(143, 52, 229, 0.144); border-radius: 10px; -} \ No newline at end of file +} + +/* Button */ +.btn-sm { + border-radius: 20px; + padding: 0.25rem 0.8rem; +} + +.btn-outline-success:hover { + background-color: #28a745; + border-color: #28a745; +} + +/* Profile Page*/ +.profile-image { + width: 200px; + height: 200px; + object-fit: cover; + border-radius: 50%; + border: 5px solid #fff; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); + margin-bottom: 20px; + transition: transform 0.3s; +} + +.profile-image:hover { + transform: scale(1.05); +} + +.profile-card { + text-align: center; + padding: 20px; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0,0,0,0.1); + background-color: #fff; + margin-bottom: 20px; +} + +.stat-card { + border-radius: 10px; + padding: 15px; + margin-bottom: 15px; + background-color: #f8f9fa; + transition: transform 0.3s, box-shadow 0.3s; +} + +.stat-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0,0,0,0.1); +} + +.stat-number { + font-size: 2.5rem; + font-weight: bold; + color: #198754; +} + +.stat-label { + color: #6c757d; + font-size: 1rem; +} + +.user-info { + margin: 20px 0; +} + +.user-info-item { + display: flex; + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.user-info-label { + font-weight: bold; + min-width: 120px; +} + +.action-buttons { + margin-top: 20px; +} + +.action-buttons .btn { + margin: 0 5px; + padding: 8px 20px; +} + diff --git a/Client/static/js/home-filter.js b/Client/static/js/home-filter.js new file mode 100644 index 0000000..eae845e --- /dev/null +++ b/Client/static/js/home-filter.js @@ -0,0 +1,68 @@ +// home-filter.js -category filtering for home page and vault page + +document.addEventListener('DOMContentLoaded', function() { + + const filterButtons = document.querySelectorAll('[data-filter]'); + + filterButtons.forEach(button => { + button.addEventListener('click', function() { + const category = this.getAttribute('data-filter'); + + + filterButtons.forEach(btn => { + btn.classList.remove('active'); + }); + + + this.classList.add('active'); + + if (category === 'all') { + showAllPosts(); + } else { + filterByCategory(category); + } + }); + }); +}); + +/** + * Show posts from a specific category + */ +function filterByCategory(category) { + const cards = document.querySelectorAll('.card'); + cards.forEach(card => { + const cardCategory = card.querySelector('.card-text').textContent; + if (cardCategory === category) { + card.style.display = 'block'; + } else { + card.style.display = 'none'; + } + }); + + + relayoutMasonry(); +} +/* Show all posts*/ +function showAllPosts() { + const cards = document.querySelectorAll('.card'); + cards.forEach(card => { + card.style.display = 'block'; + }); + + // Re-layout masonry + relayoutMasonry(); +} + +/** + * Re-layout masonry grid + */ +function relayoutMasonry() { + const grid = document.querySelector('#masonry-grid'); + if (grid) { + + setTimeout(() => { + const msnry = new Masonry(grid); + msnry.layout(); + }, 100); + } +} \ No newline at end of file diff --git a/Client/static/js/image-grid.js b/Client/static/js/image-grid.js new file mode 100644 index 0000000..7f2a195 --- /dev/null +++ b/Client/static/js/image-grid.js @@ -0,0 +1,49 @@ +document.addEventListener('DOMContentLoaded', function () { + //masonry info: https://masonry.desandro.com/ + //to make a grid, pinterest style + var grid = document.querySelector('#masonry-grid'); + var masonry = new Masonry(grid, { + itemSelector: '.card', + columnWidth: '.card', + percentPosition: true, + gutter: 15 + }); + + //layout after loading images + var imgLoad = imagesLoaded(grid); + imgLoad.on('progress', function () { + masonry.layout(); + }); +}); + +//when clicking the save image button on a specific image +document.addEventListener("DOMContentLoaded", function() { + const saveButtons = document.querySelectorAll(".save-btn"); + + saveButtons.forEach(button => { + button.addEventListener("click", function() { + const imageId = this.getAttribute("data-id"); + + fetch('/save_image', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ image_id: imageId }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Image saved to your vault!!'); + } else { + alert('Couldnt save the image, please try again later.' + data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Couldnt save the image, please try again later.'); + }); + }); + }); +}); + diff --git a/Client/static/js/profile.js b/Client/static/js/profile.js new file mode 100644 index 0000000..e5079c3 --- /dev/null +++ b/Client/static/js/profile.js @@ -0,0 +1,34 @@ + +document.addEventListener('DOMContentLoaded', function() { + + initProfilePicture(); + initProfileStats(); +}); + +/** + * Profile picture change no point in integrating really + */ +function initProfilePicture() { + const changeProfileBtn = document.getElementById('change-profile-btn'); + if (changeProfileBtn) { + changeProfileBtn.addEventListener('click', function() { + alert('Profile picture upload functionality will be added soon!'); + }); + } +} + +/** profile stats*/ +function initProfileStats() { + const statCards = document.querySelectorAll('.stat-card'); + if (statCards.length) { + statCards.forEach(card => { + card.addEventListener('mouseenter', function() { + this.classList.add('shadow'); + }); + + card.addEventListener('mouseleave', function() { + this.classList.remove('shadow'); + }); + }); + } +} \ No newline at end of file diff --git a/Client/static/uploads/das.jpg b/Client/static/uploads/das.jpg new file mode 100644 index 0000000..9d0c52d Binary files /dev/null and b/Client/static/uploads/das.jpg differ diff --git a/Client/static/uploads/test44.jpg b/Client/static/uploads/test44.jpg new file mode 100644 index 0000000..15f5426 Binary files /dev/null and b/Client/static/uploads/test44.jpg differ diff --git a/Client/tcp_client.py b/Client/tcp_client.py index fd643f9..a96ec3d 100644 --- a/Client/tcp_client.py +++ b/Client/tcp_client.py @@ -4,16 +4,26 @@ import hashlib import time -# Logging structure +# Logging structure for btoh clent and server logging.basicConfig( filename='client_log.txt', filemode='w', level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + format='%(asctime)s - TCPClient - %(levelname)s - %(message)s' ) logger = logging.getLogger('TCPClient') +# tried to add a filter to exclude static file requests from logs(kind of works) +class FilterStaticRequests(logging.Filter): + def filter(self, record): + if hasattr(record, 'msg') and isinstance(record.msg, str): + if '/static/' in record.msg or 'GET /static' in record.msg: + return False + return True + +logger.addFilter(FilterStaticRequests()) + class TCPClient: """ TCP/IP Socket client for communicating with the server @@ -56,7 +66,7 @@ def send_request(self, command, data): if not self.connected: if not self.connect(): return False, "Failed to connect to server" - + # Create request packet packet = self.create_packet(command, data) @@ -64,16 +74,35 @@ def send_request(self, command, data): try: packet_json = json.dumps(packet) self.socket.sendall(packet_json.encode('utf-8')) - logger.info(f"Sent request: {command} with {len(packet_json)} bytes") - # Wait for response + + logger.info(f"SENDING REQUEST TO SERVER: Command={command}") + + # For important commands it will log more details + if command in ["LOGIN", "REGISTER", "LOGOUT", "SAVE_IMAGE", "UNSAVE_IMAGE", "UPLOAD_IMAGE", "ADD_COMMENT"]: + if command == "LOGIN": + logger.info(f"User '{data.get('username')}' logging in") + elif command == "REGISTER": + logger.info(f"New user '{data.get('username')}' registering") + elif command == "LOGOUT": + logger.info(f"User logging out") + elif command == "SAVE_IMAGE": + logger.info(f"Saving image ID: {data.get('image_id')}") + elif command == "UNSAVE_IMAGE": + logger.info(f"Removing saved image ID: {data.get('image_id')}") + elif command == "UPLOAD_IMAGE": + logger.info(f"Uploading new image with caption: {data.get('caption')}") + elif command == "ADD_COMMENT": + logger.info(f"Adding comment to image ID: {data.get('image_id')}") + + # Wait for response from the server to then log it response = self.receive_response() if response: - logger.info(f"Received response: {response['body']['command']}") + logger.info(f"RECEIVED RESPONSE FROM SERVER: {response['body']['command']}") return True, response else: - logger.warning("No response from server") + logger.warning("No response received from server") return False, "No response from server" except Exception as e: @@ -127,15 +156,15 @@ def receive_response(self, timeout=10): logger.warning("Received empty response from server") return None - # Parse response + response = json.loads(data) - # Valid8 header + # Validate header if 'header' not in response: logger.warning("Invalid response format - missing header") return None - # Valid8 checksum + # Validate checksum if 'footer' in response and 'checksum' in response['footer']: if not self.validate_checksum(json.dumps(response['body']), response['footer']['checksum']): logger.warning("Response checksum validation failed") @@ -154,29 +183,4 @@ def receive_response(self, timeout=10): return None except Exception as e: logger.error(f"Error receiving response: {str(e)}") - return None - -# Example ( testing) -if __name__ == "__main__": - # Create client - client = TCPClient() - - # Connect to server - if not client.connect(): - print("Failed to connect to server") - exit(1) - - # Send a test request - success, response = client.send_request("TEST", { - "message": "Hello, server!", - "timestamp": time.time() - }) - - if success: - print("Request successful!") - print(f"Response: {response['body']['data'].get('message', 'No message')}") - else: - print(f"Request failed: {response}") - - - client.disconnect() \ No newline at end of file + return None \ No newline at end of file diff --git a/Client/templates/base.html b/Client/templates/base.html index 9923276..2032c4e 100644 --- a/Client/templates/base.html +++ b/Client/templates/base.html @@ -10,12 +10,13 @@ {% block title %}Aftermath Network - Home{% endblock %} + {% block extra_css %}{% endblock %} -