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:
Sucukdeluxe
2026-08-10 19:42:49 +02:00
parent 069babfd54
commit a5758aa905
61 changed files with 9117 additions and 6754 deletions
+75
View File
@@ -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>
);
}