Compare commits

..

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

30 changed files with 121 additions and 240 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.2"
}

View file

@ -1,34 +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)

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.2",
"type": "module",
"packageManager": "pnpm@10.18.0",
"scripts": {

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

@ -140,10 +140,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 +166,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.2",
"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

@ -761,21 +761,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

@ -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) {