Voteのサポート
This commit is contained in:
parent
980bc54f33
commit
3635b58fed
|
|
@ -181,6 +181,8 @@ function parseVideo(renderer: any): PlaylistVideo | null {
|
||||||
addedBy: null,
|
addedBy: null,
|
||||||
voteCount: null,
|
voteCount: null,
|
||||||
likeCount: null,
|
likeCount: null,
|
||||||
|
setVideoId: null,
|
||||||
|
voteStatus: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { onPlaylistPageReady, getPlaylistId } from "./navigation";
|
||||||
import { parseInitialData, buildPlaylistData } from "./extractor";
|
import { parseInitialData, buildPlaylistData } from "./extractor";
|
||||||
import type { PlaylistVideo } from "../types/playlist";
|
import type { PlaylistVideo } from "../types/playlist";
|
||||||
import type { Message } from "../shared/messages";
|
import type { Message } from "../shared/messages";
|
||||||
import { mountTable, appendToTable, setTableComplete, updateTableDetails } from "./ui/lifecycle";
|
import { mountTable, appendToTable, setTableComplete, updateTableDetails, updateVoteStatuses } from "./ui/lifecycle";
|
||||||
|
|
||||||
const LOG = "[yt-playlist-features]";
|
const LOG = "[yt-playlist-features]";
|
||||||
|
|
||||||
|
|
@ -38,6 +38,8 @@ function mapRawVideo(v: any): PlaylistVideo {
|
||||||
addedBy: v.addedBy ?? null,
|
addedBy: v.addedBy ?? null,
|
||||||
voteCount: v.voteCount ?? null,
|
voteCount: v.voteCount ?? null,
|
||||||
likeCount: v.likeCount ?? null,
|
likeCount: v.likeCount ?? null,
|
||||||
|
setVideoId: v.setVideoId ?? null,
|
||||||
|
voteStatus: v.voteStatus ?? null,
|
||||||
} satisfies PlaylistVideo;
|
} satisfies PlaylistVideo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,6 +124,7 @@ function handlePlaylistAppend(event: Event): void {
|
||||||
document.addEventListener("__yt_playlist_ext_data", handlePlaylistData);
|
document.addEventListener("__yt_playlist_ext_data", handlePlaylistData);
|
||||||
document.addEventListener("__yt_playlist_ext_data", handlePlaylistAppend);
|
document.addEventListener("__yt_playlist_ext_data", handlePlaylistAppend);
|
||||||
|
|
||||||
|
|
||||||
// Listen for detail updates from background service worker
|
// Listen for detail updates from background service worker
|
||||||
browser.runtime.onMessage.addListener((message: unknown) => {
|
browser.runtime.onMessage.addListener((message: unknown) => {
|
||||||
const msg = message as Message;
|
const msg = message as Message;
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,10 @@ export function updateTableDetails(updates: DetailUpdate[]): void {
|
||||||
currentHandle?.updateDetails(updates);
|
currentHandle?.updateDetails(updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateVoteStatuses(statuses: { setVideoId: string; voteStatus: "up" | "down" | null }[]): void {
|
||||||
|
currentHandle?.updateVoteStatuses(statuses);
|
||||||
|
}
|
||||||
|
|
||||||
export function unmountTable(): void {
|
export function unmountTable(): void {
|
||||||
if (pendingObserver) {
|
if (pendingObserver) {
|
||||||
pendingObserver.disconnect();
|
pendingObserver.disconnect();
|
||||||
|
|
|
||||||
|
|
@ -515,7 +515,70 @@ html[dark] .ytpf-tr:hover {
|
||||||
.ytpf-col-category { width: 120px; }
|
.ytpf-col-category { width: 120px; }
|
||||||
.ytpf-col-likes { width: 90px; text-align: right; font-family: "Roboto Mono", monospace; }
|
.ytpf-col-likes { width: 90px; text-align: right; font-family: "Roboto Mono", monospace; }
|
||||||
.ytpf-col-addedby { width: 140px; }
|
.ytpf-col-addedby { width: 140px; }
|
||||||
.ytpf-col-votes { width: 60px; text-align: center; font-family: "Roboto Mono", monospace; }
|
.ytpf-col-votes { width: 90px; text-align: center; font-family: "Roboto Mono", monospace; }
|
||||||
|
|
||||||
|
.ytpf-vote {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytpf-vote-btn {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--yt-spec-text-secondary, #606060);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dark] .ytpf-vote-btn {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytpf-vote-btn:hover:not(:disabled) {
|
||||||
|
background: var(--yt-spec-badge-chip-background, #f2f2f2);
|
||||||
|
color: var(--yt-spec-text-primary, #0f0f0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dark] .ytpf-vote-btn:hover:not(:disabled) {
|
||||||
|
background: #3e3e3e;
|
||||||
|
color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytpf-vote-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytpf-vote-count {
|
||||||
|
min-width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytpf-vote-count--voted {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytpf-vote-btn--active {
|
||||||
|
color: var(--yt-spec-call-to-action, #065fd4);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dark] .ytpf-vote-btn--active {
|
||||||
|
color: #3ea6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytpf-vote-btn--dim {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
.ytpf-fetch-views-btn {
|
.ytpf-fetch-views-btn {
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,14 @@ function compareFn(key: SortKey, dir: SortDir) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCell(video: PlaylistVideo, col: Column, playlistId: string): HTMLTableCellElement {
|
type VoteHandler = (video: PlaylistVideo, action: "up" | "down") => void;
|
||||||
|
|
||||||
|
function buildCell(
|
||||||
|
video: PlaylistVideo,
|
||||||
|
col: Column,
|
||||||
|
playlistId: string,
|
||||||
|
onVote?: VoteHandler,
|
||||||
|
): HTMLTableCellElement {
|
||||||
const td = document.createElement("td");
|
const td = document.createElement("td");
|
||||||
td.className = `ytpf-td ${col.cls}`;
|
td.className = `ytpf-td ${col.cls}`;
|
||||||
|
|
||||||
|
|
@ -341,7 +348,44 @@ function buildCell(video: PlaylistVideo, col: Column, playlistId: string): HTMLT
|
||||||
td.textContent = video.addedBy ?? "--";
|
td.textContent = video.addedBy ?? "--";
|
||||||
break;
|
break;
|
||||||
case "votes":
|
case "votes":
|
||||||
|
if (video.setVideoId && onVote) {
|
||||||
|
const wrap = document.createElement("span");
|
||||||
|
wrap.className = "ytpf-vote";
|
||||||
|
|
||||||
|
const vs = video.voteStatus;
|
||||||
|
|
||||||
|
const upBtn = document.createElement("button");
|
||||||
|
upBtn.className = "ytpf-vote-btn";
|
||||||
|
if (vs === "up") upBtn.classList.add("ytpf-vote-btn--active");
|
||||||
|
else if (vs === "down") upBtn.classList.add("ytpf-vote-btn--dim");
|
||||||
|
upBtn.textContent = "\u25B2";
|
||||||
|
upBtn.title = "Vote up";
|
||||||
|
upBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onVote(video, "up");
|
||||||
|
});
|
||||||
|
|
||||||
|
const countSpan = document.createElement("span");
|
||||||
|
countSpan.className = "ytpf-vote-count";
|
||||||
|
if (vs) countSpan.classList.add("ytpf-vote-count--voted");
|
||||||
|
countSpan.textContent = video.voteCount != null ? String(video.voteCount) : "0";
|
||||||
|
|
||||||
|
const downBtn = document.createElement("button");
|
||||||
|
downBtn.className = "ytpf-vote-btn";
|
||||||
|
if (vs === "down") downBtn.classList.add("ytpf-vote-btn--active");
|
||||||
|
else if (vs === "up") downBtn.classList.add("ytpf-vote-btn--dim");
|
||||||
|
downBtn.textContent = "\u25BC";
|
||||||
|
downBtn.title = "Vote down";
|
||||||
|
downBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onVote(video, "down");
|
||||||
|
});
|
||||||
|
|
||||||
|
wrap.append(upBtn, countSpan, downBtn);
|
||||||
|
td.appendChild(wrap);
|
||||||
|
} else {
|
||||||
td.textContent = video.voteCount != null ? String(video.voteCount) : "--";
|
td.textContent = video.voteCount != null ? String(video.voteCount) : "--";
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -352,13 +396,14 @@ function buildRow(
|
||||||
video: PlaylistVideo,
|
video: PlaylistVideo,
|
||||||
playlistId: string,
|
playlistId: string,
|
||||||
visibleColumns: Column[],
|
visibleColumns: Column[],
|
||||||
|
onVote?: VoteHandler,
|
||||||
): HTMLTableRowElement {
|
): HTMLTableRowElement {
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.className = "ytpf-tr";
|
tr.className = "ytpf-tr";
|
||||||
if (!video.isPlayable) tr.classList.add("ytpf-tr--unplayable");
|
if (!video.isPlayable) tr.classList.add("ytpf-tr--unplayable");
|
||||||
|
|
||||||
for (const col of visibleColumns) {
|
for (const col of visibleColumns) {
|
||||||
tr.appendChild(buildCell(video, col, playlistId));
|
tr.appendChild(buildCell(video, col, playlistId, onVote));
|
||||||
}
|
}
|
||||||
|
|
||||||
return tr;
|
return tr;
|
||||||
|
|
@ -369,6 +414,7 @@ export interface PlaylistTableHandle {
|
||||||
appendVideos(newVideos: PlaylistVideo[]): void;
|
appendVideos(newVideos: PlaylistVideo[]): void;
|
||||||
setComplete(extractedCount: number): void;
|
setComplete(extractedCount: number): void;
|
||||||
updateDetails(updates: DetailUpdate[]): void;
|
updateDetails(updates: DetailUpdate[]): void;
|
||||||
|
updateVoteStatuses(statuses: { setVideoId: string; voteStatus: "up" | "down" | null }[]): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs): PlaylistTableHandle {
|
export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs): PlaylistTableHandle {
|
||||||
|
|
@ -870,11 +916,81 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vote handler for collaborative playlists
|
||||||
|
const pendingVotes = new Map<string, { prevCount: number; prevStatus: "up" | "down" | null }>();
|
||||||
|
|
||||||
|
const voteHandler: VoteHandler | undefined = isCollab
|
||||||
|
? (video, action) => {
|
||||||
|
if (!video.setVideoId || pendingVotes.has(video.setVideoId)) return;
|
||||||
|
|
||||||
|
// Save previous state for revert
|
||||||
|
const prevCount = video.voteCount ?? 0;
|
||||||
|
const prevStatus = video.voteStatus;
|
||||||
|
pendingVotes.set(video.setVideoId, { prevCount, prevStatus });
|
||||||
|
|
||||||
|
// Optimistic update
|
||||||
|
if (action === "up") {
|
||||||
|
if (video.voteStatus === "up") {
|
||||||
|
video.voteCount = prevCount - 1;
|
||||||
|
video.voteStatus = null;
|
||||||
|
} else {
|
||||||
|
video.voteCount = prevCount + 1;
|
||||||
|
video.voteStatus = "up";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (video.voteStatus === "down") {
|
||||||
|
video.voteCount = prevCount + 1;
|
||||||
|
video.voteStatus = null;
|
||||||
|
} else {
|
||||||
|
video.voteCount = prevCount - 1;
|
||||||
|
video.voteStatus = "down";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderRows();
|
||||||
|
|
||||||
|
// Dispatch to page script
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("__yt_playlist_ext_vote", {
|
||||||
|
detail: JSON.stringify({
|
||||||
|
setVideoId: video.setVideoId,
|
||||||
|
action,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Listen for vote results from page script
|
||||||
|
if (isCollab) {
|
||||||
|
document.addEventListener("__yt_playlist_ext_vote_result", ((e: Event) => {
|
||||||
|
const { setVideoId, success, newVoteStatus } = JSON.parse((e as CustomEvent).detail);
|
||||||
|
const prev = pendingVotes.get(setVideoId);
|
||||||
|
pendingVotes.delete(setVideoId);
|
||||||
|
|
||||||
|
if (!success && prev) {
|
||||||
|
// Revert optimistic update
|
||||||
|
const video = videos.find((v) => v.setVideoId === setVideoId);
|
||||||
|
if (video) {
|
||||||
|
video.voteCount = prev.prevCount;
|
||||||
|
video.voteStatus = prev.prevStatus;
|
||||||
|
renderRows();
|
||||||
|
}
|
||||||
|
} else if (success) {
|
||||||
|
// Confirm new status
|
||||||
|
const video = videos.find((v) => v.setVideoId === setVideoId);
|
||||||
|
if (video) {
|
||||||
|
video.voteStatus = (newVoteStatus as "up" | "down" | null) ?? null;
|
||||||
|
renderRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as EventListener);
|
||||||
|
}
|
||||||
|
|
||||||
function renderRows() {
|
function renderRows() {
|
||||||
const filtered = getFilteredVideos();
|
const filtered = getFilteredVideos();
|
||||||
tbody.textContent = "";
|
tbody.textContent = "";
|
||||||
for (const video of filtered) {
|
for (const video of filtered) {
|
||||||
tbody.appendChild(buildRow(video, playlistId, visibleColumns));
|
tbody.appendChild(buildRow(video, playlistId, visibleColumns, voteHandler));
|
||||||
}
|
}
|
||||||
filterCount.textContent = filtered.length < videos.length
|
filterCount.textContent = filtered.length < videos.length
|
||||||
? `${filtered.length} / ${videos.length}`
|
? `${filtered.length} / ${videos.length}`
|
||||||
|
|
@ -1019,5 +1135,18 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
|
||||||
updateStats();
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { element: wrapper, appendVideos, setComplete, updateDetails };
|
function updateVoteStatuses(statuses: { setVideoId: string; voteStatus: "up" | "down" | null }[]) {
|
||||||
|
const map = new Map(statuses.map((s) => [s.setVideoId, s.voteStatus]));
|
||||||
|
let changed = false;
|
||||||
|
for (const v of videos) {
|
||||||
|
const status = map.get(v.setVideoId ?? "");
|
||||||
|
if (status !== undefined && v.voteStatus !== status) {
|
||||||
|
v.voteStatus = status;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) renderRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { element: wrapper, appendVideos, setComplete, updateDetails, updateVoteStatuses };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,54 @@
|
||||||
let collabCache: Map<string, string> = new Map();
|
let collabCache: Map<string, string> = new Map();
|
||||||
let collabCachePlaylistId: string | null = null;
|
let collabCachePlaylistId: string | null = null;
|
||||||
|
|
||||||
|
// Cache vote feedback tokens per setVideoId
|
||||||
|
interface VoteFeedback {
|
||||||
|
upToken: string | null;
|
||||||
|
upToggledToken: string | null;
|
||||||
|
downToken: string | null;
|
||||||
|
downToggledToken: string | null;
|
||||||
|
isUpToggled: boolean;
|
||||||
|
isDownToggled: boolean;
|
||||||
|
}
|
||||||
|
const voteFeedbackCache = new Map<string, VoteFeedback>();
|
||||||
|
|
||||||
|
function extractTokenFromButtonVM(model: any): string | null {
|
||||||
|
return model?.buttonViewModel?.onTap?.innertubeCommand
|
||||||
|
?.feedbackEndpoint?.feedbackToken ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract vote feedback tokens from playlistVideoRenderer.engagementBar */
|
||||||
|
function extractVoteInfo(renderer: any): "up" | "down" | null {
|
||||||
|
const setVideoId = renderer.setVideoId;
|
||||||
|
if (!setVideoId) return null;
|
||||||
|
|
||||||
|
const actions = renderer.engagementBar?.engagementBarViewModel?.actions;
|
||||||
|
if (!actions) return null;
|
||||||
|
|
||||||
|
for (const action of actions) {
|
||||||
|
const voting = action.votingViewModel;
|
||||||
|
if (!voting) continue;
|
||||||
|
|
||||||
|
const up = voting.upvoteButton?.toggleButtonViewModel;
|
||||||
|
const down = voting.downvoteButton?.toggleButtonViewModel;
|
||||||
|
|
||||||
|
const info: VoteFeedback = {
|
||||||
|
upToken: extractTokenFromButtonVM(up?.defaultButtonViewModel),
|
||||||
|
upToggledToken: extractTokenFromButtonVM(up?.toggledButtonViewModel),
|
||||||
|
downToken: extractTokenFromButtonVM(down?.defaultButtonViewModel),
|
||||||
|
downToggledToken: extractTokenFromButtonVM(down?.toggledButtonViewModel),
|
||||||
|
isUpToggled: !!up?.isToggled,
|
||||||
|
isDownToggled: !!down?.isToggled,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (info.upToken || info.downToken) {
|
||||||
|
voteFeedbackCache.set(setVideoId, info);
|
||||||
|
return info.isUpToggled ? "up" : info.isDownToggled ? "down" : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function runExtraction() {
|
function runExtraction() {
|
||||||
if (!isPlaylistUrl()) return;
|
if (!isPlaylistUrl()) return;
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
|
|
@ -128,6 +176,7 @@
|
||||||
function mapRenderers(renderers: any[], startIndex: number) {
|
function mapRenderers(renderers: any[], startIndex: number) {
|
||||||
return renderers.map((d, i) => {
|
return renderers.map((d, i) => {
|
||||||
const { viewCountText } = parseVideoInfo(d.videoInfo);
|
const { viewCountText } = parseVideoInfo(d.videoInfo);
|
||||||
|
const voteStatus = extractVoteInfo(d);
|
||||||
return {
|
return {
|
||||||
videoId: d.videoId,
|
videoId: d.videoId,
|
||||||
title: textOf(d.title),
|
title: textOf(d.title),
|
||||||
|
|
@ -143,6 +192,8 @@
|
||||||
category: null as string | null,
|
category: null as string | null,
|
||||||
voteCount: typeof d.voteCount === "number" ? d.voteCount : null,
|
voteCount: typeof d.voteCount === "number" ? d.voteCount : null,
|
||||||
addedBy: resolveAddedBy(d, avatarToName),
|
addedBy: resolveAddedBy(d, avatarToName),
|
||||||
|
setVideoId: d.setVideoId ?? null,
|
||||||
|
voteStatus,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +270,7 @@
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
|
|
@ -528,4 +580,82 @@
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vote handler: content script dispatches this event to vote on collaborative playlist videos
|
||||||
|
document.addEventListener("__yt_playlist_ext_vote", async (event: Event) => {
|
||||||
|
const { setVideoId, action } = JSON.parse(
|
||||||
|
(event as CustomEvent).detail,
|
||||||
|
);
|
||||||
|
const { cfg, baseContext } = getConfig();
|
||||||
|
const authHeaders = buildAuthHeaders(cfg, baseContext);
|
||||||
|
|
||||||
|
const cached = voteFeedbackCache.get(setVideoId);
|
||||||
|
if (!cached) {
|
||||||
|
console.warn(LOG, `No vote feedback tokens for ${setVideoId}`);
|
||||||
|
sendVoteResult(setVideoId, false, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick the correct feedbackToken based on action and current toggle state
|
||||||
|
let feedbackToken: string | null = null;
|
||||||
|
if (action === "up") {
|
||||||
|
feedbackToken = cached.isUpToggled ? cached.upToggledToken : cached.upToken;
|
||||||
|
} else {
|
||||||
|
feedbackToken = cached.isDownToggled ? cached.downToggledToken : cached.downToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!feedbackToken) {
|
||||||
|
console.warn(LOG, `No feedback token for ${action} on ${setVideoId}`);
|
||||||
|
sendVoteResult(setVideoId, false, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
"https://www.youtube.com/youtubei/v1/feedback?prettyPrint=false",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...authHeaders,
|
||||||
|
},
|
||||||
|
credentials: "same-origin",
|
||||||
|
body: JSON.stringify({
|
||||||
|
context: baseContext,
|
||||||
|
feedbackTokens: [feedbackToken],
|
||||||
|
isFeedbackTokenUnencrypted: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const success = res.ok && Array.isArray(data?.feedbackResponses);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Update cached toggle state
|
||||||
|
if (action === "up") {
|
||||||
|
cached.isUpToggled = !cached.isUpToggled;
|
||||||
|
if (cached.isUpToggled) cached.isDownToggled = false;
|
||||||
|
} else {
|
||||||
|
cached.isDownToggled = !cached.isDownToggled;
|
||||||
|
if (cached.isDownToggled) cached.isUpToggled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStatus = cached.isUpToggled ? "up" : cached.isDownToggled ? "down" : null;
|
||||||
|
console.log(LOG, `Vote ${action} on ${setVideoId}: ${success ? "OK" : "FAILED"} → ${newStatus}`);
|
||||||
|
sendVoteResult(setVideoId, success, newStatus);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(LOG, "Vote failed:", e);
|
||||||
|
sendVoteResult(setVideoId, false, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendVoteResult(setVideoId: string, success: boolean, newVoteStatus: string | null) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("__yt_playlist_ext_vote_result", {
|
||||||
|
detail: JSON.stringify({ setVideoId, success, newVoteStatus }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ export interface PlaylistVideo {
|
||||||
addedBy: string | null;
|
addedBy: string | null;
|
||||||
/** Vote count / approvals (collaborative playlists only) */
|
/** Vote count / approvals (collaborative playlists only) */
|
||||||
voteCount: number | null;
|
voteCount: number | null;
|
||||||
|
/** Playlist-specific video ID for operations (vote, reorder, etc.) */
|
||||||
|
setVideoId: string | null;
|
||||||
|
/** Current user's vote status on collaborative playlists */
|
||||||
|
voteStatus: "up" | "down" | null;
|
||||||
/** Like count from Data API */
|
/** Like count from Data API */
|
||||||
likeCount: number | null;
|
likeCount: number | null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user