17 Commits

Author SHA1 Message Date
25a8de02b5 Edit poljubne in ustvari novo. #11 2026-03-21 16:21:22 +01:00
67fa26a0b1 Osveževanje baze besedil preko web-a #5 2026-03-21 11:24:43 +01:00
64e478eafa Poskus predelave na dbus msg (namesto sleep procesa). TODO: stestiraj do kraja. #10 2026-03-20 23:37:40 +01:00
d5354b8757 Sistematično poimenovanje Projektor besedil oz. Projektor. Besedo pesem dopolnil z 'ali drugo besedilo' oz. jo nadomeščal z besedilo, kjer je bilo možno ali pa se celo v celoti izognil temu poimenovanju. #8 2026-03-20 22:53:03 +01:00
91bff12ed7 Urejevalnik pesmi (feature #1) 2026-03-20 22:20:18 +01:00
ef16b32e62 Vizualni feedback ob vnosu številke (preko ekrana ali preko tipkovnice). 2026-03-19 22:16:30 +01:00
2cf509e796 Iskalnik pesmi #2 2026-03-19 22:04:52 +01:00
4afc5e30d9 Hamburger menu z obstoječimi opcijami (skrij tipkovnico in velike/male črke). #4 2026-03-19 21:45:54 +01:00
8c5fa82b2e predelava web vmesnika - zaslonska tipkovnica + drugi gumbi 2026-03-16 21:52:38 +01:00
4dd06bb7f0 restart app (9997) 2026-03-16 21:45:38 +01:00
fc478c1fa1 9998 restart server 2026-03-14 17:39:54 +01:00
ac373614bd kozmetika 2026-03-11 11:15:15 +01:00
6427352425 popravek v množino (kot je bilo v originalu) 2026-03-08 22:27:24 +01:00
3d20c3a3df mali popravki (poenotenje poimenovanja mape), poevečanje backup kopij na 6 (če kdo 2x ali 3x reboot-a s ključkom v rčaunalniku) 2026-03-08 22:20:39 +01:00
3bbabc5f81 startup za linux, dodan backup baze (just in case) 2026-03-08 22:08:15 +01:00
b779f49556 sleep inhibitor - popravljen za linux (bazira na gnome-session-inhibit) 2026-03-08 21:35:57 +01:00
c4b230a9b0 preprečevanje screensaver-ja (še ne dela na wayland) 2026-03-07 21:40:58 +01:00
9 changed files with 1404 additions and 294 deletions

View File

@@ -1,4 +1,4 @@
# Projektor pesmi (Song Projector) # Projekcija besedil (Lyrics Projector)
A Tkinter-based song projector application with optional Flask web interface for remote control. A Tkinter-based song projector application with optional Flask web interface for remote control.
@@ -17,7 +17,7 @@ A Tkinter-based song projector application with optional Flask web interface for
## Project Structure ## Project Structure
``` ```
Projekcija/ projekcija/
├── projector.py # Main Tkinter application ├── projector.py # Main Tkinter application
├── nastavitve.py # Settings defaults and initialization ├── nastavitve.py # Settings defaults and initialization
├── add_song.py # Utility to import songs into database ├── add_song.py # Utility to import songs into database

View File

@@ -8,8 +8,11 @@ import os
import math import math
import subprocess import subprocess
import sys import sys
import ctypes
import tkinter.messagebox as messagebox import tkinter.messagebox as messagebox
from web.server import start_server_thread from web.server import start_server_thread
import urllib.request
import tempfile
DB_PATH = 'songs.db' DB_PATH = 'songs.db'
SETTINGS_PATH = 'settings.json' SETTINGS_PATH = 'settings.json'
@@ -24,7 +27,7 @@ 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
# check_same_thread=False omogoča uporabo v večih nitih # 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.conn = sqlite3.connect(DB_PATH, 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
@@ -49,7 +52,8 @@ class SongProjector:
"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 # TODO: mogoče nekoč (prelom po kitici namesto po višini) "split_by_stanza": False, # TODO: mogoče nekoč (prelom po kitici namesto po višini)
"db_update_url": ""
} }
if not os.path.exists(SETTINGS_PATH): if not os.path.exists(SETTINGS_PATH):
@@ -159,12 +163,48 @@ class SongProjector:
except Exception as e: except Exception as e:
print(f"Napaka pri zagonu web serverja: {e}") print(f"Napaka pri zagonu web serverja: {e}")
# --------------------------------------------------
# Preprečevanje ohranjevalnika zaslona
# --------------------------------------------------
self.keep_awake()
def keep_awake(self):
"""
Prepreči vklop ohranjevalnika zaslona ali spanja.
"""
self.inhibitor_cookie = None
try:
if sys.platform.startswith("win"):
# Windows: ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED
# 0x80000000 | 0x00000002 | 0x00000001
# Uporabimo c_uint za zagotovitev pravilnega tipa
ctypes.windll.kernel32.SetThreadExecutionState(ctypes.c_uint(0x80000003))
elif sys.platform == "linux":
# GNOME DBus inhibitor (org.gnome.SessionManager)
# Flags: 1=logout, 2=switch user, 4=suspend, 8=idle
# Uporabimo 12 (4+8) za suspend in idle.
# toplevel_xid=0 (uint32:0)
cmd = [
"dbus-send", "--print-reply", "--dest=org.gnome.SessionManager",
"/org/gnome/SessionManager", "org.gnome.SessionManager.Inhibit",
"string:projector", "uint32:0", "string:Projecting lyrics", "uint32:12"
]
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode()
# Output format: uint32 12345678
for line in output.splitlines():
if "uint32" in line:
self.inhibitor_cookie = line.split()[-1]
break
except Exception as e:
print(f"Napaka pri preprečevanju spanja: {e}")
# ------------------------------------------------------ # ------------------------------------------------------
# NOVA METODA: enakomeren prelom predolgih vrstic # NOVA METODA: enakomeren prelom predolgih vrstic
# ------------------------------------------------------ # ------------------------------------------------------
def split_long_line(self, line): def split_long_line(self, line):
""" """
Vrne seznam podvrstic, ki skupaj tvorijo `line`, Vrne seznam pod-vrstic, ki skupaj tvorijo `line`,
pri čemer so (približno) enako dolge glede na število znakov. pri čemer so (približno) enako dolge glede na število znakov.
""" """
avg_char_width = int(self.settings["font_size"]) * 0.57 avg_char_width = int(self.settings["font_size"]) * 0.57
@@ -221,7 +261,7 @@ class SongProjector:
self.next_page() self.next_page()
# ------------------------------------------------------ # ------------------------------------------------------
# Nalaganje in obdelava pesmi # Nalaganje in obdelava besedila
# ------------------------------------------------------ # ------------------------------------------------------
def load_song(self): def load_song(self):
if not self.song_number: if not self.song_number:
@@ -245,6 +285,15 @@ class SongProjector:
else: else:
subprocess.Popen(["shutdown", "-h", "now"]) subprocess.Popen(["shutdown", "-h", "now"])
return return
elif self.song_number == "9998":
if sys.platform.startswith("win"):
subprocess.Popen(["shutdown", "/r", "/t", "0"])
else:
subprocess.Popen(["shutdown", "-r", "now"])
return
elif self.song_number == "9997":
self.restart_program()
return
elif self.song_number == "7777": elif self.song_number == "7777":
subprocess.Popen([sys.executable, os.path.join(BASE_DIR, "nastavitve.py")]) # fixme: novi proces ne dobi stdin-a; predelati na tkinter aplikacijo? subprocess.Popen([sys.executable, os.path.join(BASE_DIR, "nastavitve.py")]) # fixme: novi proces ne dobi stdin-a; predelati na tkinter aplikacijo?
self.exit_program() self.exit_program()
@@ -256,6 +305,9 @@ class SongProjector:
subprocess.Popen([sys.executable, os.path.join(BASE_DIR, "add_song.py")]) # fixme: novi proces ne dobi stdin-a; predelati na tkinter aplikacijo? subprocess.Popen([sys.executable, os.path.join(BASE_DIR, "add_song.py")]) # fixme: novi proces ne dobi stdin-a; predelati na tkinter aplikacijo?
self.exit_program() self.exit_program()
return return
elif self.song_number == "9901":
self.update_songs_database()
return
try: try:
song_id = int(self.song_number) song_id = int(self.song_number)
@@ -398,7 +450,7 @@ class SongProjector:
self.song_info_label.config(text="") self.song_info_label.config(text="")
# ------------------------------------------------------ # ------------------------------------------------------
# Iskanje pesmi # Iskanje po naslovu
# ------------------------------------------------------ # ------------------------------------------------------
def search_song(self, event=None): def search_song(self, event=None):
self.clear_screen() self.clear_screen()
@@ -459,14 +511,119 @@ class SongProjector:
else: else:
self.label.config(text="Ni zadetkov.") self.label.config(text="Ni zadetkov.")
# ------------------------------------------------------
# Posodobitev baze pesmi
# ------------------------------------------------------
def update_songs_database(self):
url = self.settings.get("db_update_url", "").strip()
if not url:
msg = "URL za posodobitev ni nastavljen v settings.json."
print(msg)
self.label.config(text=msg)
self.song_number = ""
return
msg = f"Prenašam posodobitev iz: {url}..."
print(msg)
self.label.config(text=msg)
self.root.update()
temp_db_path = None
try:
# Prenos datoteke
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
temp_db_path = tmp_file.name
with urllib.request.urlopen(url) as response:
tmp_file.write(response.read())
# Preverjanje integritete
print("Preverjam integriteto prenesene baze...")
check_conn = sqlite3.connect(temp_db_path)
check_cursor = check_conn.cursor()
# PRAGMA integrity_check
check_cursor.execute("PRAGMA integrity_check")
integrity_result = check_cursor.fetchone()[0]
if integrity_result != "ok":
raise Exception(f"Integriteta baze ni OK: {integrity_result}")
# Preverjanje obstoja tabele songs
check_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='songs'")
if not check_cursor.fetchone():
raise Exception("Tabela 'songs' ne obstaja v preneseni bazi.")
# Upsert v trenutno bazo
print("Izvajam upsert v lokalno bazo...")
check_cursor.execute("SELECT id, title, lyrics FROM songs")
new_songs = check_cursor.fetchall()
upsert_count = 0
for song_id, title, lyrics in new_songs:
self.cursor.execute("""
INSERT INTO songs (id, title, lyrics) VALUES (?, ?, ?)
ON CONFLICT(id) DO UPDATE SET title=excluded.title, lyrics=excluded.lyrics
""", (song_id, title, lyrics))
upsert_count += 1
self.conn.commit()
check_conn.close()
final_msg = f"Posodobitev uspešna! Posodobljenih/dodanih {upsert_count} pesmi."
print(final_msg)
self.label.config(text=final_msg)
except Exception as e:
error_msg = f"Napaka pri posodobitvi: {e}"
print(error_msg)
self.label.config(text=error_msg)
finally:
if temp_db_path and os.path.exists(temp_db_path):
try:
os.remove(temp_db_path)
except:
pass
self.song_number = ""
# ------------------------------------------------------ # ------------------------------------------------------
# Izhod # Izhod
# ------------------------------------------------------ # ------------------------------------------------------
def exit_program(self, event=None): def exit_program(self, event=None):
# Ponastavi stanje preprečevanja spanja na Windows
if sys.platform.startswith("win"):
try:
# ES_CONTINUOUS (0x80000000)
ctypes.windll.kernel32.SetThreadExecutionState(ctypes.c_uint(0x80000000))
except:
pass
elif sys.platform == "linux":
try:
# Sprostimo DBus inhibitor, če ga imamo
if hasattr(self, "inhibitor_cookie") and self.inhibitor_cookie:
subprocess.Popen([
"dbus-send", "--dest=org.gnome.SessionManager",
"/org/gnome/SessionManager", "org.gnome.SessionManager.UnInhibit",
f"uint32:{self.inhibitor_cookie}"
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except:
pass
self.conn.close() self.conn.close()
self.root.destroy() self.root.destroy()
# ------------------------------------------------------
# Restart programa
# ------------------------------------------------------
def restart_program(self):
try:
if self.conn:
self.conn.close()
except Exception:
pass
python = sys.executable
args = [python] + sys.argv
subprocess.Popen(args)
self.root.after(200, self.exit_program)
# ---------------------------------------------------------- # ----------------------------------------------------------
# Zagon aplikacije # Zagon aplikacije
# ---------------------------------------------------------- # ----------------------------------------------------------

View File

@@ -1,11 +1,12 @@
{ {
"font_name": "Noto Sans Display", "font_name": "Noto Sans",
"bg_color": "#000000", "bg_color": "#000000",
"fg_color": "#FFFFFF", "fg_color": "#FFFFFF",
"font_size": 36, "font_size": 32,
"screen_width_percent": 60, "screen_width_percent": 60,
"font_bold": false, "font_bold": false,
"show_song_info": true, "show_song_info": true,
"split_by_stanza": false, "split_by_stanza": false,
"web_port": 5000 "web_port": 5000,
} "db_update_url": "https://k5c.korenjak.si/s/dKeE6eXMmBPteQF/download"
}

View File

@@ -1,25 +1,55 @@
@echo off @echo off
cls cls
set "TARGET=%USERPROFILE%\OneDrive\Namizje" set "TARGET=%USERPROFILE%\Projekcija"
set "BACKUP_DIR=%TARGET%\backup"
:: Najprej preveri ali obstaja mapa Projekcije cerkev :: Najprej preveri ali obstaja mapa Projekcija na USB (D:)
IF EXIST "D:\Projekcije cerkev" ( IF EXIST "D:\Projekcija" (
echo Mapa 'Projekcije cerkev' obstaja. echo Mapa 'Projekcija' na USB pogonu obstaja.
echo Kopiram mapo na namizje...
:: 1. Ustvari backup obstoječe baze na namizju, če obstaja
IF EXIST "%TARGET%\songs.db" (
echo Ustvarjam backup baze...
if not exist "%BACKUP_DIR%" mkdir "%BACKUP_DIR%"
:: Pridobi timestamp (YYYYMMDD_HHMMSS)
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c%%a%%b)
for /f "tokens=1-2 delims=: " %%a in ('time /t') do (set mytime=%%a%%b)
set "TS=%date:~10,4%%date:~4,2%%date:~7,2%_%time:~0,2%%time:~3,2%%time:~6,2%"
set "TS=%TS: =0%"
set "CURRENT_BACKUP=%BACKUP_DIR%\backup_%TS%"
mkdir "%CURRENT_BACKUP%"
if exist "%TARGET%\settings.conf" copy "%TARGET%\settings.conf" "%CURRENT_BACKUP%\" >nul
if exist "%TARGET%\songs.db" copy "%TARGET%\songs.db" "%CURRENT_BACKUP%\" >nul
echo Backup ustvarjen v: %CURRENT_BACKUP%
:: 2. Ohrani samo zadnjih 6 backupov (pobriši starejše)
pushd "%BACKUP_DIR%"
for /f "skip=6 delims=" %%F in ('dir /b /ad /o-n backup_*') do (
echo Brisanje starega backupa: %%F
rd /s /q "%%F"
)
popd
)
:: Kopiranje z robocopy echo Kopiram nove datoteke z USB na namizje...
robocopy "D:\Projekcije cerkev" "%TARGET%\Projekcije cerkev" /E :: Kopiranje z robocopy (/E - vse podmape, /XO - samo novejše datoteke, da ne povozimo backupa če ni treba)
robocopy "D:\Projekcija" "%TARGET%" /E
echo Zagon projector.py ... echo Zagon projector.py ...
pushd "%TARGET%\Projekcije cerkev" pushd "%TARGET%"
python projector.py python projector.py
popd popd
) ELSE ( ) ELSE (
cls cls
echo USB ključek ni najden. Zagon lokalne verzije...
echo Zagon projector.py ... echo Zagon projector.py ...
pushd "%TARGET%\Projekcije cerkev" pushd "%TARGET%"
python projector.py py "projekcija\projector.py"
popd popd
) )

44
startup.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
# Ciljna mapa na Linuxu (v uporabnikovem home-u)
TARGET="$HOME/Projekcija"
BACKUP_DIR="$TARGET/backup"
# Iskanje vira na vseh priklopljenih medijih (USB ključek)
SOURCE=$(find /media/$USER -maxdepth 2 -type d -name "Projekcija" 2>/dev/null | head -n 1)
if [ -n "$SOURCE" ]; then
echo "Najden ključek: $SOURCE"
# 1. Ustvari backup obstoječe baze, če obstaja
if [ -f "$TARGET/songs.db" ]; then
echo "Ustvarjam backup baze..."
mkdir -p "$BACKUP_DIR"
# Časovni žig po ISO 8601 formatu (npr. 2026-03-08T20:56:42)
TS=$(date +"%Y-%m-%dT%H:%M:%S")
CURRENT_BACKUP="$BACKUP_DIR/backup_$TS"
mkdir -p "$CURRENT_BACKUP"
[ -f "$TARGET/settings.conf" ] && cp "$TARGET/settings.conf" "$CURRENT_BACKUP/"
[ -f "$TARGET/songs.db" ] && cp "$TARGET/songs.db" "$CURRENT_BACKUP/"
echo "Backup ustvarjen v: $CURRENT_BACKUP"
# 2. Ohrani samo zadnjih 6 backupov (pobriši starejše)
# ls -dt izpiše mape po času (novejše prej), tail -n +7 pa preskoči prvih šest
cd "$BACKUP_DIR" && ls -dt backup_* 2>/dev/null | tail -n +7 | xargs -r rm -rf
fi
echo "Sinhronizacija datotek z USB v home mapo..."
mkdir -p "$TARGET"
# rsync -av --exclude='backup' sinhronizira vsebino brez backup mape
rsync -av --exclude='backup' "$SOURCE/" "$TARGET/"
fi
echo "Zagon projector.py..."
if [ -d "$TARGET" ]; then
cd "$TARGET" && projekcija/projector.py
else
echo "Napaka: Mapa $TARGET ne obstaja."
fi

View File

@@ -65,7 +65,7 @@ def get_state():
can_prev = _projector_app.current_page_index > 0 can_prev = _projector_app.current_page_index > 0
can_next = _projector_app.current_page_index + 1 < len(_projector_app.pages) can_next = _projector_app.current_page_index + 1 < len(_projector_app.pages)
else: else:
current_text = "Pripravljeno. Vpiši številko pesmi." current_text = "Pripravljeno. Vpiši številko pesmi ali drugega besedila."
return jsonify({ return jsonify({
'current_text': current_text, 'current_text': current_text,
@@ -133,7 +133,7 @@ def toggle_caps():
@app.route('/api/search_songs', methods=['POST']) @app.route('/api/search_songs', methods=['POST'])
def search_songs(): def search_songs():
"""Iskanje pesmi po naslovu""" """Iskanje besedil po naslovu"""
if _projector_app is None: if _projector_app is None:
return jsonify({'results': []}) return jsonify({'results': []})
@@ -154,6 +154,91 @@ def search_songs():
return jsonify({'error': str(e)}) return jsonify({'error': str(e)})
@app.route('/api/get_song_details', methods=['GET'])
def get_song_details():
"""Vrne podrobnosti trenutno naložene pesmi ali pesmi po ID-ju"""
if _projector_app is None:
return jsonify({'status': 'error', 'message': 'Aplikacija ni inicijalizirana'})
song_id_param = request.args.get('id')
if song_id_param:
try:
song_id = int(song_id_param)
except ValueError:
return jsonify({'status': 'error', 'message': 'Neveljaven ID pesmi'})
elif _projector_app.song_number_last:
try:
song_id = int(_projector_app.song_number_last)
except ValueError:
return jsonify({'status': 'error', 'message': 'Neveljavna številka naložene pesmi'})
else:
return jsonify({'status': 'error', 'message': 'Nobena pesem ni naložena'})
try:
_projector_app.cursor.execute("SELECT id, title, lyrics FROM songs WHERE id=?", (song_id,))
result = _projector_app.cursor.fetchone()
if result:
return jsonify({
'status': 'ok',
'song': {
'id': result[0],
'title': result[1],
'lyrics': result[2]
}
})
else:
return jsonify({'status': 'error', 'message': f'Pesem s številko {song_id} ni najdena v bazi'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)})
@app.route('/api/update_song', methods=['POST'])
def update_song():
"""Posodobi naslov in besedilo pesmi ali ustvari novo"""
if _projector_app is None:
return jsonify({'status': 'error', 'message': 'Aplikacija ni inicijalizirana'})
data = request.get_json()
song_id = data.get('id')
title = data.get('title', '').strip()
lyrics = data.get('lyrics', '').strip()
if not song_id or not title or not lyrics:
return jsonify({'status': 'error', 'message': 'Manjkajoči podatki'})
try:
if song_id == 'new':
# Pridobi prvo naslednjo prosto številko
_projector_app.cursor.execute("SELECT MAX(id) FROM songs")
max_id = _projector_app.cursor.fetchone()[0]
new_id = (max_id or 0) + 1
_projector_app.cursor.execute(
"INSERT INTO songs (id, title, lyrics) VALUES (?, ?, ?)",
(new_id, title, lyrics)
)
_projector_app.conn.commit()
# Naloži novo pesem
_projector_app.song_number = str(new_id)
_projector_app.load_song()
return jsonify({'status': 'ok', 'new_id': new_id})
# Obstoječa pesem
_projector_app.cursor.execute("UPDATE songs SET title=?, lyrics=? WHERE id=?", (title, lyrics, song_id))
_projector_app.conn.commit()
# Osvežimo trenutno pesem, če je to ta, ki smo jo pravkar uredili
if str(_projector_app.song_number_last) == str(song_id):
_projector_app.song_number = str(song_id)
_projector_app.load_song()
return jsonify({'status': 'ok'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)})
def run_server(host='127.0.0.1', port=5000): def run_server(host='127.0.0.1', port=5000):
"""Zaženi Flask server""" """Zaženi Flask server"""
app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True)

View File

@@ -3,90 +3,216 @@ console.log('JavaScript se izvaja...');
// DOM elementi // DOM elementi
const songNumberInput = document.getElementById('song-number'); const songNumberInput = document.getElementById('song-number');
const loadBtn = document.getElementById('load-btn'); 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 displayArea = document.getElementById('display-area');
const prevBtn = document.getElementById('prev-btn'); const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn'); const nextBtn = document.getElementById('next-btn');
const darkBtn = document.getElementById('dark-btn'); const darkBtn = document.getElementById('dark-btn');
const pageInfo = document.getElementById('page-info'); const pageInfo = document.getElementById('page-info');
const clearBtn = document.getElementById('clear-btn');
const keypadButtons = document.querySelectorAll('.btn-key');
const keypadWrapper = document.getElementById('keypad-wrapper');
const toggleKeypadBtn = document.getElementById('toggle-keypad-btn');
const menuToggle = document.getElementById('menu-toggle');
const menuDropdown = document.getElementById('menu-dropdown');
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const editSongBtn = document.getElementById('edit-song-btn');
const editModal = document.getElementById('edit-modal');
const editTitleInput = document.getElementById('edit-title');
const editLyricsInput = document.getElementById('edit-lyrics');
const saveEditBtn = document.getElementById('save-edit-btn');
const cancelEditBtn = document.getElementById('cancel-edit-btn');
const editSongNumberDisplay = document.getElementById('edit-song-number-display');
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 }); // Prompt Modal elementi
const promptModal = document.getElementById('prompt-modal');
const promptInput = document.getElementById('prompt-input');
const promptEditBtn = document.getElementById('prompt-edit-btn');
const promptCancelBtn = document.getElementById('prompt-cancel-btn');
const promptNewBtn = document.getElementById('prompt-new-btn');
// Info Modal elementi
const infoModal = document.getElementById('info-modal');
const infoMessage = document.getElementById('info-message');
const infoOkBtn = document.getElementById('info-ok-btn');
let capsMode = false; let capsMode = false;
let wakeLock = null;
let lastStateSignature = "";
let lastPageInfo = "";
// Naloži trenutne podatke // vibracija telefona
async function updateState() { function vibrate() {
console.log('updateState() se kliče...'); if (navigator.vibrate && /Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
navigator.vibrate([25, 30, 25]);
}
}
// poskus, da se zaslon ne ugaša
async function requestWakeLock() {
try { try {
console.log('Pošiljam fetch za /api/state...'); if ('wakeLock' in navigator) {
const response = await fetch('/api/state'); wakeLock = await navigator.wakeLock.request('screen');
console.log('Response status:', response.status); console.log('Wake lock aktiviran');
const data = await response.json();
console.log('Response data:', data); wakeLock.addEventListener('release', () => {
console.log('Wake lock sproščen');
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');
} }
} catch (err) {
console.log('Wake lock ni uspel:', err);
}
}
// ob vrnitvi v zavihek ponovno zahtevaj wake lock
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') {
await requestWakeLock();
}
});
// posodobi stanje
async function updateState(force = false) {
try {
const response = await fetch('/api/state', { cache: 'no-store' });
const data = await response.json();
const signature = JSON.stringify({
current_text: data.current_text || '',
page_info: data.page_info || '',
caps_mode: data.caps_mode || false,
can_prev: !!data.can_prev
});
if (!force && signature === lastStateSignature) {
return;
}
lastStateSignature = signature;
displayArea.textContent = data.current_text || 'Pripravljeno. Vpiši številko pesmi ali drugega besedila.';
lastPageInfo = data.page_info || '';
updatePageInfoDisplay();
capsMode = data.caps_mode || false;
if (capsMode) {
darkBtn.classList.add('active');
} else {
darkBtn.classList.remove('active');
}
prevBtn.disabled = !data.can_prev; prevBtn.disabled = !data.can_prev;
nextBtn.disabled = !data.can_next;
console.log('updateState() uspešno zaključeno');
} catch (error) { } catch (error) {
console.error('Napaka pri posodabljanju stanja:', error); console.error('Napaka pri posodabljanju stanja:', error);
displayArea.innerHTML = '<span class="status-message">Napaka: ni povezave do strežnika</span>'; 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 // Posodobi prikaz v page-info (vključno z vnosom številke)
function updatePageInfoDisplay() {
const songNumber = songNumberInput.value;
if (songNumber) {
pageInfo.textContent = 'Vnos: ' + songNumber;
pageInfo.classList.add('input-active');
} else {
pageInfo.textContent = lastPageInfo;
pageInfo.classList.remove('input-active');
}
}
// dodaj številko
function addDigit(digit) {
songNumberInput.value += digit;
updatePageInfoDisplay();
}
// počisti vnos
function clearInput() {
songNumberInput.value = '';
updatePageInfoDisplay();
}
// Enter:
// - če je številka -> naloži pesem
// - če ni številke -> naslednja kitica
async function loadSong() { async function loadSong() {
console.log('loadSong() se kliče');
const songNumber = songNumberInput.value.trim(); const songNumber = songNumberInput.value.trim();
console.log('Song number:', songNumber);
if (!songNumber) return;
try { try {
console.log('Pošiljam POST za /api/load_song...'); if (songNumber) {
const response = await fetch('/api/load_song', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({song_number: songNumber}) }); await fetch('/api/load_song', {
console.log('Response status:', response.status); method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ song_number: songNumber })
});
songNumberInput.value = '';
updatePageInfoDisplay();
} else {
await fetch('/api/next_page', { method: 'POST' });
}
await updateState(true);
} catch (error) {
console.error('Napaka:', error);
displayArea.innerHTML = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
}
}
// Iskanje besedil po naslovu
async function searchSongs() {
const query = searchInput.value.trim();
if (!query) {
searchResults.innerHTML = '';
searchResults.classList.remove('show');
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(); const data = await response.json();
console.log('Response data:', data);
if (data.results && data.results.length > 0) {
songNumberInput.value = ''; searchResults.innerHTML = '';
updateState(); data.results.forEach(song => {
const item = document.createElement('div');
item.className = 'search-item';
item.innerHTML = `
<span class="song-id">${song[0]}</span>
<span class="song-title">${song[1]}</span>
`;
item.addEventListener('click', () => {
songNumberInput.value = song[0];
updatePageInfoDisplay();
loadSong();
searchInput.value = '';
searchResults.innerHTML = '';
searchResults.classList.remove('show');
});
searchResults.appendChild(item);
});
searchResults.classList.add('show');
} else {
searchResults.innerHTML = '<div class="search-item"><span class="song-title">Ni zadetkov</span></div>';
searchResults.classList.add('show');
}
} catch (error) { } catch (error) {
console.error('Napaka pri nalaganju pesmi:', error); console.error('Napaka pri iskanju:', error);
displayArea.innerHTML = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
} }
} }
// Naslednja stran // prejšnja kitica
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() { async function prevPage() {
try { try {
await fetch('/api/prev_page', {method: 'POST'}); await fetch('/api/prev_page', { method: 'POST' });
updateState(); await updateState(true);
} catch (error) { } catch (error) {
console.error('Napaka pri navigaciji:', error); console.error('Napaka pri navigaciji:', error);
displayArea.innerHTML = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
} }
} }
@@ -94,7 +220,7 @@ async function prevPage() {
async function clearScreen() { async function clearScreen() {
try { try {
await fetch('/api/clear_screen', {method: 'POST'}); await fetch('/api/clear_screen', {method: 'POST'});
updateState(); await updateState(true);
} catch (error) { } catch (error) {
console.error('Napaka pri zatamnitvi ekrana:', error); console.error('Napaka pri zatamnitvi ekrana:', error);
displayArea.innerHTML = '<span class="status-message">Napaka: ni povezave do strežnika</span>'; displayArea.innerHTML = '<span class="status-message">Napaka: ni povezave do strežnika</span>';
@@ -104,54 +230,348 @@ async function clearScreen() {
// Preklop VELIKIH ČRK // Preklop VELIKIH ČRK
async function toggleCaps() { async function toggleCaps() {
try { try {
const response = await fetch('/api/toggle_caps', {method: 'POST'}); await fetch('/api/toggle_caps', { method: 'POST' });
updateState(); await updateState(true);
} catch (error) { } catch (error) {
console.error('Napaka pri preklopa velikih črk:', error); displayArea.innerHTML = '<span class="status-message">Napaka: ni povezave do strežnika</span>'; } console.error('Napaka pri preklopu velikih črk:', error);
}
// 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, '&lt;').replace(/>/g, '&gt;').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 // Odpri urejevalnik
console.log('Dodajam event listenerje...'); async function openEditor(songId = null) {
loadBtn.addEventListener('click', loadSong); try {
console.log('loadBtn listener dodan'); if (songId === 'new') {
songNumberInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') loadSong(); }); editTitleInput.value = '';
console.log('songNumberInput listener dodan'); editLyricsInput.value = '';
editModal.dataset.songId = 'new';
editSongNumberDisplay.textContent = '';
editModal.style.display = 'block';
menuDropdown.classList.remove('show');
return;
}
/* searchBtn.addEventListener('click', searchSongs); const url = songId ? `/api/get_song_details?id=${songId}` : '/api/get_song_details';
searchQueryInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') searchSongs(); }); */ const response = await fetch(url);
const data = await response.json();
capsBtn.addEventListener('click', toggleCaps); if (data.status === 'ok') {
prevBtn.addEventListener('click', prevPage); editTitleInput.value = data.song.title;
nextBtn.addEventListener('click', nextPage); editLyricsInput.value = data.song.lyrics;
darkBtn.addEventListener('click', clearScreen); editModal.dataset.songId = data.song.id;
console.log('Vsi event listenerji dodani'); editSongNumberDisplay.textContent = data.song.id;
editModal.style.display = 'block';
menuDropdown.classList.remove('show');
} else {
if (!songId) {
// Če ni naložene pesmi, pokaži prompt
promptModal.style.display = 'block';
promptInput.value = '';
menuDropdown.classList.remove('show');
} else {
alert(data.message || 'Napaka pri pridobivanju podatkov.');
}
}
} catch (error) {
console.error('Napaka pri pridobivanju podatkov:', error);
alert('Napaka pri povezavi s strežnikom.');
}
}
// Začetna inicijalizacija function showInfo(message) {
console.log('Začenjam začetno inicijalizacijo...'); infoMessage.textContent = message;
updateState(); infoModal.style.display = 'block';
}
// Zapri urejevalnik
function closeEditor() {
editModal.style.display = 'none';
}
// Shrani spremembe
async function saveSongEdit() {
const songId = editModal.dataset.songId;
const title = editTitleInput.value.trim();
const lyrics = editLyricsInput.value.trim();
if (!title || !lyrics) {
alert('Naslov in besedilo ne smeta biti prazna.');
return;
}
try {
const response = await fetch('/api/update_song', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: songId,
title: title,
lyrics: lyrics
})
});
const data = await response.json();
if (data.status === 'ok') {
closeEditor();
if (data.new_id) {
showInfo(`Nova pesem je bila shranjena pod številko: ${data.new_id}`);
}
await updateState(true);
} else {
alert('Napaka pri shranjevanju: ' + data.message);
}
} catch (error) {
console.error('Napaka pri shranjevanju besedila:', error);
alert('Napaka pri povezavi s strežnikom.');
}
}
// skrij/pokaži tipkovnico
function toggleKeypad() {
if (!keypadWrapper || !toggleKeypadBtn) return;
keypadWrapper.classList.toggle('hidden');
if (keypadWrapper.classList.contains('hidden')) {
toggleKeypadBtn.textContent = 'Pokaži tipkovnico';
} else {
toggleKeypadBtn.textContent = 'Skrij tipkovnico';
}
}
// ============================
// EVENT LISTENERJI
// ============================
// številke na zaslonu
keypadButtons.forEach(btn => {
btn.addEventListener('click', () => {
vibrate();
const key = btn.dataset.key;
if (key !== undefined) {
addDigit(key);
}
});
});
// Enter
loadBtn.addEventListener('click', () => {
vibrate();
loadSong();
});
// C
clearBtn.addEventListener('click', () => {
vibrate();
clearInput();
});
// Nazaj
prevBtn.addEventListener('click', () => {
vibrate();
prevPage();
});
// Zastri
nextBtn.addEventListener('click', () => {
vibrate();
clearScreen();
});
// AAaa
darkBtn.addEventListener('click', () => {
vibrate();
toggleCaps();
});
// Skrij/Pokaži tipkovnico
if (toggleKeypadBtn) {
toggleKeypadBtn.addEventListener('click', (e) => {
toggleKeypad();
menuDropdown.classList.remove('show');
});
}
// Uredi besedilo
if (editSongBtn) {
editSongBtn.addEventListener('click', () => {
vibrate();
openEditor();
});
}
// Prekliči urejanje
if (cancelEditBtn) {
cancelEditBtn.addEventListener('click', () => {
vibrate();
closeEditor();
});
}
// Posodobi (Shrani) urejanje
if (saveEditBtn) {
saveEditBtn.addEventListener('click', () => {
vibrate();
saveSongEdit();
});
}
// Prompt Modal dogodki
if (promptEditBtn) {
promptEditBtn.addEventListener('click', () => {
const id = promptInput.value.trim();
if (id) {
promptModal.style.display = 'none';
openEditor(id);
} else {
alert('Vnesite številko pesmi.');
}
});
}
if (promptCancelBtn) {
promptCancelBtn.addEventListener('click', () => {
promptModal.style.display = 'none';
});
}
if (promptNewBtn) {
promptNewBtn.addEventListener('click', () => {
promptModal.style.display = 'none';
openEditor('new');
});
}
// Info Modal dogodki
if (infoOkBtn) {
infoOkBtn.addEventListener('click', () => {
infoModal.style.display = 'none';
});
}
// Hamburger menu toggle
if (menuToggle) {
menuToggle.addEventListener('click', (e) => {
e.stopPropagation();
menuDropdown.classList.toggle('show');
});
}
// Zapri menu ob kliku drugam
document.addEventListener('click', (e) => {
if (menuDropdown && !menuDropdown.contains(e.target) && e.target !== menuToggle) {
menuDropdown.classList.remove('show');
}
});
// Iskanje dogodki
if (searchInput) {
searchInput.addEventListener('input', () => {
searchSongs();
});
searchInput.addEventListener('focus', () => {
if (searchInput.value.trim()) {
searchResults.classList.add('show');
}
});
}
// Zapri rezultate iskanja ob kliku drugam
document.addEventListener('click', (e) => {
if (searchResults && !searchResults.contains(e.target) && e.target !== searchInput) {
searchResults.classList.remove('show');
}
});
// fizična tipkovnica
document.addEventListener('keydown', (e) => {
// Če smo v iskalnem polju, ne procesiraj bližnjic za numerično tipkovnico
if (document.activeElement === searchInput) {
if (e.key === 'Escape') {
searchInput.blur();
searchResults.classList.remove('show');
}
return;
}
// Če smo v urejevalniku, ne procesiraj bližnjic za numerično tipkovnico
if (editModal && editModal.style.display === 'block') {
if (e.key === 'Escape') {
closeEditor();
}
return;
}
// Če smo v promptu za številko, ne procesiraj bližnjic za numerično tipkovnico
if (promptModal && promptModal.style.display === 'block') {
if (e.key === 'Escape') {
promptModal.style.display = 'none';
}
return;
}
// na telefonu ni potrebe; na velikih ekranih pa naj dela
if (window.innerWidth < 901) return;
// da ne ponavlja pri držanju tipke
if (e.repeat) return;
// številke
if (e.key >= '0' && e.key <= '9') {
e.preventDefault();
addDigit(e.key);
return;
}
// Enter
if (e.key === 'Enter' || e.code === 'NumpadEnter') {
e.preventDefault();
loadSong();
return;
}
// C
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault();
clearInput();
return;
}
// Zastri
if (e.key === '+' || e.code === 'NumpadAdd') {
e.preventDefault();
clearScreen();
return;
}
// Nazaj
if (e.key === '-' || e.code === 'NumpadSubtract') {
e.preventDefault();
prevPage();
return;
}
// AAaa
if (e.key === '*' || e.code === 'NumpadMultiply') {
e.preventDefault();
toggleCaps();
return;
}
// numpad številke
if (e.code.startsWith('Numpad') && /\d/.test(e.key)) {
e.preventDefault();
addDigit(e.key);
}
});
// začetno stanje
updateState(true);
requestWakeLock();
// osveževanje za sinhronizacijo med več napravami
setInterval(() => {
updateState(false);
}, 1000);
// Osveži stanje vsako sekundo (za sinhronizacijo s tipkovnico)
// setInterval(updateState, 1000);
console.log('JavaScript inicializacija zaključena'); console.log('JavaScript inicializacija zaključena');

View File

@@ -4,101 +4,256 @@
box-sizing: border-box; box-sizing: border-box;
} }
:root {
--vh: 100vh;
--container-min-h: 100vh;
--keypad-gap: 10px;
--panel-bg: #111111;
--body-bg: #000000;
--text-main: #ffffff;
--text-muted: #aaaaaa;
}
@supports (height: 100svh) {
:root {
--vh: 100svh;
--container-min-h: 100svh;
}
}
@supports (height: 100dvh) {
:root {
--vh: 100dvh;
}
}
body { body {
font-family: 'Times New Roman', serif; font-family: 'Times New Roman', serif;
background-color: #0a0a0a; background-color: #0a0a0a;
color: #ffffff; color: #ffffff;
display: flex; min-height: var(--container-min-h);
flex-direction: column; overflow: auto;
height: 100vh;
overflow: hidden; /* Prevent body scroll */
} }
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; min-height: var(--vh);
padding: 0; height: var(--vh);
overflow: hidden; overflow: hidden;
background-color: var(--body-bg);
} }
/* Gornja vrstica - Vnos pesmi */ /* skrito vnosno polje - script.js ga še vedno uporablja */
.top-bar { #song-number {
background-color: #1a1a1a; display: none;
padding: 15px; }
border-bottom: 2px solid #333;
/* vrstica s stanjem */
.status-bar {
background-color: #111111;
border-bottom: 1px solid #222;
color: #dddddd;
padding: 8px 12px;
flex-shrink: 0;
min-height: 40px;
display: flex; display: flex;
gap: 10px;
align-items: center; align-items: center;
flex-wrap: wrap; justify-content: space-between;
flex-shrink: 0; /* Keep fixed size */ gap: 12px;
} }
.top-bar label { .page-info {
font-size: 16px; color: #cccccc;
font-size: 20px;
line-height: 1.2;
white-space: nowrap;
transition: color 0.2s ease, font-weight 0.2s ease;
}
.page-info.input-active {
color: #1f8a46;
font-weight: bold; font-weight: bold;
} }
.top-bar input { .menu-container {
padding: 10px 15px; position: relative;
font-size: 16px; }
background-color: #2a2a2a;
.search-container {
position: relative;
flex: 1;
max-width: 400px;
margin: 0 auto;
}
#search-input {
width: 100%;
padding: 6px 12px;
border-radius: 20px;
border: 1px solid #444; border: 1px solid #444;
color: #ffffff; background-color: #222;
border-radius: 4px; color: #fff;
width: 100px;
}
.top-bar button {
padding: 10px 20px;
font-size: 16px; font-size: 16px;
background-color: #3a7ca5; outline: none;
border: none;
color: white;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s;
} }
.top-bar button:hover { #search-input:focus {
background-color: #2d6183; border-color: #1f8a46;
background-color: #2a2a2a;
} }
.caps-toggle { .search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: #222;
border: 1px solid #444;
border-top: none;
border-radius: 0 0 8px 8px;
max-height: 300px;
overflow-y: auto;
z-index: 1001;
display: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.search-results.show {
display: block;
}
.search-item {
padding: 10px 15px; padding: 10px 15px;
font-size: 14px; border-bottom: 1px solid #333;
background-color: #4a4a4a;
border: 1px solid #666;
color: #ffffff;
cursor: pointer; cursor: pointer;
border-radius: 4px; display: flex;
transition: background-color 0.3s; gap: 10px;
} }
.caps-toggle.active { .search-item:last-child {
background-color: #3a7ca5; border-bottom: none;
} }
/* Sredenska vrstica - Prikaz besedila */ .search-item:hover {
background-color: #333;
}
.search-item .song-id {
color: #1f8a46;
font-weight: bold;
min-width: 30px;
}
.search-item .song-title {
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.menu-toggle {
background: transparent;
border: none;
cursor: pointer;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.hamburger-icon {
position: relative;
width: 24px;
height: 2px;
background-color: #fff;
display: block;
}
.hamburger-icon::before,
.hamburger-icon::after {
content: '';
position: absolute;
width: 24px;
height: 2px;
background-color: #fff;
left: 0;
}
.hamburger-icon::before {
top: -8px;
}
.hamburger-icon::after {
bottom: -8px;
}
.menu-dropdown {
display: none;
position: absolute;
top: 100%;
right: 0;
background-color: #222;
border: 1px solid #444;
border-radius: 8px;
min-width: 200px;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
overflow: hidden;
}
.menu-dropdown.show {
display: block;
}
.menu-item {
display: block;
width: 100%;
padding: 12px 16px;
background: transparent;
border: none;
color: #fff;
text-align: left;
font-size: 16px;
cursor: pointer;
border-bottom: 1px solid #333;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:hover {
background-color: #333;
}
.menu-item.active {
background-color: #444;
font-weight: bold;
}
/* glavni prikaz besedila */
.content { .content {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: flex-start; /* Better for scrolling long text */ align-items: flex-start;
justify-content: center; justify-content: center;
padding: 20px; padding: 20px 16px;
overflow-y: auto; overflow-y: auto;
background-color: #000000; background-color: #000000;
-webkit-overflow-scrolling: touch; /* Smooth scroll on iOS */ -webkit-overflow-scrolling: touch;
} }
.lyrics-display { .lyrics-display {
text-align: center; text-align: center;
font-size: 24px; /* Adjusted for mobile */ font-size: 24px;
line-height: 1.4; line-height: 1.45;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
max-width: 100%; max-width: 100%;
width: 100%;
padding-bottom: 20px; padding-bottom: 20px;
color: #ffffff;
} }
.status-message { .status-message {
@@ -106,125 +261,270 @@ body {
font-size: 20px; font-size: 20px;
} }
/* Donja vrstica - Navigacijski gumbi */ /* ovijalec tipkovnice */
.bottom-bar { .keypad-wrapper {
background-color: #1a1a1a; flex-shrink: 0;
padding: 15px; background-color: #111111;
border-top: 2px solid #333; border-top: 1px solid #222;
display: flex;
justify-content: space-around;
gap: 10px;
align-items: center;
flex-shrink: 0; /* Keep fixed size */
} }
.bottom-bar button { .keypad-wrapper.hidden {
padding: 12px 10px; display: none;
font-size: 16px; }
/* numerična tipkovnica */
.keypad {
background-color: #111111;
padding: 12px;
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--keypad-gap);
width: 100%;
}
/* osnovni videz gumbov */
.keypad button {
min-height: 58px;
border: none; border: none;
color: white; border-radius: 10px;
cursor: pointer; cursor: pointer;
border-radius: 4px; font-family: Arial, sans-serif;
transition: background-color 0.3s; font-size: 28px;
flex: 1; font-weight: bold;
min-width: 0; /* Allow shrinking */ transition: transform 0.05s ease, filter 0.2s ease, opacity 0.2s ease, box-shadow 0.05s ease;
max-width: 120px; -webkit-tap-highlight-color: transparent;
box-shadow: 0 3px 0 rgba(0, 0, 0, 0.35);
} }
.btn-nav { .keypad button:active {
background-color: #556b79; transform: scale(0.80);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
} }
.btn-nav:hover { .keypad button:disabled {
background-color: #3d4a52; opacity: 0.45;
}
.btn-nav:disabled {
background-color: #2a2a2a;
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5;
} }
.btn-dark { /* številke */
background-color: #8b4513; .btn-key {
background-color: #f2f2f2;
color: #111111;
} }
.btn-dark:hover { /* Zastri / Nazaj */
background-color: #6b3410; .btn-action {
background-color: #c28b24;
color: #ffffff;
font-size: 20px;
} }
.page-info { /* Placeholder for removed AAaa button */
color: #999; .keypad-placeholder {
visibility: hidden;
}
/* C */
.btn-clear {
background-color: #555555;
color: #ffffff;
font-size: 20px;
}
/* Enter čez dve vrstici */
.btn-enter {
background-color: #1f8a46;
color: #ffffff;
grid-row: span 2;
font-size: 24px;
}
/* hover samo za naprave z miško */
@media (hover: hover) and (pointer: fine) {
.btn-key:hover {
filter: brightness(0.92);
}
.btn-action:hover,
.btn-dark:hover,
.btn-clear:hover,
.btn-enter:hover {
filter: brightness(1.08);
}
}
/* tablica / manjši laptop */
@media (max-width: 900px) {
.lyrics-display {
font-size: 22px;
}
.page-info {
font-size: 18px;
}
.keypad button {
min-height: 54px;
font-size: 24px;
}
}
/* telefon */
@media (max-width: 600px) {
.content {
padding: 14px 10px;
}
.lyrics-display {
font-size: 20px;
line-height: 1.4;
}
.status-message {
font-size: 18px;
}
.page-info {
font-size: 18px;
}
.keypad {
gap: 8px;
padding: 10px;
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
}
.keypad button {
min-height: 52px;
font-size: 22px;
border-radius: 9px;
}
}
/* zelo majhen telefon */
@media (max-width: 400px) {
.lyrics-display {
font-size: 18px;
}
.page-info {
font-size: 16px;
}
.keypad button {
min-height: 48px;
font-size: 20px;
}
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
overflow-y: auto;
}
.modal-content {
background-color: #222;
margin: 5% auto;
padding: 20px;
border: 1px solid #444;
width: 90%;
max-width: 800px;
border-radius: 8px;
color: white;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-control {
width: 100%;
padding: 10px;
background-color: #333;
border: 1px solid #555;
color: white;
border-radius: 4px;
box-sizing: border-box;
font-size: 16px; font-size: 16px;
min-width: 60px;
} }
/* ====== Viewport & varni odmik (mobilni) ====== */ textarea.form-control {
resize: vertical;
/* Fallback: najprej uporabimo 100vh, potem nove enote (svh/dvh) kjer so podprte */ font-family: inherit;
:root {
--vh: 100vh; /* fallback */
--container-min-h: 100vh; /* fallback */
--bottom-bar-h: 60px; /* realna višina spodnje vrstice (glej .bottom-bar padding) */
} }
/* Novejši brskalniki stabilna višina toolbara */ .modal-footer {
@supports (height: 100svh) { display: flex;
:root { --vh: 100svh; --container-min-h: 100svh; } justify-content: flex-end;
} gap: 10px;
/* Alternativa: dinamična višina (ko toolbar skrije/prikaže) */ margin-top: 20px;
@supports (height: 100dvh) {
:root { --vh: 100dvh; }
} }
/* Odstrani zaklepanje scrolla to pogosto povzroči prekrivanje na mobilnih */ .btn-primary {
body { background-color: #1f8a46;
/* odstrani prejšnje overflow: hidden; */ color: white;
overflow: auto !important; border: none;
min-height: var(--container-min-h); padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
} }
/* Glavni vsebnik naj razpne celoten pogled in upošteva varne robove */ .btn-secondary {
.container { background-color: #555;
min-height: var(--vh); color: white;
padding-bottom: calc(var(--bottom-bar-h) + env(safe-area-inset-bottom, 0px)); border: none;
/* ostalo iz tvojega CSS lahko ostane */ padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
} }
/* Vsebinski del naj dopušča scroll, kot že imaš */ .btn-primary:hover {
.content { filter: brightness(1.1);
/* ostane: flex:1; overflow-y:auto; ... */
/* vendar dodamo malo dna, da zadnja vrstica besedila ne trči v panel */
padding-bottom: 12px;
} }
/* Spodnja vrstica naj bo vedno vidna in nad UI brskalnika */ .btn-secondary:hover {
.bottom-bar { filter: brightness(1.1);
position: sticky; /* ostane z dokumentom, a se prilepi na dno ob scrollu */
bottom: 0;
z-index: 1000; /* nad vsebino */
/* ozadje + robovi ostanejo */
padding-bottom: calc(15px + env(safe-area-inset-bottom, 0px));
/* poravnaj navidezno višino: */
min-height: var(--bottom-bar-h);
} }
/* Gumbi v spodnjem panelu: malenkost večja višina za lažji tap */ /* veliki ekrani */
.bottom-bar button { @media (min-width: 901px) {
min-height: 44px; /* Apple/Google priporočilo za tap targets */
}
/* Top bar: zadrži stabilno višino, a brez vpliva na dno */ .keypad {
.top-bar { width: fit-content;
position: sticky; margin: 0 auto;
top: 0; grid-template-columns: repeat(4, 120px);
z-index: 1000; gap: 10px;
} }
/* --- (Opcijsko) tema: prilagodi spodnji panel za boljši kontrast nad browser UI --- */ .keypad button {
@supports (backdrop-filter: blur(4px)) { height: 80px;
.bottom-bar { min-height: 80px;
background-color: rgba(26, 26, 26, 0.95); font-size: 22px;
backdrop-filter: saturate(120%) blur(4px); }
}
.btn-action,
.btn-dark,
.btn-clear {
font-size: 18px;
}
.btn-enter {
font-size: 22px;
height: auto;
}
} }

View File

@@ -3,41 +3,114 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title>Projektor pesmi - Web sučelje</title> <title>Projekcija besedil</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<!-- Gornja vrstica -->
<div class="top-bar"> <input type="text" id="song-number">
<!-- <label for="song-number">Pesem:</label> -->
<input type="number" id="song-number" placeholder="Vpiši številko" min="0"> <div class="status-bar">
<button id="load-btn">Naloži</button> <span id="page-info" class="page-info"></span>
<div class="search-container">
<!-- <label for="search-query">Iskanje:</label> <input type="text" id="search-input" placeholder="Išči po naslovu ..." autocomplete="off">
<input type="text" id="search-query" placeholder="Naslovna beseda..."> <div id="search-results" class="search-results"></div>
<button id="search-btn">Išči</button> -->
<button id="caps-btn" class="btn-dark">A/a</button>
</div> </div>
<div class="menu-container">
<!-- Sredenska vrstica - Prikaz --> <button id="menu-toggle" class="menu-toggle" type="button">
<div class="content"> <span class="hamburger-icon"></span>
<div id="display-area" class="lyrics-display"> </button>
<span class="status-message">Pripravljeno. Vpiši številko pesmi.</span> <div id="menu-dropdown" class="menu-dropdown">
<button id="toggle-keypad-btn" class="menu-item" type="button">Skrij tipkovnico</button>
<button id="dark-btn" class="menu-item" type="button">Velike/male črke (AAaa)</button>
<button id="edit-song-btn" class="menu-item" type="button">Uredi besedilo</button>
</div> </div>
</div> </div>
</div>
<!-- Spodnja vrstica - Navigacija -->
<div class="bottom-bar"> <!-- Edit Modal -->
<button id="prev-btn" class="btn-nav">◀ Nazaj</button> <div id="edit-modal" class="modal">
<span id="page-info" class="page-info"></span> <div class="modal-content">
<button id="dark-btn" class="btn-dark">**</button> <h2 id="edit-modal-title">Uredi besedilo <span id="edit-song-number-display" style="float: right; color: var(--text-muted);"></span></h2>
<span id="page-info-right" class="page-info"></span> <div class="form-group">
<button id="next-btn" class="btn-nav">Naprej ▶</button> <label for="edit-title">Naslov:</label>
<input type="text" id="edit-title" class="form-control">
</div>
<div class="form-group">
<label for="edit-lyrics">Besedilo:</label>
<textarea id="edit-lyrics" class="form-control" rows="15"></textarea>
</div>
<div class="modal-footer">
<button id="cancel-edit-btn" class="btn-secondary">Prekliči</button>
<button id="save-edit-btn" class="btn-primary">Posodobi</button>
</div>
</div> </div>
</div> </div>
<script src="{{ url_for('static', filename='script.js') }}"></script> <!-- Prompt Modal -->
<div id="prompt-modal" class="modal">
<div class="modal-content">
<h2 id="prompt-title">Uredi besedilo</h2>
<p id="prompt-message">Nobena pesem ni naložena. Vnesite številko pesmi za urejanje ali ustvarite novo.</p>
<div class="form-group">
<input type="number" id="prompt-input" class="form-control" placeholder="Številka pesmi">
</div>
<div class="modal-footer" style="justify-content: space-between;">
<button id="prompt-new-btn" class="btn-secondary" style="background-color: #c28b24; color: white; border: none;">Ustvari novo</button>
<div style="display: flex; gap: 10px;">
<button id="prompt-cancel-btn" class="btn-secondary">Prekliči</button>
<button id="prompt-edit-btn" class="btn-primary">Uredi</button>
</div>
</div>
</div>
</div>
<!-- Info Modal -->
<div id="info-modal" class="modal">
<div class="modal-content">
<h2 id="info-title">Informacija</h2>
<p id="info-message"></p>
<div class="modal-footer">
<button id="info-ok-btn" class="btn-primary">V redu</button>
</div>
</div>
</div>
<div class="content">
<div id="display-area" class="lyrics-display">
<span class="status-message">Pripravljeno. Vpiši številko pesmi ali drugega besedila.</span>
</div>
</div>
<div id="keypad-wrapper" class="keypad-wrapper">
<div class="keypad">
<button class="btn-key" data-key="7">7</button>
<button class="btn-key" data-key="8">8</button>
<button class="btn-key" data-key="9">9</button>
<button id="next-btn" class="btn-action">Zastri</button>
<button class="btn-key" data-key="4">4</button>
<button class="btn-key" data-key="5">5</button>
<button class="btn-key" data-key="6">6</button>
<button id="prev-btn" class="btn-action">Nazaj</button>
<button class="btn-key" data-key="1">1</button>
<button class="btn-key" data-key="2">2</button>
<button class="btn-key" data-key="3">3</button>
<button id="load-btn" class="btn-enter">Enter</button>
<div class="keypad-placeholder"></div>
<button class="btn-key" data-key="0">0</button>
<button id="clear-btn" class="btn-clear">C</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body> </body>
</html> </html>