Skip to content

Commit

Permalink
feat(app): allow choose link opener for arbitrary url
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenlx committed Feb 14, 2024
1 parent 6aceaf7 commit 2076ec8
Show file tree
Hide file tree
Showing 12 changed files with 289 additions and 71 deletions.
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tailwind-merge": "^2.1.0",
"urlpattern-polyfill": "^10.0.0",
"use-callback-ref": "^1.3.1",
"zustand": "^4.4.7"
}
Expand Down
64 changes: 38 additions & 26 deletions apps/app/src/media-note/leaf-open/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import type { MediaEmbedViewState } from "@/media-view/iframe-view";
import type { MediaInfo } from "@/media-view/media-info";
import { isFileMediaInfo } from "@/media-view/media-info";
import type { MediaUrlViewState } from "@/media-view/url-view";
import type { MediaView } from "@/media-view/view-type";
import type {
MediaView,
MediaViewType,
RemoteMediaViewType,
} from "@/media-view/view-type";
import { isMediaViewType } from "@/media-view/view-type";
import type { MediaWebpageViewState } from "@/media-view/webpage-view";
import type MxPlugin from "@/mx-main";
import type { MediaURL } from "@/web/url-match";
import { getSupportedViewType } from "@/web/url-match/view-type";
import { filterFileLeaf, filterUrlLeaf, sortByMtime } from "./utils";
import "./active.global.less";

Expand Down Expand Up @@ -123,10 +125,12 @@ export class LeafOpener extends Component {
return fallback();
}

