diff --git a/locales/en.json b/locales/en.json
index 8529c70..b66bcb5 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -22,6 +22,7 @@
"enable_selected": "Enable Selected",
"disable_selected": "Disable Selected",
"print": "Print",
+ "export": "Export JSON",
"visible": "Visible",
"date": "Date",
"view": "View",
@@ -140,6 +141,33 @@
"highest_score": "Highest Score",
"average_final": "Average Final",
"5_tournament_league": "5 Tournament League - Best 4 Count",
+ "combine_leagues": "Combine Leagues",
+ "combine_mode": "Combine Leagues",
+ "convert_mode": "Convert Tournaments to League",
+ "upload_league_files": "Upload League JSON Files",
+ "upload_tournament_files": "Upload Tournament JSON Files",
+ "combine_leagues_desc": "Upload multiple league JSON files to combine them into a single results table.",
+ "convert_tournaments_desc": "Upload 1-5 tournament JSON files to create a league. You can upload partial leagues as tournaments complete. Final scoring uses best 4 results when all 5 are uploaded.",
+ "click_browse": "Click to browse",
+ "drag_drop_files": "or drag and drop league JSON files here",
+ "supports_multiple": "Supports multiple file selection",
+ "clear_all": "Clear All",
+ "combine_preview": "Combine & Preview",
+ "convert_to_league": "Convert to League",
+ "valid_league": "Valid League",
+ "valid_tournament": "Valid Tournament",
+ "participants": "participants",
+ "tournaments": "tournaments",
+ "invalid_format": "Invalid file format",
+ "select_mode": "Select Mode",
+ "start_over": "Start Over",
+ "export_combined_results": "Export Combined Results",
+ "combined_league_info": "Combined League Information",
+ "uploaded_files": "Uploaded Files",
+ "remove": "Remove",
+ "type": "Type",
+ "failed_parse_json": "Failed to parse JSON",
+ "invalid_tournament_format": "Invalid tournament file format",
"joker_used_badge": "Joker Used",
"search_players_placeholder": "Search players by name...",
"no_players_found": "No players found matching your search criteria.",
@@ -166,7 +194,8 @@
"system": "System",
"camera": "Camera",
"tournaments": "Tournaments",
- "results.most_tens": "Most 10s"
+ "results.most_tens": "Most 10s",
+ "combine_leagues": "Combine Leagues"
},
"results": {
"results": "Results",
diff --git a/locales/sl.json b/locales/sl.json
index dd1a415..b1fa566 100644
--- a/locales/sl.json
+++ b/locales/sl.json
@@ -22,6 +22,7 @@
"enable_selected": "Omogoči Izbrane",
"disable_selected": "Onemogoči Izbrane",
"print": "Natisni",
+ "export": "Izvozi JSON",
"visible": "Vidni",
"date": "Datum",
"view": "Oglej",
@@ -171,8 +172,33 @@
"system": "Sistem",
"camera": "Kamera",
"tournaments": "Turnirji",
- "results.most_tens": "Največ Desetk"
-
+ "results.most_tens": "Največ Desetk",
+ "combine_leagues": "Združi Lige",
+ "combine_mode": "Združi Lige",
+ "convert_mode": "Pretvori Turnirje v Ligo",
+ "upload_league_files": "Naloži Datoteke Lig",
+ "upload_tournament_files": "Naloži Datoteke Turnirjev",
+ "combine_leagues_desc": "Naloži več datotek lig JSON, da jih združiš v eno tabelo rezultatov.",
+ "convert_tournaments_desc": "Naloži 1-5 datotek turnirjev JSON za ustvarjanje lige. Lahko naložiš delne lige ko turnirji napredujejo. Končno točkovanje uporablja najboljše 4 rezultate, ko so vseh 5 naloženih.",
+ "click_browse": "Klikni za brskanje",
+ "drag_drop_files": "ali povleci in spusti datoteke JSON lig tukaj",
+ "supports_multiple": "Podpira izbiro več datotek",
+ "combine_preview": "Združi in Predoglej",
+ "convert_to_league": "Pretvori v Ligo",
+ "valid_league": "Veljavna Liga",
+ "valid_tournament": "Veljaven Turnir",
+ "participants": "udeleženci",
+ "invalid_format": "Neveljavna oblika datoteke",
+ "select_mode": "Izberi Način",
+ "start_over": "Začni Znova",
+ "export_combined_results": "Izvozi Združene Rezultate",
+ "combined_league_info": "Informacije o Združeni Ligi",
+ "uploaded_files": "Naložene Datoteke",
+ "remove": "Odstrani",
+ "type": "Tip",
+ "failed_parse_json": "Napaka pri branju JSON",
+ "invalid_tournament_format": "Neveljavna oblika datoteke turnirja"
+
},
"results": {
"results": "Rezultati",
diff --git a/static/js/i18n.js b/static/js/i18n.js
index dfe1e98..15a755f 100644
--- a/static/js/i18n.js
+++ b/static/js/i18n.js
@@ -274,4 +274,11 @@ document.addEventListener('DOMContentLoaded', initI18n);
// Export functions for global use
window.t = t;
window.changeLanguage = changeLanguage;
-window.createLanguageSelector = createLanguageSelector;
\ No newline at end of file
+window.createLanguageSelector = createLanguageSelector;
+
+// Export i18n object with useful functions
+window.i18n = {
+ t: t,
+ updatePageTranslations: translatePage,
+ changeLanguage: changeLanguage
+};
\ No newline at end of file
diff --git a/templates/draft.html b/templates/draft.html
index b23d1c6..780220a 100644
--- a/templates/draft.html
+++ b/templates/draft.html
@@ -122,7 +122,7 @@
}
.round-row.current {
- border-left: 4px solid #28a745;
+ border-left: 4px solid #007bff;
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.25);
}
@@ -171,7 +171,7 @@
}
.current-badge {
- background: #28a745;
+ background: #007bff;
color: white;
}
diff --git a/templates/index.html b/templates/index.html
index 3dda3ab..338b3bb 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -683,21 +683,17 @@
{% endif %}
- {% if settings.tournament_active %}
-
- {% endif %}
-
diff --git a/templates/league_combine.html b/templates/league_combine.html
new file mode 100644
index 0000000..99e09d0
--- /dev/null
+++ b/templates/league_combine.html
@@ -0,0 +1,771 @@
+
+
+
+
+
+ Combine Leagues
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select Mode :
+
+
+
+ 🔗 Combine Leagues
+
+
+ 🔄 Convert Tournaments to League
+
+
+
+
+
+
📤 Upload League JSON Files
+
+ Upload multiple league JSON files to combine them into a single results table.
+
+
+
+
+
📤 Upload Tournament JSON Files
+
+ Upload 1-5 tournament JSON files to create a league. You can upload partial leagues as tournaments complete. Final scoring uses best 4 results when all 5 are uploaded.
+
+
+
+
+
📁
+
Click to browse or drag and drop league JSON files here
+
Supports multiple file selection
+
+
+
+
+
+
+
+ 🗑️ Clear All
+
+
+ 🔗 Combine & Preview
+
+
+
+
+
+
+
ℹ️ Combined League Information
+
+
+
+
+
+
+
+
+
+ 🔄 Start Over
+
+
+ 💾 Export Combined Results
+
+
+
+
+
+
+
+
+
diff --git a/templates/league_scoreboard_display.html b/templates/league_scoreboard_display.html
index 4fa43aa..f7661bf 100644
--- a/templates/league_scoreboard_display.html
+++ b/templates/league_scoreboard_display.html
@@ -787,6 +787,7 @@
@@ -1199,6 +1200,31 @@
// Initialize when page loads
document.addEventListener('DOMContentLoaded', initializePage);
+ // Export league data as JSON
+ function exportLeagueJSON() {
+ const leagueData = {{ league | tojson | safe }};
+
+ // Wrap in archive format for compatibility
+ const archiveData = {
+ league: leagueData,
+ archived_at: new Date().toISOString()
+ };
+
+ const dataStr = JSON.stringify(archiveData, null, 2);
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(dataBlob);
+ const link = document.createElement('a');
+ link.href = url;
+
+ // Generate filename with league info
+ const leagueId = leagueData.league_id || 'league';
+ const date = new Date().toISOString().slice(0, 10);
+ link.download = `${leagueId}_${date}.json`;
+
+ link.click();
+ URL.revokeObjectURL(url);
+ }
+
// Keyboard shortcuts
document.addEventListener('keydown', function(event) {
if (event.key === 'r' || event.key === 'R') {
diff --git a/templates/results_display.html b/templates/results_display.html
index 5e8ee2b..d616b26 100644
--- a/templates/results_display.html
+++ b/templates/results_display.html
@@ -657,6 +657,7 @@
@@ -962,6 +963,33 @@
}
}
+ // Export results as JSON
+ function exportResultsJSON() {
+ const tournamentData = {{ tournament | tojson | safe }};
+ const resultsData = {{ results | tojson | safe }};
+
+ // Wrap in archive format for compatibility
+ const archiveData = {
+ tournament: tournamentData,
+ results: resultsData,
+ archived_at: new Date().toISOString()
+ };
+
+ const dataStr = JSON.stringify(archiveData, null, 2);
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(dataBlob);
+ const link = document.createElement('a');
+ link.href = url;
+
+ // Generate filename with tournament info
+ const tournamentId = tournamentData.tournament_id || 'tournament';
+ const date = new Date().toISOString().slice(0, 10);
+ link.download = `${tournamentId}_${date}.json`;
+
+ link.click();
+ URL.revokeObjectURL(url);
+ }
+
// Initialize when page loads
document.addEventListener('DOMContentLoaded', initializePage);
diff --git a/tv_app.py b/tv_app.py
index 25ee86d..ebc29c7 100644
--- a/tv_app.py
+++ b/tv_app.py
@@ -1087,7 +1087,21 @@ def results_display():
translations=get_translations(),
current_language=get_current_language())
- # Priority 1.5: Check if current results are from a finished league (even if league state was archived)
+ # Priority 1.5: Check if results contain league_data (from league preview/combiner)
+ elif results and results.get('league_data'):
+ league_data = results.get('league_data')
+ calculate_league_final_scores(league_data)
+ participants = get_league_final_rankings(league_data)
+
+ return render_template('league_scoreboard_display.html',
+ league=league_data,
+ participants=participants,
+ results=None,
+ preview_mode=True,
+ translations=get_translations(),
+ current_language=get_current_language())
+
+ # Priority 1.6: Check if current results are from a finished league (even if league state was archived)
elif results and results.get('league_tournament_number'):
# This is a league tournament result, but league state was archived
# Try to find the archived league data
@@ -1697,7 +1711,327 @@ def get_league():
return jsonify(league_state)
else:
return jsonify({'status': 'error', 'message': 'No league found'}), 404
-
+
+@app.route('/league/combine')
+def league_combine_page():
+ """Page for combining multiple league JSON files"""
+ if is_mobile_device():
+ return redirect('/mobile')
+
+ return render_template('league_combine.html',
+ translations=get_translations(),
+ current_language=get_current_language())
+
+@app.route('/league/preview')
+def league_preview():
+ """Display league preview from session"""
+ # Get league data from session
+ league_data = session.get('preview_league')
+
+ if not league_data:
+ return redirect('/league/combine')
+
+ # Calculate scores and get rankings
+ calculate_league_final_scores(league_data)
+ participants = get_league_final_rankings(league_data)
+
+ return render_template('league_scoreboard_display.html',
+ league=league_data,
+ participants=participants,
+ results=None,
+ preview_mode=True,
+ translations=get_translations(),
+ current_language=get_current_language())
+
+@app.route('/league/set-preview', methods=['POST'])
+def set_league_preview():
+ """Store league data in session for preview"""
+ try:
+ data = request.get_json()
+ combined_league = data.get('league')
+
+ if not combined_league:
+ return jsonify({'status': 'error', 'message': 'No league data provided'}), 400
+
+ # Store in session
+ session['preview_league'] = combined_league
+
+ return jsonify({
+ 'status': 'success',
+ 'redirect_url': '/league/preview'
+ })
+ except Exception as e:
+ print(f"Error setting league preview: {e}")
+ return jsonify({'status': 'error', 'message': str(e)}), 500
+
+@app.route('/api/league/combine', methods=['POST'])
+def combine_leagues():
+ """API endpoint to combine multiple league files"""
+ try:
+ data = request.get_json()
+ leagues = data.get('leagues', [])
+
+ if len(leagues) < 1:
+ return jsonify({'status': 'error', 'message': 'Need at least 1 league'}), 400
+
+ # If only one league, just return it for preview
+ if len(leagues) == 1:
+ league = leagues[0]
+ calculate_league_final_scores(league)
+ participants = get_league_final_rankings(league)
+
+ highest_score = max((p['final_score'] for p in participants), default=0)
+ avg_score = sum(p['final_score'] for p in participants) / len(participants) if participants else 0
+
+ return jsonify({
+ 'status': 'success',
+ 'combined_league': league,
+ 'participants': participants,
+ 'source_count': 1,
+ 'tournament_type': league.get('tournament_type', '20_targets'),
+ 'total_participants': len(participants),
+ 'total_tournaments': league.get('total_tournaments', 5),
+ 'highest_score': highest_score,
+ 'avg_score': round(avg_score, 2)
+ })
+
+ # Get tournament type from first league
+ tournament_type = leagues[0].get('tournament_type', '20_targets')
+
+ # Create combined league structure
+ combined_league = {
+ 'league_id': f'combined_{datetime.now().strftime("%Y%m%d_%H%M%S")}',
+ 'created_at': datetime.now().isoformat(),
+ 'tournament_type': tournament_type,
+ 'total_tournaments': sum(len(league.get('completed_tournaments', [])) for league in leagues),
+ 'current_tournament': 0,
+ 'participants': {},
+ 'completed_tournaments': [],
+ 'league_finished': True,
+ 'combined_from': len(leagues)
+ }
+
+ # Build name-to-player mapping for combining by name
+ name_to_combined_id = {} # Maps player name to their ID in combined league
+ next_player_id = 1
+
+ # Combine tournament results from all leagues
+ tournament_counter = 0
+ for league_idx, league in enumerate(leagues):
+ # Process each participant in this league
+ for player_id, league_participant in league.get('participants', {}).items():
+ player_name = league_participant.get('name', f'Player {player_id}')
+
+ # Check if we've seen this player name before
+ if player_name in name_to_combined_id:
+ # Use existing combined player
+ combined_id = name_to_combined_id[player_name]
+ else:
+ # Create new combined player
+ combined_id = str(next_player_id)
+ next_player_id += 1
+ name_to_combined_id[player_name] = combined_id
+
+ # Initialize new participant
+ combined_league['participants'][combined_id] = {
+ 'name': player_name,
+ 'joker_used': False,
+ 'tournament_results': [],
+ 'total_score': 0,
+ 'final_score': 0,
+ 'tournaments_participated': 0
+ }
+
+ combined_participant = combined_league['participants'][combined_id]
+
+ # Add all tournament results from this league
+ for result in league_participant.get('tournament_results', []):
+ combined_participant['tournament_results'].append({
+ 'tournament': tournament_counter + result.get('tournament', 0),
+ 'score': result.get('score', 0),
+ 'tens_count': result.get('tens_count', 0),
+ 'participated': result.get('participated', True),
+ 'source_league': league_idx + 1
+ })
+
+ if result.get('participated', True):
+ combined_participant['tournaments_participated'] += 1
+ combined_participant['total_score'] += result.get('score', 0)
+
+ # Add completed tournaments
+ for completed in league.get('completed_tournaments', []):
+ combined_league['completed_tournaments'].append({
+ **completed,
+ 'source_league': league_idx + 1
+ })
+
+ tournament_counter += len(league.get('completed_tournaments', []))
+
+ # Calculate final scores
+ calculate_league_final_scores(combined_league)
+
+ # Get rankings
+ participants = get_league_final_rankings(combined_league)
+
+ # Calculate stats
+ highest_score = max((p['final_score'] for p in participants), default=0)
+ avg_score = sum(p['final_score'] for p in participants) / len(participants) if participants else 0
+
+ return jsonify({
+ 'status': 'success',
+ 'combined_league': combined_league,
+ 'participants': participants,
+ 'source_count': len(leagues),
+ 'tournament_type': tournament_type,
+ 'total_participants': len(participants),
+ 'total_tournaments': combined_league['total_tournaments'],
+ 'highest_score': highest_score,
+ 'avg_score': round(avg_score, 2)
+ })
+
+ except Exception as e:
+ print(f"Error combining leagues: {e}")
+ import traceback
+ traceback.print_exc()
+ return jsonify({'status': 'error', 'message': str(e)}), 400
+
+@app.route('/api/league/convert', methods=['POST'])
+def convert_tournaments_to_league():
+ """API endpoint to convert 1-5 tournament files into a league"""
+ try:
+ data = request.get_json()
+ tournaments = data.get('tournaments', [])
+
+ if len(tournaments) < 1 or len(tournaments) > 5:
+ return jsonify({'status': 'error', 'message': 'Need 1-5 tournament files'}), 400
+
+ # Sort tournaments by finished_at timestamp to maintain chronological order
+ tournaments.sort(key=lambda t: t.get('results', {}).get('finished_at', ''))
+
+ # Get tournament type from first tournament
+ tournament_type = tournaments[0].get('results', {}).get('tournament_type', '20_targets')
+ num_tournaments = len(tournaments)
+
+ # Collect all unique participants
+ all_player_ids = set()
+ for tournament in tournaments:
+ results = tournament.get('results', {})
+ all_player_ids.update(results.get('participants', {}).keys())
+
+ # Create league structure
+ created_league = {
+ 'league_id': f'converted_{datetime.now().strftime("%Y%m%d_%H%M%S")}',
+ 'created_at': datetime.now().isoformat(),
+ 'tournament_type': tournament_type,
+ 'total_tournaments': 5, # Always 5 for a full league
+ 'current_tournament': num_tournaments, # How many completed so far
+ 'participants': {},
+ 'completed_tournaments': [],
+ 'league_finished': num_tournaments >= 5, # Only finished if all 5 uploaded
+ 'converted_from_tournaments': True,
+ 'is_partial': num_tournaments < 5
+ }
+
+ # Initialize participants
+ for player_id in all_player_ids:
+ created_league['participants'][player_id] = {
+ 'name': '',
+ 'joker_used': False,
+ 'tournament_results': [],
+ 'total_score': 0,
+ 'final_score': 0,
+ 'tournaments_participated': 0
+ }
+
+ # Process each tournament
+ for tournament_idx, tournament in enumerate(tournaments):
+ results = tournament.get('results', {})
+ tournament_results_data = results.get('participants', {})
+
+ # Add completed tournament metadata
+ created_league['completed_tournaments'].append({
+ 'tournament_number': tournament_idx + 1,
+ 'tournament_type': results.get('tournament_type', tournament_type),
+ 'finished_at': results.get('finished_at', datetime.now().isoformat()),
+ 'results_summary': {
+ 'participants': len(tournament_results_data),
+ 'tournament_id': tournament.get('tournament', {}).get('tournament_id', f'tournament_{tournament_idx + 1}')
+ }
+ })
+
+ # Add results for each participant
+ for player_id in all_player_ids:
+ league_participant = created_league['participants'][player_id]
+
+ if player_id in tournament_results_data:
+ participant_data = tournament_results_data[player_id]
+
+ # Set name if not set
+ if not league_participant['name']:
+ league_participant['name'] = participant_data.get('name', f'Player {player_id}')
+
+ # Count tens
+ tens_count = 0
+ targets = participant_data.get('targets', {})
+ for target in targets.values():
+ for shot_key, shot_value in target.items():
+ if shot_key.startswith('shot') and shot_value == 10:
+ tens_count += 1
+
+ # Add tournament result
+ score = participant_data.get('total_score', 0)
+ league_participant['tournament_results'].append({
+ 'tournament': tournament_idx + 1,
+ 'score': score,
+ 'tens_count': tens_count,
+ 'participated': True
+ })
+
+ league_participant['tournaments_participated'] += 1
+ league_participant['total_score'] += score
+ else:
+ # Player didn't participate (used joker)
+ league_participant['tournament_results'].append({
+ 'tournament': tournament_idx + 1,
+ 'score': 0,
+ 'tens_count': 0,
+ 'participated': False,
+ 'joker': True
+ })
+
+ if tournament_idx == 0:
+ # Set joker_used for players who didn't participate in first tournament
+ league_participant['joker_used'] = True
+
+ # Calculate final scores using league rules
+ calculate_league_final_scores(created_league)
+
+ # Get rankings
+ participants = get_league_final_rankings(created_league)
+
+ # Calculate stats
+ highest_score = max((p['final_score'] for p in participants), default=0)
+ avg_score = sum(p['final_score'] for p in participants) / len(participants) if participants else 0
+
+ return jsonify({
+ 'status': 'success',
+ 'combined_league': created_league,
+ 'participants': participants,
+ 'source_count': num_tournaments,
+ 'tournament_type': tournament_type,
+ 'total_participants': len(participants),
+ 'total_tournaments': num_tournaments,
+ 'is_partial': num_tournaments < 5,
+ 'highest_score': highest_score,
+ 'avg_score': round(avg_score, 2)
+ })
+
+ except Exception as e:
+ print(f"Error converting tournaments to league: {e}")
+ import traceback
+ traceback.print_exc()
+ return jsonify({'status': 'error', 'message': str(e)}), 400
+
# Add this route to your Flask app (around line 850, with the other mobile routes)
@app.route('/mobile/remote')