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,
|
"screen_width_percent": 60,
|
||||||
"font_bold": True,
|
"font_bold": True,
|
||||||
"show_song_info": True,
|
"show_song_info": True,
|
||||||
"split_by_stanza": False
|
"split_by_stanza": False,
|
||||||
|
"web_port": 5000
|
||||||
}
|
}
|
||||||
|
|
||||||
SETTINGS_FILE = "settings.json"
|
SETTINGS_FILE = "settings.json"
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
===
|
===
|
||||||
1
|
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
|
||||||
Zdrava, Marija, milosti polna, Gospod je s Teboj,
|
Zdrava, Marija, milosti polna, Gospod je s Teboj,
|
||||||
blagoslovljena si med ženami
|
blagoslovljena si med ženami
|
||||||
|
|||||||
21
projector.py
21
projector.py
@@ -9,6 +9,7 @@ import math
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tkinter.messagebox as messagebox
|
import tkinter.messagebox as messagebox
|
||||||
|
from web.server import start_server_thread
|
||||||
|
|
||||||
DB_PATH = 'songs.db'
|
DB_PATH = 'songs.db'
|
||||||
SETTINGS_PATH = 'settings.json'
|
SETTINGS_PATH = 'settings.json'
|
||||||
@@ -22,7 +23,8 @@ class SongProjector:
|
|||||||
# ...
|
# ...
|
||||||
try:
|
try:
|
||||||
# Odpri read-only; ne bo ustvaril prazne baze, če datoteka manjka
|
# 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()
|
self.cursor = self.conn.cursor()
|
||||||
except sqlite3.OperationalError as e:
|
except sqlite3.OperationalError as e:
|
||||||
# Jasno sporočilo in varen izhod
|
# Jasno sporočilo in varen izhod
|
||||||
@@ -143,6 +145,17 @@ class SongProjector:
|
|||||||
|
|
||||||
self.clear_screen()
|
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
|
# NOVA METODA: enakomeren prelom predolgih vrstic
|
||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
@@ -370,12 +383,16 @@ class SongProjector:
|
|||||||
# Očisti zaslon
|
# Očisti zaslon
|
||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
def clear_screen(self, event=None):
|
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.config(text="")
|
||||||
self.label.pack_forget()
|
self.label.pack_forget()
|
||||||
self.color_frame.config(bg="black", width=self.color_width, height=self.screen_height)
|
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.color_frame.place(relx=0.5, rely=0.5, anchor="center")
|
||||||
self.song_info_label.config(text="")
|
self.song_info_label.config(text="")
|
||||||
self.waiting_for_song = True
|
|
||||||
|
|
||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
# Iskanje pesmi
|
# Iskanje pesmi
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
"font_name": "Times New Roman",
|
"font_name": "Times New Roman",
|
||||||
"bg_color": "#000000",
|
"bg_color": "#000000",
|
||||||
"fg_color": "#FFFFFF",
|
"fg_color": "#FFFFFF",
|
||||||
"font_size": 32,
|
"font_size": 36,
|
||||||
"screen_width_percent": 60,
|
"screen_width_percent": 60,
|
||||||
"font_bold": true,
|
"font_bold": true,
|
||||||
"show_song_info": 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