mirror of
https://github.com/System-End/hackatime-desktop.git
synced 2026-04-19 22:05:10 +00:00
Compare commits
22 commits
app-v1.6.1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
832a610693 | ||
|
|
472867d58f | ||
|
|
d037bdb529 | ||
|
|
6509fc85e0 | ||
|
|
e84c1d6a37 | ||
|
|
a5746811a9 | ||
|
|
3688e39424 | ||
|
|
59008e3849 | ||
|
|
145b3b9422 | ||
|
|
ece57e2981 | ||
|
|
d0c183e71c | ||
|
|
1705082522 | ||
|
|
3ec4f3386a | ||
|
|
0feffa4a50 | ||
|
|
bc8a8121b1 | ||
|
|
fe6de62c2d | ||
|
|
48146f0091 | ||
|
|
a266acc5d4 | ||
|
|
dab9a807a5 | ||
|
|
db732471b7 | ||
|
|
8680de90f1 | ||
|
|
8171e059ae |
42 changed files with 815 additions and 262 deletions
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
|
|
@ -250,7 +250,7 @@ jobs:
|
||||||
- name: Generate update manifest
|
- name: Generate update manifest
|
||||||
id: generate_manifest
|
id: generate_manifest
|
||||||
env:
|
env:
|
||||||
DOWNLOAD_URL_BASE: 'https://pub-d35fbe65a5b5426bb6d62ff02a8c7d03.r2.dev'
|
DOWNLOAD_URL_BASE: 'https://desktop.hackatime.hackclub-assets.com'
|
||||||
VERSION: '${{ steps.get_version.outputs.version }}'
|
VERSION: '${{ steps.get_version.outputs.version }}'
|
||||||
run: >
|
run: >
|
||||||
# Read signatures into variables
|
# Read signatures into variables
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"app": "0.0.0",
|
"app": "0.0.0",
|
||||||
".": "1.6.1"
|
".": "1.7.5"
|
||||||
}
|
}
|
||||||
69
CHANGELOG.md
69
CHANGELOG.md
|
|
@ -1,5 +1,74 @@
|
||||||
# Changelog
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
|
||||||
|
* resolve sentry error reports and improve data validation ([8171e05](https://github.com/hackclub/hackatime-desktop/commit/8171e059ae52aefa180c9c295ab71ba23c3e111f))
|
||||||
|
|
||||||
## [1.6.1](https://github.com/hackclub/hackatime-desktop/compare/app-v1.6.0...app-v1.6.1) (2025-10-09)
|
## [1.6.1](https://github.com/hackclub/hackatime-desktop/compare/app-v1.6.0...app-v1.6.1) (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
|
# 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)
|
[](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.
|
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",
|
"name": "desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.6.1",
|
"version": "1.7.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.18.0",
|
"packageManager": "pnpm@10.18.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/vue": "^10.18.0",
|
"@sentry/vue": "^10.18.0",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-autostart": "^2",
|
||||||
"@tauri-apps/plugin-deep-link": "^2.4.3",
|
"@tauri-apps/plugin-deep-link": "^2.4.3",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-process": "~2",
|
"@tauri-apps/plugin-process": "~2",
|
||||||
|
|
|
||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -14,6 +14,9 @@ importers:
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.8.0
|
version: 2.8.0
|
||||||
|
'@tauri-apps/plugin-autostart':
|
||||||
|
specifier: ^2
|
||||||
|
version: 2.5.0
|
||||||
'@tauri-apps/plugin-deep-link':
|
'@tauri-apps/plugin-deep-link':
|
||||||
specifier: ^2.4.3
|
specifier: ^2.4.3
|
||||||
version: 2.4.3
|
version: 2.4.3
|
||||||
|
|
@ -579,6 +582,9 @@ packages:
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-autostart@2.5.0':
|
||||||
|
resolution: {integrity: sha512-smSt0vydfVB950AeYRbO2S/c01SZrgMVg4FOrFLQLom0R0amsu/8zYaxgttriBdxcofjBZuHv4hmROBQIBVXmA==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-deep-link@2.4.3':
|
'@tauri-apps/plugin-deep-link@2.4.3':
|
||||||
resolution: {integrity: sha512-yVCZpVG1ZrtfCvE7K5LRSrGqlyPlCrqlKgoREJHnfjyYdDtUhFmZqScOXpL8XL2PizJHDsoahEweuTaUPEokPA==}
|
resolution: {integrity: sha512-yVCZpVG1ZrtfCvE7K5LRSrGqlyPlCrqlKgoREJHnfjyYdDtUhFmZqScOXpL8XL2PizJHDsoahEweuTaUPEokPA==}
|
||||||
|
|
||||||
|
|
@ -1290,6 +1296,10 @@ snapshots:
|
||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.8.4
|
'@tauri-apps/cli-win32-ia32-msvc': 2.8.4
|
||||||
'@tauri-apps/cli-win32-x64-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':
|
'@tauri-apps/plugin-deep-link@2.4.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.8.0
|
'@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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
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]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -833,6 +844,7 @@ dependencies = [
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-autostart",
|
||||||
"tauri-plugin-deep-link",
|
"tauri-plugin-deep-link",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
|
|
@ -855,13 +867,33 @@ dependencies = [
|
||||||
"subtle",
|
"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]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
dependencies = [
|
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]]
|
[[package]]
|
||||||
|
|
@ -872,7 +904,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.61.1",
|
"windows-sys 0.61.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1012,7 +1044,7 @@ dependencies = [
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 0.9.7",
|
"toml 0.9.7",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg 0.55.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3633,6 +3665,17 @@ dependencies = [
|
||||||
"bitflags 2.9.4",
|
"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]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
|
@ -4740,7 +4783,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie",
|
"cookie",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"dunce",
|
"dunce",
|
||||||
"embed_plist",
|
"embed_plist",
|
||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
|
|
@ -4791,7 +4834,7 @@ checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"glob",
|
"glob",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
|
|
@ -4863,6 +4906,20 @@ dependencies = [
|
||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-deep-link"
|
name = "tauri-plugin-deep-link"
|
||||||
version = "2.4.3"
|
version = "2.4.3"
|
||||||
|
|
@ -4938,7 +4995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
|
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
|
|
@ -5447,7 +5504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2"
|
checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"libappindicator",
|
"libappindicator",
|
||||||
"muda",
|
"muda",
|
||||||
"objc2 0.6.3",
|
"objc2 0.6.3",
|
||||||
|
|
@ -6469,6 +6526,15 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.55.0"
|
version = "0.55.0"
|
||||||
|
|
@ -6501,7 +6567,7 @@ dependencies = [
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
"cookie",
|
"cookie",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"dpi",
|
"dpi",
|
||||||
"dunce",
|
"dunce",
|
||||||
"gdkx11",
|
"gdkx11",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ tauri = { version = "2", features = ["tray-icon"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
|
tauri-plugin-autostart = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
open = "5"
|
open = "5"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"process:default"
|
"process:default",
|
||||||
|
"autostart:allow-enable",
|
||||||
|
"autostart:allow-disable",
|
||||||
|
"autostart:allow-is-enabled"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"updater: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()
|
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 database;
|
||||||
mod db_commands;
|
mod db_commands;
|
||||||
mod discord_rpc;
|
mod discord_rpc;
|
||||||
|
mod preferences;
|
||||||
mod projects;
|
mod projects;
|
||||||
mod session;
|
mod session;
|
||||||
mod setup;
|
mod setup;
|
||||||
|
|
@ -35,6 +36,11 @@ fn get_app_version(app: tauri::AppHandle) -> String {
|
||||||
app.package_info().version.to_string()
|
app.package_info().version.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_current_os() -> String {
|
||||||
|
std::env::consts::OS.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, serde::Serialize)]
|
#[derive(Clone, serde::Serialize)]
|
||||||
struct LogEntry {
|
struct LogEntry {
|
||||||
ts: i64,
|
ts: i64,
|
||||||
|
|
@ -69,14 +75,15 @@ pub fn run() {
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
||||||
push_log("info", "backend", format!("Single instance detected. Args: {:?}, CWD: {}", 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") {
|
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.show();
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
push_log("info", "backend", "Brought existing window to front".to_string());
|
push_log("info", "backend", "Brought existing window to front".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process any deep links from the new instance attempt
|
|
||||||
for arg in args {
|
for arg in args {
|
||||||
if arg.starts_with("hackatime://") {
|
if arg.starts_with("hackatime://") {
|
||||||
push_log("info", "backend", format!("Processing deep link from second instance: {}", arg));
|
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_process::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
|
@ -109,6 +117,7 @@ pub fn run() {
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
greet,
|
||||||
get_app_version,
|
get_app_version,
|
||||||
|
get_current_os,
|
||||||
get_recent_logs,
|
get_recent_logs,
|
||||||
|
|
||||||
database::get_platform_info,
|
database::get_platform_info,
|
||||||
|
|
@ -128,6 +137,14 @@ pub fn run() {
|
||||||
auth::load_auth_state,
|
auth::load_auth_state,
|
||||||
auth::clear_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_macos_linux,
|
||||||
setup::setup_hackatime_windows,
|
setup::setup_hackatime_windows,
|
||||||
setup::test_hackatime_heartbeat,
|
setup::test_hackatime_heartbeat,
|
||||||
|
|
@ -153,8 +170,6 @@ pub fn run() {
|
||||||
discord_rpc::discord_rpc_update_from_heartbeat,
|
discord_rpc::discord_rpc_update_from_heartbeat,
|
||||||
discord_rpc::discord_rpc_auto_connect,
|
discord_rpc::discord_rpc_auto_connect,
|
||||||
discord_rpc::discord_rpc_auto_disconnect,
|
discord_rpc::discord_rpc_auto_disconnect,
|
||||||
discord_rpc::get_discord_rpc_enabled,
|
|
||||||
discord_rpc::set_discord_rpc_enabled,
|
|
||||||
|
|
||||||
projects::get_projects,
|
projects::get_projects,
|
||||||
projects::get_project_details,
|
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)),
|
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"))]
|
#[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());
|
push_log("info", "backend", "🪟 Window close requested - hiding to tray".to_string());
|
||||||
api.prevent_close();
|
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();
|
let app_clone = app_handle.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
if let Some(win) = app_clone.get_webview_window("main") {
|
if let Some(win) = app_clone.get_webview_window("main") {
|
||||||
let _ = win.hide();
|
let _ = win.hide();
|
||||||
push_log("info", "backend", "✅ Window hidden to tray".to_string());
|
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(_) => {
|
WindowEvent::Resized(_) => {
|
||||||
// Handle resize events gracefully - no action needed
|
|
||||||
// This prevents potential crashes on macOS with transparent windows
|
|
||||||
}
|
}
|
||||||
WindowEvent::Moved(_) => {
|
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::auth::AuthState;
|
||||||
use crate::config::ApiConfig;
|
use crate::config::ApiConfig;
|
||||||
|
use crate::push_log;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_projects(
|
pub async fn get_projects(
|
||||||
|
|
@ -50,6 +51,16 @@ pub async fn get_projects(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to parse projects response: {}", e))?;
|
.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)
|
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
|
for line in normalized.lines() {
|
||||||
.replace("\r\n", "\n")
|
let trimmed = line.trim();
|
||||||
.lines()
|
|
||||||
.map(|line| line.trim())
|
if trimmed.starts_with("api_url") {
|
||||||
.filter(|line| !line.is_empty())
|
if let Some(value) = trimmed.split('=').nth(1) {
|
||||||
.collect::<Vec<_>>()
|
let value = value.trim();
|
||||||
.join("\n")
|
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]
|
#[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 {
|
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 {
|
} else {
|
||||||
false
|
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)
|
let config_content = fs::read_to_string(&config_path)
|
||||||
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
||||||
|
|
||||||
let lines: Vec<&str> = config_content.lines().collect();
|
if !check_config_has_required_values(&config_content, &api_key, &api_url) {
|
||||||
let mut found_api_url = false;
|
return Err("Config file is missing required api_url and api_key values".to_string());
|
||||||
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!(
|
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)
|
let config_content = fs::read_to_string(&config_path)
|
||||||
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
||||||
|
|
||||||
let lines: Vec<&str> = config_content.lines().collect();
|
if !check_config_has_required_values(&config_content, &api_key, &api_url) {
|
||||||
let mut found_api_url = false;
|
return Err("Config file is missing required api_url and api_key values".to_string());
|
||||||
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!(
|
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 let Some(window) = app.get_webview_window("main") {
|
||||||
if window.is_visible().unwrap_or(false) {
|
if window.is_visible().unwrap_or(false) {
|
||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
} else {
|
} else {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let _ = app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||||
|
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
let _ = window.set_focus();
|
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();
|
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") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ use tauri::Manager;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn show_window(app: tauri::AppHandle) -> Result<(), String> {
|
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") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
window
|
window
|
||||||
.show()
|
.show()
|
||||||
|
|
@ -20,6 +23,10 @@ pub async fn hide_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
.hide()
|
.hide()
|
||||||
.map_err(|e| format!("Failed to hide window: {}", e))?;
|
.map_err(|e| format!("Failed to hide window: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,7 +37,13 @@ pub async fn toggle_window(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
window
|
window
|
||||||
.hide()
|
.hide()
|
||||||
.map_err(|e| format!("Failed to hide window: {}", e))?;
|
.map_err(|e| format!("Failed to hide window: {}", e))?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
} else {
|
} else {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let _ = app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||||
|
|
||||||
window
|
window
|
||||||
.show()
|
.show()
|
||||||
.map_err(|e| format!("Failed to show window: {}", e))?;
|
.map_err(|e| format!("Failed to show window: {}", e))?;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Hackatime Desktop",
|
"productName": "Hackatime Desktop",
|
||||||
"version": "1.6.1",
|
"version": "1.7.5",
|
||||||
"identifier": "com.hackclub.hackatime",
|
"identifier": "com.hackclub.hackatime",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|
@ -29,8 +29,9 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"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
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
|
|
@ -63,7 +64,7 @@
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdERjg4QTFCNTJFMDk0MUQKUldRZGxPQlNHNHI0ZlRkMDN0MGI1MnllY1dUVStZalV3dVdhcTFuREx5SGtBc0txQ2xnTWs3WU4K",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdERjg4QTFCNTJFMDk0MUQKUldRZGxPQlNHNHI0ZlRkMDN0MGI1MnllY1dUVStZalV3dVdhcTFuREx5SGtBc0txQ2xnTWs3WU4K",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://pub-d35fbe65a5b5426bb6d62ff02a8c7d03.r2.dev/update-manifest.json"
|
"https://desktop.hackatime.hackclub-assets.com/update-manifest.json"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
123
src/App.vue
123
src/App.vue
|
|
@ -28,6 +28,26 @@ if (!(window as any).__hackatimeConsoleWrapped) {
|
||||||
console.info('[CONSOLE] Console wrapper initialized - logs will be captured');
|
console.info('[CONSOLE] Console wrapper initialized - logs will be captured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(window as any).__hackatimeErrorHandlerSet) {
|
||||||
|
(window as any).__hackatimeErrorHandlerSet = true;
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
console.error('[UNHANDLED REJECTION]', event.reason);
|
||||||
|
|
||||||
|
const errorMessage = event.reason?.message || String(event.reason);
|
||||||
|
if (errorMessage.includes('callbackId') ||
|
||||||
|
errorMessage.includes('IPC') ||
|
||||||
|
errorMessage.includes('Load failed')) {
|
||||||
|
event.preventDefault();
|
||||||
|
console.warn('[IPC] Suppressed IPC-related error that was already logged');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
console.error('[UNHANDLED ERROR]', event.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
is_authenticated: boolean;
|
is_authenticated: boolean;
|
||||||
access_token: string | null;
|
access_token: string | null;
|
||||||
|
|
@ -61,9 +81,11 @@ const sessionStats = ref<any>(null);
|
||||||
const presenceData = ref<any>(null);
|
const presenceData = ref<any>(null);
|
||||||
const presenceRefreshInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
const presenceRefreshInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
||||||
const presenceFetchInProgress = ref(false);
|
const presenceFetchInProgress = ref(false);
|
||||||
|
const updateCheckInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
||||||
const oauthUrl = ref<string | null>(null);
|
const oauthUrl = ref<string | null>(null);
|
||||||
const nextPresenceFetchAllowedAt = ref<number>(0);
|
const nextPresenceFetchAllowedAt = ref<number>(0);
|
||||||
const lastPresenceFetchAt = ref<number>(0);
|
const lastPresenceFetchAt = ref<number>(0);
|
||||||
|
const currentOs = ref<string | null>(null);
|
||||||
|
|
||||||
const currentPage = ref<'home' | 'projects' | 'statistics' | 'settings'>('home');
|
const currentPage = ref<'home' | 'projects' | 'statistics' | 'settings'>('home');
|
||||||
|
|
||||||
|
|
@ -77,6 +99,8 @@ const updateData = ref<any>(null);
|
||||||
const showUpdateModal = ref(false);
|
const showUpdateModal = ref(false);
|
||||||
const isInstallingUpdate = ref(false);
|
const isInstallingUpdate = ref(false);
|
||||||
const currentVersion = ref<string>('1.5.1');
|
const currentVersion = ref<string>('1.5.1');
|
||||||
|
const lastUpdateCheckTime = ref<number>(0);
|
||||||
|
const updateCheckInProgress = ref(false);
|
||||||
|
|
||||||
|
|
||||||
const weeklyChartData = computed(() => {
|
const weeklyChartData = computed(() => {
|
||||||
|
|
@ -111,6 +135,7 @@ onMounted(async () => {
|
||||||
await loadAuthState();
|
await loadAuthState();
|
||||||
await loadApiConfig();
|
await loadApiConfig();
|
||||||
await loadHackatimeInfo();
|
await loadHackatimeInfo();
|
||||||
|
await loadCurrentOs();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const appVersion = await invoke("get_app_version") as string;
|
const appVersion = await invoke("get_app_version") as string;
|
||||||
|
|
@ -119,10 +144,9 @@ onMounted(async () => {
|
||||||
console.warn("Failed to get app version:", error);
|
console.warn("Failed to get app version:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
isDevMode.value = apiConfig.value.base_url.includes('localhost') ||
|
isDevMode.value = window.location.hostname === 'localhost' ||
|
||||||
apiConfig.value.base_url.includes('127.0.0.1') ||
|
window.location.hostname === '127.0.0.1' ||
|
||||||
window.location.hostname === 'localhost' ||
|
window.location.protocol === 'http:';
|
||||||
window.location.hostname === '127.0.0.1';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const startUrls = await getCurrent();
|
const startUrls = await getCurrent();
|
||||||
|
|
@ -163,11 +187,17 @@ onMounted(async () => {
|
||||||
|
|
||||||
window.addEventListener('focus', async () => {
|
window.addEventListener('focus', async () => {
|
||||||
await loadAuthState();
|
await loadAuthState();
|
||||||
|
if (authState.value.is_authenticated) {
|
||||||
|
checkForUpdatesAndInstall();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', async () => {
|
document.addEventListener('visibilitychange', async () => {
|
||||||
if (!document.hidden) {
|
if (!document.hidden) {
|
||||||
await loadAuthState();
|
await loadAuthState();
|
||||||
|
if (authState.value.is_authenticated) {
|
||||||
|
checkForUpdatesAndInstall();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -177,10 +207,12 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
checkForUpdatesAndInstall();
|
checkForUpdatesAndInstall();
|
||||||
|
startUpdateChecks();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopPresenceRefresh();
|
stopPresenceRefresh();
|
||||||
|
stopUpdateChecks();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadAuthState() {
|
async function loadAuthState() {
|
||||||
|
|
@ -313,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() {
|
async function loadPresenceData() {
|
||||||
if (presenceFetchInProgress.value) {
|
if (presenceFetchInProgress.value) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -358,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() {
|
async function authenticate() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
@ -470,12 +530,25 @@ async function handleDirectOAuthAuth(token?: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkForUpdatesAndInstall() {
|
async function checkForUpdatesAndInstall(retryCount = 0) {
|
||||||
if (isDevMode.value) {
|
|
||||||
console.info('[AUTO-UPDATE] Skipping auto-update check in development mode');
|
if (updateCheckInProgress.value) {
|
||||||
|
console.info('[AUTO-UPDATE] Update check already in progress, skipping');
|
||||||
return;
|
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 {
|
try {
|
||||||
console.info('[AUTO-UPDATE] Checking for updates...');
|
console.info('[AUTO-UPDATE] Checking for updates...');
|
||||||
const update = await check();
|
const update = await check();
|
||||||
|
|
@ -490,6 +563,19 @@ async function checkForUpdatesAndInstall() {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AUTO-UPDATE] Auto-update check failed:', 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -554,6 +640,7 @@ async function handleInstallNow() {
|
||||||
:isLoading="isLoading"
|
:isLoading="isLoading"
|
||||||
:isDevMode="isDevMode"
|
:isDevMode="isDevMode"
|
||||||
:oauthUrl="oauthUrl"
|
:oauthUrl="oauthUrl"
|
||||||
|
:currentOs="currentOs"
|
||||||
@authenticate="authenticate"
|
@authenticate="authenticate"
|
||||||
@handleDirectOAuthAuth="handleDirectOAuthAuth"
|
@handleDirectOAuthAuth="handleDirectOAuthAuth"
|
||||||
@openOAuthUrlManually="openOAuthUrlManually"
|
@openOAuthUrlManually="openOAuthUrlManually"
|
||||||
|
|
@ -674,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 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="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="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">
|
<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;">
|
<h2 class="text-white text-[16px] font-bold italic m-0" style="font-family: 'Outfit', sans-serif;">
|
||||||
leaderboard
|
leaderboard
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex gap-2 text-[10px]" style="font-family: 'Outfit', sans-serif;">
|
<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 underline cursor-pointer">friends</span>
|
||||||
<span class="text-white cursor-pointer">global</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>
|
||||||
</div>
|
</div>
|
||||||
<!-- Leaderboard content would go here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card-3d">
|
<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="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="flex items-start space-x-4">
|
||||||
<div class="text-3xl">{{ icon }}</div>
|
<div class="text-3xl flex-shrink-0">{{ icon }}</div>
|
||||||
<div class="flex-1 flex flex-col">
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
<h3 class="text-lg font-semibold text-text-primary mb-2">{{ title }}</h3>
|
<h3 class="text-lg font-semibold text-text-primary mb-2">{{ title }}</h3>
|
||||||
<p class="text-text-secondary mb-3 flex-1">{{ description }}</p>
|
<p class="text-text-secondary mb-4 text-sm line-clamp-2">{{ description }}</p>
|
||||||
<div class="flex items-center justify-between">
|
<div class="mt-auto">
|
||||||
<div class="text-2xl font-bold" :style="{ color: color }">{{ value }}</div>
|
<div class="text-2xl font-bold mb-1" :style="{ color: color }">{{ value }}</div>
|
||||||
<div class="text-sm text-text-secondary">{{ trend }}</div>
|
<div class="text-sm text-text-secondary">{{ trend }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
50
src/main.ts
50
src/main.ts
|
|
@ -38,7 +38,55 @@ Sentry.init({
|
||||||
|
|
||||||
profilesSampleRate: __SENTRY_ENVIRONMENT__ === 'production' ? 0.1 : 1.0,
|
profilesSampleRate: __SENTRY_ENVIRONMENT__ === 'production' ? 0.1 : 1.0,
|
||||||
|
|
||||||
enableLogs: true
|
enableLogs: true,
|
||||||
|
|
||||||
|
beforeSend(event, hint) {
|
||||||
|
const error = hint.originalException as Error | undefined
|
||||||
|
const errorMessage = (error as any)?.message || error?.toString() || ''
|
||||||
|
|
||||||
|
if (
|
||||||
|
errorMessage.includes('callbackId') ||
|
||||||
|
errorMessage.includes('IPC custom protocol failed') ||
|
||||||
|
errorMessage.includes('Tauri will now use the postMessage interface') ||
|
||||||
|
errorMessage.includes('Load failed') && errorMessage.includes('localhost') ||
|
||||||
|
errorMessage.includes('ipc://localhost') ||
|
||||||
|
errorMessage.includes('project.editors.some') ||
|
||||||
|
errorMessage.includes('project.total_heartbeats.toLocaleString') ||
|
||||||
|
errorMessage.includes('el.__vnode') ||
|
||||||
|
errorMessage.includes('patchElement') && errorMessage.includes('null') ||
|
||||||
|
event.exception?.values?.some(value =>
|
||||||
|
value.value?.includes('callbackId') ||
|
||||||
|
value.value?.includes('[callbackId, data]') ||
|
||||||
|
value.value?.includes('project.editors.some') ||
|
||||||
|
value.value?.includes('project.total_heartbeats') ||
|
||||||
|
value.value?.includes('el.__vnode') ||
|
||||||
|
(value.value?.includes('patchElement') && value.value?.includes('null'))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.log('[SENTRY] Filtered out known benign error:', errorMessage)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
},
|
||||||
|
|
||||||
|
ignoreErrors: [
|
||||||
|
'IPC custom protocol failed',
|
||||||
|
'callbackId',
|
||||||
|
'Load failed',
|
||||||
|
'ipc://localhost',
|
||||||
|
'undefined is not an object (evaluating \'[callbackId, data]\')',
|
||||||
|
'undefined is not an object (evaluating \'project.editors.some\')',
|
||||||
|
'undefined is not an object (evaluating \'project.total_heartbeats.toLocaleString\')',
|
||||||
|
'null is not an object (evaluating \'el.__vnode = n2\')',
|
||||||
|
'null is not an object (evaluating \'el.__vnode\')',
|
||||||
|
/IPC custom protocol/,
|
||||||
|
/callbackId/,
|
||||||
|
/project\.editors\.some/,
|
||||||
|
/project\.total_heartbeats/,
|
||||||
|
/el\.__vnode/,
|
||||||
|
/patchElement/,
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,28 @@
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
@click="cancelAuth"
|
@click="cancelAuth"
|
||||||
class="text-white/60 text-base hover:text-white transition-colors font-medium"
|
class="text-white/60 text-base hover:text-white transition-colors font-medium"
|
||||||
|
|
@ -108,12 +130,6 @@ const emit = defineEmits<{
|
||||||
openOAuthUrlManually: [];
|
openOAuthUrlManually: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
isLoading: boolean;
|
|
||||||
isDevMode: boolean;
|
|
||||||
oauthUrl: string | null;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const authInProgress = ref(false);
|
const authInProgress = ref(false);
|
||||||
const directToken = ref('');
|
const directToken = ref('');
|
||||||
|
|
||||||
|
|
@ -138,6 +154,25 @@ function handleDirectAuth() {
|
||||||
directToken.value = '';
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -128,56 +128,45 @@
|
||||||
<div class="flex-1 overflow-y-auto min-h-0" ref="scrollContainer">
|
<div class="flex-1 overflow-y-auto min-h-0" ref="scrollContainer">
|
||||||
<div class="grid gap-4 pt-2 pb-4">
|
<div class="grid gap-4 pt-2 pb-4">
|
||||||
<div
|
<div
|
||||||
v-for="project in paginatedProjects"
|
v-for="(project, index) in paginatedProjects"
|
||||||
:key="project.name"
|
:key="`${project?.name || 'unnamed'}-${index}`"
|
||||||
class="card-3d"
|
class="card-3d"
|
||||||
@click="selectProject(project)"
|
@click="selectProject(project)"
|
||||||
>
|
>
|
||||||
<div class="rounded-[8px] border border-black p-4 card-3d-front cursor-pointer hover:bg-[#4a3a4b] transition-colors" style="background-color: #3D2C3E;">
|
<div class="rounded-[8px] border border-black p-4 card-3d-front cursor-pointer hover:bg-[#4a3a4b] transition-colors" style="background-color: #3D2C3E;">
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div class="flex-1 min-w-0">
|
<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 }}</h4>
|
<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;">
|
<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>{{ formatDuration(project.total_seconds || 0) }}</span>
|
<span v-if="project?.most_recent_heartbeat" class="text-white/40">
|
||||||
<span v-if="project.recent_activity_seconds && project.recent_activity_seconds > 0" class="text-[#E99682] font-medium">
|
Last active: {{ formatDate(project.most_recent_heartbeat) }}
|
||||||
Active recently
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right flex-shrink-0">
|
<div class="text-right flex-shrink-0">
|
||||||
<div class="text-xl font-bold text-[#E99682]" style="font-family: 'Outfit', sans-serif;">
|
<div class="text-xl font-bold text-[#E99682]" style="font-family: 'Outfit', sans-serif;">
|
||||||
{{ ((project.total_seconds || 0) / 3600).toFixed(1) }}h
|
{{ ((project?.total_seconds ?? 0) / 3600).toFixed(1) }}h
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Languages and Editors -->
|
<!-- Languages and Editors -->
|
||||||
<div class="flex flex-wrap gap-2 mb-3">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span
|
<span
|
||||||
v-for="language in (project.languages || []).slice(0, 3)"
|
v-for="(language, langIndex) in (project?.languages || []).slice(0, 3)"
|
||||||
:key="language"
|
:key="`${project?.name}-lang-${langIndex}-${language}`"
|
||||||
class="px-2 py-1 bg-[rgba(233,150,130,0.15)] text-[#E99682] text-xs rounded-md font-medium"
|
class="px-2 py-1 bg-[rgba(233,150,130,0.15)] text-[#E99682] text-xs rounded-md font-medium"
|
||||||
style="font-family: 'Outfit', sans-serif;"
|
style="font-family: 'Outfit', sans-serif;"
|
||||||
>
|
>
|
||||||
{{ language }}
|
{{ language }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="(project.languages || []).length > 3"
|
v-if="(project?.languages || []).length > 3"
|
||||||
class="px-2 py-1 bg-[rgba(50,36,51,0.15)] text-white/60 text-xs rounded-md"
|
class="px-2 py-1 bg-[rgba(50,36,51,0.15)] text-white/60 text-xs rounded-md"
|
||||||
style="font-family: 'Outfit', sans-serif;"
|
style="font-family: 'Outfit', sans-serif;"
|
||||||
>
|
>
|
||||||
+{{ (project.languages || []).length - 3 }} more
|
+{{ (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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -225,12 +214,12 @@
|
||||||
<div class="flex items-start justify-between mb-3">
|
<div class="flex items-start justify-between mb-3">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h2 class="text-3xl font-bold text-white m-0 mb-2 truncate" style="font-family: 'Outfit', sans-serif;">
|
<h2 class="text-3xl font-bold text-white m-0 mb-2 truncate" style="font-family: 'Outfit', sans-serif;">
|
||||||
{{ selectedProject.name }}
|
{{ selectedProject?.name || 'Unnamed' }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex items-center gap-4 text-white/60 flex-wrap" style="font-family: 'Outfit', sans-serif;">
|
<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 class="text-base">{{ formatDuration(selectedProject?.total_seconds ?? 0) }}</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">
|
<span v-if="selectedProject?.most_recent_heartbeat" class="text-sm text-white/40">
|
||||||
Active recently
|
Last active: {{ formatDate(selectedProject.most_recent_heartbeat) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -248,35 +237,23 @@
|
||||||
<!-- Modal Content -->
|
<!-- Modal Content -->
|
||||||
<div class="flex-1 overflow-y-auto p-6 space-y-6 min-h-0">
|
<div class="flex-1 overflow-y-auto p-6 space-y-6 min-h-0">
|
||||||
<!-- Stats Grid -->
|
<!-- 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-6">
|
||||||
<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-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;">
|
||||||
<div class="text-3xl font-bold text-white" style="font-family: 'Outfit', sans-serif;">
|
{{ ((selectedProject?.total_seconds ?? 0) / 3600).toFixed(1) }}h
|
||||||
{{ ((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>
|
</div>
|
||||||
|
<div class="text-white/40 text-base" style="font-family: 'Outfit', sans-serif;">
|
||||||
<div class="bg-[rgba(42,31,43,0.5)] border-2 border-[rgba(0,0,0,0.3)] rounded-lg p-4">
|
{{ formatDuration(selectedProject?.total_seconds ?? 0) }}
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Languages Section -->
|
<!-- Languages Section -->
|
||||||
<div v-if="selectedProject.languages && selectedProject.languages.length > 0">
|
<div v-if="selectedProject?.languages && selectedProject.languages.length > 0">
|
||||||
<h3 class="text-white text-lg font-bold mb-3" style="font-family: 'Outfit', sans-serif;">Languages</h3>
|
<h3 class="text-white text-lg font-bold mb-3" style="font-family: 'Outfit', sans-serif;">Languages</h3>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span
|
<span
|
||||||
v-for="language in selectedProject.languages"
|
v-for="(language, langIndex) in selectedProject.languages"
|
||||||
:key="language"
|
:key="`modal-lang-${langIndex}-${language}`"
|
||||||
class="px-3 py-2 bg-[rgba(233,150,130,0.15)] text-[#E99682] text-sm rounded-lg font-medium border-2 border-[rgba(233,150,130,0.3)]"
|
class="px-3 py-2 bg-[rgba(233,150,130,0.15)] text-[#E99682] text-sm rounded-lg font-medium border-2 border-[rgba(233,150,130,0.3)]"
|
||||||
style="font-family: 'Outfit', sans-serif;"
|
style="font-family: 'Outfit', sans-serif;"
|
||||||
>
|
>
|
||||||
|
|
@ -284,39 +261,6 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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 in selectedProject.editors"
|
|
||||||
:key="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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -332,14 +276,8 @@ import RandomLoader from "../components/RandomLoader.vue";
|
||||||
interface Project {
|
interface Project {
|
||||||
name: string;
|
name: string;
|
||||||
total_seconds: number;
|
total_seconds: number;
|
||||||
total_heartbeats: number;
|
|
||||||
languages: string[];
|
languages: string[];
|
||||||
editors: string[];
|
most_recent_heartbeat: string | null;
|
||||||
first_heartbeat: string | null;
|
|
||||||
last_heartbeat: string | null;
|
|
||||||
repo_url: string | null;
|
|
||||||
recent_activity_seconds: number;
|
|
||||||
recent_activity_formatted: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectsResponse {
|
interface ProjectsResponse {
|
||||||
|
|
@ -368,8 +306,7 @@ const languageDropdownRef = ref<HTMLElement | null>(null);
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{ value: 'recent', label: 'Most Recent' },
|
{ value: 'recent', label: 'Most Recent' },
|
||||||
{ value: 'time', label: 'Most Time' },
|
{ value: 'time', label: 'Most Time' },
|
||||||
{ value: 'name', label: 'Name (A-Z)' },
|
{ value: 'name', label: 'Name (A-Z)' }
|
||||||
{ value: 'heartbeats', label: 'Most Active' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const itemsPerPage = ref(20);
|
const itemsPerPage = ref(20);
|
||||||
|
|
@ -390,47 +327,52 @@ const sortByLabel = computed(() => {
|
||||||
const allLanguages = computed(() => {
|
const allLanguages = computed(() => {
|
||||||
const languages = new Set<string>();
|
const languages = new Set<string>();
|
||||||
allProjects.value.forEach(project => {
|
allProjects.value.forEach(project => {
|
||||||
if (project.languages && Array.isArray(project.languages)) {
|
if (project && project.languages && Array.isArray(project.languages)) {
|
||||||
project.languages.forEach(lang => languages.add(lang));
|
project.languages.forEach(lang => {
|
||||||
|
if (lang && typeof lang === 'string') {
|
||||||
|
languages.add(lang);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return Array.from(languages).sort();
|
return Array.from(languages).sort();
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredProjects = computed(() => {
|
const filteredProjects = computed(() => {
|
||||||
let filtered = [...allProjects.value];
|
let filtered = [...allProjects.value].filter(project => project && typeof project === 'object');
|
||||||
|
|
||||||
if (searchQuery.value.trim()) {
|
if (searchQuery.value.trim()) {
|
||||||
const query = searchQuery.value.toLowerCase();
|
const query = searchQuery.value.toLowerCase();
|
||||||
filtered = filtered.filter(project =>
|
filtered = filtered.filter(project => {
|
||||||
project.name?.toLowerCase().includes(query) ||
|
if (!project) return false;
|
||||||
(project.languages || []).some(lang => lang.toLowerCase().includes(query)) ||
|
|
||||||
(project.editors || []).some(editor => editor.toLowerCase().includes(query))
|
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));
|
||||||
|
|
||||||
|
return nameMatch || languageMatch;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterLanguage.value) {
|
if (filterLanguage.value) {
|
||||||
filtered = filtered.filter(project =>
|
filtered = filtered.filter(project =>
|
||||||
(project.languages || []).includes(filterLanguage.value)
|
project && Array.isArray(project.languages) && project.languages.includes(filterLanguage.value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (sortBy.value) {
|
switch (sortBy.value) {
|
||||||
case "recent":
|
case "recent":
|
||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
const dateA = a.last_heartbeat ? new Date(a.last_heartbeat).getTime() : 0;
|
const dateA = a?.most_recent_heartbeat ? new Date(a.most_recent_heartbeat).getTime() : 0;
|
||||||
const dateB = b.last_heartbeat ? new Date(b.last_heartbeat).getTime() : 0;
|
const dateB = b?.most_recent_heartbeat ? new Date(b.most_recent_heartbeat).getTime() : 0;
|
||||||
return dateB - dateA;
|
return dateB - dateA;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "time":
|
case "time":
|
||||||
filtered.sort((a, b) => (b.total_seconds || 0) - (a.total_seconds || 0));
|
filtered.sort((a, b) => (b?.total_seconds || 0) - (a?.total_seconds || 0));
|
||||||
break;
|
break;
|
||||||
case "name":
|
case "name":
|
||||||
filtered.sort((a, b) => (a.name || '').localeCompare(b.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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,6 +388,15 @@ const hasMoreProjects = computed(() => {
|
||||||
return paginatedProjects.value.length < filteredProjects.value.length;
|
return paginatedProjects.value.length < filteredProjects.value.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function normalizeProject(project: any): Project {
|
||||||
|
return {
|
||||||
|
name: project?.name || 'Unnamed Project',
|
||||||
|
total_seconds: Number(project?.total_seconds) || 0,
|
||||||
|
languages: Array.isArray(project?.languages) ? project.languages : [],
|
||||||
|
most_recent_heartbeat: project?.most_recent_heartbeat || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
@ -464,7 +415,9 @@ async function loadProjects() {
|
||||||
apiConfig: props.apiConfig
|
apiConfig: props.apiConfig
|
||||||
}) as ProjectsResponse;
|
}) as ProjectsResponse;
|
||||||
console.log("Projects loaded:", response);
|
console.log("Projects loaded:", response);
|
||||||
allProjects.value = response.projects || [];
|
|
||||||
|
const projects = response?.projects || [];
|
||||||
|
allProjects.value = projects.map(normalizeProject).filter(p => p.name && p.name !== 'Unnamed Project');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load projects:", err);
|
console.error("Failed to load projects:", err);
|
||||||
error.value = err instanceof Error ? err.message : String(err);
|
error.value = err instanceof Error ? err.message : String(err);
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@
|
||||||
<h4 class="font-medium text-text-primary mb-1">Auto-start</h4>
|
<h4 class="font-medium text-text-primary mb-1">Auto-start</h4>
|
||||||
<p class="text-sm text-text-secondary">Start with system</p>
|
<p class="text-sm text-text-secondary">Start with system</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="switch">
|
<label class="switch" :class="{ 'opacity-50 cursor-not-allowed': isLoading }">
|
||||||
<input type="checkbox">
|
<input type="checkbox" :checked="autostartEnabled" :disabled="isLoading" @change="toggleAutostart">
|
||||||
<span class="slider"></span>
|
<span class="slider" :class="{ 'animate-pulse': isLoading }"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|
@ -45,9 +45,9 @@
|
||||||
<h4 class="font-medium text-text-primary mb-1">Notifications</h4>
|
<h4 class="font-medium text-text-primary mb-1">Notifications</h4>
|
||||||
<p class="text-sm text-text-secondary">Show desktop notifications</p>
|
<p class="text-sm text-text-secondary">Show desktop notifications</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="switch">
|
<label class="switch" :class="{ 'opacity-50 cursor-not-allowed': isLoading }">
|
||||||
<input type="checkbox" checked>
|
<input type="checkbox" :checked="notificationsEnabled" :disabled="isLoading" @change="toggleNotifications">
|
||||||
<span class="slider"></span>
|
<span class="slider" :class="{ 'animate-pulse': isLoading }"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -290,6 +290,8 @@ const emit = defineEmits<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const discordRpcEnabled = ref(false);
|
const discordRpcEnabled = ref(false);
|
||||||
|
const autostartEnabled = ref(false);
|
||||||
|
const notificationsEnabled = ref(false);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const appVersion = ref('...');
|
const appVersion = ref('...');
|
||||||
const isClearingCache = ref(false);
|
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() {
|
async function toggleDiscordRpc() {
|
||||||
if (isLoading.value) return;
|
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() {
|
function copyApiKey() {
|
||||||
emit('copyApiKey')
|
emit('copyApiKey')
|
||||||
}
|
}
|
||||||
|
|
@ -662,6 +714,8 @@ async function downloadAndInstallUpdate() {
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadDiscordRpcState();
|
loadDiscordRpcState();
|
||||||
|
loadAutostartState();
|
||||||
|
loadNotificationsState();
|
||||||
try {
|
try {
|
||||||
appVersion.value = await getVersion();
|
appVersion.value = await getVersion();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue