Skip to content

Commit

Permalink
feat(transcript): timestamp link copy and dnd
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenlx committed May 8, 2024
1 parent 147074f commit d2c9c47
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 30 deletions.
15 changes: 11 additions & 4 deletions apps/app/src/components/transcript/cue-line-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ export interface CueLineListProps {
children: VTTCueWithId[];
activeCueIDs?: Set<string>;
onPlay?: (evt: React.MouseEvent | React.KeyboardEvent, time: number) => void;
onCopy?: (
evt: React.MouseEvent | React.KeyboardEvent,
time: number,
text: string,
) => void;
}
export const CueLineList = forwardRef<CueLineListRef, CueLineListProps>(
function CueLineList(
{ children: cues, className, searchResult, onPlay, activeCueIDs },
{ children: cues, className, searchResult, onPlay, onCopy, activeCueIDs },
ref,
) {
const parentRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -93,9 +98,11 @@ export const CueLineList = forwardRef<CueLineListRef, CueLineListProps>(
actions={
<CueActions
onPlay={onPlay && ((evt) => onPlay(evt, cue.startTime))}
>
{cue}
</CueActions>
onCopy={
onCopy &&
((evt) => onCopy(evt, cue.startTime, cue.text))
}
/>
}
>
{cue.text}
Expand Down
63 changes: 38 additions & 25 deletions apps/app/src/components/transcript/cue-line.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Notice, htmlToMarkdown } from "obsidian";
import { forwardRef, useMemo } from "react";
import { forwardRef, useMemo, useRef } from "react";
import { useMergeRefs } from "use-callback-ref";
import { formatDuration } from "@/lib/hash/format";
import { cn } from "@/lib/utils";
import type { VTTCueWithId } from "@/transcript/handle/type";
import { useTimestampLink } from "@/transcript/view/context";
import { CopyIcon, PlayIcon } from "../icon";

export interface CueLineProps extends React.HTMLProps<HTMLDivElement> {
Expand All @@ -14,31 +14,27 @@ export interface CueLineProps extends React.HTMLProps<HTMLDivElement> {
}

export interface CueActionsProps {
children: VTTCueWithId;
// children: VTTCueWithId;
onPlay?: (evt: React.MouseEvent | React.KeyboardEvent) => void;
onCopy?: (evt: React.MouseEvent | React.KeyboardEvent) => void;
}

export function CueActions({ children: cue, onPlay }: CueActionsProps) {
async function handleCopy() {
const { text, startTime } = cue;
const mainText = htmlToMarkdown(`<pre>${text}</pre>`);
const timestamp = `\\[${formatDuration(startTime)}\\]`;
await navigator.clipboard.writeText(`${timestamp} ${mainText}`);
new Notice("Copied to clipboard");
}
export function CueActions({ onPlay, onCopy }: CueActionsProps) {
return (
<>
<div
role="button"
tabIndex={0}
onClick={handleCopy}
aria-label="Copy markdown"
onKeyDown={(evt) => {
if (evt.key === "Enter") handleCopy();
}}
>
<CopyIcon className="w-3 h-3" />
</div>
{onCopy && (
<div
role="button"
tabIndex={0}
onClick={onCopy}
aria-label="Copy markdown"
onKeyDown={(evt) => {
if (evt.key === "Enter") onCopy(evt);
}}
>
<CopyIcon className="w-3 h-3" />
</div>
)}
{onPlay && (
<div
role="button"
Expand Down Expand Up @@ -75,10 +71,16 @@ export const CueLine = forwardRef<HTMLDivElement, CueLineProps>(
insertLineBreaks(content, segments, 0);
return segments;
})(content);

const getTimestamp = useTimestampLink();

const containerRef = useRef<HTMLDivElement>(null);
const domRef = useMergeRefs([ref, containerRef]);

return (
<div
{...props}
ref={ref}
ref={domRef}
className={cn(
"grid items-center group hover:bg-accent pr-2 py-1 mr-1 transition-all rounded-md hover:delay-100",
className,
Expand All @@ -87,7 +89,18 @@ export const CueLine = forwardRef<HTMLDivElement, CueLineProps>(
)}
>
<div className="text-xs font-medium select-none text-gray-400 group-hover:text-black group-hover:pl-1 transition-all flex items-center group-hover:delay-100">
<div className="flex cursor-pointer items-center">
<div
className="flex cursor-pointer items-center"
draggable
onDragStart={(evt) => {
evt.dataTransfer.setData(
"text/plain",
getTimestamp(time, content).text,
);
evt.dataTransfer.dropEffect = "copy";
evt.dataTransfer.setDragImage(containerRef.current!, 0, 0);
}}
>
<Timestamp>{time}</Timestamp>
<div className="ml-1 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity group-hover:duration-300 group-hover:delay-100">
{actions}
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/components/transcript/line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assertNever } from "assert-never";
import { useMemo, useRef, useState } from "react";
import type { CueSearchResult } from "@/transcript/view/context";
import {
useCopy,
usePlay,
useSearch,
useSearchBox,
Expand All @@ -15,6 +16,7 @@ export default function Lines() {
const cues = useTranscriptViewStore((s) => s.textTrack?.content.cues || []);
const activeCueIDs = useTranscriptViewStore((s) => s.activeCueIDs);
const play = usePlay();
const copy = useCopy();
const cueHash = useMemo(
() => new Map(cues.map((cue, index) => [cue.id, { cue, index }])),
[cues],
Expand Down Expand Up @@ -102,6 +104,7 @@ export default function Lines() {
className="p-[var(--file-margins)] pt-0"
ref={cueListRef}
onPlay={play}
onCopy={copy}
activeCueIDs={activeCueIDs}
searchResult={searchResult}
>
Expand Down
4 changes: 3 additions & 1 deletion apps/app/src/components/use-tracks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ export function useTextTracks() {
if (!provider) return new Response(null, { status: 500 });
const vtt = await provider.media.methods.getTrack(id);
if (!vtt) return new Response(null, { status: 404 });
return Response.json({ cues: vtt.cues, regions: vtt.regions });
return new Response(JSON.stringify(vtt), {
headers: { "Content-Type": "application/json" },
});
};

const defaultTrackPredicate = genDefaultTrackPredicate(
Expand Down
36 changes: 36 additions & 0 deletions apps/app/src/transcript/view/context.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { VTTCue } from "media-captions";
import MiniSearch from "minisearch";
import { Notice, htmlToMarkdown } from "obsidian";
import { createContext, useContext } from "react";
// eslint-disable-next-line import/no-deprecated
import { createStore, useStore } from "zustand";
import type { MediaInfo } from "@/info/media-info";
import { MediaURL } from "@/info/media-url";
import type { ParsedTextTrack } from "@/info/track-info";
import { formatDuration } from "@/lib/hash/format";
import { langCodeToLabel, vaildate } from "@/lib/lang/lang";
import { compare } from "@/media-note/note-index/def";
import { timestampGenerator } from "@/media-note/timestamp/utils";
import type MxPlugin from "@/mx-main";
import { isModEvent } from "@/patch/mod-evt";
import type { MxSettings } from "@/settings/def";
Expand Down Expand Up @@ -195,3 +198,36 @@ export function usePlay() {
});
};
}

export function useTimestampLink() {
const plugin = usePlugin();
const media = useTranscriptViewStore((s) => s.media);
if (!media)
return function (time: number, text: string) {
const mainText = htmlToMarkdown(`<pre>${text}</pre>`);
const timestamp = formatDuration(time);
return { text: `${timestamp} ${mainText}`, timestamp };
};
return function (time: number, text: string) {
const genTimestamp = timestampGenerator(time, media, {
app: plugin.app,
settings: plugin.settings.getState(),
});
const timestamp = genTimestamp("");
const mainText = htmlToMarkdown(`<pre>${text}</pre>`);
return { text: `${timestamp} ${mainText}`, timestamp };
};
}

export function useCopy() {
const buildTimestampLink = useTimestampLink();
return async (
_: React.MouseEvent | React.KeyboardEvent,
time: number,
text: string,
) => {
const { text: content, timestamp } = await buildTimestampLink(time, text);
await navigator.clipboard.writeText(content);
new Notice(`Copied at ${timestamp} to clipboard`);
};
}

0 comments on commit d2c9c47

Please sign in to comment.