Skip to content

Commit

Permalink
feat(playlist): impl basic playlist components
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenlx committed Mar 16, 2024
1 parent fe2c160 commit 37058bc
Show file tree
Hide file tree
Showing 17 changed files with 275 additions and 82 deletions.
11 changes: 10 additions & 1 deletion apps/app/src/components/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import noop from "@/lib/no-op";
import { WebiviewMediaProvider } from "@/lib/remote-player/provider";
import type { ScreenshotInfo } from "@/lib/screenshot";
import { getTracksInVault } from "@/lib/subtitle";
import type {
Playlist,
PlaylistItem,
PlaylistItemWithMedia,
} from "@/media-note/note-index/playlist";
import { titleFromUrl } from "@/media-view/base";
import {
MEDIA_FILE_VIEW_TYPE,
Expand Down Expand Up @@ -115,7 +120,7 @@ export function createMediaViewStore() {
},
async loadFile(file, { vault, subpath, defaultLang }) {
const textTracks = await getTracksInVault(file, vault, defaultLang);
const url = fromFile(file, vault);
const url = fromFile(file, subpath ?? "", vault);
if (!url.inferredType) throw new Error("Unsupported media type");
const viewType = MEDIA_FILE_VIEW_TYPE[url.inferredType];
set(({ source, hash }) => ({
Expand Down Expand Up @@ -183,6 +188,7 @@ export const MediaViewContext = createContext<{
reload: () => void;
onScreenshot?: (info: ScreenshotInfo) => any;
onTimestamp?: (timestamp: number) => any;
onPlaylistChange?: (item: PlaylistItemWithMedia, list: Playlist) => any;
}>(null as any);

export function useMediaViewStore<U>(
Expand Down Expand Up @@ -224,6 +230,9 @@ export function usePlugin() {
export function useScreenshot() {
return useContext(MediaViewContext).onScreenshot;
}
export function usePlaylistChange() {
return useContext(MediaViewContext).onPlaylistChange;
}
export function useTimestamp() {
return useContext(MediaViewContext).onTimestamp;
}
Expand Down
19 changes: 19 additions & 0 deletions apps/app/src/components/hook/use-playlist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useState } from "react";
import { useMediaViewStore, usePlugin } from "../context";

export function usePlaylist() {
const media = useMediaViewStore((s) => s.source?.url);
const plugin = usePlugin();
const [playlist, setPlaylist] = useState(() => plugin.playlist.get(media));
useEffect(() => {
function onPlaylistChange() {
setPlaylist(plugin.playlist.get(media));
}
onPlaylistChange();
plugin.app.metadataCache.on("mx-playlist-change", onPlaylistChange);
return () => {
plugin.app.metadataCache.off("mx-playlist-change", onPlaylistChange);
};
}, [media, plugin.playlist, plugin.app.metadataCache]);
return playlist;
}
5 changes: 4 additions & 1 deletion apps/app/src/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,7 @@ export const PlayIcon = makeIcon("play"),
PinIcon = makeIcon("pin"),
MoreIcon = makeIcon("more-horizontal"),
PlusIcon = makeIcon("plus"),
TrashIcon = makeIcon("trash");
TrashIcon = makeIcon("trash"),
PlaylistIcon = makeIcon("list-music"),
NextIcon = makeIcon("skip-forward"),
PreviousIcon = makeIcon("skip-back");
54 changes: 54 additions & 0 deletions apps/app/src/components/player/buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@ import {
EditIcon,
ImageDownIcon,
PinIcon,
NextIcon,
PreviousIcon,
} from "@/components/icon";
import { cn } from "@/lib/utils";
import { findWithMedia, isWithMedia } from "@/media-note/note-index/playlist";
import {
useIsEmbed,
usePlaylistChange,
useScreenshot,
useSettings,
useTimestamp,
} from "../context";
import { usePlaylist } from "../hook/use-playlist";
import { canProviderScreenshot, takeScreenshot } from "./screenshot";

export const buttonClass =
Expand Down Expand Up @@ -214,4 +219,53 @@ export function Timestamp() {
);
}

export function Next() {
const onPlaylistChange = usePlaylistChange();
const playlist = usePlaylist()[0];
if (!playlist || !onPlaylistChange) return null;
const curr = playlist.list[playlist.active];
if (!(curr && isWithMedia(curr))) return null;
const next = findWithMedia(
playlist.list,
(li) => !curr.media.compare(li.media),
{ fromIndex: playlist.active + 1 },
);
if (!next) return null;
return (
<button
className="group ring-mod-border-focus relative inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-md outline-none ring-inset hover:bg-white/20 focus-visible:ring-2 aria-disabled:hidden"
onClick={() => {
onPlaylistChange(next, playlist);
}}
aria-label={`Next: ${next.title}`}
>
<NextIcon className="w-7 h-7" />
</button>
);
}
export function Previous() {
const onPlaylistChange = usePlaylistChange();
const playlist = usePlaylist()[0];
if (!playlist || !onPlaylistChange) return null;
const curr = playlist.list[playlist.active];
if (!(curr && isWithMedia(curr))) return null;
const prev = findWithMedia(
playlist.list,
(li) => !curr.media.compare(li.media),
{ fromIndex: playlist.active - 1, reverse: true },
);
if (!prev) return null;
return (
<button
className="group ring-mod-border-focus relative inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-md outline-none ring-inset hover:bg-white/20 focus-visible:ring-2 aria-disabled:hidden"
onClick={() => {
onPlaylistChange(prev, playlist);
}}
aria-label={`Prev: ${prev.title}`}
>
<PreviousIcon className="w-7 h-7" />
</button>
);
}

export const dataLpPassthrough = "data-lp-pass-through";
3 changes: 3 additions & 0 deletions apps/app/src/components/player/layouts/audio-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ export function AudioLayout({ thumbnails }: VideoLayoutProps) {
<Buttons.Rewind seconds={30} />
<Buttons.Play />
<Buttons.FastForward seconds={30} />
<Buttons.Previous />
<Buttons.Next />
<Buttons.Mute />
<Sliders.Volume />
<TimeGroup />
<Title />
<div className="flex-1" />
<Buttons.EditorEdit />
<Menus.Playlist />
<Menus.MoreOptions />
</Controls.Group>
</Tooltip.Provider>
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/components/player/layouts/video-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ export function VideoLayout({ thumbnails }: VideoLayoutProps) {
</Controls.Group>
<Controls.Group className="-mt-0.5 flex w-full items-center px-2 pb-2">
<Buttons.Play />
<Buttons.Previous />
<Buttons.Next />
<Buttons.Mute />
<Sliders.Volume />
<TimeGroup />
<Title />
<div className="flex-1" />
<Menus.Captions />
<Menus.Playlist />
<Buttons.Screenshot />
<Buttons.Fullscreen />
<Buttons.EditorEdit />
Expand Down
58 changes: 57 additions & 1 deletion apps/app/src/components/player/menus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import {
import { around } from "monkey-around";
import { Menu } from "obsidian";
import { useRef } from "react";
import { MoreIcon, SubtitlesIcon } from "@/components/icon";
import { MoreIcon, PlaylistIcon, SubtitlesIcon } from "@/components/icon";
import { showAtButton } from "@/lib/menu";
import { isWithMedia } from "@/media-note/note-index/playlist";
import {
useApp,
useIsEmbed,
useMediaViewStore,
useMediaViewStoreInst,
usePlaylistChange,
usePlugin,
useReload,
} from "../context";
import { usePlaylist } from "../hook/use-playlist";
import { dataLpPassthrough } from "./buttons";

function useMenu(onMenu: (menu: Menu) => boolean) {
Expand All @@ -43,6 +46,59 @@ function useMenu(onMenu: (menu: Menu) => boolean) {
};
}

export function Playlist() {
const playlists = usePlaylist();
const onPlaylistChange = usePlaylistChange();
const current = useMediaViewStore((s) => s.source?.url);
const app = useApp();
const playlist = playlists.first();
const onClick = useMenu((mainMenu) => {
if (!onPlaylistChange || !current || !playlist) return false;
mainMenu
.addItem((item) =>
item
.setTitle(playlist.title)
.setIsLabel(true)
.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(() => {
onPlaylistChange(li, playlists[0]);
});
} else {
item.setIsLabel(true);
}
});
});
return true;
});

if (!onPlaylistChange || !current || !playlist) return null;

return (
<button
className="group ring-mod-border-focus relative inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-md outline-none ring-inset hover:bg-white/20 focus-visible:ring-2 aria-disabled:hidden"
{...{ [dataLpPassthrough]: true }}
onClick={onClick}
aria-label="Select Playlist"
>
<PlaylistIcon className="w-7 h-7" />
</button>
);
}

export function Captions() {
const options = useCaptionOptions();
const tracks = useMediaState("textTracks");
Expand Down
19 changes: 11 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,6 +24,7 @@ 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 @@ -217,28 +218,30 @@ export class LeafOpener extends Component {
} else {
leaf = workspace.getLeaf(newLeaf as "split", direction);
}
return this.#openMedia(leaf, mediaInfo, viewType);
return this.openMediaIn(leaf, mediaInfo, viewType);
}

async #openMedia(
async openMediaIn(
leaf: WorkspaceLeaf,
mediaInfo: MediaInfo,
viewType?: RemoteMediaViewType,
) {
if (isFileMediaInfo(mediaInfo)) {
await leaf.openFile(mediaInfo.file, {
eState: { subpath: mediaInfo.hash },
const url = mediaInfoToURL(mediaInfo, this.app.vault);
const file = url.getVaultFile(this.app.vault);
if (file) {
await leaf.openFile(file, {
eState: { subpath: url.hash },
active: true,
});
} else {
const { hash, source } = mediaInfo.jsonState;
const { hash, source } = url.jsonState;
const state:
| MediaEmbedViewState
| MediaWebpageViewState
| MediaUrlViewState = {
source,
};
viewType ??= this.plugin.urlViewType.getPreferred(mediaInfo);
viewType ??= this.plugin.urlViewType.getPreferred(url);
await leaf.setViewState(
{
type: viewType,
Expand All @@ -257,7 +260,7 @@ export class LeafOpener extends Component {
): Promise<MediaLeaf | null> {
const pinned = this.findPinnedPlayer();
if (pinned) {
return await this.#openMedia(pinned, info, remoteViewType);
return await this.openMediaIn(pinned, info, remoteViewType);
}
const opened = this.findPlayerWithSameMedia(info);
if (opened) {
Expand Down
27 changes: 11 additions & 16 deletions apps/app/src/media-note/note-index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Component, TFolder, TFile, parseLinktext } from "obsidian";
import type { MetadataCache, Vault, CachedMetadata } from "obsidian";
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 } from "../../media-view/media-info";

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

findNotes(media: MediaInfo): TFile[] {
const notes = this.mediaToNoteIndex.get(mediaInfoToString(media));
const notes = this.mediaToNoteIndex.get(this.mediaInfoToString(media));
if (!notes) return [];
return [...notes];
}
Expand Down Expand Up @@ -67,11 +67,16 @@ 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 = mediaInfoToString(mediaInfo);
const mediaInfoKey = this.mediaInfoToString(mediaInfo);
const mediaNotes = this.mediaToNoteIndex.get(mediaInfoKey);
if (!mediaNotes) return;
mediaNotes.delete(toRemove);
Expand All @@ -81,12 +86,10 @@ export class MediaNoteIndex extends Component {
}
addMediaNote(mediaInfo: MediaInfo, newNote: TFile) {
this.noteToMediaIndex.set(newNote.path, mediaInfo);
const mediaNotes = this.mediaToNoteIndex.get(mediaInfoToString(mediaInfo));
const key = this.mediaInfoToString(mediaInfo);
const mediaNotes = this.mediaToNoteIndex.get(key);
if (!mediaNotes) {
this.mediaToNoteIndex.set(
mediaInfoToString(mediaInfo),
new Set([newNote]),
);
this.mediaToNoteIndex.set(key, new Set([newNote]));
} else {
mediaNotes.add(newNote);
}
Expand Down Expand Up @@ -205,11 +208,3 @@ function getField(
if (typeof content !== "string") return null;
return ctx.plugin.resolveUrl(content);
}

function mediaInfoToString(info: MediaInfo) {
if (isFileMediaInfo(info)) {
return `file:${info.file.path}`;
} else {
return `url:${info.jsonState.source}`;
}
}
Loading

0 comments on commit 37058bc

Please sign in to comment.