release: publish Multi-Debrid Downloader v2.0.14
Add live English and German localization, queue availability and metadata resolution, responsive package controls, polished navigation and drag interactions, clearer history and account states, and a rebuilt public README. Harden Windows packaging with verified icons and version metadata, archive inspection, and expanded release tests.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type ReactElement } from "react";
|
||||
|
||||
export type RollingMetricDirection = "up" | "down" | "none";
|
||||
|
||||
interface RollingMetricValueProps {
|
||||
numericValue: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface RollingMetricTransition {
|
||||
direction: Exclude<RollingMetricDirection, "none">;
|
||||
from: string;
|
||||
id: number;
|
||||
to: string;
|
||||
}
|
||||
|
||||
const useMetricLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
||||
|
||||
export function getRollingMetricDirection(previous: number, next: number): RollingMetricDirection {
|
||||
if (next > previous) return "up";
|
||||
if (next < previous) return "down";
|
||||
return "none";
|
||||
}
|
||||
|
||||
export function RollingMetricValue({ numericValue, value }: RollingMetricValueProps): ReactElement {
|
||||
const previousRef = useRef({ numericValue, value });
|
||||
const sequenceRef = useRef(0);
|
||||
const outgoingRef = useRef<HTMLSpanElement>(null);
|
||||
const incomingRef = useRef<HTMLSpanElement>(null);
|
||||
const [transition, setTransition] = useState<RollingMetricTransition | null>(null);
|
||||
|
||||
useMetricLayoutEffect(() => {
|
||||
const previous = previousRef.current;
|
||||
if (previous.value === value && previous.numericValue === numericValue) return;
|
||||
previousRef.current = { numericValue, value };
|
||||
const direction = getRollingMetricDirection(previous.numericValue, numericValue);
|
||||
if (direction === "none") {
|
||||
setTransition(null);
|
||||
return;
|
||||
}
|
||||
sequenceRef.current += 1;
|
||||
setTransition({ direction, from: previous.value, id: sequenceRef.current, to: value });
|
||||
}, [numericValue, value]);
|
||||
|
||||
useMetricLayoutEffect(() => {
|
||||
if (!transition || !outgoingRef.current || !incomingRef.current) return;
|
||||
const distance = transition.direction === "up" ? -1 : 1;
|
||||
const options: KeyframeAnimationOptions = { duration: 320, easing: "cubic-bezier(0.22, 1, 0.36, 1)", fill: "forwards" };
|
||||
const outgoing = outgoingRef.current.animate([
|
||||
{ opacity: 1, transform: "translateY(0)" },
|
||||
{ opacity: 0, transform: `translateY(${distance * 115}%)` }
|
||||
], options);
|
||||
const incoming = incomingRef.current.animate([
|
||||
{ opacity: 0, transform: `translateY(${-distance * 115}%)` },
|
||||
{ opacity: 1, transform: "translateY(0)" }
|
||||
], options);
|
||||
let active = true;
|
||||
Promise.all([outgoing.finished, incoming.finished]).then(() => {
|
||||
if (active) setTransition((current) => current?.id === transition.id ? null : current);
|
||||
}).catch(() => {});
|
||||
return () => {
|
||||
active = false;
|
||||
outgoing.cancel();
|
||||
incoming.cancel();
|
||||
};
|
||||
}, [transition]);
|
||||
|
||||
return (
|
||||
<strong aria-label={value} className="downloads-rolling-value" data-direction={transition?.direction ?? "none"}>
|
||||
{transition
|
||||
? <><span aria-hidden="true" className="downloads-rolling-value-layer is-outgoing" ref={outgoingRef}>{transition.from}</span><span aria-hidden="true" className="downloads-rolling-value-layer is-incoming" ref={incomingRef}>{transition.to}</span></>
|
||||
: <span aria-hidden="true" className="downloads-rolling-value-layer">{value}</span>}
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user