diff options
| author | 2026-01-15 18:17:49 +0800 | |
|---|---|---|
| committer | 2026-01-15 18:17:49 +0800 | |
| commit | 314ce0e656107ab43a8e8681d85525a551f83f21 (patch) | |
| tree | 3beffaf90afc62221eca10dbfd72695800290843 | |
| parent | 20cd97d8b3af67050fbe7b5f8d6d5fb1c1f3237b (diff) | |
| download | DropOut-314ce0e656107ab43a8e8681d85525a551f83f21.tar.gz DropOut-314ce0e656107ab43a8e8681d85525a551f83f21.zip | |
feat: Implement logout confirmation dialog and enhance account status display in BottomBar for improved user experience
| -rw-r--r-- | ui/src/App.svelte | 26 | ||||
| -rw-r--r-- | ui/src/components/BottomBar.svelte | 23 | ||||
| -rw-r--r-- | ui/src/stores/auth.svelte.ts | 54 | ||||
| -rw-r--r-- | ui/src/types/index.ts | 3 |
4 files changed, 97 insertions, 9 deletions
diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 0bb31ae..760a15f 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -141,6 +141,32 @@ <LoginModal /> <StatusToast /> + <!-- Logout Confirmation Dialog --> + {#if authState.isLogoutConfirmOpen} + <div class="fixed inset-0 z-[200] bg-black/70 backdrop-blur-sm flex items-center justify-center p-4"> + <div class="bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-6 max-w-sm w-full animate-in fade-in zoom-in-95 duration-200"> + <h3 class="text-lg font-bold text-white mb-2">Logout</h3> + <p class="text-zinc-400 text-sm mb-6"> + Are you sure you want to logout <span class="text-white font-medium">{authState.currentAccount?.username}</span>? + </p> + <div class="flex gap-3 justify-end"> + <button + onclick={() => authState.cancelLogout()} + class="px-4 py-2 text-sm font-medium text-zinc-300 hover:text-white bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors" + > + Cancel + </button> + <button + onclick={() => authState.confirmLogout()} + class="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-500 rounded-lg transition-colors" + > + Logout + </button> + </div> + </div> + </div> + {/if} + {#if uiState.showConsole} <div class="fixed inset-0 z-[100] bg-black/80 backdrop-blur-sm flex items-center justify-center p-8"> <div class="w-full h-full max-w-6xl max-h-[85vh] bg-[#1e1e1e] rounded-lg overflow-hidden border border-zinc-700 shadow-2xl relative flex flex-col"> diff --git a/ui/src/components/BottomBar.svelte b/ui/src/components/BottomBar.svelte index abb0b23..9dcb9ac 100644 --- a/ui/src/components/BottomBar.svelte +++ b/ui/src/components/BottomBar.svelte @@ -67,12 +67,23 @@ {authState.currentAccount ? authState.currentAccount.username : "Login Account"} </div> <div class="text-[10px] uppercase tracking-wider dark:text-zinc-500 text-gray-500 flex items-center gap-2"> - <span - class="w-1.5 h-1.5 rounded-full {authState.currentAccount - ? 'bg-emerald-500' - : 'bg-zinc-400'}" - ></span> - {authState.currentAccount ? "Online" : "Guest"} + {#if authState.currentAccount} + {#if authState.currentAccount.type === "Microsoft"} + {#if authState.currentAccount.expires_at && authState.currentAccount.expires_at * 1000 < Date.now()} + <span class="w-1.5 h-1.5 rounded-full bg-red-500"></span> + <span class="text-red-400">Expired</span> + {:else} + <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span> + Online + {/if} + {:else} + <span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span> + Offline + {/if} + {:else} + <span class="w-1.5 h-1.5 rounded-full bg-zinc-400"></span> + Guest + {/if} </div> </div> </div> diff --git a/ui/src/stores/auth.svelte.ts b/ui/src/stores/auth.svelte.ts index 3d58245..eb9dccd 100644 --- a/ui/src/stores/auth.svelte.ts +++ b/ui/src/stores/auth.svelte.ts @@ -1,11 +1,14 @@ import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-shell"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import type { Account, DeviceCodeResponse } from "../types"; import { uiState } from "./ui.svelte"; +import { logsState } from "./logs.svelte"; export class AuthState { currentAccount = $state<Account | null>(null); isLoginModalOpen = $state(false); + isLogoutConfirmOpen = $state(false); loginMode = $state<"select" | "offline" | "microsoft">("select"); offlineUsername = $state(""); deviceCodeData = $state<DeviceCodeResponse | null>(null); @@ -14,6 +17,7 @@ export class AuthState { private pollInterval: ReturnType<typeof setInterval> | null = null; private isPollingRequestActive = false; + private authProgressUnlisten: UnlistenFn | null = null; async checkAccount() { try { @@ -26,15 +30,29 @@ export class AuthState { openLoginModal() { if (this.currentAccount) { - if (confirm("Logout " + this.currentAccount.username + "?")) { - invoke("logout").then(() => (this.currentAccount = null)); - } + // Show custom logout confirmation dialog + this.isLogoutConfirmOpen = true; return; } this.resetLoginState(); this.isLoginModalOpen = true; } + cancelLogout() { + this.isLogoutConfirmOpen = false; + } + + async confirmLogout() { + this.isLogoutConfirmOpen = false; + try { + await invoke("logout"); + this.currentAccount = null; + uiState.setStatus("Logged out successfully"); + } catch (e) { + console.error("Logout failed:", e); + } + } + closeLoginModal() { this.stopPolling(); this.isLoginModalOpen = false; @@ -65,6 +83,9 @@ export class AuthState { this.msLoginStatus = "Waiting for authorization..."; this.stopPolling(); + // Setup auth progress listener + this.setupAuthProgressListener(); + try { this.deviceCodeData = (await invoke( "start_microsoft_login" @@ -78,6 +99,7 @@ export class AuthState { } open(this.deviceCodeData.verification_uri); + logsState.addLog("info", "Auth", "Microsoft login started, waiting for browser authorization..."); console.log("Starting polling for token..."); const intervalMs = (this.deviceCodeData.interval || 5) * 1000; @@ -87,6 +109,7 @@ export class AuthState { ); } } catch (e) { + logsState.addLog("error", "Auth", `Failed to start Microsoft login: ${e}`); alert("Failed to start Microsoft login: " + e); this.loginMode = "select"; } finally { @@ -94,6 +117,27 @@ export class AuthState { } } + private async setupAuthProgressListener() { + // Clean up previous listener if exists + if (this.authProgressUnlisten) { + this.authProgressUnlisten(); + this.authProgressUnlisten = null; + } + + this.authProgressUnlisten = await listen<string>("auth-progress", (event) => { + const message = event.payload; + this.msLoginStatus = message; + logsState.addLog("info", "Auth", message); + }); + } + + private cleanupAuthListener() { + if (this.authProgressUnlisten) { + this.authProgressUnlisten(); + this.authProgressUnlisten = null; + } + } + stopPolling() { if (this.pollInterval) { clearInterval(this.pollInterval); @@ -113,7 +157,9 @@ export class AuthState { console.log("Login Successful!", this.currentAccount); this.stopPolling(); + this.cleanupAuthListener(); this.isLoginModalOpen = false; + logsState.addLog("info", "Auth", `Login successful! Welcome, ${this.currentAccount.username}`); uiState.setStatus("Welcome back, " + this.currentAccount.username); } catch (e: any) { const errStr = e.toString(); @@ -122,12 +168,14 @@ export class AuthState { } else { console.error("Polling Error:", errStr); this.msLoginStatus = "Error: " + errStr; + logsState.addLog("error", "Auth", `Login error: ${errStr}`); if ( errStr.includes("expired_token") || errStr.includes("access_denied") ) { this.stopPolling(); + this.cleanupAuthListener(); alert("Login failed: " + errStr); this.loginMode = "select"; } diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 09a7d5e..0f02d64 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -12,6 +12,9 @@ export interface Account { type: "Offline" | "Microsoft"; username: string; uuid: string; + access_token?: string; + refresh_token?: string; + expires_at?: number; // Unix timestamp for Microsoft accounts } export interface DeviceCodeResponse { |