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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user