findExistingPlayer(info: MediaInfo) {
const leaves = getMediaLeavesOf(info, this.workspace);
if (leaves.length === 0) return null;
return leaves[0];
findExistingPlayer(info: MediaInfo): MediaLeaf | null {
for (const type of this.plugin.urlViewType.getSupported(info)) {
const leaves = getMediaLeavesOf(info, type, this.workspace);
if (leaves.length > 0) return leaves[0];
}
return null;
}

get settings() {
Expand Down Expand Up @@ -157,7 +161,13 @@ export class LeafOpener extends Component {
async openMedia(
mediaInfo: MediaInfo,
newLeaf?: PaneType | false,
direction?: SplitDirection,
{
direction,
viewType,
}: {
viewType?: RemoteMediaViewType;
direction?: SplitDirection;
} = {},
): Promise<MediaLeaf> {
const { workspace } = this.app;
if (!newLeaf) {
Expand All @@ -166,15 +176,28 @@ export class LeafOpener extends Component {
}

const leaf = workspace.getLeaf(
this.getSplitBehavior(newLeaf) as any,
this.getSplitBehavior(newLeaf) as "split",
direction,
);
if (isFileMediaInfo(mediaInfo)) {
await leaf.openFile(mediaInfo.file, {
eState: { subpath: mediaInfo.hash },
active: true,
});
} else {
await openInLeaf(mediaInfo, leaf);
const state:
| MediaEmbedViewState
| MediaWebpageViewState
| MediaUrlViewState = { source: mediaInfo };
viewType ??= this.plugin.urlViewType.getPreferred(mediaInfo);
await leaf.setViewState(
{
type: viewType,
state,
active: true,
},
{ subpath: mediaInfo.hash },
);
}
return leaf as MediaLeaf;
}
Expand Down Expand Up @@ -278,8 +301,11 @@ function getAllMediaLeaves(workspace: Workspace) {
return leaves as MediaLeaf[];
}

function getMediaLeavesOf(info: MediaInfo, workspace: Workspace) {
const viewType = getSupportedViewType(info)[0];
function getMediaLeavesOf(
info: MediaInfo,
viewType: MediaViewType,
workspace: Workspace,
) {
const leaves = workspace.getLeavesOfType(viewType).filter((leaf) => {
if (isFileMediaInfo(info)) {
return filterFileLeaf(leaf, info);
Expand All @@ -302,17 +328,3 @@ function byActiveTime(a: WorkspaceLeaf, b: WorkspaceLeaf) {
function updateHash(hash: string, leaf: WorkspaceLeaf) {
leaf.setEphemeralState({ subpath: hash });
}

async function openInLeaf(info: MediaURL, leaf: WorkspaceLeaf) {
const state: MediaEmbedViewState | MediaWebpageViewState | MediaUrlViewState =
{ source: info };
const viewType = getSupportedViewType(info)[0];
await leaf.setViewState(
{
type: viewType,
state,
active: true,
},
{ subpath: info.srcHash },
);
}
106 changes: 105 additions & 1 deletion apps/app/src/media-note/link-click.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,123 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { MenuItem } from "obsidian";
import { parseLinktext } from "obsidian";
import type { RemoteMediaViewType } from "@/media-view/view-type";
import { MEDIA_FILE_VIEW_TYPE } from "@/media-view/view-type";
import type MxPlugin from "@/mx-main";
import type { LinkEvent } from "@/patch/event";
import { checkMediaType } from "@/patch/media-type";
import type { MediaURL } from "@/web/url-match";
import { MediaHost } from "@/web/url-match/supported";

function shouldOpenMedia(url: MediaURL, plugin: MxPlugin): boolean {
return !!(
(url.isFileUrl && url.inferredType) ||
url.tempFrag ||
url.type !== MediaHost.Generic ||
plugin.urlViewType.getPreferred(url, true)
);
}

export const onExternalLinkClick: LinkEvent["onExternalLinkClick"] =
async function (this, link, newLeaf, fallback) {
const url = this.resolveUrl(link);
if (!url || (url.type === MediaHost.Generic && !url.isFileUrl)) {
if (!url || !shouldOpenMedia(url, this)) {
fallback();
return;
}
await this.leafOpener.openMedia(url, newLeaf);
};

const mediaTypeDisplay: Record<
RemoteMediaViewType,
{ label: string; icon: string }
> = {
"mx-embed": { label: "iframe", icon: "code" },
"mx-url-audio": { label: "regular audio", icon: "headphones" },
"mx-url-video": { label: "regular video", icon: "film" },
"mx-webpage": { label: "webpage", icon: "globe" },
};

export function handleExternalLinkMenu(plugin: MxPlugin) {
plugin.registerEvent(
plugin.app.workspace.on("url-menu", (menu, link) => {
const url = plugin.resolveUrl(link);
if (!url) return;
const { protocol, hostname, pathname } = url;
const supported = plugin.urlViewType.getSupported(url);
const preferred = plugin.urlViewType.getPreferred(url);
const showInMenu = shouldOpenMedia(url, plugin)
? supported.filter((t) => t !== preferred)
: supported;
if (showInMenu.length === 0) return;
function setLabel(
item: MenuItem,
viewType: RemoteMediaViewType,
noPrefix = false,
) {
const label = mediaTypeDisplay[viewType].label;
return item
.setTitle(noPrefix ? label : `Open as ${label}`)
.setIcon(mediaTypeDisplay[viewType].icon);
}
showInMenu.forEach((viewType) => {
menu.addItem((item) =>
setLabel(item, viewType)
.setSection("mx-link")
.onClick(async () => {
await plugin.leafOpener.openMedia(url, undefined, { viewType });
}),
);
});
menu.addItem((item) => {
const matchUrl = item
.setTitle("Always open this url as")
.setIcon("external-link")
.setSection("mx-link")
.setSubmenu();
showInMenu.forEach((viewType) => {
matchUrl.addItem((item) =>
setLabel(item, viewType, true)
.setSection("mx-link")
.onClick(async () => {
plugin.urlViewType.setPreferred(
{ protocol, hostname, pathname },
viewType,
);
await plugin.leafOpener.openMedia(url, undefined, {
viewType,
});
}),
);
});
});
if (hostname)
menu.addItem((item) => {
const matchHostname = item
.setTitle(`Always open ${hostname} as`)
.setIcon("external-link")
.setSection("mx-link")
.setSubmenu();
showInMenu.forEach((viewType) => {
matchHostname.addItem((item) =>
setLabel(item, viewType, true)
.setSection("mx-link")
.onClick(async () => {
plugin.urlViewType.setPreferred(
{ protocol, hostname },
viewType,
);
await plugin.leafOpener.openMedia(url, undefined, {
viewType,
});
}),
);
});
});
}),
);
}

export const onInternalLinkClick: LinkEvent["onInternalLinkClick"] =
async function (this, linktext, sourcePath, newLeaf, fallback) {
const { metadataCache } = this.app;
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/media-view/menu/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MediaPlayerInstance } from "@vidstack/react";
import type { MediaViewState } from "@/components/context";
import { handleExternalLinkMenu } from "@/media-note/link-click";
import type MxPlugin from "@/mx-main";
import type { MediaURL } from "@/web/url-match";
import { muteMenu } from "./mute";
Expand Down Expand Up @@ -55,6 +56,7 @@ declare module "obsidian" {
}

export default function registerMediaMenu(this: MxPlugin) {
handleExternalLinkMenu(this);
this.registerEvent(
this.app.workspace.on("mx-media-menu", (menu, ctx, source) => {
if (source !== "sidebar-context-menu" && source !== "tab-header") {
Expand Down
23 changes: 15 additions & 8 deletions apps/app/src/mx-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ import { BilibiliRequestHacker } from "./web/bili-req";
import { modifySession } from "./web/session";
import "./login/modal";
import { MediaURL, resolveMxProtocol } from "./web/url-match";
import { URLViewType } from "./web/url-match/view-type";

interface MxAPI {
openUrl: (
url: string,
newLeaf?: PaneType,
direction?: SplitDirection,
) => Promise<void>;
}

export default class MxPlugin extends Plugin {
settings = createSettingsStore(this);
Expand All @@ -54,21 +63,19 @@ export default class MxPlugin extends Plugin {
const urlInfo = MediaURL.create(resolved);
return urlInfo;
}
api = {
openUrl: async (
url: string,
newLeaf?: PaneType,
direction?: SplitDirection,
) => {
api: MxAPI = {
openUrl: async (url, newLeaf, direction) => {
const urlInfo = this.resolveUrl(url);
if (!urlInfo) {
new Notice("Provider not yet supported");
new Notice("Protocol not yet supported");
return;
}
await this.leafOpener.openMedia(urlInfo, newLeaf as "split", direction);
await this.leafOpener.openMedia(urlInfo, newLeaf, { direction });
},
};

urlViewType = this.addChild(new URLViewType(this));

async onload() {
this.addSettingTab(new MxSettingTabs(this));
await this.loadSettings();
Expand Down
3 changes: 1 addition & 2 deletions apps/app/src/patch/embed-widget/syntax-to-decos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type MediaExtended from "@/mx-main";

// import { getFileHashFromLinktext } from "../../player/thunk/set-media";
import { MediaHost } from "@/web/url-match/supported";
import { getSupportedViewType } from "@/web/url-match/view-type";
import { isMdFavorInternalLink } from "./utils";
import { WidgetCtorMap } from "./widget";

Expand Down Expand Up @@ -57,7 +56,7 @@ const getPlayerDecos = (
}
const urlInfo = plugin.resolveUrl(imgUrlText);
if (urlInfo && urlInfo.type !== MediaHost.Generic) {
const viewType = getSupportedViewType(urlInfo)[0];
const viewType = plugin.urlViewType.getPreferred(urlInfo);
const widget = new WidgetCtorMap[viewType](
plugin,
urlInfo,
Expand Down
3 changes: 1 addition & 2 deletions apps/app/src/patch/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { MEDIA_WEBPAGE_VIEW_TYPE } from "@/media-view/view-type";
import type { RemoteMediaViewType } from "@/media-view/view-type";
import type MxPlugin from "@/mx-main";
import type { MediaURL } from "@/web/url-match";
import { getSupportedViewType } from "@/web/url-match/view-type";
import setupEmbedWidget from "./embed-widget";
import { MediaFileExtensions } from "./media-type";
import { reloadMarkdownPreview } from "./utils";
Expand Down Expand Up @@ -100,7 +99,7 @@ function injectUrlMediaEmbed(this: MxPlugin) {
function replace({ title, url }: EmbedSource, target: HTMLElement) {
const src = plguin.resolveUrl(url);
if (!src) return;
const viewType = getSupportedViewType(src)[0];
const viewType = plguin.urlViewType.getPreferred(src);
const newWarpper = createSpan({
cls: ["media-embed", "external-embed", "is-loaded"],
attr: {
Expand Down
Loading

0 comments on commit 2076ec8

Please sign in to comment.