Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
<i data-lucide="library" class="nav-icon"></i>
My Library
</a>
<a href="#" class="nav-item" data-view="queue">
<i data-lucide="users" class="nav-icon"></i>
Server Queue
</a>
</nav>
</div>
<div class="header-center">
Expand Down Expand Up @@ -105,6 +109,100 @@ <h2>My Games</h2>
</div>
</section>

<!-- Queue View -->
<section id="queue-view" class="view hidden">
<div class="queue-header">
<h1>Server Queue Status</h1>
<div class="queue-controls">
<div class="queue-filters">
<div class="custom-dropdown compact" data-dropdown="queue-region-filter">
<div class="dropdown-trigger" tabindex="0">
<span class="dropdown-text">All Regions</span>
<i data-lucide="chevron-down" class="dropdown-arrow"></i>
</div>
<div class="dropdown-menu">
<div class="dropdown-option selected" data-value="all">All Regions</div>
<div class="dropdown-option" data-value="US">US</div>
<div class="dropdown-option" data-value="EU">EU</div>
<div class="dropdown-option" data-value="JP">Japan</div>
<div class="dropdown-option" data-value="CA">Canada</div>
<div class="dropdown-option" data-value="KR">Korea</div>
<div class="dropdown-option" data-value="THAI">Thailand</div>
<div class="dropdown-option" data-value="MY">Malaysia</div>
</div>
</div>
<div class="custom-dropdown compact" data-dropdown="queue-gpu-filter">
<div class="dropdown-trigger" tabindex="0">
<span class="dropdown-text">All GPUs</span>
<i data-lucide="chevron-down" class="dropdown-arrow"></i>
</div>
<div class="dropdown-menu">
<div class="dropdown-option selected" data-value="all">All GPUs</div>
<div class="dropdown-option" data-value="4080">RTX 4080</div>
<div class="dropdown-option" data-value="5080">RTX 5080</div>
<div class="dropdown-option" data-value="other">Other</div>
</div>
</div>
<label class="queue-filter-checkbox">
<input type="checkbox" id="hide-nuked-servers" checked />
Hide Inactive Servers
</label>
</div>
<button id="refresh-queue-btn" class="btn btn-primary">
<i data-lucide="refresh-cw" class="btn-icon"></i>
Refresh
</button>
</div>
</div>

<div class="queue-stats">
<div class="queue-stat-card">
<span class="stat-value" id="queue-total-servers">--</span>
<span class="stat-label">Active Servers</span>
</div>
<div class="queue-stat-card">
<span class="stat-value" id="queue-avg-position">--</span>
<span class="stat-label">Avg Queue</span>
</div>
<div class="queue-stat-card">
<span class="stat-value" id="queue-best-server">--</span>
<span class="stat-label">Shortest Queue</span>
</div>
<div class="queue-stat-card">
<span class="stat-value" id="queue-last-updated">--</span>
<span class="stat-label">Last Updated</span>
</div>
</div>

<div class="content-section">
<div id="queue-loading" class="queue-loading">
<div class="loading-spinner"></div>
<span>Loading queue data...</span>
</div>
<div id="queue-error" class="queue-error hidden">
<i data-lucide="alert-circle"></i>
<span id="queue-error-message">Failed to load queue data</span>
</div>
<div id="queue-table-container" class="queue-table-container hidden">
<table class="queue-table">
<thead>
<tr>
<th>Server</th>
<th>Location</th>
<th>Region</th>
<th>GPU</th>
<th>Queue Position</th>
<th>Est. Wait Time</th>
<th>Status</th>
</tr>
</thead>
<tbody id="queue-table-body">
</tbody>
</table>
</div>
</div>
</section>

</main>

<!-- Game Detail Modal -->
Expand Down
5 changes: 5 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ mod discord;
mod proxy;
#[cfg(feature = "tauri-app")]
mod cursor;
#[cfg(feature = "tauri-app")]
mod queue;

