Compare commits

..

No commits in common. "main" and "app-v1.7.0" have entirely different histories.

32 changed files with 134 additions and 386 deletions

View file

@ -250,7 +250,7 @@ jobs:
- name: Generate update manifest
id: generate_manifest
env:
DOWNLOAD_URL_BASE: 'https://desktop.hackatime.hackclub-assets.com'
DOWNLOAD_URL_BASE: 'https://pub-d35fbe65a5b5426bb6d62ff02a8c7d03.r2.dev'
VERSION: '${{ steps.get_version.outputs.version }}'
run: >
# Read signatures into variables

View file

@ -1,4 +1,4 @@
{
"app": "0.0.0",
".": "1.7.5"
".": "1.7.0"
}

View file

@ -1,49 +1,5 @@
# Changelog
## [1.7.5](https://github.com/hackclub/hackatime-desktop/compare/app-v1.7.4...app-v1.7.5) (2025-10-24)
### 🐛 Bugfixes
* eliminate duplicate discord definitions ([472867d](https://github.com/hackclub/hackatime-desktop/commit/472867d58f306d70241b07b5f3135c34055ad555))
* make option enabled by default ([d037bdb](https://github.com/hackclub/hackatime-desktop/commit/d037bdb529a4a078a5d10f7daa68698da9726e5f))
## [1.7.4](https://github.com/hackclub/hackatime-desktop/compare/app-v1.7.3...app-v1.7.4) (2025-10-24)
### 🐛 Bugfixes
* update hackatime url ([e84c1d6](https://github.com/hackclub/hackatime-desktop/commit/e84c1d6a37fdef397189e8a9108f68d4ddb11641))
## [1.7.3](https://github.com/hackclub/hackatime-desktop/compare/app-v1.7.2...app-v1.7.3) (2025-10-24)
### 🐛 Bugfixes
* change hackatime icon ([3688e39](https://github.com/hackclub/hackatime-desktop/commit/3688e39424c6d3e1c0441fde81e8451feded8178))
* correct card alignment issues ([59008e3](https://github.com/hackclub/hackatime-desktop/commit/59008e3849753ccaac037657d65bad2a70387e3c))
### 👽 Miscellaneous
* add license ([ece57e2](https://github.com/hackclub/hackatime-desktop/commit/ece57e29811ca228a255bef7bfe3117c3e3d236d))
* add security policy ([145b3b9](https://github.com/hackclub/hackatime-desktop/commit/145b3b9422bb5b5095a6e5aa59aa66b749338b5a))
## [1.7.2](https://github.com/hackclub/hackatime-desktop/compare/app-v1.7.1...app-v1.7.2) (2025-10-10)
### 🐛 Bugfixes
* add manual oauth link copy for linux ([3ec4f33](https://github.com/hackclub/hackatime-desktop/commit/3ec4f3386a3e2bbf5a2c4bddc80bf28d789b9705))
* fix update popup not showing ([1705082](https://github.com/hackclub/hackatime-desktop/commit/17050825223327da6e603cfe21500cf78c74e215))
## [1.7.1](https://github.com/hackclub/hackatime-desktop/compare/app-v1.7.0...app-v1.7.1) (2025-10-10)
### 👽 Miscellaneous
* update pnpm lock ([bc8a812](https://github.com/hackclub/hackatime-desktop/commit/bc8a8121b1a9feb7e6d3a578375e0c7db4b4970f))
## [1.7.0](https://github.com/hackclub/hackatime-desktop/compare/app-v1.6.2...app-v1.7.0) (2025-10-10)

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Hack Club
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,26 +0,0 @@
# Security Policy
> **Note**: This security policy is specifically for the **Hackatime Desktop** application. For vulnerabilities related to the main Hackatime web app, please refer to the [hackclub/hackatime repository](https://github.com/hackclub/hackatime).
## Supported Versions
We are currently providing security updates for the following versions:
| Version | Supported |
| ------- | ------------------ |
| 1.x.x | :white_check_mark: |
## Reporting a Vulnerability
If you discover a security vulnerability in this project, please report it through one of the following channels:
- **Email**: sebastian@hackclub.com or security@leafd.dev
- **Hack Club Slack**: Send a direct message to @lfd
Please include as much information as possible in your report:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Any suggested fixes (optional)
Thank you for helping me keep this project secure :)

View file

@ -1,7 +1,7 @@
{
"name": "desktop",
"private": true,
"version": "1.7.5",
"version": "1.7.0",
"type": "module",
"packageManager": "pnpm@10.18.0",
"scripts": {

10
pnpm-lock.yaml generated
View file

@ -14,9 +14,6 @@ importers:
'@tauri-apps/api':
specifier: ^2
version: 2.8.0
'@tauri-apps/plugin-autostart':
specifier: ^2
version: 2.5.0
'@tauri-apps/plugin-deep-link':
specifier: ^2.4.3
version: 2.4.3
@ -582,9 +579,6 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-autostart@2.5.0':
resolution: {integrity: sha512-smSt0vydfVB950AeYRbO2S/c01SZrgMVg4FOrFLQLom0R0amsu/8zYaxgttriBdxcofjBZuHv4hmROBQIBVXmA==}
'@tauri-apps/plugin-deep-link@2.4.3':
resolution: {integrity: sha512-yVCZpVG1ZrtfCvE7K5LRSrGqlyPlCrqlKgoREJHnfjyYdDtUhFmZqScOXpL8XL2PizJHDsoahEweuTaUPEokPA==}
@ -1296,10 +1290,6 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.8.4
'@tauri-apps/cli-win32-x64-msvc': 2.8.4
'@tauri-apps/plugin-autostart@2.5.0':
dependencies:
'@tauri-apps/api': 2.8.0
'@tauri-apps/plugin-deep-link@2.4.3':
dependencies:
'@tauri-apps/api': 2.8.0

BIN
src-tauri/icons/128x128.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/128x128@2x.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/32x32.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/Square107x107Logo.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/Square142x142Logo.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/Square150x150Logo.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/Square284x284Logo.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/Square30x30Logo.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/Square310x310Logo.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/Square44x44Logo.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/Square71x71Logo.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/Square89x89Logo.png (Stored with Git LFS)

Binary file not shown.

BIN
src-tauri/icons/StoreLogo.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png (Stored with Git LFS)

Binary file not shown.

View file

@ -283,3 +283,26 @@ pub async fn discord_rpc_auto_disconnect(
rpc_service.disconnect()
}
#[tauri::command]
pub async fn get_discord_rpc_enabled(
discord_rpc_state: State<'_, Arc<tauri::async_runtime::Mutex<DiscordRpcService>>>,
) -> Result<bool, String> {
let rpc_service = discord_rpc_state.lock().await;
Ok(rpc_service.is_connected())
}
#[tauri::command]
pub async fn set_discord_rpc_enabled(
enabled: bool,
discord_rpc_state: State<'_, Arc<tauri::async_runtime::Mutex<DiscordRpcService>>>,
) -> Result<(), String> {
let mut rpc_service = discord_rpc_state.lock().await;
if enabled {
let default_client_id = "1234567890123456789";
rpc_service.connect(default_client_id)
} else {
rpc_service.disconnect()
}
}

View file

@ -36,11 +36,6 @@ fn get_app_version(app: tauri::AppHandle) -> String {
app.package_info().version.to_string()
}
#[tauri::command]
fn get_current_os() -> String {
std::env::consts::OS.to_string()
}
#[derive(Clone, serde::Serialize)]
struct LogEntry {
ts: i64,
@ -117,7 +112,6 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
greet,
get_app_version,
get_current_os,
get_recent_logs,
database::get_platform_info,
@ -140,10 +134,6 @@ pub fn run() {
preferences::get_preferences,
preferences::set_autostart_enabled,
preferences::get_autostart_enabled,
preferences::set_notifications_enabled,
preferences::get_notifications_enabled,
preferences::set_discord_rpc_enabled,
preferences::get_discord_rpc_enabled,
setup::setup_hackatime_macos_linux,
setup::setup_hackatime_windows,
@ -170,6 +160,8 @@ pub fn run() {
discord_rpc::discord_rpc_update_from_heartbeat,
discord_rpc::discord_rpc_auto_connect,
discord_rpc::discord_rpc_auto_disconnect,
discord_rpc::get_discord_rpc_enabled,
discord_rpc::set_discord_rpc_enabled,
projects::get_projects,
projects::get_project_details,

View file

@ -9,16 +9,12 @@ use crate::push_log;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Preferences {
pub autostart_enabled: bool,
pub notifications_enabled: bool,
pub discord_rpc_enabled: bool,
}
impl Default for Preferences {
fn default() -> Self {
Self {
autostart_enabled: true,
notifications_enabled: true,
discord_rpc_enabled: true,
autostart_enabled: false,
}
}
}
@ -90,45 +86,3 @@ pub fn get_autostart_enabled() -> Result<bool, String> {
Ok(preferences.autostart_enabled)
}
#[tauri::command]
pub fn set_notifications_enabled(enabled: bool) -> Result<(), String> {
let mut preferences = load_preferences().unwrap_or_default();
preferences.notifications_enabled = enabled;
save_preferences(&preferences)?;
if enabled {
push_log("info", "backend", "Notifications enabled".to_string());
} else {
push_log("info", "backend", "Notifications disabled".to_string());
}
Ok(())
}
#[tauri::command]
pub fn get_notifications_enabled() -> Result<bool, String> {
let preferences = load_preferences().unwrap_or_default();
Ok(preferences.notifications_enabled)
}
#[tauri::command]
pub fn set_discord_rpc_enabled(enabled: bool) -> Result<(), String> {
let mut preferences = load_preferences().unwrap_or_default();
preferences.discord_rpc_enabled = enabled;
save_preferences(&preferences)?;
if enabled {
push_log("info", "backend", "Discord RPC enabled".to_string());
} else {
push_log("info", "backend", "Discord RPC disabled".to_string());
}
Ok(())
}
#[tauri::command]
pub fn get_discord_rpc_enabled() -> Result<bool, String> {
let preferences = load_preferences().unwrap_or_default();
Ok(preferences.discord_rpc_enabled)
}

View file

@ -45,32 +45,15 @@ fn get_expected_config_content(api_key: &str, api_url: &str) -> String {
}
}
fn check_config_has_required_values(content: &str, api_key: &str, api_url: &str) -> bool {
let normalized = content.replace("\r\n", "\n");
let mut found_api_url = false;
let mut found_api_key = false;
fn normalize_config_content(content: &str) -> String {
for line in normalized.lines() {
let trimmed = line.trim();
if trimmed.starts_with("api_url") {
if let Some(value) = trimmed.split('=').nth(1) {
let value = value.trim();
if value == api_url {
found_api_url = true;
}
}
} else if trimmed.starts_with("api_key") {
if let Some(value) = trimmed.split('=').nth(1) {
let value = value.trim();
if value == api_key {
found_api_key = true;
}
}
}
}
found_api_url && found_api_key
content
.replace("\r\n", "\n")
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
#[tauri::command]
@ -89,7 +72,7 @@ pub async fn check_wakatime_config(api_key: String, api_url: String) -> Result<W
};
let matches = if let Some(ref actual) = actual_content {
check_config_has_required_values(actual, &api_key, &api_url)
normalize_config_content(actual) == normalize_config_content(&expected_content)
} else {
false
};
@ -153,8 +136,23 @@ pub async fn setup_hackatime_macos_linux(api_key: String, api_url: String) -> Re
let config_content = fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read config file: {}", e))?;
if !check_config_has_required_values(&config_content, &api_key, &api_url) {
return Err("Config file is missing required api_url and api_key values".to_string());
let lines: Vec<&str> = config_content.lines().collect();
let mut found_api_url = false;
let mut found_api_key = false;
let mut found_heartbeat_rate = false;
for line in lines {
if line.starts_with("api_url =") {
found_api_url = true;
} else if line.starts_with("api_key =") {
found_api_key = true;
} else if line.starts_with("heartbeat_rate_limit_seconds =") {
found_heartbeat_rate = true;
}
}
if !found_api_url || !found_api_key || !found_heartbeat_rate {
return Err("Config file is missing required fields".to_string());
}
Ok(format!(
@ -193,8 +191,23 @@ pub async fn setup_hackatime_windows(api_key: String, api_url: String) -> Result
let config_content = fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read config file: {}", e))?;
if !check_config_has_required_values(&config_content, &api_key, &api_url) {
return Err("Config file is missing required api_url and api_key values".to_string());
let lines: Vec<&str> = config_content.lines().collect();
let mut found_api_url = false;
let mut found_api_key = false;
let mut found_heartbeat_rate = false;
for line in lines {
if line.starts_with("api_url =") {
found_api_url = true;
} else if line.starts_with("api_key =") {
found_api_key = true;
} else if line.starts_with("heartbeat_rate_limit_seconds =") {
found_heartbeat_rate = true;
}
}
if !found_api_url || !found_api_key || !found_heartbeat_rate {
return Err("Config file is missing required fields".to_string());
}
Ok(format!(

View file

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Hackatime Desktop",
"version": "1.7.5",
"version": "1.7.0",
"identifier": "com.hackclub.hackatime",
"build": {
"beforeDevCommand": "pnpm dev",
@ -29,7 +29,7 @@
}
],
"security": {
"csp": "default-src 'self' ipc: http://ipc.localhost; connect-src 'self' ipc: http://ipc.localhost https://hackatime.hackclub.com https://desktop.hackatime.hackclub-assets.com wss://*.ingest.us.sentry.io https://us.i.posthog.com https://fonts.googleapis.com https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; worker-src 'self' blob:;"
"csp": "default-src 'self' ipc: http://ipc.localhost; connect-src 'self' ipc: http://ipc.localhost https://hackatime.hackclub.com https://pub-d35fbe65a5b5426bb6d62ff02a8c7d03.r2.dev wss://*.ingest.us.sentry.io https://us.i.posthog.com https://fonts.googleapis.com https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; worker-src 'self' blob:;"
},
"withGlobalTauri": true
},
@ -64,7 +64,7 @@
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdERjg4QTFCNTJFMDk0MUQKUldRZGxPQlNHNHI0ZlRkMDN0MGI1MnllY1dUVStZalV3dVdhcTFuREx5SGtBc0txQ2xnTWs3WU4K",
"endpoints": [
"https://desktop.hackatime.hackclub-assets.com/update-manifest.json"
"https://pub-d35fbe65a5b5426bb6d62ff02a8c7d03.r2.dev/update-manifest.json"
]
}
}

View file

@ -81,11 +81,9 @@ const sessionStats = ref<any>(null);
const presenceData = ref<any>(null);
const presenceRefreshInterval = ref<ReturnType<typeof setInterval> | null>(null);
const presenceFetchInProgress = ref(false);
const updateCheckInterval = ref<ReturnType<typeof setInterval> | null>(null);
const oauthUrl = ref<string | null>(null);
const nextPresenceFetchAllowedAt = ref<number>(0);
const lastPresenceFetchAt = ref<number>(0);
const currentOs = ref<string | null>(null);
const currentPage = ref<'home' | 'projects' | 'statistics' | 'settings'>('home');
@ -99,8 +97,6 @@ const updateData = ref<any>(null);
const showUpdateModal = ref(false);
const isInstallingUpdate = ref(false);
const currentVersion = ref<string>('1.5.1');
const lastUpdateCheckTime = ref<number>(0);
const updateCheckInProgress = ref(false);
const weeklyChartData = computed(() => {
@ -135,7 +131,6 @@ onMounted(async () => {
await loadAuthState();
await loadApiConfig();
await loadHackatimeInfo();
await loadCurrentOs();
try {
const appVersion = await invoke("get_app_version") as string;
@ -144,9 +139,10 @@ onMounted(async () => {
console.warn("Failed to get app version:", error);
}
isDevMode.value = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.protocol === 'http:';
isDevMode.value = apiConfig.value.base_url.includes('localhost') ||
apiConfig.value.base_url.includes('127.0.0.1') ||
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
try {
const startUrls = await getCurrent();
@ -187,17 +183,11 @@ onMounted(async () => {
window.addEventListener('focus', async () => {
await loadAuthState();
if (authState.value.is_authenticated) {
checkForUpdatesAndInstall();
}
});
document.addEventListener('visibilitychange', async () => {
if (!document.hidden) {
await loadAuthState();
if (authState.value.is_authenticated) {
checkForUpdatesAndInstall();
}
}
});
@ -207,12 +197,10 @@ onMounted(async () => {
}
checkForUpdatesAndInstall();
startUpdateChecks();
});
onUnmounted(() => {
stopPresenceRefresh();
stopUpdateChecks();
});
async function loadAuthState() {
@ -345,16 +333,6 @@ async function loadHackatimeInfo() {
}
}
async function loadCurrentOs() {
try {
currentOs.value = await invoke("get_current_os") as string;
console.log("Current OS detected:", currentOs.value);
} catch (error) {
console.error("Failed to detect current OS:", error);
currentOs.value = null;
}
}
async function loadPresenceData() {
if (presenceFetchInProgress.value) {
return;
@ -400,24 +378,6 @@ function stopPresenceRefresh() {
}
}
function startUpdateChecks() {
if (updateCheckInterval.value) {
clearInterval(updateCheckInterval.value);
updateCheckInterval.value = null;
}
updateCheckInterval.value = setInterval(() => {
checkForUpdatesAndInstall();
}, 60 * 60 * 1000); // Check every hour
console.log("Started periodic update checks (every 60 minutes)");
}
function stopUpdateChecks() {
if (updateCheckInterval.value) {
clearInterval(updateCheckInterval.value);
updateCheckInterval.value = null;
}
}
async function authenticate() {
isLoading.value = true;
@ -530,25 +490,12 @@ async function handleDirectOAuthAuth(token?: string) {
}
}
async function checkForUpdatesAndInstall(retryCount = 0) {
if (updateCheckInProgress.value) {
console.info('[AUTO-UPDATE] Update check already in progress, skipping');
async function checkForUpdatesAndInstall() {
if (isDevMode.value) {
console.info('[AUTO-UPDATE] Skipping auto-update check in development mode');
return;
}
const now = Date.now();
const timeSinceLastCheck = now - lastUpdateCheckTime.value;
const minInterval = 5 * 60 * 1000; // 5 minutes minimum between checks
if (timeSinceLastCheck < minInterval && lastUpdateCheckTime.value > 0) {
console.info(`[AUTO-UPDATE] Skipping update check, last check was ${Math.round(timeSinceLastCheck / 1000)}s ago`);
return;
}
updateCheckInProgress.value = true;
lastUpdateCheckTime.value = now;
try {
console.info('[AUTO-UPDATE] Checking for updates...');
const update = await check();
@ -563,19 +510,6 @@ async function checkForUpdatesAndInstall(retryCount = 0) {
}
} catch (error) {
console.error('[AUTO-UPDATE] Auto-update check failed:', error);
if (retryCount < 2) {
console.info(`[AUTO-UPDATE] Retrying update check in 10 seconds (attempt ${retryCount + 1}/3)`);
setTimeout(() => {
updateCheckInProgress.value = false;
checkForUpdatesAndInstall(retryCount + 1);
}, 10000);
return;
} else {
console.error('[AUTO-UPDATE] Failed to check for updates after 3 attempts');
}
} finally {
updateCheckInProgress.value = false;
}
}
@ -640,7 +574,6 @@ async function handleInstallNow() {
:isLoading="isLoading"
:isDevMode="isDevMode"
:oauthUrl="oauthUrl"
:currentOs="currentOs"
@authenticate="authenticate"
@handleDirectOAuthAuth="handleDirectOAuthAuth"
@openOAuthUrlManually="openOAuthUrlManually"
@ -761,21 +694,17 @@ async function handleInstallNow() {
<div v-if="authState.is_authenticated && userStats" class="w-64 min-w-64 flex flex-col responsive-full-width">
<div class="card-3d-app h-full">
<div class="rounded-[8px] border border-black p-4 card-3d-app-front h-full flex flex-col" style="background-color: #3D2C3E;">
<div class="flex items-center justify-between mb-4">
<h2 class="text-white text-[16px] font-bold italic m-0" style="font-family: 'Outfit', sans-serif;">
leaderboard
</h2>
<div class="flex gap-2 text-[10px]" style="font-family: 'Outfit', sans-serif;">
<span class="text-white underline cursor-pointer">friends</span>
<span class="text-white cursor-pointer">global</span>
</div>
</div>
<div class="flex items-center justify-center h-full">
<p class="text-white text-[18px] font-semibold opacity-60" style="font-family: 'Outfit', sans-serif;">
Coming Soon...
</p>
<div class="flex items-center justify-between mb-4">
<h2 class="text-white text-[16px] font-bold italic m-0" style="font-family: 'Outfit', sans-serif;">
leaderboard
</h2>
<div class="flex gap-2 text-[10px]" style="font-family: 'Outfit', sans-serif;">
<span class="text-white underline cursor-pointer">friends</span>
<span class="text-white cursor-pointer">global</span>
</div>
</div>
<!-- Leaderboard content would go here -->
</div>
</div>
</div>
</div>

View file

@ -1,13 +1,13 @@
<template>
<div class="card-3d">
<div class="rounded-[8px] border border-black p-6 card-3d-front h-full flex flex-col" style="background-color: #3D2C3E;">
<div class="flex items-start space-x-4">
<div class="text-3xl flex-shrink-0">{{ icon }}</div>
<div class="flex-1 flex flex-col min-w-0">
<div class="flex items-start space-x-4 flex-1">
<div class="text-3xl">{{ icon }}</div>
<div class="flex-1 flex flex-col">
<h3 class="text-lg font-semibold text-text-primary mb-2">{{ title }}</h3>
<p class="text-text-secondary mb-4 text-sm line-clamp-2">{{ description }}</p>
<div class="mt-auto">
<div class="text-2xl font-bold mb-1" :style="{ color: color }">{{ value }}</div>
<p class="text-text-secondary mb-3 flex-1">{{ description }}</p>
<div class="flex items-center justify-between">
<div class="text-2xl font-bold" :style="{ color: color }">{{ value }}</div>
<div class="text-sm text-text-secondary">{{ trend }}</div>
</div>
</div>

View file

@ -83,28 +83,6 @@
</span>
</button>
<!-- Linux-specific OAuth URL copy section -->
<div v-if="currentOs === 'linux' && oauthUrl" class="mt-6 mb-6 p-4 bg-[#2A1F2B] border border-white/20 rounded-lg">
<p class="text-white/70 text-sm mb-3" style="font-family: 'Outfit', sans-serif;">
<strong>Linux:</strong> Copy the link to open in your browser manually
</p>
<div class="flex gap-2">
<input
:value="oauthUrl"
readonly
class="flex-1 p-3 bg-[#3D2C3E] border border-white/20 rounded-lg text-white font-mono text-xs focus:outline-none focus:border-[#E99682] transition-colors select-all"
@click="($event.target as HTMLInputElement)?.select()"
/>
<button
@click="copyOAuthUrl"
class="px-4 py-3 rounded-lg border-2 border-[rgba(0,0,0,0.35)] font-bold text-sm transition-all bg-[#E99682] text-white hover:bg-[#d88672]"
style="font-family: 'Outfit', sans-serif;"
>
Copy
</button>
</div>
</div>
<button
@click="cancelAuth"
class="text-white/60 text-base hover:text-white transition-colors font-medium"
@ -130,6 +108,12 @@ const emit = defineEmits<{
openOAuthUrlManually: [];
}>();
defineProps<{
isLoading: boolean;
isDevMode: boolean;
oauthUrl: string | null;
}>();
const authInProgress = ref(false);
const directToken = ref('');
@ -154,25 +138,6 @@ function handleDirectAuth() {
directToken.value = '';
}
}
const props = defineProps<{
isLoading: boolean;
isDevMode: boolean;
oauthUrl: string | null;
currentOs: string | null;
}>();
async function copyOAuthUrl() {
if (!props.oauthUrl) return;
try {
await navigator.clipboard.writeText(props.oauthUrl);
alert("OAuth URL copied to clipboard!");
} catch (error) {
console.error("Failed to copy OAuth URL:", error);
alert("Failed to copy OAuth URL to clipboard");
}
}
</script>
<style scoped>

View file

@ -45,9 +45,9 @@
<h4 class="font-medium text-text-primary mb-1">Notifications</h4>
<p class="text-sm text-text-secondary">Show desktop notifications</p>
</div>
<label class="switch" :class="{ 'opacity-50 cursor-not-allowed': isLoading }">
<input type="checkbox" :checked="notificationsEnabled" :disabled="isLoading" @change="toggleNotifications">
<span class="slider" :class="{ 'animate-pulse': isLoading }"></span>
<label class="switch">
<input type="checkbox" checked>
<span class="slider"></span>
</label>
</div>
</div>
@ -291,7 +291,6 @@ const emit = defineEmits<{
const discordRpcEnabled = ref(false);
const autostartEnabled = ref(false);
const notificationsEnabled = ref(false);
const isLoading = ref(false);
const appVersion = ref('...');
const isClearingCache = ref(false);
@ -566,14 +565,6 @@ async function loadAutostartState() {
}
}
async function loadNotificationsState() {
try {
notificationsEnabled.value = await invoke("get_notifications_enabled");
} catch (error) {
console.error("Failed to load notifications state:", error);
}
}
async function toggleAutostart() {
if (isLoading.value) return;
@ -608,23 +599,6 @@ async function toggleDiscordRpc() {
}
}
async function toggleNotifications() {
if (isLoading.value) return;
isLoading.value = true;
try {
const newState = !notificationsEnabled.value;
await invoke("set_notifications_enabled", { enabled: newState });
notificationsEnabled.value = newState;
} catch (error) {
console.error("Failed to toggle notifications:", error);
notificationsEnabled.value = !notificationsEnabled.value;
} finally {
isLoading.value = false;
}
}
function copyApiKey() {
emit('copyApiKey')
}
@ -715,7 +689,6 @@ async function downloadAndInstallUpdate() {
onMounted(async () => {
loadDiscordRpcState();
loadAutostartState();
loadNotificationsState();
try {
appVersion.value = await getVersion();
} catch (error) {