Prva verzija mini web serverja (flask). Hvala, Github Copilot.
This commit is contained in:
153
README.md
Normal file
153
README.md
Normal file
@@ -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:<web_port>/
|
||||
```
|
||||
|
||||
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]
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
21
projector.py
21
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
143
tests/test_api.py
Normal file
143
tests/test_api.py
Normal file
@@ -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()
|
||||
185
tests/test_integration.py
Normal file
185
tests/test_integration.py
Normal file
@@ -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)
|
||||
106
tests/test_mock.py
Normal file
106
tests/test_mock.py
Normal file
@@ -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)
|
||||
5
web/__init__.py
Normal file
5
web/__init__.py
Normal file
@@ -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"]
|
||||
173
web/server.py
Normal file
173
web/server.py
Normal file
@@ -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
|
||||
157
web/static/script.js
Normal file
157
web/static/script.js
Normal file
@@ -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 = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
|
||||
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 = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Naslednja stran
|
||||
async function nextPage() {
|
||||
try {
|
||||
await fetch('/api/next_page', {method: 'POST'});
|
||||
updateState();
|
||||
} catch (error) {
|
||||
console.error('Napaka pri navigaciji:', error);
|
||||
displayArea.innerHTML = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = '<span class="status-message">Napaka: ni povezave do strežnika</span>'; }
|
||||
}
|
||||
|
||||
// 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 = '<span class="status-message">' + resultList.replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>') + '</span>';
|
||||
} else {
|
||||
displayArea.innerHTML = '<span class="status-message">Ni zadetkov.</span>';
|
||||
}
|
||||
searchQueryInput.value = '';
|
||||
} catch (error) {
|
||||
console.error('Napaka pri iskanju:', error);
|
||||
displayArea.innerHTML = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
153
web/static/styles.css
Normal file
153
web/static/styles.css
Normal file
@@ -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;
|
||||
}
|
||||
43
web/templates/index.html
Normal file
43
web/templates/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="sl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Projektor pesmi - Web sučelje</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Gornja vrstica -->
|
||||
<div class="top-bar">
|
||||
<label for="song-number">Pesem:</label>
|
||||
<input type="number" id="song-number" placeholder="Vpiši številko" min="0">
|
||||
<button id="load-btn">Naloži</button>
|
||||
|
||||
<!-- <label for="search-query">Iskanje:</label>
|
||||
<input type="text" id="search-query" placeholder="Naslovna beseda...">
|
||||
<button id="search-btn">Išči</button> -->
|
||||
|
||||
<button id="caps-btn" class="btn-dark">VELIKE črke</button>
|
||||
</div>
|
||||
|
||||
<!-- Sredenska vrstica - Prikaz -->
|
||||
<div class="content">
|
||||
<div id="display-area" class="lyrics-display">
|
||||
<span class="status-message">Pripravljeno. Vpiši številko pesmi.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spodnja vrstica - Navigacija -->
|
||||
<div class="bottom-bar">
|
||||
<button id="prev-btn" class="btn-nav">◀ Nazaj</button>
|
||||
<span id="page-info" class="page-info"></span>
|
||||
<button id="dark-btn" class="btn-dark">Zatemni ekran</button>
|
||||
<span id="page-info-right" class="page-info"></span>
|
||||
<button id="next-btn" class="btn-nav">Naprej ▶</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user