Compare commits

..

No commits in common. "0e1869aacf288a2212aeba4012f11ece5e6e78ec" and "005fb72a4850be89579425aee4f16d83608e2068" have entirely different histories.

7 changed files with 54 additions and 73 deletions

View File

@ -3,7 +3,7 @@
"name": "YT Playlist Features",
"version": "0.1.0",
"description": "Extract and work with YouTube playlist data",
"permissions": [],
"permissions": ["storage"],
"background": {
"service_worker": "background/service-worker.js"
},

View File

@ -3,7 +3,7 @@
"name": "YT Playlist Features",
"version": "0.1.0",
"description": "Extract and work with YouTube playlist data",
"permissions": [],
"permissions": ["storage"],
"background": {
"scripts": ["background/service-worker.js"]
},

View File

@ -1,5 +1,6 @@
import browser from "webextension-polyfill";
import type { Message } from "../shared/messages";
import { savePlaylist, getPlaylist } from "../shared/storage";
const LOG_PREFIX = "[yt-playlist-features:bg]";
@ -10,8 +11,15 @@ browser.runtime.onMessage.addListener(
if (msg.type === "PLAYLIST_EXTRACTED") {
console.log(
LOG_PREFIX,
`Playlist: "${msg.data.metadata.title}" (${msg.data.extractedCount} videos)`,
`Saving playlist: "${msg.data.metadata.title}" (${msg.data.extractedCount} videos)`,
);
await savePlaylist(msg.data);
return;
}
if (msg.type === "GET_PLAYLIST") {
const data = await getPlaylist(msg.playlistId);
return { type: "PLAYLIST_RESPONSE", data } satisfies Message;
}
},
);

View File

@ -1,52 +0,0 @@
type MessageKey =
| "colIndex"
| "colTitle"
| "colChannel"
| "colDuration"
| "colAddedBy"
| "colVotes"
| "filterTitle"
| "filterChannel"
| "filterAddedBy"
| "badgeLive"
| "headerVideos";
const messages: Record<string, Record<MessageKey, string>> = {
ja: {
colIndex: "#",
colTitle: "タイトル",
colChannel: "チャンネル",
colDuration: "長さ",
colAddedBy: "追加者",
colVotes: "投票",
filterTitle: "タイトル検索...",
filterChannel: "チャンネル...",
filterAddedBy: "追加者...",
badgeLive: "ライブ",
headerVideos: "本の動画",
},
en: {
colIndex: "#",
colTitle: "Title",
colChannel: "Channel",
colDuration: "Duration",
colAddedBy: "Added by",
colVotes: "Votes",
filterTitle: "Search title...",
filterChannel: "Channel...",
filterAddedBy: "Added by...",
badgeLive: "LIVE",
headerVideos: "videos",
},
};
function detectLang(): string {
const htmlLang = document.documentElement.lang;
if (htmlLang?.startsWith("ja")) return "ja";
return "en";
}
export function t(key: MessageKey): string {
const lang = detectLang();
return (messages[lang] ?? messages.en)[key];
}

View File

