mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-27 19:17:13 +00:00
@@ -14,7 +14,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
COLUMN_ALIGNMENT_JUSTIFY,
|
COLUMN_ALIGNMENT_JUSTIFY,
|
||||||
DOWNLOAD_ACTIONS_COLUMN_WIDTH,
|
DOWNLOAD_ACTIONS_COLUMN_WIDTH,
|
||||||
DOWNLOAD_ACTIONS_VIEWPORT_INSET,
|
getDownloadActionPosition,
|
||||||
getColumnGridColumn,
|
getColumnGridColumn,
|
||||||
type DownloadColumnAlignment,
|
type DownloadColumnAlignment,
|
||||||
type DownloadTableColumnKey
|
type DownloadTableColumnKey
|
||||||
@@ -64,7 +64,8 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
const [isRowHovered, setIsRowHovered] = React.useState(false);
|
const [isRowHovered, setIsRowHovered] = React.useState(false);
|
||||||
const [isRowFocused, setIsRowFocused] = React.useState(false);
|
const [isRowFocused, setIsRowFocused] = React.useState(false);
|
||||||
const [actionPosition, setActionPosition] = React.useState<React.CSSProperties | undefined>();
|
const [actionPosition, setActionPosition] = React.useState<React.CSSProperties | undefined>();
|
||||||
const isActionVisible = isRowHovered || isRowFocused;
|
const hasRowActions = download.status !== 'completed';
|
||||||
|
const isActionVisible = hasRowActions && (isRowHovered || isRowFocused);
|
||||||
const mediaQualityLabel = (() => {
|
const mediaQualityLabel = (() => {
|
||||||
if (!download.isMedia || typeof download.mediaQuality !== 'string') return undefined;
|
if (!download.isMedia || typeof download.mediaQuality !== 'string') return undefined;
|
||||||
const normalized = download.mediaQuality.replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim();
|
const normalized = download.mediaQuality.replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
@@ -87,19 +88,12 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
const rowRect = row.getBoundingClientRect();
|
const rowRect = row.getBoundingClientRect();
|
||||||
const horizontalViewportRect = horizontalViewport.getBoundingClientRect();
|
const horizontalViewportRect = horizontalViewport.getBoundingClientRect();
|
||||||
const verticalViewportRect = verticalViewport.getBoundingClientRect();
|
const verticalViewportRect = verticalViewport.getBoundingClientRect();
|
||||||
const viewportTolerance = 1;
|
const nextPosition = getDownloadActionPosition(
|
||||||
const visibleTop = Math.max(rowRect.top, verticalViewportRect.top);
|
rowRect,
|
||||||
const visibleBottom = Math.min(rowRect.bottom, verticalViewportRect.bottom);
|
horizontalViewportRect,
|
||||||
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
verticalViewportRect,
|
||||||
const isVisible = visibleHeight > viewportTolerance;
|
window.innerWidth
|
||||||
const actionHeight = Math.min(rowRect.height, visibleHeight);
|
);
|
||||||
const nextPosition: React.CSSProperties = {
|
|
||||||
top: visibleTop,
|
|
||||||
right: Math.max(0, window.innerWidth - horizontalViewportRect.right) + DOWNLOAD_ACTIONS_VIEWPORT_INSET,
|
|
||||||
height: actionHeight,
|
|
||||||
overflow: actionHeight < rowRect.height ? 'hidden' : 'visible',
|
|
||||||
visibility: isVisible ? 'visible' : 'hidden',
|
|
||||||
};
|
|
||||||
|
|
||||||
setActionPosition(previous => (
|
setActionPosition(previous => (
|
||||||
previous?.top === nextPosition.top &&
|
previous?.top === nextPosition.top &&
|
||||||
@@ -115,11 +109,11 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isActionVisible) return;
|
if (!isActionVisible) return;
|
||||||
|
|
||||||
let frame = 0;
|
let frame: number | null = null;
|
||||||
const schedulePositionUpdate = () => {
|
const schedulePositionUpdate = () => {
|
||||||
if (frame) window.cancelAnimationFrame(frame);
|
if (frame !== null) window.cancelAnimationFrame(frame);
|
||||||
frame = window.requestAnimationFrame(() => {
|
frame = window.requestAnimationFrame(() => {
|
||||||
frame = 0;
|
frame = null;
|
||||||
updateActionPosition();
|
updateActionPosition();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -143,7 +137,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
window.removeEventListener('resize', schedulePositionUpdate);
|
window.removeEventListener('resize', schedulePositionUpdate);
|
||||||
window.removeEventListener('scroll', schedulePositionUpdate, true);
|
window.removeEventListener('scroll', schedulePositionUpdate, true);
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
if (frame) window.cancelAnimationFrame(frame);
|
if (frame !== null) window.cancelAnimationFrame(frame);
|
||||||
};
|
};
|
||||||
}, [isActionVisible, updateActionPosition]);
|
}, [isActionVisible, updateActionPosition]);
|
||||||
|
|
||||||
@@ -325,11 +319,12 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowActions = (
|
const rowActions = hasRowActions ? (
|
||||||
<div
|
<div
|
||||||
className="download-row-actions items-center gap-0.5"
|
className="download-row-actions items-center gap-0.5"
|
||||||
style={{
|
style={{
|
||||||
...actionPosition,
|
...actionPosition,
|
||||||
|
visibility: isActionVisible && actionPosition?.visibility === 'visible' ? 'visible' : 'hidden',
|
||||||
width: `${DOWNLOAD_ACTIONS_COLUMN_WIDTH}px`,
|
width: `${DOWNLOAD_ACTIONS_COLUMN_WIDTH}px`,
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@@ -356,7 +351,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
<MoreVertical size={14} />
|
<MoreVertical size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -367,7 +362,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setIsRowHovered(true);
|
setIsRowHovered(true);
|
||||||
updateActionPosition();
|
if (hasRowActions) updateActionPosition();
|
||||||
}}
|
}}
|
||||||
onMouseLeave={event => {
|
onMouseLeave={event => {
|
||||||
const nextTarget = event.relatedTarget;
|
const nextTarget = event.relatedTarget;
|
||||||
@@ -377,7 +372,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
|||||||
}}
|
}}
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
setIsRowFocused(true);
|
setIsRowFocused(true);
|
||||||
updateActionPosition();
|
if (hasRowActions) updateActionPosition();
|
||||||
}}
|
}}
|
||||||
onBlur={event => {
|
onBlur={event => {
|
||||||
const nextTarget = event.relatedTarget;
|
const nextTarget = event.relatedTarget;
|
||||||
|
|||||||
+6
-9
@@ -2789,16 +2789,18 @@ body.is-queue-dragging * {
|
|||||||
|
|
||||||
.download-row-actions {
|
.download-row-actions {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-self: stretch;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0;
|
padding: 0 2px;
|
||||||
background: transparent;
|
border: 1px solid hsl(var(--border-color) / 0.88);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: hsl(var(--surface-raised));
|
||||||
|
box-shadow: 0 1px 2px hsl(var(--shadow-color));
|
||||||
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 120ms ease;
|
transition: opacity 120ms ease;
|
||||||
@@ -2809,11 +2811,6 @@ body.is-queue-dragging * {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-row > .download-row-actions {
|
|
||||||
overflow: visible;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-row-actions .app-icon-button,
|
.download-row-actions .app-icon-button,
|
||||||
.download-row-actions .app-icon-button:hover:not(:disabled),
|
.download-row-actions .app-icon-button:hover:not(:disabled),
|
||||||
.download-row-actions .app-icon-button:active:not(:disabled) {
|
.download-row-actions .app-icon-button:active:not(:disabled) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
DEFAULT_COLUMN_ORDER,
|
DEFAULT_COLUMN_ORDER,
|
||||||
DEFAULT_COLUMN_WIDTHS,
|
DEFAULT_COLUMN_WIDTHS,
|
||||||
buildColumnGridTemplate,
|
buildColumnGridTemplate,
|
||||||
|
getDownloadActionPosition,
|
||||||
getColumnGridColumn,
|
getColumnGridColumn,
|
||||||
normalizeColumnAlignments,
|
normalizeColumnAlignments,
|
||||||
normalizeColumnOrder,
|
normalizeColumnOrder,
|
||||||
@@ -54,12 +55,43 @@ describe('download table column preferences', () => {
|
|||||||
expect(normalizeColumnAlignments(null)).toEqual(DEFAULT_COLUMN_ALIGNMENTS);
|
expect(normalizeColumnAlignments(null)).toEqual(DEFAULT_COLUMN_ALIGNMENTS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps user widths fixed while reserving a shared spacer before the final column', () => {
|
it('keeps every data column fixed while reserving a flexible fill track after them', () => {
|
||||||
expect(buildColumnGridTemplate([...DEFAULT_COLUMN_ORDER], [...DEFAULT_COLUMN_WIDTHS])).toBe(
|
expect(buildColumnGridTemplate([...DEFAULT_COLUMN_ORDER], [...DEFAULT_COLUMN_WIDTHS])).toBe(
|
||||||
'340px 100px 220px 100px 80px minmax(0, 1fr) 170px'
|
'340px 100px 220px 100px 80px 170px minmax(0, 1fr)'
|
||||||
);
|
);
|
||||||
expect(getColumnGridColumn('Date Added', [...DEFAULT_COLUMN_ORDER])).toBe('7');
|
expect(getColumnGridColumn('Date Added', [...DEFAULT_COLUMN_ORDER])).toBe('6');
|
||||||
expect(getColumnGridColumn('Date Added', ['Date Added', ...DEFAULT_COLUMN_ORDER.slice(0, -1)])).toBeUndefined();
|
expect(getColumnGridColumn('Date Added', ['Date Added', ...DEFAULT_COLUMN_ORDER.slice(0, -1)])).toBeUndefined();
|
||||||
expect(getColumnGridColumn('ETA', ['Date Added', 'File Name', 'Size', 'Status', 'Speed', 'ETA'])).toBe('7');
|
expect(getColumnGridColumn('ETA', ['Date Added', 'File Name', 'Size', 'Status', 'Speed', 'ETA'])).toBe('6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps row actions inside the visible row edge while preserving viewport anchoring for clipped rows', () => {
|
||||||
|
const viewport = { top: 0, right: 800, bottom: 600, left: 0 };
|
||||||
|
expect(getDownloadActionPosition(
|
||||||
|
{ top: 40, right: 1000, bottom: 72, left: -200 },
|
||||||
|
viewport,
|
||||||
|
viewport,
|
||||||
|
800
|
||||||
|
)).toMatchObject({
|
||||||
|
right: 8,
|
||||||
|
height: 30,
|
||||||
|
visibility: 'visible',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getDownloadActionPosition(
|
||||||
|
{ top: 40, right: 400, bottom: 72, left: 100 },
|
||||||
|
viewport,
|
||||||
|
viewport,
|
||||||
|
800
|
||||||
|
)).toMatchObject({
|
||||||
|
right: 408,
|
||||||
|
visibility: 'visible',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getDownloadActionPosition(
|
||||||
|
{ top: 40, right: -20, bottom: 72, left: -300 },
|
||||||
|
viewport,
|
||||||
|
viewport,
|
||||||
|
800
|
||||||
|
).visibility).toBe('hidden');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,10 +14,53 @@ export const DEFAULT_COLUMN_ORDER = [
|
|||||||
|
|
||||||
export const DEFAULT_COLUMN_WIDTHS = [340, 100, 220, 100, 80, 170] as const;
|
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 COLUMN_MINIMUMS = [160, 58, 92, 58, 48, 144] as const;
|
||||||
// Width of the fixed action rail shown while hovering a row.
|
// Width of the compact action rail shown while hovering or focusing a row.
|
||||||
export const DOWNLOAD_ACTIONS_COLUMN_WIDTH = 84;
|
export const DOWNLOAD_ACTIONS_COLUMN_WIDTH = 64;
|
||||||
// Keep the fixed rail clear of the viewport edge and horizontal scrollbar.
|
// Keep the viewport-anchored rail clear of the window edge and scrollbar.
|
||||||
export const DOWNLOAD_ACTIONS_VIEWPORT_INSET = 8;
|
export const DOWNLOAD_ACTIONS_VIEWPORT_INSET = 8;
|
||||||
|
export const DOWNLOAD_ACTIONS_MAX_HEIGHT = 30;
|
||||||
|
export const DOWNLOAD_ACTIONS_VIEWPORT_TOLERANCE = 1;
|
||||||
|
|
||||||
|
export interface DownloadActionViewportRect {
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadActionPosition {
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
height: number;
|
||||||
|
overflow: 'hidden' | 'visible';
|
||||||
|
visibility: 'hidden' | 'visible';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDownloadActionPosition = (
|
||||||
|
rowRect: DownloadActionViewportRect,
|
||||||
|
horizontalViewportRect: DownloadActionViewportRect,
|
||||||
|
verticalViewportRect: DownloadActionViewportRect,
|
||||||
|
windowWidth: number
|
||||||
|
): DownloadActionPosition => {
|
||||||
|
const visibleTop = Math.max(rowRect.top, verticalViewportRect.top);
|
||||||
|
const visibleBottom = Math.min(rowRect.bottom, verticalViewportRect.bottom);
|
||||||
|
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
||||||
|
const visibleLeft = Math.max(rowRect.left, horizontalViewportRect.left);
|
||||||
|
const visibleRight = Math.min(rowRect.right, horizontalViewportRect.right);
|
||||||
|
const visibleWidth = Math.max(0, visibleRight - visibleLeft);
|
||||||
|
const isVisible = visibleHeight > DOWNLOAD_ACTIONS_VIEWPORT_TOLERANCE &&
|
||||||
|
visibleWidth > DOWNLOAD_ACTIONS_VIEWPORT_TOLERANCE;
|
||||||
|
const actionHeight = Math.min(DOWNLOAD_ACTIONS_MAX_HEIGHT, rowRect.bottom - rowRect.top, visibleHeight);
|
||||||
|
const actionRightEdge = Math.min(rowRect.right, horizontalViewportRect.right);
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: visibleTop + Math.max(0, (visibleHeight - actionHeight) / 2),
|
||||||
|
right: Math.max(0, windowWidth - actionRightEdge) + DOWNLOAD_ACTIONS_VIEWPORT_INSET,
|
||||||
|
height: actionHeight,
|
||||||
|
overflow: actionHeight < rowRect.bottom - rowRect.top ? 'hidden' : 'visible',
|
||||||
|
visibility: isVisible ? 'visible' : 'hidden',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const COLUMN_WIDTHS_STORAGE_KEY = 'firelink-download-column-widths';
|
export const COLUMN_WIDTHS_STORAGE_KEY = 'firelink-download-column-widths';
|
||||||
export const COLUMN_ORDER_STORAGE_KEY = 'firelink-download-column-order';
|
export const COLUMN_ORDER_STORAGE_KEY = 'firelink-download-column-order';
|
||||||
@@ -99,9 +142,8 @@ export const buildColumnGridTemplate = (
|
|||||||
const normalizedWidths = normalizeColumnWidths(widths);
|
const normalizedWidths = normalizeColumnWidths(widths);
|
||||||
const orderedWidths = order.map(key => normalizedWidths[columnIndex(key)]);
|
const orderedWidths = order.map(key => normalizedWidths[columnIndex(key)]);
|
||||||
return [
|
return [
|
||||||
...orderedWidths.slice(0, -1).map(width => `${width}px`),
|
...orderedWidths.map(width => `${width}px`),
|
||||||
'minmax(0, 1fr)',
|
'minmax(0, 1fr)',
|
||||||
`${orderedWidths[orderedWidths.length - 1]}px`,
|
|
||||||
].join(' ');
|
].join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,5 +151,5 @@ export const getColumnGridColumn = (
|
|||||||
key: DownloadTableColumnKey,
|
key: DownloadTableColumnKey,
|
||||||
order: DownloadTableColumnKey[]
|
order: DownloadTableColumnKey[]
|
||||||
): string | undefined => key === order[order.length - 1]
|
): string | undefined => key === order[order.length - 1]
|
||||||
? `${order.length + 1}`
|
? `${order.length}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user