feat(MediaManager): implement MarqueeText component for improved overflow handling in titles
This commit is contained in:
parent
e6dabbe0a6
commit
04fa5ab054
|
|
@ -1,4 +1,4 @@
|
||||||
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
|
@ -126,6 +126,53 @@ function ViewerModeIcon({ mode, className }: { mode: ViewMode; className?: strin
|
||||||
|
|
||||||
// EntryIcon is imported from MediaEntry.
|
// EntryIcon is imported from MediaEntry.
|
||||||
|
|
||||||
|
// ─── MarqueeText ──────────────────────────────────────────────────────────────
|
||||||
|
// Scrolls text left→right when it overflows its container, then back — ping-pong.
|
||||||
|
// Uses a CSS variable for the per-instance scroll distance so a single keyframe
|
||||||
|
// declaration covers every instance.
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined' && !document.getElementById('mm-marquee-kf')) {
|
||||||
|
const s = document.createElement('style');
|
||||||
|
s.id = 'mm-marquee-kf';
|
||||||
|
s.textContent = `@keyframes mm-scroll {
|
||||||
|
0%,12% { transform: translateX(0) }
|
||||||
|
88%,100%{ transform: translateX(var(--mm-dx,0px)) }
|
||||||
|
}`;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarqueeText({ children, className }: { children?: string; className?: string }) {
|
||||||
|
const wrapRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const textRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const [dx, setDx] = useState(0);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const wrap = wrapRef.current;
|
||||||
|
const text = textRef.current;
|
||||||
|
if (!wrap || !text) return;
|
||||||
|
const measure = () => setDx(Math.max(0, text.scrollWidth - wrap.offsetWidth));
|
||||||
|
measure();
|
||||||
|
const ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(wrap);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span ref={wrapRef} className={`overflow-hidden whitespace-nowrap block ${className ?? ''}`}>
|
||||||
|
<span
|
||||||
|
ref={textRef}
|
||||||
|
style={dx > 0 ? {
|
||||||
|
display: 'inline-block',
|
||||||
|
'--mm-dx': `-${dx}px`,
|
||||||
|
animation: `mm-scroll ${1.5 + dx / 80}s ease-in-out 1s infinite alternate`,
|
||||||
|
} as React.CSSProperties : undefined}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── ActionsModal ─────────────────────────────────────────────────────────────
|
// ─── ActionsModal ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface FolderManagementActions {
|
interface FolderManagementActions {
|
||||||
|
|
@ -164,7 +211,7 @@ function ActionsModal({ entry, onClose, onOpen, onMount, onDownload, onDuplicate
|
||||||
<Dialog open={entry !== null} onOpenChange={open => !open && onClose()}>
|
<Dialog open={entry !== null} onOpenChange={open => !open && onClose()}>
|
||||||
<DialogContent className="max-w-sm">
|
<DialogContent className="max-w-sm">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="truncate">{entry?.name || '/'}</DialogTitle>
|
<DialogTitle><MarqueeText>{entry?.name || '/'}</MarqueeText></DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{isFolder ? 'Folder' : humanFileSize(entry?.size ?? 0)}
|
{isFolder ? 'Folder' : humanFileSize(entry?.size ?? 0)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
@ -1269,12 +1316,8 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
|
||||||
<DialogContent className="max-w-sm flex flex-col max-h-[90dvh]">
|
<DialogContent className="max-w-sm flex flex-col max-h-[90dvh]">
|
||||||
<DialogHeader className="flex-shrink-0">
|
<DialogHeader className="flex-shrink-0">
|
||||||
<DialogTitle>Mount on Virtual Drive</DialogTitle>
|
<DialogTitle>Mount on Virtual Drive</DialogTitle>
|
||||||
<DialogDescription
|
<DialogDescription title={mountEntry?.name}>
|
||||||
className="block overflow-hidden whitespace-nowrap"
|
<MarqueeText>{mountEntry?.name}</MarqueeText>
|
||||||
style={{ direction: 'rtl', textOverflow: 'ellipsis' }}
|
|
||||||
title={mountEntry?.name}
|
|
||||||
>
|
|
||||||
{mountEntry?.name}
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="overflow-y-auto flex-1 min-h-0">
|
<div className="overflow-y-auto flex-1 min-h-0">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user