UI Update
Some checks failed
Build and Release / Test (push) Has been cancelled
Build and Release / Build Windows (push) Has been cancelled
Build and Release / Build Linux (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled
Build and Release / Create Release (push) Has been cancelled
Build and Release / Development Build (push) Has been cancelled

This commit is contained in:
2025-11-07 17:34:19 -05:00
parent ec77825fa6
commit 9c56f8f3f7
60 changed files with 3304 additions and 493 deletions

293
.gitea/workflows/build.yml Normal file
View File

@@ -0,0 +1,293 @@
name: Build and Release
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: https://github.com/oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install Node dependencies
run: bun install
- name: Run type check
run: bun run check
- name: Install Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Check Rust formatting
run: cargo fmt --check --manifest-path src-tauri/Cargo.toml
- name: Run Rust clippy
run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
build-windows:
name: Build Windows
runs-on: windows-latest
needs: test
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: https://github.com/oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Install Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Build Tauri app
run: bun tauri build
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Upload Windows artifacts
uses: https://github.com/actions/upload-artifact@v4
with:
name: windows-artifacts
path: |
src-tauri/target/release/bundle/msi/*.msi
src-tauri/target/release/bundle/msi/*.msi.zip
src-tauri/target/release/bundle/msi/*.msi.zip.sig
src-tauri/target/release/bundle/nsis/*.exe
build-linux:
name: Build Linux
runs-on: ubuntu-latest
needs: test
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: https://github.com/oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Install Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev
- name: Build Tauri app
run: bun tauri build
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Upload Linux artifacts
uses: https://github.com/actions/upload-artifact@v4
with:
name: linux-artifacts
path: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz
src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz.sig
build-macos:
name: Build macOS
runs-on: macos-latest
needs: test
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: https://github.com/oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Install Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
- name: Cache Rust dependencies
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Build Tauri app (Universal)
run: bun tauri build --target universal-apple-darwin
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Upload macOS artifacts
uses: https://github.com/actions/upload-artifact@v4
with:
name: macos-artifacts
path: |
src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg
src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz
src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz.sig
release:
name: Create Release
runs-on: ubuntu-latest
needs: [build-windows, build-linux, build-macos]
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: https://github.com/actions/download-artifact@v4
with:
path: artifacts
- name: Get version from tag
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Generate release notes
id: release_notes
run: |
echo "NOTES<<EOF" >> $GITHUB_OUTPUT
git log $(git describe --tags --abbrev=0 HEAD^)..HEAD --pretty=format:"- %s" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create latest.json for updater
run: |
cat > latest.json << EOF
{
"version": "${{ steps.get_version.outputs.VERSION }}",
"notes": "${{ steps.release_notes.outputs.NOTES }}",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"windows-x86_64": {
"signature": "$(cat artifacts/windows-artifacts/*.msi.zip.sig)",
"url": "https://git.sirblob.co/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/gitea-desktop_${{ steps.get_version.outputs.VERSION }}_x64_en-US.msi.zip"
},
"linux-x86_64": {
"signature": "$(cat artifacts/linux-artifacts/*.AppImage.tar.gz.sig)",
"url": "https://git.sirblob.co/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/gitea-desktop_${{ steps.get_version.outputs.VERSION }}_amd64.AppImage.tar.gz"
},
"darwin-universal": {
"signature": "$(cat artifacts/macos-artifacts/*.app.tar.gz.sig)",
"url": "https://git.sirblob.co/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/gitea-desktop_${{ steps.get_version.outputs.VERSION }}_universal.app.tar.gz"
}
}
}
EOF
- name: Create Release
uses: https://github.com/softprops/action-gh-release@v1
with:
name: Release v${{ steps.get_version.outputs.VERSION }}
body: |
## What's Changed
${{ steps.release_notes.outputs.NOTES }}
## Downloads
- **Windows**: `.msi` installer
- **Linux**: `.deb` or `.AppImage`
- **macOS**: `.dmg` or `.app`
## Installation
Download the appropriate file for your platform and install it.
files: |
artifacts/windows-artifacts/*
artifacts/linux-artifacts/*
artifacts/macos-artifacts/*
latest.json
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
dev-build:
name: Development Build
runs-on: windows-latest
if: github.ref == 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: https://github.com/oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Install Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Build Tauri app (dev)
run: bun tauri build --debug
- name: Upload dev build
uses: https://github.com/actions/upload-artifact@v4
with:
name: dev-build-windows
path: |
src-tauri/target/debug/bundle/**/*
retention-days: 7

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
bun.lock
cargo.lock

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"svelte.svelte-vscode",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
}

View File

@@ -1,3 +0,0 @@
{
"svelte.enable-ts-plugin": true
}

253
bun.lock
View File

@@ -1,253 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "gitea-desktop",
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tauri-apps/cli": "^2",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
"vite": "^6.0.3",
},
},
},
"packages": {
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.5", "", { "os": "android", "cpu": "arm64" }, "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.5", "", { "os": "none", "cpu": "arm64" }, "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="],
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
"@sveltejs/kit": ["@sveltejs/kit@2.48.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="],
"@tauri-apps/api": ["@tauri-apps/api@2.9.0", "", {}, "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.9.3", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.3", "@tauri-apps/cli-darwin-x64": "2.9.3", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.3", "@tauri-apps/cli-linux-arm64-gnu": "2.9.3", "@tauri-apps/cli-linux-arm64-musl": "2.9.3", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.3", "@tauri-apps/cli-linux-x64-gnu": "2.9.3", "@tauri-apps/cli-linux-x64-musl": "2.9.3", "@tauri-apps/cli-win32-arm64-msvc": "2.9.3", "@tauri-apps/cli-win32-ia32-msvc": "2.9.3", "@tauri-apps/cli-win32-x64-msvc": "2.9.3" }, "bin": { "tauri": "tauri.js" } }, "sha512-BQ7iLUXTQcyG1PpzLWeVSmBCedYDpnA/6Cm/kRFGtqjTf/eVUlyYO5S2ee07tLum3nWwDBWTGFZeruO8yEukfA=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W8FQXZXQmQ0Fmj9UJXNrm2mLdIaLLriKVY7o/FzmizyIKTPIvHjfZALTNybbpTQRbJvKoGHLrW1DNzAWVDWJYg=="],
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-zDwu40rlshijt3TU6aRvzPUyVpapsx1sNfOlreDMTaMelQLHl6YoQzSRpLHYwrHrhimxyX2uDqnKIiuGel0Lhg=="],
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.9.3", "", { "os": "linux", "cpu": "arm" }, "sha512-+Oc2OfcTRwYtW93VJqd/HOk77buORwC9IToj/qsEvM7bTMq6Kda4alpZprzwrCHYANSw+zD8PgjJdljTpe4p+g=="],
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-59GqU/J1n9wFyAtleoQOaU0oVIo+kwQynEw4meFDoKRXszKGor6lTsbsS3r0QKLSPbc0o/yYGJhqqCtkYjb/eg=="],
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-fzvG+jEn5/iYGNH6Z2IRMheYFC4pJdXa19BR9fFm6Bdn2cuajRLDKdUcEME/DCtwqclphXtFZTrT4oezY5vI/A=="],
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.3", "", { "os": "linux", "cpu": "none" }, "sha512-qV8DZXI/fZwawk6T3Th1g6smiNC2KeQTk7XFgKvqZ6btC01z3UTsQmNGvI602zwm3Ld1TBZb4+rEWu2QmQimmw=="],
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tquyEONCNRfqEBWEe4eAHnxFN5yY5lFkCuD4w79XLIovUxVftQ684+xLp7zkhntkt4y20SMj2AgJa/+MOlx4Kg=="],
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-v2cBIB/6ji8DL+aiL5QUykU3ZO8OoJGyx50/qv2HQVzkf85KdaYSis3D/oVRemN/pcDz+vyCnnL3XnzFnDl4JQ=="],
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.9.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZGvBy7nvrHPbE0HeKp/ioaiw8bNgAHxWnb7JRZ4/G0A+oFj0SeSFxl9k5uU6FKnM7bHM23Gd1oeaDex9g5Fceg=="],
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.9.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-UsgIwOnpCoY9NK9/65QiwgmWVIE80LE7SwRYVblGtmlY9RYfsYvpbItwsovA/AcHMTiO+OCvS/q9yLeqS3m6Sg=="],
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-fmw7NrrHE5m49idCvJAx9T9bsupjdJ0a3p3DPCNCZRGANU6R1tA1L+KTlVuUtdAldX2NqU/9UPo2SCslYKgJHQ=="],
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"devalue": ["devalue@5.4.2", "", {}, "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"esrap": ["esrap@2.1.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"svelte": ["svelte@5.43.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-tPNp21nDWB0PSHE+VrTvEy9cFtDp2Q+ATxQoFomISEVdikZ1QZ69UqBPz/LlT+Oc8/LYS/COYwDQZrmZEUr+JQ=="],
"svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
}
}

View File

@@ -13,17 +13,28 @@
},
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2"
"@iconify/json": "^2.2.404",
"@iconify/svelte": "^5.1.0",
"@iconify/tailwind": "^1.2.0",
"@tailwindcss/vite": "^4.1.17",
"@tauri-apps/api": "^2.9.0",
"@tauri-apps/plugin-http": "^2.5.4",
"@tauri-apps/plugin-opener": "^2.5.2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.9.0",
"axios": "^1.13.2",
"tailwindcss": "^4.1.17"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
"vite": "^6.0.3",
"@tauri-apps/cli": "^2"
"@skeletonlabs/skeleton": "^4.2.4",
"@skeletonlabs/skeleton-svelte": "^4.2.4",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.48.4",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@tauri-apps/cli": "^2.9.3",
"svelte": "^5.43.4",
"svelte-check": "^4.3.3",
"typescript": "~5.6.3",
"vite": "^6.4.1"
}
}

627
src-tauri/Cargo.lock generated
View File

