8 Commits
0.2 ... 0.4

9 changed files with 820 additions and 73 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.
@@ -17,7 +17,7 @@ A Tkinter-based song projector application with optional Flask web interface for
## Project Structure
```
Projekcija/
projekcija/
├── projector.py # Main Tkinter application
├── nastavitve.py # Settings defaults and initialization
├── add_song.py # Utility to import songs into database

View File

@@ -11,6 +11,8 @@ import sys
import ctypes
import tkinter.messagebox as messagebox
from web.server import start_server_thread
import urllib.request
import tempfile
DB_PATH = 'songs.db'
SETTINGS_PATH = 'settings.json'
@@ -25,7 +27,7 @@ class SongProjector:
try:
# Odpri read-only; ne bo ustvaril prazne baze, če datoteka manjka
# 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()
except sqlite3.OperationalError as e:
# Jasno sporočilo in varen izhod
@@ -50,7 +52,8 @@ class SongProjector:
"screen_width_percent": 60,
"font_bold": 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):
@@ -169,6 +172,7 @@ class SongProjector:
"""
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
@@ -176,14 +180,21 @@ class SongProjector:
# Uporabimo c_uint za zagotovitev pravilnega tipa
ctypes.windll.kernel32.SetThreadExecutionState(ctypes.c_uint(0x80000003))
elif sys.platform == "linux":
# Zaženi inhibitor, dokler tvoj program dela (hrani handle!)
inhibitor = subprocess.Popen([
"gnome-session-inhibit",
"--inhibit", "idle",
"--inhibit", "suspend",
"--reason", "Projecting lyrics",
"sleep", "infinity"
])
# 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}")
@@ -250,7 +261,7 @@ class SongProjector:
self.next_page()
# ------------------------------------------------------
# Nalaganje in obdelava pesmi
# Nalaganje in obdelava besedila
# ------------------------------------------------------
def load_song(self):
if not self.song_number:
@@ -294,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?
self.exit_program()
return
elif self.song_number == "9901":
self.update_songs_database()
return
try:
song_id = int(self.song_number)
@@ -436,7 +450,7 @@ class SongProjector:
self.song_info_label.config(text="")
# ------------------------------------------------------
# Iskanje pesmi
# Iskanje po naslovu
# ------------------------------------------------------
def search_song(self, event=None):
self.clear_screen()
@@ -497,6 +511,79 @@ class SongProjector:
else:
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
# ------------------------------------------------------
@@ -510,13 +597,13 @@ class SongProjector:
pass
elif sys.platform == "linux":
try:
# Ponovno vklopi ohranjevalnik in DPMS (privzete vrednosti)
subprocess.Popen(["xset", "s", "on", "+dpms"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# Za Wayland/DBus inhibit običajno ni treba eksplicitno sprostiti,
# če se proces konča, vendar bi za popolnost lahko uporabili UnInhibit,
# če bi shranili cookie. Ker ga nismo, se zanašamo na to, da DBus
# samodejno sprosti inhibit ob zaprtju povezave/procesa.
# 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

View File

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

View File

@@ -1,12 +1,12 @@
@echo off
cls
set "TARGET=%USERPROFILE%\OneDrive\Namizje\Projekcije cerkev"
set "TARGET=%USERPROFILE%\Projekcija"
set "BACKUP_DIR=%TARGET%\backup"
:: Najprej preveri ali obstaja mapa Projekcije cerkev na USB (D:)
IF EXIST "D:\Projekcije cerkev" (
echo Mapa 'Projekcije cerkev' na USB pogonu obstaja.
:: Najprej preveri ali obstaja mapa Projekcija na USB (D:)
IF EXIST "D:\Projekcija" (
echo Mapa 'Projekcija' na USB pogonu obstaja.
:: 1. Ustvari backup obstoječe baze na namizju, če obstaja
IF EXIST "%TARGET%\songs.db" (
@@ -38,7 +38,7 @@ IF EXIST "D:\Projekcije cerkev" (
echo Kopiram nove datoteke z USB na namizje...
:: Kopiranje z robocopy (/E - vse podmape, /XO - samo novejše datoteke, da ne povozimo backupa če ni treba)
robocopy "D:\Projekcije cerkev" "%TARGET%" /E
robocopy "D:\Projekcija" "%TARGET%" /E
echo Zagon projector.py ...
pushd "%TARGET%"

View File

@@ -1,11 +1,11 @@
#!/bin/bash
# Ciljna mapa na Linuxu (v uporabnikovem home-u)
TARGET="$HOME/Projekcije cerkev"
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 "Projekcije cerkev" 2>/dev/null | head -n 1)
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"

View File

@@ -65,7 +65,7 @@ def get_state():
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."
current_text = "Pripravljeno. Vpiši številko pesmi ali drugega besedila."
return jsonify({
'current_text': current_text,
@@ -133,7 +133,7 @@ def toggle_caps():
@app.route('/api/search_songs', methods=['POST'])
def search_songs():
"""Iskanje pesmi po naslovu"""
"""Iskanje besedil po naslovu"""
if _projector_app is None:
return jsonify({'results': []})
@@ -154,6 +154,91 @@ def search_songs():
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):
"""Zaženi Flask server"""
app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True)

View File

@@ -12,10 +12,34 @@ 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');
// 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 wakeLock = null;
let lastStateSignature = "";
let lastPageInfo = "";
// vibracija telefona
function vibrate() {
@@ -66,8 +90,9 @@ async function updateState(force = false) {
lastStateSignature = signature;
displayArea.textContent = data.current_text || 'Pripravljeno. Vpiši številko pesmi.';
pageInfo.textContent = data.page_info || '';
displayArea.textContent = data.current_text || 'Pripravljeno. Vpiši številko pesmi ali drugega besedila.';
lastPageInfo = data.page_info || '';
updatePageInfoDisplay();
capsMode = data.caps_mode || false;
@@ -84,14 +109,28 @@ async function updateState(force = false) {
}
}
// 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:
@@ -109,6 +148,7 @@ async function loadSong() {
});
songNumberInput.value = '';
updatePageInfoDisplay();
} else {
await fetch('/api/next_page', { method: 'POST' });
}
@@ -120,6 +160,52 @@ async function loadSong() {
}
}
// 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();
if (data.results && data.results.length > 0) {
searchResults.innerHTML = '';
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) {
console.error('Napaka pri iskanju:', error);
}
}
// prejšnja kitica
async function prevPage() {
try {
@@ -151,6 +237,94 @@ async function toggleCaps() {
}
}
// Odpri urejevalnik
async function openEditor(songId = null) {
try {
if (songId === 'new') {
editTitleInput.value = '';
editLyricsInput.value = '';
editModal.dataset.songId = 'new';
editSongNumberDisplay.textContent = '';
editModal.style.display = 'block';
menuDropdown.classList.remove('show');
return;
}
const url = songId ? `/api/get_song_details?id=${songId}` : '/api/get_song_details';
const response = await fetch(url);
const data = await response.json();
if (data.status === 'ok') {
editTitleInput.value = data.song.title;
editLyricsInput.value = data.song.lyrics;
editModal.dataset.songId = data.song.id;
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.');
}
}
function showInfo(message) {
infoMessage.textContent = message;
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;
@@ -211,11 +385,131 @@ darkBtn.addEventListener('click', () => {
// Skrij/Pokaži tipkovnico
if (toggleKeypadBtn) {
toggleKeypadBtn.addEventListener('click', toggleKeypad);
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;

View File

@@ -68,21 +68,168 @@ body {
font-size: 20px;
line-height: 1.2;
white-space: nowrap;
transition: color 0.2s ease, font-weight 0.2s ease;
}
.toggle-keypad-btn {
display: none;
background-color: #333;
.page-info.input-active {
color: #1f8a46;
font-weight: bold;
}
.menu-container {
position: relative;
}
.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;
background-color: #222;
color: #fff;
border: none;
border-radius: 8px;
padding: 8px 12px;
font-size: 15px;
cursor: pointer;
font-size: 16px;
outline: none;
}
.toggle-keypad-btn:hover {
filter: brightness(1.08);
#search-input:focus {
border-color: #1f8a46;
background-color: #2a2a2a;
}
.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;
border-bottom: 1px solid #333;
cursor: pointer;
display: flex;
gap: 10px;
}
.search-item:last-child {
border-bottom: none;
}
.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 */
@@ -173,16 +320,9 @@ body {
font-size: 20px;
}
/* AAaa */
.btn-dark {
background-color: #8b4513;
color: #ffffff;
font-size: 20px;
}
.btn-dark.active {
outline: 2px solid #ffffff;
outline-offset: -2px;
/* Placeholder for removed AAaa button */
.keypad-placeholder {
visibility: hidden;
}
/* C */
@@ -209,8 +349,7 @@ body {
.btn-action:hover,
.btn-dark:hover,
.btn-clear:hover,
.btn-enter:hover,
.toggle-keypad-btn:hover {
.btn-enter:hover {
filter: brightness(1.08);
}
}
@@ -279,11 +418,91 @@ body {
}
}
/* 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;
}
textarea.form-control {
resize: vertical;
font-family: inherit;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.btn-primary {
background-color: #1f8a46;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.btn-secondary {
background-color: #555;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.btn-primary:hover {
filter: brightness(1.1);
}
.btn-secondary:hover {
filter: brightness(1.1);
}
/* veliki ekrani */
@media (min-width: 901px) {
.toggle-keypad-btn {
display: inline-block;
}
.keypad {
width: fit-content;

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title>Projektor pesmi</title>
<title>Projekcija besedil</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
@@ -14,12 +14,73 @@
<div class="status-bar">
<span id="page-info" class="page-info"></span>
<button id="toggle-keypad-btn" class="toggle-keypad-btn" type="button">Skrij tipkovnico</button>
<div class="search-container">
<input type="text" id="search-input" placeholder="Išči po naslovu ..." autocomplete="off">
<div id="search-results" class="search-results"></div>
</div>
<div class="menu-container">
<button id="menu-toggle" class="menu-toggle" type="button">
<span class="hamburger-icon"></span>
</button>
<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>
<!-- Edit Modal -->
<div id="edit-modal" class="modal">
<div class="modal-content">
<h2 id="edit-modal-title">Uredi besedilo <span id="edit-song-number-display" style="float: right; color: var(--text-muted);"></span></h2>
<div class="form-group">
<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>
<!-- 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.</span>
<span class="status-message">Pripravljeno. Vpiši številko pesmi ali drugega besedila.</span>
</div>
</div>
@@ -41,7 +102,7 @@
<button class="btn-key" data-key="3">3</button>
<button id="load-btn" class="btn-enter">Enter</button>
<button id="dark-btn" class="btn-dark">AAaa</button>
<div class="keypad-placeholder"></div>
<button class="btn-key" data-key="0">0</button>
<button id="clear-btn" class="btn-clear">C</button>