統計の追加
This commit is contained in:
parent
4c19c6491c
commit
87f5cbc75c
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user