Skip to content

Commit 452f050

Browse files
committed
Added saving playback time to video, updated to new React rendering, and added pause
1 parent 82804c8 commit 452f050

File tree

5 files changed

+66
-22
lines changed

5 files changed

+66
-22
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ This plugin solves this issue by allowing you to:
1515
- Set the hotkeys for opening the YouTube Player and inserting timestamps (my default is cmnd-shift-y and cmnd-y, respectively)
1616
- Highlight a YouTube url and select either the Ribbon note icon or the Open YouTube Player hotkey
1717
- Jot down notes and anytime you want to insert a timestamp, press the registered hotkey
18-
- Toggle pause the player by using hotkey (my default is option space)
19-
- Close the player by right-clicking the icon above the YouTube player and selecting close
18+
- Toggle pausing/playing the video by using hotkey (my default is option space)
19+
- Open videos at the timestamp you left off on (this is reset if plugin is disabled)
20+
- Close the player by right-clicking the icon above the YouTube player and selecting close
2021

2122
## Demo
2223

YTContainer.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,28 @@ import * as React from "react";
22
import { useState } from 'react';
33
import YouTube, { YouTubeProps, YouTubePlayer } from 'react-youtube';
44

5-
export const YTContainer = ({ url, setupPlayer }: { url: string, setupPlayer: (yt: YouTubePlayer) => void }): JSX.Element => {
5+
6+
export interface YTContainerProps {
7+
url: string;
8+
setupPlayer: (yt: YouTubePlayer) => void;
9+
start: number
10+
}
11+
12+
export const YTContainer = ({ url, setupPlayer, start }: YTContainerProps): JSX.Element => {
13+
614
const [options] = useState<YouTubeProps['opts']>({
715
height: '410',
816
width: '100%',
917
playerVars: {
1018
// https://developers.google.com/youtube/player_parameters
1119
autoplay: 1,
20+
start: start,
1221
}
1322
});
1423

15-
1624
const onPlayerReady: YouTubeProps['onReady'] = (event) => {
1725
// access to player in all event handlers via event.target
18-
setupPlayer(event.target)
26+
setupPlayer(event.target);
1927
}
2028

2129
return (

main.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import { YouTubePlayer } from 'react-youtube';
55
interface YoutubeTimestampPluginSettings {
66
mySetting: string;
77
player: YouTubePlayer;
8+
urlStartTimeMap: Map<string, number>;
9+
currURL: string;
810
}
911

1012
const DEFAULT_SETTINGS: YoutubeTimestampPluginSettings = {
1113
mySetting: "",
12-
player: undefined
14+
player: undefined,
15+
urlStartTimeMap: new Map<string, number>(),
16+
currURL: undefined,
1317
}
1418

1519
export default class YoutubeTimestampPlugin extends Plugin {
1620
settings: YoutubeTimestampPluginSettings;
17-
1821
// Helper function to validate url and activate view
1922
validateURL = (url: string) => {
2023
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
@@ -75,7 +78,7 @@ export default class YoutubeTimestampPlugin extends Plugin {
7578
id: 'trigger-youtube-player',
7679
name: 'Open Youtube Player (copy youtube url and use hotkey)',
7780
editorCallback: (editor: Editor, view: MarkdownView) => {
78-
// Get selected text and match against youtube url to convert link to youtube video id
81+
// Get selected text and match against youtube url to convert link to youtube video id => also triggers activateView in validateURL
7982
const url = editor.getSelection().trim();
8083
editor.replaceSelection(editor.getSelection() + "\n" + this.validateURL(url));
8184
editor.setCursor(editor.getCursor().line + 1)
@@ -126,8 +129,15 @@ export default class YoutubeTimestampPlugin extends Plugin {
126129
});
127130
}
128131

129-
onunload() {
132+
async onunload() {
133+
if (this.settings.player) {
134+
this.settings.player.destroy();
135+
}
136+
137+
this.settings.player = null;
138+
this.settings.currURL = null;
130139
this.app.workspace.detachLeavesOfType(YOUTUBE_VIEW);
140+
await this.saveSettings();
131141
}
132142

133143
// This is called when a valid url is found => it activates the View which loads the React view
@@ -144,21 +154,36 @@ export default class YoutubeTimestampPlugin extends Plugin {
144154
);
145155

146156

157+
this.settings.currURL = url;
147158
// This triggers the React component to be loaded
148159
this.app.workspace.getLeavesOfType(YOUTUBE_VIEW).forEach(async (leaf) => {
149160
if (leaf.view instanceof YoutubeView) {
150161

151162
const setupPlayer = (yt: YouTubePlayer) => {
152163
this.settings.player = yt;
153164
}
154-
leaf.setEphemeralState({ url, setupPlayer });
165+
166+
const saveTimeOnUnload = async () => {
167+
if (this.settings.player) {
168+
this.settings.urlStartTimeMap.set(this.settings.currURL, this.settings.player.getCurrentTime().toFixed(0));
169+
}
170+
await this.saveSettings();
171+
}
172+
173+
// create a new YoutubeView instance, sets up state/unload functionality, and passes in a start time if available else 0
174+
leaf.setEphemeralState({ url, setupPlayer, saveTimeOnUnload, start: ~~this.settings.urlStartTimeMap.get(url) });
175+
155176
await this.saveSettings();
156177
}
157178
});
158179
}
159180

160181
async loadSettings() {
161-
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
182+
// Fix for a weird bug that turns default map into a normal object when loaded
183+
const data = await this.loadData()
184+
const map = new Map(Object.keys(data.urlStartTimeMap).map(k => [k, data.urlStartTimeMap[k]]))
185+
186+
this.settings = { ...DEFAULT_SETTINGS, ...data, urlStartTimeMap: map };
162187
}
163188

164189

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "obsidian-youtube-timestamp-notes",
33
"name": "YouTube Timestamp Notes",
4-
"version": "1.0.3",
4+
"version": "1.0.4",
55
"minAppVersion": "0.12.0",
66
"description": "This plugin allows side-by-side notetaking with YouTube videos. Annotate your notes with timestamps to directly control the video and remember where each note comes from.",
77
"author": "Julian Grunauer",

view/YoutubeView.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import { ItemView, WorkspaceLeaf } from 'obsidian';
22
import * as React from "react";
33
import * as ReactDOM from "react-dom";
4-
import { YTContainer } from "../YTContainer"
5-
import { YouTubePlayer } from 'react-youtube';
4+
import { createRoot, Root } from 'react-dom/client';
5+
6+
import { YTContainer, YTContainerProps } from "../YTContainer"
7+
8+
export interface YTViewProps extends YTContainerProps {
9+
saveTimeOnUnload: () => void;
10+
}
611

712
export const YOUTUBE_VIEW = "example-view";
813
export class YoutubeView extends ItemView {
914
component: ReactDOM.Renderer
15+
saveTimeOnUnload: () => void
16+
root: Root
1017
constructor(leaf: WorkspaceLeaf) {
1118
super(leaf);
19+
this.saveTimeOnUnload = () => { };
20+
this.root = createRoot(this.containerEl.children[1])
1221
}
1322

1423
getViewType() {
@@ -19,21 +28,22 @@ export class YoutubeView extends ItemView {
1928
return "Example view";
2029
}
2130

22-
setEphemeralState({ url, setupPlayer }: { url: string, setupPlayer: (yt: YouTubePlayer) => void }) {
23-
ReactDOM.render(
24-
// @ts-ignore
25-
// <AppContext.Provider value={this.app}>
26-
<YTContainer url={url} setupPlayer={setupPlayer} />,
27-
// </AppContext.Provider>,
28-
this.containerEl.children[1]
29-
);
31+
setEphemeralState({ url, setupPlayer, saveTimeOnUnload, start }: YTViewProps) {
32+
33+
// Allows view to save the playback time in the setting state when the view is closed
34+
this.saveTimeOnUnload = saveTimeOnUnload;
35+
36+
// Create a root element for the view to render into
37+
this.root.render(<YTContainer url={url} setupPlayer={setupPlayer} start={start} />);
3038
}
3139

3240
async onOpen() {
3341

3442
}
3543

3644
async onClose() {
45+
if (this.saveTimeOnUnload) await this.saveTimeOnUnload();
46+
this.root.unmount()
3747
ReactDOM.unmountComponentAtNode(this.containerEl.children[1]);
3848
}
3949
}

0 commit comments

Comments
 (0)