Skip to content

Commit

Permalink
feat(transcript): initial webpage transcript view support
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenlx committed Apr 9, 2024
1 parent 48b31ce commit 0249053
Show file tree
Hide file tree
Showing 16 changed files with 317 additions and 62 deletions.
2 changes: 2 additions & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@mx/config": "workspace:*",
"@total-typescript/ts-reset": "^0.5.1",
"@types/eslint": "~8.4.6",
"@types/lodash-es": "^4.17.12",
"@types/mdast": "^4.0.3",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
Expand Down Expand Up @@ -71,6 +72,7 @@
"filenamify": "^6.0.0",
"idb": "^8.0.0",
"iso-639-1": "^3.1.0",
"lodash-es": "^4.17.21",
"maverick.js": "0.41.2",
"mime": "^4.0.1",
"minisearch": "^6.3.0",
Expand Down
5 changes: 3 additions & 2 deletions apps/app/src/components/transcript/cue-line-list.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useVirtualizer } from "@tanstack/react-virtual";
import type { VTTCue } from "media-captions";
import type { VTTCueInit } from "@vidstack/react";
import { forwardRef, useImperativeHandle, useMemo, useRef } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import type { CueSearchResult } from "@/transcript/context";
import type { VTTCueWithId } from "@/transcript/store";
import { CueActions, CueLine } from "./cue-line";

