-
Notifications
You must be signed in to change notification settings - Fork 881
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add link parser for audio timestamp navigate
Signed-off-by: Innei <i@innei.in>
- Loading branch information
Showing
8 changed files
with
138 additions
and
123 deletions.
There are no files selected for viewing
27 changes: 27 additions & 0 deletions
27
src/renderer/src/components/ui/markdown/components/TimeStamp.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Player } from "@renderer/atoms/player" | ||
import { nextFrame } from "@renderer/lib/dom" | ||
import { useEntryContentContext } from "@renderer/modules/entry-content/hooks" | ||
|
||
import { timeStringToSeconds } from "../utils" | ||
|
||
export const TimeStamp = (props: { time: string }) => { | ||
const { entryId, audioSrc: src } = useEntryContentContext() | ||
|
||
if (!src) return <span>{props.time}</span> | ||
return ( | ||
<span | ||
className="cursor-pointer tabular-nums text-theme-accent dark:text-theme-accent-500" | ||
onClick={() => { | ||
Player.mount({ | ||
type: "audio", | ||
entryId, | ||
src, | ||
currentTime: 0, | ||
}) | ||
nextFrame(() => Player.seek(timeStringToSeconds(props.time) || 0)) | ||
}} | ||
> | ||
{props.time} | ||
</span> | ||
) | ||
} |
66 changes: 40 additions & 26 deletions
66
src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,50 @@ | ||
import { FeedViewType } from "@renderer/lib/enum" | ||
import { useEntryContentContext } from "@renderer/modules/entry-content/hooks" | ||
|
||
import type { LinkProps } from "../../link" | ||
import { | ||
Tooltip, | ||
TooltipContent, | ||
TooltipPortal, | ||
TooltipTrigger, | ||
} from "../../tooltip" | ||
import { ensureAndRenderTimeStamp } from "../utils" | ||
|
||
export const MarkdownLink = (props: LinkProps) => { | ||
const { view } = useEntryContentContext() | ||
|
||
export const MarkdownLink = (props: LinkProps) => ( | ||
<Tooltip | ||
const parseTimeStamp = view === FeedViewType.Audios | ||
if (parseTimeStamp) { | ||
const childrenText = props.children | ||
|
||
delayDuration={0} | ||
> | ||
<TooltipTrigger asChild> | ||
<a | ||
className="follow-link--underline font-semibold text-foreground no-underline" | ||
href={props.href} | ||
title={props.title} | ||
target="_blank" | ||
> | ||
{props.children} | ||
if (typeof childrenText === "string") { | ||
const renderer = ensureAndRenderTimeStamp(childrenText) | ||
if (renderer) return renderer | ||
} | ||
} | ||
return ( | ||
<Tooltip delayDuration={0}> | ||
<TooltipTrigger asChild> | ||
<a | ||
className="follow-link--underline font-semibold text-foreground no-underline" | ||
href={props.href} | ||
title={props.title} | ||
target="_blank" | ||
> | ||
{props.children} | ||
|
||
{typeof props.children === "string" && ( | ||
<i className="i-mgc-arrow-right-up-cute-re size-[0.9em] translate-y-[2px] opacity-70" /> | ||
)} | ||
</a> | ||
</TooltipTrigger> | ||
{!!props.href && ( | ||
<TooltipPortal> | ||
<TooltipContent align="start" className="break-all" side="bottom"> | ||
{props.href} | ||
</TooltipContent> | ||
</TooltipPortal> | ||
)} | ||
</Tooltip> | ||
) | ||
{typeof props.children === "string" && ( | ||
<i className="i-mgc-arrow-right-up-cute-re size-[0.9em] translate-y-[2px] opacity-70" /> | ||
)} | ||
</a> | ||
</TooltipTrigger> | ||
{!!props.href && ( | ||
<TooltipPortal> | ||
<TooltipContent align="start" className="break-all" side="bottom"> | ||
{props.href} | ||
</TooltipContent> | ||
</TooltipPortal> | ||
)} | ||
</Tooltip> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { TimeStamp } from "../components/TimeStamp" | ||
|
||
export const ensureAndRenderTimeStamp = (children: string) => { | ||
const firstPart = children.replace(" ", " ").split(" ")[0] | ||
// 00:00 , 00:00:00 | ||
if (!firstPart) { | ||
return | ||
} | ||
const isTime = isValidTimeString(firstPart.trim()) | ||
if (isTime) { | ||
return ( | ||
<> | ||
<TimeStamp time={firstPart} /> | ||
<span>{children.slice(firstPart.length)}</span> | ||
</> | ||
) | ||
} | ||
return false | ||
} | ||
function isValidTimeString(time: string): boolean { | ||
const timeRegex = /^\d{1,2}:[0-5]\d(?::[0-5]\d)?$/ | ||
return timeRegex.test(time) | ||
} | ||
|
||
export function timeStringToSeconds(time: string): number | null { | ||
const timeParts = time.split(":").map(Number) | ||
|
||
if (timeParts.length === 2) { | ||
const [minutes, seconds] = timeParts | ||
return minutes * 60 + seconds | ||
} else if (timeParts.length === 3) { | ||
const [hours, minutes, seconds] = timeParts | ||
return hours * 3600 + minutes * 60 + seconds | ||
} else { | ||
return null | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { useContext } from "react" | ||
|
||
import { EntryContentContext } from "./provider" | ||
|
||
export const useEntryContentContext = () => { | ||
const ctx = useContext(EntryContentContext) | ||
if (!ctx) { | ||
throw new Error( | ||
"useEntryContentContext must be used within EntryContentProvider", | ||
) | ||
} | ||
return ctx | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,21 @@ | ||
import { createContext, useContext } from "react" | ||
import type { FeedViewType } from "@renderer/lib/enum" | ||
import { createContext } from "react" | ||
|
||
interface ContextValue { | ||
export interface EntryContentContext { | ||
entryId: string | ||
feedId: string | ||
|
||
audioSrc?: string | ||
|
||
view: FeedViewType | ||
} | ||
const EntryContentContext = createContext<ContextValue>(null!) | ||
export const EntryContentContext = createContext<EntryContentContext>(null!) | ||
|
||
export const EntryContentProvider: Component<ContextValue> = ({ | ||
export const EntryContentProvider: Component<EntryContentContext> = ({ | ||
children, | ||
...value | ||
}) => ( | ||
<EntryContentContext.Provider value={value}> | ||
{children} | ||
</EntryContentContext.Provider> | ||
) | ||
|
||
export const useEntryContentContext = () => { | ||
const ctx = useContext(EntryContentContext) | ||
if (!ctx) { | ||
throw new Error( | ||
"useEntryContentContext must be used within EntryContentProvider", | ||
) | ||
} | ||
return ctx | ||
} |