mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #250 from PR0M3TH3AN/codex/introduce-utility-for-card-observer-management
Refactor health observers to share card observer utility
This commit is contained in:
3 files changed
+272
-124
No files matched your search
@@ -0,0 +1,208 @@
|
||||
const DEFAULT_CARD_SELECTOR = ".video-card";
|
||||
|
||||
function defaultIsCardVisible(entry) {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
const isIntersecting = Boolean(entry.isIntersecting);
|
||||
const ratio = typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0;
|
||||
return isIntersecting && ratio > 0;
|
||||
}
|
||||
|
||||
function normalizeState(value) {
|
||||
if (value && typeof value === "object") {
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function createCardObserver(options = {}) {
|
||||
const {
|
||||
rootMargin = "0px",
|
||||
threshold = 0,
|
||||
cardSelector = DEFAULT_CARD_SELECTOR,
|
||||
isCardVisible = defaultIsCardVisible,
|
||||
prepareEntries,
|
||||
onCardVisible,
|
||||
onCardRegister,
|
||||
createState,
|
||||
} = options;
|
||||
|
||||
const containerState = new WeakMap();
|
||||
|
||||
function ensureState(container) {
|
||||
let state = containerState.get(container);
|
||||
if (state) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const observedCards = new WeakSet();
|
||||
const customState = normalizeState(
|
||||
typeof createState === "function" ? createState(container) : {}
|
||||
);
|
||||
|
||||
const stateWrapper = {
|
||||
container,
|
||||
observedCards,
|
||||
customState,
|
||||
observer: null,
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
handleEntries(entries, stateWrapper);
|
||||
},
|
||||
{ root: null, rootMargin, threshold }
|
||||
);
|
||||
|
||||
stateWrapper.observer = observer;
|
||||
containerState.set(container, stateWrapper);
|
||||
return stateWrapper;
|
||||
}
|
||||
|
||||
function mapEntries(entries, state) {
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const context = {
|
||||
container: state.container,
|
||||
observer: state.observer,
|
||||
state: state.customState,
|
||||
};
|
||||
|
||||
if (typeof prepareEntries === "function") {
|
||||
try {
|
||||
const prepared = prepareEntries(entries, context);
|
||||
if (Array.isArray(prepared)) {
|
||||
return prepared;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[cardObserver] prepareEntries failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function handleEntries(entries, state) {
|
||||
const mapped = mapEntries(entries, state);
|
||||
if (!mapped.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapped.forEach((item) => {
|
||||
let entry = item;
|
||||
let card;
|
||||
let meta;
|
||||
|
||||
if (item && typeof item === "object" && "entry" in item) {
|
||||
entry = item.entry;
|
||||
card = item.card instanceof HTMLElement ? item.card : entry?.target;
|
||||
if ("meta" in item) {
|
||||
meta = item.meta;
|
||||
} else if ("data" in item) {
|
||||
meta = item.data;
|
||||
} else if (typeof item.priority !== "undefined") {
|
||||
meta = { priority: item.priority };
|
||||
}
|
||||
} else if (entry && entry.target instanceof HTMLElement) {
|
||||
card = entry.target;
|
||||
}
|
||||
|
||||
if (!(card instanceof HTMLElement) || !entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
let visible = false;
|
||||
try {
|
||||
visible = Boolean(isCardVisible(entry, card, {
|
||||
container: state.container,
|
||||
observer: state.observer,
|
||||
state: state.customState,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.warn("[cardObserver] isCardVisible failed", err);
|
||||
visible = false;
|
||||
}
|
||||
|
||||
if (!visible || typeof onCardVisible !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
onCardVisible({
|
||||
card,
|
||||
entry,
|
||||
meta,
|
||||
state: state.customState,
|
||||
context: {
|
||||
container: state.container,
|
||||
observer: state.observer,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("[cardObserver] onCardVisible failed", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function observe(container) {
|
||||
if (!(container instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const state = ensureState(container);
|
||||
const cards = container.querySelectorAll(cardSelector);
|
||||
cards.forEach((card) => {
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
if (state.observedCards.has(card)) {
|
||||
return;
|
||||
}
|
||||
state.observedCards.add(card);
|
||||
if (typeof onCardRegister === "function") {
|
||||
try {
|
||||
onCardRegister({
|
||||
card,
|
||||
state: state.customState,
|
||||
context: { container },
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("[cardObserver] onCardRegister failed", err);
|
||||
}
|
||||
}
|
||||
state.observer.observe(card);
|
||||
});
|
||||
|
||||
const records = state.observer.takeRecords();
|
||||
if (records.length) {
|
||||
handleEntries(records, state);
|
||||
}
|
||||
|
||||
return state.customState;
|
||||
}
|
||||
|
||||
function refresh(container) {
|
||||
const state = containerState.get(container);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const records = state.observer.takeRecords();
|
||||
if (records.length) {
|
||||
handleEntries(records, state);
|
||||
}
|
||||
}
|
||||
|
||||
function getState(container) {
|
||||
const state = containerState.get(container);
|
||||
return state ? state.customState : null;
|
||||
}
|
||||
|
||||
return {
|
||||
observe,
|
||||
refresh,
|
||||
getState,
|
||||
};
|
||||
}
|
||||
+44
-60
@@ -1,7 +1,7 @@
|
||||
import { createCardObserver } from "./dom/cardObserver.js";
|
||||
import { infoHashFromMagnet } from "./magnets.js";
|
||||
import { TorrentClient, torrentClient } from "./webtorrent.js";
|
||||
|
||||
const containerState = new WeakMap();
|
||||
const ROOT_MARGIN = "200px 0px";
|
||||
|
||||
function now() {
|
||||
@@ -249,28 +249,6 @@ const probeQueue = new ProbeQueue(PROBE_CONCURRENCY);
|
||||
const probeCache = new Map();
|
||||
const probeInflight = new Map();
|
||||
|
||||
function ensureState(container) {
|
||||
let state = containerState.get(container);
|
||||
if (state) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const pendingByCard = new WeakMap();
|
||||
const observedCards = new WeakSet();
|
||||
state = { observer: null, pendingByCard, observedCards };
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
processObserverEntries(entries, state);
|
||||
},
|
||||
{ root: null, rootMargin: ROOT_MARGIN, threshold: 0.01 }
|
||||
);
|
||||
|
||||
state.observer = observer;
|
||||
containerState.set(container, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function getViewportCenter() {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
@@ -389,18 +367,6 @@ function prioritizeEntries(entries, viewportCenter) {
|
||||
}));
|
||||
}
|
||||
|
||||
function processObserverEntries(entries, state) {
|
||||
if (!state || !entries || entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
const viewportCenter = getViewportCenter();
|
||||
const prioritized = prioritizeEntries(entries, viewportCenter);
|
||||
prioritized.forEach(({ entry, priority }) => {
|
||||
const card = entry.target;
|
||||
handleCardVisible({ card, pendingByCard: state.pendingByCard, priority });
|
||||
});
|
||||
}
|
||||
|
||||
function getIntersectionRect(entry) {
|
||||
if (!entry) {
|
||||
return null;
|
||||
@@ -539,6 +505,10 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(pendingByCard instanceof WeakMap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingByCard.has(card)) {
|
||||
return;
|
||||
}
|
||||
@@ -590,36 +560,50 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) {
|
||||
});
|
||||
}
|
||||
|
||||
const gridCardObserver = createCardObserver({
|
||||
rootMargin: ROOT_MARGIN,
|
||||
threshold: 0.01,
|
||||
createState: () => ({ pendingByCard: new WeakMap() }),
|
||||
prepareEntries: (entries) => {
|
||||
const viewportCenter = getViewportCenter();
|
||||
return prioritizeEntries(entries, viewportCenter);
|
||||
},
|
||||
isCardVisible: (entry) => {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
const isIntersecting = Boolean(entry.isIntersecting);
|
||||
const ratio = typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0;
|
||||
return isIntersecting && ratio > 0;
|
||||
},
|
||||
onCardRegister: ({ card }) => {
|
||||
if (!card.dataset.magnet) {
|
||||
setBadge(card, "unhealthy", { reason: "missing-source" });
|
||||
}
|
||||
},
|
||||
onCardVisible: ({ card, meta, state }) => {
|
||||
if (!state || !(state.pendingByCard instanceof WeakMap)) {
|
||||
return;
|
||||
}
|
||||
const priority =
|
||||
meta && typeof meta === "object" && Number.isFinite(meta.priority)
|
||||
? meta.priority
|
||||
: PRIORITY_BASELINE;
|
||||
handleCardVisible({
|
||||
card,
|
||||
pendingByCard: state.pendingByCard,
|
||||
priority,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export function attachHealthBadges(container) {
|
||||
if (!(container instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const state = ensureState(container);
|
||||
const cards = container.querySelectorAll(".video-card");
|
||||
cards.forEach((card) => {
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
if (state.observedCards.has(card)) {
|
||||
return;
|
||||
}
|
||||
state.observedCards.add(card);
|
||||
state.observer.observe(card);
|
||||
if (!card.dataset.magnet) {
|
||||
setBadge(card, "unhealthy", { reason: "missing-source" });
|
||||
}
|
||||
});
|
||||
processObserverEntries(state.observer.takeRecords(), state);
|
||||
gridCardObserver.observe(container);
|
||||
}
|
||||
|
||||
export function refreshHealthBadges(container) {
|
||||
if (!(container instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const state = containerState.get(container);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const records = state.observer.takeRecords();
|
||||
processObserverEntries(records, state);
|
||||
gridCardObserver.refresh(container);
|
||||
}
|
||||
+20
-64
@@ -1,4 +1,5 @@
|
||||
const containerState = new WeakMap();
|
||||
import { createCardObserver } from "./dom/cardObserver.js";
|
||||
|
||||
const ROOT_MARGIN = "0px";
|
||||
const THRESHOLD = 0.25;
|
||||
|
||||
@@ -13,41 +14,20 @@ function decodeUrl(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureState(container) {
|
||||
let state = containerState.get(container);
|
||||
if (state) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const observedCards = new WeakSet();
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
processEntries(entries, state);
|
||||
},
|
||||
{ root: null, rootMargin: ROOT_MARGIN, threshold: THRESHOLD }
|
||||
);
|
||||
|
||||
state = {
|
||||
observer,
|
||||
observedCards,
|
||||
onCheck: null,
|
||||
};
|
||||
containerState.set(container, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function processEntries(entries, state) {
|
||||
if (!state?.onCheck || !Array.isArray(entries) || !entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting || entry.intersectionRatio <= 0) {
|
||||
return;
|
||||
const urlCardObserver = createCardObserver({
|
||||
rootMargin: ROOT_MARGIN,
|
||||
threshold: THRESHOLD,
|
||||
isCardVisible: (entry) => {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const card = entry.target;
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
const isIntersecting = Boolean(entry.isIntersecting);
|
||||
const ratio = typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0;
|
||||
return isIntersecting && ratio > 0;
|
||||
},
|
||||
createState: () => ({ onCheck: null }),
|
||||
onCardVisible: ({ card, state }) => {
|
||||
if (!state || typeof state.onCheck !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,44 +61,20 @@ function processEntries(entries, state) {
|
||||
} catch (err) {
|
||||
console.warn("[urlHealthObserver] onCheck handler failed", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export function attachUrlHealthBadges(container, onCheck) {
|
||||
if (!(container instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = ensureState(container);
|
||||
if (typeof onCheck === "function") {
|
||||
const state = urlCardObserver.observe(container);
|
||||
if (state && typeof onCheck === "function") {
|
||||
state.onCheck = onCheck;
|
||||
}
|
||||
|
||||
const cards = container.querySelectorAll(".video-card");
|
||||
cards.forEach((card) => {
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
if (state.observedCards.has(card)) {
|
||||
return;
|
||||
}
|
||||
state.observedCards.add(card);
|
||||
state.observer.observe(card);
|
||||
});
|
||||
|
||||
const records = state.observer.takeRecords();
|
||||
if (records.length) {
|
||||
processEntries(records, state);
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshUrlHealthBadges(container) {
|
||||
const state = containerState.get(container);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const records = state.observer.takeRecords();
|
||||
if (records.length) {
|
||||
processEntries(records, state);
|
||||
}
|
||||
urlCardObserver.refresh(container);
|
||||
}
|
||||
Reference in new issue
Block a user