@ -1,5 +1,4 @@
import type { PlaylistData, PlaylistVideo } from "../../types/playlist";
import { t } from "./i18n";
type SortKey = "index" | "title" | "channel" | "duration" | "addedBy" | "votes";
type SortDir = "asc" | "desc";
@ -11,16 +10,14 @@ interface Column {
collab?: boolean; // only shown for collaborative playlists
}
function getAllColumns(): Column[] {
return [
{ label: t("colIndex"), cls: "ytpf-col-index", key: "index" },
{ label: t("colTitle"), cls: "ytpf-col-title", key: "title" },
{ label: t("colChannel"), cls: "ytpf-col-channel", key: "channel" },
{ label: t("colDuration"), cls: "ytpf-col-duration", key: "duration" },
{ label: t("colAddedBy"), cls: "ytpf-col-addedby", key: "addedBy", collab: true },
{ label: t("colVotes"), cls: "ytpf-col-votes", key: "votes", collab: true },
];
}
const allColumns: Column[] = [
{ label: "#", cls: "ytpf-col-index", key: "index" },
{ label: "Title", cls: "ytpf-col-title", key: "title" },
{ label: "Channel", cls: "ytpf-col-channel", key: "channel" },
{ label: "Duration", cls: "ytpf-col-duration", key: "duration" },
{ label: "Added by", cls: "ytpf-col-addedby", key: "addedBy", collab: true },
{ label: "Votes", cls: "ytpf-col-votes", key: "votes", collab: true },
];
interface TagInput {
container: HTMLElement;
@ -254,7 +251,7 @@ function buildRow(
if (video.isLive) {
const badge = document.createElement("span");
badge.className = "ytpf-live";
badge.textContent = t("badgeLive");
badge.textContent = "LIVE";
tdDuration.appendChild(badge);
} else {
tdDuration.textContent = video.durationText ?? "--";
@ -282,7 +279,7 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
const isCollab = data.videos.some(
(v) => v.addedBy != null || v.voteCount != null,
);
const columns = getAllColumns().filter((c) => !c.collab || isCollab);
const columns = allColumns.filter((c) => !c.collab || isCollab);
const wrapper = document.createElement("div");
wrapper.className = "ytpf-wrapper";
@ -293,7 +290,7 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
const titleSpan = document.createElement("span");
titleSpan.className = "ytpf-header-title";
titleSpan.textContent = `${data.metadata.title}${data.extractedCount} ${t("headerVideos")}`;
titleSpan.textContent = `${data.metadata.title}${data.extractedCount} videos`;
const metaSpan = document.createElement("span");
metaSpan.className = "ytpf-header-meta";
@ -316,12 +313,12 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
titleInput.className = "ytpf-filter-input";
titleInput.type = "text";
titleInput.name = "ytpf-title-filter";
titleInput.placeholder = t("filterTitle");
titleInput.placeholder = "タイトル検索...";
filters.appendChild(titleInput);
// Channel tag input
const channelNames = [...new Set(data.videos.map((v) => v.channel.name))].sort();
const channelTagInput = createTagInput(t("filterChannel"), channelNames, () => applyFilters());
const channelTagInput = createTagInput("チャンネル...", channelNames, () => applyFilters());
filters.appendChild(channelTagInput.container);
// Added-by tag input (collab only)
@ -330,7 +327,7 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
const addedByNames = [...new Set(
data.videos.map((v) => v.addedBy).filter((n): n is string => n != null),
)].sort();
addedByTagInput = createTagInput(t("filterAddedBy"), addedByNames, () => applyFilters());
addedByTagInput = createTagInput("追加者...", addedByNames, () => applyFilters());
filters.appendChild(addedByTagInput.container);
}

View File

@ -1,3 +1,6 @@
import type { PlaylistData } from "../types/playlist";
export type Message = { type: "PLAYLIST_EXTRACTED"; data: PlaylistData };
export type Message =
| { type: "PLAYLIST_EXTRACTED"; data: PlaylistData }
| { type: "GET_PLAYLIST"; playlistId: string }
| { type: "PLAYLIST_RESPONSE"; data: PlaylistData | null };

25
src/shared/storage.ts Normal file
View File

@ -0,0 +1,25 @@
import browser from "webextension-polyfill";
import type { PlaylistData } from "../types/playlist";
const STORAGE_PREFIX = "playlist:";
export async function savePlaylist(data: PlaylistData): Promise<void> {
await browser.storage.local.set({
[STORAGE_PREFIX + data.metadata.playlistId]: data,
});
}
export async function getPlaylist(
playlistId: string,
): Promise<PlaylistData | null> {
const key = STORAGE_PREFIX + playlistId;
const result = await browser.storage.local.get(key);
return (result[key] as PlaylistData) ?? null;
}
export async function getAllPlaylists(): Promise<PlaylistData[]> {
const all = await browser.storage.local.get(null);
return Object.entries(all)
.filter(([key]) => key.startsWith(STORAGE_PREFIX))
.map(([, value]) => value as PlaylistData);
}