This updates the Windows taskbar overlay badge to show ahead and behind separately with distinct colors, and clears it when both are zero. It also adds a new authenticated fetch command wired through the Tauri backend and UI, including a silent background fetch to keep ahead/behind (and the badge) accurate without user interaction. - Add Windows dual badge rendering for ahead/behind - Implement Tauri fetch command with auth error handling - Add UI fetch action plus periodic background fetch updates
225 lines
6.9 KiB
Svelte
225 lines
6.9 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
AlertCircle,
|
|
Download,
|
|
Eye,
|
|
EyeOff,
|
|
Key,
|
|
LoaderCircle,
|
|
Lock,
|
|
ShieldCheck,
|
|
Upload,
|
|
User,
|
|
X,
|
|
} from "@lucide/svelte";
|
|
|
|
interface Props {
|
|
action: "push" | "pull" | "fetch";
|
|
error: string;
|
|
isBusy: boolean;
|
|
onSubmit: (username: string, password: string, save: boolean, expiresAt: string | null) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
let {
|
|
action,
|
|
error = "",
|
|
isBusy = false,
|
|
onSubmit,
|
|
onCancel,
|
|
}: Props = $props();
|
|
|
|
type Mode = "credentials" | "token";
|
|
|
|
let mode = $state<Mode>("credentials");
|
|
let username = $state("");
|
|
let password = $state("");
|
|
let showPassword = $state(false);
|
|
let saveSession = $state(false);
|
|
let expiresAt = $state("");
|
|
|
|
let canSubmit = $derived(
|
|
!isBusy &&
|
|
password.trim().length > 0 &&
|
|
(mode === "token" || username.trim().length > 0),
|
|
);
|
|
let actionLabel = $derived(action === "push" ? "Push" : action === "fetch" ? "Fetch" : "Pull");
|
|
let actionTitle = $derived(
|
|
action === "push" ? "Authenticate push" : action === "fetch" ? "Authenticate fetch" : "Authenticate pull",
|
|
);
|
|
let actionHint = $derived(action === "push"
|
|
? "The remote needs write access. Use a password or a token with the appropriate repository permissions."
|
|
: "The remote needs access to the repository. Use your Git credentials or a personal access token.");
|
|
|
|
function handleSubmit(e: SubmitEvent) {
|
|
e.preventDefault();
|
|
if (!canSubmit) return;
|
|
onSubmit(
|
|
mode === "token" ? "oauth2" : username,
|
|
password,
|
|
saveSession,
|
|
saveSession && expiresAt ? expiresAt : null,
|
|
);
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
class="dialog-backdrop"
|
|
role="presentation"
|
|
onclick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
|
|
>
|
|
<div class="cred-card" role="dialog" aria-modal="true" aria-label="Git credentials" tabindex="-1">
|
|
<div class="cred-hero">
|
|
<div class="cred-hero-top">
|
|
<div class="cred-hero-icon">
|
|
{#if action === "push"}
|
|
<Upload size={27} aria-hidden="true" />
|
|
{:else}
|
|
<Download size={27} aria-hidden="true" />
|
|
{/if}
|
|
</div>
|
|
<div class="cred-hero-text">
|
|
<p class="cred-hero-label">{actionLabel} Remote</p>
|
|
<h2 class="cred-hero-title">{actionTitle}</h2>
|
|
</div>
|
|
<button class="cred-close" type="button" onclick={onCancel} title="Cancel" aria-label="Cancel">
|
|
<X size={16} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
|
|
<p class="cred-hero-copy">{actionHint}</p>
|
|
|
|
<div class="cred-security-note">
|
|
<ShieldCheck size={14} aria-hidden="true" />
|
|
<span>When saved, the token is stored encrypted in the operating system's keychain — never in plain text.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<form class="cred-body" onsubmit={handleSubmit}>
|
|
<div class="cred-segment" role="group" aria-label="Authentication method">
|
|
<button
|
|
type="button"
|
|
class="cred-seg-btn"
|
|
class:active={mode === "credentials"}
|
|
onclick={() => { mode = "credentials"; }}
|
|
aria-pressed={mode === "credentials"}
|
|
>
|
|
<User size={13} aria-hidden="true" />
|
|
Username + password
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="cred-seg-btn"
|
|
class:active={mode === "token"}
|
|
onclick={() => { mode = "token"; }}
|
|
aria-pressed={mode === "token"}
|
|
>
|
|
<Key size={13} aria-hidden="true" />
|
|
Token
|
|
</button>
|
|
</div>
|
|
|
|
<div class="cred-fields">
|
|
{#if mode === "credentials"}
|
|
<div class="cred-field">
|
|
<label class="cred-field-label" for="cred-username">Username</label>
|
|
<div class="cred-input">
|
|
<User size={15} class="cred-field-icon" aria-hidden="true" />
|
|
<input
|
|
id="cred-username"
|
|
type="text"
|
|
bind:value={username}
|
|
placeholder="e.g. my-github-username"
|
|
autocomplete="username"
|
|
disabled={isBusy}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="cred-field">
|
|
<label class="cred-field-label" for="cred-password">
|
|
{mode === "token" ? "Token" : "Password"}
|
|
</label>
|
|
<div class="cred-input">
|
|
<Lock size={15} class="cred-field-icon" aria-hidden="true" />
|
|
<input
|
|
id="cred-password"
|
|
type={showPassword ? "text" : "password"}
|
|
bind:value={password}
|
|
placeholder={mode === "token"
|
|
? "ghp_... or another access token"
|
|
: "Password or personal access token"}
|
|
autocomplete="current-password"
|
|
disabled={isBusy}
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="cred-reveal"
|
|
onclick={() => { showPassword = !showPassword; }}
|
|
tabindex="-1"
|
|
aria-label={showPassword ? "Hide" : "Show"}
|
|
>
|
|
{#if showPassword}
|
|
<EyeOff size={14} aria-hidden="true" />
|
|
{:else}
|
|
<Eye size={14} aria-hidden="true" />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{#if mode === "token"}
|
|
<div class="cred-token-hint">
|
|
<Key size={13} aria-hidden="true" />
|
|
<span>Username is automatically set to <code>oauth2</code>. This works with GitHub, GitLab, and Bitbucket.</span>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if error}
|
|
<div class="cred-error" role="alert">
|
|
<AlertCircle size={14} aria-hidden="true" />
|
|
<span>{error}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if saveSession}
|
|
<div class="cred-expiry">
|
|
<label class="cred-field-label" for="cred-expiry">Expiration date (optional)</label>
|
|
<input
|
|
id="cred-expiry"
|
|
type="date"
|
|
bind:value={expiresAt}
|
|
disabled={isBusy}
|
|
/>
|
|
<span class="cred-expiry-hint">After this date you'll automatically be asked to log in again.</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="cred-footer">
|
|
<label class="cred-save">
|
|
<input type="checkbox" bind:checked={saveSession} disabled={isBusy} />
|
|
<span>Save in keychain</span>
|
|
</label>
|
|
|
|
<div class="cred-btns">
|
|
<button type="button" class="cred-cancel" onclick={onCancel} disabled={isBusy}>
|
|
Cancel
|
|
</button>
|
|
<button class="cred-submit" type="submit" disabled={!canSubmit}>
|
|
{#if isBusy}
|
|
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
|
{:else if action === "push"}
|
|
<Upload size={15} aria-hidden="true" />
|
|
{:else}
|
|
<Download size={15} aria-hidden="true" />
|
|
{/if}
|
|
{actionLabel}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|