@@ -47,6 +47,15 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -397,9 +406,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.44"
version = "1.2.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -487,10 +496,39 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "cookie_store"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
dependencies = [
"cookie",
"document-features",
"idna",
"log",
"publicsuffix",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -514,7 +552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.10.0",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"libc",
@@ -527,7 +565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.10.0",
"core-foundation",
"core-foundation 0.10.1",
"libc",
]
@@ -646,6 +684,12 @@ dependencies = [
"syn 2.0.109",
]
[[package]]
name = "data-url"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
name = "deranged"
version = "0.5.5"
@@ -656,6 +700,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.109",
]
[[package]]
name = "derive_more"
version = "0.99.20"
@@ -688,6 +743,16 @@ dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
@@ -696,10 +761,21 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"redox_users 0.5.2",
"windows-sys 0.61.2",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users 0.4.6",
"winapi",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -750,6 +826,15 @@ dependencies = [
"syn 2.0.109",
]
[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
[[package]]
name = "dpi"
version = "0.1.2"
@@ -806,6 +891,15 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.0"
@@ -841,9 +935,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-serde"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3"
dependencies = [
"serde",
"serde_core",
@@ -906,6 +1000,18 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "filetime"
version = "0.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.60.2",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
@@ -1195,8 +1301,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -1206,9 +1314,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasip2",
"wasm-bindgen",
]
[[package]]
@@ -1247,11 +1357,15 @@ dependencies = [
name = "gitea-desktop"
version = "0.1.0"
dependencies = [
"dirs-next",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-http",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-updater",
]
[[package]]
@@ -1370,6 +1484,25 @@ dependencies = [
"syn 2.0.109",
]
[[package]]
name = "h2"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap 2.12.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1468,6 +1601,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"h2",
"http",
"http-body",
"httparse",
@@ -1479,6 +1613,23 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
name = "hyper-util"
version = "0.1.17"
@@ -1498,9 +1649,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@@ -1872,6 +2025,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.10.0",
"libc",
"redox_syscall",
]
[[package]]
@@ -1886,6 +2040,12 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -1901,6 +2061,12 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "mac"
version = "0.1.1"
@@ -1959,6 +2125,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -2294,6 +2466,18 @@ dependencies = [
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.10.0",
"objc2 0.6.3",
"objc2-app-kit",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-quartz-core"
version = "0.2.2"
@@ -2391,6 +2575,20 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2 0.6.3",
"objc2-foundation 0.3.2",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.17",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -2758,6 +2956,22 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psl-types"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "publicsuffix"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
dependencies = [
"idna",
"psl-types",
]
[[package]]
name = "quick-xml"
version = "0.38.3"
@@ -2767,6 +2981,61 @@ dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.17",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.2",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.17",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
]
[[package]]
name = "quote"
version = "1.0.42"
@@ -2807,6 +3076,16 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
@@ -2827,6 +3106,16 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
name = "rand_core"
version = "0.5.1"
@@ -2845,6 +3134,15 @@ dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
@@ -2878,6 +3176,17 @@ dependencies = [
"bitflags 2.10.0",
]
[[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"
@@ -2946,22 +3255,32 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
dependencies = [
"base64 0.22.1",
"bytes",
"cookie",
"cookie_store",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"mime",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -2971,8 +3290,29 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -2995,6 +3335,41 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -3428,6 +3803,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -3481,6 +3862,27 @@ dependencies = [
"syn 2.0.109",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.10.0",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -3502,7 +3904,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dispatch",
@@ -3545,6 +3947,17 @@ dependencies = [
"syn 2.0.109",
]
[[package]]
name = "tar"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
@@ -3682,6 +4095,52 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9"
dependencies = [
"anyhow",
"dunce",
"glob",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.17",
"toml 0.9.8",
"url",
]
[[package]]
name = "tauri-plugin-http"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00685aceab12643cf024f712ab0448ba8fcadf86f2391d49d2e5aa732aacc70"
dependencies = [
"bytes",
"cookie_store",
"data-url",
"http",
"regex",
"reqwest",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.17",
"tokio",
"url",
"urlpattern",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.2"
@@ -3704,6 +4163,48 @@ dependencies = [
"zbus",
]
[[package]]
name = "tauri-plugin-process"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.17",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
]
[[package]]
name = "tauri-runtime"
version = "2.9.1"
@@ -3909,6 +4410,21 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.48.0"
@@ -3920,9 +4436,31 @@ dependencies = [
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.109",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.17"
@@ -4212,6 +4750,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.7"
@@ -4413,6 +4957,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webkit2gtk"
version = "2.0.1"
@@ -4457,6 +5011,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-roots"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.0"
@@ -4642,6 +5205,17 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-registry"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -4687,6 +5261,15 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -5024,6 +5607,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]]
name = "yoke"
version = "0.8.1"
@@ -5149,6 +5742,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.3"
@@ -5182,6 +5781,18 @@ dependencies = [
"syn 2.0.109",
]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.12.0",
"memchr",
]
[[package]]
name = "zvariant"
version = "5.8.0"

View File

@@ -1,8 +1,8 @@
[package]
name = "gitea-desktop"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
description = "Gitea Desktop Application"
authors = ["SirBlob"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -20,6 +20,10 @@ tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-http = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dirs-next = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"

View File

@@ -5,6 +5,29 @@
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-close",
"core:window:allow-toggle-maximize",
"core:window:allow-is-maximized",
"opener:default",
"http:default",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"process:default",
"process:allow-restart",
{
"identifier": "http:allow-fetch",
"allow": [
{
"url": "https://**"
},
{
"url": "http://**"
}
]
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
src-tauri/icons/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

23
src-tauri/src/git/mod.rs Normal file
View File

@@ -0,0 +1,23 @@
pub fn git_check_installed() -> bool {
match std::process::Command::new("git").arg("--version").output() {
Ok(output) => output.status.success(),
Err(_) => false,
}
}
#[allow(dead_code)]
fn git_get_version() -> Result<String, String> {
match std::process::Command::new("git").arg("--version").output() {
Ok(output) => {
if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(version)
} else {
Err("Failed to get git version".to_string())
}
}
Err(e) => Err(e.to_string()),
}
}

View File

@@ -1,14 +1,74 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
pub mod git;
#[tauri::command]
fn greet(name: &str) -> String {
git::git_check_installed();
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[tauri::command]
fn store_token(token: String) -> Result<(), String> {
// Persist token to the app config directory. For MVP this is plaintext storage.
// Replace with OS keychain plugin (tauri-plugin-keychain) for production.
let dir = dirs_next::config_dir().ok_or_else(|| "could not determine config directory".to_string())?;
let app_dir = dir.join("gitea-desktop");
std::fs::create_dir_all(&app_dir).map_err(|e| e.to_string())?;
let token_file = app_dir.join("token");
std::fs::write(token_file, token).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
fn get_token() -> Result<String, String> {
let dir = dirs_next::config_dir().ok_or_else(|| "could not determine config directory".to_string())?;
let token_file = dir.join("gitea-desktop").join("token");
match std::fs::read_to_string(token_file) {
Ok(s) => Ok(s),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(String::new())
} else {
Err(e.to_string())
}
}
}
}
#[tauri::command]
fn store_base_url(base_url: String) -> Result<(), String> {
let dir = dirs_next::config_dir().ok_or_else(|| "could not determine config directory".to_string())?;
let app_dir = dir.join("gitea-desktop");
std::fs::create_dir_all(&app_dir).map_err(|e| e.to_string())?;
let url_file = app_dir.join("base_url");
std::fs::write(url_file, base_url).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
fn get_base_url() -> Result<String, String> {
let dir = dirs_next::config_dir().ok_or_else(|| "could not determine config directory".to_string())?;
let url_file = dir.join("gitea-desktop").join("base_url");
match std::fs::read_to_string(url_file) {
Ok(s) => Ok(s),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(String::new())
} else {
Err(e.to_string())
}
}
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.invoke_handler(tauri::generate_handler![greet, store_token, get_token, store_base_url, get_base_url])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -1,35 +1,41 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "gitea-desktop",
"version": "0.1.0",
"identifier": "co.sirblob.gitea-desktop",
"build": {
"beforeDevCommand": "bun run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "bun run build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "gitea-desktop",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
"$schema": "https://schema.tauri.app/config/2",
"productName": "gitea-desktop",
"version": "0.1.0",
"identifier": "co.sirblob.gitea-desktop",
"build": {
"beforeDevCommand": "bun run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "bun run build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "gitea-desktop",
"width": 1200,
"height": 800,
"decorations": false,
"transparent": false
}
],
"security": {
"csp": null
}
},
"plugins": {
"updater": {
"active": true,
"endpoints": [
"https://git.sirblob.co/SirBlob/Gitea-Desktop/releases/latest/download/latest.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQzNkEzQURGNEE5QUFGQ0QKUldUTnI1cEszenBxUXlWUE1INjR6RGdtSlUvSUMyaTNxZjBteDI4T0d5WWRaNm5HODFNbDN0SG4K"
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": ["icons/favicon.png", "icons/favicon.ico"]
}
}

View File

@@ -1,13 +1,13 @@
<!doctype html>
<html lang="en">
<html lang="en" data-theme="cerberus">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tauri + SvelteKit + Typescript App</title>
<title>Gitea Desktop</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<body data-sveltekit-preload-data="hover" >
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import Icon from "@iconify/svelte";
</script>
<div class="empty-state">
<div class="text-center max-w-md px-8">
<div class="mb-6 flex justify-center">
<Icon icon="simple-icons:gitea" class="text-[128px] text-[#3e4451]" />
</div>
<h3 class="text-xl font-semibold mb-2 text-white">Get started with Gitea Desktop</h3>
<p class="text-[#9399a8] mb-6">
Select a repository from the list or clone a new one to get started.
</p>
<div class="space-y-2">
<button class="w-full px-4 py-2.5 rounded bg-[#4c9ac9] hover:bg-[#5da9d6] text-white font-medium text-sm transition-colors flex items-center justify-center gap-2">
<Icon icon="mdi:download" class="text-base" />
Clone a Repository
</button>
<button class="w-full px-4 py-2.5 rounded bg-[#3e4451] hover:bg-[#4a5064] text-[#c0c6d4] font-medium text-sm transition-colors flex items-center justify-center gap-2">
<Icon icon="mdi:plus" class="text-base" />
Create New Repository
</button>
</div>
</div>
</div>
<style>
.empty-state {
display: flex;
align-items: center;
justify-content: center;
background-color: #1c2027;
height: 100%;
width: 100%;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,157 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import type { Repository } from "$lib/gitea/types";
interface Props {
repos: Repository[];
selectedRepo: Repository | null;
loading: boolean;
error: string;
onSelectRepo: (repo: Repository) => void;
}
let { repos, selectedRepo, loading, error, onSelectRepo }: Props = $props();
let searchQuery = $state("");
let filteredRepos = $derived(
searchQuery.trim()
? repos.filter(
(repo) =>
repo.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
repo.owner.login
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
repo.description?.toLowerCase().includes(searchQuery.toLowerCase())
)
: repos
);
</script>
<div class="flex-1 flex flex-col min-h-0">
<div class="p-3 border-b border-[#3e4451] shrink-0">
<div class="relative">
<Icon
icon="mdi:magnify"
class="absolute left-3 top-1/2 -translate-y-1/2 text-[#6b7280] text-base"
/>
<input
type="text"
placeholder="Find a repository..."
bind:value={searchQuery}
class="w-full pl-9 pr-3 py-2 bg-[#1c2027] border border-[#3e4451] rounded-lg text-sm text-white placeholder-[#6b7280] focus:outline-none focus:border-[#4c9ac9] focus:ring-1 focus:ring-[#4c9ac9]/50 transition-all"
/>
</div>
</div>
<div class="flex-1 overflow-y-auto p-2 min-h-0">
{#if error}
<div class="m-2">
<div
class="px-3 py-2 rounded bg-[#7d2f2f] border border-[#a04141] text-white text-xs"
>
{error}
</div>
</div>
{/if}
{#if loading && repos.length === 0}
<div class="flex flex-col items-center justify-center py-12 text-center">
<Icon
icon="mdi:loading"
class="text-2xl mb-2 animate-spin text-[#84c5fb]"
/>
<p class="text-sm text-[#9399a8]">Loading repositories...</p>
</div>
{:else if repos.length === 0}
<div
class="flex flex-col items-center justify-center py-12 text-center px-4"
>
<p class="text-sm text-[#9399a8] mb-2">No repositories found</p>
<p class="text-xs text-[#6b7280]">
Create a repository on your Gitea instance to get started
</p>
</div>
{:else if filteredRepos.length === 0}
<div
class="flex flex-col items-center justify-center py-12 text-center px-4"
>
<Icon
icon="mdi:file-search-outline"
class="text-4xl text-[#3e4451] mb-2"
/>
<p class="text-sm text-[#9399a8] mb-1">
No repositories match your search
</p>
<p class="text-xs text-[#6b7280]">Try a different search term</p>
</div>
{:else}
<div class="space-y-1">
{#each filteredRepos as repo}
<button
class={"w-full px-3 py-2.5 rounded text-left transition-colors " +
(selectedRepo?.id === repo.id
? "bg-[#4c9ac9] text-white"
: "hover:bg-[#3e4451] text-[#c0c6d4]")}
onclick={() => onSelectRepo(repo)}
>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="font-semibold truncate text-sm">{repo.name}</span>
{#if repo.private}
<span
class="px-1.5 py-0.5 rounded text-[10px] font-medium bg-[#7d2f2f] text-white flex items-center gap-1"
>
<Icon icon="mdi:lock" class="text-[10px]" />
Private
</span>
{:else}
<span
class="px-1.5 py-0.5 rounded text-[10px] font-medium bg-[#2d6a3d] text-white flex items-center gap-1"
>
<Icon icon="mdi:earth" class="text-[10px]" />
Public
</span>
{/if}
</div>
<div class="text-xs {
selectedRepo?.id === repo.id
? "text-[#d1d5db]"
: "text-[#6b7280]"
} truncate">
{repo.owner.login}
</div>
{#if repo.description}
<div class="text-xs {
selectedRepo?.id === repo.id
? "text-[#d1d5db]"
: "text-[#6b7280]"
} truncate mt-1">
{repo.description}
</div>
{/if}
</div>
</button>
{/each}
</div>
{/if}
</div>
<div class="p-3 border-t border-[#3e4451] space-y-2 bg-[#23272f] shrink-0">
<button
class="btn w-full px-4 py-2 rounded bg-[#4c9ac9] hover:bg-[#5da9d6] text-white font-medium text-sm transition-colors flex items-center justify-center gap-2"
>
<Icon icon="mdi:download" class="text-base" />
Clone Repository
</button>
<button
class="btn w-full px-4 py-2 rounded bg-[#3e4451] hover:bg-[#4a5064] text-[#c0c6d4] font-medium text-sm transition-colors flex items-center justify-center gap-2"
>
<Icon icon="mdi:plus" class="text-base" />
Create New Repository
</button>
</div>
</div>

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import type { Repository } from "$lib/gitea/types";
interface Props {
selectedRepo: Repository | null;
loading: boolean;
onRefresh: () => void;
}
let { selectedRepo, loading, onRefresh }: Props = $props();
</script>
<div class="p-3 border-b border-[#3e4451]">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-semibold text-[#9399a8] uppercase tracking-wide">Current Repository</span>
<button
class="p-1.5 rounded hover:bg-[#3e4451] text-[#9399a8] hover:text-white transition-colors disabled:opacity-50"
onclick={onRefresh}
disabled={loading}
title="Refresh repositories"
>
{#if loading}
<Icon icon="mdi:loading" class="text-sm animate-spin" />
{:else}
<Icon icon="mdi:refresh" class="text-sm" />
{/if}
</button>
</div>
{#if selectedRepo}
<button class="w-full px-3 py-2 rounded bg-[#3e4451] hover:bg-[#4a5064] text-left transition-colors flex items-center justify-between group">
<div class="flex-1 min-w-0">
<div class="font-semibold truncate text-white text-sm">{selectedRepo.name}</div>
<div class="text-xs text-[#9399a8] truncate">
{selectedRepo.owner.login}
</div>
</div>
<Icon icon="mdi:chevron-down" class="text-[#9399a8] ml-2 shrink-0" />
</button>
{:else}
<button class="w-full px-3 py-2 rounded bg-[#3e4451] hover:bg-[#4a5064] text-left transition-colors flex items-center justify-between">
<span class="text-[#9399a8] text-sm">Select a repository...</span>
<Icon icon="mdi:chevron-down" class="text-[#9399a8] ml-2" />
</button>
{/if}
</div>

View File

@@ -0,0 +1,167 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import type { Repository } from "$lib/gitea/types";
interface Props {
repo: Repository;
baseUrl: string;
}
let { repo, baseUrl }: Props = $props();
</script>
<div class="repository-view">
<div class="bg-[#2a2e3a] border-b border-[#3e4451] p-6 shrink-0">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-xl font-bold mb-2 text-white">
<span class="text-[#84c5fb]">
<a href="{baseUrl}\{repo.owner.login}" target="_blank">{repo.owner.login}</a>
</span>
<span class="text-[#6b7280] mx-2">/</span>
{repo.name}
</h2>
{#if repo.description}
<p class="text-[#9399a8] text-sm">{repo.description}</p>
{/if}
</div>
{#if repo.html_url}
<a
href={repo.html_url}
target="_blank"
class="px-4 py-2 rounded bg-[#3e4451] hover:bg-[#4a5064] text-[#c0c6d4] text-sm font-medium transition-colors flex items-center gap-2"
>
<Icon icon="mdi:open-in-new" class="text-base" />
View on Gitea
</a>
{/if}
</div>
<div class="flex gap-6 text-sm">
{#if repo.stars_count !== undefined}
<div class="flex items-center gap-2 text-[#9399a8]">
<Icon icon="mdi:star" class="text-[#fbbf24]" />
<span class="font-semibold text-white">{repo.stars_count}</span>
<span>stars</span>
</div>
{/if}
{#if repo.forks_count !== undefined}
<div class="flex items-center gap-2 text-[#9399a8]">
<Icon icon="mdi:source-fork" class="text-[#84c5fb]" />
<span class="font-semibold text-white">{repo.forks_count}</span>
<span>forks</span>
</div>
{/if}
{#if repo.open_issues_count !== undefined}
<div class="flex items-center gap-2 text-[#9399a8]">
<Icon icon="mdi:alert-circle" class="text-[#ef4444]" />
<span class="font-semibold text-white">{repo.open_issues_count}</span>
<span>issues</span>
</div>
{/if}
</div>
</div>
<div class="flex border-b border-[#3e4451] bg-[#23272f] shrink-0">
<button class="px-6 py-3 border-b-2 border-[#4c9ac9] font-semibold text-white text-sm">
Changes
</button>
<button class="px-6 py-3 text-[#9399a8] hover:text-white text-sm transition-colors">
History
</button>
<button class="px-6 py-3 text-[#9399a8] hover:text-white text-sm transition-colors">
Pull Requests
</button>
<button class="px-6 py-3 text-[#9399a8] hover:text-white text-sm transition-colors">
Actions
</button>
</div>
<div class="flex-1 overflow-y-auto p-6 min-h-0">
<div class="max-w-3xl mx-auto">
<div class="text-center py-16">
<div class="mb-6 flex justify-center">
<Icon icon="mdi:file-document-outline" class="text-[128px] text-[#3e4451]" />
</div>
<h3 class="text-xl font-semibold mb-2 text-white">No local changes</h3>
<p class="text-[#9399a8] mb-8">
There are no uncommitted changes in this repository. Here are some friendly suggestions for what to do next.
</p>
<div class="space-y-3 text-left">
<div class="rounded-lg bg-[#2a2e3a] border border-[#3e4451] p-4">
<h4 class="font-semibold mb-2 text-white">Open the repository in your external editor</h4>
<p class="text-sm text-[#9399a8] mb-3">
Select your editor in Options
</p>
<button class="px-4 py-2 rounded bg-[#4c9ac9] hover:bg-[#5da9d6] text-white font-medium text-sm transition-colors flex items-center gap-2">
<Icon icon="mdi:microsoft-visual-studio-code" class="text-base" />
Open in Visual Studio Code
</button>
</div>
<div class="rounded-lg bg-[#2a2e3a] border border-[#3e4451] p-4">
<h4 class="font-semibold mb-2 text-white">View the files of your repository in Explorer</h4>
<p class="text-sm text-[#9399a8] mb-3">
Repository menu or <kbd class="px-2 py-1 rounded bg-[#3e4451] text-xs text-[#c0c6d4] font-mono">Ctrl + Shift + F</kbd>
</p>
<button class="px-4 py-2 rounded bg-[#4c9ac9] hover:bg-[#5da9d6] text-white font-medium text-sm transition-colors flex items-center gap-2">
<Icon icon="mdi:folder-open" class="text-base" />
Show in Explorer
</button>
</div>
<div class="rounded-lg bg-[#2a2e3a] border border-[#3e4451] p-4">
<h4 class="font-semibold mb-2 text-white">Open the repository page on Gitea in your browser</h4>
<p class="text-sm text-[#9399a8] mb-3">
Repository menu or <kbd class="px-2 py-1 rounded bg-[#3e4451] text-xs text-[#c0c6d4] font-mono">Ctrl + Shift + G</kbd>
</p>
{#if repo.html_url}
<a href={repo.html_url} target="_blank" class="inline-flex px-4 py-2 rounded bg-[#4c9ac9] hover:bg-[#5da9d6] text-white font-medium text-sm transition-colors items-center gap-2">
<Icon icon="mdi:web" class="text-base" />
View on Gitea
</a>
{/if}
</div>
</div>
</div>
</div>
</div>
<div class="bg-[#2a2e3a] border-t border-[#3e4451] px-6 py-3 flex items-center justify-between shrink-0">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<span class="text-xs font-semibold text-[#9399a8]">Current branch</span>
<button class="px-3 py-1.5 rounded bg-[#3e4451] hover:bg-[#4a5064] text-[#c0c6d4] text-sm transition-colors flex items-center gap-2">
<Icon icon="mdi:source-branch" class="text-base" />
main
<Icon icon="mdi:chevron-down" class="text-sm ml-1" />
</button>
</div>
<button class="px-3 py-1.5 rounded bg-[#3e4451] hover:bg-[#4a5064] text-[#c0c6d4] text-sm transition-colors flex items-center gap-2">
<Icon icon="mdi:refresh" class="text-base" />
Fetch origin
</button>
</div>
<div class="text-xs text-[#6b7280]">
Last fetched 9 months ago
</div>
</div>
</div>
<style>
.repository-view {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
overflow: hidden;
background-color: #1c2027;
}
</style>

View File

@@ -0,0 +1,215 @@
<script lang="ts">
import { getCurrentWindow } from "@tauri-apps/api/window";
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import Icon from "@iconify/svelte";
interface Props {
baseUrl?: string;
serverVersion?: string;
onSignOut?: () => void;
showMenus?: boolean;
}
let {
baseUrl = "",
serverVersion = "",
onSignOut,
showMenus = false,
}: Props = $props();
let isMaximized = $state(false);
let updateAvailable = $state(false);
let checkingUpdate = $state(false);
const appWindow = getCurrentWindow();
async function minimize() {
await appWindow.minimize();
}
async function toggleMaximize() {
await appWindow.toggleMaximize();
isMaximized = await appWindow.isMaximized();
}
async function close() {
await appWindow.close();
}
async function checkForUpdates() {
checkingUpdate = true;
try {
const update = await check();
if (update?.available) {
updateAvailable = true;
const confirmed = confirm(
`Update to ${update.version} is available!\n\nRelease notes: ${update.body}\n\nWould you like to install it now?`
);
if (confirmed) {
await update.downloadAndInstall();
await relaunch();
}
} else {
alert("You are on the latest version!");
}
} catch (error) {
console.error("Failed to check for updates:", error);
alert(`Failed to check for updates: ${error}`);
} finally {
checkingUpdate = false;
}
}
// Check maximized state on mount
$effect(() => {
appWindow.isMaximized().then((maximized) => {
isMaximized = maximized;
});
});
</script>
<div
class="title-bar flex items-center justify-between bg-linear-to-r from-[#2a2e3a] to-[#23272f] border-b border-[#3e4451] select-none shadow-lg"
data-tauri-drag-region
>
<div class="flex items-center gap-4 px-4 py-2 h-11">
<div class="flex items-center gap-2.5">
<div
class="w-6 h-6 rounded-md flex items-center justify-center shadow-md"
>
<Icon icon="simple-icons:gitea" class="text-white text-base" />
</div>
<span class="font-bold text-base text-white tracking-tight"
>Gitea Desktop</span
>
</div>
{#if showMenus}
<div class="flex gap-1 text-xs ml-2">
<button class="menu-item">File</button>
<button class="menu-item">Edit</button>
<button class="menu-item">View</button>
<button class="menu-item">Repository</button>
<button class="menu-item">Branch</button>
<button class="menu-item">Help</button>
</div>
<div class="flex items-center gap-2 ml-4 text-xs">
<div
class="flex items-center gap-2 rounded-full bg-[#1c2027]/50 border border-[#3e4451]"
>
<Icon icon="mdi:server" class="text-[#84c5fb] mx-2.5 my-1 text-sm" />
<span class="text-[#9399a8] truncate max-w-[200px] my-1">{baseUrl}</span>
{#if serverVersion}
<div
class="px-2.5 py-1 rounded-full bg-linear-to-r from-[#4c9ac9]/20 to-[#84c5fb]/20 border border-[#4c9ac9]/30"
>
<span class="text-[#84c5fb] text-[10px] font-bold"
>v{serverVersion}</span
>
</div>
{/if}
</div>
</div>
{/if}
</div>
<div class="flex items-center h-11">
{#if showMenus && onSignOut}
<button
class="window-control hover:bg-[#3e4451] text-[#c0c6d4] hover:text-white"
onclick={onSignOut}
title="Sign Out"
>
<Icon icon="mdi:logout" class="text-base" />
</button>
{/if}
<button
class="window-control hover:bg-[#3e4451] text-[#c0c6d4] hover:text-white"
onclick={checkForUpdates}
disabled={checkingUpdate}
title="Check for updates"
>
{#if checkingUpdate}
<Icon icon="mdi:loading" class="animate-spin text-base" />
{:else if updateAvailable}
<Icon icon="mdi:download-circle" class="text-[#4c9ac9] text-base" />
{:else}
<Icon icon="mdi:update" class="text-base" />
{/if}
</button>
<div class="flex items-center border-l border-[#3e4451]/50">
<button
class="window-control hover:bg-[#3e4451] text-[#c0c6d4] hover:text-white"
onclick={minimize}
title="Minimize"
>
<Icon icon="mdi:window-minimize" class="text-base" />
</button>
<button
class="window-control hover:bg-[#3e4451] text-[#c0c6d4] hover:text-white"
onclick={toggleMaximize}
title={isMaximized ? "Restore" : "Maximize"}
>
{#if isMaximized}
<Icon icon="mdi:window-restore" class="text-base" />
{:else}
<Icon icon="mdi:window-maximize" class="text-base" />
{/if}
</button>
<button
class="window-control hover:bg-[#dc2626] hover:text-white text-[#c0c6d4]"
onclick={close}
title="Close"
>
<Icon icon="mdi:close" class="text-base" />
</button>
</div>
</div>
</div>
<style>
.title-bar {
-webkit-app-region: drag;
}
.window-control {
-webkit-app-region: no-drag;
width: 46px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
cursor: pointer;
transition: all 0.2s ease;
}
.window-control:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.menu-item {
-webkit-app-region: no-drag;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
background: transparent;
border: none;
color: #c0c6d4;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.menu-item:hover {
background: #3e4451;
color: white;
}
</style>

View File

@@ -0,0 +1,6 @@
export { default as WindowBar } from './WindowBar.svelte';
export { default as TitleBar } from './WindowBar.svelte';
export { default as RepositorySelector } from './RepositorySelector.svelte';
export { default as RepositoryList } from './RepositoryList.svelte';
export { default as RepositoryView } from './RepositoryView.svelte';
export { default as EmptyState } from './EmptyState.svelte';

5
src/lib/css/app.css Normal file
View File

@@ -0,0 +1,5 @@
@import 'tailwindcss';
@import '@skeletonlabs/skeleton';
@import '@skeletonlabs/skeleton-svelte';
@import '@skeletonlabs/skeleton/themes/cerberus';

34
src/lib/gitea/branch.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { AxiosInstance } from "axios";
import type { Branch } from "./types";
export class BranchAPI {
constructor(private client: AxiosInstance) {}
async listBranches(owner: string, repo: string): Promise<Branch[]> {
const { data } = await this.client.get<Branch[]>(`/repos/${owner}/${repo}/branches`);
return data;
}
async getBranch(owner: string, repo: string, branch: string): Promise<Branch> {
const { data } = await this.client.get<Branch>(
`/repos/${owner}/${repo}/branches/${branch}`,
);
return data;
}
async createBranch(
owner: string,
repo: string,
payload: { new_branch_name: string; old_branch_name?: string; old_ref_name?: string },
): Promise<Branch> {
const { data } = await this.client.post<Branch>(
`/repos/${owner}/${repo}/branches`,
payload,
);
return data;
}
async deleteBranch(owner: string, repo: string, branch: string): Promise<void> {
await this.client.delete(`/repos/${owner}/${repo}/branches/${branch}`);
}
}

199
src/lib/gitea/client.ts Normal file
View File

@@ -0,0 +1,199 @@
import { fetch } from "@tauri-apps/plugin-http";
export type {
User,
Repository,
Branch,
Commit,
Issue,
PullRequest,
Label,
Milestone,
Comment,
Organization,
Notification,
Tag,
Release,
FileContent,
} from "./types";
import type {
User,
Repository,
Branch,
Commit,
Issue,
PullRequest,
Label,
Milestone,
Comment,
Organization,
Notification,
Tag,
Release,
FileContent,
} from "./types";
export class GiteaClient {
private baseUrl: string;
private token?: string;
constructor(baseUrl: string, token?: string) {
this.baseUrl = baseUrl.replace(/\/$/, "");
this.token = token;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}/api/v1${endpoint}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...((options.headers as Record<string, string>) || {}),
};
if (this.token) {
headers["Authorization"] = `token ${this.token}`;
}
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`API Error (${response.status}): ${errorText || response.statusText}`
);
}
return response.json();
}
setToken(token: string) {
this.token = token;
}
clearToken() {
this.token = undefined;
}
async getCurrentUser(): Promise<User> {
return this.request<User>("/user");
}
async searchUsers(query: string, limit = 10): Promise<User[]> {
const data = await this.request<{ data: User[] }>(
`/users/search?q=${encodeURIComponent(query)}&limit=${limit}`
);
return data.data || [];
}
async getUser(username: string): Promise<User> {
return this.request<User>(`/users/${username}`);
}
async listUserRepos(): Promise<Repository[]> {
return this.request<Repository[]>("/user/repos");
}
async searchRepos(
query: string,
options?: { limit?: number; sort?: string; order?: string }
): Promise<Repository[]> {
const params = new URLSearchParams({ q: query });
if (options?.limit) params.set("limit", String(options.limit));
if (options?.sort) params.set("sort", options.sort);
if (options?.order) params.set("order", options.order);
const data = await this.request<{ data: Repository[] }>(
`/repos/search?${params}`
);
return data.data || [];
}
async getRepo(owner: string, repo: string): Promise<Repository> {
return this.request<Repository>(`/repos/${owner}/${repo}`);
}
async createRepo(payload: {
name: string;
description?: string;
private?: boolean;
auto_init?: boolean;
gitignores?: string;
license?: string;
readme?: string;
default_branch?: string;
}): Promise<Repository> {
return this.request<Repository>("/user/repos", {
method: "POST",
body: JSON.stringify(payload),
});
}
async getVersion(): Promise<{ version: string }> {
return this.request<{ version: string }>("/version");
}
async listBranches(owner: string, repo: string): Promise<Branch[]> {
return this.request<Branch[]>(`/repos/${owner}/${repo}/branches`);
}
async listCommits(
owner: string,
repo: string,
options?: { sha?: string; path?: string; page?: number; limit?: number }
): Promise<Commit[]> {
const params = new URLSearchParams();
if (options?.sha) params.set("sha", options.sha);
if (options?.path) params.set("path", options.path);
if (options?.page) params.set("page", String(options.page));
if (options?.limit) params.set("limit", String(options.limit));
const query = params.toString();
return this.request<Commit[]>(
`/repos/${owner}/${repo}/commits${query ? `?${query}` : ""}`
);
}
async listIssues(
owner: string,
repo: string,
options?: {
state?: "open" | "closed" | "all";
labels?: string;
page?: number;
limit?: number;
}
): Promise<Issue[]> {
const params = new URLSearchParams();
if (options?.state) params.set("state", options.state);
if (options?.labels) params.set("labels", options.labels);
if (options?.page) params.set("page", String(options.page));
if (options?.limit) params.set("limit", String(options.limit));
const query = params.toString();
return this.request<Issue[]>(
`/repos/${owner}/${repo}/issues${query ? `?${query}` : ""}`
);
}
async listPullRequests(
owner: string,
repo: string,
options?: { state?: "open" | "closed" | "all"; page?: number; limit?: number }
): Promise<PullRequest[]> {
const params = new URLSearchParams();
if (options?.state) params.set("state", options.state);
if (options?.page) params.set("page", String(options.page));
if (options?.limit) params.set("limit", String(options.limit));
const query = params.toString();
return this.request<PullRequest[]>(
`/repos/${owner}/${repo}/pulls${query ? `?${query}` : ""}`
);
}
}

33
src/lib/gitea/commit.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { AxiosInstance } from "axios";
import type { Commit } from "./types";
export class CommitAPI {
constructor(private client: AxiosInstance) {}
async listCommits(
owner: string,
repo: string,
options?: { sha?: string; path?: string; page?: number; limit?: number },
): Promise<Commit[]> {
const { data } = await this.client.get<Commit[]>(`/repos/${owner}/${repo}/commits`, {
params: options,
});
return data;
}
async getCommit(owner: string, repo: string, sha: string): Promise<Commit> {
const { data } = await this.client.get<Commit>(
`/repos/${owner}/${repo}/git/commits/${sha}`,
);
return data;
}
async compareCommits(
owner: string,
repo: string,
basehead: string,
): Promise<{ commits: Commit[] }> {
const { data } = await this.client.get(`/repos/${owner}/${repo}/compare/${basehead}`);
return data;
}
}

29
src/lib/gitea/file.ts Normal file
View File

@@ -0,0 +1,29 @@
import type { AxiosInstance } from "axios";
import type { FileContent } from "./types";
export class FileAPI {
constructor(private client: AxiosInstance) {}
async getContents(
owner: string,
repo: string,
filepath: string,
ref?: string,
): Promise<FileContent | FileContent[]> {
const { data } = await this.client.get<FileContent | FileContent[]>(
`/repos/${owner}/${repo}/contents/${filepath}`,
{ params: { ref } },
);
return data;
}
async getRawFile(owner: string, repo: string, filepath: string): Promise<string> {
const { data } = await this.client.get<string>(
`/repos/${owner}/${repo}/raw/${filepath}`,
{
responseType: "text",
},
);
return data;
}
}

78
src/lib/gitea/gitea.ts Normal file
View File

@@ -0,0 +1,78 @@
import axios, { type AxiosInstance } from "axios";
import { UserAPI } from "./user";
import { RepositoryAPI } from "./repository";
import { BranchAPI } from "./branch";
import { CommitAPI } from "./commit";
import { IssueAPI } from "./issue";
import { PullRequestAPI } from "./pull-request";
import { FileAPI } from "./file";
import { ReleaseAPI } from "./release";
import { OrganizationAPI } from "./organization";
import { NotificationAPI } from "./notification";
import { MiscAPI } from "./misc";
export * from "./types";
export class GiteaClient {
private client: AxiosInstance;
public baseUrl: string;
public user: UserAPI;
public repository: RepositoryAPI;
public branch: BranchAPI;
public commit: CommitAPI;
public issue: IssueAPI;
public pullRequest: PullRequestAPI;
public file: FileAPI;
public release: ReleaseAPI;
public organization: OrganizationAPI;
public notification: NotificationAPI;
public misc: MiscAPI;
constructor(baseUrl: string, token?: string) {
this.baseUrl = baseUrl.replace(/\/$/, "");
this.client = axios.create({
baseURL: `${this.baseUrl}/api/v1`,
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `token ${token}` } : {}),
},
timeout: 30000,
});
this.client.interceptors.response.use(
(response) => response,
(error) => {
const message =
error.response?.data?.message ||
error.response?.statusText ||
error.message ||
"Unknown error";
throw new Error(`API Error (${error.response?.status || "network"}): ${message}`);
},
);
this.user = new UserAPI(this.client);
this.repository = new RepositoryAPI(this.client);
this.branch = new BranchAPI(this.client);
this.commit = new CommitAPI(this.client);
this.issue = new IssueAPI(this.client);
this.pullRequest = new PullRequestAPI(this.client);
this.file = new FileAPI(this.client);
this.release = new ReleaseAPI(this.client);
this.organization = new OrganizationAPI(this.client);
this.notification = new NotificationAPI(this.client);
this.misc = new MiscAPI(this.client);
}
setToken(token: string) {
this.client.defaults.headers.common["Authorization"] = `token ${token}`;
}
clearToken() {
delete this.client.defaults.headers.common["Authorization"];
}
}
export default GiteaClient;

102
src/lib/gitea/issue.ts Normal file
View File

@@ -0,0 +1,102 @@
import type { AxiosInstance } from "axios";
import type { Issue, Comment, Label, Milestone } from "./types";
export class IssueAPI {
constructor(private client: AxiosInstance) {}
async listIssues(
owner: string,
repo: string,
options?: {
state?: "open" | "closed" | "all";
labels?: string;
page?: number;
limit?: number;
},
): Promise<Issue[]> {
const { data } = await this.client.get<Issue[]>(`/repos/${owner}/${repo}/issues`, {
params: options,
});
return data;
}
async getIssue(owner: string, repo: string, index: number): Promise<Issue> {
const { data } = await this.client.get<Issue>(`/repos/${owner}/${repo}/issues/${index}`);
return data;
}
async createIssue(
owner: string,
repo: string,
payload: {
title: string;
body?: string;
assignees?: string[];
labels?: number[];
milestone?: number;
},
): Promise<Issue> {
const { data } = await this.client.post<Issue>(
`/repos/${owner}/${repo}/issues`,
payload,
);
return data;
}
async editIssue(
owner: string,
repo: string,
index: number,
payload: Partial<{
title: string;
body: string;
state: "open" | "closed";
assignees: string[];
labels: number[];
milestone: number;
}>,
): Promise<Issue> {
const { data } = await this.client.patch<Issue>(
`/repos/${owner}/${repo}/issues/${index}`,
payload,
);
return data;
}
async listIssueComments(owner: string, repo: string, index: number): Promise<Comment[]> {
const { data } = await this.client.get<Comment[]>(
`/repos/${owner}/${repo}/issues/${index}/comments`,
);
return data;
}
async addIssueComment(
owner: string,
repo: string,
index: number,
body: string,
): Promise<Comment> {
const { data } = await this.client.post<Comment>(
`/repos/${owner}/${repo}/issues/${index}/comments`,
{ body },
);
return data;
}
async listLabels(owner: string, repo: string): Promise<Label[]> {
const { data } = await this.client.get<Label[]>(`/repos/${owner}/${repo}/labels`);
return data;
}
async listMilestones(
owner: string,
repo: string,
state?: "open" | "closed" | "all",
): Promise<Milestone[]> {
const { data } = await this.client.get<Milestone[]>(
`/repos/${owner}/${repo}/milestones`,
{ params: { state } },
);
return data;
}
}

19
src/lib/gitea/misc.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { AxiosInstance } from "axios";
export class MiscAPI {
constructor(private client: AxiosInstance) {}
async getVersion(): Promise<{ version: string }> {
const { data } = await this.client.get<{ version: string }>("/version");
return data;
}
async renderMarkdown(text: string, mode?: "gfm" | "markdown"): Promise<string> {
const { data } = await this.client.post<string>(
"/markdown",
{ Text: text, Mode: mode || "gfm" },
{ responseType: "text" },
);
return data;
}
}

View File

@@ -0,0 +1,32 @@
import type { AxiosInstance } from "axios";
import type { Notification } from "./types";
export class NotificationAPI {
constructor(private client: AxiosInstance) {}
async listNotifications(options?: {
all?: boolean;
page?: number;
limit?: number;
}): Promise<Notification[]> {
const { data } = await this.client.get<Notification[]>("/notifications", {
params: options,
});
return data;
}
async markNotificationsRead(lastReadAt?: string): Promise<void> {
await this.client.put("/notifications", {
last_read_at: lastReadAt || new Date().toISOString(),
});
}
async getNotificationThread(id: string): Promise<Notification> {
const { data } = await this.client.get<Notification>(`/notifications/threads/${id}`);
return data;
}
async markNotificationThreadRead(id: string): Promise<void> {
await this.client.patch(`/notifications/threads/${id}`);
}
}

View File

@@ -0,0 +1,26 @@
import type { AxiosInstance } from "axios";
import type { Organization, Repository, User } from "./types";
export class OrganizationAPI {
constructor(private client: AxiosInstance) {}
async listUserOrgs(): Promise<Organization[]> {
const { data } = await this.client.get<Organization[]>("/user/orgs");
return data;
}
async getOrg(org: string): Promise<Organization> {
const { data } = await this.client.get<Organization>(`/orgs/${org}`);
return data;
}
async listOrgRepos(org: string): Promise<Repository[]> {
const { data } = await this.client.get<Repository[]>(`/orgs/${org}/repos`);
return data;
}
async listOrgMembers(org: string): Promise<User[]> {
const { data } = await this.client.get<User[]>(`/orgs/${org}/members`);
return data;
}
}

View File

@@ -0,0 +1,108 @@
import type { AxiosInstance } from "axios";
import type { PullRequest, Commit, FileContent } from "./types";
export class PullRequestAPI {
constructor(private client: AxiosInstance) {}
async listPullRequests(
owner: string,
repo: string,
options?: { state?: "open" | "closed" | "all"; page?: number; limit?: number },
): Promise<PullRequest[]> {
const { data } = await this.client.get<PullRequest[]>(
`/repos/${owner}/${repo}/pulls`,
{ params: options },
);
return data;
}
async getPullRequest(owner: string, repo: string, index: number): Promise<PullRequest> {
const { data } = await this.client.get<PullRequest>(
`/repos/${owner}/${repo}/pulls/${index}`,
);
return data;
}
async createPullRequest(
owner: string,
repo: string,
payload: {
title: string;
body?: string;
head: string;
base: string;
assignees?: string[];
labels?: number[];
milestone?: number;
},
): Promise<PullRequest> {
const { data } = await this.client.post<PullRequest>(
`/repos/${owner}/${repo}/pulls`,
payload,
);
return data;
}
async editPullRequest(
owner: string,
repo: string,
index: number,
payload: Partial<{
title: string;
body: string;
state: "open" | "closed";
base: string;
assignees: string[];
labels: number[];
milestone: number;
}>,
): Promise<PullRequest> {
const { data } = await this.client.patch<PullRequest>(
`/repos/${owner}/${repo}/pulls/${index}`,
payload,
);
return data;
}
async mergePullRequest(
owner: string,
repo: string,
index: number,
method?: "merge" | "rebase" | "squash",
): Promise<void> {
await this.client.post(`/repos/${owner}/${repo}/pulls/${index}/merge`, {
Do: method || "merge",
});
}
async isPullRequestMerged(owner: string, repo: string, index: number): Promise<boolean> {
try {
await this.client.get(`/repos/${owner}/${repo}/pulls/${index}/merge`);
return true;
} catch {
return false;
}
}
async listPullRequestCommits(
owner: string,
repo: string,
index: number,
): Promise<Commit[]> {
const { data } = await this.client.get<Commit[]>(
`/repos/${owner}/${repo}/pulls/${index}/commits`,
);
return data;
}
async listPullRequestFiles(
owner: string,
repo: string,
index: number,
): Promise<FileContent[]> {
const { data } = await this.client.get<FileContent[]>(
`/repos/${owner}/${repo}/pulls/${index}/files`,
);
return data;
}
}

49
src/lib/gitea/release.ts Normal file
View File

@@ -0,0 +1,49 @@
import type { AxiosInstance } from "axios";
import type { Tag, Release } from "./types";
export class ReleaseAPI {
constructor(private client: AxiosInstance) {}
async listTags(owner: string, repo: string): Promise<Tag[]> {
const { data } = await this.client.get<Tag[]>(`/repos/${owner}/${repo}/tags`);
return data;
}
async listReleases(owner: string, repo: string): Promise<Release[]> {
const { data } = await this.client.get<Release[]>(`/repos/${owner}/${repo}/releases`);
return data;
}
async getLatestRelease(owner: string, repo: string): Promise<Release> {
const { data } = await this.client.get<Release>(
`/repos/${owner}/${repo}/releases/latest`,
);
return data;
}
async getReleaseByTag(owner: string, repo: string, tag: string): Promise<Release> {
const { data } = await this.client.get<Release>(
`/repos/${owner}/${repo}/releases/tags/${tag}`,
);
return data;
}
async createRelease(
owner: string,
repo: string,
payload: {
tag_name: string;
target_commitish?: string;
name?: string;
body?: string;
draft?: boolean;
prerelease?: boolean;
},
): Promise<Release> {
const { data } = await this.client.post<Release>(
`/repos/${owner}/${repo}/releases`,
payload,
);
return data;
}
}

102
src/lib/gitea/repository.ts Normal file
View File

@@ -0,0 +1,102 @@
import type { AxiosInstance } from "axios";
import type { Repository } from "./types";
export class RepositoryAPI {
constructor(private client: AxiosInstance) {}
async listUserRepos(): Promise<Repository[]> {
const { data } = await this.client.get<Repository[]>("/user/repos");
return data;
}
async searchRepos(
query: string,
options?: { limit?: number; sort?: string; order?: string },
): Promise<Repository[]> {
const { data } = await this.client.get<{ data: Repository[] }>("/repos/search", {
params: { q: query, ...options },
});
return data.data || [];
}
async getRepo(owner: string, repo: string): Promise<Repository> {
const { data } = await this.client.get<Repository>(`/repos/${owner}/${repo}`);
return data;
}
async createRepo(payload: {
name: string;
description?: string;
private?: boolean;
auto_init?: boolean;
gitignores?: string;
license?: string;
readme?: string;
default_branch?: string;
}): Promise<Repository> {
const { data } = await this.client.post<Repository>("/user/repos", payload);
return data;
}
async editRepo(
owner: string,
repo: string,
payload: Partial<{
name: string;
description: string;
private: boolean;
default_branch: string;
has_issues: boolean;
has_wiki: boolean;
has_pull_requests: boolean;
archived: boolean;
}>,
): Promise<Repository> {
const { data } = await this.client.patch<Repository>(
`/repos/${owner}/${repo}`,
payload,
);
return data;
}
async deleteRepo(owner: string, repo: string): Promise<void> {
await this.client.delete(`/repos/${owner}/${repo}`);
}
async forkRepo(owner: string, repo: string, organization?: string): Promise<Repository> {
const { data } = await this.client.post<Repository>(`/repos/${owner}/${repo}/forks`, {
organization,
});
return data;
}
async listForks(owner: string, repo: string): Promise<Repository[]> {
const { data } = await this.client.get<Repository[]>(`/repos/${owner}/${repo}/forks`);
return data;
}
async starRepo(owner: string, repo: string): Promise<void> {
await this.client.put(`/user/starred/${owner}/${repo}`);
}
async unstarRepo(owner: string, repo: string): Promise<void> {
await this.client.delete(`/user/starred/${owner}/${repo}`);
}
async isRepoStarred(owner: string, repo: string): Promise<boolean> {
try {
await this.client.get(`/user/starred/${owner}/${repo}`);
return true;
} catch {
return false;
}
}
async watchRepo(owner: string, repo: string): Promise<void> {
await this.client.put(`/repos/${owner}/${repo}/subscription`);
}
async unwatchRepo(owner: string, repo: string): Promise<void> {
await this.client.delete(`/repos/${owner}/${repo}/subscription`);
}
}

226
src/lib/gitea/types.ts Normal file
View File

@@ -0,0 +1,226 @@
export interface User {
id: number;
login: string;
full_name?: string;
email?: string;
avatar_url?: string;
username?: string;
is_admin?: boolean;
created?: string;
restricted?: boolean;
}
export interface Repository {
id: number;
owner: User;
name: string;
full_name: string;
description?: string;
private?: boolean;
fork?: boolean;
html_url?: string;
clone_url?: string;
ssh_url?: string;
default_branch?: string;
created_at?: string;
updated_at?: string;
stars_count?: number;
forks_count?: number;
open_issues_count?: number;
watchers_count?: number;
size?: number;
archived?: boolean;
empty?: boolean;
mirror?: boolean;
template?: boolean;
has_issues?: boolean;
has_wiki?: boolean;
has_pull_requests?: boolean;
}
export interface Branch {
name: string;
commit?: Commit;
protected?: boolean;
effective_branch_protection_name?: string;
}
export interface Commit {
id?: string;
sha?: string;
url?: string;
author?: CommitUser;
committer?: CommitUser;
message?: string;
created?: string;
html_url?: string;
tree?: {
sha?: string;
url?: string;
};
parents?: Array<{ sha?: string; url?: string }>;
}
export interface CommitUser {
name?: string;
email?: string;
date?: string;
}
export interface Issue {
id: number;
number: number;
user: User;
title: string;
body?: string;
state?: "open" | "closed";
labels?: Label[];
milestone?: Milestone;
assignees?: User[];
comments?: number;
created_at?: string;
updated_at?: string;
closed_at?: string;
due_date?: string;
html_url?: string;
pull_request?: { merged?: boolean };
}
export interface Label {
id: number;
name: string;
color: string;
description?: string;
url?: string;
}
export interface Milestone {
id: number;
title: string;
description?: string;
state?: "open" | "closed";
open_issues?: number;
closed_issues?: number;
due_on?: string;
created_at?: string;
updated_at?: string;
closed_at?: string;
}
export interface PullRequest {
id: number;
number: number;
user: User;
title: string;
body?: string;
state?: "open" | "closed";
head?: PRBranchInfo;
base?: PRBranchInfo;
mergeable?: boolean;
merged?: boolean;
merged_at?: string;
merge_commit_sha?: string;
labels?: Label[];
milestone?: Milestone;
assignees?: User[];
comments?: number;
created_at?: string;
updated_at?: string;
closed_at?: string;
html_url?: string;
diff_url?: string;
patch_url?: string;
}
export interface PRBranchInfo {
label?: string;
ref?: string;
sha?: string;
repo?: Repository;
}
export interface Comment {
id: number;
user: User;
body?: string;
created_at?: string;
updated_at?: string;
html_url?: string;
}
export interface Organization {
id: number;
username: string;
full_name?: string;
avatar_url?: string;
description?: string;
website?: string;
location?: string;
visibility?: string;
repo_admin_change_team_access?: boolean;
}
export interface Notification {
id: number;
unread?: boolean;
pinned?: boolean;
subject?: {
title?: string;
url?: string;
type?: string;
state?: string;
};
repository?: Repository;
updated_at?: string;
url?: string;
}
export interface Tag {
name: string;
commit?: {
sha?: string;
url?: string;
};
zipball_url?: string;
tarball_url?: string;
}
export interface Release {
id: number;
tag_name: string;
target_commitish?: string;
name?: string;
body?: string;
draft?: boolean;
prerelease?: boolean;
created_at?: string;
published_at?: string;
author?: User;
assets?: ReleaseAsset[];
html_url?: string;
tarball_url?: string;
zipball_url?: string;
}
export interface ReleaseAsset {
id: number;
name: string;
size?: number;
download_count?: number;
created_at?: string;
browser_download_url?: string;
}
export interface FileContent {
type?: "file" | "dir" | "symlink" | "submodule";
encoding?: string;
size?: number;
name?: string;
path?: string;
content?: string;
sha?: string;
url?: string;
git_url?: string;
html_url?: string;
download_url?: string;
}

41
src/lib/gitea/user.ts Normal file
View File

@@ -0,0 +1,41 @@
import type { AxiosInstance } from "axios";
import type { User } from "./types";
export class UserAPI {
constructor(private client: AxiosInstance) {}
async getCurrentUser(): Promise<User> {
const { data } = await this.client.get<User>("/user");
return data;
}
async searchUsers(query: string, limit = 10): Promise<User[]> {
const { data } = await this.client.get<{ data: User[] }>("/users/search", {
params: { q: query, limit },
});
return data.data || [];
}
async getUser(username: string): Promise<User> {
const { data } = await this.client.get<User>(`/users/${username}`);
return data;
}
async getUserFollowers(): Promise<User[]> {
const { data } = await this.client.get<User[]>("/user/followers");
return data;
}
async getUserFollowing(): Promise<User[]> {
const { data } = await this.client.get<User[]>("/user/following");
return data;
}
async followUser(username: string): Promise<void> {
await this.client.put(`/user/following/${username}`);
}
async unfollowUser(username: string): Promise<void> {
await this.client.delete(`/user/following/${username}`);
}
}

14
src/lib/stores/auth.ts Normal file
View File

@@ -0,0 +1,14 @@
import { writable } from 'svelte/store';
export type AuthState = {
token?: string;
baseUrl?: string;
};
export const auth = writable<AuthState>({});
export function setAuth(token?: string, baseUrl?: string) {
auth.set({ token, baseUrl });
}
export default auth;

View File

@@ -0,0 +1,15 @@
import { writable } from 'svelte/store';
export interface TitleBarState {
baseUrl: string;
serverVersion: string;
showMenus: boolean;
onSignOut?: () => void;
}
export const titleBarStore = writable<TitleBarState>({
baseUrl: "",
serverVersion: "",
showMenus: false,
onSignOut: undefined
});

37
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,37 @@
<script lang="ts">
import "$lib/css/app.css";
import WindowBar from "$lib/components/WindowBar.svelte";
import { titleBarStore } from "$lib/stores/titleBar";
interface Props {
children: any;
}
let { children }: Props = $props();
</script>
<div class="app-container">
<WindowBar
baseUrl={$titleBarStore.baseUrl}
serverVersion={$titleBarStore.serverVersion}
showMenus={$titleBarStore.showMenus}
onSignOut={$titleBarStore.onSignOut}
/>
{@render children()}
</div>
<style>
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
:global(.app-container > :last-child) {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -1,5 +1,3 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we use adapter-static with a fallback to index.html to put the site in SPA mode
// See: https://svelte.dev/docs/kit/single-page-apps
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const ssr = false;

View File

@@ -1,156 +1,203 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { invoke } from "@tauri-apps/api/core";
import { GiteaClient } from "$lib/gitea/client";
import type { Repository } from "$lib/gitea/types";
import { setAuth } from "$lib/stores/auth";
import { titleBarStore } from "$lib/stores/titleBar";
import Icon from "@iconify/svelte";
import {
RepositorySelector,
RepositoryList,
RepositoryView,
EmptyState
} from "$lib/components";
let name = $state("");
let greetMsg = $state("");
let baseUrl = $state("");
let token = $state("");
let loading = $state(false);
let error = $state("");
let isSignedIn = $state(false);
let repos: Repository[] = $state([]);
let serverVersion = $state("");
let checkingAuth = $state(true);
let selectedRepo: Repository | null = $state(null);
// Update title bar store reactively
$effect(() => {
titleBarStore.set({
baseUrl,
serverVersion,
showMenus: isSignedIn,
onSignOut: handleSignOut
});
});
async function greet(event: Event) {
event.preventDefault();
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
greetMsg = await invoke("greet", { name });
}
async function handleSignIn(e: Event) {
e.preventDefault();
error = "";
if (!baseUrl.trim()) {
error = "Please enter your Gitea server URL";
return;
}
if (!token.trim()) {
error = "Please enter your personal access token";
return;
}
loading = true;
try {
const normalizedUrl = baseUrl.trim().replace(/\/$/, "");
const client = new GiteaClient(normalizedUrl, token.trim());
const user = await client.getCurrentUser();
const version = await client.getVersion();
serverVersion = version.version;
await invoke("store_token", { token: token.trim() });
await invoke("store_base_url", { baseUrl: normalizedUrl });
setAuth(token.trim(), normalizedUrl);
isSignedIn = true;
await loadRepositories(client);
} catch (e) {
error = `Failed to sign in: ${String(e)}`;
isSignedIn = false;
} finally {
loading = false;
}
}
async function loadRepositories(client?: GiteaClient) {
if (!client && (!baseUrl || !token)) return;
loading = true;
error = "";
try {
const giteaClient = client || new GiteaClient(baseUrl, token);
repos = await giteaClient.listUserRepos();
} catch (e) {
error = `Failed to load repositories: ${String(e)}`;
repos = [];
} finally {
loading = false;
}
}
async function handleSignOut() {
try {
await invoke("store_token", { token: "" });
await invoke("store_base_url", { baseUrl: "" });
token = "";
baseUrl = "";
isSignedIn = false;
repos = [];
serverVersion = "";
error = "";
goto("/oauth");
} catch (e) {
error = `Failed to sign out: ${String(e)}`;
}
}
onMount(async () => {
try {
const existingToken: string = await invoke("get_token");
const existingBaseUrl: string = await invoke("get_base_url");
if (!existingToken || !existingBaseUrl) {
checkingAuth = false;
goto("/oauth");
return;
}
token = existingToken;
baseUrl = existingBaseUrl;
setAuth(token, baseUrl);
const client = new GiteaClient(baseUrl, token);
await client.getCurrentUser();
const version = await client.getVersion();
serverVersion = version.version;
isSignedIn = true;
await loadRepositories(client);
checkingAuth = false;
} catch (e) {
console.error("Failed to check auth:", e);
isSignedIn = false;
checkingAuth = false;
goto("/oauth");
}
});
</script>
<main class="container">
<h1>Welcome to Tauri + Svelte</h1>
{#if checkingAuth}
<div class="flex items-center justify-center flex-1 bg-[#1c2027]">
<div class="flex flex-col items-center gap-4">
<Icon icon="mdi:loading" class="text-4xl animate-spin text-[#4c9ac9]" />
<p class="text-[#9399a8]">Checking authentication...</p>
</div>
</div>
{:else if isSignedIn}
<div class="main-container">
<!-- Left Sidebar -->
<aside class="sidebar">
<RepositorySelector
{selectedRepo}
{loading}
onRefresh={() => loadRepositories()}
/>
<RepositoryList
{repos}
{selectedRepo}
{loading}
{error}
onSelectRepo={(repo: Repository) => selectedRepo = repo}
/>
</aside>
<div class="row">
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo vite" alt="Vite Logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" class="logo tauri" alt="Tauri Logo" />
</a>
<a href="https://svelte.dev" target="_blank">
<img src="/svelte.svg" class="logo svelte-kit" alt="SvelteKit Logo" />
</a>
</div>
<p>Click on the Tauri, Vite, and SvelteKit logos to learn more.</p>
<form class="row" onsubmit={greet}>
<input id="greet-input" placeholder="Enter a name..." bind:value={name} />
<button type="submit">Greet</button>
</form>
<p>{greetMsg}</p>
</main>
<!-- Main Content -->
<main class="main-content">
{#if selectedRepo}
<RepositoryView baseUrl={baseUrl} repo={selectedRepo} />
{:else}
<EmptyState />
{/if}
</main>
</div>
{/if}
<style>
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.main-container {
display: flex;
flex-direction: row;
flex: 1;
overflow: hidden;
background-color: #1c2027;
height: 100%;
width: 100%;
}
.logo.svelte-kit:hover {
filter: drop-shadow(0 0 2em #ff3e00);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}
.sidebar {
width: 320px;
background-color: #23272f;
border-right: 1px solid #3e4451;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
height: 100%;
}
.main-content {
flex: 1;
overflow: hidden;
min-width: 0;
height: 100%;
width: 100%;
}
</style>

View File

@@ -0,0 +1,217 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { invoke } from "@tauri-apps/api/core";
import { GiteaClient } from "$lib/gitea/client";
import { setAuth } from "$lib/stores/auth";
import { titleBarStore } from "$lib/stores/titleBar";
import Icon from "@iconify/svelte";
import { onMount } from "svelte";
let baseUrl = $state("");
let loading = $state(false);
let error = $state("");
let step = $state<"url" | "token">("url");
let token = $state("");
// Hide menu items on OAuth page
onMount(() => {
titleBarStore.set({
baseUrl: "",
serverVersion: "",
showMenus: false,
onSignOut: undefined
});
});
async function handleServerSubmit(e: Event) {
e.preventDefault();
error = "";
if (!baseUrl.trim()) {
error = "Please enter your Gitea server URL";
return;
}
step = "token";
}
async function handleTokenSubmit(e: Event) {
e.preventDefault();
error = "";
if (!token.trim()) {
error = "Please enter your personal access token";
return;
}
loading = true;
try {
const normalizedUrl = baseUrl.trim().replace(/\/$/, "");
const client = new GiteaClient(normalizedUrl, token.trim());
await client.getCurrentUser();
await invoke("store_token", { token: token.trim() });
await invoke("store_base_url", { baseUrl: normalizedUrl });
setAuth(token.trim(), normalizedUrl);
goto("/");
} catch (e) {
error = `Failed to authenticate: ${String(e)}`;
} finally {
loading = false;
}
}
function goBack() {
if (step === "token") {
step = "url";
error = "";
}
}
</script>
<div class="min-h-screen flex items-center justify-center p-4 md:p-8 bg-[#1c2027]">
<div class="bg-[#2a2e3a] border border-[#3e4451] rounded-xl p-8 md:p-12 max-w-2xl w-full shadow-2xl">
<!-- Header with Icon -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-xl bg-linear-to-br from-[#4c9ac9] to-[#3a7ea5] mb-4 shadow-lg">
<Icon icon="mdi:git" class="text-3xl text-white" />
</div>
<h1 class="text-3xl font-bold text-white mb-2">Authentication Required</h1>
<p class="text-[#9399a8] text-sm">
{#if step === "url"}
Enter your Gitea instance URL to continue
{:else}
Generate a personal access token to authenticate
{/if}
</p>
</div>
{#if step === "url"}
<form class="space-y-6" onsubmit={handleServerSubmit}>
<div class="space-y-2">
<label class="block">
<span class="text-sm font-semibold text-white mb-2 flex items-center gap-2">
<Icon icon="mdi:server" class="text-[#4c9ac9]" />
Gitea Server URL
</span>
<input
class="w-full px-4 py-3 bg-[#1c2027] border border-[#3e4451] rounded-lg text-white placeholder-[#6b7280] focus:outline-none focus:border-[#4c9ac9] focus:ring-2 focus:ring-[#4c9ac9]/30 transition-all disabled:opacity-50"
type="url"
bind:value={baseUrl}
placeholder="https://gitea.example.com"
disabled={loading}
required
/>
<span class="text-xs text-[#6b7280] mt-1.5 block">
Enter the URL of your Gitea instance (e.g., https://git.gitea.com)
</span>
</label>
</div>
{#if error}
<div class="px-4 py-3 rounded-lg bg-[#7d2f2f]/20 border border-[#a04141] text-[#ff6b6b] flex items-start gap-3">
<Icon icon="mdi:alert-circle" class="text-xl shrink-0 mt-0.5" />
<p class="text-sm">{error}</p>
</div>
{/if}
<button
type="submit"
class="w-full px-4 py-3 rounded-lg bg-linear-to-r from-[#4c9ac9] to-[#5da9d6] hover:from-[#5da9d6] hover:to-[#6eb8e3] text-white font-semibold transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg hover:shadow-xl flex items-center justify-center gap-2"
disabled={loading}
>
{#if loading}
<Icon icon="mdi:loading" class="animate-spin" />
Connecting...
{:else}
Continue
<Icon icon="mdi:arrow-right" />
{/if}
</button>
</form>
{:else}
<!-- Token Instructions Card -->
<div class="bg-[#1c2027] border border-[#3e4451] rounded-lg p-6 mb-6">
<h3 class="text-lg font-bold text-white mb-4 flex items-center gap-2">
<Icon icon="mdi:information" class="text-[#4c9ac9]" />
How to generate a token:
</h3>
<ol class="list-decimal list-inside space-y-3 text-sm text-[#c0c6d4]">
<li class="pl-2">
Go to <span class="px-2 py-0.5 rounded bg-[#3e4451] text-[#84c5fb] font-mono text-xs">{baseUrl}</span>
</li>
<li class="pl-2">Navigate to <strong class="text-white">Settings → Applications</strong></li>
<li class="pl-2">Click <strong class="text-white">Generate New Token</strong></li>
<li class="pl-2">Give it a name (e.g., "Gitea Desktop")</li>
<li class="pl-2">
Select scopes:
<span class="inline-flex gap-2 flex-wrap mt-1">
<code class="px-2 py-0.5 rounded bg-[#3e4451] text-[#84c5fb] font-mono text-xs">read:user</code>
<code class="px-2 py-0.5 rounded bg-[#3e4451] text-[#84c5fb] font-mono text-xs">read:repository</code>
<code class="px-2 py-0.5 rounded bg-[#3e4451] text-[#84c5fb] font-mono text-xs">write:repository</code>
</span>
</li>
<li class="pl-2">Click <strong class="text-white">Generate Token</strong></li>
<li class="pl-2">Copy the token and paste it below</li>
</ol>
</div>
<form class="space-y-6" onsubmit={handleTokenSubmit}>
<div class="space-y-2">
<label class="block">
<span class="text-sm font-semibold text-white mb-2 flex items-center gap-2">
<Icon icon="mdi:key" class="text-[#4c9ac9]" />
Personal Access Token
</span>
<input
class="w-full px-4 py-3 bg-[#1c2027] border border-[#3e4451] rounded-lg text-white placeholder-[#6b7280] focus:outline-none focus:border-[#4c9ac9] focus:ring-2 focus:ring-[#4c9ac9]/30 transition-all disabled:opacity-50 font-mono"
type="password"
bind:value={token}
placeholder="Paste your token here"
disabled={loading}
required
/>
<span class="text-xs text-[#6b7280] mt-1.5 flex items-center gap-1.5">
<Icon icon="mdi:lock" class="text-sm" />
This token will be stored securely on your device
</span>
</label>
</div>
{#if error}
<div class="px-4 py-3 rounded-lg bg-[#7d2f2f]/20 border border-[#a04141] text-[#ff6b6b] flex items-start gap-3">
<Icon icon="mdi:alert-circle" class="text-xl shrink-0 mt-0.5" />
<p class="text-sm">{error}</p>
</div>
{/if}
<div class="flex flex-col-reverse md:flex-row gap-3">
<button
type="button"
class="px-4 py-2.5 rounded-lg bg-[#3e4451] hover:bg-[#4a5064] text-[#c0c6d4] font-medium transition-all flex items-center justify-center gap-2"
onclick={goBack}
disabled={loading}
>
<Icon icon="mdi:arrow-left" />
Back
</button>
<button
type="submit"
class="flex-1 px-4 py-2.5 rounded-lg bg-linear-to-r from-[#2d6a3d] to-[#3a7d4c] hover:from-[#3a7d4c] hover:to-[#47905b] text-white font-semibold transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg hover:shadow-xl flex items-center justify-center gap-2"
disabled={loading}
>
{#if loading}
<Icon icon="mdi:loading" class="animate-spin text-lg" />
Authenticating...
{:else}
<Icon icon="mdi:login" />
Sign In
{/if}
</button>
</div>
</form>
{/if}
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,32 +1,33 @@
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/
export default defineConfig(async () => ({
plugins: [sveltekit()],
plugins: [sveltekit(), tailwindcss()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent Vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell Vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent Vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell Vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));