mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +00:00
refactor scrolling with a reusable component
This commit is contained in:
parent
1199a21df5
commit
544fe97c5e
3 changed files with 102 additions and 38 deletions
|
|
@ -20,6 +20,7 @@ import { PlayerMeta } from "@/stores/player/slices/source";
|
|||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { scrollToElement } from "@/utils/scroll";
|
||||
|
||||
import { hasAired } from "../utils/aired";
|
||||
|
||||
|
|
@ -832,7 +833,7 @@ export function EpisodesView({
|
|||
carouselRef.current.scrollLeft += scrollPosition;
|
||||
} else {
|
||||
// vertical scroll
|
||||
activeEpisodeRef.current.scrollIntoView({
|
||||
scrollToElement(activeEpisodeRef.current, {
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { useLanguageStore } from "@/stores/language";
|
|||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { useSubtitleStore } from "@/stores/subtitles";
|
||||
import { usePreviewThemeStore, useThemeStore } from "@/stores/theme";
|
||||
import { scrollToElement, scrollToHash } from "@/utils/scroll";
|
||||
|
||||
import { SubPageLayout } from "./layouts/SubPageLayout";
|
||||
import { AppInfoPart } from "./parts/settings/AppInfoPart";
|
||||
|
|
@ -195,19 +196,11 @@ export function SettingsPage() {
|
|||
const categoryId = subSectionToCategory[hashId];
|
||||
setSelectedCategory(categoryId);
|
||||
// Wait for the section to render, then scroll
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(hash);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, 100);
|
||||
scrollToHash(hash, { delay: 100 });
|
||||
} else if (validCategories.includes(hashId)) {
|
||||
// It's a category hash
|
||||
setSelectedCategory(hashId);
|
||||
const element = document.querySelector(hash);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
scrollToHash(hash);
|
||||
} else {
|
||||
// Try to find the element anyway (might be a sub-section)
|
||||
const element = document.querySelector(hash);
|
||||
|
|
@ -218,12 +211,10 @@ export function SettingsPage() {
|
|||
const categoryId = parentSection.id;
|
||||
if (validCategories.includes(categoryId)) {
|
||||
setSelectedCategory(categoryId);
|
||||
setTimeout(() => {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}, 100);
|
||||
scrollToHash(hash, { delay: 100 });
|
||||
}
|
||||
} else {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
scrollToHash(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -251,20 +242,10 @@ export function SettingsPage() {
|
|||
if (subSectionToCategory[hashId]) {
|
||||
const categoryId = subSectionToCategory[hashId];
|
||||
setSelectedCategory(categoryId);
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(hash);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, 100);
|
||||
scrollToHash(hash, { delay: 100 });
|
||||
} else if (validCategories.includes(hashId)) {
|
||||
setSelectedCategory(hashId);
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(hash);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, 100);
|
||||
scrollToHash(hash, { delay: 100 });
|
||||
} else {
|
||||
const element = document.querySelector(hash);
|
||||
if (element) {
|
||||
|
|
@ -273,12 +254,10 @@ export function SettingsPage() {
|
|||
const categoryId = parentSection.id;
|
||||
if (validCategories.includes(categoryId)) {
|
||||
setSelectedCategory(categoryId);
|
||||
setTimeout(() => {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}, 100);
|
||||
scrollToHash(hash, { delay: 100 });
|
||||
}
|
||||
} else {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
scrollToHash(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -372,13 +351,10 @@ export function SettingsPage() {
|
|||
}
|
||||
|
||||
// Scroll to first highlighted element
|
||||
const firstHighlighted = document.querySelector(".search-highlight");
|
||||
if (firstHighlighted) {
|
||||
firstHighlighted.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
scrollToElement(".search-highlight", {
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
87
src/utils/scroll.ts
Normal file
87
src/utils/scroll.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* Scrolls an element into view with configurable options
|
||||
* @param selector - CSS selector string, Element, or null
|
||||
* @param options - Scroll options
|
||||
* @returns void (always returns, even if element not found)
|
||||
*/
|
||||
export function scrollToElement(
|
||||
selector: string | Element | null,
|
||||
options?: {
|
||||
behavior?: ScrollBehavior;
|
||||
block?: ScrollLogicalPosition;
|
||||
inline?: ScrollLogicalPosition;
|
||||
offset?: number; // Additional offset in pixels (positive = scroll down more)
|
||||
delay?: number; // Delay in milliseconds before scrolling (useful when element needs to render)
|
||||
},
|
||||
): void {
|
||||
const {
|
||||
behavior = "smooth",
|
||||
block = "start",
|
||||
inline = "nearest",
|
||||
offset = 0,
|
||||
delay = 0,
|
||||
} = options || {};
|
||||
|
||||
const scroll = (): void => {
|
||||
let element: Element | null = null;
|
||||
|
||||
if (selector === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof selector === "string") {
|
||||
element = document.querySelector(selector);
|
||||
} else {
|
||||
element = selector;
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (offset === 0) {
|
||||
// Use native scrollIntoView when no offset is needed
|
||||
element.scrollIntoView({ behavior, block, inline });
|
||||
return;
|
||||
}
|
||||
|
||||
// Custom scroll with offset
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
||||
const offsetPosition = absoluteElementTop - offset;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior,
|
||||
});
|
||||
};
|
||||
|
||||
if (delay > 0) {
|
||||
setTimeout(() => {
|
||||
scroll();
|
||||
}, delay);
|
||||
return;
|
||||
}
|
||||
|
||||
scroll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls to an element by hash (useful for hash navigation)
|
||||
* @param hash - Hash string (with or without #)
|
||||
* @param options - Scroll options
|
||||
* @returns void (always returns, even if element not found)
|
||||
*/
|
||||
export function scrollToHash(
|
||||
hash: string,
|
||||
options?: {
|
||||
behavior?: ScrollBehavior;
|
||||
block?: ScrollLogicalPosition;
|
||||
inline?: ScrollLogicalPosition;
|
||||
offset?: number;
|
||||
delay?: number;
|
||||
},
|
||||
): void {
|
||||
const normalizedHash = hash.startsWith("#") ? hash : `#${hash}`;
|
||||
scrollToElement(normalizedHash, options);
|
||||
}
|
||||
Loading…
Reference in a new issue