Download leaderboards button and single instance enforcement

This commit is contained in:
2025-07-22 00:02:28 -07:00
parent 5f919b4d40
commit 42daffee84
4 changed files with 91 additions and 21 deletions

View File

@@ -26,3 +26,6 @@ zip = "4.3.0"
libc = "0.2.174" libc = "0.2.174"
tauri-plugin-dialog = "2.3.1" tauri-plugin-dialog = "2.3.1"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.3.1"

View File

@@ -1,13 +1,15 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use std::{ use std::{
fs::{create_dir_all, File}, fs::{File, create_dir_all},
io::{copy, BufReader}, io::{BufReader, Write, copy},
path::PathBuf, path::PathBuf,
process::Command, time::Duration, process::Command,
time::Duration,
}; };
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use tauri_plugin_opener::OpenerExt;
use tokio::{io::AsyncWriteExt, task::spawn_blocking, time::timeout}; use tokio::{io::AsyncWriteExt, task::spawn_blocking, time::timeout};
use zip::ZipArchive; use zip::ZipArchive;
@@ -100,7 +102,8 @@ async fn download(
0 0
}; };
app.emit("download-progress", format!("{}:{}", &name, progress)).unwrap(); app.emit("download-progress", format!("{}:{}", &name, progress))
.unwrap();
} }
if total_size > 0 && downloaded < total_size { if total_size > 0 && downloaded < total_size {
@@ -163,15 +166,66 @@ fn launch_game(app: AppHandle, name: String, executable: String) {
} }
} }
#[tauri::command]
fn download_leaderboard(app: AppHandle, content: String) {
app.dialog().file().save_file(move |file_path| {
if let Some(path) = file_path {
let mut path_buf = PathBuf::from(path.to_string());
if path_buf.extension().map(|ext| ext != "csv").unwrap_or(true) {
path_buf.set_extension("csv");
}
let path_str = path_buf.to_string_lossy().to_string();
if path_str.is_empty() {
app.dialog()
.message("No file selected.")
.kind(MessageDialogKind::Error)
.title("Error")
.show(|_| {});
return;
}
let mut file = match File::create(&path_buf) {
Ok(f) => f,
Err(e) => {
app.dialog()
.message(format!("Failed to create file: {}", e))
.kind(MessageDialogKind::Error)
.title("Error")
.show(|_| {});
return;
}
};
if let Err(e) = file.write_all(content.as_bytes()) {
app.dialog()
.message(format!("Failed to write to file: {}", e))
.kind(MessageDialogKind::Error)
.title("Error")
.show(|_| {});
} else {
let _ = app.opener().open_path(path.to_string(), None::<&str>);
}
}
})
}
pub fn run() { pub fn run() {
#[allow(unused_variables)] #[allow(unused_variables)]
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
let _ = app
.get_webview_window("main")
.expect("no main window")
.set_focus();
}))
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_decorum::init()) .plugin(tauri_plugin_decorum::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![download, launch_game]) .invoke_handler(tauri::generate_handler![
download,
launch_game,
download_leaderboard
])
.setup(|app| { .setup(|app| {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {

View File

@@ -29,14 +29,6 @@
} }
}, },
"bundle": { "bundle": {
"active": true, "active": false
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
} }
} }

View File

@@ -5,6 +5,7 @@ import { LeaderboardEntry } from '../types/LeaderboardEntry'
import { app } from '@tauri-apps/api' import { app } from '@tauri-apps/api'
import { platform } from '@tauri-apps/plugin-os' import { platform } from '@tauri-apps/plugin-os'
import { decrypt } from '../util/Encryption' import { decrypt } from '../util/Encryption'
import { invoke } from '@tauri-apps/api/core'
export default function Leaderboards () { export default function Leaderboards () {
const [leaderboardData, setLeaderboardData] = useState<LeaderboardEntry[]>([]) const [leaderboardData, setLeaderboardData] = useState<LeaderboardEntry[]>([])
@@ -30,6 +31,17 @@ export default function Leaderboards () {
.finally(() => setLoading(false)) .finally(() => setLoading(false))
} }
function downloadLeaderboard () {
let content = 'Username,Score\n'
leaderboardData.forEach(entry => {
content += `${entry.username},${entry.value}\n`
})
while (content.endsWith('\n')) {
content = content.slice(0, -1)
}
invoke('download_leaderboard', { content })
}
useEffect(() => { useEffect(() => {
refresh() refresh()
}, []) }, [])
@@ -38,13 +50,22 @@ export default function Leaderboards () {
<div className='mx-4 mt-4'> <div className='mx-4 mt-4'>
<div className='flex justify-between items-center mb-4'> <div className='flex justify-between items-center mb-4'>
<p className='text-3xl'>Leaderboards</p> <p className='text-3xl'>Leaderboards</p>
<button <div className='flex gap-2'>
className='button text-3xl' <button
onClick={refresh} className='button text-3xl'
disabled={loading} onClick={downloadLeaderboard}
> disabled={loading || leaderboardData.length === 0}
Refresh >
</button> Download Leaderboards
</button>
<button
className='button text-3xl'
onClick={refresh}
disabled={loading}
>
Refresh
</button>
</div>
</div> </div>
<div className='leaderboard-container'> <div className='leaderboard-container'>
<div className='leaderboard-scroll'> <div className='leaderboard-scroll'>