mirror of
https://github.com/System-End/hackatime-desktop.git
synced 2026-04-19 22:05:10 +00:00
Compare commits
20 commits
app-v1.6.2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
832a610693 | ||
|
|
472867d58f | ||
|
|
d037bdb529 | ||
|
|
6509fc85e0 | ||
|
|
e84c1d6a37 | ||
|
|
a5746811a9 | ||
|
|
3688e39424 | ||
|
|
59008e3849 | ||
|
|
145b3b9422 | ||
|
|
ece57e2981 | ||
|
|
d0c183e71c | ||
|
|
1705082522 | ||
|
|
3ec4f3386a | ||
|
|
0feffa4a50 | ||
|
|
bc8a8121b1 | ||
|
|
fe6de62c2d | ||
|
|
48146f0091 | ||
|
|
a266acc5d4 | ||
|
|
dab9a807a5 | ||
|
|
db732471b7 |
41 changed files with 695 additions and 245 deletions
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
|
|
@ -250,7 +250,7 @@ jobs:
|
|||
- name: Generate update manifest
|
||||
id: generate_manifest
|
||||
env:
|
||||
DOWNLOAD_URL_BASE: 'https://pub-d35fbe65a5b5426bb6d62ff02a8c7d03.r2.dev'
|
||||
DOWNLOAD_URL_BASE: 'https://desktop.hackatime.hackclub-assets.com'
|
||||
VERSION: '${{ steps.get_version.outputs.version }}'
|
||||
run: >
|
||||
# Read signatures into variables
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"app": "0.0.0",
|
||||
".": "1.6.2"
|
||||
".": "1.7.5"
|
||||
}
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
|
|
@ -1,5 +1,67 @@
|
|||
# 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)
|
||||
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* add autostart functionality ([dab9a80](https://github.com/hackclub/hackatime-desktop/commit/dab9a807a52d5cbcdf04022e7d440d59f612a75c))
|
||||
|
||||
|
||||
### 🐛 Bugfixes
|
||||
|
||||
* adjust project page to api response ([48146f0](https://github.com/hackclub/hackatime-desktop/commit/48146f009150d13421f82642a7cc7a32ece36810))
|
||||
* app now hides on closing ([a266acc](https://github.com/hackclub/hackatime-desktop/commit/a266acc5d44f8b6e8319d0f49889c2ee0e13d6e6))
|
||||
|
||||
|
||||
### 👽 Miscellaneous
|
||||
|
||||
* update readme with download buttons ([db73247](https://github.com/hackclub/hackatime-desktop/commit/db732471b7f037f4b19ea20b9fdabbc3f5ce51b3))
|
||||
|
||||
## [1.6.2](https://github.com/hackclub/hackatime-desktop/compare/app-v1.6.1...app-v1.6.2) (2025-10-09)
|
||||
|
||||
|
||||
|
|
|
|||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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.
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
# Hackatime Desktop
|
||||
|
||||
[](https://hackatimerelease.leafd.workers.dev/latest?platform=macos)
|
||||
[](https://hackatimerelease.leafd.workers.dev/latest?platform=windows)
|
||||
|
||||
[](https://github.com/hackclub/hackatime-desktop/actions/workflows/release.yaml)
|
||||
|
||||
Desktop app for [Hackatime](https://hackatime.hackclub.com). Built with Tauri, Vue 3, TypeScript, and Rust.
|
||||
|
|
|
|||
26
SECURITY.md
Normal file
26
SECURITY.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# 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 :)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "desktop",
|
||||
"private": true,
|
||||
"version": "1.6.2",
|
||||
"version": "1.7.5",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.18.0",
|
||||
"scripts": {
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"dependencies": {
|
||||
"@sentry/vue": "^10.18.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-autostart": "^2",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.3",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-process": "~2",
|
||||
|
|
|
|||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -14,6 +14,9 @@ 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
|
||||
|
|
@ -579,6 +582,9 @@ 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==}
|
||||
|
||||
|
|
@ -1290,6 +1296,10 @@ 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
|
||||
|
|
|
|||
82
src-tauri/Cargo.lock
generated
82
src-tauri/Cargo.lock
generated
|
|
@ -240,6 +240,17 @@ version = "1.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "auto-launch"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
|
||||
dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"thiserror 1.0.69",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
|
|
@ -833,6 +844,7 @@ dependencies = [
|
|||
"sqlx",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
|
|
@ -855,13 +867,33 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||
dependencies = [
|
||||
"dirs-sys 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users 0.4.6",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -872,7 +904,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
|
|
@ -1012,7 +1044,7 @@ dependencies = [
|
|||
"rustc_version",
|
||||
"toml 0.9.7",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
"winreg 0.55.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3633,6 +3665,17 @@ dependencies = [
|
|||
"bitflags 2.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
|
|
@ -4740,7 +4783,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
"embed_plist",
|
||||
"getrandom 0.3.3",
|
||||
|
|
@ -4791,7 +4834,7 @@ checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"glob",
|
||||
"heck 0.5.0",
|
||||
"json-patch",
|
||||
|
|
@ -4863,6 +4906,20 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-autostart"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "062cdcd483d5e3148c9a64dabf8c574e239e2aa1193cf208d95cf89a676f87a5"
|
||||
dependencies = [
|
||||
"auto-launch",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.4.3"
|
||||
|
|
@ -4938,7 +4995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
|
|
@ -5447,7 +5504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"libappindicator",
|
||||
"muda",
|
||||
"objc2 0.6.3",
|
||||
|
|
@ -6469,6 +6526,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.55.0"
|
||||
|
|
@ -6501,7 +6567,7 @@ dependencies = [
|
|||
"block2 0.6.2",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"dpi",
|
||||
"dunce",
|
||||
"gdkx11",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ tauri = { version = "2", features = ["tray-icon"] }
|
|||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-autostart = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
open = "5"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
"opener:default",
|
||||
"deep-link:default",
|
||||
"updater:default",
|
||||
"process:default"
|
||||
"process:default",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@
|
|||
"opener:default",
|
||||
"deep-link:default",
|
||||
"updater:default",
|
||||
"process:default"
|
||||
"process:default",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled"
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/icons/128x128.png
(Stored with Git LFS)
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)
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)
BIN
src-tauri/icons/32x32.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/Square107x107Logo.png
(Stored with Git LFS)
BIN
src-tauri/icons/Square107x107Logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/Square142x142Logo.png
(Stored with Git LFS)
BIN
src-tauri/icons/Square142x142Logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/Square150x150Logo.png
(Stored with Git LFS)
BIN
src-tauri/icons/Square150x150Logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/Square284x284Logo.png
(Stored with Git LFS)
BIN
src-tauri/icons/Square284x284Logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/Square30x30Logo.png
(Stored with Git LFS)
BIN
src-tauri/icons/Square30x30Logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/Square310x310Logo.png
(Stored with Git LFS)
BIN
src-tauri/icons/Square310x310Logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/Square44x44Logo.png
(Stored with Git LFS)
BIN
src-tauri/icons/Square44x44Logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/Square71x71Logo.png
(Stored with Git LFS)
BIN
src-tauri/icons/Square71x71Logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/Square89x89Logo.png
(Stored with Git LFS)
BIN
src-tauri/icons/Square89x89Logo.png
(Stored with Git LFS)
Binary file not shown.
BIN
src-tauri/icons/StoreLogo.png
(Stored with Git LFS)
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: 85 KiB After Width: | Height: | Size: 45 KiB |
BIN
src-tauri/icons/icon.png
(Stored with Git LFS)
BIN
src-tauri/icons/icon.png
(Stored with Git LFS)
Binary file not shown.
|
|
@ -283,26 +283,3 @@ 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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ mod config;
|
|||
mod database;
|
||||
mod db_commands;
|
||||
mod discord_rpc;
|
||||
mod preferences;
|
||||
mod projects;
|
||||
mod session;
|
||||
mod setup;
|
||||
|
|
@ -35,6 +36,11 @@ 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,
|
||||
|
|
@ -69,14 +75,15 @@ pub fn run() {
|
|||
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
||||
push_log("info", "backend", format!("Single instance detected. Args: {:?}, CWD: {}", args, cwd));
|
||||
|
||||
// Show the existing window
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
push_log("info", "backend", "Brought existing window to front".to_string());
|
||||
}
|
||||
|
||||
// Process any deep links from the new instance attempt
|
||||
for arg in args {
|
||||
if arg.starts_with("hackatime://") {
|
||||
push_log("info", "backend", format!("Processing deep link from second instance: {}", arg));
|
||||
|
|
@ -84,6 +91,7 @@ pub fn run() {
|
|||
}
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec!["--minimized"])))
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
|
|
@ -109,6 +117,7 @@ pub fn run() {
|
|||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
get_app_version,
|
||||
get_current_os,
|
||||
get_recent_logs,
|
||||
|
||||
database::get_platform_info,
|
||||
|
|
@ -128,6 +137,14 @@ pub fn run() {
|
|||
auth::load_auth_state,
|
||||
auth::clear_auth_state,
|
||||
|
||||
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,
|
||||
setup::test_hackatime_heartbeat,
|
||||
|
|
@ -153,8 +170,6 @@ 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,
|
||||
|
|
@ -260,6 +275,27 @@ pub fn run() {
|
|||
Err(e) => push_log("warn", "backend", format!("Discord RPC auto-connect failed (this is optional): {}", e)),
|
||||
}
|
||||
});
|
||||
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
let autolaunch_manager = app.autolaunch();
|
||||
match preferences::load_preferences() {
|
||||
Ok(prefs) => {
|
||||
if prefs.autostart_enabled {
|
||||
match autolaunch_manager.enable() {
|
||||
Ok(_) => push_log("info", "backend", "Autostart enabled on app startup".to_string()),
|
||||
Err(e) => push_log("error", "backend", format!("Failed to enable autostart: {}", e)),
|
||||
}
|
||||
} else {
|
||||
match autolaunch_manager.disable() {
|
||||
Ok(_) => push_log("info", "backend", "Autostart disabled on app startup".to_string()),
|
||||
Err(e) => push_log("error", "backend", format!("Failed to disable autostart: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
push_log("warn", "backend", format!("Failed to load preferences for autostart: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
|
|
@ -281,22 +317,23 @@ pub fn run() {
|
|||
push_log("info", "backend", "🪟 Window close requested - hiding to tray".to_string());
|
||||
api.prevent_close();
|
||||
|
||||
// Use the app handle to get the window and hide it asynchronously
|
||||
// This prevents potential re-entrancy issues
|
||||
let app_clone = app_handle.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(win) = app_clone.get_webview_window("main") {
|
||||
let _ = win.hide();
|
||||
push_log("info", "backend", "✅ Window hidden to tray".to_string());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = app_clone.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
push_log("info", "backend", "✅ App removed from Dock".to_string());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
WindowEvent::Resized(_) => {
|
||||
// Handle resize events gracefully - no action needed
|
||||
// This prevents potential crashes on macOS with transparent windows
|
||||
}
|
||||
WindowEvent::Moved(_) => {
|
||||
// Handle move events gracefully
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
134
src-tauri/src/preferences.rs
Normal file
134
src-tauri/src/preferences.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
use crate::database::get_hackatime_config_dir;
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_preferences_path() -> Result<PathBuf, String> {
|
||||
let config_dir = get_hackatime_config_dir()?;
|
||||
Ok(config_dir.join("preferences.json"))
|
||||
}
|
||||
|
||||
pub fn load_preferences() -> Result<Preferences, String> {
|
||||
let path = get_preferences_path()?;
|
||||
|
||||
if !path.exists() {
|
||||
push_log("info", "backend", "No preferences file found, using defaults".to_string());
|
||||
return Ok(Preferences::default());
|
||||
}
|
||||
|
||||
let contents = fs::read_to_string(&path)
|
||||
.map_err(|e| format!("Failed to read preferences file: {}", e))?;
|
||||
|
||||
let preferences: Preferences = serde_json::from_str(&contents)
|
||||
.map_err(|e| format!("Failed to parse preferences: {}", e))?;
|
||||
|
||||
push_log("info", "backend", "Loaded preferences successfully".to_string());
|
||||
Ok(preferences)
|
||||
}
|
||||
|
||||
pub fn save_preferences(preferences: &Preferences) -> Result<(), String> {
|
||||
let path = get_preferences_path()?;
|
||||
|
||||
let contents = serde_json::to_string_pretty(preferences)
|
||||
.map_err(|e| format!("Failed to serialize preferences: {}", e))?;
|
||||
|
||||
fs::write(&path, contents)
|
||||
.map_err(|e| format!("Failed to write preferences file: {}", e))?;
|
||||
|
||||
push_log("info", "backend", "Saved preferences successfully".to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_preferences() -> Result<Preferences, String> {
|
||||
load_preferences()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_autostart_enabled(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let mut preferences = load_preferences().unwrap_or_default();
|
||||
preferences.autostart_enabled = enabled;
|
||||
save_preferences(&preferences)?;
|
||||
|
||||
let autolaunch_manager = app.autolaunch();
|
||||
if enabled {
|
||||
autolaunch_manager.enable()
|
||||
.map_err(|e| format!("Failed to enable autostart: {}", e))?;
|
||||
push_log("info", "backend", "Autostart enabled".to_string());
|
||||
} else {
|
||||
autolaunch_manager.disable()
|
||||
.map_err(|e| format!("Failed to disable autostart: {}", e))?;
|
||||
push_log("info", "backend", "Autostart disabled".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_autostart_enabled() -> Result<bool, String> {
|
||||
let preferences = load_preferences().unwrap_or_default();
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -3,6 +3,7 @@ use tauri::State;
|
|||
|
||||
use crate::auth::AuthState;
|
||||
use crate::config::ApiConfig;
|
||||
use crate::push_log;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_projects(
|
||||
|
|
@ -50,6 +51,16 @@ pub async fn get_projects(
|
|||
.await
|
||||
.map_err(|e| format!("Failed to parse projects response: {}", e))?;
|
||||
|
||||
push_log(
|
||||
"info",
|
||||
"backend",
|
||||
format!(
|
||||
"RAW PROJECTS API RESPONSE: {}",
|
||||
serde_json::to_string_pretty(&projects_response)
|
||||
.unwrap_or_else(|_| "Failed to serialize".to_string())
|
||||
),
|
||||
);
|
||||
|
||||
Ok(projects_response)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,15 +45,32 @@ fn get_expected_config_content(api_key: &str, api_url: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn normalize_config_content(content: &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;
|
||||
|
||||
content
|
||||
.replace("\r\n", "\n")
|
||||
.lines()
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
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
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -72,7 +89,7 @@ pub async fn check_wakatime_config(api_key: String, api_url: String) -> Result<W
|
|||
};
|
||||
|
||||
let matches = if let Some(ref actual) = actual_content {
|
||||
normalize_config_content(actual) == normalize_config_content(&expected_content)
|
||||
check_config_has_required_values(actual, &api_key, &api_url)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
|
@ -136,23 +153,8 @@ 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))?;
|
||||
|
||||
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());
|
||||
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());
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
|
|
@ -191,23 +193,8 @@ 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))?;
|
||||
|
||||
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());
|
||||
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());
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
|
|
|
|||
|
|
@ -54,7 +54,12 @@ pub fn setup_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
|||
if let Some(window) = app.get_webview_window("main") {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
let _ = window.hide();
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
|
|
@ -65,6 +70,10 @@ pub fn setup_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
|||
..
|
||||
} => {
|
||||
let app = tray.app_handle();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ use tauri::Manager;
|
|||
|
||||
#[tauri::command]
|
||||
pub async fn show_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
window
|
||||
.show()
|
||||
|
|
@ -20,6 +23,10 @@ pub async fn hide_window(app: tauri::AppHandle) -> Result<(), String> {
|
|||
.hide()
|
||||
.map_err(|e| format!("Failed to hide window: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -30,7 +37,13 @@ pub async fn toggle_window(app: tauri::AppHandle) -> Result<(), String> {
|
|||
window
|
||||
.hide()
|
||||
.map_err(|e| format!("Failed to hide window: {}", e))?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||
|
||||
window
|
||||
.show()
|
||||
.map_err(|e| format!("Failed to show window: {}", e))?;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Hackatime Desktop",
|
||||
"version": "1.6.2",
|
||||
"version": "1.7.5",
|
||||
"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://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:;"
|
||||
"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:;"
|
||||
},
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdERjg4QTFCNTJFMDk0MUQKUldRZGxPQlNHNHI0ZlRkMDN0MGI1MnllY1dUVStZalV3dVdhcTFuREx5SGtBc0txQ2xnTWs3WU4K",
|
||||
"endpoints": [
|
||||
"https://pub-d35fbe65a5b5426bb6d62ff02a8c7d03.r2.dev/update-manifest.json"
|
||||
"https://desktop.hackatime.hackclub-assets.com/update-manifest.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
103
src/App.vue
103
src/App.vue
|
|
@ -81,9 +81,11 @@ 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');
|
||||
|
||||
|
|
@ -97,6 +99,8 @@ 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(() => {
|
||||
|
|
@ -131,6 +135,7 @@ onMounted(async () => {
|
|||
await loadAuthState();
|
||||
await loadApiConfig();
|
||||
await loadHackatimeInfo();
|
||||
await loadCurrentOs();
|
||||
|
||||
try {
|
||||
const appVersion = await invoke("get_app_version") as string;
|
||||
|
|
@ -139,10 +144,9 @@ onMounted(async () => {
|
|||
console.warn("Failed to get app version:", error);
|
||||
}
|
||||
|
||||
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';
|
||||
isDevMode.value = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1' ||
|
||||
window.location.protocol === 'http:';
|
||||
|
||||
try {
|
||||
const startUrls = await getCurrent();
|
||||
|
|
@ -183,11 +187,17 @@ 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -197,10 +207,12 @@ onMounted(async () => {
|
|||
}
|
||||
|
||||
checkForUpdatesAndInstall();
|
||||
startUpdateChecks();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPresenceRefresh();
|
||||
stopUpdateChecks();
|
||||
});
|
||||
|
||||
async function loadAuthState() {
|
||||
|
|
@ -333,6 +345,16 @@ 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;
|
||||
|
|
@ -378,6 +400,24 @@ 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;
|
||||
|
|
@ -490,12 +530,25 @@ async function handleDirectOAuthAuth(token?: string) {
|
|||
}
|
||||
}
|
||||
|
||||
async function checkForUpdatesAndInstall() {
|
||||
if (isDevMode.value) {
|
||||
console.info('[AUTO-UPDATE] Skipping auto-update check in development mode');
|
||||
async function checkForUpdatesAndInstall(retryCount = 0) {
|
||||
|
||||
if (updateCheckInProgress.value) {
|
||||
console.info('[AUTO-UPDATE] Update check already in progress, skipping');
|
||||
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();
|
||||
|
|
@ -510,6 +563,19 @@ async function checkForUpdatesAndInstall() {
|
|||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -574,6 +640,7 @@ async function handleInstallNow() {
|
|||
:isLoading="isLoading"
|
||||
:isDevMode="isDevMode"
|
||||
:oauthUrl="oauthUrl"
|
||||
:currentOs="currentOs"
|
||||
@authenticate="authenticate"
|
||||
@handleDirectOAuthAuth="handleDirectOAuthAuth"
|
||||
@openOAuthUrlManually="openOAuthUrlManually"
|
||||
|
|
@ -694,17 +761,21 @@ 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 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>
|
||||
</div>
|
||||
<!-- Leaderboard content would go here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 flex-1">
|
||||
<div class="text-3xl">{{ icon }}</div>
|
||||
<div class="flex-1 flex flex-col">
|
||||
<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">
|
||||
<h3 class="text-lg font-semibold text-text-primary mb-2">{{ title }}</h3>
|
||||
<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>
|
||||
<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>
|
||||
<div class="text-sm text-text-secondary">{{ trend }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -83,6 +83,28 @@
|
|||
</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"
|
||||
|
|
@ -108,12 +130,6 @@ const emit = defineEmits<{
|
|||
openOAuthUrlManually: [];
|
||||
}>();
|
||||
|
||||
defineProps<{
|
||||
isLoading: boolean;
|
||||
isDevMode: boolean;
|
||||
oauthUrl: string | null;
|
||||
}>();
|
||||
|
||||
const authInProgress = ref(false);
|
||||
const directToken = ref('');
|
||||
|
||||
|
|
@ -138,6 +154,25 @@ 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>
|
||||
|
|
|
|||
|
|
@ -138,10 +138,9 @@
|
|||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-white font-semibold text-lg mb-1 truncate" style="font-family: 'Outfit', sans-serif;">{{ project?.name || 'Unnamed' }}</h4>
|
||||
<div class="flex items-center gap-4 text-sm text-white/60 flex-wrap" style="font-family: 'Outfit', sans-serif;">
|
||||
<span>{{ ((project?.total_heartbeats ?? 0)).toLocaleString() }} heartbeats</span>
|
||||
<span>{{ formatDuration(project?.total_seconds ?? 0) }}</span>
|
||||
<span v-if="project?.recent_activity_seconds && project.recent_activity_seconds > 0" class="text-[#E99682] font-medium">
|
||||
Active recently
|
||||
<span v-if="project?.most_recent_heartbeat" class="text-white/40">
|
||||
Last active: {{ formatDate(project.most_recent_heartbeat) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -153,7 +152,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Languages and Editors -->
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="(language, langIndex) in (project?.languages || []).slice(0, 3)"
|
||||
:key="`${project?.name}-lang-${langIndex}-${language}`"
|
||||
|
|
@ -170,16 +169,6 @@
|
|||
+{{ (project?.languages || []).length - 3 }} more
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Time Range -->
|
||||
<div class="text-xs text-white/50" style="font-family: 'Outfit', sans-serif;">
|
||||
<span v-if="project?.first_heartbeat">
|
||||
First: {{ formatDate(project.first_heartbeat) }}
|
||||
</span>
|
||||
<span v-if="project?.last_heartbeat" class="ml-4">
|
||||
Last: {{ formatDate(project.last_heartbeat) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -228,9 +217,9 @@
|
|||
{{ selectedProject?.name || 'Unnamed' }}
|
||||
</h2>
|
||||
<div class="flex items-center gap-4 text-white/60 flex-wrap" style="font-family: 'Outfit', sans-serif;">
|
||||
<span class="text-base">{{ ((selectedProject?.total_heartbeats ?? 0)).toLocaleString() }} heartbeats</span>
|
||||
<span v-if="selectedProject?.recent_activity_seconds && selectedProject.recent_activity_seconds > 0" class="px-2 py-1 bg-[rgba(233,150,130,0.2)] text-[#E99682] text-sm rounded-md font-medium">
|
||||
Active recently
|
||||
<span class="text-base">{{ formatDuration(selectedProject?.total_seconds ?? 0) }}</span>
|
||||
<span v-if="selectedProject?.most_recent_heartbeat" class="text-sm text-white/40">
|
||||
Last active: {{ formatDate(selectedProject.most_recent_heartbeat) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -248,25 +237,13 @@
|
|||
<!-- Modal Content -->
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-6 min-h-0">
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="bg-[rgba(42,31,43,0.5)] border-2 border-[rgba(0,0,0,0.3)] rounded-lg p-4">
|
||||
<div class="text-white/60 text-sm mb-1" style="font-family: 'Outfit', sans-serif;">Total Time</div>
|
||||
<div class="text-3xl font-bold text-white" style="font-family: 'Outfit', sans-serif;">
|
||||
{{ ((selectedProject?.total_seconds ?? 0) / 3600).toFixed(1) }}h
|
||||
</div>
|
||||
<div class="text-white/40 text-xs mt-1" style="font-family: 'Outfit', sans-serif;">
|
||||
{{ formatDuration(selectedProject?.total_seconds ?? 0) }}
|
||||
</div>
|
||||
<div class="bg-[rgba(42,31,43,0.5)] border-2 border-[rgba(0,0,0,0.3)] rounded-lg p-6">
|
||||
<div class="text-white/60 text-sm mb-1" style="font-family: 'Outfit', sans-serif;">Total Time</div>
|
||||
<div class="text-4xl font-bold text-white mb-2" style="font-family: 'Outfit', sans-serif;">
|
||||
{{ ((selectedProject?.total_seconds ?? 0) / 3600).toFixed(1) }}h
|
||||
</div>
|
||||
|
||||
<div class="bg-[rgba(42,31,43,0.5)] border-2 border-[rgba(0,0,0,0.3)] rounded-lg p-4">
|
||||
<div class="text-white/60 text-sm mb-1" style="font-family: 'Outfit', sans-serif;">Heartbeats</div>
|
||||
<div class="text-3xl font-bold text-white" style="font-family: 'Outfit', sans-serif;">
|
||||
{{ ((selectedProject?.total_heartbeats ?? 0)).toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-white/40 text-xs mt-1" style="font-family: 'Outfit', sans-serif;">
|
||||
Activity events
|
||||
</div>
|
||||
<div class="text-white/40 text-base" style="font-family: 'Outfit', sans-serif;">
|
||||
{{ formatDuration(selectedProject?.total_seconds ?? 0) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -284,39 +261,6 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editors Section -->
|
||||
<div v-if="selectedProject?.editors && selectedProject.editors.length > 0">
|
||||
<h3 class="text-white text-lg font-bold mb-3" style="font-family: 'Outfit', sans-serif;">Editors</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="(editor, editorIndex) in selectedProject.editors"
|
||||
:key="`modal-editor-${editorIndex}-${editor}`"
|
||||
class="px-3 py-2 bg-[rgba(232,133,146,0.15)] text-[#E88592] text-sm rounded-lg font-medium border-2 border-[rgba(232,133,146,0.3)]"
|
||||
style="font-family: 'Outfit', sans-serif;"
|
||||
>
|
||||
{{ editor }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repository Link -->
|
||||
<div v-if="selectedProject?.repo_url">
|
||||
<a
|
||||
:href="selectedProject.repo_url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="pushable pushable-active w-full block"
|
||||
style="font-family: 'Outfit', sans-serif;"
|
||||
>
|
||||
<span class="front w-full py-3 px-4 rounded-lg border-2 border-[rgba(0,0,0,0.35)] font-bold flex items-center justify-center gap-2" style="background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
View Repository
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -332,14 +276,8 @@ import RandomLoader from "../components/RandomLoader.vue";
|
|||
interface Project {
|
||||
name: string;
|
||||
total_seconds: number;
|
||||
total_heartbeats: number;
|
||||
languages: string[];
|
||||
editors: string[];
|
||||
first_heartbeat: string | null;
|
||||
last_heartbeat: string | null;
|
||||
repo_url: string | null;
|
||||
recent_activity_seconds: number;
|
||||
recent_activity_formatted: string;
|
||||
most_recent_heartbeat: string | null;
|
||||
}
|
||||
|
||||
interface ProjectsResponse {
|
||||
|
|
@ -368,8 +306,7 @@ const languageDropdownRef = ref<HTMLElement | null>(null);
|
|||
const sortOptions = [
|
||||
{ value: 'recent', label: 'Most Recent' },
|
||||
{ value: 'time', label: 'Most Time' },
|
||||
{ value: 'name', label: 'Name (A-Z)' },
|
||||
{ value: 'heartbeats', label: 'Most Active' }
|
||||
{ value: 'name', label: 'Name (A-Z)' }
|
||||
];
|
||||
|
||||
const itemsPerPage = ref(20);
|
||||
|
|
@ -412,10 +349,8 @@ const filteredProjects = computed(() => {
|
|||
const nameMatch = project.name?.toLowerCase().includes(query);
|
||||
const languageMatch = Array.isArray(project.languages) &&
|
||||
project.languages.some(lang => lang && typeof lang === 'string' && lang.toLowerCase().includes(query));
|
||||
const editorMatch = Array.isArray(project.editors) &&
|
||||
project.editors.some(editor => editor && typeof editor === 'string' && editor.toLowerCase().includes(query));
|
||||
|
||||
return nameMatch || languageMatch || editorMatch;
|
||||
return nameMatch || languageMatch;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -428,8 +363,8 @@ const filteredProjects = computed(() => {
|
|||
switch (sortBy.value) {
|
||||
case "recent":
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = a?.last_heartbeat ? new Date(a.last_heartbeat).getTime() : 0;
|
||||
const dateB = b?.last_heartbeat ? new Date(b.last_heartbeat).getTime() : 0;
|
||||
const dateA = a?.most_recent_heartbeat ? new Date(a.most_recent_heartbeat).getTime() : 0;
|
||||
const dateB = b?.most_recent_heartbeat ? new Date(b.most_recent_heartbeat).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
break;
|
||||
|
|
@ -439,9 +374,6 @@ const filteredProjects = computed(() => {
|
|||
case "name":
|
||||
filtered.sort((a, b) => (a?.name || '').localeCompare(b?.name || ''));
|
||||
break;
|
||||
case "heartbeats":
|
||||
filtered.sort((a, b) => (b?.total_heartbeats || 0) - (a?.total_heartbeats || 0));
|
||||
break;
|
||||
}
|
||||
|
||||
return filtered;
|
||||
|
|
@ -460,14 +392,8 @@ function normalizeProject(project: any): Project {
|
|||
return {
|
||||
name: project?.name || 'Unnamed Project',
|
||||
total_seconds: Number(project?.total_seconds) || 0,
|
||||
total_heartbeats: Number(project?.total_heartbeats) || 0,
|
||||
languages: Array.isArray(project?.languages) ? project.languages : [],
|
||||
editors: Array.isArray(project?.editors) ? project.editors : [],
|
||||
first_heartbeat: project?.first_heartbeat || null,
|
||||
last_heartbeat: project?.last_heartbeat || null,
|
||||
repo_url: project?.repo_url || null,
|
||||
recent_activity_seconds: Number(project?.recent_activity_seconds) || 0,
|
||||
recent_activity_formatted: project?.recent_activity_formatted || ''
|
||||
most_recent_heartbeat: project?.most_recent_heartbeat || null
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@
|
|||
<h4 class="font-medium text-text-primary mb-1">Auto-start</h4>
|
||||
<p class="text-sm text-text-secondary">Start with system</p>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox">
|
||||
<span class="slider"></span>
|
||||
<label class="switch" :class="{ 'opacity-50 cursor-not-allowed': isLoading }">
|
||||
<input type="checkbox" :checked="autostartEnabled" :disabled="isLoading" @change="toggleAutostart">
|
||||
<span class="slider" :class="{ 'animate-pulse': isLoading }"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
|
|
@ -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">
|
||||
<input type="checkbox" checked>
|
||||
<span class="slider"></span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -290,6 +290,8 @@ 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);
|
||||
|
|
@ -556,6 +558,39 @@ async function loadDiscordRpcState() {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadAutostartState() {
|
||||
try {
|
||||
autostartEnabled.value = await invoke("get_autostart_enabled");
|
||||
} catch (error) {
|
||||
console.error("Failed to load autostart state:", error);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const newState = !autostartEnabled.value;
|
||||
await invoke("set_autostart_enabled", { enabled: newState });
|
||||
autostartEnabled.value = newState;
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle autostart:", error);
|
||||
|
||||
autostartEnabled.value = !autostartEnabled.value;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleDiscordRpc() {
|
||||
if (isLoading.value) return;
|
||||
|
||||
|
|
@ -573,6 +608,23 @@ 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')
|
||||
}
|
||||
|
|
@ -662,6 +714,8 @@ async function downloadAndInstallUpdate() {
|
|||
|
||||
onMounted(async () => {
|
||||
loadDiscordRpcState();
|
||||
loadAutostartState();
|
||||
loadNotificationsState();
|
||||
try {
|
||||
appVersion.value = await getVersion();
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue