mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-21 04:32:25 +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 { usePlayerStore } from "@/stores/player/store";
|
||||||
import { usePreferencesStore } from "@/stores/preferences";
|
import { usePreferencesStore } from "@/stores/preferences";
|
||||||
import { useProgressStore } from "@/stores/progress";
|
import { useProgressStore } from "@/stores/progress";
|
||||||
|
import { scrollToElement } from "@/utils/scroll";
|
||||||
|
|
||||||
import { hasAired } from "../utils/aired";
|
import { hasAired } from "../utils/aired";
|
||||||
|
|
||||||
|
|
@ -832,7 +833,7 @@ export function EpisodesView({
|
||||||
carouselRef.current.scrollLeft += scrollPosition;
|
carouselRef.current.scrollLeft += scrollPosition;
|
||||||
} else {
|
} else {
|
||||||
// vertical scroll
|
// vertical scroll
|
||||||
activeEpisodeRef.current.scrollIntoView({
|
scrollToElement(activeEpisodeRef.current, {
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "center",
|
block: "center",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import { useLanguageStore } from "@/stores/language";
|
||||||
import { usePreferencesStore } from "@/stores/preferences";
|
import { usePreferencesStore } from "@/stores/preferences";
|
||||||
import { useSubtitleStore } from "@/stores/subtitles";
|
import { useSubtitleStore } from "@/stores/subtitles";
|
||||||
import { usePreviewThemeStore, useThemeStore } from "@/stores/theme";
|
import { usePreviewThemeStore, useThemeStore } from "@/stores/theme";
|
||||||
|
import { scrollToElement, scrollToHash } from "@/utils/scroll";
|
||||||
|
|
||||||
import { SubPageLayout } from "./layouts/SubPageLayout";
|
import { SubPageLayout } from "./layouts/SubPageLayout";
|
||||||
import { AppInfoPart } from "./parts/settings/AppInfoPart";
|
import { AppInfoPart } from "./parts/settings/AppInfoPart";
|
||||||
|
|
@ -195,19 +196,11 @@ export function SettingsPage() {
|
||||||
const categoryId = subSectionToCategory[hashId];
|
const categoryId = subSectionToCategory[hashId];
|
||||||
setSelectedCategory(categoryId);
|
setSelectedCategory(categoryId);
|
||||||
// Wait for the section to render, then scroll
|
// Wait for the section to render, then scroll
|
||||||
setTimeout(() => {
|
scrollToHash(hash, { delay: 100 });
|
||||||
const element = document.querySelector(hash);
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: "smooth" });
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
} else if (validCategories.includes(hashId)) {
|
} else if (validCategories.includes(hashId)) {
|
||||||
// It's a category hash
|
// It's a category hash
|
||||||
setSelectedCategory(hashId);
|
setSelectedCategory(hashId);
|
||||||
const element = document.querySelector(hash);
|
scrollToHash(hash);
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: "smooth" });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Try to find the element anyway (might be a sub-section)
|
// Try to find the element anyway (might be a sub-section)
|
||||||
const element = document.querySelector(hash);
|
const element = document.querySelector(hash);
|
||||||
|
|
@ -218,12 +211,10 @@ export function SettingsPage() {
|
||||||
const categoryId = parentSection.id;
|
const categoryId = parentSection.id;
|
||||||
if (validCategories.includes(categoryId)) {
|
if (validCategories.includes(categoryId)) {
|
||||||
setSelectedCategory(categoryId);
|
setSelectedCategory(categoryId);
|
||||||
setTimeout(() => {
|
scrollToHash(hash, { delay: 100 });
|
||||||
element.scrollIntoView({ behavior: "smooth" });
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
element.scrollIntoView({ behavior: "smooth" });
|
scrollToHash(hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -251,20 +242,10 @@ export function SettingsPage() {
|
||||||
if (subSectionToCategory[hashId]) {
|
if (subSectionToCategory[hashId]) {
|
||||||
const categoryId = subSectionToCategory[hashId];
|
const categoryId = subSectionToCategory[hashId];
|
||||||
setSelectedCategory(categoryId);
|
setSelectedCategory(categoryId);
|
||||||
setTimeout(() => {
|
scrollToHash(hash, { delay: 100 });
|
||||||
const element = document.querySelector(hash);
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: "smooth" });
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
} else if (validCategories.includes(hashId)) {
|
} else if (validCategories.includes(hashId)) {
|
||||||
setSelectedCategory(hashId);
|
setSelectedCategory(hashId);
|
||||||
setTimeout(() => {
|
scrollToHash(hash, { delay: 100 });
|
||||||
const element = document.querySelector(hash);
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: "smooth" });
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
} else {
|
} else {
|
||||||
const element = document.querySelector(hash);
|
const element = document.querySelector(hash);
|
||||||
if (element) {
|
if (element) {
|
||||||
|
|
@ -273,12 +254,10 @@ export function SettingsPage() {
|
||||||
const categoryId = parentSection.id;
|
const categoryId = parentSection.id;
|
||||||
if (validCategories.includes(categoryId)) {
|
if (validCategories.includes(categoryId)) {
|
||||||
setSelectedCategory(categoryId);
|
setSelectedCategory(categoryId);
|
||||||
setTimeout(() => {
|
scrollToHash(hash, { delay: 100 });
|
||||||
element.scrollIntoView({ behavior: "smooth" });
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
element.scrollIntoView({ behavior: "smooth" });
|
scrollToHash(hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -372,13 +351,10 @@ export function SettingsPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll to first highlighted element
|
// Scroll to first highlighted element
|
||||||
const firstHighlighted = document.querySelector(".search-highlight");
|
scrollToElement(".search-highlight", {
|
||||||
if (firstHighlighted) {
|
behavior: "smooth",
|
||||||
firstHighlighted.scrollIntoView({
|
block: "center",
|
||||||
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