fix(ui): harden table drag and keychain startup

This commit is contained in:
NimBold
2026-07-22 11:54:56 +03:30
parent 16377aa0d6
commit 297f2c08fb
8 changed files with 231 additions and 116 deletions
+9
View File
@@ -2,6 +2,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -41,6 +42,14 @@ function runNpmScript(script) {
run('npm', ['run', script]);
}
// A packaged Tauri artifact must have its own consent identity. A source or
// commit fingerprint cannot distinguish two fresh signed/package builds made
// from the same checkout, which would let an updated binary silently reuse the
// previous binary's credential-store approval. Keep any configured identity
// as useful provenance, but always add the artifact nonce.
const configuredBuildId = process.env.VITE_BUILD_ID?.trim();
process.env.VITE_BUILD_ID = `${configuredBuildId || 'artifact'}-${randomUUID()}`;
run(process.execPath, ['scripts/stage-engines.js']);
run(process.execPath, ['scripts/verify-binaries.js', '--staged']);
+7 -7
View File
@@ -3,6 +3,7 @@ import { schedulerCompletionState } from './utils/schedulerCompletion';
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
import { Sidebar, SidebarFilter } from "./components/Sidebar";
import { DownloadTable } from "./components/DownloadTable";
import { KeychainPermissionModal } from './components/KeychainPermissionModal';
import { extractValidDownloadUrls } from './utils/url';
import { readClipboardDownloadUrls } from './utils/clipboard';
import { listenEvent as listen, invokeCommand as invoke } from "./ipc";
@@ -52,10 +53,6 @@ const PropertiesModal = lazy(() => import('./components/PropertiesModal').then(m
const DeleteConfirmationModal = lazy(() => import('./components/DeleteConfirmationModal').then(module => ({
default: module.DeleteConfirmationModal,
})));
const KeychainPermissionModal = lazy(() => import('./components/KeychainPermissionModal').then(module => ({
default: module.KeychainPermissionModal,
})));
const preloadPageChunks = async () => {
for (const load of pageChunkLoaders) {
try {
@@ -500,14 +497,17 @@ function App() {
let changed = false;
if (deferKeychainHydration) {
settings.setKeychainAccessReady(false);
if (showKeychainPrompt) {
// Commit the explanation before the harmless session-token IPC so
// a slow startup cannot leave the user facing an unexplained
// credential-store request.
settings.setShowKeychainModal(true);
}
// This token is already owned by the backend and does not access
// the OS credential store. Render our explanation before any native
// Keychain/Credential Manager prompt can be user-triggered.
await settings.hydrateSessionPairingToken(isStartupActive);
if (!active) return;
if (showKeychainPrompt) {
settings.setShowKeychainModal(true);
}
} else {
// The backend keeps credential-store access disabled for every new
// process. Arm it only after the persisted startup decision has
+6 -9
View File
@@ -98,13 +98,10 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
gridColumn: getColumnGridColumn(key, columnOrder),
} as React.CSSProperties);
const trailingColumnClass = (key: DownloadTableColumnKey) =>
key === columnOrder[columnOrder.length - 1] ? 'download-column-cell--trailing' : '';
const cells: Record<DownloadTableColumnKey, React.ReactNode> = {
'File Name': (
<div
className={`download-column-cell download-file-cell download-column-file-name ${trailingColumnClass('File Name')}`}
className="download-column-cell download-file-cell download-column-file-name"
style={columnStyle('File Name')}
>
<div className="download-cell-content">
@@ -119,7 +116,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
),
Size: (
<div
className={`download-column-cell download-cell-truncate download-size-cell tabular-nums ${trailingColumnClass('Size')}`}
className="download-column-cell download-cell-truncate download-size-cell tabular-nums"
style={columnStyle('Size')}
title={hasDownloadedAmount ? downloadedSizeLabel : completedSizeLabel}
aria-label={hasDownloadedAmount ? downloadedSizeLabel : completedSizeLabel}
@@ -141,7 +138,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
),
Status: (
<div
className={`download-column-cell download-status-cell ${trailingColumnClass('Status')}`}
className="download-column-cell download-status-cell"
style={columnStyle('Status')}
>
{download.status === 'completed' ? (
@@ -202,21 +199,21 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
</div>
),
Speed: (
<div className={`download-column-cell download-cell-truncate ${trailingColumnClass('Speed')}`} style={columnStyle('Speed')}>
<div className="download-column-cell download-cell-truncate" style={columnStyle('Speed')}>
<span className="download-cell-content tabular-nums" title={displaySpeed}>
{displaySpeed}
</span>
</div>
),
ETA: (
<div className={`download-column-cell download-cell-truncate ${trailingColumnClass('ETA')}`} style={columnStyle('ETA')}>
<div className="download-column-cell download-cell-truncate" style={columnStyle('ETA')}>
<span className="download-cell-content tabular-nums" title={displayEta}>
{displayEta}
</span>
</div>
),
'Date Added': (
<div className={`download-column-cell download-cell-right download-column-date-added ${trailingColumnClass('Date Added')}`} style={columnStyle('Date Added')}>
<div className="download-column-cell download-cell-right download-column-date-added" style={columnStyle('Date Added')}>
<span
className="download-cell-content download-date-value tabular-nums"
title={download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
+196 -82
View File
@@ -37,6 +37,7 @@ import {
DEFAULT_COLUMN_ALIGNMENTS,
DEFAULT_COLUMN_ORDER,
DEFAULT_COLUMN_WIDTHS,
DOWNLOAD_ACTIONS_COLUMN_WIDTH,
buildColumnGridTemplate,
columnIndex,
getColumnGridColumn,
@@ -92,6 +93,11 @@ interface ColumnMenuState {
y: number;
}
interface ColumnLayoutBounds {
left: number;
right: number;
}
export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const { t } = useTranslation();
const { downloads, queues, assignToQueue, openDeleteModal, redownload, moveInQueue } = useDownloadStore();
@@ -110,7 +116,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
const [animationParent] = useAutoAnimate<HTMLDivElement>();
const [headerAnimationParent] = useAutoAnimate<HTMLDivElement>({
const [headerAnimationParent, setHeaderAnimationEnabled] = useAutoAnimate<HTMLDivElement>({
duration: 140,
easing: 'ease-out',
});
@@ -128,8 +134,17 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
} | null>(null);
const headerElementsRef = useRef(new Map<DownloadTableColumnKey, HTMLDivElement>());
const headerContainerRef = useRef<HTMLDivElement>(null);
const headerRef = useCallback((element: HTMLDivElement | null) => {
headerContainerRef.current = element;
headerAnimationParent(element);
}, [headerAnimationParent]);
const columnDragStateRef = useRef<ColumnDragState | null>(null);
const columnDragOrderRef = useRef<DownloadTableColumnKey[] | null>(null);
const columnDragLayoutRef = useRef<Map<DownloadTableColumnKey, ColumnLayoutBounds> | null>(null);
const columnDragTargetRef = useRef<HTMLDivElement | null>(null);
const columnDragCaptureTargetRef = useRef<HTMLDivElement | null>(null);
const columnDragCapturePointerIdRef = useRef<number | null>(null);
const columnDragCleanupRef = useRef<(() => void) | null>(null);
const columnDropFlashTimerRef = useRef<number | null>(null);
const suppressHeaderClickRef = useRef(false);
const selectedIdsRef = useRef(selectedIds);
@@ -166,17 +181,14 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const [, setMenuViewportVersion] = useState(0);
const columnWidthsRef = useRef(columnWidths);
const columnOrderRef = useRef(columnOrder);
columnWidthsRef.current = columnWidths;
columnOrderRef.current = columnOrder;
columnDragOrderRef.current = columnDragOrder;
const normalizedColumnWidths = columnWidths.map((width, index) =>
const normalizedColumnWidths = columnWidthsRef.current.map((width, index) =>
Math.max(COLUMN_MINIMUMS[index], width)
);
const orderedColumns = columnDragOrder ?? columnOrder;
const orderedColumns = columnDragOrderRef.current ?? columnDragOrder ?? columnOrder;
const tableGridTemplate = buildColumnGridTemplate(orderedColumns, normalizedColumnWidths);
const tableMinWidth = normalizedColumnWidths.reduce(
(total, width) => total + width,
0
DOWNLOAD_ACTIONS_COLUMN_WIDTH
);
const tableMinWidthWithPadding = 'calc(' + tableMinWidth + 'px + var(--download-row-padding-x) + var(--download-row-padding-x))';
const downloadsViewRef = useRef<HTMLDivElement>(null);
@@ -212,6 +224,15 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
};
};
const captureColumnLayout = (order: DownloadTableColumnKey[]) => {
const layout = new Map<DownloadTableColumnKey, ColumnLayoutBounds>();
for (const key of order) {
const element = headerElementsRef.current.get(key);
if (element) layout.set(key, getColumnLayoutBounds(element));
}
return layout;
};
const getColumnDropPosition = (
key: DownloadTableColumnKey,
pointerX: number,
@@ -221,7 +242,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const targetIndex = remainingColumns.findIndex(columnKey => {
const element = headerElementsRef.current.get(columnKey);
if (!element) return false;
const bounds = getColumnLayoutBounds(element);
const bounds = columnDragLayoutRef.current?.get(columnKey) ?? getColumnLayoutBounds(element);
return pointerX < bounds.left + (bounds.right - bounds.left) / 2;
});
const dropIndex = targetIndex === -1 ? remainingColumns.length : targetIndex;
@@ -235,78 +256,26 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
return { dropIndex, markerX };
};
const handleColumnPointerDown = (key: DownloadTableColumnKey, event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
if (event.target instanceof Element && event.target.closest('.column-resize-handle, .download-column-options')) return;
event.currentTarget.setPointerCapture(event.pointerId);
dragGestureRef.current = {
key,
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
active: false,
};
};
const handleColumnPointerMove = (key: DownloadTableColumnKey, event: React.PointerEvent<HTMLDivElement>) => {
const finishColumnDrag = (key: DownloadTableColumnKey, pointerId: number, cancelled = false) => {
const gesture = dragGestureRef.current;
if (!gesture || gesture.key !== key || gesture.pointerId !== event.pointerId) return;
const distance = Math.hypot(event.clientX - gesture.startX, event.clientY - gesture.startY);
if (!gesture.active) {
if (distance < 5) return;
gesture.active = true;
suppressHeaderClickRef.current = true;
const rect = event.currentTarget.getBoundingClientRect();
const currentOrder = columnOrderRef.current;
columnDragOrderRef.current = [...currentOrder];
setColumnDragOrder([...currentOrder]);
const { dropIndex, markerX } = getColumnDropPosition(key, event.clientX, currentOrder);
updateColumnDragState({
key,
startX: gesture.startX,
pointerX: event.clientX,
offsetX: event.clientX - rect.left,
top: rect.top,
width: rect.width,
dropIndex,
markerX,
});
} else {
const current = columnDragStateRef.current;
if (!current) return;
const currentOrder = columnDragOrderRef.current ?? columnOrderRef.current;
const { dropIndex, markerX } = getColumnDropPosition(key, event.clientX, currentOrder);
const nextOrder = reorderColumn(currentOrder, key, dropIndex);
const orderChanged = nextOrder.some((columnKey, index) => columnKey !== currentOrder[index]);
if (orderChanged) {
columnDragOrderRef.current = nextOrder;
setColumnDragOrder(nextOrder);
}
updateColumnDragState({
...current,
pointerX: event.clientX,
dropIndex: nextOrder.indexOf(key),
markerX,
});
}
event.preventDefault();
};
const finishColumnDrag = (key: DownloadTableColumnKey, event: React.PointerEvent<HTMLDivElement>, cancelled = false) => {
const gesture = dragGestureRef.current;
if (!gesture || gesture.key !== key || gesture.pointerId !== event.pointerId) return;
if (!gesture || gesture.key !== key || gesture.pointerId !== pointerId) return;
const current = columnDragStateRef.current;
dragGestureRef.current = null;
updateColumnDragState(null);
const nextOrder = columnDragOrderRef.current ?? columnOrderRef.current;
const captureTarget = columnDragCaptureTargetRef.current;
dragGestureRef.current = null;
columnDragTargetRef.current = null;
columnDragCaptureTargetRef.current = null;
columnDragCapturePointerIdRef.current = null;
columnDragOrderRef.current = null;
columnDragLayoutRef.current = null;
columnDragCleanupRef.current?.();
columnDragCleanupRef.current = null;
updateColumnDragState(null);
setColumnDragOrder(null);
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
setHeaderAnimationEnabled(true);
if (captureTarget?.hasPointerCapture(pointerId)) {
captureTarget.releasePointerCapture(pointerId);
}
if (cancelled || !gesture.active || !current) {
suppressHeaderClickRef.current = false;
@@ -318,6 +287,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
if (nextOrder.join('|') === columnOrderRef.current.join('|')) return;
columnOrderRef.current = nextOrder;
setColumnOrder(nextOrder);
persistColumnOrder(nextOrder);
if (columnDropFlashTimerRef.current !== null) {
@@ -330,6 +300,134 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
}, 260);
};
const handleColumnPointerMove = (
key: DownloadTableColumnKey,
pointerId: number,
clientX: number,
clientY: number,
preventDefault?: () => void
) => {
const gesture = dragGestureRef.current;
if (!gesture || gesture.key !== key || gesture.pointerId !== pointerId) return;
const distance = Math.hypot(clientX - gesture.startX, clientY - gesture.startY);
if (!gesture.active) {
if (distance < 5) return;
gesture.active = true;
suppressHeaderClickRef.current = true;
const target = columnDragTargetRef.current;
const rect = target?.getBoundingClientRect();
if (!rect) return;
// The header children are reordered in the live grid while dragging.
// AutoAnimate's MutationObserver must not animate the same structural
// changes that drive pointer hit-testing; it is restored on completion.
setHeaderAnimationEnabled(false);
const currentOrder = columnOrderRef.current;
columnDragLayoutRef.current = captureColumnLayout(currentOrder);
columnDragOrderRef.current = [...currentOrder];
setColumnDragOrder([...currentOrder]);
const { dropIndex, markerX } = getColumnDropPosition(key, clientX, currentOrder);
updateColumnDragState({
key,
startX: gesture.startX,
pointerX: clientX,
offsetX: clientX - rect.left,
top: rect.top,
width: rect.width,
dropIndex,
markerX,
});
} else {
const current = columnDragStateRef.current;
if (!current) return;
const currentOrder = columnDragOrderRef.current ?? columnOrderRef.current;
const { dropIndex, markerX } = getColumnDropPosition(key, clientX, currentOrder);
const nextOrder = reorderColumn(currentOrder, key, dropIndex);
const orderChanged = nextOrder.some((columnKey, index) => columnKey !== currentOrder[index]);
if (orderChanged) {
columnDragOrderRef.current = nextOrder;
setColumnDragOrder(nextOrder);
}
updateColumnDragState({
...current,
pointerX: clientX,
dropIndex: nextOrder.indexOf(key),
markerX,
});
}
preventDefault?.();
};
const handleColumnPointerDown = (key: DownloadTableColumnKey, event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
if (event.target instanceof Element && event.target.closest('.column-resize-handle, .download-column-options')) return;
const existingGesture = dragGestureRef.current;
if (existingGesture) {
finishColumnDrag(existingGesture.key, existingGesture.pointerId, true);
}
const target = event.currentTarget;
const pointerId = event.pointerId;
const captureTarget = headerContainerRef.current;
columnDragTargetRef.current = target;
columnDragCaptureTargetRef.current = captureTarget;
columnDragCapturePointerIdRef.current = pointerId;
if (captureTarget) {
try {
// Capture on the stable grid parent, never on a column that is
// structurally moved during the live reorder.
captureTarget.setPointerCapture(pointerId);
} catch {
// Window listeners below still handle browsers that reject capture
// after the pointer has already been cancelled.
}
}
columnDragLayoutRef.current = null;
dragGestureRef.current = {
key,
pointerId,
startX: event.clientX,
startY: event.clientY,
active: false,
};
const pointerMove = (pointerEvent: PointerEvent) => {
if (pointerEvent.pointerId !== pointerId) return;
handleColumnPointerMove(
key,
pointerEvent.pointerId,
pointerEvent.clientX,
pointerEvent.clientY,
() => pointerEvent.preventDefault()
);
};
const pointerUp = (pointerEvent: PointerEvent) => {
if (pointerEvent.pointerId === pointerId) {
finishColumnDrag(key, pointerEvent.pointerId);
}
};
const pointerCancel = (pointerEvent: PointerEvent) => {
if (pointerEvent.pointerId === pointerId) {
finishColumnDrag(key, pointerEvent.pointerId, true);
}
};
const lostPointerCapture = (pointerEvent: PointerEvent) => {
if (pointerEvent.pointerId === pointerId) {
finishColumnDrag(key, pointerEvent.pointerId, true);
}
};
window.addEventListener('pointermove', pointerMove);
window.addEventListener('pointerup', pointerUp);
window.addEventListener('pointercancel', pointerCancel);
captureTarget?.addEventListener('lostpointercapture', lostPointerCapture);
columnDragCleanupRef.current = () => {
window.removeEventListener('pointermove', pointerMove);
window.removeEventListener('pointerup', pointerUp);
window.removeEventListener('pointercancel', pointerCancel);
captureTarget?.removeEventListener('lostpointercapture', lostPointerCapture);
};
};
const startColumnResize = (key: DownloadTableColumnKey, event: React.PointerEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
@@ -411,6 +509,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const alignments = { ...DEFAULT_COLUMN_ALIGNMENTS };
setColumnWidths(widths);
columnWidthsRef.current = widths;
columnOrderRef.current = order;
setColumnOrder(order);
setColumnAlignments(alignments);
persistColumnWidths(widths);
@@ -440,6 +539,16 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
useEffect(() => () => {
resizeCleanupRef.current?.();
columnDragCleanupRef.current?.();
columnDragCleanupRef.current = null;
const captureTarget = columnDragCaptureTargetRef.current;
const capturePointerId = columnDragCapturePointerIdRef.current;
columnDragTargetRef.current = null;
columnDragCaptureTargetRef.current = null;
columnDragCapturePointerIdRef.current = null;
if (captureTarget && capturePointerId !== null && captureTarget.hasPointerCapture(capturePointerId)) {
captureTarget.releasePointerCapture(capturePointerId);
}
if (columnDropFlashTimerRef.current !== null) {
window.clearTimeout(columnDropFlashTimerRef.current);
}
@@ -448,9 +557,21 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
useEffect(() => {
const cancelColumnDrag = () => {
if (!dragGestureRef.current) return;
columnDragCleanupRef.current?.();
columnDragCleanupRef.current = null;
const captureTarget = columnDragCaptureTargetRef.current;
const capturePointerId = columnDragCapturePointerIdRef.current;
columnDragTargetRef.current = null;
columnDragCaptureTargetRef.current = null;
columnDragCapturePointerIdRef.current = null;
if (captureTarget && capturePointerId !== null && captureTarget.hasPointerCapture(capturePointerId)) {
captureTarget.releasePointerCapture(capturePointerId);
}
dragGestureRef.current = null;
columnDragStateRef.current = null;
columnDragOrderRef.current = null;
columnDragLayoutRef.current = null;
setHeaderAnimationEnabled(true);
setColumnDragState(null);
setColumnDragOrder(null);
suppressHeaderClickRef.current = false;
@@ -923,10 +1044,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="downloads-table flex-1 flex flex-col">
<div className="download-table-scroll">
<div
ref={element => {
headerContainerRef.current = element;
headerAnimationParent(element);
}}
ref={headerRef}
className={`download-table-header ${columnDragState ? 'is-column-dragging' : ''}`}
style={{ gridTemplateColumns: tableGridTemplate, minWidth: tableMinWidthWithPadding }}
>
@@ -958,10 +1076,6 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
openColumnMenu(key, event.clientX, event.clientY);
}}
onPointerDown={event => handleColumnPointerDown(key, event)}
onPointerMove={event => handleColumnPointerMove(key, event)}
onPointerUp={event => finishColumnDrag(key, event)}
onPointerCancel={event => finishColumnDrag(key, event, true)}
onLostPointerCapture={event => finishColumnDrag(key, event, true)}
>
<button
type="button"
+6 -14
View File
@@ -1927,6 +1927,7 @@ html[data-list-density="relaxed"] {
min-width: 0;
overflow: hidden;
padding-inline: var(--download-column-padding-x);
padding-right: calc(var(--download-column-padding-x) + 22px);
cursor: grab;
transition: background-color 120ms ease, opacity 120ms ease, box-shadow 120ms ease;
}
@@ -1937,7 +1938,6 @@ html[data-list-density="relaxed"] {
.download-column-header.is-dragging {
opacity: 0;
pointer-events: none;
}
.download-column-header.is-drop-flashing {
@@ -2126,10 +2126,6 @@ html[data-list-density="relaxed"] .download-ghost-row {
overflow: hidden;
}
.download-row:hover .download-column-cell--trailing .download-cell-content {
visibility: hidden;
}
.download-file-cell {
direction: ltr;
text-align: left;
@@ -2205,17 +2201,13 @@ html[data-list-density="relaxed"] .download-ghost-row {
}
.download-row-actions {
position: absolute;
top: 50%;
right: var(--download-row-padding-x);
left: auto;
grid-column: 8;
align-self: stretch;
align-items: center;
justify-content: flex-end;
z-index: 2;
transform: translateY(-50%);
padding-inline-start: 4px;
border-radius: 6px;
background: hsl(var(--surface-overlay) / 0.94);
max-width: 100%;
padding-inline: 4px;
padding: 0;
}
.download-column-menu {
+1 -1
View File
@@ -56,7 +56,7 @@ describe('download table column preferences', () => {
it('keeps user widths fixed while reserving a shared trailing spacer track', () => {
expect(buildColumnGridTemplate([...DEFAULT_COLUMN_ORDER], [...DEFAULT_COLUMN_WIDTHS])).toBe(
'340px 100px 220px 100px 80px minmax(0, 1fr) 170px'
'340px 100px 220px 100px 80px minmax(0, 1fr) 170px 32px'
);
expect(getColumnGridColumn('Date Added', [...DEFAULT_COLUMN_ORDER])).toBe('7');
expect(getColumnGridColumn('Date Added', ['Date Added', ...DEFAULT_COLUMN_ORDER.slice(0, -1)])).toBeUndefined();
+2
View File
@@ -14,6 +14,7 @@ export const DEFAULT_COLUMN_ORDER = [
export const DEFAULT_COLUMN_WIDTHS = [340, 100, 220, 100, 80, 170] as const;
export const COLUMN_MINIMUMS = [160, 58, 92, 58, 48, 144] as const;
export const DOWNLOAD_ACTIONS_COLUMN_WIDTH = 32;
export const COLUMN_WIDTHS_STORAGE_KEY = 'firelink-download-column-widths';
export const COLUMN_ORDER_STORAGE_KEY = 'firelink-download-column-order';
@@ -98,6 +99,7 @@ export const buildColumnGridTemplate = (
...orderedWidths.slice(0, -1).map(width => `${width}px`),
'minmax(0, 1fr)',
`${orderedWidths[orderedWidths.length - 1]}px`,
`${DOWNLOAD_ACTIONS_COLUMN_WIDTH}px`,
].join(' ');
};
+4 -3
View File
@@ -18,9 +18,10 @@ export type KeychainAccessReadiness = {
};
// The semantic app version can remain unchanged across release-candidate and
// packaging rebuilds. Use the build identity so an updated binary cannot skip
// Firelink's explanation and invoke the OS prompt directly. The policy epoch
// remains only as a safe fallback for builds created outside the Git checkout.
// packaging rebuilds. The Tauri packaging hook appends a fresh artifact nonce
// to the build identity, so replacing the binary cannot skip Firelink's
// explanation and invoke the OS prompt directly. The policy epoch remains
// only as a safe fallback for builds created outside the Git checkout.
const KEYCHAIN_CONSENT_POLICY_VERSION = '2';
const buildId = typeof import.meta.env.VITE_BUILD_ID === 'string'
? import.meta.env.VITE_BUILD_ID.trim()