Compare commits
12
Commits
2026.7.19
...
ff00925b13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff00925b13 | ||
|
|
982dbf136d | ||
|
|
646dcc341e | ||
|
|
5f3e55dcd7 | ||
|
|
fdbff8175e | ||
|
|
a6e7e991dd | ||
|
|
3491b8efb3 | ||
|
|
59a8f34e0e | ||
|
|
286b106baa | ||
|
|
670e7e24fe | ||
|
|
6729d61dca | ||
|
|
b181b384e7 |
@@ -104,7 +104,19 @@
|
||||
"Read(//home/christoph/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.11.5/src/**)",
|
||||
"Read(//home/christoph/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-wry-2.11.4/src/**)",
|
||||
"Read(//home/christoph/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.11.5/src/window/**)",
|
||||
"WebSearch"
|
||||
"WebSearch",
|
||||
"Bash(node -p \"require\\('./package.json'\\).version\")",
|
||||
"Bash(bash -n PKGBUILD)",
|
||||
"Bash(makepkg --printsrcinfo)",
|
||||
"Bash(npx tauri *)",
|
||||
"Bash(echo \"---exit code:$?---\")",
|
||||
"Bash(git -C /mnt/data/Development/GitLite status --short)",
|
||||
"Bash(makepkg -f --noconfirm)",
|
||||
"Bash(tar -tf gitty-2026.7.19-1-x86_64.pkg.tar.zst)",
|
||||
"Bash(cp /mnt/data/Development/GitLite/PKGBUILD .)",
|
||||
"Bash(sed -i 's/^pkgrel=1/pkgrel=3/' PKGBUILD)",
|
||||
"Bash(node -e \"const fs=require\\('fs'\\); const version='2026.8.1'; const pkgbuild=fs.readFileSync\\('PKGBUILD','utf8'\\).replace\\(/^pkgver=.*\\\\$/m, 'pkgver='+version\\).replace\\(/^pkgrel=.*\\\\$/m, 'pkgrel=1'\\); fs.writeFileSync\\('PKGBUILD', pkgbuild\\);\")",
|
||||
"Bash(rm PKGBUILD)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ jobs:
|
||||
run: |
|
||||
node -e "const fs=require('fs'); const path='src-tauri/tauri.conf.json'; const config=JSON.parse(fs.readFileSync(path,'utf8')); config.version=process.env.RELEASE_VERSION; fs.writeFileSync(path, JSON.stringify(config,null,2)+'\n');"
|
||||
|
||||
- name: Update PKGBUILD Version
|
||||
run: |
|
||||
node -e "const fs=require('fs'); const version=require('./package.json').version; const pkgbuild=fs.readFileSync('PKGBUILD','utf8').replace(/^pkgver=.*$/m, 'pkgver='+version).replace(/^pkgrel=.*$/m, 'pkgrel=1'); fs.writeFileSync('PKGBUILD', pkgbuild);"
|
||||
|
||||
- name: Commit and Push Changes
|
||||
if: matrix.platform == 'windows-latest'
|
||||
shell: powershell
|
||||
@@ -95,7 +99,7 @@ jobs:
|
||||
git config user.email '${{ vars.EMAIL_GIT }}'
|
||||
# npm version also bumps the version inside package-lock.json, so stage it
|
||||
# too -- otherwise it stays as an unstaged change and blocks the rebase.
|
||||
git add package.json package-lock.json src-tauri/tauri.conf.json
|
||||
git add package.json package-lock.json src-tauri/tauri.conf.json PKGBUILD
|
||||
|
||||
git commit -m 'Update version to ${{ github.ref_name }}'
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
@@ -7,3 +7,6 @@
|
||||
~
|
||||
.codex*
|
||||
target
|
||||
|
||||
*.pkg.tar.zst
|
||||
pkg
|
||||
@@ -0,0 +1,66 @@
|
||||
# Maintainer: Christoph Brandau <c.brandau91@googlemail.com>
|
||||
#
|
||||
# Local/in-tree PKGBUILD: builds straight from this working directory (no
|
||||
# source download, no tauri-bundler/AppImage step) and installs the raw
|
||||
# binary + desktop entry. Run `makepkg -si` from the repo root.
|
||||
|
||||
pkgname=gitty
|
||||
pkgver=2026.7.19
|
||||
pkgrel=1
|
||||
pkgdesc="A lightweight, modern Git client built with Tauri"
|
||||
arch=('x86_64')
|
||||
url="https://git.cbsk-tech.de/Christoph/GitLite"
|
||||
license=('MIT')
|
||||
depends=('webkit2gtk-4.1' 'gtk3' 'git' 'hicolor-icon-theme')
|
||||
makedepends=('rust' 'nodejs' 'npm')
|
||||
options=('!lto')
|
||||
|
||||
source=()
|
||||
sha256sums=()
|
||||
|
||||
# Keeps pkgver in sync with package.json (the release CI bumps that file).
|
||||
pkgver() {
|
||||
cd "$startdir"
|
||||
node -p "require('./package.json').version"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$startdir"
|
||||
npm ci
|
||||
# Build through the Tauri CLI (not a raw `cargo build --release`): it enables
|
||||
# the `custom-protocol` cargo feature that makes the binary load the embedded
|
||||
# `dist/` assets instead of the Vite dev server URL (127.0.0.1:1420), and runs
|
||||
# `beforeBuildCommand` (`npm run build`) for us. `--no-bundle` skips deb/rpm/
|
||||
# appimage packaging (and with it linuxdeploy/FUSE) since pacman owns that here.
|
||||
npm run tauri -- build --no-bundle
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$startdir"
|
||||
|
||||
install -Dm755 "src-tauri/target/release/gitty" "$pkgdir/usr/bin/gitty"
|
||||
|
||||
install -Dm644 "src-tauri/icons/32x32.png" \
|
||||
"$pkgdir/usr/share/icons/hicolor/32x32/apps/gitty.png"
|
||||
install -Dm644 "src-tauri/icons/128x128.png" \
|
||||
"$pkgdir/usr/share/icons/hicolor/128x128/apps/gitty.png"
|
||||
install -Dm644 "src-tauri/icons/128x128@2x.png" \
|
||||
"$pkgdir/usr/share/icons/hicolor/256x256@2/apps/gitty.png"
|
||||
install -Dm644 "src-tauri/icons/icon.png" \
|
||||
"$pkgdir/usr/share/icons/hicolor/512x512/apps/gitty.png"
|
||||
|
||||
install -d "$pkgdir/usr/share/applications"
|
||||
cat > "$pkgdir/usr/share/applications/gitty.desktop" <<-EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Gitty
|
||||
Comment=$pkgdesc
|
||||
Exec=gitty
|
||||
Icon=gitty
|
||||
Terminal=false
|
||||
Categories=Development;RevisionControl;
|
||||
StartupWMClass=gitty
|
||||
EOF
|
||||
|
||||
install -Dm644 "README.md" "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "gitty",
|
||||
"version": "0.200.10",
|
||||
"version": "2026.7.19",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gitty",
|
||||
"version": "0.200.10",
|
||||
"version": "2026.7.19",
|
||||
"dependencies": {
|
||||
"@lucide/svelte": "^1.21.0",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitty",
|
||||
"version": "0.200.10",
|
||||
"version": "2026.7.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -27,6 +27,12 @@ tauri-build = { version = "2", features = [] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2"
|
||||
|
||||
# Self-updating only makes sense for install methods this app ships itself
|
||||
# (NSIS on Windows, the .app bundle on macOS). On Linux the app is meant to be
|
||||
# installed via the system package manager (e.g. the PKGBUILD), which owns
|
||||
# updates instead — so the updater plugin isn't even compiled in there.
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios", target_os = "linux")))'.dependencies]
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
# ── Build profiles ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": [
|
||||
"macOS",
|
||||
"windows",
|
||||
"linux"
|
||||
"windows"
|
||||
],
|
||||
"windows": [
|
||||
"main"
|
||||
|
||||
+11
-3
@@ -33,6 +33,7 @@ fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> {
|
||||
}
|
||||
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.maximize();
|
||||
window
|
||||
.show()
|
||||
.map_err(|error| format!("failed to show main window: {error}"))?;
|
||||
@@ -52,7 +53,7 @@ async fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
tauri::Builder::default()
|
||||
let builder =tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
||||
#[cfg(desktop)]
|
||||
let _ = app
|
||||
@@ -60,7 +61,6 @@ async fn main() {
|
||||
.expect("no main window")
|
||||
.set_focus();
|
||||
}))
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(
|
||||
tauri_plugin_aptabase::Builder::new("A-SH-1344793789")
|
||||
.with_options(tauri_plugin_aptabase::InitOptions {
|
||||
@@ -71,7 +71,15 @@ async fn main() {
|
||||
)
|
||||
.manage(SearchCancellationState::default())
|
||||
.manage(commit_ai::CommitAiEngine::new())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_dialog::init());
|
||||
|
||||
// Linux installs are expected to come from the system package manager (see the
|
||||
// PKGBUILD), which owns updates itself — the self-updater is only wired up for
|
||||
// the platforms whose install method this app ships (NSIS/Windows, .app/macOS).
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
|
||||
builder
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
open_repository,
|
||||
clone_repository,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Gitty",
|
||||
"version": "0.200.10",
|
||||
"version": "2026.7.19",
|
||||
"identifier": "com.gitty",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
+81
-17
@@ -6,6 +6,7 @@
|
||||
import { AlertCircle, BookOpen, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
|
||||
|
||||
import TitleBar from "./lib/TitleBar.svelte";
|
||||
import RepoToolbar from "./lib/RepoToolbar.svelte";
|
||||
import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte";
|
||||
import AnalyticsNoticeDialog from "./lib/components/AnalyticsNoticeDialog.svelte";
|
||||
import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte";
|
||||
@@ -21,6 +22,7 @@
|
||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
||||
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
|
||||
import HelpOverlay from "./lib/components/HelpOverlay.svelte";
|
||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||
import InteractiveRebaseDialog from "./lib/components/InteractiveRebaseDialog.svelte";
|
||||
import LinePatchDialog from "./lib/components/LinePatchDialog.svelte";
|
||||
@@ -104,6 +106,7 @@
|
||||
|
||||
import type {
|
||||
AiSettings,
|
||||
AppLanguage,
|
||||
AppTheme,
|
||||
AnalyticsSettings,
|
||||
CommitAiPhase,
|
||||
@@ -176,6 +179,7 @@
|
||||
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
||||
const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1";
|
||||
const APP_THEME_KEY = "gitlite.theme.v1";
|
||||
const APP_LANGUAGE_KEY = "gitlite.language.v1";
|
||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
|
||||
const LEFT_BRANCH_PANEL_HEIGHT_KEY = "gitlite.leftBranchPanelHeight.v1";
|
||||
@@ -248,9 +252,11 @@
|
||||
let aiSettings: AiSettings = defaultAiSettings();
|
||||
let aiSettingsOpen = false;
|
||||
let appSettingsOpen = false;
|
||||
let helpOpen = false;
|
||||
let analyticsNoticeOpen = false;
|
||||
let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings();
|
||||
let appTheme: AppTheme = loadThemePreference();
|
||||
let appLanguage: AppLanguage = loadLanguagePreference();
|
||||
let localModelOptions: LocalModelOption[] = [];
|
||||
let errorMessage = "";
|
||||
let operation = "";
|
||||
@@ -412,6 +418,7 @@
|
||||
$: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
|
||||
|
||||
$: applyThemePreference(appTheme);
|
||||
$: applyLanguagePreference(appLanguage);
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -672,7 +679,7 @@
|
||||
}
|
||||
|
||||
async function autoRefreshTick() {
|
||||
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || newBranchCommit || globalSearchOpen) return;
|
||||
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || newBranchCommit || globalSearchOpen || helpOpen) return;
|
||||
const path = activeRepoPath;
|
||||
autoRefreshInFlight = true;
|
||||
try {
|
||||
@@ -813,6 +820,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
function loadLanguagePreference(): AppLanguage {
|
||||
try {
|
||||
const stored = localStorage.getItem(APP_LANGUAGE_KEY);
|
||||
if (stored === "en" || stored === "de") return stored;
|
||||
} catch {
|
||||
// Local storage is optional; English is the first-start default.
|
||||
}
|
||||
return "en";
|
||||
}
|
||||
|
||||
function persistLanguagePreference(next: AppLanguage) {
|
||||
try {
|
||||
localStorage.setItem(APP_LANGUAGE_KEY, next);
|
||||
} catch {
|
||||
// Ignore storage quota/private-mode errors.
|
||||
}
|
||||
}
|
||||
|
||||
function applyLanguagePreference(next: AppLanguage) {
|
||||
document.documentElement.lang = next;
|
||||
document.documentElement.dataset.language = next;
|
||||
}
|
||||
|
||||
function applyThemePreference(next: AppTheme) {
|
||||
const prefersLight = themeMediaQuery?.matches ?? window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||
const resolved = next === "system"
|
||||
@@ -828,13 +858,15 @@
|
||||
if (appTheme === "system") applyThemePreference(appTheme);
|
||||
}
|
||||
|
||||
function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme) {
|
||||
function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme, nextLanguage: AppLanguage) {
|
||||
analyticsSettings = next;
|
||||
appTheme = nextTheme;
|
||||
appLanguage = nextLanguage;
|
||||
persistAnalyticsSettings(next);
|
||||
persistThemePreference(nextTheme);
|
||||
persistLanguagePreference(nextLanguage);
|
||||
appSettingsOpen = false;
|
||||
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme });
|
||||
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme, language: nextLanguage });
|
||||
}
|
||||
|
||||
function updateCommitMessage(message: string) {
|
||||
@@ -3259,6 +3291,11 @@
|
||||
trackEvent("global_search_opened");
|
||||
}
|
||||
|
||||
function openHelp() {
|
||||
helpOpen = true;
|
||||
trackEvent("help_opened");
|
||||
}
|
||||
|
||||
async function compareSelectedCommits() {
|
||||
if (!canCompare) return;
|
||||
await runOperation("Comparing commits", async () => {
|
||||
@@ -3449,6 +3486,15 @@
|
||||
// ── Event handlers ─────────────────────────────────────────────────────────
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "/") {
|
||||
event.preventDefault();
|
||||
openHelp();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape" && helpOpen) {
|
||||
helpOpen = false;
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape" && repoTabContextMenu) closeRepoTabContextMenu();
|
||||
else if (event.key === "Escape" && pendingDiscard && !isBusy) closeDiscardConfirm();
|
||||
else if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
||||
@@ -3484,21 +3530,9 @@
|
||||
behind={status?.behind ?? 0}
|
||||
repoName={activeRepoPath ? (activeRepoPath.split(/[\\/]/).filter(Boolean).slice(-1)[0] ?? "") : ""}
|
||||
hasRepository={workspaceActive}
|
||||
{isBusy}
|
||||
{operation}
|
||||
{autoRefreshEnabled}
|
||||
{autoRefreshInFlight}
|
||||
onFetch={fetchRepo}
|
||||
onPull={pullRepo}
|
||||
onPush={pushRepo}
|
||||
onRefresh={refreshRepo}
|
||||
onSearch={openGlobalSearchDialog}
|
||||
onCompare={openCompareSelect}
|
||||
onInteractiveRebase={openInteractiveRebase}
|
||||
onReflog={openReflog}
|
||||
onOpenInExplorer={openActiveRepoInExplorer}
|
||||
onToggleAutoRefresh={toggleAutoRefresh}
|
||||
onOpenSettings={() => { appSettingsOpen = true; }}
|
||||
onOpenHelp={openHelp}
|
||||
language={appLanguage}
|
||||
/>
|
||||
|
||||
<div class="shell-body">
|
||||
@@ -3563,6 +3597,31 @@
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Repo actions live under the tab bar and disappear in Repository
|
||||
Management, where none of them are applicable. -->
|
||||
{#if workspaceActive}
|
||||
<RepoToolbar
|
||||
hasRepository={workspaceActive}
|
||||
{isBusy}
|
||||
{operation}
|
||||
ahead={status?.ahead ?? 0}
|
||||
behind={status?.behind ?? 0}
|
||||
{autoRefreshEnabled}
|
||||
{autoRefreshInFlight}
|
||||
language={appLanguage}
|
||||
onFetch={fetchRepo}
|
||||
onPull={pullRepo}
|
||||
onPush={pushRepo}
|
||||
onRefresh={refreshRepo}
|
||||
onSearch={openGlobalSearchDialog}
|
||||
onCompare={openCompareSelect}
|
||||
onInteractiveRebase={openInteractiveRebase}
|
||||
onReflog={openReflog}
|
||||
onOpenInExplorer={openActiveRepoInExplorer}
|
||||
onToggleAutoRefresh={toggleAutoRefresh}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if repoTabContextMenu}
|
||||
{@const menu = repoTabContextMenu}
|
||||
{@const contextTab = repoTabs.find((tab) => sameRepoPath(tab.path, menu.path))}
|
||||
@@ -4113,11 +4172,16 @@
|
||||
<AppSettingsDialog
|
||||
analytics={analyticsSettings}
|
||||
theme={appTheme}
|
||||
language={appLanguage}
|
||||
onSave={saveAppSettings}
|
||||
onClose={() => { appSettingsOpen = false; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if helpOpen}
|
||||
<HelpOverlay language={appLanguage} onClose={() => { helpOpen = false; }} />
|
||||
{/if}
|
||||
|
||||
{#if linePatchOpen && linePatchFile}
|
||||
<LinePatchDialog
|
||||
file={linePatchFile}
|
||||
|
||||
+203
-8
@@ -541,6 +541,7 @@
|
||||
|
||||
.shell {
|
||||
--app-titlebar-height: 42px;
|
||||
--app-toolbar-height: 58px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@@ -562,7 +563,7 @@
|
||||
position: relative;
|
||||
z-index: 500;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
height: var(--app-titlebar-height);
|
||||
background:
|
||||
@@ -639,8 +640,187 @@
|
||||
|
||||
.tb-no-repo { color: var(--color-bar-muted); font-size: 12px; font-style: italic; }
|
||||
|
||||
.titlebar-right { display: flex; align-items: stretch; height: 100%; border-left: 1px solid rgba(255,255,255,0.06); }
|
||||
.titlebar-actions { display: flex; align-items: stretch; }
|
||||
/* App-global actions (help, settings) that must stay reachable in every view,
|
||||
including Repository Management where the repo toolbar is hidden. */
|
||||
.titlebar-globals {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Repository actions are ordered by workflow: sync, inspect/history, utilities. */
|
||||
.repo-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
height: var(--app-toolbar-height);
|
||||
padding: 5px 8px;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: 10px;
|
||||
background: rgba(16, 17, 29, 0.9);
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.repo-toolbar-sync {
|
||||
display: grid;
|
||||
grid-template-rows: 11px 34px;
|
||||
gap: 2px;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
}
|
||||
.repo-toolbar-group-label {
|
||||
padding-left: 6px;
|
||||
color: var(--color-ink-faint);
|
||||
font-size: 8px;
|
||||
font-weight: 850;
|
||||
line-height: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.repo-action-group {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 34px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: 7px;
|
||||
background: rgba(255,255,255,0.018);
|
||||
}
|
||||
.repo-action-group > .repo-action,
|
||||
.repo-action-group > .repo-history-wrap > .repo-action {
|
||||
border-right: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.repo-action-group > .repo-action:last-child,
|
||||
.repo-action-group > .repo-history-wrap:last-child > .repo-action {
|
||||
border-right: 0;
|
||||
}
|
||||
.repo-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 38px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: var(--color-ink-muted);
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color 120ms ease, background 120ms ease;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
.repo-action:first-child { border-radius: 6px 0 0 6px; }
|
||||
.repo-action:last-child { border-radius: 0 6px 6px 0; }
|
||||
.repo-action:only-child { border-radius: 6px; }
|
||||
.repo-action:hover:not(:disabled),
|
||||
.repo-action.active {
|
||||
color: var(--color-ink);
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
.repo-action:disabled { opacity: 0.34; cursor: not-allowed; }
|
||||
.repo-action.sync-primary { color: var(--color-accent); }
|
||||
.repo-action.sync-primary:hover:not(:disabled) { background: rgba(90,140,248,0.1); }
|
||||
.repo-action-label { font-size: 12px; font-weight: 750; }
|
||||
.repo-action-count {
|
||||
color: currentColor;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9.5px;
|
||||
font-weight: 850;
|
||||
}
|
||||
.repo-action-count.behind { color: #7aacff; }
|
||||
.repo-action-count.ahead { color: #e0a040; }
|
||||
.repo-toolbar-divider {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
margin: 0 1px 2px;
|
||||
background: var(--color-border-subtle);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.repo-toolbar-spacer { flex: 1 1 auto; min-width: 6px; }
|
||||
.repo-inspect-actions,
|
||||
.repo-utility-actions { margin-bottom: 1px; }
|
||||
.repo-history-wrap { position: relative; height: 32px; }
|
||||
.history-trigger { border-radius: 0 6px 6px 0; }
|
||||
.history-trigger .history-chevron { transition: transform 120ms ease; }
|
||||
.history-trigger.active .history-chevron { transform: rotate(180deg); }
|
||||
.repo-history-menu {
|
||||
position: absolute;
|
||||
z-index: 90;
|
||||
top: calc(100% + 7px);
|
||||
right: -1px;
|
||||
display: grid;
|
||||
width: 240px;
|
||||
padding: 5px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 9px;
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: 0 16px 42px rgba(0,0,0,0.36);
|
||||
}
|
||||
.repo-history-menu > button {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 48px;
|
||||
padding: 7px 8px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: var(--color-ink-muted);
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
}
|
||||
.repo-history-menu > button:hover { color: var(--color-ink); background: var(--color-surface-hover); }
|
||||
.repo-history-menu > button > svg { color: var(--color-accent); }
|
||||
.repo-history-menu > button > span { display: grid; gap: 2px; min-width: 0; }
|
||||
.repo-history-menu strong { color: inherit; font-size: 11px; }
|
||||
.repo-history-menu small { overflow: hidden; color: var(--color-ink-faint); font-size: 9.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.repo-utility-actions { flex: 0 0 auto; }
|
||||
.repo-action.icon-action { width: 36px; padding: 0; }
|
||||
.repo-auto-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0 8px 0 10px;
|
||||
border: 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
color: var(--color-ink-muted);
|
||||
background: transparent;
|
||||
font-size: 11.5px;
|
||||
font-weight: 750;
|
||||
}
|
||||
.repo-auto-toggle:hover { color: var(--color-ink); background: var(--color-surface-hover); }
|
||||
.repo-switch {
|
||||
position: relative;
|
||||
width: 27px;
|
||||
height: 15px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-border-input);
|
||||
transition: background 140ms ease;
|
||||
}
|
||||
.repo-switch > span {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.28);
|
||||
transition: transform 140ms ease;
|
||||
}
|
||||
.repo-auto-toggle.active { color: #4eca76; }
|
||||
.repo-auto-toggle.active .repo-switch { background: #26924f; }
|
||||
.repo-auto-toggle.active .repo-switch > span { transform: translateX(12px); }
|
||||
.repo-switch.busy { box-shadow: 0 0 0 3px rgba(78,202,118,0.12); }
|
||||
|
||||
.tb-action {
|
||||
display: flex;
|
||||
@@ -655,6 +835,7 @@
|
||||
color: var(--color-bar-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background 120ms ease, color 120ms ease;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
@@ -664,9 +845,9 @@
|
||||
.tb-action.auto-toggle.active:hover:not(:disabled) { color: #6ee090; background: rgba(78,202,118,0.1); }
|
||||
.tb-action-label { font-size: 12px; font-weight: 600; }
|
||||
|
||||
.titlebar-divider { width: 1px; margin: 8px 0; background: rgba(255,255,255,0.08); flex-shrink: 0; }
|
||||
.titlebar-divider { width: 1px; margin: 8px 6px; background: rgba(255,255,255,0.08); flex-shrink: 0; }
|
||||
|
||||
.titlebar-controls { display: flex; align-items: stretch; height: 100%; }
|
||||
.titlebar-controls { display: flex; align-items: stretch; height: 100%; border-left: 1px solid rgba(255,255,255,0.06); }
|
||||
|
||||
.tb-btn {
|
||||
display: flex;
|
||||
@@ -3083,6 +3264,10 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-segmented.settings-language {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.settings-toggle-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
@@ -4716,10 +4901,15 @@
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .titlebar-brand,
|
||||
:root[data-theme="light"] .titlebar-right {
|
||||
:root[data-theme="light"] .titlebar-controls {
|
||||
border-color: rgba(34,49,78,0.1);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .repo-toolbar {
|
||||
border-color: var(--color-border);
|
||||
background: rgba(255,255,255,0.78);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .titlebar-brand-icon img {
|
||||
filter: drop-shadow(0 1px 2px rgba(28,44,74,0.18));
|
||||
}
|
||||
@@ -5187,6 +5377,7 @@
|
||||
.branch-actions { flex-direction: column; align-items: flex-end; gap: 4px; }
|
||||
.branch-actions button { width: 100%; }
|
||||
.tb-action-label { display: none; }
|
||||
.repo-action-label.utility-label { display: none; }
|
||||
}
|
||||
|
||||
/* Narrow: single column */
|
||||
@@ -5194,8 +5385,7 @@
|
||||
body { overflow: auto; }
|
||||
.shell-body { min-height: 100%; gap: 6px; }
|
||||
.titlebar-info { padding: 0 8px; }
|
||||
.titlebar-actions .tb-action:not(.auto-toggle):not(:last-child) { display: none; }
|
||||
.tb-action { width: 36px; padding: 0 8px; }
|
||||
.tb-action { padding: 0 9px; }
|
||||
.tb-btn { width: 34px; }
|
||||
.workspace { grid-template-columns: 1fr; gap: 6px; }
|
||||
.left-sidebar-resize-handle { display: none; }
|
||||
@@ -5261,4 +5451,9 @@
|
||||
.dialog-files { border-right: none; border-bottom: 1px solid var(--color-border-subtle); }
|
||||
.branch-actions { flex-direction: row; justify-content: flex-start; }
|
||||
.tb-action-label { display: none; }
|
||||
.repo-action-label { display: none; }
|
||||
.repo-action { padding-inline: 8px; }
|
||||
.repo-toolbar { gap: 5px; padding-inline: 6px; }
|
||||
.repo-toolbar-divider { margin-inline: 0; }
|
||||
.repo-auto-toggle { padding-left: 7px; gap: 6px; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
ChevronDown,
|
||||
CloudDownload,
|
||||
Download,
|
||||
FolderOpen,
|
||||
GitCompare,
|
||||
History,
|
||||
ListRestart,
|
||||
LoaderCircle,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Upload,
|
||||
} from "@lucide/svelte";
|
||||
|
||||
export let hasRepository: boolean = false;
|
||||
export let isBusy: boolean = false;
|
||||
export let operation: string = "";
|
||||
export let ahead: number = 0;
|
||||
export let behind: number = 0;
|
||||
export let autoRefreshEnabled: boolean = true;
|
||||
export let autoRefreshInFlight: boolean = false;
|
||||
export let language: "en" | "de" = "en";
|
||||
export let onFetch: () => void = () => {};
|
||||
export let onPull: () => void = () => {};
|
||||
export let onPush: () => void = () => {};
|
||||
export let onRefresh: () => void = () => {};
|
||||
export let onSearch: () => void = () => {};
|
||||
export let onCompare: () => void = () => {};
|
||||
export let onInteractiveRebase: () => void = () => {};
|
||||
export let onReflog: () => void = () => {};
|
||||
export let onOpenInExplorer: () => void = () => {};
|
||||
export let onToggleAutoRefresh: () => void = () => {};
|
||||
|
||||
let historyOpen = false;
|
||||
let toolbarElement: HTMLDivElement;
|
||||
|
||||
$: isGerman = language === "de";
|
||||
|
||||
function runHistoryAction(action: () => void) {
|
||||
historyOpen = false;
|
||||
action();
|
||||
}
|
||||
|
||||
function handleWindowClick(event: MouseEvent) {
|
||||
if (historyOpen && toolbarElement && !toolbarElement.contains(event.target as Node)) {
|
||||
historyOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape" && historyOpen) {
|
||||
event.stopPropagation();
|
||||
historyOpen = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={handleWindowClick} onkeydown={handleWindowKeydown} />
|
||||
|
||||
<div bind:this={toolbarElement} class="repo-toolbar" role="toolbar" aria-label={isGerman ? "Repository-Aktionen" : "Repository actions"}>
|
||||
<div class="repo-toolbar-sync">
|
||||
<span class="repo-toolbar-group-label">Sync</span>
|
||||
<div class="repo-action-group repo-sync-actions">
|
||||
<button
|
||||
class="repo-action fetch"
|
||||
onclick={onFetch}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Fetch"
|
||||
aria-label="Fetch"
|
||||
>
|
||||
{#if operation === "Fetching"}
|
||||
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
||||
{:else}
|
||||
<CloudDownload size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="repo-action-label">Fetch</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-action sync-primary"
|
||||
onclick={onPull}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Pull"
|
||||
aria-label={behind > 0
|
||||
? `Pull, ${behind} ${isGerman ? (behind === 1 ? "Remote-Commit voraus" : "Remote-Commits voraus") : (behind === 1 ? "commit behind" : "commits behind")}`
|
||||
: "Pull"}
|
||||
>
|
||||
{#if operation === "Pulling"}
|
||||
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
||||
{:else}
|
||||
<Download size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="repo-action-label">Pull</span>
|
||||
{#if behind > 0}<span class="repo-action-count behind">↓{behind}</span>{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-action sync-primary"
|
||||
onclick={onPush}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Push"
|
||||
aria-label={ahead > 0
|
||||
? `Push, ${ahead} ${isGerman ? (ahead === 1 ? "lokaler Commit voraus" : "lokale Commits voraus") : (ahead === 1 ? "commit ahead" : "commits ahead")}`
|
||||
: "Push"}
|
||||
>
|
||||
{#if operation === "Pushing"}
|
||||
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
||||
{:else}
|
||||
<Upload size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="repo-action-label">Push</span>
|
||||
{#if ahead > 0}<span class="repo-action-count ahead">↑{ahead}</span>{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-toolbar-divider" aria-hidden="true"></div>
|
||||
|
||||
<div class="repo-action-group repo-inspect-actions">
|
||||
<button
|
||||
class="repo-action"
|
||||
onclick={onSearch}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title={isGerman ? "Globale Suche" : "Global search"}
|
||||
aria-label={isGerman ? "Globale Suche" : "Global search"}
|
||||
>
|
||||
<Search size={15} aria-hidden="true" />
|
||||
<span class="repo-action-label">{isGerman ? "Suchen" : "Search"}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-action"
|
||||
onclick={onCompare}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title={isGerman ? "Commits vergleichen" : "Compare commits"}
|
||||
aria-label={isGerman ? "Commits vergleichen" : "Compare commits"}
|
||||
>
|
||||
<GitCompare size={15} aria-hidden="true" />
|
||||
<span class="repo-action-label">{isGerman ? "Vergleichen" : "Compare"}</span>
|
||||
</button>
|
||||
|
||||
<div class="repo-history-wrap">
|
||||
<button
|
||||
class="repo-action history-trigger"
|
||||
class:active={historyOpen}
|
||||
type="button"
|
||||
onclick={() => { historyOpen = !historyOpen; }}
|
||||
disabled={!hasRepository || isBusy}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={historyOpen}
|
||||
title={isGerman ? "Verlauf und Rebase" : "History and rebase"}
|
||||
>
|
||||
<History size={15} aria-hidden="true" />
|
||||
<span class="repo-action-label">{isGerman ? "Verlauf" : "History"}</span>
|
||||
<ChevronDown class="history-chevron" size={13} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{#if historyOpen}
|
||||
<div class="repo-history-menu" role="menu">
|
||||
<button type="button" role="menuitem" onclick={() => runHistoryAction(onReflog)}>
|
||||
<History size={15} aria-hidden="true" />
|
||||
<span>
|
||||
<strong>Reflog</strong>
|
||||
<small>{isGerman ? "Lokale Referenzbewegungen" : "Local reference movements"}</small>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" role="menuitem" onclick={() => runHistoryAction(onInteractiveRebase)}>
|
||||
<ListRestart size={15} aria-hidden="true" />
|
||||
<span>
|
||||
<strong>{isGerman ? "Interaktiver Rebase" : "Interactive rebase"}</strong>
|
||||
<small>{isGerman ? "Commits ordnen und zusammenfassen" : "Reorder and combine commits"}</small>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-toolbar-spacer"></div>
|
||||
<div class="repo-toolbar-divider utility" aria-hidden="true"></div>
|
||||
|
||||
<div class="repo-action-group repo-utility-actions">
|
||||
<button
|
||||
class="repo-action"
|
||||
onclick={onOpenInExplorer}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title={isGerman ? "Repository im Explorer öffnen" : "Open repository in Explorer"}
|
||||
aria-label={isGerman ? "Repository im Explorer öffnen" : "Open repository in Explorer"}
|
||||
>
|
||||
<FolderOpen size={15} aria-hidden="true" />
|
||||
<span class="repo-action-label utility-label">Explorer</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-action icon-action"
|
||||
onclick={onRefresh}
|
||||
disabled={isBusy || !hasRepository}
|
||||
title={isGerman ? "Manuell aktualisieren" : "Refresh now"}
|
||||
aria-label={isGerman ? "Manuell aktualisieren" : "Refresh now"}
|
||||
>
|
||||
<RefreshCw class={operation === "Refreshing" ? "spin" : ""} size={15} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-auto-toggle"
|
||||
class:active={autoRefreshEnabled}
|
||||
onclick={onToggleAutoRefresh}
|
||||
aria-pressed={autoRefreshEnabled}
|
||||
title={autoRefreshEnabled
|
||||
? (isGerman ? "Auto-Aktualisierung ausschalten" : "Turn auto-refresh off")
|
||||
: (isGerman ? "Auto-Aktualisierung einschalten" : "Turn auto-refresh on")}
|
||||
>
|
||||
<span>{isGerman ? "Auto" : "Auto"}</span>
|
||||
<span class="repo-switch" class:busy={autoRefreshInFlight} aria-hidden="true"><span></span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
+46
-186
@@ -2,7 +2,7 @@
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, History, ListRestart, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte";
|
||||
import { CircleHelp, GitBranch, Minus, Settings, X } from "@lucide/svelte";
|
||||
import iconUrl from "../../src-tauri/icons/icon.png";
|
||||
|
||||
export let branch: string = "";
|
||||
@@ -10,21 +10,9 @@
|
||||
export let behind: number = 0;
|
||||
export let repoName: string = "";
|
||||
export let hasRepository: boolean = false;
|
||||
export let isBusy: boolean = false;
|
||||
export let operation: string = "";
|
||||
export let autoRefreshEnabled: boolean = true;
|
||||
export let autoRefreshInFlight: boolean = false;
|
||||
export let onFetch: () => void = () => {};
|
||||
export let onPull: () => void = () => {};
|
||||
export let onPush: () => void = () => {};
|
||||
export let onRefresh: () => void = () => {};
|
||||
export let onSearch: () => void = () => {};
|
||||
export let onCompare: () => void = () => {};
|
||||
export let onInteractiveRebase: () => void = () => {};
|
||||
export let onReflog: () => void = () => {};
|
||||
export let onOpenInExplorer: () => void = () => {};
|
||||
export let onToggleAutoRefresh: () => void = () => {};
|
||||
export let onOpenHelp: () => void = () => {};
|
||||
export let onOpenSettings: () => void = () => {};
|
||||
export let language: "en" | "de" = "en";
|
||||
|
||||
let win: ReturnType<typeof getCurrentWindow> | null = null;
|
||||
let isMaximized = false;
|
||||
@@ -104,177 +92,49 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Right: actions + window controls -->
|
||||
<div class="titlebar-right">
|
||||
<div class="titlebar-actions" role="toolbar" aria-label="Repository actions">
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onOpenInExplorer}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Open repository in Explorer"
|
||||
aria-label="Open repository in Explorer"
|
||||
>
|
||||
<FolderOpen size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Explorer</span>
|
||||
</button>
|
||||
<!-- Right: app-global actions + window controls -->
|
||||
<div class="titlebar-globals">
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onOpenHelp}
|
||||
title={language === "de" ? "Hilfe (Ctrl+/)" : "Help (Ctrl+/)"}
|
||||
aria-label={language === "de" ? "Hilfe öffnen" : "Open help"}
|
||||
>
|
||||
<CircleHelp size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onOpenSettings}
|
||||
title={language === "de" ? "Einstellungen" : "Settings"}
|
||||
aria-label={language === "de" ? "Einstellungen" : "Settings"}
|
||||
>
|
||||
<Settings size={14} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onSearch}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Global search"
|
||||
aria-label="Global search"
|
||||
>
|
||||
<Search size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Search</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onCompare}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Compare commits"
|
||||
aria-label="Compare commits"
|
||||
>
|
||||
<GitCompare size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Compare</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onInteractiveRebase}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Interactive rebase"
|
||||
aria-label="Interactive rebase"
|
||||
>
|
||||
<ListRestart size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Rebase</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onReflog}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Reflog"
|
||||
aria-label="Reflog"
|
||||
>
|
||||
<History size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Reflog</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onFetch}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Fetch"
|
||||
aria-label="Fetch"
|
||||
>
|
||||
{#if operation === "Fetching"}
|
||||
<LoaderCircle class="spin" size={14} aria-hidden="true" />
|
||||
{:else}
|
||||
<CloudDownload size={14} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="tb-action-label">Fetch</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onPull}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Pull"
|
||||
aria-label="Pull"
|
||||
>
|
||||
{#if operation === "Pulling"}
|
||||
<LoaderCircle class="spin" size={14} aria-hidden="true" />
|
||||
{:else}
|
||||
<Download size={14} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="tb-action-label">Pull</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onPush}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Push"
|
||||
aria-label="Push"
|
||||
>
|
||||
{#if operation === "Pushing"}
|
||||
<LoaderCircle class="spin" size={14} aria-hidden="true" />
|
||||
{:else}
|
||||
<Upload size={14} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="tb-action-label">Push</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onRefresh}
|
||||
disabled={isBusy || !hasRepository}
|
||||
title="Refresh"
|
||||
aria-label="Refresh"
|
||||
>
|
||||
<RefreshCw
|
||||
class={operation === "Refreshing" ? "spin" : ""}
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="tb-action-label">Refresh</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action auto-toggle"
|
||||
class:active={autoRefreshEnabled}
|
||||
onclick={onToggleAutoRefresh}
|
||||
aria-pressed={autoRefreshEnabled}
|
||||
title={autoRefreshEnabled ? "Auto-refresh on — click to disable" : "Auto-refresh off — click to enable"}
|
||||
aria-label="Toggle auto-refresh"
|
||||
>
|
||||
<RefreshCw
|
||||
class={autoRefreshInFlight ? "spin" : ""}
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="tb-action-label">Auto</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onOpenSettings}
|
||||
title="Settings"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<Settings size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="titlebar-divider" aria-hidden="true"></div>
|
||||
|
||||
<div class="titlebar-controls">
|
||||
<button class="tb-btn" onclick={minimizeWindow} title="Minimize" aria-label="Minimize">
|
||||
<Minus size={12} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
onclick={toggleMaximizeWindow}
|
||||
title={isMaximized ? "Restore" : "Maximize"}
|
||||
aria-label={isMaximized ? "Restore" : "Maximize"}
|
||||
>
|
||||
{#if isMaximized}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="3" y="1" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M1 3v7a1 1 0 0 0 1 1h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="1" y="1" width="10" height="10" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button class="tb-btn close" onclick={closeWindow} title="Close" aria-label="Close">
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="titlebar-controls">
|
||||
<button class="tb-btn" onclick={minimizeWindow} title="Minimize" aria-label="Minimize">
|
||||
<Minus size={12} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
onclick={toggleMaximizeWindow}
|
||||
title={isMaximized ? "Restore" : "Maximize"}
|
||||
aria-label={isMaximized ? "Restore" : "Maximize"}
|
||||
>
|
||||
{#if isMaximized}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="3" y="1" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M1 3v7a1 1 0 0 0 1 1h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="1" y="1" width="10" height="10" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button class="tb-btn close" onclick={closeWindow} title="Close" aria-label="Close">
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Check, Settings, X } from "@lucide/svelte";
|
||||
import type { AnalyticsSettings, AppTheme } from "../types";
|
||||
import { Check, Languages, Settings, X } from "@lucide/svelte";
|
||||
import type { AnalyticsSettings, AppLanguage, AppTheme } from "../types";
|
||||
|
||||
interface Props {
|
||||
analytics: AnalyticsSettings;
|
||||
theme: AppTheme;
|
||||
onSave: (settings: AnalyticsSettings, theme: AppTheme) => void;
|
||||
language: AppLanguage;
|
||||
onSave: (settings: AnalyticsSettings, theme: AppTheme, language: AppLanguage) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { analytics, theme = "system", onSave = () => {}, onClose = () => {} }: Props = $props();
|
||||
let { analytics, theme = "system", language = "en", onSave = () => {}, onClose = () => {} }: Props = $props();
|
||||
|
||||
let analyticsEnabled = $state(true);
|
||||
let selectedTheme = $state<AppTheme>("system");
|
||||
let selectedLanguage = $state<AppLanguage>("en");
|
||||
const isGerman = $derived(selectedLanguage === "de");
|
||||
|
||||
$effect(() => {
|
||||
analyticsEnabled = analytics.enabled;
|
||||
selectedTheme = theme;
|
||||
selectedLanguage = language;
|
||||
});
|
||||
|
||||
function save() {
|
||||
@@ -24,18 +28,18 @@
|
||||
...analytics,
|
||||
enabled: analyticsEnabled,
|
||||
noticeSeen: true,
|
||||
}, selectedTheme);
|
||||
}, selectedTheme, selectedLanguage);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dialog-backdrop" role="presentation">
|
||||
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label="Settings" tabindex="-1">
|
||||
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label={isGerman ? "Einstellungen" : "Settings"} tabindex="-1">
|
||||
<header class="dialog-header">
|
||||
<div>
|
||||
<span class="eyebrow">Gitty</span>
|
||||
<h2 class="dialog-title">Settings</h2>
|
||||
<h2 class="dialog-title">{isGerman ? "Einstellungen" : "Settings"}</h2>
|
||||
</div>
|
||||
<button class="dialog-close" type="button" onclick={onClose} title="Close">
|
||||
<button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"}>
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
@@ -45,23 +49,44 @@
|
||||
<header>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
<div>
|
||||
<span class="eyebrow">Appearance</span>
|
||||
<h3>Theme</h3>
|
||||
<span class="eyebrow">{isGerman ? "Darstellung" : "Appearance"}</span>
|
||||
<h3>{isGerman ? "Farbschema" : "Theme"}</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="settings-segmented" role="radiogroup" aria-label="Theme">
|
||||
<div class="settings-segmented" role="radiogroup" aria-label={isGerman ? "Farbschema" : "Theme"}>
|
||||
<label class:active={selectedTheme === "system"}>
|
||||
<input type="radio" bind:group={selectedTheme} value="system" />
|
||||
<span>System</span>
|
||||
</label>
|
||||
<label class:active={selectedTheme === "light"}>
|
||||
<input type="radio" bind:group={selectedTheme} value="light" />
|
||||
<span>Light</span>
|
||||
<span>{isGerman ? "Hell" : "Light"}</span>
|
||||
</label>
|
||||
<label class:active={selectedTheme === "dark"}>
|
||||
<input type="radio" bind:group={selectedTheme} value="dark" />
|
||||
<span>Dark</span>
|
||||
<span>{isGerman ? "Dunkel" : "Dark"}</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<header>
|
||||
<Languages size={16} aria-hidden="true" />
|
||||
<div>
|
||||
<span class="eyebrow">{isGerman ? "Sprache" : "Language"}</span>
|
||||
<h3>{isGerman ? "App-Sprache" : "App language"}</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="settings-segmented settings-language" role="radiogroup" aria-label={isGerman ? "App-Sprache" : "App language"}>
|
||||
<label class:active={selectedLanguage === "en"}>
|
||||
<input type="radio" bind:group={selectedLanguage} value="en" />
|
||||
<span>EN · English</span>
|
||||
</label>
|
||||
<label class:active={selectedLanguage === "de"}>
|
||||
<input type="radio" bind:group={selectedLanguage} value="de" />
|
||||
<span>DE · Deutsch</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
@@ -71,24 +96,24 @@
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
<div>
|
||||
<span class="eyebrow">Analytics</span>
|
||||
<h3>Anonymous usage analytics</h3>
|
||||
<h3>{isGerman ? "Anonyme Nutzungsanalyse" : "Anonymous usage analytics"}</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<label class="settings-toggle-row">
|
||||
<input type="checkbox" bind:checked={analyticsEnabled} />
|
||||
<span>
|
||||
<strong>Allow anonymous Aptabase events</strong>
|
||||
<small>No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent.</small>
|
||||
<strong>{isGerman ? "Anonyme Aptabase-Ereignisse erlauben" : "Allow anonymous Aptabase events"}</strong>
|
||||
<small>{isGerman ? "Es werden keine Repository-Pfade, Remotes, Branches, Commit-Nachrichten, Dateinamen, Diffs, Zugangsdaten oder Code übertragen." : "No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent."}</small>
|
||||
</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div class="new-branch-actions">
|
||||
<button class="btn-secondary" type="button" onclick={onClose}>Cancel</button>
|
||||
<button class="btn-secondary" type="button" onclick={onClose}>{isGerman ? "Abbrechen" : "Cancel"}</button>
|
||||
<button class="btn-primary" type="submit">
|
||||
<Check size={16} aria-hidden="true" />
|
||||
Save
|
||||
{isGerman ? "Speichern" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ export type CommitAiPhase = "idle" | "loading" | "ready" | "error";
|
||||
export type CommitAiProvider = "local" | "openai" | "anthropic" | "custom";
|
||||
export type CommitAiLocalProfile = "fast" | "balanced" | "detailed";
|
||||
export type AppTheme = "system" | "light" | "dark";
|
||||
export type AppLanguage = "en" | "de";
|
||||
|
||||
export interface CommitAiStatus {
|
||||
phase: CommitAiPhase;
|
||||
|
||||
Reference in New Issue
Block a user