export interface CueLineListRef {
Expand All @@ -12,7 +13,7 @@ export interface CueLineListRef {
export interface CueLineListProps {
className?: string;
searchResult?: CueSearchResult[];
children: VTTCue[];
children: VTTCueWithId[];
}
export const CueLineList = forwardRef<CueLineListRef, CueLineListProps>(
function CueLineList({ children: cues, className, searchResult }, ref) {
Expand Down
4 changes: 2 additions & 2 deletions apps/app/src/components/transcript/cue-line.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { VTTCue } from "media-captions";
import { Notice, htmlToMarkdown } from "obsidian";
import { forwardRef, useMemo } from "react";
import { formatDuration } from "@/lib/hash/format";
import { cn } from "@/lib/utils";
import type { VTTCueWithId } from "@/transcript/store";
import { CopyIcon, PlayIcon } from "../icon";

export interface CueLineProps extends React.HTMLProps<HTMLDivElement> {
Expand All @@ -13,7 +13,7 @@ export interface CueLineProps extends React.HTMLProps<HTMLDivElement> {
}

export interface CueActionsProps {
children: VTTCue;
children: VTTCueWithId;
onPlay?: () => void;
}

Expand Down
7 changes: 2 additions & 5 deletions apps/app/src/lib/remote-player/interface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type {
MediaErrorCode,
TextTrackInit,
VTTContent,
} from "@vidstack/react";
import type { MediaErrorCode, TextTrackInit } from "@vidstack/react";
import type { VTTContent } from "@/transcript/store";
import type { BilibiliPlayerManifest } from "@/web/bili-api/base";
import type { MessageController, Nil } from "../message";
// import { enumerate } from "../must-include";
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/lib/remote-player/lib/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { VTTContent } from "@vidstack/react";
import { LifeCycle } from "@/lib/lifecycle";
import { TimeoutError } from "@/lib/message";
import type { VTTContent } from "@/transcript/store";
import { registerEvents } from "../hook/event-register";
import type { MediaStateRef } from "../hook/handler-register";
import { registerHandlers } from "../hook/handler-register";
Expand Down
14 changes: 14 additions & 0 deletions apps/app/src/lib/view-title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { View } from "obsidian";

export function updateTitle(this: View) {
const newTitle = this.getDisplayText();
this.titleEl.setText(newTitle);

if (
// eslint-disable-next-line deprecation/deprecation
this.app.workspace.activeLeaf === this.leaf &&
this.app.workspace.requestActiveLeafEvents()
) {
this.leaf.updateHeader();
}
}
21 changes: 7 additions & 14 deletions apps/app/src/media-view/remote-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {
import { Player } from "@/components/player";
import type { MediaURL } from "@/info/media-url";
import type { PaneMenuSource } from "@/lib/menu";
import { updateTitle } from "@/lib/view-title";
import { handleWindowMigration } from "@/lib/window-migration";
import { compare } from "@/media-note/note-index/def";
import { compare, toInfoKey } from "@/media-note/note-index/def";
import type MediaExtended from "@/mx-main";
import { isFileMediaInfo } from "../info/media-info";
import type { PlayerComponent } from "./base";
Expand Down Expand Up @@ -78,6 +79,10 @@ export abstract class MediaRemoteView
player.subscribe(({ title }) => {
title;
this.updateTitle();
const source = this.getMediaInfo()!;
if (!source) return;
const id = toInfoKey(source);
this.plugin.cacheStore.updateSourceCache(id, { title });
}),
),
);
Expand Down Expand Up @@ -137,19 +142,7 @@ export abstract class MediaRemoteView
this.render();
}

updateTitle() {
const newTitle = this.getDisplayText();
this.titleEl.setText(newTitle);

if (
// eslint-disable-next-line deprecation/deprecation
this.app.workspace.activeLeaf === this.leaf &&
this.app.workspace.requestActiveLeafEvents()
) {
this.leaf.updateHeader();
}
}

updateTitle = updateTitle;
render() {
if (this.root) {
// this.contentEl
Expand Down
5 changes: 4 additions & 1 deletion apps/app/src/transcript/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { format, langCodeToLabel } from "@/lib/lang/lang";

export const localTranscriptViewType = "mx-transcript-local";
export const transcriptViewType = {
local: "mx-transcript-local",
webpage: "mx-transcript-webpage",
};

export const supportedCaptionExts = ["vtt", "srt", "ass", "ssa"] as const;
export type SupportedCaptionExt = (typeof supportedCaptionExts)[number];
Expand Down
42 changes: 34 additions & 8 deletions apps/app/src/transcript/context.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import type {
CaptionsFileFormat,
ParsedCaptionsResult,
VTTCue,
} from "media-captions";
import type { CaptionsFileFormat, VTTCue } from "media-captions";
import { parseText } from "media-captions";
import MiniSearch from "minisearch";
import { createContext, useContext } from "react";
import { createStore, useStore } from "zustand";
import type { MediaURL } from "@/info/media-url";
import { toInfoKey } from "@/media-note/note-index/def";
import type MxPlugin from "@/mx-main";
import type { MxSettings } from "@/settings/def";
import "./style.less";
import type { VTTContent, VTTCueWithId } from "./store";

interface TranscriptViewState {
showSearchBox: boolean;
source: {
url: MediaURL;
id: string;
/** track label for lang, etc */
label?: string;
} | null;
title?: string;
setSource(
source: { id: string; url: MediaURL },
plugin: MxPlugin,
): Promise<void>;
toggleSearchBox(val?: boolean): void;
captions: {
_minisearch: MiniSearch<VTTCue> | null;
result: ParsedCaptionsResult;
result: VTTContent;
locales: string[];
} | null;
setCaptions(
result: {
result: ParsedCaptionsResult;
result: VTTContent;
locales: string[];
} | null,
): void;
Expand All @@ -45,6 +55,22 @@ export interface CueSearchResult {

export function createTranscriptViewStore() {
const store = createStore<TranscriptViewState>((set, get, _store) => ({
source: null,
async setSource(src, plugin) {
const sid = toInfoKey(src.url);
const [title, caption] = await Promise.all([
plugin.cacheStore.getTitle(sid),
plugin.cacheStore.getCaption(sid, src.id),
]);
if (title) set({ title });
if (!caption) {
set({ source: src });
} else {
set({ source: { ...src, label: caption.label || caption.language } });
const locales = caption.language ? [caption.language] : [];
this.setCaptions({ result: caption.content, locales });
}
},
showSearchBox: false,
toggleSearchBox(val) {
if (typeof val === "boolean") {
Expand All @@ -66,7 +92,7 @@ export function createTranscriptViewStore() {
const { locales, result } = info;
get().captions?._minisearch?.removeAll();
const segmenter = getSegmenter(...info.locales);
const minisearch = new MiniSearch<VTTCue>({
const minisearch = new MiniSearch<VTTCueWithId>({
idField: "id",
fields: ["text"],
tokenize: segmenter
Expand Down
8 changes: 4 additions & 4 deletions apps/app/src/transcript/file-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ import type MxPlugin from "@/mx-main";
import {
isCaptionFile,
isSupportedCaptionExt,
localTranscriptViewType,
supportedCaptionExts,
toTrack,
transcriptViewType,
} from "./const";
import { createTranscriptViewStore, TranscriptViewContext } from "./context";

export class LocalTranscriptView extends EditableFileView {
static register(plugin: MxPlugin) {
plugin.registerView(
localTranscriptViewType,
transcriptViewType.local,
(leaf) => new LocalTranscriptView(leaf, plugin),
);
plugin.registerExtensions(
supportedCaptionExts as unknown as string[],
localTranscriptViewType,
transcriptViewType.local,
);
}

Expand All @@ -38,7 +38,7 @@ export class LocalTranscriptView extends EditableFileView {
return isSupportedCaptionExt(extension);
}
getViewType(): string {
return localTranscriptViewType;
return transcriptViewType.local;
}
async onLoadFile(file: TFile): Promise<void> {
await super.onLoadFile(file);
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/transcript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { around } from "monkey-around";
import type { Command } from "obsidian";
import type MxPlugin from "@/mx-main";
import { LocalTranscriptView } from "./file-view";
import { WebpageTranscriptView } from "./webpage-view";

declare module "obsidian" {
interface App {
Expand All @@ -13,6 +14,7 @@ declare module "obsidian" {

export function registerTranscriptView(plugin: MxPlugin) {
LocalTranscriptView.register(plugin);
WebpageTranscriptView.register(plugin);
plugin.app.workspace.onLayoutReady(() =>
plugin.register(
around(plugin.app.commands.commands["editor:open-search"] as Command, {
Expand Down
Loading

0 comments on commit 0249053

Please sign in to comment.