mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-03-12 22:06:03 +00:00
Add watch history view
This commit is contained in:
22
assets/svg/history-icon.svg
Normal file
22
assets/svg/history-icon.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xml:space="preserve"
|
||||
xmlns:serif="http://www.serif.com/"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
||||
>
|
||||
<g transform="matrix(1,0,0,1,12,12)">
|
||||
<g transform="matrix(1,0,0,1,-12,-12)">
|
||||
<path
|
||||
d="M12,2C6.486,2 2,6.486 2,12C2,17.514 6.486,22 12,22C17.514,22 22,17.514 22,12C22,6.486 17.514,2 12,2ZM12,4C16.411,4 20,7.589 20,12C20,16.411 16.411,20 12,20C7.589,20 4,16.411 4,12C4,7.589 7.589,4 12,4ZM11,7C10.448,7 10,7.448 10,8L10,13C10,13.266 10.105,13.52 10.293,13.707L13.293,16.707C13.683,17.098 14.317,17.098 14.707,16.707C15.098,16.317 15.098,15.683 14.707,15.293L12,12.586L12,8C12,7.448 11.552,7 11,7Z"
|
||||
style="fill:rgb(92,111,138);fill-rule:nonzero;"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -34,6 +34,17 @@
|
||||
/>
|
||||
<span class="sidebar-text">Explore</span>
|
||||
</a>
|
||||
<a
|
||||
href="#view=history"
|
||||
class="flex items-center py-2 px-4 hover:bg-gray-700 rounded font-semibold"
|
||||
>
|
||||
<img
|
||||
src="assets/svg/history-icon.svg"
|
||||
alt="History"
|
||||
class="w-6 h-6 mr-3 flex-shrink-0"
|
||||
/>
|
||||
<span class="sidebar-text">History</span>
|
||||
</a>
|
||||
<a
|
||||
id="subscriptionsLink"
|
||||
href="#view=subscriptions"
|
||||
|
||||
220
js/historyView.js
Normal file
220
js/historyView.js
Normal file
@@ -0,0 +1,220 @@
|
||||
// js/historyView.js
|
||||
|
||||
import { nostrClient } from "./nostr.js";
|
||||
import { WATCH_HISTORY_BATCH_RESOLVE } from "./config.js";
|
||||
import { subscriptions } from "./subscriptions.js";
|
||||
|
||||
const DEFAULT_BATCH_SIZE = 20;
|
||||
const BATCH_SIZE = WATCH_HISTORY_BATCH_RESOLVE ? DEFAULT_BATCH_SIZE : 1;
|
||||
const EMPTY_COPY =
|
||||
"Your watch history is empty. Watch some videos to populate this list.";
|
||||
|
||||
let isLoading = false;
|
||||
let hasMore = true;
|
||||
let observer = null;
|
||||
let scrollHandler = null;
|
||||
let resolvedVideos = [];
|
||||
|
||||
function getElements() {
|
||||
return {
|
||||
view: document.getElementById("watchHistoryView"),
|
||||
grid: document.getElementById("watchHistoryGrid"),
|
||||
loadingEl: document.getElementById("watchHistoryLoading"),
|
||||
emptyEl: document.getElementById("watchHistoryEmpty"),
|
||||
sentinel: document.getElementById("watchHistorySentinel"),
|
||||
};
|
||||
}
|
||||
|
||||
function resetUiState() {
|
||||
const { grid, loadingEl, emptyEl } = getElements();
|
||||
if (grid) {
|
||||
grid.innerHTML = "";
|
||||
grid.classList.add("hidden");
|
||||
}
|
||||
if (loadingEl) {
|
||||
loadingEl.classList.remove("hidden");
|
||||
}
|
||||
if (emptyEl) {
|
||||
emptyEl.textContent = EMPTY_COPY;
|
||||
emptyEl.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupObservers() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
if (scrollHandler) {
|
||||
window.removeEventListener("scroll", scrollHandler);
|
||||
scrollHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setLoadingVisible(visible) {
|
||||
const { loadingEl } = getElements();
|
||||
if (!loadingEl) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
loadingEl.classList.remove("hidden");
|
||||
} else {
|
||||
loadingEl.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function showEmptyState(message = EMPTY_COPY) {
|
||||
const { grid, emptyEl } = getElements();
|
||||
if (grid) {
|
||||
grid.innerHTML = "";
|
||||
grid.classList.add("hidden");
|
||||
}
|
||||
if (emptyEl) {
|
||||
emptyEl.textContent = message;
|
||||
emptyEl.classList.remove("hidden");
|
||||
}
|
||||
setLoadingVisible(false);
|
||||
hasMore = false;
|
||||
cleanupObservers();
|
||||
}
|
||||
|
||||
function mergeResolvedVideos(nextVideos) {
|
||||
if (!Array.isArray(nextVideos) || !nextVideos.length) {
|
||||
return;
|
||||
}
|
||||
const dedupeMap = new Map();
|
||||
resolvedVideos.forEach((video) => {
|
||||
if (video && video.id) {
|
||||
dedupeMap.set(video.id, video);
|
||||
}
|
||||
});
|
||||
nextVideos.forEach((video) => {
|
||||
if (video && video.id) {
|
||||
dedupeMap.set(video.id, video);
|
||||
}
|
||||
});
|
||||
resolvedVideos = Array.from(dedupeMap.values());
|
||||
}
|
||||
|
||||
function renderResolvedVideos() {
|
||||
const { grid, emptyEl } = getElements();
|
||||
if (!grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resolvedVideos.length) {
|
||||
if (emptyEl) {
|
||||
emptyEl.textContent = EMPTY_COPY;
|
||||
emptyEl.classList.remove("hidden");
|
||||
}
|
||||
grid.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
subscriptions.renderSameGridStyle(resolvedVideos, "watchHistoryGrid");
|
||||
grid.classList.remove("hidden");
|
||||
if (emptyEl) {
|
||||
emptyEl.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNextBatch({ initial = false } = {}) {
|
||||
if (isLoading || !hasMore) {
|
||||
if (initial && !isLoading && resolvedVideos.length === 0) {
|
||||
setLoadingVisible(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const videos = await nostrClient.resolveWatchHistory(BATCH_SIZE);
|
||||
|
||||
if (videos.length === 0) {
|
||||
if (initial && resolvedVideos.length === 0) {
|
||||
showEmptyState();
|
||||
} else {
|
||||
hasMore = false;
|
||||
cleanupObservers();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
mergeResolvedVideos(videos);
|
||||
renderResolvedVideos();
|
||||
setLoadingVisible(false);
|
||||
} catch (error) {
|
||||
console.error("[historyView] Failed to resolve watch history:", error);
|
||||
if (initial && resolvedVideos.length === 0) {
|
||||
showEmptyState("We couldn't load your watch history. Please try again later.");
|
||||
}
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function attachObservers() {
|
||||
const { sentinel } = getElements();
|
||||
if (!sentinel || !hasMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("IntersectionObserver" in window) {
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
loadNextBatch();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "300px 0px" }
|
||||
);
|
||||
observer.observe(sentinel);
|
||||
return;
|
||||
}
|
||||
|
||||
scrollHandler = () => {
|
||||
const rect = sentinel.getBoundingClientRect();
|
||||
if (rect.top <= window.innerHeight + 200) {
|
||||
loadNextBatch();
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", scrollHandler, { passive: true });
|
||||
scrollHandler();
|
||||
}
|
||||
|
||||
export async function initHistoryView() {
|
||||
const { view } = getElements();
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanupObservers();
|
||||
resolvedVideos = [];
|
||||
hasMore = true;
|
||||
isLoading = false;
|
||||
resetUiState();
|
||||
|
||||
try {
|
||||
const actor = window.app?.pubkey || undefined;
|
||||
await nostrClient.fetchWatchHistory(actor);
|
||||
} catch (error) {
|
||||
console.error("[historyView] Failed to fetch watch history list:", error);
|
||||
showEmptyState("We couldn't load your watch history. Please try again later.");
|
||||
return;
|
||||
}
|
||||
|
||||
await loadNextBatch({ initial: true });
|
||||
|
||||
if (hasMore) {
|
||||
attachObservers();
|
||||
}
|
||||
}
|
||||
|
||||
// Expose init on window for debugging/manual triggers if needed.
|
||||
if (!window.bitvid) {
|
||||
window.bitvid = {};
|
||||
}
|
||||
window.bitvid.initHistoryView = initHistoryView;
|
||||
@@ -67,6 +67,16 @@ export const viewInitRegistry = {
|
||||
explore: () => {
|
||||
console.log("Explore view loaded.");
|
||||
},
|
||||
history: async () => {
|
||||
try {
|
||||
const module = await import("./historyView.js");
|
||||
if (typeof module.initHistoryView === "function") {
|
||||
await module.initHistoryView();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize history view:", error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscriptions view:
|
||||
|
||||
19
views/history.html
Normal file
19
views/history.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!-- views/history.html -->
|
||||
<section id="watchHistoryView" class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold text-white">Watch History</h2>
|
||||
</div>
|
||||
<div id="watchHistoryLoading" class="flex justify-center py-12">
|
||||
<div class="h-10 w-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<div
|
||||
id="watchHistoryGrid"
|
||||
class="hidden"
|
||||
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 2rem;"
|
||||
></div>
|
||||
<p id="watchHistoryEmpty" class="hidden text-gray-400 text-center py-12">
|
||||
Your watch history is empty. Watch some videos to populate this list.
|
||||
</p>
|
||||
<div id="watchHistorySentinel" class="h-1 w-full"></div>
|
||||
<script type="module" src="js/historyView.js"></script>
|
||||
</section>
|
||||
Reference in New Issue
Block a user