diff --git a/src/App.tsx b/src/App.tsx index 0fd460d..39af64c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -577,7 +577,7 @@ function App() { return (
- {platform.os === 'windows' && } + {(platform.os === 'windows' || platform.os === 'linux') && }
= ({ filter }) => { const { downloads, queues, assignToQueue, toggleAddModal, openDeleteModal, redownload } = useDownloadStore(); const { isSidebarVisible, toggleSidebar } = useSettingsStore(); const { addToast } = useToast(); + const platform = usePlatformInfo(); const isMac = navigator.userAgent.includes('Mac'); + const usesCustomWindowControls = !isMac && platform.os !== 'macos'; const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null); const [animationParent] = useAutoAnimate(); @@ -373,7 +376,10 @@ export const DownloadTable: React.FC = ({ filter }) => { return (
-
+
{!isSidebarVisible && (
diff --git a/src/components/KeychainPermissionModal.tsx b/src/components/KeychainPermissionModal.tsx index a22822a..db93fe2 100644 --- a/src/components/KeychainPermissionModal.tsx +++ b/src/components/KeychainPermissionModal.tsx @@ -2,10 +2,12 @@ import React, { useState } from 'react'; import { useSettingsStore } from '../store/useSettingsStore'; import { invokeCommand as invoke } from '../ipc'; import { KeyRound, ShieldAlert } from 'lucide-react'; +import { usePlatformInfo } from '../utils/platform'; export const KeychainPermissionModal: React.FC = () => { const showKeychainModal = useSettingsStore(state => state.showKeychainModal); const setShowKeychainModal = useSettingsStore(state => state.setShowKeychainModal); + const platform = usePlatformInfo(); const [isGranting, setIsGranting] = useState(false); const [error, setError] = useState(null); @@ -13,16 +15,26 @@ export const KeychainPermissionModal: React.FC = () => { return null; } + const isMac = platform.os === 'macos'; + const storeName = + platform.os === 'windows' + ? 'Windows Credential Manager' + : platform.os === 'linux' + ? 'your Linux credential store' + : platform.os === 'macos' + ? 'macOS Keychain' + : "this system's credential store"; + const grantLabel = isMac ? 'Grant Access' : 'Enable Secure Storage'; + const handleGrant = async () => { setIsGranting(true); setError(null); + try { const result = await invoke('grant_keychain_access'); if (result.persistent) { - // Set all keychain-related state directly from the grant result - // instead of calling hydratePairingToken() again, which would - // re-read the DB before Zustand's persist middleware has written - // keychainAccessGranted and could flip persistent back to false. + // Keep state in sync with the grant result instead of rehydrating + // before Zustand has persisted keychainAccessGranted. useSettingsStore.setState({ keychainAccessGranted: true, extensionPairingToken: result.token, @@ -30,7 +42,7 @@ export const KeychainPermissionModal: React.FC = () => { }); setShowKeychainModal(false); } else { - setError(result.error || 'Failed to grant keychain access.'); + setError(result.error || `${storeName} is unavailable.`); } } catch (e: any) { setError(e.toString()); @@ -50,35 +62,42 @@ export const KeychainPermissionModal: React.FC = () => { onClick={(e) => e.stopPropagation()} >
-
+
-

Credential Storage Access Needed

+

Credential Storage Access Needed

- +

- Firelink uses the browser extension to seamlessly capture downloads. - To securely store the pairing token that connects the app and the extension, - we need access to this system's credential store. + Firelink uses the browser extension to capture downloads. To keep the extension paired after restarts, + Firelink stores its pairing token in {storeName}.

+

- Note: Firelink only requests access to its own dedicated credential entry. It cannot and will not access other saved passwords or credential items on your system. + {isMac + ? 'macOS may show a Keychain prompt after you grant access.' + : 'This usually completes silently. If the credential service is unavailable, Firelink will show the error here and the extension will stay paired for this session only.'}

- + +

+ Note: Firelink only writes its own dedicated credential entry. It cannot access other + saved passwords or credential items on your system. +

+ {error && (
{error}
)} - +
- Hint: If you select Later, the extension will only work for this session. - You can grant access anytime from Settings > Integrations. + Hint: If you select Later, the extension will only work for this session. + You can enable secure storage anytime from Settings > Integrations.
- +
diff --git a/src/components/WindowControls.tsx b/src/components/WindowControls.tsx index 4de45da..d790620 100644 --- a/src/components/WindowControls.tsx +++ b/src/components/WindowControls.tsx @@ -1,37 +1,54 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; import { Minus, Square, X } from 'lucide-react'; +import type { PointerEvent } from 'react'; const appWindow = getCurrentWindow(); +const stopTitlebarDrag = (event: PointerEvent) => { + event.stopPropagation(); +}; + export function WindowControls() { return (
- - + +
); diff --git a/src/index.css b/src/index.css index a794a7f..ceb3d24 100644 --- a/src/index.css +++ b/src/index.css @@ -1285,46 +1285,73 @@ .window-controls { position: fixed; - top: 10px; - left: 12px; + top: 15px; + left: 22px; z-index: 60; display: inline-flex; align-items: center; - gap: 7px; + gap: 8px; + -webkit-app-region: no-drag; + pointer-events: auto; } .window-control { - width: 22px; - height: 22px; + width: 14px; + height: 14px; display: inline-flex; align-items: center; justify-content: center; - color: hsl(var(--text-muted)); + color: hsl(0 0% 10% / 0); border-radius: 999px; + border: 0.5px solid hsl(0 0% 0% / 0.28); + box-shadow: + inset 0 1px 0 hsl(0 0% 100% / 0.26), + 0 1px 2px hsl(0 0% 0% / 0.16); + transition: + filter 120ms ease, + color 120ms ease, + transform 120ms ease; + -webkit-app-region: no-drag; +} + +.window-control.close { + background: #ff5f57; +} + +.window-control.minimize { + background: #ffbd2e; +} + +.window-control.maximize { + background: #28c840; } .window-control:hover { - color: hsl(var(--text-primary)); - background: hsl(var(--item-hover)); + color: hsl(0 0% 8% / 0.68); + filter: saturate(1.14) brightness(1.05); } -.window-control.close:hover { - color: white; - background: #c42b1c; +.window-control:active { + transform: scale(0.92); + filter: brightness(0.9); } .main-titlebar { height: 52px; display: flex; - align-items: center; - padding: 0 18px; + align-items: center; + padding: 0 18px; border-bottom: 1px solid hsl(var(--border-color)); - background: hsl(var(--statusbar-bg)); - } + background: hsl(var(--statusbar-bg)); +} - .main-titlebar-title { - font-size: 14px; - font-weight: 700; +.main-titlebar--custom-controls-collapsed { + padding-left: 124px; +} + +.main-titlebar-title { + font-size: 14px; + font-weight: 700; color: hsl(var(--text-primary)); letter-spacing: -0.01em; }