統計の追加
This commit is contained in:
parent
4c19c6491c
commit
87f5cbc75c
|
|
@ -19,7 +19,11 @@ type MessageKey =
|
||||||
| "fetchViewsProgress"
|
| "fetchViewsProgress"
|
||||||
| "fetchViewsDone"
|
| "fetchViewsDone"
|
||||||
| "colSettings"
|
| "colSettings"
|
||||||
| "colSettingsReset";
|
| "colSettingsReset"
|
||||||
|
| "statsDuration"
|
||||||
|
| "statsChannels"
|
||||||
|
| "statsPlayable"
|
||||||
|
| "statsTotalViews";
|
||||||
|
|
||||||
const messages: Record<string, Record<MessageKey, string>> = {
|
const messages: Record<string, Record<MessageKey, string>> = {
|
||||||
ja: {
|
ja: {
|
||||||
|
|
@ -44,6 +48,10 @@ const messages: Record<string, Record<MessageKey, string>> = {
|
||||||
fetchViewsDone: "取得完了",
|
fetchViewsDone: "取得完了",
|
||||||
colSettings: "表示",
|
colSettings: "表示",
|
||||||
colSettingsReset: "リセット",
|
colSettingsReset: "リセット",
|
||||||
|
statsDuration: "合計時間",
|
||||||
|
statsChannels: "チャンネル",
|
||||||
|
statsPlayable: "再生可能",
|
||||||
|
statsTotalViews: "総再生数",
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
colIndex: "#",
|
colIndex: "#",
|
||||||
|
|
@ -67,6 +75,10 @@ const messages: Record<string, Record<MessageKey, string>> = {
|
||||||
fetchViewsDone: "Done",
|
fetchViewsDone: "Done",
|
||||||
colSettings: "View",
|
colSettings: "View",
|
||||||
colSettingsReset: "Reset",
|
colSettingsReset: "Reset",
|
||||||
|
statsDuration: "Total",
|
||||||
|
statsChannels: "Channels",
|
||||||
|
statsPlayable: "Playable",
|
||||||
|
statsTotalViews: "Total views",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,46 @@ html[dark] .ytpf-toggle {
|
||||||
transform: rotate(-90deg);
|
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 {
|
.ytpf-filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
|
||||||
|
|
@ -393,35 +393,10 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
|
||||||
}
|
}
|
||||||
updateHeader();
|
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");
|
const toggle = document.createElement("span");
|
||||||
toggle.className = "ytpf-toggle";
|
toggle.className = "ytpf-toggle";
|
||||||
toggle.textContent = "\u25BC";
|
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
|
// ⋮ kebab menu
|
||||||
const menuAnchor = document.createElement("div");
|
const menuAnchor = document.createElement("div");
|
||||||
menuAnchor.className = "ytpf-menu-anchor";
|
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);
|
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
|
// Filter bar
|
||||||
const filters = document.createElement("div");
|
const filters = document.createElement("div");
|
||||||
filters.className = "ytpf-filters";
|
filters.className = "ytpf-filters";
|
||||||
|
|
@ -681,6 +734,7 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
|
||||||
titleInput.addEventListener("input", applyFilters);
|
titleInput.addEventListener("input", applyFilters);
|
||||||
|
|
||||||
renderRows();
|
renderRows();
|
||||||
|
updateStats();
|
||||||
|
|
||||||
function updateSortIndicators() {
|
function updateSortIndicators() {
|
||||||
for (const th of thElements) {
|
for (const th of thElements) {
|
||||||
|
|
@ -727,6 +781,7 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
|
||||||
// Toggle collapse
|
// Toggle collapse
|
||||||
header.addEventListener("click", () => {
|
header.addEventListener("click", () => {
|
||||||
body.classList.toggle("ytpf-body--hidden");
|
body.classList.toggle("ytpf-body--hidden");
|
||||||
|
statsBar.classList.toggle("ytpf-stats--hidden");
|
||||||
filters.classList.toggle("ytpf-filters--hidden");
|
filters.classList.toggle("ytpf-filters--hidden");
|
||||||
toggle.classList.toggle("ytpf-toggle--collapsed");
|
toggle.classList.toggle("ytpf-toggle--collapsed");
|
||||||
});
|
});
|
||||||
|
|
@ -747,6 +802,7 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
|
||||||
}
|
}
|
||||||
renderRows();
|
renderRows();
|
||||||
updateHeader();
|
updateHeader();
|
||||||
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setComplete(count: number) {
|
function setComplete(count: number) {
|
||||||
|
|
@ -805,6 +861,7 @@ export function renderPlaylistTable(data: PlaylistData, columnPrefs: ColumnPrefs
|
||||||
videos.sort(compareFn(sortKey, sortDir));
|
videos.sort(compareFn(sortKey, sortDir));
|
||||||
}
|
}
|
||||||
renderRows();
|
renderRows();
|
||||||
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { element: wrapper, appendVideos, setComplete, updateDetails };
|
return { element: wrapper, appendVideos, setComplete, updateDetails };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user