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>
);
}
+81
View File
@@ -0,0 +1,81 @@
import { createElement, useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactElement, type ReactNode } from "react";
const useSelectionLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
export interface SlidingSelectionProps extends HTMLAttributes<HTMLElement> {
activeKey: string;
as?: "div" | "nav";
axis: "horizontal" | "vertical";
children: ReactNode;
}
export function scheduleSelectionLayout(
hasPosition: boolean,
frame: number,
apply: () => void,
requestFrame: (callback: FrameRequestCallback) => number = requestAnimationFrame,
cancelFrame: (frame: number) => void = cancelAnimationFrame,
enableTransitions: () => void = () => {}
): number {
if (!hasPosition) {
apply();
return requestFrame(enableTransitions);
}
cancelFrame(frame);
return requestFrame(apply);
}
export function SlidingSelection({ activeKey, as = "div", axis, children, className = "", ...attributes }: SlidingSelectionProps): ReactElement {
const ref = useRef<HTMLElement>(null);
const hasPositionRef = useRef(false);
useSelectionLayoutEffect(() => {
const element = ref.current;
if (!element) return;
let frame = 0;
let transitionFrame = 0;
const applyLayout = (): void => {
const active = element.querySelector<HTMLElement>('[data-sliding-selection-active="true"]');
if (!active) {
element.style.setProperty("--ui-sliding-selection-width", "0px");
element.style.setProperty("--ui-sliding-selection-height", "0px");
return;
}
const containerRect = element.getBoundingClientRect();
const activeRect = active.getBoundingClientRect();
element.style.setProperty("--ui-sliding-selection-x", `${activeRect.left - containerRect.left + element.scrollLeft}px`);
element.style.setProperty("--ui-sliding-selection-y", `${activeRect.top - containerRect.top + element.scrollTop}px`);
element.style.setProperty("--ui-sliding-selection-width", `${activeRect.width}px`);
element.style.setProperty("--ui-sliding-selection-height", `${activeRect.height}px`);
hasPositionRef.current = true;
};
const sync = (): void => {
if (!hasPositionRef.current) {
transitionFrame = scheduleSelectionLayout(false, transitionFrame, applyLayout, requestAnimationFrame, cancelAnimationFrame, () => {
if (hasPositionRef.current) {
element.style.setProperty("--ui-sliding-selection-duration", "420ms");
}
});
return;
}
frame = scheduleSelectionLayout(hasPositionRef.current, frame, applyLayout);
};
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(sync);
observer?.observe(element);
element.querySelectorAll<HTMLElement>('[data-sliding-selection-item="true"]').forEach((item) => observer?.observe(item));
element.addEventListener("scroll", sync, { passive: true });
sync();
return () => {
cancelAnimationFrame(frame);
cancelAnimationFrame(transitionFrame);
observer?.disconnect();
element.removeEventListener("scroll", sync);
};
}, [activeKey]);
return createElement(as, {
...attributes,
className: `ui-sliding-selection ui-sliding-selection-${axis}${className ? ` ${className}` : ""}`,
ref
}, children);
}