統計の追加

This commit is contained in:
Keisuke Hirata 2026-04-09 05:03:26 +09:00
parent 4c19c6491c
commit 87f5cbc75c
3 changed files with 136 additions and 27 deletions

View File

@ -19,7 +19,11 @@ type MessageKey =
| "fetchViewsProgress"
| "fetchViewsDone"
| "colSettings"
| "colSettingsReset";
| "colSettingsReset"
| "statsDuration"
| "statsChannels"
| "statsPlayable"
| "statsTotalViews";
const messages: Record<string, Record<MessageKey, string>> = {
ja: {
@ -44,6 +48,10 @@ const messages: Record<string, Record<MessageKey, string>> = {
fetchViewsDone: "取得完了",
colSettings: "表示",
colSettingsReset: "リセット",
statsDuration: "合計時間",
statsChannels: "チャンネル",
statsPlayable: "再生可能",
statsTotalViews: "総再生数",
},
en: {
colIndex: "#",
@ -67,6 +75,10 @@ const messages: Record<string, Record<MessageKey, string>> = {
fetchViewsDone: "Done",
colSettings: "View",
colSettingsReset: "Reset",
statsDuration: "Total",
statsChannels: "Channels",
statsPlayable: "Playable",
statsTotalViews: "Total views",
},
};

View File

@ -49,6 +49,46 @@ html[dark] .ytpf-toggle {
transform: rotate(-90deg);
}
.ytpf-stats {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 16px;
flex-wrap: wrap;
border-bottom: 1px solid var(--yt-spec-10-percent-layer, rgba(0,0,0,0.06));
}
html[dark] .ytpf-stats {
border-bottom-color: rgba(255,255,255,0.06);
}
.ytpf-stats--hidden {
display: none;
}
.ytpf-stat {
font-size: 12px;
color: var(--yt-spec-text-secondary, #606060);
white-space: nowrap;
}
html[dark] .ytpf-stat {
color: #aaa;
}
.ytpf-stat-value {
font-family: "Roboto Mono", monospace;
color: var(--yt-spec-text-primary, #0f0f0f);
}
html[dark] .ytpf-stat-value {
color: #f1f1f1;
}
.ytpf-stats-spacer {
flex: 1;
}
.ytpf-filters {
display: flex;
align-items: flex-start;

View File

@ -393,35 +393,10 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
}
updateHeader();
const metaSpan = document.createElement("span");
metaSpan.className = "ytpf-header-meta";
if (data.metadata.totalDurationText) {
metaSpan.textContent = data.metadata.totalDurationText;
}
const toggle = document.createElement("span");
toggle.className = "ytpf-toggle";
toggle.textContent = "\u25BC";
// Fetch details button
const fetchBtn = document.createElement("button");
fetchBtn.className = "ytpf-fetch-views-btn";
fetchBtn.textContent = t("fetchViews");
let detailsFetched = false;
fetchBtn.addEventListener("click", (e) => {
e.stopPropagation();
if (detailsFetched) return;
const targetIds = videos.filter((v) => v.isPlayable).map((v) => v.videoId);
if (targetIds.length === 0) return;
detailsFetched = true;
fetchBtn.disabled = true;
fetchBtn.textContent = `${t("fetchViewsProgress")} 0/${targetIds.length}`;
browser.runtime.sendMessage({
type: "FETCH_VIDEO_DETAILS",
videoIds: targetIds,
} satisfies Message);
});
// ⋮ kebab menu
const menuAnchor = document.createElement("div");
menuAnchor.className = "ytpf-menu-anchor";
@ -508,9 +483,87 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
}
});
header.append(titleSpan, metaSpan, fetchBtn, toggle);
header.append(titleSpan, toggle);
wrapper.appendChild(header);
// Stats bar (between header and filters)
const statsBar = document.createElement("div");
statsBar.className = "ytpf-stats";
function makeStat(label: string, value: string): HTMLElement {
const span = document.createElement("span");
span.className = "ytpf-stat";
span.textContent = `${label}: `;
const val = document.createElement("span");
val.className = "ytpf-stat-value";
val.textContent = value;
span.appendChild(val);
return span;
}
function formatNumber(n: number): string {
return n.toLocaleString();
}
function formatDuration(totalSec: number): string {
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
return `${m}:${String(s).padStart(2, "0")}`;
}
const statDuration = makeStat(t("statsDuration"), data.metadata.totalDurationText ?? "--");
const statChannels = makeStat(t("statsChannels"), "0");
const statPlayable = makeStat(t("statsPlayable"), "0");
const statTotalViews = makeStat(t("statsTotalViews"), "--");
statTotalViews.style.display = "none";
function updateStats() {
const channelSet = new Set(videos.map((v) => v.channel.name));
statChannels.querySelector(".ytpf-stat-value")!.textContent = String(channelSet.size);
const playable = videos.filter((v) => v.isPlayable).length;
statPlayable.querySelector(".ytpf-stat-value")!.textContent =
playable === videos.length ? String(playable) : `${playable}/${videos.length}`;
// Duration from individual videos (more accurate than metadata when appending)
const totalSec = videos.reduce((sum, v) => sum + (v.durationSeconds ?? 0), 0);
if (totalSec > 0) {
statDuration.querySelector(".ytpf-stat-value")!.textContent = formatDuration(totalSec);
}
// Total views (only when some videos have viewCountText)
const viewCounts = videos.map((v) => parseViewCount(v.viewCountText)).filter((n) => n >= 0);
if (viewCounts.length > 0) {
const total = viewCounts.reduce((a, b) => a + b, 0);
statTotalViews.querySelector(".ytpf-stat-value")!.textContent = formatNumber(total);
statTotalViews.style.display = "";
}
}
const statsSpacer = document.createElement("span");
statsSpacer.className = "ytpf-stats-spacer";
// Fetch details button
const fetchBtn = document.createElement("button");
fetchBtn.className = "ytpf-fetch-views-btn";
fetchBtn.textContent = t("fetchViews");
let detailsFetched = false;
fetchBtn.addEventListener("click", (e) => {
e.stopPropagation();
if (detailsFetched) return;
const targetIds = videos.filter((v) => v.isPlayable).map((v) => v.videoId);
if (targetIds.length === 0) return;
detailsFetched = true;
fetchBtn.disabled = true;
fetchBtn.textContent = `${t("fetchViewsProgress")} 0/${targetIds.length}`;
browser.runtime.sendMessage({
type: "FETCH_VIDEO_DETAILS",
videoIds: targetIds,
} satisfies Message);
});
statsBar.append(statDuration, statChannels, statPlayable, statTotalViews, statsSpacer, fetchBtn);
wrapper.appendChild(statsBar);
// Filter bar
const filters = document.createElement("div");
filters.className = "ytpf-filters";
@ -681,6 +734,7 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
titleInput.addEventListener("input", applyFilters);
renderRows();
updateStats();
function updateSortIndicators() {
for (const th of thElements) {
@ -727,6 +781,7 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
// Toggle collapse
header.addEventListener("click", () => {
body.classList.toggle("ytpf-body--hidden");
statsBar.classList.toggle("ytpf-stats--hidden");
filters.classList.toggle("ytpf-filters--hidden");
toggle.classList.toggle("ytpf-toggle--collapsed");
});
@ -747,6 +802,7 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
}
renderRows();
updateHeader();
updateStats();
}
function setComplete(count: number) {
@ -805,6 +861,7 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
videos.sort(compareFn(sortKey, sortDir));
}
renderRows();
updateStats();
}
return { element: wrapper, appendVideos, setComplete, updateDetails };