diff --git a/README.md b/README.md new file mode 100644 index 0000000..6761f47 --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# Projektor pesmi (Song Projector) + +A Tkinter-based song projector application with optional Flask web interface for remote control. + +## Features + +- **Tkinter GUI**: Full-screen song display with customizable fonts, colors, and sizing +- **Song Database**: SQLite database with lyrics and metadata +- **Web Interface** (optional): Control the projector via web browser + - Load songs by number or search by title + - Navigate between lyrics pages + - Toggle uppercase display + - Clear/dim the screen +- **Keyboard Shortcuts**: Quick navigation and screen control +- **Settings**: JSON-based configuration for fonts, colors, and display options + +## Project Structure + +``` +Projekcija/ +├── projector.py # Main Tkinter application +├── nastavitve.py # Settings defaults and initialization +├── add_song.py # Utility to import songs into database +├── songs.db # SQLite database with lyrics +├── settings.json # User configuration +├── web/ # Flask web server package +│ ├── __init__.py +│ ├── server.py # Flask app and API endpoints +│ ├── static/ # Web assets +│ │ ├── styles.css +│ │ └── script.js +│ └── templates/ +│ └── index.html +├── tests/ # Test suite +│ ├── test_api.py +│ ├── test_integration.py +│ └── test_mock.py +└── startup.bat # Windows batch launcher +``` + +## Installation + +### Requirements +- Python 3.8+ +- Flask (for web interface) +- sqlite3 (included with Python) + +### Setup + +```bash +# Install Flask +pip install flask + +# Create or import songs into database (optional) +python add_song.py +``` + +## Usage + +### Launch the Application + +```bash +python projector.py +``` + +Or use the batch file: +```bash +startup.bat +``` + +### Keyboard Shortcuts + +- **Enter**: Load song by number (when typing in keyboard mode) +- **+** (plus): Clear/dim the screen +- **-** (minus): Previous page +- **/** (slash): Search songs by title + +### Configuration + +Edit `settings.json` to customize: +- Font name, size, and color +- Background color +- Display width percentage +- **web_port**: Set to a port number (e.g., 5000) to enable web interface, or 0 to disable + +Example `settings.json`: +```json +{ + "font_name": "Times New Roman", + "font_size": 32, + "fg_color": "#FFFFFF", + "bg_color": "#000000", + "screen_width_percent": 60, + "font_bold": true, + "web_port": 5000 +} +``` + +### Web Interface + +If `web_port` is configured and > 0, the web interface opens automatically: + +``` +http://127.0.0.1:/ +``` + +Features: +- **Load by Number**: Input song ID and press "Naloži" +- **Search**: Find songs by keyword +- **Navigation**: Next/Previous buttons to flip pages +- **Case Toggle**: "velikE črke" button for uppercase +- **Clear Screen**: "Zatemniti ekran" button to dim display + +## Testing + +Run the integration test to verify the web server: + +```bash +python -m tests.test_integration +``` + +This starts a mock projector, launches the Flask server, and exercises all API endpoints. + +## Architecture + +### Database Schema +- `songs` table: `id, title, lyrics` +- Read-only mode (no modifications via web interface) + +### API Endpoints +- `GET /` – HTML interface +- `GET /api/state` – Current projector state (text, page, navigation flags) +- `POST /api/load_song` – Load a song +- `POST /api/next_page` – Next page +- `POST /api/prev_page` – Previous page +- `POST /api/clear_screen` – Clear display +- `POST /api/toggle_caps` – Toggle uppercase +- `POST /api/search_songs` – Find songs by title + +### Threading +- Web server runs in a daemon thread +- Safe SQLite access with `check_same_thread=False` + +## Notes + +- The web interface is optional; disable it by setting `web_port: 0` in settings +- HTML, CSS, and JavaScript are stored in separate files under `web/` +- For production deployment, use a WSGI server (Gunicorn, uWSGI) instead of Flask development server +- Songs database is read-only via web interface; add songs using admin tools or direct SQL + +## License + +[Add your license here] diff --git a/nastavitve.py b/nastavitve.py index d5c938f..8f98088 100755 --- a/nastavitve.py +++ b/nastavitve.py @@ -15,7 +15,8 @@ DEFAULT_SETTINGS = { "screen_width_percent": 60, "font_bold": True, "show_song_info": True, - "split_by_stanza": False + "split_by_stanza": False, + "web_port": 5000 } SETTINGS_FILE = "settings.json" diff --git a/pesmi_export.txt b/pesmi_export.txt index 43b4a68..ff0e716 100644 --- a/pesmi_export.txt +++ b/pesmi_export.txt @@ -1,5 +1,27 @@ === 1 +Prva Pesem +Bela črka sredi teme, +zdaj prebija mrak dvorane, +da rešila bi dileme, +ko beseda na zid plane. + +Črtica, šiv in žebelj ostri, +preizkus za vse znake tiste, +ki v jeziku so nam blizu, +jasne, svetle in pa čiste. + +Vrstica dolga se razteza, +čez rob platna skoraj steče, +a oko jo hitro ujame, +ko se v nov slide preobleče. + +Ena, dve in tri, štiri, +test končuje svojo pot, +slika mirno naj se umiri, +brez napak in brez zmot. +=== +2 Zdrava Marija Zdrava, Marija, milosti polna, Gospod je s Teboj, blagoslovljena si med ženami diff --git a/projector.py b/projector.py index 6559920..8e9a526 100755 --- a/projector.py +++ b/projector.py @@ -9,6 +9,7 @@ import math import subprocess import sys import tkinter.messagebox as messagebox +from web.server import start_server_thread DB_PATH = 'songs.db' SETTINGS_PATH = 'settings.json' @@ -22,7 +23,8 @@ class SongProjector: # ... try: # Odpri read-only; ne bo ustvaril prazne baze, če datoteka manjka - self.conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True) + # check_same_thread=False omogoča uporabo v večih nitih + self.conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True, check_same_thread=False) self.cursor = self.conn.cursor() except sqlite3.OperationalError as e: # Jasno sporočilo in varen izhod @@ -143,6 +145,17 @@ class SongProjector: self.clear_screen() + # -------------------------------------------------- + # Zagon Flask web serverja (če je port nastavljen) + # -------------------------------------------------- + web_port = self.settings.get("web_port", 0) + if web_port and web_port > 0: + try: + start_server_thread(self, host='127.0.0.1', port=web_port) + print(f"Web server zažet na http://127.0.0.1:{web_port}") + except Exception as e: + print(f"Napaka pri zagonu web servera: {e}") + # ------------------------------------------------------ # NOVA METODA: enakomeren prelom predolgih vrstic # ------------------------------------------------------ @@ -370,12 +383,16 @@ class SongProjector: # Očisti zaslon # ------------------------------------------------------ def clear_screen(self, event=None): + # odstranimo vse strani, da se tudi API posodobi + self.pages = [] + self.current_page_index = 0 + self.waiting_for_song = True + self.label.config(text="") self.label.pack_forget() self.color_frame.config(bg="black", width=self.color_width, height=self.screen_height) self.color_frame.place(relx=0.5, rely=0.5, anchor="center") self.song_info_label.config(text="") - self.waiting_for_song = True # ------------------------------------------------------ # Iskanje pesmi diff --git a/settings.json b/settings.json index f33cd86..70e4aae 100644 --- a/settings.json +++ b/settings.json @@ -2,9 +2,10 @@ "font_name": "Times New Roman", "bg_color": "#000000", "fg_color": "#FFFFFF", - "font_size": 32, + "font_size": 36, "screen_width_percent": 60, "font_bold": true, "show_song_info": true, - "split_by_stanza": false + "split_by_stanza": false, + "web_port": 5000 } \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..e7267d8 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +import time +import json + +BASE_URL = 'http://127.0.0.1:5000' + +def test_api(): + """Testiraj web API""" + + print("=" * 60) + print("Testiranje Web API-ja") + print("=" * 60) + + # Testiranje GET /api/state + print("\n1. Test GET /api/state (početno stanje)") + try: + response = requests.get(f'{BASE_URL}/api/state') + data = response.json() + print(f" Status: {response.status_code}") + print(f" Current text: {data.get('current_text', '')[:50]}...") + print(f" Caps mode: {data.get('caps_mode')}") + print(" ✓ PASS") + except Exception as e: + print(f" ✗ FAIL: {e}") + + # Testiranje POST /api/load_song + print("\n2. Test POST /api/load_song (broj 1)") + try: + response = requests.post( + f'{BASE_URL}/api/load_song', + json={'song_number': '1'} + ) + print(f" Status: {response.status_code}") + data = response.json() + print(f" Response: {data}") + + # Pričekaj da se pesma naloži + time.sleep(0.5) + + # Provjeri stanje + response = requests.get(f'{BASE_URL}/api/state') + state = response.json() + print(f" Current text: {state.get('current_text', '')[:50]}...") + print(f" Can next: {state.get('can_next')}") + print(f" Can prev: {state.get('can_prev')}") + print(" ✓ PASS") + except Exception as e: + print(f" ✗ FAIL: {e}") + + # Testiranje POST /api/next_page + print("\n3. Test POST /api/next_page") + try: + response = requests.post(f'{BASE_URL}/api/next_page') + print(f" Status: {response.status_code}") + + # Provjeri stanje + time.sleep(0.3) + response = requests.get(f'{BASE_URL}/api/state') + state = response.json() + print(f" Page info: {state.get('page_info')}") + print(" ✓ PASS") + except Exception as e: + print(f" ✗ FAIL: {e}") + + # Testiranje POST /api/prev_page + print("\n4. Test POST /api/prev_page") + try: + response = requests.post(f'{BASE_URL}/api/prev_page') + print(f" Status: {response.status_code}") + + # Provjeri stanje + time.sleep(0.3) + response = requests.get(f'{BASE_URL}/api/state') + state = response.json() + print(f" Page info: {state.get('page_info')}") + print(" ✓ PASS") + except Exception as e: + print(f" ✗ FAIL: {e}") + + # Testiranje POST /api/toggle_caps + print("\n5. Test POST /api/toggle_caps") + try: + response = requests.post(f'{BASE_URL}/api/toggle_caps') + print(f" Status: {response.status_code}") + + time.sleep(0.3) + response = requests.get(f'{BASE_URL}/api/state') + state = response.json() + print(f" Caps mode: {state.get('caps_mode')}") + print(f" Text (caps): {state.get('current_text', '')[:50]}...") + print(" ✓ PASS") + except Exception as e: + print(f" ✗ FAIL: {e}") + + # Testiranje POST /api/clear_screen + print("\n6. Test POST /api/clear_screen") + try: + response = requests.post(f'{BASE_URL}/api/clear_screen') + print(f" Status: {response.status_code}") + + time.sleep(0.3) + response = requests.get(f'{BASE_URL}/api/state') + state = response.json() + print(f" Current text: {state.get('current_text', '')[:50]}...") + print(" ✓ PASS") + except Exception as e: + print(f" ✗ FAIL: {e}") + + # Testiranje POST /api/search_songs + print("\n7. Test POST /api/search_songs") + try: + response = requests.post( + f'{BASE_URL}/api/search_songs', + json={'query': 'pesem'} + ) + print(f" Status: {response.status_code}") + data = response.json() + results = data.get('results', []) + print(f" Rezultati: {results}") + print(" ✓ PASS") + except Exception as e: + print(f" ✗ FAIL: {e}") + + # Testiranje GET / + print("\n8. Test GET / (HTML stranicu)") + try: + response = requests.get(f'{BASE_URL}/') + print(f" Status: {response.status_code}") + print(f" HTML length: {len(response.text)} bytes") + print(f" Contains 'Flask': {'Flask' in response.text}") + print(" ✓ PASS") + except Exception as e: + print(f" ✗ FAIL: {e}") + + print("\n" + "=" * 60) + print("Testiranje završeno!") + print("=" * 60) + +if __name__ == "__main__": + test_api() diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..e126acd --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Integracijski test Flask servera s mock aplikacijom +""" + +import sqlite3 +import json +import sys +import os +import time +import threading +import requests +from web.server import set_projector_app, run_server + +# Simuliraj SongProjector stanu +class MockSongProjector: + def __init__(self): + self.current_song = None + self.pages = [] + self.current_page_index = 0 + self.song_number = "" + self.song_number_last = "" + self.all_caps_mode = False + self.waiting_for_song = True + + # Otvori bazu + try: + self.conn = sqlite3.connect(f"file:songs.db?mode=ro", uri=True, check_same_thread=False) + self.cursor = self.conn.cursor() + except: + self.conn = None + self.cursor = None + + # Čitaj settings + with open('settings.json', 'r', encoding='utf-8') as f: + self.settings = json.load(f) + + def load_song(self): + """Simulacija load_song metode""" + if not self.song_number or not self.cursor: + return + + song_number_to_load = self.song_number # Spremi broj prije nego što ga obriši + + try: + song_id = int(song_number_to_load) + self.cursor.execute("SELECT lyrics FROM songs WHERE id= ?", (song_id,)) + result = self.cursor.fetchone() + if result: + lyrics = result[0] + # Jednostavna podijela pesme na samo jednu stranicu za test + self.pages = [lyrics] + self.current_page_index = 0 + self.waiting_for_song = False + self.song_number_last = song_number_to_load + else: + self.pages = [] + self.waiting_for_song = True + except: + self.pages = [] + finally: + self.song_number = "" + + def next_page(self): + if self.pages and self.current_page_index + 1 < len(self.pages): + self.current_page_index += 1 + + def prev_page(self): + if self.pages and self.current_page_index > 0: + self.current_page_index -= 1 + + def clear_screen(self): + self.pages = [] + self.current_page_index = 0 + self.waiting_for_song = True + + def show_page(self): + pass # Dummy + + def toggle_caps(self): + self.all_caps_mode = not self.all_caps_mode + self.show_page() + +# Kreiraj mock aplikaciju +print("Iniciijalisiranje mock aplikacije...") +mock_app = MockSongProjector() +set_projector_app(mock_app) + +# Pokreni Flask server u zvjenoj niti +print("Pokretanje Flask servera...") +web_port = mock_app.settings.get('web_port', 5000) +server_thread = threading.Thread( + target=run_server, + args=('127.0.0.1', web_port), + daemon=True +) +server_thread.start() + +# Čekaj da se server pokreće +time.sleep(2) + +print("=" * 60) +print("Test Flask API-ja") +print("=" * 60) + +BASE_URL = f'http://127.0.0.1:{web_port}' + +try: + print("\n1. GET / (HTML stranica)") + response = requests.get(f'{BASE_URL}/') + print(f" Status: {response.status_code}") + print(f" HTML size: {len(response.text)} bytes") + assert response.status_code == 200, f"Expected 200, got {response.status_code}" + print(" PASS") + + print("\n2. GET /api/state (initial state)") + response = requests.get(f'{BASE_URL}/api/state') + data = response.json() + print(f" Status: {response.status_code}") + print(f" Current text: {data.get('current_text', '')[:50]}...") + assert response.status_code == 200 + print(" PASS") + + print("\n3. POST /api/load_song (song 1)") + response = requests.post( + f'{BASE_URL}/api/load_song', + json={'song_number': '1'} + ) + print(f" Status: {response.status_code}") + time.sleep(0.5) + + response = requests.get(f'{BASE_URL}/api/state') + state = response.json() + print(f" Text loaded: {state.get('current_text', '')[:50]}...") + assert 'Prva linija' in state.get('current_text', ''), "Song not loaded" + print(" PASS") + + print("\n4. POST /api/toggle_caps") + response = requests.post(f'{BASE_URL}/api/toggle_caps') + print(f" Status: {response.status_code}") + time.sleep(0.3) + + response = requests.get(f'{BASE_URL}/api/state') + state = response.json() + print(f" Caps mode: {state.get('caps_mode')}") + print(f" Text (caps): {state.get('current_text', '')[:50]}...") + print(" PASS") + + print("\n5. POST /api/clear_screen") + response = requests.post(f'{BASE_URL}/api/clear_screen') + print(f" Status: {response.status_code}") + time.sleep(0.3) + + response = requests.get(f'{BASE_URL}/api/state') + state = response.json() + print(f" Pages cleared: {len(mock_app.pages) == 0}") + print(f" Current text after clear: '{state.get('current_text')}'") + assert state.get('current_text', '').startswith('Pripravljeno'), "Clear screen didn't update state" + print(" PASS") + + print("\n6. POST /api/search_songs") + response = requests.post( + f'{BASE_URL}/api/search_songs', + json={'query': 'pesem'} + ) + data = response.json() + print(f" Status: {response.status_code}") + print(f" Results: {data.get('results', [])}") + print(" PASS") + + print("\n" + "=" * 60) + print("ALL TESTS PASSED!") + print("=" * 60) + print(f"\nFlask server available at: http://127.0.0.1:{web_port}") + +except AssertionError as e: + print(f" FAIL: {e}") + sys.exit(1) +except Exception as e: + print(f" FAIL: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/test_mock.py b/tests/test_mock.py new file mode 100644 index 0000000..b4bd18f --- /dev/null +++ b/tests/test_mock.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Test Flask servera brez Tkinter aplikacije +""" + +import sqlite3 +import json +import sys +import os + +# Simuliraj SongProjector stanu +class MockSongProjector: + def __init__(self): + self.current_song = None + self.pages = [] + self.current_page_index = 0 + self.song_number = "" + self.song_number_last = "" + self.all_caps_mode = False + self.waiting_for_song = True + + # Otvori bazu + try: + self.conn = sqlite3.connect(f"file:songs.db?mode=ro", uri=True) + self.cursor = self.conn.cursor() + except: + self.conn = None + self.cursor = None + + # Čitaj settings + with open('settings.json', 'r', encoding='utf-8') as f: + self.settings = json.load(f) + + def load_song(self): + """Simulacija load_song metode""" + if not self.song_number or not self.cursor: + return + + try: + song_id = int(self.song_number) + self.cursor.execute("SELECT lyrics FROM songs WHERE id=?", (song_id,)) + result = self.cursor.fetchone() + if result: + lyrics = result[0] + self.pages = [lyrics] # Jednostavno - brez stranica + self.current_page_index = 0 + self.waiting_for_song = False + self.song_number_last = self.song_number + else: + self.pages = [] + self.waiting_for_song = True + except: + self.pages = [] + finally: + self.song_number = "" + + def next_page(self): + if self.pages and self.current_page_index + 1 < len(self.pages): + self.current_page_index += 1 + + def prev_page(self): + if self.pages and self.current_page_index > 0: + self.current_page_index -= 1 + + def clear_screen(self): + self.pages = [] + self.current_page_index = 0 + self.waiting_for_song = True + + def show_page(self): + pass # Dummy + +# Kreiraj mock aplikacijo +app_mock = MockSongProjector() + +# Testiraj +print("=" * 60) +print("Test MockSongProjector") +print("=" * 60) + +print("\n1. Početno stanje:") +print(f" Pages: {len(app_mock.pages)}") +print(f" Current page: {app_mock.current_page_index}") +print(f" Waiting for song: {app_mock.waiting_for_song}") + +print("\n2. Naloži pesmu broj 1:") +app_mock.song_number = "1" +app_mock.load_song() +print(f" Pages: {len(app_mock.pages)}") +print(f" Current page: {app_mock.current_page_index}") +print(f" Waiting for song: {app_mock.waiting_for_song}") +print(f" Text: {(app_mock.pages[0] if app_mock.pages else 'N/A')[:50]}...") + +print("\n3. Očisti ekran:") +app_mock.clear_screen() +print(f" Pages: {len(app_mock.pages)}") +print(f" Waiting for song: {app_mock.waiting_for_song}") + +print("\n4. Web konfiguracija:") +print(f" Web port: {app_mock.settings.get('web_port', 'nije postavljeno')}") + +print("\n" + "=" * 60) +print("Svi testovi su prošli!") +print("=" * 60) diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..6737149 --- /dev/null +++ b/web/__init__.py @@ -0,0 +1,5 @@ +"""Package for web-related modules.""" + +from .server import app, set_projector_app, start_server_thread, run_server + +__all__ = ["app", "set_projector_app", "start_server_thread", "run_server"] \ No newline at end of file diff --git a/web/server.py b/web/server.py new file mode 100644 index 0000000..7489235 --- /dev/null +++ b/web/server.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""Flask web server for the song projector application. + +HTML, CSS and JavaScript are now stored in separate template/static files rather +than being generated inside Python code. +""" + +import threading +import os + +from flask import Flask, render_template, request, jsonify + +# create Flask app with proper folders relative to this file +app = Flask(__name__, static_folder="static", template_folder="templates") + +# Globalna referenca na SongProjector aplikaciju +_projector_app = None + + +def set_projector_app(projector_app): + """Postavi referenco na SongProjector aplikacijo""" + global _projector_app + _projector_app = projector_app + + +# ---------------------------------------------------------- +# Flask rute +# ---------------------------------------------------------- + +@app.route("/", methods=["GET"]) +def index(): + """Prikaže glavno HTML stranko""" + # predaja nadzora Jinja2, ki poišče web/templates/index.html + return render_template("index.html") + + +@app.route('/api/state', methods=['GET']) +def get_state(): + """Vrati trenutno stanje aplikacije""" + if _projector_app is None: + return jsonify({ + 'current_text': 'Napaka: Aplikacija ni inicijalizirana', + 'page_info': '', + 'caps_mode': False, + 'can_prev': False, + 'can_next': False + }) + + current_text = "" + page_info = "" + can_prev = False + can_next = False + + if _projector_app.pages: + current_text = _projector_app.pages[_projector_app.current_page_index] + if _projector_app.all_caps_mode: + current_text = current_text.upper() + + current_page = _projector_app.current_page_index + 1 + total_pages = len(_projector_app.pages) + page_info = f"{_projector_app.song_number_last} {current_page}/{total_pages}" + + can_prev = _projector_app.current_page_index > 0 + can_next = _projector_app.current_page_index + 1 < len(_projector_app.pages) + else: + current_text = "Pripravljeno. Vpiši številko pesmi." + + return jsonify({ + 'current_text': current_text, + 'page_info': page_info, + 'caps_mode': _projector_app.all_caps_mode, + 'can_prev': can_prev, + 'can_next': can_next + }) + + +@app.route('/api/load_song', methods=['POST']) +def load_song(): + """Naloži pesem po številki""" + if _projector_app is None: + return jsonify({'status': 'error', 'message': 'Aplikacija ni inicijalizirana'}) + + data = request.get_json() + song_number = data.get('song_number', '').strip() + + if song_number: + _projector_app.song_number = song_number + _projector_app.load_song() + if hasattr(_projector_app, 'show_page'): + _projector_app.show_page() + + return jsonify({'status': 'ok'}) + + +@app.route('/api/next_page', methods=['POST']) +def next_page(): + """Naslednja stran""" + if _projector_app is not None: + _projector_app.next_page() + + return jsonify({'status': 'ok'}) + + +@app.route('/api/prev_page', methods=['POST']) +def prev_page(): + """Prejšnja stran""" + if _projector_app is not None: + _projector_app.prev_page() + + return jsonify({'status': 'ok'}) + + +@app.route('/api/clear_screen', methods=['POST']) +def clear_screen(): + """Zatemniti ekran""" + if _projector_app is not None: + _projector_app.clear_screen() + + return jsonify({'status': 'ok'}) + + +@app.route('/api/toggle_caps', methods=['POST']) +def toggle_caps(): + """Preklop med velikimi in malimi črkami""" + if _projector_app is not None: + _projector_app.all_caps_mode = not _projector_app.all_caps_mode + _projector_app.show_page() + + return jsonify({'status': 'ok'}) + + +@app.route('/api/search_songs', methods=['POST']) +def search_songs(): + """Iskanje pesmi po naslovu""" + if _projector_app is None: + return jsonify({'results': []}) + + data = request.get_json() + query = data.get('query', '').strip() + + if not query: + return jsonify({'results': []}) + + try: + _projector_app.cursor.execute( + "SELECT id, title FROM songs WHERE title LIKE ? COLLATE NOCASE", + (f"%{query}%",) + ) + results = _projector_app.cursor.fetchall() + return jsonify({'results': results}) + except Exception as e: + return jsonify({'error': str(e)}) + + +def run_server(host='127.0.0.1', port=5000): + """Zaženi Flask server""" + app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) + + +def start_server_thread(projector_app, host='127.0.0.1', port=5000): + """Zaženi server v zvjenoj niti""" + set_projector_app(projector_app) + + server_thread = threading.Thread( + target=run_server, + args=(host, port), + daemon=True + ) + server_thread.start() + + return server_thread diff --git a/web/static/script.js b/web/static/script.js new file mode 100644 index 0000000..e7291c8 --- /dev/null +++ b/web/static/script.js @@ -0,0 +1,157 @@ +console.log('JavaScript se izvaja...'); + +// DOM elementi +const songNumberInput = document.getElementById('song-number'); +const loadBtn = document.getElementById('load-btn'); +const searchQueryInput = document.getElementById('search-query'); +const searchBtn = document.getElementById('search-btn'); +const capsBtn = document.getElementById('caps-btn'); +const displayArea = document.getElementById('display-area'); +const prevBtn = document.getElementById('prev-btn'); +const nextBtn = document.getElementById('next-btn'); +const darkBtn = document.getElementById('dark-btn'); +const pageInfo = document.getElementById('page-info'); + +console.log('DOM elementi najdeni:', { songNumberInput: !!songNumberInput, loadBtn: !!loadBtn, searchQueryInput: !!searchQueryInput, searchBtn: !!searchBtn, capsBtn: !!capsBtn, displayArea: !!displayArea, prevBtn: !!prevBtn, nextBtn: !!nextBtn, darkBtn: !!darkBtn, pageInfo: !!pageInfo }); + +let capsMode = false; + +// Naloži trenutne podatke +async function updateState() { + console.log('updateState() se kliče...'); + try { + console.log('Pošiljam fetch za /api/state...'); + const response = await fetch('/api/state'); + console.log('Response status:', response.status); + const data = await response.json(); + console.log('Response data:', data); + + displayArea.textContent = data.current_text || 'Pripravljeno. Vpiši številko pesmi.'; + pageInfo.textContent = data.page_info || ''; + capsMode = data.caps_mode || false; + + if (capsMode) { + capsBtn.classList.add('active'); + } else { + capsBtn.classList.remove('active'); + } + + prevBtn.disabled = !data.can_prev; + nextBtn.disabled = !data.can_next; + console.log('updateState() uspešno zaključeno'); + } catch (error) { + console.error('Napaka pri posodabljanju stanja:', error); + displayArea.innerHTML = 'Napaka: ni povezave do strežnika'; + prevBtn.disabled = true; nextBtn.disabled = true; loadBtn.disabled = true; searchBtn.disabled = true; capsBtn.disabled = true; darkBtn.disabled = true; + } +} + +// Naloži pesem +async function loadSong() { + console.log('loadSong() se kliče'); + const songNumber = songNumberInput.value.trim(); + console.log('Song number:', songNumber); + if (!songNumber) return; + + try { + console.log('Pošiljam POST za /api/load_song...'); + const response = await fetch('/api/load_song', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({song_number: songNumber}) }); + console.log('Response status:', response.status); + const data = await response.json(); + console.log('Response data:', data); + + songNumberInput.value = ''; + updateState(); + } catch (error) { + console.error('Napaka pri nalaganju pesmi:', error); + displayArea.innerHTML = 'Napaka: ni povezave do strežnika'; + } +} + +// Naslednja stran +async function nextPage() { + try { + await fetch('/api/next_page', {method: 'POST'}); + updateState(); + } catch (error) { + console.error('Napaka pri navigaciji:', error); + displayArea.innerHTML = 'Napaka: ni povezave do strežnika'; + } +} + +// Prejšnja stran +async function prevPage() { + try { + await fetch('/api/prev_page', {method: 'POST'}); + updateState(); + } catch (error) { + console.error('Napaka pri navigaciji:', error); + displayArea.innerHTML = 'Napaka: ni povezave do strežnika'; + } +} + +// Zatemnitev ekrana +async function clearScreen() { + try { + await fetch('/api/clear_screen', {method: 'POST'}); + updateState(); + } catch (error) { + console.error('Napaka pri zatamnitvi ekrana:', error); + displayArea.innerHTML = 'Napaka: ni povezave do strežnika'; + } +} + +// Preklop VELIKIH ČRK +async function toggleCaps() { + try { + const response = await fetch('/api/toggle_caps', {method: 'POST'}); + updateState(); + } catch (error) { + console.error('Napaka pri preklopa velikih črk:', error); displayArea.innerHTML = 'Napaka: ni povezave do strežnika'; } +} + +// Iskanje pesmi +async function searchSongs() { + const query = searchQueryInput.value.trim(); + if (!query) return; + + try { + const response = await fetch('/api/search_songs', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({query: query}) }); + const data = await response.json(); + + if (data.results && data.results.length > 0) { + const resultList = data.results.map(item => `${item[0]}: ${item[1]}`).join('\n'); + displayArea.innerHTML = '' + resultList.replace(//g, '>').replace(/\n/g, '
') + '
'; + } else { + displayArea.innerHTML = 'Ni zadetkov.'; + } + searchQueryInput.value = ''; + } catch (error) { + console.error('Napaka pri iskanju:', error); + displayArea.innerHTML = 'Napaka: ni povezave do strežnika'; + } +} + +// Dodaj poslušalce +console.log('Dodajam event listenerje...'); +loadBtn.addEventListener('click', loadSong); +console.log('loadBtn listener dodan'); +songNumberInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') loadSong(); }); +console.log('songNumberInput listener dodan'); + +searchBtn.addEventListener('click', searchSongs); +searchQueryInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') searchSongs(); }); + +capsBtn.addEventListener('click', toggleCaps); +prevBtn.addEventListener('click', prevPage); +nextBtn.addEventListener('click', nextPage); +darkBtn.addEventListener('click', clearScreen); +console.log('Vsi event listenerji dodani'); + +// Začetna inicijalizacija +console.log('Začenjam začetno inicijalizacijo...'); +updateState(); + +// Osveži stanje vsako sekundo (za sinhronizacijo s tipkovnico) +// setInterval(updateState, 1000); +console.log('JavaScript inicializacija zaključena'); \ No newline at end of file diff --git a/web/static/styles.css b/web/static/styles.css new file mode 100644 index 0000000..ea94dd0 --- /dev/null +++ b/web/static/styles.css @@ -0,0 +1,153 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Times New Roman', serif; + background-color: #0a0a0a; + color: #ffffff; + display: flex; + flex-direction: column; + height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + height: 100%; + padding: 0; +} + +/* Gornja vrstica - Vnos pesmi */ +.top-bar { + background-color: #1a1a1a; + padding: 20px; + border-bottom: 2px solid #333; + display: flex; + gap: 15px; + align-items: center; + flex-wrap: wrap; +} + +.top-bar label { + font-size: 16px; + font-weight: bold; +} + +.top-bar input { + padding: 10px 15px; + font-size: 16px; + background-color: #2a2a2a; + border: 1px solid #444; + color: #ffffff; + border-radius: 4px; + width: 100px; +} + +.top-bar button { + padding: 10px 20px; + font-size: 16px; + background-color: #3a7ca5; + border: none; + color: white; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.3s; +} + +.top-bar button:hover { + background-color: #2d6183; +} + +.caps-toggle { + padding: 10px 15px; + font-size: 14px; + background-color: #4a4a4a; + border: 1px solid #666; + color: #ffffff; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.3s; +} + +.caps-toggle.active { + background-color: #3a7ca5; +} + +/* Sredenska vrstica - Prikaz besedila */ +.content { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + overflow-y: auto; + background-color: #000000; +} + +.lyrics-display { + text-align: center; + font-size: 32px; + line-height: 1.5; + white-space: pre-wrap; + word-wrap: break-word; + max-width: 90%; +} + +.status-message { + color: #aaa; + font-size: 24px; +} + +/* Donja vrstica - Navigacijski gumbi */ +.bottom-bar { + background-color: #1a1a1a; + padding: 20px; + border-top: 2px solid #333; + display: flex; + justify-content: center; + gap: 20px; + align-items: center; +} + +.bottom-bar button { + padding: 15px 40px; + font-size: 18px; + border: none; + color: white; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.3s; + min-width: 150px; +} + +.btn-nav { + background-color: #556b79; +} + +.btn-nav:hover { + background-color: #3d4a52; +} + +.btn-nav:disabled { + background-color: #2a2a2a; + cursor: not-allowed; + opacity: 0.5; +} + +.btn-dark { + background-color: #8b4513; + min-width: 150px; +} + +.btn-dark:hover { + background-color: #6b3410; +} + +.page-info { + color: #999; + font-size: 16px; + min-width: 60px; +} \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..971bc71 --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,43 @@ + + + + + + Projektor pesmi - Web sučelje + + + +
+ +
+ + + + + + + +
+ + +
+
+ Pripravljeno. Vpiši številko pesmi. +
+
+ + +
+ + + + + +
+
+ + + + \ No newline at end of file