Skip to content

Commit

Permalink
feat(playlist): update playlist menu render; fix in-vault file url re…
Browse files Browse the repository at this point in the history
…soltion
  • Loading branch information
aidenlx committed Mar 17, 2024
1 parent 9631f67 commit 0c1399b
Show file tree
Hide file tree
Showing 19 changed files with 231 additions and 111 deletions.
2 changes: 1 addition & 1 deletion apps/app/src/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ export const PlayIcon = makeIcon("play"),
MoreIcon = makeIcon("more-horizontal"),
PlusIcon = makeIcon("plus"),
TrashIcon = makeIcon("trash"),
PlaylistIcon = makeIcon("list-music"),
PlaylistIcon = makeIcon("list-video"),
NextIcon = makeIcon("skip-forward"),
PreviousIcon = makeIcon("skip-back");
54 changes: 39 additions & 15 deletions apps/app/src/components/player/menus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {
useMediaState,
} from "@vidstack/react";
import { around } from "monkey-around";
import type { MenuItem } from "obsidian";
import { Menu } from "obsidian";
import { useRef } from "react";
import { MoreIcon, PlaylistIcon, SubtitlesIcon } from "@/components/icon";
import { showAtButton } from "@/lib/menu";
import type { PlaylistItem } from "@/media-note/playlist/def";
import { isWithMedia } from "@/media-note/playlist/def";
import {
useApp,
Expand All @@ -22,6 +24,7 @@ import {
} from "../context";
import { usePlaylist } from "../hook/use-playlist";
import { dataLpPassthrough } from "./buttons";
import { addItemsToMenu } from "./playlist-menu";

function useMenu(onMenu: (menu: Menu) => boolean) {
const menuRef = useRef<Menu | null>(null);
Expand Down Expand Up @@ -53,33 +56,54 @@ export function Playlist() {
const app = useApp();
const onClick = useMenu((mainMenu) => {
if (!onPlaylistChange || !current || !playlist) return false;

mainMenu
.addItem((item) =>
item
.setTitle(playlist.title)
.setIsLabel(true)
.setIcon("list-video")
.onClick(() => {
app.workspace.openLinkText(playlist.file.path, "", "tab");
}),
)
.addSeparator();

playlist.list.forEach((li) => {
if (li.type === "subtitle") return;
mainMenu.addItem((item) => {
if (current.compare(li?.media)) {
item.setChecked(true);
}
const title = li.parent >= 0 ? `(${li.parent})${li.title}` : li.title;
item.setTitle(title);
if (isWithMedia(li) && !li.media.compare(current)) {
item.onClick(() => {
addItemsToMenu(mainMenu, playlist.list, (menu, li, submenu) => {
if (li.type === "subtitle") return null;
let subTrigger: MenuItem | null = null;
if (isWithMedia(li)) {
const renderExtra = li.children.length > 0;
menu.addItem((item) => {
item.setTitle(li.title).onClick(() => {
onPlaylistChange(li, playlist);
});
} else {
item.setIsLabel(true);
}
});
if (current.compare(li.media)) {
item.setChecked(true);
const checkParent = (node: PlaylistItem) => {
if (node.parent < 0) return;
submenu.get(node.parent)?.setChecked(true);
const parent = playlist.list[node.parent];
if (!parent) return;
checkParent(parent);
};
checkParent(li);
}
if (!renderExtra) subTrigger = item;
});
if (renderExtra)
// render an extra menu item as submenu trigger
menu.addItem((item) => {
item.setTitle(" ↳");
subTrigger = item;
});
} else {
// render label
menu.addItem((item) => {
item.setTitle(li.title).setIcon("hash");
subTrigger = item;
});
}
return subTrigger;
});
return true;
});
Expand Down
64 changes: 64 additions & 0 deletions apps/app/src/components/player/playlist-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Menu, MenuItem } from "obsidian";
import type { PlaylistItem } from "@/media-note/playlist/def";

interface PlaylistItemNode extends PlaylistItem {
index: number;
children: PlaylistItemNode[];
}

export function addItemsToMenu(
menu: Menu,
list: PlaylistItem[],
onMenu: (
item: Menu,
node: PlaylistItemNode,
submenuTriggerMap: SubmenuTriggerMap,
) => MenuItem | null,
) {
const tree = buildTree(list);
addNodesToMenu(menu, tree, onMenu, new Map());
}

type SubmenuTriggerMap = Map<number, MenuItem>;

// Recursive function to build the menu
function addNodesToMenu(
menu: Menu,
nodes: PlaylistItemNode[],
onMenu: (
menu: Menu,
node: PlaylistItemNode,
submenuTriggerMap: SubmenuTriggerMap,
) => MenuItem | null,
submenuTriggerMap: SubmenuTriggerMap,
) {
nodes.forEach((li) => {
const submenuItem = onMenu(menu, li, submenuTriggerMap);
if (submenuItem && li.children.length > 0) {
submenuTriggerMap.set(li.index, submenuItem);
const submenu = submenuItem.setSubmenu();
addNodesToMenu(submenu, li.children, onMenu, submenuTriggerMap);
}
});
}
function buildTree(list: PlaylistItem[]): PlaylistItemNode[] {
const roots: PlaylistItemNode[] = [];
const nodes = list.map(
(node, index): PlaylistItemNode => ({
...node,
index,
children: [],
}),
);

nodes.forEach((node) => {
if (node.parent >= 0) {
// if you have dangling branches check that map[node.parentId] exists
nodes[node.parent]?.children.push(node);
} else {
roots.push(node);
}
});

return roots;
}
13 changes: 5 additions & 8 deletions apps/app/src/media-note/leaf-open/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import type { MediaWebpageViewState } from "@/media-view/webpage-view";
import type MxPlugin from "@/mx-main";
import { toPaneAction } from "@/patch/mod-evt";
import type { OpenLinkBehavior } from "@/settings/def";
import { mediaInfoToURL } from "@/web/url-match";
import { filterFileLeaf, filterUrlLeaf, sortByMtime } from "./utils";
import "./active.global.less";

Expand Down Expand Up @@ -226,22 +225,20 @@ export class LeafOpener extends Component {
mediaInfo: MediaInfo,
viewType?: RemoteMediaViewType,
) {
const url = mediaInfoToURL(mediaInfo, this.app.vault);
const file = url.getVaultFile(this.app.vault);
if (file) {
await leaf.openFile(file, {
eState: { subpath: url.hash },
if (isFileMediaInfo(mediaInfo)) {
await leaf.openFile(mediaInfo.file, {
eState: { subpath: mediaInfo.hash },
active: true,
});
} else {
const { hash, source } = url.jsonState;
const { hash, source } = mediaInfo.jsonState;
const state:
| MediaEmbedViewState
| MediaWebpageViewState
| MediaUrlViewState = {
source,
};
viewType ??= this.plugin.urlViewType.getPreferred(url);
viewType ??= this.plugin.urlViewType.getPreferred(mediaInfo);
await leaf.setViewState(
{
type: viewType,
Expand Down
10 changes: 10 additions & 0 deletions apps/app/src/media-note/link-click.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { Notice, Platform, parseLinktext } from "obsidian";
import { isFileMediaInfo } from "@/media-view/media-info";
import { MEDIA_FILE_VIEW_TYPE } from "@/media-view/view-type";
import type MxPlugin from "@/mx-main";
import type { LinkEvent } from "@/patch/event";
Expand All @@ -21,6 +22,11 @@ export function shouldOpenMedia(url: MediaURL, plugin: MxPlugin): boolean {
export const onExternalLinkClick: LinkEvent["onExternalLinkClick"] =
async function (this, link, newLeaf, fallback) {
const url = this.resolveUrl(link);
if (isFileMediaInfo(url)) {
new Notice("For in-vault media, use internal link instead");
fallback();
return;
}
if (!url || !shouldOpenMedia(url, this)) {
fallback();
return;
Expand Down Expand Up @@ -53,6 +59,10 @@ export function handleExternalLinkMenu(plugin: MxPlugin) {
plugin.app.workspace.on("url-menu", (menu, link) => {
const url = plugin.resolveUrl(link);
if (!url) return;
if (isFileMediaInfo(url)) {
new Notice("For in-vault media, use internal link instead");
return;
}

if (Platform.isDesktopApp && url.isFileUrl && url.filePath) {
const filePath = url.filePath;
Expand Down
30 changes: 16 additions & 14 deletions apps/app/src/media-note/note-index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import type { MetadataCache, Vault, CachedMetadata } from "obsidian";
import { waitUntilResolve } from "@/lib/meta-resolve";
import type MxPlugin from "@/mx-main";
import { checkMediaType } from "@/patch/media-type";
import { mediaInfoToURL } from "@/web/url-match";
import type { MediaInfo } from "../../media-view/media-info";
import { isFileMediaInfo, type MediaInfo } from "../../media-view/media-info";

declare module "obsidian" {
interface MetadataCache {
Expand All @@ -25,7 +24,7 @@ export class MediaNoteIndex extends Component {
private mediaToNoteIndex = new Map<string, Set<TFile>>();

findNotes(media: MediaInfo): TFile[] {
const notes = this.mediaToNoteIndex.get(this.mediaInfoToString(media));
const notes = this.mediaToNoteIndex.get(toInfoKey(media));
if (!notes) return [];
return [...notes];
}
Expand Down Expand Up @@ -68,16 +67,11 @@ export class MediaNoteIndex extends Component {
);
}

private mediaInfoToString(info: MediaInfo) {
const url = mediaInfoToURL(info, this.app.vault);
return `url:${url.jsonState.source}`;
}

removeMediaNote(toRemove: TFile) {
const mediaInfo = this.noteToMediaIndex.get(toRemove.path)!;
if (!mediaInfo) return;
this.noteToMediaIndex.delete(toRemove.path);
const mediaInfoKey = this.mediaInfoToString(mediaInfo);
const mediaInfoKey = toInfoKey(mediaInfo);
const mediaNotes = this.mediaToNoteIndex.get(mediaInfoKey);
if (!mediaNotes) return;
mediaNotes.delete(toRemove);
Expand All @@ -87,7 +81,7 @@ export class MediaNoteIndex extends Component {
}
addMediaNote(mediaInfo: MediaInfo, newNote: TFile) {
this.noteToMediaIndex.set(newNote.path, mediaInfo);
const key = this.mediaInfoToString(mediaInfo);
const key = toInfoKey(mediaInfo);
const mediaNotes = this.mediaToNoteIndex.get(key);
if (!mediaNotes) {
this.mediaToNoteIndex.set(key, new Set([newNote]));
Expand Down Expand Up @@ -131,10 +125,18 @@ export const mediaSourceFieldMap = {
video: "video",
audio: "audio",
} as const;
type MediaType = (typeof mediaSourceFieldMap)[keyof typeof mediaSourceFieldMap];
export type MediaSourceFieldType =
(typeof mediaSourceFieldMap)[keyof typeof mediaSourceFieldMap];
export const mediaSourceFields = Object.values(
mediaSourceFieldMap,
) as MediaType[];
) as MediaSourceFieldType[];

export function toInfoKey(mediaInfo: MediaInfo) {
if (isFileMediaInfo(mediaInfo)) {
return `file:${mediaInfo.file.path}`;
}
return `url:${mediaInfo.jsonState.source}`;
}

export interface InternalLinkField {
type: "internal";
Expand All @@ -145,7 +147,7 @@ export interface InternalLinkField {
}
export interface ExternalLinkField {
type: "external";
media: MediaType;
media: MediaSourceFieldType;
source: URL;
subpath: string;
original: string;
Expand All @@ -169,7 +171,7 @@ function getMediaNoteMeta(
}

function getField(
key: MediaType,
key: MediaSourceFieldType,
meta: CachedMetadata,
ctx: { metadataCache: MetadataCache; sourcePath: string; plugin: MxPlugin },
): MediaInfo | null {
Expand Down
3 changes: 2 additions & 1 deletion apps/app/src/media-note/playlist/def.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { TFile } from "obsidian";
import type { MediaInfo } from "@/media-view/media-info";
import type { MediaType } from "@/patch/media-type";
import { type MediaURL } from "@/web/url-match";

Expand Down Expand Up @@ -36,7 +37,7 @@ export interface PlaylistWithActive extends Playlist {
active: number;
}
export interface PlaylistItem {
media: MediaURL | null;
media: MediaInfo | null;
title: string;
type: MediaTaskSymbolType;
/**
Expand Down
10 changes: 4 additions & 6 deletions apps/app/src/media-note/playlist/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import type {
ListItemCache,
TFile,
} from "obsidian";
import type { MediaInfo } from "@/media-view/media-info";
import { getMediaInfoFor } from "@/media-view/media-info";
import type MxPlugin from "@/mx-main";
import { mediaInfoToURL, type MediaURL } from "@/web/url-match";
import { mediaTitle } from "../title";
import { isMediaTaskSymbol, taskSymbolMediaTypeMap } from "./def";
import type { MediaTaskSymbol, PlaylistItem, Playlist } from "./def";
Expand Down Expand Up @@ -83,7 +83,7 @@ async function parsePlaylist(
const media = ctx.plugin.resolveUrl(url);
let { display } = externalLink;
if (media && (display === url || !display)) {
display = mediaTitle(media, { vault });
display = mediaTitle(media);
}
return { media, title: display, type: type || "generic", parent };
}
Expand All @@ -96,12 +96,10 @@ async function parsePlaylist(
});
return playlist;

function mediaInfoFromInternalLink({ link }: LinkCache): MediaURL | null {
function mediaInfoFromInternalLink({ link }: LinkCache): MediaInfo | null {
const { path, subpath } = parseLinktext(link);
const file = metadataCache.getFirstLinkpathDest(path, ctx.source.path);
const mediaInfo = getMediaInfoFor(file, subpath);
if (!mediaInfo) return null;
return mediaInfoToURL(mediaInfo, vault);
return getMediaInfoFor(file, subpath);
}
}

Expand Down
Loading

0 comments on commit 0c1399b

Please sign in to comment.