Added camera enabling and disabling

This commit is contained in:
2026-06-04 16:32:30 +02:00
parent 988cb53de1
commit b71a685c58
12 changed files with 11539 additions and 168 deletions
+40 -18
View File
@@ -1,5 +1,5 @@
""" """
TV_APP V1.0.1 - Tournament and League Management System TV_APP V1.0.2 - Tournament and League Management System
Flask web application for managing tournaments with multi-camera streaming Flask web application for managing tournaments with multi-camera streaming
""" """
@@ -87,8 +87,8 @@ def archive_league(league_data):
def create_league(enabled_players, tournament_type): def create_league(enabled_players, tournament_type):
return Tournament.create_league(enabled_players, tournament_type) return Tournament.create_league(enabled_players, tournament_type)
def create_draft(enabled_players, tournament_type='20_targets', league_tournament_number=None): def create_draft(enabled_players, tournament_type='20_targets', league_tournament_number=None, active_slots=None):
return Tournament.create_draft(enabled_players, tournament_type, league_tournament_number) return Tournament.create_draft(enabled_players, tournament_type, league_tournament_number, active_slots)
def create_results_structure(tournament_data): def create_results_structure(tournament_data):
return Tournament.create_results_structure(tournament_data) return Tournament.create_results_structure(tournament_data)
@@ -826,14 +826,18 @@ def index():
if current_round_data: if current_round_data:
tournament_titles = {} tournament_titles = {}
players = current_round_data['players'] players = current_round_data['players']
disabled_cameras = settings.get('disabled_cameras', [])
for i in range(6): # Always 6 positions
if i < len(players):
tournament_titles[str(i + 1)] = players[i]['name']
else:
# Use "Prazno" for Slovenian, "Empty" for English
current_language = get_current_language() current_language = get_current_language()
tournament_titles[str(i + 1)] = 'Prazno' if current_language == 'sl' else 'Empty' player_idx = 0
for i in range(6):
cam_id = str(i + 1)
if i + 1 in disabled_cameras:
tournament_titles[cam_id] = 'Prazno' if current_language == 'sl' else 'Empty'
elif player_idx < len(players):
tournament_titles[cam_id] = players[player_idx]['name']
player_idx += 1
else:
tournament_titles[cam_id] = 'Prazno' if current_language == 'sl' else 'Empty'
# Create a copy of settings with tournament titles # Create a copy of settings with tournament titles
display_settings = settings.copy() display_settings = settings.copy()
@@ -1071,9 +1075,11 @@ def tournament_draft():
if not tournament_state: if not tournament_state:
return redirect('/tournament') return redirect('/tournament')
settings = load_settings()
return render_template('draft.html', return render_template('draft.html',
tournament=tournament_state, tournament=tournament_state,
league=league_state) league=league_state,
disabled_cameras=settings.get('disabled_cameras', []))
@app.route('/results/calculator') @app.route('/results/calculator')
def results_calculator(): def results_calculator():
@@ -1244,6 +1250,9 @@ def update_settings():
if 'display_options' in new_settings: if 'display_options' in new_settings:
current_settings['display_options'].update(new_settings['display_options']) current_settings['display_options'].update(new_settings['display_options'])
if 'disabled_cameras' in new_settings:
current_settings['disabled_cameras'] = new_settings['disabled_cameras']
# Save updated settings # Save updated settings
if save_settings(current_settings): if save_settings(current_settings):
return jsonify({'status': 'success', 'settings': current_settings}) return jsonify({'status': 'success', 'settings': current_settings})
@@ -1414,10 +1423,14 @@ def start_league_tournament():
# Start next tournament # Start next tournament
league_state['current_tournament'] = current_tournament + 1 league_state['current_tournament'] = current_tournament + 1
settings = load_settings()
disabled = settings.get('disabled_cameras', [])
active_slots = [i for i in range(1, 7) if i not in disabled]
tournament_data = create_draft( tournament_data = create_draft(
participating_players, participating_players,
league_state['tournament_type'], league_state['tournament_type'],
league_state['current_tournament'] league_state['current_tournament'],
active_slots
) )
# Save states # Save states
@@ -1474,7 +1487,10 @@ def start_tournament():
if len(enabled_players) < 1: if len(enabled_players) < 1:
return jsonify({'status': 'error', 'message': 'Need at least 1 enabled player'}), 400 return jsonify({'status': 'error', 'message': 'Need at least 1 enabled player'}), 400
tournament_data = create_draft(enabled_players, tournament_type) settings = load_settings()
disabled = settings.get('disabled_cameras', [])
active_slots = [i for i in range(1, 7) if i not in disabled]
tournament_data = create_draft(enabled_players, tournament_type, active_slots=active_slots)
if save_tournament_state(tournament_data): if save_tournament_state(tournament_data):
# Create results structure when tournament starts # Create results structure when tournament starts
@@ -1996,13 +2012,19 @@ def get_camera_titles():
titles = {} titles = {}
if current_round_data: if current_round_data:
# Tournament active - use player names # Tournament active - use player names, skip disabled cameras
players = current_round_data['players'] players = current_round_data['players']
disabled_cameras = settings.get('disabled_cameras', [])
player_idx = 0
for i in range(6): for i in range(6):
if i < len(players): cam_id = str(i + 1)
titles[str(i + 1)] = players[i]['name'] if i + 1 in disabled_cameras:
titles[cam_id] = 'Empty'
elif player_idx < len(players):
titles[cam_id] = players[player_idx]['name']
player_idx += 1
else: else:
titles[str(i + 1)] = 'Empty' titles[cam_id] = 'Empty'
else: else:
# No tournament - use configured titles # No tournament - use configured titles
titles = settings['camera_titles'] titles = settings['camera_titles']
@@ -2541,4 +2563,4 @@ def set_language(language):
}), 400 }), 400
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=4000, debug=True) app.run(host='0.0.0.0', port=5000, debug=True)
+10 -6
View File
@@ -42,19 +42,22 @@ class Tournament:
return league_data return league_data
@staticmethod @staticmethod
def create_draft(enabled_players, tournament_type='20_targets', league_tournament_number=None): def create_draft(enabled_players, tournament_type='20_targets', league_tournament_number=None, active_slots=None):
"""Create draft groups of 6 players and organize rounds""" """Create draft groups organized by shooting positions and organize rounds"""
if len(enabled_players) < 1: if len(enabled_players) < 1:
return None return None
slots = sorted(active_slots) if active_slots else list(range(1, NUM_CAMERAS + 1))
positions = len(slots)
# Shuffle players for random grouping # Shuffle players for random grouping
players_copy = enabled_players.copy() players_copy = enabled_players.copy()
random.shuffle(players_copy) random.shuffle(players_copy)
# Create groups of up to 6 # Create groups of up to positions
groups = [] groups = []
for i in range(0, len(players_copy), NUM_CAMERAS): for i in range(0, len(players_copy), positions):
group = players_copy[i:i + NUM_CAMERAS] group = players_copy[i:i + positions]
groups.append(group) groups.append(group)
# Create rounds # Create rounds
@@ -72,7 +75,8 @@ class Tournament:
'total_players': len(enabled_players), 'total_players': len(enabled_players),
'total_rounds': len(rounds), 'total_rounds': len(rounds),
'current_round': 1, 'current_round': 1,
'tournament_type': tournament_type 'tournament_type': tournament_type,
'active_slots': slots
} }
if league_tournament_number: if league_tournament_number:
+2 -1
View File
@@ -34,7 +34,8 @@ DEFAULT_SETTINGS = {
'display_options': { 'display_options': {
'show_titles': True, 'show_titles': True,
'title_size': 1.1 'title_size': 1.1
} },
'disabled_cameras': []
} }
+2 -1
View File
@@ -12,5 +12,6 @@
"title_size": 1.4, "title_size": 1.4,
"target_number_size": 1.7 "target_number_size": 1.7
}, },
"tv_view": "cameras" "tv_view": "cameras",
"disabled_cameras": []
} }
+43 -43
View File
@@ -3,32 +3,32 @@
{ {
"id": 1, "id": 1,
"name": "Domen Pleterski", "name": "Domen Pleterski",
"enabled": false "enabled": true
}, },
{ {
"id": 2, "id": 2,
"name": "Nik Pleterski", "name": "Nik Pleterski",
"enabled": false "enabled": true
}, },
{ {
"id": 3, "id": 3,
"name": "Ivan Tandler", "name": "Ivan Tandler",
"enabled": false "enabled": true
}, },
{ {
"id": 4, "id": 4,
"name": "Mateja Pleterski", "name": "Mateja Pleterski",
"enabled": false "enabled": true
}, },
{ {
"id": 5, "id": 5,
"name": "Jože Verhnjak", "name": "Jože Verhnjak",
"enabled": false "enabled": true
}, },
{ {
"id": 6, "id": 6,
"name": "Mateja Senica", "name": "Mateja Senica",
"enabled": false "enabled": true
}, },
{ {
"id": 7, "id": 7,
@@ -38,27 +38,27 @@
{ {
"id": 8, "id": 8,
"name": "Franc Žigart", "name": "Franc Žigart",
"enabled": false "enabled": true
}, },
{ {
"id": 9, "id": 9,
"name": "Janez Božič", "name": "Janez Božič",
"enabled": false "enabled": true
}, },
{ {
"id": 10, "id": 10,
"name": "Mitja Čeh", "name": "Mitja Čeh",
"enabled": false "enabled": true
}, },
{ {
"id": 11, "id": 11,
"name": "Rado Kefer", "name": "Rado Kefer",
"enabled": false "enabled": true
}, },
{ {
"id": 12, "id": 12,
"name": "Matej Kvasnik", "name": "Matej Kvasnik",
"enabled": false "enabled": true
}, },
{ {
"id": 13, "id": 13,
@@ -68,57 +68,57 @@
{ {
"id": 14, "id": 14,
"name": "Karli Proje", "name": "Karli Proje",
"enabled": false "enabled": true
}, },
{ {
"id": 15, "id": 15,
"name": "Jan Pleterski", "name": "Jan Pleterski",
"enabled": false "enabled": true
}, },
{ {
"id": 16, "id": 16,
"name": "Silvo Poročnik", "name": "Silvo Poročnik",
"enabled": false "enabled": true
}, },
{ {
"id": 17, "id": 17,
"name": "Dušan Onuk", "name": "Dušan Onuk",
"enabled": false "enabled": true
}, },
{ {
"id": 18, "id": 18,
"name": "Matjaž Pleterski", "name": "Matjaž Pleterski",
"enabled": false "enabled": true
}, },
{ {
"id": 19, "id": 19,
"name": "Franc Rizmal", "name": "Franc Rizmal",
"enabled": false "enabled": true
}, },
{ {
"id": 20, "id": 20,
"name": "Jože Preglav", "name": "Jože Preglav",
"enabled": false "enabled": true
}, },
{ {
"id": 21, "id": 21,
"name": "Marko Blimen", "name": "Marko Blimen",
"enabled": false "enabled": true
}, },
{ {
"id": 22, "id": 22,
"name": "Doris Fesel", "name": "Doris Fesel",
"enabled": false "enabled": true
}, },
{ {
"id": 23, "id": 23,
"name": "Robi Krautberger", "name": "Robi Krautberger",
"enabled": false "enabled": true
}, },
{ {
"id": 24, "id": 24,
"name": "Jože Verdinek", "name": "Jože Verdinek",
"enabled": false "enabled": true
}, },
{ {
"id": 25, "id": 25,
@@ -128,12 +128,12 @@
{ {
"id": 26, "id": 26,
"name": "Jakob Herman", "name": "Jakob Herman",
"enabled": false "enabled": true
}, },
{ {
"id": 27, "id": 27,
"name": "Janez Mrak", "name": "Janez Mrak",
"enabled": false "enabled": true
}, },
{ {
"id": 28, "id": 28,
@@ -148,12 +148,12 @@
{ {
"id": 30, "id": 30,
"name": "Maja Hirtl", "name": "Maja Hirtl",
"enabled": false "enabled": true
}, },
{ {
"id": 31, "id": 31,
"name": "Dejan Kučnik", "name": "Dejan Kučnik",
"enabled": false "enabled": true
}, },
{ {
"id": 32, "id": 32,
@@ -163,27 +163,27 @@
{ {
"id": 33, "id": 33,
"name": "Namir Uzunović", "name": "Namir Uzunović",
"enabled": false "enabled": true
}, },
{ {
"id": 34, "id": 34,
"name": "Jože Planinšec", "name": "Jože Planinšec",
"enabled": false "enabled": true
}, },
{ {
"id": 35, "id": 35,
"name": "Vanja Kolar", "name": "Vanja Kolar",
"enabled": false "enabled": true
}, },
{ {
"id": 36, "id": 36,
"name": "Klara Wankmuller", "name": "Klara Wankmuller",
"enabled": false "enabled": true
}, },
{ {
"id": 37, "id": 37,
"name": "Milan Stramec", "name": "Milan Stramec",
"enabled": false "enabled": true
}, },
{ {
"id": 38, "id": 38,
@@ -193,27 +193,27 @@
{ {
"id": 39, "id": 39,
"name": "Tia Sudar", "name": "Tia Sudar",
"enabled": false "enabled": true
}, },
{ {
"id": 40, "id": 40,
"name": "Jaka Cvar", "name": "Jaka Cvar",
"enabled": false "enabled": true
}, },
{ {
"id": 41, "id": 41,
"name": "Tadej Štruc", "name": "Tadej Štruc",
"enabled": false "enabled": true
}, },
{ {
"id": 42, "id": 42,
"name": "Jure Glaser", "name": "Jure Glaser",
"enabled": false "enabled": true
}, },
{ {
"id": 43, "id": 43,
"name": "Marko Pokržnik", "name": "Marko Pokržnik",
"enabled": false "enabled": true
}, },
{ {
"id": 44, "id": 44,
@@ -223,37 +223,37 @@
{ {
"id": 45, "id": 45,
"name": "Lidija Blimen", "name": "Lidija Blimen",
"enabled": false "enabled": true
}, },
{ {
"id": 46, "id": 46,
"name": "Tijana Štumpfl", "name": "Tijana Štumpfl",
"enabled": false "enabled": true
}, },
{ {
"id": 47, "id": 47,
"name": "Ljuba Mršak", "name": "Ljuba Mršak",
"enabled": false "enabled": true
}, },
{ {
"id": 48, "id": 48,
"name": "Janja Salcman", "name": "Janja Salcman",
"enabled": false "enabled": true
}, },
{ {
"id": 49, "id": 49,
"name": "Jolanda Verhnjak", "name": "Jolanda Verhnjak",
"enabled": false "enabled": true
}, },
{ {
"id": 50, "id": 50,
"name": "Vid Ravnjak", "name": "Vid Ravnjak",
"enabled": false "enabled": true
}, },
{ {
"id": 51, "id": 51,
"name": "Robi Ovčar", "name": "Robi Ovčar",
"enabled": false "enabled": true
} }
] ]
} }
@@ -0,0 +1,777 @@
{
"tournament": {
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 29,
"name": "Alen Kolar",
"enabled": true
},
{
"id": 32,
"name": "David Strniša",
"enabled": true
},
{
"id": 44,
"name": "Anka Kačnik",
"enabled": true
},
{
"id": 38,
"name": "Bojan Sudar",
"enabled": true
},
{
"id": 25,
"name": "Andrej Herman",
"enabled": true
},
{
"id": 28,
"name": "Anže Kolar",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 2,
"players": [
{
"id": 13,
"name": "Angelca Mrak",
"enabled": true
},
{
"id": 7,
"name": "Branko Pokeržnik",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2026-04-19T14:50:47.646818",
"total_players": 8,
"total_rounds": 2,
"current_round": 1,
"tournament_type": "20_targets"
},
"results": {
"tournament_id": "2026-04-19T14:50:47.646818",
"tournament_type": "20_targets",
"participants": {
"29": {
"name": "Alen Kolar",
"targets": {
"1": {
"shot1": 5,
"shot2": 3
},
"2": {
"shot1": 6,
"shot2": 3
},
"3": {
"shot1": 9,
"shot2": 2
},
"4": {
"shot1": 10,
"shot2": 10
},
"5": {
"shot1": 10,
"shot2": 0
},
"6": {
"shot1": 8,
"shot2": 4
},
"7": {
"shot1": 4,
"shot2": 9
},
"8": {
"shot1": 2,
"shot2": 10
},
"9": {
"shot1": 4,
"shot2": 4
},
"10": {
"shot1": 9,
"shot2": 3
},
"11": {
"shot1": 7,
"shot2": 6
},
"12": {
"shot1": 3,
"shot2": 4
},
"13": {
"shot1": 5,
"shot2": 2
},
"14": {
"shot1": 10,
"shot2": 2
},
"15": {
"shot1": 10,
"shot2": 1
},
"16": {
"shot1": 8,
"shot2": 2
},
"17": {
"shot1": 3,
"shot2": 2
},
"18": {
"shot1": 0,
"shot2": 2
},
"19": {
"shot1": 10,
"shot2": 3
},
"20": {
"shot1": 9,
"shot2": 0
}
},
"total_score": 204,
"completed": true,
"joker_selected": false
},
"32": {
"name": "David Strniša",
"targets": {
"1": {
"shot1": 1,
"shot2": 3
},
"2": {
"shot1": 4,
"shot2": 6
},
"3": {
"shot1": 7,
"shot2": 8
},
"4": {
"shot1": 10,
"shot2": 5
},
"5": {
"shot1": 2,
"shot2": 1
},
"6": {
"shot1": 9,
"shot2": 6
},
"7": {
"shot1": 9,
"shot2": 2
},
"8": {
"shot1": 2,
"shot2": 10
},
"9": {
"shot1": 1,
"shot2": 7
},
"10": {
"shot1": 6,
"shot2": 1
},
"11": {
"shot1": 4,
"shot2": 10
},
"12": {
"shot1": 1,
"shot2": 7
},
"13": {
"shot1": 4,
"shot2": 0
},
"14": {
"shot1": 10,
"shot2": 3
},
"15": {
"shot1": 0,
"shot2": 7
},
"16": {
"shot1": 2,
"shot2": 10
},
"17": {
"shot1": 5,
"shot2": 10
},
"18": {
"shot1": 2,
"shot2": 7
},
"19": {
"shot1": 4,
"shot2": 0
},
"20": {
"shot1": 10,
"shot2": 0
}
},
"total_score": 196,
"completed": true,
"joker_selected": false
},
"44": {
"name": "Anka Kačnik",
"targets": {
"1": {
"shot1": 8,
"shot2": 3
},
"2": {
"shot1": 2,
"shot2": 10
},
"3": {
"shot1": 10,
"shot2": 9
},
"4": {
"shot1": 9,
"shot2": 8
},
"5": {
"shot1": 1,
"shot2": 5
},
"6": {
"shot1": 3,
"shot2": 1
},
"7": {
"shot1": 5,
"shot2": 9
},
"8": {
"shot1": 7,
"shot2": 1
},
"9": {
"shot1": 9,
"shot2": 7
},
"10": {
"shot1": 4,
"shot2": 0
},
"11": {
"shot1": 6,
"shot2": 5
},
"12": {
"shot1": 5,
"shot2": 6
},
"13": {
"shot1": 1,
"shot2": 8
},
"14": {
"shot1": 2,
"shot2": 1
},
"15": {
"shot1": 5,
"shot2": 8
},
"16": {
"shot1": 10,
"shot2": 5
},
"17": {
"shot1": 3,
"shot2": 1
},
"18": {
"shot1": 1,
"shot2": 8
},
"19": {
"shot1": 5,
"shot2": 8
},
"20": {
"shot1": 6,
"shot2": 8
}
},
"total_score": 213,
"completed": true,
"joker_selected": false
},
"38": {
"name": "Bojan Sudar",
"targets": {
"1": {
"shot1": 10,
"shot2": 4
},
"2": {
"shot1": 6,
"shot2": 5
},
"3": {
"shot1": 10,
"shot2": 9
},
"4": {
"shot1": 9,
"shot2": 7
},
"5": {
"shot1": 0,
"shot2": 8
},
"6": {
"shot1": 2,
"shot2": 6
},
"7": {
"shot1": 0,
"shot2": 2
},
"8": {
"shot1": 1,
"shot2": 2
},
"9": {
"shot1": 0,
"shot2": 7
},
"10": {
"shot1": 7,
"shot2": 9
},
"11": {
"shot1": 3,
"shot2": 9
},
"12": {
"shot1": 7,
"shot2": 8
},
"13": {
"shot1": 8,
"shot2": 10
},
"14": {
"shot1": 9,
"shot2": 1
},
"15": {
"shot1": 5,
"shot2": 5
},
"16": {
"shot1": 1,
"shot2": 5
},
"17": {
"shot1": 5,
"shot2": 4
},
"18": {
"shot1": 8,
"shot2": 9
},
"19": {
"shot1": 0,
"shot2": 4
},
"20": {
"shot1": 1,
"shot2": 6
}
},
"total_score": 212,
"completed": true,
"joker_selected": false
},
"25": {
"name": "Andrej Herman",
"targets": {
"1": {
"shot1": 8,
"shot2": 3
},
"2": {
"shot1": 9,
"shot2": 6
},
"3": {
"shot1": 7,
"shot2": 9
},
"4": {
"shot1": 2,
"shot2": 2
},
"5": {
"shot1": 10,
"shot2": 0
},
"6": {
"shot1": 8,
"shot2": 7
},
"7": {
"shot1": 10,
"shot2": 4
},
"8": {
"shot1": 9,
"shot2": 6
},
"9": {
"shot1": 4,
"shot2": 2
},
"10": {
"shot1": 2,
"shot2": 10
},
"11": {
"shot1": 7,
"shot2": 7
},
"12": {
"shot1": 0,
"shot2": 5
},
"13": {
"shot1": 4,
"shot2": 9
},
"14": {
"shot1": 6,
"shot2": 7
},
"15": {
"shot1": 1,
"shot2": 6
},
"16": {
"shot1": 5,
"shot2": 1
},
"17": {
"shot1": 4,
"shot2": 9
},
"18": {
"shot1": 2,
"shot2": 7
},
"19": {
"shot1": 6,
"shot2": 6
},
"20": {
"shot1": 1,
"shot2": 1
}
},
"total_score": 212,
"completed": true,
"joker_selected": false
},
"28": {
"name": "Anže Kolar",
"targets": {
"1": {
"shot1": 4,
"shot2": 10
},
"2": {
"shot1": 4,
"shot2": 0
},
"3": {
"shot1": 2,
"shot2": 0
},
"4": {
"shot1": 10,
"shot2": 8
},
"5": {
"shot1": 5,
"shot2": 0
},
"6": {
"shot1": 1,
"shot2": 0
},
"7": {
"shot1": 6,
"shot2": 1
},
"8": {
"shot1": 8,
"shot2": 3
},
"9": {
"shot1": 7,
"shot2": 9
},
"10": {
"shot1": 4,
"shot2": 2
},
"11": {
"shot1": 8,
"shot2": 7
},
"12": {
"shot1": 10,
"shot2": 1
},
"13": {
"shot1": 1,
"shot2": 10
},
"14": {
"shot1": 2,
"shot2": 3
},
"15": {
"shot1": 10,
"shot2": 6
},
"16": {
"shot1": 0,
"shot2": 7
},
"17": {
"shot1": 6,
"shot2": 1
},
"18": {
"shot1": 8,
"shot2": 10
},
"19": {
"shot1": 2,
"shot2": 0
},
"20": {
"shot1": 2,
"shot2": 8
}
},
"total_score": 186,
"completed": true,
"joker_selected": false
},
"13": {
"name": "Angelca Mrak",
"targets": {
"1": {
"shot1": 9,
"shot2": 5
},
"2": {
"shot1": 2,
"shot2": 3
},
"3": {
"shot1": 3,
"shot2": 0
},
"4": {
"shot1": 2,
"shot2": 1
},
"5": {
"shot1": 4,
"shot2": 1
},
"6": {
"shot1": 6,
"shot2": 8
},
"7": {
"shot1": 8,
"shot2": 9
},
"8": {
"shot1": 3,
"shot2": 4
},
"9": {
"shot1": 3,
"shot2": 1
},
"10": {
"shot1": 10,
"shot2": 9
},
"11": {
"shot1": 6,
"shot2": 10
},
"12": {
"shot1": 3,
"shot2": 10
},
"13": {
"shot1": 2,
"shot2": 5
},
"14": {
"shot1": 10,
"shot2": 5
},
"15": {
"shot1": 7,
"shot2": 2
},
"16": {
"shot1": 6,
"shot2": 7
},
"17": {
"shot1": 0,
"shot2": 6
},
"18": {
"shot1": 6,
"shot2": 0
},
"19": {
"shot1": 8,
"shot2": 6
},
"20": {
"shot1": 5,
"shot2": 2
}
},
"total_score": 197,
"completed": true,
"joker_selected": false
},
"7": {
"name": "Branko Pokeržnik",
"targets": {
"1": {
"shot1": 5,
"shot2": 0
},
"2": {
"shot1": 7,
"shot2": 0
},
"3": {
"shot1": 1,
"shot2": 9
},
"4": {
"shot1": 0,
"shot2": 4
},
"5": {
"shot1": 9,
"shot2": 2
},
"6": {
"shot1": 0,
"shot2": 8
},
"7": {
"shot1": 9,
"shot2": 8
},
"8": {
"shot1": 8,
"shot2": 10
},
"9": {
"shot1": 1,
"shot2": 4
},
"10": {
"shot1": 5,
"shot2": 1
},
"11": {
"shot1": 10,
"shot2": 9
},
"12": {
"shot1": 1,
"shot2": 7
},
"13": {
"shot1": 8,
"shot2": 10
},
"14": {
"shot1": 3,
"shot2": 1
},
"15": {
"shot1": 4,
"shot2": 4
},
"16": {
"shot1": 5,
"shot2": 3
},
"17": {
"shot1": 6,
"shot2": 3
},
"18": {
"shot1": 4,
"shot2": 4
},
"19": {
"shot1": 4,
"shot2": 9
},
"20": {
"shot1": 0,
"shot2": 9
}
},
"total_score": 195,
"completed": true,
"joker_selected": false
}
},
"tournament_finished": true,
"created_at": "2026-04-19T14:50:47.646988",
"finished_at": "2026-04-19T14:51:31.954362"
},
"archived_at": "2026-04-19T14:51:31.954403"
}
@@ -0,0 +1,777 @@
{
"tournament": {
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 28,
"name": "Anže Kolar",
"enabled": true
},
{
"id": 32,
"name": "David Strniša",
"enabled": true
},
{
"id": 7,
"name": "Branko Pokeržnik",
"enabled": true
},
{
"id": 38,
"name": "Bojan Sudar",
"enabled": true
},
{
"id": 25,
"name": "Andrej Herman",
"enabled": true
},
{
"id": 13,
"name": "Angelca Mrak",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 2,
"players": [
{
"id": 29,
"name": "Alen Kolar",
"enabled": true
},
{
"id": 44,
"name": "Anka Kačnik",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2026-04-19T14:52:03.834406",
"total_players": 8,
"total_rounds": 2,
"current_round": 1,
"tournament_type": "20_targets"
},
"results": {
"tournament_id": "2026-04-19T14:52:03.834406",
"tournament_type": "20_targets",
"participants": {
"28": {
"name": "Anže Kolar",
"targets": {
"1": {
"shot1": 4,
"shot2": 2
},
"2": {
"shot1": 9,
"shot2": 0
},
"3": {
"shot1": 0,
"shot2": 8
},
"4": {
"shot1": 10,
"shot2": 2
},
"5": {
"shot1": 4,
"shot2": 10
},
"6": {
"shot1": 10,
"shot2": 3
},
"7": {
"shot1": 9,
"shot2": 8
},
"8": {
"shot1": 6,
"shot2": 9
},
"9": {
"shot1": 2,
"shot2": 6
},
"10": {
"shot1": 2,
"shot2": 8
},
"11": {
"shot1": 3,
"shot2": 6
},
"12": {
"shot1": 7,
"shot2": 5
},
"13": {
"shot1": 6,
"shot2": 8
},
"14": {
"shot1": 5,
"shot2": 7
},
"15": {
"shot1": 4,
"shot2": 3
},
"16": {
"shot1": 10,
"shot2": 7
},
"17": {
"shot1": 1,
"shot2": 7
},
"18": {
"shot1": 5,
"shot2": 9
},
"19": {
"shot1": 7,
"shot2": 1
},
"20": {
"shot1": 2,
"shot2": 8
}
},
"total_score": 223,
"completed": true,
"joker_selected": false
},
"32": {
"name": "David Strniša",
"targets": {
"1": {
"shot1": 8,
"shot2": 1
},
"2": {
"shot1": 6,
"shot2": 10
},
"3": {
"shot1": 1,
"shot2": 5
},
"4": {
"shot1": 9,
"shot2": 7
},
"5": {
"shot1": 0,
"shot2": 8
},
"6": {
"shot1": 5,
"shot2": 8
},
"7": {
"shot1": 2,
"shot2": 3
},
"8": {
"shot1": 5,
"shot2": 10
},
"9": {
"shot1": 10,
"shot2": 9
},
"10": {
"shot1": 9,
"shot2": 7
},
"11": {
"shot1": 3,
"shot2": 2
},
"12": {
"shot1": 5,
"shot2": 6
},
"13": {
"shot1": 0,
"shot2": 2
},
"14": {
"shot1": 2,
"shot2": 1
},
"15": {
"shot1": 8,
"shot2": 8
},
"16": {
"shot1": 0,
"shot2": 2
},
"17": {
"shot1": 8,
"shot2": 6
},
"18": {
"shot1": 4,
"shot2": 0
},
"19": {
"shot1": 9,
"shot2": 1
},
"20": {
"shot1": 9,
"shot2": 2
}
},
"total_score": 201,
"completed": true,
"joker_selected": false
},
"7": {
"name": "Branko Pokeržnik",
"targets": {
"1": {
"shot1": 2,
"shot2": 2
},
"2": {
"shot1": 5,
"shot2": 4
},
"3": {
"shot1": 1,
"shot2": 9
},
"4": {
"shot1": 1,
"shot2": 1
},
"5": {
"shot1": 2,
"shot2": 4
},
"6": {
"shot1": 3,
"shot2": 8
},
"7": {
"shot1": 10,
"shot2": 6
},
"8": {
"shot1": 3,
"shot2": 9
},
"9": {
"shot1": 0,
"shot2": 7
},
"10": {
"shot1": 6,
"shot2": 6
},
"11": {
"shot1": 9,
"shot2": 0
},
"12": {
"shot1": 4,
"shot2": 7
},
"13": {
"shot1": 1,
"shot2": 5
},
"14": {
"shot1": 3,
"shot2": 6
},
"15": {
"shot1": 2,
"shot2": 3
},
"16": {
"shot1": 2,
"shot2": 8
},
"17": {
"shot1": 0,
"shot2": 3
},
"18": {
"shot1": 7,
"shot2": 5
},
"19": {
"shot1": 7,
"shot2": 1
},
"20": {
"shot1": 8,
"shot2": 8
}
},
"total_score": 178,
"completed": true,
"joker_selected": false
},
"38": {
"name": "Bojan Sudar",
"targets": {
"1": {
"shot1": 3,
"shot2": 1
},
"2": {
"shot1": 8,
"shot2": 5
},
"3": {
"shot1": 3,
"shot2": 0
},
"4": {
"shot1": 6,
"shot2": 5
},
"5": {
"shot1": 8,
"shot2": 10
},
"6": {
"shot1": 5,
"shot2": 5
},
"7": {
"shot1": 3,
"shot2": 2
},
"8": {
"shot1": 5,
"shot2": 1
},
"9": {
"shot1": 0,
"shot2": 0
},
"10": {
"shot1": 10,
"shot2": 5
},
"11": {
"shot1": 1,
"shot2": 10
},
"12": {
"shot1": 8,
"shot2": 0
},
"13": {
"shot1": 4,
"shot2": 0
},
"14": {
"shot1": 7,
"shot2": 10
},
"15": {
"shot1": 7,
"shot2": 5
},
"16": {
"shot1": 3,
"shot2": 5
},
"17": {
"shot1": 3,
"shot2": 4
},
"18": {
"shot1": 1,
"shot2": 10
},
"19": {
"shot1": 4,
"shot2": 9
},
"20": {
"shot1": 0,
"shot2": 10
}
},
"total_score": 186,
"completed": true,
"joker_selected": false
},
"25": {
"name": "Andrej Herman",
"targets": {
"1": {
"shot1": 2,
"shot2": 0
},
"2": {
"shot1": 1,
"shot2": 8
},
"3": {
"shot1": 2,
"shot2": 1
},
"4": {
"shot1": 9,
"shot2": 7
},
"5": {
"shot1": 4,
"shot2": 3
},
"6": {
"shot1": 7,
"shot2": 0
},
"7": {
"shot1": 2,
"shot2": 5
},
"8": {
"shot1": 1,
"shot2": 7
},
"9": {
"shot1": 8,
"shot2": 0
},
"10": {
"shot1": 8,
"shot2": 4
},
"11": {
"shot1": 10,
"shot2": 10
},
"12": {
"shot1": 2,
"shot2": 5
},
"13": {
"shot1": 5,
"shot2": 0
},
"14": {
"shot1": 10,
"shot2": 10
},
"15": {
"shot1": 0,
"shot2": 5
},
"16": {
"shot1": 10,
"shot2": 3
},
"17": {
"shot1": 4,
"shot2": 6
},
"18": {
"shot1": 2,
"shot2": 5
},
"19": {
"shot1": 2,
"shot2": 9
},
"20": {
"shot1": 6,
"shot2": 5
}
},
"total_score": 188,
"completed": true,
"joker_selected": false
},
"13": {
"name": "Angelca Mrak",
"targets": {
"1": {
"shot1": 4,
"shot2": 0
},
"2": {
"shot1": 4,
"shot2": 3
},
"3": {
"shot1": 10,
"shot2": 0
},
"4": {
"shot1": 8,
"shot2": 0
},
"5": {
"shot1": 2,
"shot2": 4
},
"6": {
"shot1": 5,
"shot2": 8
},
"7": {
"shot1": 9,
"shot2": 1
},
"8": {
"shot1": 7,
"shot2": 9
},
"9": {
"shot1": 7,
"shot2": 8
},
"10": {
"shot1": 5,
"shot2": 5
},
"11": {
"shot1": 0,
"shot2": 3
},
"12": {
"shot1": 1,
"shot2": 0
},
"13": {
"shot1": 7,
"shot2": 10
},
"14": {
"shot1": 8,
"shot2": 7
},
"15": {
"shot1": 8,
"shot2": 1
},
"16": {
"shot1": 7,
"shot2": 8
},
"17": {
"shot1": 10,
"shot2": 7
},
"18": {
"shot1": 8,
"shot2": 7
},
"19": {
"shot1": 1,
"shot2": 6
},
"20": {
"shot1": 7,
"shot2": 5
}
},
"total_score": 210,
"completed": true,
"joker_selected": false
},
"29": {
"name": "Alen Kolar",
"targets": {
"1": {
"shot1": 3,
"shot2": 0
},
"2": {
"shot1": 3,
"shot2": 5
},
"3": {
"shot1": 3,
"shot2": 5
},
"4": {
"shot1": 2,
"shot2": 6
},
"5": {
"shot1": 3,
"shot2": 0
},
"6": {
"shot1": 2,
"shot2": 9
},
"7": {
"shot1": 8,
"shot2": 8
},
"8": {
"shot1": 5,
"shot2": 0
},
"9": {
"shot1": 9,
"shot2": 8
},
"10": {
"shot1": 9,
"shot2": 7
},
"11": {
"shot1": 8,
"shot2": 5
},
"12": {
"shot1": 4,
"shot2": 2
},
"13": {
"shot1": 1,
"shot2": 5
},
"14": {
"shot1": 9,
"shot2": 0
},
"15": {
"shot1": 3,
"shot2": 7
},
"16": {
"shot1": 5,
"shot2": 10
},
"17": {
"shot1": 0,
"shot2": 2
},
"18": {
"shot1": 8,
"shot2": 5
},
"19": {
"shot1": 0,
"shot2": 9
},
"20": {
"shot1": 0,
"shot2": 10
}
},
"total_score": 188,
"completed": true,
"joker_selected": false
},
"44": {
"name": "Anka Kačnik",
"targets": {
"1": {
"shot1": 5,
"shot2": 3
},
"2": {
"shot1": 5,
"shot2": 1
},
"3": {
"shot1": 0,
"shot2": 3
},
"4": {
"shot1": 4,
"shot2": 8
},
"5": {
"shot1": 5,
"shot2": 8
},
"6": {
"shot1": 10,
"shot2": 10
},
"7": {
"shot1": 0,
"shot2": 5
},
"8": {
"shot1": 8,
"shot2": 10
},
"9": {
"shot1": 5,
"shot2": 1
},
"10": {
"shot1": 5,
"shot2": 2
},
"11": {
"shot1": 8,
"shot2": 6
},
"12": {
"shot1": 8,
"shot2": 9
},
"13": {
"shot1": 6,
"shot2": 5
},
"14": {
"shot1": 0,
"shot2": 4
},
"15": {
"shot1": 5,
"shot2": 7
},
"16": {
"shot1": 6,
"shot2": 5
},
"17": {
"shot1": 1,
"shot2": 10
},
"18": {
"shot1": 5,
"shot2": 6
},
"19": {
"shot1": 1,
"shot2": 8
},
"20": {
"shot1": 3,
"shot2": 7
}
},
"total_score": 208,
"completed": true,
"joker_selected": false
}
},
"tournament_finished": true,
"created_at": "2026-04-19T14:52:03.834609",
"finished_at": "2026-04-19T14:52:41.243912"
},
"archived_at": "2026-04-19T14:52:41.243969"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+318
View File
@@ -0,0 +1,318 @@
{
"rounds": [
{
"round_number": 1,
"players": [
{
"id": 14,
"name": "Karli Proje",
"enabled": true
},
{
"id": 23,
"name": "Robi Krautberger",
"enabled": true
},
{
"id": 11,
"name": "Rado Kefer",
"enabled": true
},
{
"id": 43,
"name": "Marko Pokržnik",
"enabled": true
},
{
"id": 7,
"name": "Branko Pokeržnik",
"enabled": true
},
{
"id": 9,
"name": "Janez Božič",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 2,
"players": [
{
"id": 39,
"name": "Tia Sudar",
"enabled": true
},
{
"id": 16,
"name": "Silvo Poročnik",
"enabled": true
},
{
"id": 31,
"name": "Dejan Kučnik",
"enabled": true
},
{
"id": 10,
"name": "Mitja Čeh",
"enabled": true
},
{
"id": 48,
"name": "Janja Salcman",
"enabled": true
},
{
"id": 50,
"name": "Vid Ravnjak",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 3,
"players": [
{
"id": 1,
"name": "Domen Pleterski",
"enabled": true
},
{
"id": 35,
"name": "Vanja Kolar",
"enabled": true
},
{
"id": 36,
"name": "Klara Wankmuller",
"enabled": true
},
{
"id": 46,
"name": "Tijana Štumpfl",
"enabled": true
},
{
"id": 34,
"name": "Jože Planinšec",
"enabled": true
},
{
"id": 3,
"name": "Ivan Tandler",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 4,
"players": [
{
"id": 26,
"name": "Jakob Herman",
"enabled": true
},
{
"id": 25,
"name": "Andrej Herman",
"enabled": true
},
{
"id": 30,
"name": "Maja Hirtl",
"enabled": true
},
{
"id": 4,
"name": "Mateja Pleterski",
"enabled": true
},
{
"id": 29,
"name": "Alen Kolar",
"enabled": true
},
{
"id": 5,
"name": "Jože Verhnjak",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 5,
"players": [
{
"id": 44,
"name": "Anka Kačnik",
"enabled": true
},
{
"id": 42,
"name": "Jure Glaser",
"enabled": true
},
{
"id": 45,
"name": "Lidija Blimen",
"enabled": true
},
{
"id": 32,
"name": "David Strniša",
"enabled": true
},
{
"id": 51,
"name": "Robi Ovčar",
"enabled": true
},
{
"id": 41,
"name": "Tadej Štruc",
"enabled": true
}
],
"status": "completed"
},
{
"round_number": 6,
"players": [
{
"id": 13,
"name": "Angelca Mrak",
"enabled": true
},
{
"id": 24,
"name": "Jože Verdinek",
"enabled": true
},
{
"id": 49,
"name": "Jolanda Verhnjak",
"enabled": true
},
{
"id": 18,
"name": "Matjaž Pleterski",
"enabled": true
},
{
"id": 40,
"name": "Jaka Cvar",
"enabled": true
},
{
"id": 19,
"name": "Franc Rizmal",
"enabled": true
}
],
"status": "pending"
},
{
"round_number": 7,
"players": [
{
"id": 2,
"name": "Nik Pleterski",
"enabled": true
},
{
"id": 17,
"name": "Dušan Onuk",
"enabled": true
},
{
"id": 28,
"name": "Anže Kolar",
"enabled": true
},
{
"id": 21,
"name": "Marko Blimen",
"enabled": true
},
{
"id": 20,
"name": "Jože Preglav",
"enabled": true
},
{
"id": 22,
"name": "Doris Fesel",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 8,
"players": [
{
"id": 15,
"name": "Jan Pleterski",
"enabled": true
},
{
"id": 6,
"name": "Mateja Senica",
"enabled": true
},
{
"id": 8,
"name": "Franc Žigart",
"enabled": true
},
{
"id": 12,
"name": "Matej Kvasnik",
"enabled": true
},
{
"id": 33,
"name": "Namir Uzunović",
"enabled": true
},
{
"id": 38,
"name": "Bojan Sudar",
"enabled": true
}
],
"status": "waiting"
},
{
"round_number": 9,
"players": [
{
"id": 37,
"name": "Milan Stramec",
"enabled": true
},
{
"id": 27,
"name": "Janez Mrak",
"enabled": true
},
{
"id": 47,
"name": "Ljuba Mršak",
"enabled": true
}
],
"status": "waiting"
}
],
"created_at": "2026-04-22T17:12:17.015870",
"total_players": 51,
"total_rounds": 9,
"current_round": 6,
"tournament_type": "20_targets"
}
+6 -4
View File
@@ -348,7 +348,7 @@
font-weight: 500; font-weight: 500;
} }
.player-id { .player-id {
display: none; display: none;
} }
@@ -645,11 +645,13 @@
</div> </div>
<div class="positions-container"> <div class="positions-container">
{% for position in range(1, 7) %} {% set slots = tournament.active_slots if tournament.active_slots else range(1, 7) %}
{% set player = round.players[position-1] if position <= round.players|length else none %} {% for idx in range(slots|length) %}
{% set slot_num = slots[idx] %}
{% set player = round.players[idx] if idx < round.players|length else none %}
<div class="position-card {{ 'filled' if player else 'empty' }}"> <div class="position-card {{ 'filled' if player else 'empty' }}">
<div class="position-header"> <div class="position-header">
<div class="position-number">{{ position }}</div> <div class="position-number">{{ slot_num }}</div>
</div> </div>
<div class="position-body"> <div class="position-body">
{% if player %} {% if player %}
+245 -90
View File
@@ -124,28 +124,21 @@
height: calc(100vh - 50px); height: calc(100vh - 50px);
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 20px; gap: 20px;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
align-content: stretch;
} }
.camera-card { .camera-card {
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
position: relative; position: relative;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.camera-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.card-title { .card-title {
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
@@ -189,12 +182,7 @@
flex: 1; flex: 1;
} }
/* Calculate optimal size to fit within grid cell */ /* stream-wrapper width is set dynamically via JS (reshapeGrid) */
@supports (width: min(100%, calc((100vh - 50px - 20px) / 2 - 60px) * 4 / 3)) {
.stream-wrapper {
width: min(100%, calc(((100vh - 50px - 20px) / 2 - 60px) * 4 / 3));
}
}
.stream-placeholder { .stream-placeholder {
position: absolute; position: absolute;
@@ -263,35 +251,7 @@
background: linear-gradient(90deg, #999 0%, #999 60px, #1a1a1a 60px); background: linear-gradient(90deg, #999 0%, #999 60px, #1a1a1a 60px);
} }
.camera-card:hover .card-header {
background: linear-gradient(90deg, #1e7e34 0%, #1e7e34 60px, #2d2d2d 60px);
border-bottom-color: #28a745;
}
/* Tooltip for camera cards */
.camera-card::after {
content: attr(data-tooltip);
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 6px 12px;
border-radius: 4px;
font-size: 0.85rem;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
pointer-events: none;
z-index: 10;
}
.camera-card:hover::after {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(-5px);
}
/* Settings Panel */ /* Settings Panel */
#overlay { #overlay {
@@ -491,6 +451,157 @@
transform: translateY(-1px); transform: translateY(-1px);
} }
/* Camera settings panel components */
.cam-setting-toggle {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
font-size: 0.9rem;
color: #495057;
cursor: pointer;
border-bottom: 1px solid #e9ecef;
margin-bottom: 14px;
}
.cam-setting-toggle input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #28a745;
}
.cam-slider-row {
margin-bottom: 14px;
}
.cam-slider-label {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: #495057;
margin-bottom: 6px;
}
.cam-slider-value {
font-weight: 600;
color: #28a745;
}
.cam-slider {
width: 100%;
accent-color: #28a745;
}
.cam-list {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.cam-row {
border: 1.5px solid #dee2e6;
border-radius: 10px;
overflow: hidden;
background: white;
transition: opacity 0.2s ease;
}
.cam-row--disabled {
opacity: 0.55;
}
.cam-row-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.cam-row-label {
font-size: 0.85rem;
font-weight: 600;
color: #495057;
}
.cam-row .camera-input {
border: none;
border-radius: 0;
box-shadow: none;
padding: 10px 12px;
font-size: 0.9rem;
}
.cam-row .camera-input:focus {
border: none;
box-shadow: inset 0 0 0 2px #28a745;
transform: none;
}
.cam-row .camera-input:hover:not(:focus) {
border: none;
box-shadow: inset 0 0 0 2px #28a74580;
transform: none;
}
/* Toggle switch */
.cam-toggle {
display: flex;
align-items: center;
gap: 7px;
cursor: pointer;
}
.cam-toggle input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.cam-toggle-track {
width: 36px;
height: 20px;
background: #dc3545;
border-radius: 10px;
position: relative;
transition: background 0.2s ease;
flex-shrink: 0;
}
.cam-toggle input:checked ~ .cam-toggle-track {
background: #28a745;
}
.cam-toggle-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
transition: transform 0.2s ease;
}
.cam-toggle input:checked ~ .cam-toggle-track .cam-toggle-thumb {
transform: translateX(16px);
}
.cam-toggle-text {
font-size: 0.78rem;
font-weight: 600;
color: #28a745;
min-width: 22px;
}
.cam-toggle-text--off {
color: #dc3545;
}
.tournament-status-info { .tournament-status-info {
background: #f0f8ff; background: #f0f8ff;
border: 2px solid #28a745; border: 2px solid #28a745;
@@ -539,7 +650,6 @@
.grid { .grid {
height: calc(100vh - 100px); height: calc(100vh - 100px);
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: repeat(6, 1fr);
gap: 15px; gap: 15px;
padding: 20px; padding: 20px;
} }
@@ -549,9 +659,6 @@
padding: 10px 12px 10px 0; padding: 10px 12px 10px 0;
} }
.camera-card:hover .card-header {
background: linear-gradient(90deg, #1e7e34 0%, #1e7e34 50px, #2d2d2d 50px);
}
.target-number { .target-number {
width: 50px; width: 50px;
@@ -611,9 +718,6 @@
padding: 8px 10px 8px 0; padding: 8px 10px 8px 0;
} }
.camera-card:hover .card-header {
background: linear-gradient(90deg, #1e7e34 0%, #1e7e34 45px, #2d2d2d 45px);
}
.target-number { .target-number {
width: 45px; width: 45px;
@@ -719,46 +823,48 @@
<div class="settings-group" id="displayOptionsGroup"> <div class="settings-group" id="displayOptionsGroup">
<h4 data-i18n="league.camera">Kamera</h4> <h4 data-i18n="league.camera">Kamera</h4>
<div style="margin-bottom: 15px;"> <!-- Display toggles -->
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> <label class="cam-setting-toggle">
<input type="checkbox" id="toggleTitles" {% if settings.display_options.show_titles %}checked{% endif %} onchange="toggleTitles()" style="width: 18px; height: 18px;"> <input type="checkbox" id="toggleTitles" {% if settings.display_options.show_titles %}checked{% endif %} onchange="toggleTitles()">
<span data-i18n="camera.show_card_titles">Prikaiž Naslove Kartic</span> <span data-i18n="camera.show_card_titles">Prikaži Naslove Kartic</span>
</label> </label>
<!-- Sliders -->
<div class="cam-slider-row">
<div class="cam-slider-label">
<span data-i18n="camera.title_text_size">Naslov</span>
<span class="cam-slider-value"><span id="titleSizeValue">{{ settings.display_options.title_size }}</span>rem</span>
</div>
<input type="range" id="titleSize" class="cam-slider" min="0.8" max="2.5" step="0.1"
value="{{ settings.display_options.title_size }}" oninput="adjustTitleSize(this.value)">
</div> </div>
<div style="margin-bottom: 15px;"> <div class="cam-slider-row">
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #333;"> <div class="cam-slider-label">
<span data-i18n="camera.title_text_size">Velikost Besedila Naslova</span>: <span id="titleSizeValue">{{ settings.display_options.title_size }}</span>rem <span data-i18n="camera.target_number_size">Številka</span>
</label> <span class="cam-slider-value"><span id="targetNumberSizeValue">{{ settings.display_options.get('target_number_size', settings.display_options.title_size) }}</span>rem</span>
<input type="range" </div>
id="titleSize" <input type="range" id="targetNumberSize" class="cam-slider" min="0.8" max="2.5" step="0.1"
min="0.8" value="{{ settings.display_options.get('target_number_size', settings.display_options.title_size) }}" oninput="adjustTargetNumberSize(this.value)">
max="2.5"
step="0.1"
value="{{ settings.display_options.title_size }}"
oninput="adjustTitleSize(this.value)"
style="width: 100%;">
</div> </div>
<div style="margin-bottom: 15px;"> <!-- Camera rows -->
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #333;"> <div class="cam-list">
<span data-i18n="camera.target_number_size">Velikost Števila Tarče</span>: <span id="targetNumberSizeValue">{{ settings.display_options.get('target_number_size', settings.display_options.title_size) }}</span>rem
</label>
<input type="range"
id="targetNumberSize"
min="0.8"
max="2.5"
step="0.1"
value="{{ settings.display_options.get('target_number_size', settings.display_options.title_size) }}"
oninput="adjustTargetNumberSize(this.value)"
style="width: 100%;">
</div>
<div style="margin-top: 20px;">
<h5 data-i18n="camera.titles">Naslovi Kamer</h5>
{% for i in range(1, 7) %} {% for i in range(1, 7) %}
<div style="margin-bottom: 15px;"> {% set is_disabled = i in settings.get('disabled_cameras', []) %}
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #495057; font-size: 0.9rem;"><span data-i18n="camera.camera">Kamera</span> {{ i }}:</label> <div class="cam-row {% if is_disabled %}cam-row--disabled{% endif %}" id="cameraSettingsRow{{ i }}">
<div class="cam-row-header">
<span class="cam-row-label"><span data-i18n="camera.camera">Kamera</span> {{ i }}</span>
<label class="cam-toggle" id="cameraToggleLabel{{ i }}">
<input type="checkbox" id="cameraEnabled{{ i }}"
{% if not is_disabled %}checked{% endif %}
onchange="toggleCamera({{ i }}, this.checked)">
<span class="cam-toggle-track">
<span class="cam-toggle-thumb"></span>
</span>
<span class="cam-toggle-text {% if is_disabled %}cam-toggle-text--off{% endif %}" id="cameraStatusText{{ i }}">{% if is_disabled %}Off{% else %}On{% endif %}</span>
</label>
</div>
<input type="text" <input type="text"
id="titleInput{{ i }}" id="titleInput{{ i }}"
value="{{ settings.camera_titles[i|string] }}" value="{{ settings.camera_titles[i|string] }}"
@@ -779,7 +885,8 @@
<div class="grid"> <div class="grid">
{% for i in range(1, 7) %} {% for i in range(1, 7) %}
<div class="camera-card" onclick="openCameraFullscreen({{ i }})" data-camera-id="{{ i }}" data-tooltip="" data-i18n-tooltip="camera.click_to_view_fullscreen"> <div class="camera-card" data-camera-id="{{ i }}"
{% if i in settings.get('disabled_cameras', []) %}style="display:none;"{% endif %}>
<div class="stream-wrapper"> <div class="stream-wrapper">
<div class="card-header"> <div class="card-header">
<div class="target-number">{{ i }}</div> <div class="target-number">{{ i }}</div>
@@ -1174,6 +1281,58 @@
} }
} }
// Toggle camera enabled/disabled
function reshapeGrid() {
const grid = document.querySelector('.grid');
if (!grid) return;
const activeCount = document.querySelectorAll('.camera-card:not([style*="display: none"]):not([style*="display:none"])').length;
let cols;
if (activeCount <= 1) cols = 1;
else if (activeCount === 2) cols = 2;
else if (activeCount === 3) cols = 3;
else if (activeCount === 4) cols = 2;
else cols = 3;
const rows = Math.ceil(activeCount / cols);
grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
// Recompute stream-wrapper max-width so cards fill available height at 4:3
// gap=20px, padding=20px each side, header~40px, toolbar=50px
const gap = 20;
const padding = 40;
const toolbar = 50;
const headerH = 40;
const availH = (window.innerHeight - toolbar - padding - gap * (rows - 1)) / rows - headerH;
const maxW = availH * (4 / 3);
document.querySelectorAll('.stream-wrapper').forEach(w => {
w.style.width = `min(100%, ${maxW}px)`;
});
}
function toggleCamera(index, enabled) {
const card = document.querySelector(`.camera-card[data-camera-id="${index}"]`);
const titleInput = document.getElementById(`titleInput${index}`);
const label = document.getElementById(`cameraToggleLabel${index}`);
const statusText = document.getElementById(`cameraStatusText${index}`);
const row = document.getElementById(`cameraSettingsRow${index}`);
if (enabled) {
if (card) card.style.display = '';
if (row) row.classList.remove('cam-row--disabled');
if (statusText) { statusText.textContent = 'On'; statusText.classList.remove('cam-toggle-text--off'); }
currentSettings.disabled_cameras = (currentSettings.disabled_cameras || []).filter(c => c !== index);
} else {
if (card) card.style.display = 'none';
if (row) row.classList.add('cam-row--disabled');
if (statusText) { statusText.textContent = 'Off'; statusText.classList.add('cam-toggle-text--off'); }
if (!currentSettings.disabled_cameras) currentSettings.disabled_cameras = [];
if (!currentSettings.disabled_cameras.includes(index)) currentSettings.disabled_cameras.push(index);
}
reshapeGrid();
saveSettings({ disabled_cameras: currentSettings.disabled_cameras });
}
// Toggle titles // Toggle titles
function toggleTitles() { function toggleTitles() {
const show = document.getElementById('toggleTitles').checked; const show = document.getElementById('toggleTitles').checked;
@@ -1236,12 +1395,6 @@
} }
} }
// Open camera fullscreen
function openCameraFullscreen(cameraId) {
const customTitle = currentSettings.camera_titles[cameraId.toString()] || `Camera ${cameraId}`;
window.location.href = `/fullscreen/${cameraId}?title=${encodeURIComponent(customTitle)}`;
}
// Apply current settings // Apply current settings
function applyCurrentSettings() { function applyCurrentSettings() {
// Apply title visibility // Apply title visibility
@@ -1460,6 +1613,8 @@
setInterval(updateDateTime, 1000); setInterval(updateDateTime, 1000);
updateRoundNavigation(); updateRoundNavigation();
applyCurrentSettings(); applyCurrentSettings();
reshapeGrid();
window.addEventListener('resize', reshapeGrid);
addTouchFeedback(); addTouchFeedback();
// Initialize language selector after i18n is ready // Initialize language selector after i18n is ready