mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix(ui): align cross-platform chrome
This commit is contained in:
+1
-1
@@ -577,7 +577,7 @@ function App() {
|
||||
|
||||
return (
|
||||
<div className="app-shell flex h-screen w-screen overflow-hidden text-text-primary">
|
||||
{platform.os === 'windows' && <WindowControls />}
|
||||
{(platform.os === 'windows' || platform.os === 'linux') && <WindowControls />}
|
||||
<div
|
||||
className={`app-sidebar-shell relative z-20 shrink-0 transition-all duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] ${
|
||||
isSidebarVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
startActionLabel
|
||||
} from '../utils/downloadActions';
|
||||
import { isActiveDownloadStatus } from '../utils/downloads';
|
||||
import { usePlatformInfo } from '../utils/platform';
|
||||
|
||||
interface DownloadTableProps {
|
||||
filter: SidebarFilter;
|
||||
@@ -30,8 +31,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ 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<HTMLDivElement>();
|
||||
@@ -373,7 +376,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||
|
||||
return (
|
||||
<div className="downloads-view flex-1 flex flex-col h-full min-w-0">
|
||||
<div className={`main-titlebar ${!isSidebarVisible ? 'pl-[80px]' : ''}`} data-tauri-drag-region>
|
||||
<div
|
||||
className={`main-titlebar ${!isSidebarVisible ? (usesCustomWindowControls ? 'main-titlebar--custom-controls-collapsed' : 'pl-[88px]') : ''}`}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{!isSidebarVisible && (
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
@@ -647,7 +653,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
|
||||
>
|
||||
Show in Finder
|
||||
Show in Folder
|
||||
</button>
|
||||
|
||||
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||
|
||||
@@ -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<string | null>(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()}
|
||||
>
|
||||
<div className="px-5 py-4 border-b border-border-modal flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-500/10 rounded-full flex items-center justify-center">
|
||||
<div className="p-2 bg-blue-500/10 rounded-full items-center justify-center">
|
||||
<KeyRound size={20} className="text-blue-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-text-primary m-0">Credential Storage Access Needed</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary m-0">Credential Storage Access Needed</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="px-5 py-6 flex-1 text-sm text-text-secondary leading-relaxed space-y-4">
|
||||
<p>
|
||||
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}.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Note:</strong> 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.'}
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
<strong>Note:</strong> Firelink only writes its own dedicated credential entry. It cannot access other
|
||||
saved passwords or credential items on your system.
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 text-red-400 bg-red-400/10 p-3 rounded-lg border border-red-400/20 text-xs">
|
||||
<ShieldAlert size={14} className="mt-0.5 flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="bg-bg-modal-accent p-3 rounded-lg border border-border-modal text-xs">
|
||||
<strong>Hint:</strong> If you select Later, the extension will only work for this session.
|
||||
You can grant access anytime from <strong>Settings > Integrations</strong>.
|
||||
<strong>Hint:</strong> If you select Later, the extension will only work for this session.
|
||||
You can enable secure storage anytime from <strong>Settings > Integrations</strong>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="px-5 py-4 border-t border-border-modal flex justify-end gap-3 bg-bg-modal-accent">
|
||||
<button
|
||||
onClick={handleLater}
|
||||
@@ -92,7 +111,7 @@ export const KeychainPermissionModal: React.FC = () => {
|
||||
disabled={isGranting}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-accent text-white hover:bg-accent/90 disabled:opacity-50"
|
||||
>
|
||||
{isGranting ? 'Granting...' : 'Grant Access'}
|
||||
{isGranting ? 'Enabling...' : grantLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
export function WindowControls() {
|
||||
return (
|
||||
<div className="window-controls" aria-label="Window controls">
|
||||
<button
|
||||
type="button"
|
||||
className="window-control"
|
||||
title="Minimize"
|
||||
aria-label="Minimize"
|
||||
onClick={() => void appWindow.minimize()}
|
||||
>
|
||||
<Minus size={13} strokeWidth={2.25} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="window-control"
|
||||
title="Maximize"
|
||||
aria-label="Maximize"
|
||||
onClick={() => void appWindow.toggleMaximize()}
|
||||
>
|
||||
<Square size={11} strokeWidth={2.25} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="window-control close"
|
||||
title="Close"
|
||||
aria-label="Close"
|
||||
onClick={() => void appWindow.close()}
|
||||
onPointerDown={stopTitlebarDrag}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void appWindow.close();
|
||||
}}
|
||||
>
|
||||
<X size={14} strokeWidth={2.25} />
|
||||
<X size={10} strokeWidth={3} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="window-control minimize"
|
||||
title="Minimize"
|
||||
aria-label="Minimize"
|
||||
onPointerDown={stopTitlebarDrag}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void appWindow.minimize();
|
||||
}}
|
||||
>
|
||||
<Minus size={10} strokeWidth={3} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="window-control maximize"
|
||||
title="Maximize"
|
||||
aria-label="Maximize"
|
||||
onPointerDown={stopTitlebarDrag}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void appWindow.toggleMaximize();
|
||||
}}
|
||||
>
|
||||
<Square size={8} strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
+45
-18
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user