Skip to content

Commit

Permalink
feat(app): initial media note support
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenlx committed Jan 12, 2024
1 parent f714968 commit 4f83d37
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 21 deletions.
38 changes: 25 additions & 13 deletions apps/app/src/lib/link-click/external.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Workspace } from "obsidian";
import {
MEDIA_EMBED_VIEW_TYPE,
type MediaEmbedViewState,
Expand All @@ -12,12 +13,13 @@ import { matchHostForEmbed } from "@/web/match-embed";
import { matchHostForUrl } from "@/web/match-url";
import { matchHostForWeb, SupportedWebHost } from "@/web/match-webpage";

function parseUrl(url: string): {
interface UrlInfo {
viewType: string;
source: string;
hash: string;
isSameSource: (src: string) => boolean;
} | null {
}
export function parseUrl(url: string): UrlInfo | null {
const directlinkInfo = matchHostForUrl(url);

if (directlinkInfo) {
Expand Down Expand Up @@ -57,6 +59,24 @@ function parseUrl(url: string): {
}
return null;
}

export function openInOpenedPlayer(
{ hash, isSameSource, viewType }: UrlInfo,
workspace: Workspace,
) {
const opened = workspace.getLeavesOfType(viewType).filter((l) => {
const { source } = l.view.getState() as
| MediaEmbedViewState
| MediaWebpageViewState;
return source && isSameSource(source);
});
if (opened.length > 0) {
opened[0].setEphemeralState({ subpath: hash });
return true;
}
return false;
}

export async function onExternalLinkClick(
this: MxPlugin,
url: string,
Expand All @@ -71,18 +91,10 @@ export async function onExternalLinkClick(
fallback();
return;
}
const { viewType, source, hash, isSameSource } = urlInfo;
const { viewType, source, hash } = urlInfo;

if (!newLeaf && openInOpenedPlayer(urlInfo, workspace)) return;

const opened = workspace.getLeavesOfType(viewType).filter((l) => {
const { source } = l.view.getState() as
| MediaEmbedViewState
| MediaWebpageViewState;
return source && isSameSource(source);
});
if (opened.length > 0 && !newLeaf) {
opened[0].setEphemeralState({ subpath: hash });
return;
}
const leaf = newLeaf
? workspace.getLeaf("split", "vertical")
: workspace.getLeaf(false);
Expand Down
27 changes: 21 additions & 6 deletions apps/app/src/lib/link-click/internal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { TFile, Workspace } from "obsidian";
import { parseLinktext } from "obsidian";
import { MEDIA_FILE_VIEW_TYPE } from "@/media-view/file-view";
import type MxPlugin from "@/mx-main";
import type { MediaType } from "@/patch/utils";
import { checkMediaType } from "@/patch/utils";
export function onInternalLinkClick(
this: MxPlugin,
Expand All @@ -17,15 +19,28 @@ export function onInternalLinkClick(
fallback();
return;
}
if (
!newLeaf &&
openInOpenedPlayer({ file: linkFile, subpath, mediaType }, workspace)
) {
return;
}
fallback();
}

export function openInOpenedPlayer(
linkInfo: { file: TFile; subpath: string; mediaType: MediaType },
workspace: Workspace,
) {
const opened = workspace
.getLeavesOfType(MEDIA_FILE_VIEW_TYPE[mediaType])
.getLeavesOfType(MEDIA_FILE_VIEW_TYPE[linkInfo.mediaType])
.filter((l) => {
const { file: filePath } = l.view.getState() as { file: string };
return filePath === linkFile.path;
return filePath === linkInfo.file.path;
});
if (opened.length > 0 && !newLeaf) {
opened[0].setEphemeralState({ subpath });
return;
if (opened.length > 0) {
opened[0].setEphemeralState({ subpath: linkInfo.subpath });
return true;
}
fallback();
return false;
}
127 changes: 127 additions & 0 deletions apps/app/src/lib/media-note/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { CachedMetadata, TAbstractFile } from "obsidian";
import { Notice, TFile, parseLinktext } from "obsidian";
import type MxPlugin from "@/mx-main";
import { checkMediaType } from "@/patch/utils";
import { openInOpenedPlayer, parseUrl } from "../link-click/external";
import { toURL } from "../url";

export const mediaSourceField = {
generic: "media",
video: "video",
audio: "audio",
} as const;

type MediaType = (typeof mediaSourceField)[keyof typeof mediaSourceField];

interface InternalLinkField {
type: "internal";
media: MediaType;
source: string;
original: string;
}
interface ExternalLinkField {
type: "external";
media: MediaType;
source: URL;
}

export function handleMediaNote(this: MxPlugin) {
const { metadataCache, workspace } = this.app;
this.registerEvent(
workspace.on("file-menu", (menu, file, _source, _leaf) => {
const mediaInfo = getMediaInfo(file);
if (!mediaInfo) return;
menu.addItem((item) =>
item
.setSection("view")
.setIcon("play")
.setTitle("Open linked media")
.onClick(() => openMedia(mediaInfo, file as TFile)),
);
}),
);
async function openMedia(
mediaInfo: InternalLinkField | ExternalLinkField,
file: TFile,
) {
const leaf = workspace.getLeaf("split", "vertical");
if (mediaInfo.type === "internal") {
const linkFile = resolveLink(mediaInfo.source, file as TFile);
if (!linkFile) {
new Notice(`Cannot resolve media file from link ${mediaInfo.original}`);
return;
}
await leaf.openFile(linkFile.file, {
eState: { subpath: linkFile.subpath },
});
} else {
const urlInfo = parseUrl(mediaInfo.source.href);
if (!urlInfo) {
new Notice(
`Failed to open media url ${mediaInfo.source.href}, invalid url or not supported`,
);
return;
}
if (openInOpenedPlayer(urlInfo, workspace)) return;
await leaf.setViewState(
{
type: urlInfo.viewType,
state: { source: urlInfo.source },
active: true,
},
{ subpath: urlInfo.hash },
);
}
}
function getMediaInfo(
file: TAbstractFile,
): ExternalLinkField | InternalLinkField | null {
if (!(file instanceof TFile)) return null;
const meta = metadataCache.getFileCache(file);
if (!meta) return null;
const video = getField(mediaSourceField.video, meta),
audio = getField(mediaSourceField.audio, meta),
generic = getField(mediaSourceField.generic, meta);
// prefer explicit typed media
const mediaField = video || audio || generic;
if (!mediaField) return null;

return mediaField;
}

function resolveLink(linktext: string, file: TFile) {
const { path: linkpath, subpath } = parseLinktext(linktext);
const mediaFile = metadataCache.getFirstLinkpathDest(linkpath, file.path);
if (!mediaFile) return null;
// if local file, prefer detect media type
const mediaType = checkMediaType(mediaFile.extension);
if (!mediaType) return null;
return { file: mediaFile, subpath };
}
}

function getField(
key: MediaType,
meta: CachedMetadata,
): InternalLinkField | ExternalLinkField | null {
const { frontmatter, frontmatterLinks } = meta;
if (!frontmatter || !(key in frontmatter)) return null;
const internalLink = frontmatterLinks?.find((link) => link.key === key);
if (internalLink) {
return {
type: "internal",
media: key,
source: internalLink.link,
original: internalLink.original,
};
}
const content = frontmatter[key];
if (typeof content !== "string") return null;
const url = toURL(content);
if (!url) return null;
return {
type: "external",
media: key,
source: url,
};
}
2 changes: 1 addition & 1 deletion apps/app/src/media-view/file-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { setTempFrag, type PlayerComponent } from "./base";
export const MEDIA_FILE_VIEW_TYPE = {
video: "mx-file-video",
audio: "mx-file-audio",
};
} as const;

abstract class MediaFileView
extends EditableFileView
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/mx-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "./icons";
import { Plugin } from "obsidian";
import { onExternalLinkClick } from "./lib/link-click/external";
import { onInternalLinkClick } from "./lib/link-click/internal";
import { handleMediaNote } from "./lib/media-note";
import { MediaFileEmbed } from "./media-view/file-embed";
import {
AudioFileView,
Expand Down Expand Up @@ -36,8 +37,10 @@ export default class MxPlugin extends Plugin {
async onload() {
this.loadPatches();
await this.modifySession();
this.handleMediaNote();
}

handleMediaNote = handleMediaNote;
injectMediaEmbed = injectMediaEmbed;
injectMediaView = injectMediaView;
fixLinkLabel = fixLinkLabel;
Expand Down
4 changes: 3 additions & 1 deletion apps/app/src/patch/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const MediaFileExtensions = {
audio: ["mp3", "wav", "m4a", "3gp", "flac", "ogg", "oga", "opus"],
};

export function checkMediaType(ext: string) {
export function checkMediaType(ext: string): MediaType | null {
for (const type of Object.keys(
MediaFileExtensions,
) as (keyof typeof MediaFileExtensions)[]) {
Expand All @@ -58,6 +58,8 @@ export function checkMediaType(ext: string) {
return null;
}

export type MediaType = keyof typeof MediaFileExtensions;

declare module "obsidian" {
interface MarkdownPreviewView {
rerender(full?: boolean): void;
Expand Down

0 comments on commit 4f83d37

Please sign in to comment.