#[cfg(feature = "tauri-app")]
use tauri::Manager;
Expand Down Expand Up @@ -112,6 +114,9 @@ pub fn run() {
cursor::stop_mouse_polling,
cursor::get_accumulated_mouse_delta,
cursor::is_mouse_polling_active,
// Queue data commands
queue::fetch_queue_data,
queue::fetch_server_mapping,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
184 changes: 184 additions & 0 deletions src-tauri/src/queue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tauri::command;

/// Queue data for a single server
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerQueueData {
#[serde(rename = "QueuePosition")]
pub queue_position: i32,
#[serde(rename = "Last Updated")]
pub last_updated: Option<i64>,
#[serde(rename = "Region")]
pub region: Option<String>,
#[serde(default)]
pub eta: Option<i64>,
}

/// Response from the queue API
#[derive(Debug, Deserialize)]
pub struct QueueApiResponse {
pub status: bool,
#[serde(default)]
pub errors: Vec<String>,
pub data: HashMap<String, ServerQueueData>,
}

/// Server mapping info from the config API
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerMappingInfo {
pub title: String,
pub region: String,
#[serde(default)]
pub is4080_server: bool,
#[serde(default)]
pub is5080_server: bool,
#[serde(default)]
pub nuked: bool,
}

/// Response from the server mapping API
#[derive(Debug, Deserialize)]
pub struct ServerMappingApiResponse {
pub status: bool,
#[serde(default)]
pub errors: Vec<String>,
pub data: HashMap<String, ServerMappingInfo>,
}

/// Combined queue info for a single server (with mapping enrichment)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnrichedServerQueue {
pub server_id: String,
pub queue_position: i32,
pub last_updated: Option<i64>,
pub eta_seconds: Option<i64>,
pub api_region: Option<String>,
// From server mapping
pub title: Option<String>,
pub region: Option<String>,
pub is_4080_server: bool,
pub is_5080_server: bool,
pub nuked: bool,
}

/// Full queue data response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueueDataResponse {
pub servers: Vec<EnrichedServerQueue>,
pub last_fetched: i64,
}

const QUEUE_API_URL: &str = "https://api.printedwaste.com/gfn/queue/";
const SERVER_MAPPING_URL: &str = "https://remote.printedwaste.com/config/GFN_SERVERID_TO_REGION_MAPPING";

/// Fetch queue data from the external API
#[command]
pub async fn fetch_queue_data() -> Result<QueueDataResponse, String> {
let client = reqwest::Client::new();

// Fetch queue data
let queue_response = client
.get(QUEUE_API_URL)
.header("Accept", "application/json")
.send()
.await
.map_err(|e| format!("Failed to fetch queue data: {}", e))?;

if !queue_response.status().is_success() {
let status = queue_response.status();
let body = queue_response.text().await.unwrap_or_default();
return Err(format!("Queue API failed with status {}: {}", status, body));
}

let queue_data: QueueApiResponse = queue_response
.json()
.await
.map_err(|e| format!("Failed to parse queue response: {}", e))?;

if !queue_data.status {
return Err(format!("Queue API returned error: {:?}", queue_data.errors));
}

// Fetch server mapping
let mapping_response = client
.get(SERVER_MAPPING_URL)
.header("Accept", "application/json")
.send()
.await
.map_err(|e| format!("Failed to fetch server mapping: {}", e))?;

let server_mapping: HashMap<String, ServerMappingInfo> = if mapping_response.status().is_success() {
match mapping_response.json::<ServerMappingApiResponse>().await {
Ok(resp) if resp.status => resp.data,
_ => HashMap::new(),
}
} else {
HashMap::new()
};

// Combine queue data with server mapping
let mut servers: Vec<EnrichedServerQueue> = queue_data
.data
.into_iter()
.map(|(server_id, queue_info)| {
let mapping = server_mapping.get(&server_id);
EnrichedServerQueue {
server_id: server_id.clone(),
queue_position: queue_info.queue_position,
last_updated: queue_info.last_updated,
eta_seconds: queue_info.eta.map(|e| e / 1000), // Convert ms to seconds
api_region: queue_info.region,
title: mapping.map(|m| m.title.clone()),
region: mapping.map(|m| m.region.clone()),
is_4080_server: mapping.map(|m| m.is4080_server).unwrap_or(false),
is_5080_server: mapping.map(|m| m.is5080_server).unwrap_or(false),
nuked: mapping.map(|m| m.nuked).unwrap_or(false),
}
})
.collect();

// Sort by queue position (lowest first)
servers.sort_by(|a, b| a.queue_position.cmp(&b.queue_position));

let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;

Ok(QueueDataResponse {
servers,
last_fetched: now,
})
}

/// Fetch only server mapping (for caching or standalone use)
#[command]
pub async fn fetch_server_mapping() -> Result<HashMap<String, ServerMappingInfo>, String> {
let client = reqwest::Client::new();

let response = client
.get(SERVER_MAPPING_URL)
.header("Accept", "application/json")
.send()
.await
.map_err(|e| format!("Failed to fetch server mapping: {}", e))?;

if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("Server mapping API failed with status {}: {}", status, body));
}

let mapping_response: ServerMappingApiResponse = response
.json()
.await
.map_err(|e| format!("Failed to parse server mapping response: {}", e))?;

if !mapping_response.status {
return Err(format!("Server mapping API returned error: {:?}", mapping_response.errors));
}

Ok(mapping_response.data)
}
Loading
Loading