Repository: juliang22/ObsidianTimestampNotes Branch: master Commit: e1ef0c285aa5 Files: 18 Total size: 26.5 KB Directory structure: gitextract_kzqi6bzb/ ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github/ │ └── workflows/ │ └── release.yml ├── .gitignore ├── .npmrc ├── README.md ├── esbuild.config.mjs ├── main.ts ├── manifest.json ├── package.json ├── settings.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs ├── versions.json └── view/ ├── VideoContainer.tsx └── VideoView.tsx ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # top-most EditorConfig file root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = tab indent_size = 4 tab_width = 4 ================================================ FILE: .eslintignore ================================================ npm node_modules build ================================================ FILE: .eslintrc ================================================ { "root": true, "parser": "@typescript-eslint/parser", "env": { "node": true }, "plugins": [ "@typescript-eslint" ], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" ], "parserOptions": { "sourceType": "module" }, "rules": { "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], "@typescript-eslint/ban-ts-comment": "off", "no-prototype-builtins": "off", "@typescript-eslint/no-empty-function": "off" } } ================================================ FILE: .github/workflows/release.yml ================================================ name: Release Obsidian plugin on: push: tags: - "*" env: PLUGIN_NAME: obsidian-timestamp-notes jobs: build: runs-on: ubuntu-latest permissions: write-all steps: - uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v1 with: node-version: "14.x" - name: Build id: build run: | npm install npm run build mkdir ${{ env.PLUGIN_NAME }} cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} ls echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ github.ref }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: false prerelease: false - name: Upload zip file id: upload-zip uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./${{ env.PLUGIN_NAME }}.zip asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip asset_content_type: application/zip - name: Upload main.js id: upload-main uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./main.js asset_name: main.js asset_content_type: text/javascript - name: Upload manifest.json id: upload-manifest uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./manifest.json asset_name: manifest.json asset_content_type: application/json # - name: Upload styles.css # id: upload-css # uses: actions/upload-release-asset@v1 # env: # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # with: # upload_url: ${{ steps.create_release.outputs.upload_url }} # asset_path: ./styles.css # asset_name: styles.css # asset_content_type: text/css ================================================ FILE: .gitignore ================================================ # vscode .vscode # Intellij *.iml .idea # npm node_modules # Don't include the compiled main.js file in the repo. # They should be uploaded to GitHub releases instead. main.js # Exclude sourcemaps *.map # obsidian data.json # Exclude macOS Finder (System Explorer) View States .DS_Store ================================================ FILE: .npmrc ================================================ tag-version-prefix="" ================================================ FILE: README.md ================================================ *NOTE: 1/21/24: Thank you everyone for using this plugin! Seeing the small community that has grown around this project, and the diverse ways in which you have all utilized it, is truly the highest reward I could have hoped for. I initially created this plugin while I was between jobs and heavily using Obsidian. Now that neither of those things are true, I haven’t really had the time or motivation to actively maintain and improve this project. That being said, I am also thrilled to share some exciting news regarding the future of ObsidianTimestampNotes. @mattcoleanderson has kindly offered to contribute and will being taking over as an active maintainer of this project. I'm excited to pass on the baton and looking forward to seeing where he takes this plugin!* ## Obsidian Timestamp Notes ### Use Case Hello Obsidian users! Like all of you, I love using Obsidian for taking notes. My usual workflow is a video in my browser on one side of my screen while I jot down notes in Obsidian on the other side. While Obsidian itself is a great notetaking tool, I found this setup quite lacking. When reviewing my notes, it would often take me a long time to find the section of the video the note came from and I found it annoying constantly having to switch between my browser and Obsidian. ## Solution This plugin solves this issue by allowing you to: - Open up a video player in Obsidian's sidebar - Insert timestamps with a hotkey - Select timestamps to navigate to that place in the video ## Setup - Download and enable the plugin - Set the hotkeys for - Opening the video player (my default is cmnd-shift-y) - Opening a local video (my defauly is cmnd-shift-l) - Inserting timestamps (my default is cmnd-y) - Playing/pausing video (my default is cntrl-space) - Seeking forward/back (my default is cntrl-arrows) - Set options for - Colors of the url, url text, timestamp button, and timestamp text - Title that is pasted when 'Open Video Player' hotkey is used - How far you want to seek forward/back ## Usage - Highlight a video url and use the 'Open Video Player' hotkey or press your designated hotkey to select a local video to play (no need to highlight text for local videos) - Jot down notes and anytime you want to insert a timestamp, press the registered hotkey - Toggle pausing/playing the video by using hotkey (my default is option space) - Open videos at the timestamp you left off on (this is reset if plugin is disabled) - Close the player by right-clicking the icon above the video player and selecting close ## Valid Video Players This plugin should work with: - youtube - vimeo - facebook - soundcloud - wistia - mixcloud - dailymotion - twitch - local videos ## Demo https://user-images.githubusercontent.com/39292521/167230491-f5439a62-a3f7-445c-a208-839c804953d7.mov ## Known Issues 1. Inserting timestamps into a bulleted section does not work. Unfortunately, code-blocks cannot be in-line with text. Make sure to press enter/insert the timestamp on a new line. 2. If you decide to change the colors of your buttons/text, any old buttons/text will not update with the new colors until you reload the app. You can also click the '<>' when hovering over the code-block and it will refresh with the new colors. 3. If your timestamp/video button dont work, simply switch between live-editing and viewing modes. 4. Local videos currently cannot generate buttons. It's probably doable, but I couldn't find a way to make it work without glitching. ## Other Authors This plugin uses the react-player npm package: https://www.npmjs.com/package/react-player. ================================================ FILE: esbuild.config.mjs ================================================ import esbuild from "esbuild"; import process from "process"; import builtins from 'builtin-modules' const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ `; const prod = (process.argv[2] === 'production'); esbuild.build({ banner: { js: banner, }, entryPoints: ['main.ts'], bundle: true, external: [ 'obsidian', 'electron', '@codemirror/autocomplete', '@codemirror/closebrackets', '@codemirror/collab', '@codemirror/commands', '@codemirror/comment', '@codemirror/fold', '@codemirror/gutter', '@codemirror/highlight', '@codemirror/history', '@codemirror/language', '@codemirror/lint', '@codemirror/matchbrackets', '@codemirror/panel', '@codemirror/rangeset', '@codemirror/rectangular-selection', '@codemirror/search', '@codemirror/state', '@codemirror/stream-parser', '@codemirror/text', '@codemirror/tooltip', '@codemirror/view', ...builtins], format: 'cjs', watch: !prod, target: 'es2016', logLevel: "info", sourcemap: prod ? false : 'inline', treeShaking: true, outfile: 'main.js', }).catch(() => process.exit(1)); ================================================ FILE: main.ts ================================================ import { Editor, MarkdownView, Plugin, Modal, App } from 'obsidian'; import ReactPlayer from 'react-player/lazy' import { VideoView, VIDEO_VIEW } from './view/VideoView'; import { TimestampPluginSettings, TimestampPluginSettingTab, DEFAULT_SETTINGS } from 'settings'; const ERRORS: { [key: string]: string } = { "INVALID_URL": "\n> [!error] Invalid Video URL\n> The highlighted link is not a valid video url. Please try again with a valid link.\n", "NO_ACTIVE_VIDEO": "\n> [!caution] Select Video\n> A video needs to be opened before using this hotkey.\n Highlight your video link and input your 'Open video player' hotkey to register a video.\n", } export default class TimestampPlugin extends Plugin { settings: TimestampPluginSettings; player: ReactPlayer; setPlaying: React.Dispatch>; editor: Editor; async onload() { // Register view this.registerView( VIDEO_VIEW, (leaf) => new VideoView(leaf) ); // Register settings await this.loadSettings(); // Markdown processor that turns timestamps into buttons this.registerMarkdownCodeBlockProcessor("timestamp", (source, el, ctx) => { // Match mm:ss or hh:mm:ss timestamp format const regExp = /\d+:\d+:\d+|\d+:\d+/g; const rows = source.split("\n").filter((row) => row.length > 0); rows.forEach((row) => { const match = row.match(regExp); if (match) { //create button for each timestamp const div = el.createEl("div"); const button = div.createEl("button"); button.innerText = match[0]; button.style.backgroundColor = this.settings.timestampColor; button.style.color = this.settings.timestampTextColor; // convert timestamp to seconds and seek to that position when clicked button.addEventListener("click", () => { const timeArr = match[0].split(":").map((v) => parseInt(v)); const [hh, mm, ss] = timeArr.length === 2 ? [0, ...timeArr] : timeArr; const seconds = (hh || 0) * 3600 + (mm || 0) * 60 + (ss || 0); if (this.player) this.player.seekTo(seconds); }); div.appendChild(button); } }) }); // Markdown processor that turns video urls into buttons to open views of the video this.registerMarkdownCodeBlockProcessor("timestamp-url", (source, el, ctx) => { const url = source.trim(); if (ReactPlayer.canPlay(url)) { // Creates button for video url const div = el.createEl("div"); const button = div.createEl("button"); button.innerText = url; button.style.backgroundColor = this.settings.urlColor; button.style.color = this.settings.urlTextColor; button.addEventListener("click", () => { this.activateView(url, this.editor); }); } else { if (this.editor) { this.editor.replaceSelection(this.editor.getSelection() + "\n" + ERRORS["INVALID_URL"]); } } }); // Command that gets selected video link and sends it to view which passes it to React component this.addCommand({ id: 'trigger-player', name: 'Open video player (copy video url and use hotkey)', editorCallback: (editor: Editor, view: MarkdownView) => { // Get selected text and match against video url to convert link to video video id const url = editor.getSelection().trim(); // Activate the view with the valid link if (ReactPlayer.canPlay(url)) { this.activateView(url, editor); this.settings.noteTitle ? editor.replaceSelection("\n" + this.settings.noteTitle + "\n" + "```timestamp-url \n " + url + "\n ```\n") : editor.replaceSelection("```timestamp-url \n " + url + "\n ```\n") this.editor = editor; } else { editor.replaceSelection(ERRORS["INVALID_URL"]) } editor.setCursor(editor.getCursor().line + 1) } }); // This command inserts the timestamp of the playing video into the editor this.addCommand({ id: 'timestamp-insert', name: 'Insert timestamp based on videos current play time', editorCallback: (editor: Editor, view: MarkdownView) => { if (!this.player) { editor.replaceSelection(ERRORS["NO_ACTIVE_VIDEO"]) return } // convert current video time into timestamp const leadingZero = (num: number) => num < 10 ? "0" + num.toFixed(0) : num.toFixed(0); const totalSeconds = Number(this.player.getCurrentTime().toFixed(2)); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60); const seconds = totalSeconds - (hours * 3600) - (minutes * 60); const time = (hours > 0 ? leadingZero(hours) + ":" : "") + leadingZero(minutes) + ":" + leadingZero(seconds); // insert timestamp into editor editor.replaceSelection("```timestamp \n " + time + "\n ```\n") } }); //Command that play/pauses the video this.addCommand({ id: 'pause-player', name: 'Pause player', editorCallback: (editor: Editor, view: MarkdownView) => { this.setPlaying(!this.player.props.playing) } }); // Seek forward by set amount of seconds this.addCommand({ id: 'seek-forward', name: 'Seek Forward', editorCallback: (editor: Editor, view: MarkdownView) => { if (this.player) this.player.seekTo(this.player.getCurrentTime() + parseInt(this.settings.forwardSeek)); } }); // Seek backwards by set amount of seconds this.addCommand({ id: 'seek-backward', name: 'Seek Backward', editorCallback: (editor: Editor, view: MarkdownView) => { if (this.player) this.player.seekTo(this.player.getCurrentTime() - parseInt(this.settings.backwardsSeek)); } }); // This adds a complex command that can check whether the current state of the app allows execution of the command this.addCommand({ id: 'open-sample-modal-complex', name: 'Open sample modal (complex)', editorCallback: (editor: Editor, view: MarkdownView) => { this.editor = editor; new SampleModal(this.app, this.activateView.bind(this), editor).open(); // This command will only show up in Command Palette when the check function returns true return true; } }); // This adds a settings tab so the user can configure various aspects of the plugin this.addSettingTab(new TimestampPluginSettingTab(this.app, this)); } async onunload() { this.player = null; this.editor = null; this.setPlaying = null; this.app.workspace.detachLeavesOfType(VIDEO_VIEW); } // This is called when a valid url is found => it activates the View which loads the React view async activateView(url: string, editor: Editor) { this.app.workspace.detachLeavesOfType(VIDEO_VIEW); await this.app.workspace.getRightLeaf(false).setViewState({ type: VIDEO_VIEW, active: true, }); this.app.workspace.revealLeaf( this.app.workspace.getLeavesOfType(VIDEO_VIEW)[0] ); // This triggers the React component to be loaded this.app.workspace.getLeavesOfType(VIDEO_VIEW).forEach(async (leaf) => { if (leaf.view instanceof VideoView) { const setupPlayer = (player: ReactPlayer, setPlaying: React.Dispatch>) => { this.player = player; this.setPlaying = setPlaying; } const setupError = (err: string) => { editor.replaceSelection(editor.getSelection() + `\n> [!error] Streaming Error \n> ${err}\n`); } const saveTimeOnUnload = async () => { if (this.player) { this.settings.urlStartTimeMap.set(url, Number(this.player.getCurrentTime().toFixed(0))); } await this.saveSettings(); } // create a new video instance, sets up state/unload functionality, and passes in a start time if available else 0 leaf.setEphemeralState({ url, setupPlayer, setupError, saveTimeOnUnload, start: ~~this.settings.urlStartTimeMap.get(url) }); await this.saveSettings(); } }); } async loadSettings() { // Fix for a weird bug that turns default map into a normal object when loaded const data = await this.loadData() if (data) { const map = new Map(Object.keys(data.urlStartTimeMap).map(k => [k, data.urlStartTimeMap[k]])) this.settings = { ...DEFAULT_SETTINGS, ...data, urlStartTimeMap: map }; } else { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } } async saveSettings() { await this.saveData(this.settings); } } class SampleModal extends Modal { editor: Editor; activateView: (url: string, editor: Editor) => void; constructor(app: App, activateView: (url: string, editor: Editor) => void, editor: Editor) { super(app); this.activateView = activateView; this.editor = editor; } onOpen() { const { contentEl } = this; // add an input field to contentEl const input = contentEl.createEl('input'); input.setAttribute("type", "file"); input.onchange = (e: any) => { // accept local video input and make a url from input const url = URL.createObjectURL(e.target.files[0]); this.activateView(url, this.editor); // Can't get the buttons to work with local videos unfortunately // this.editor.replaceSelection("\n" + "```timestamp-url \n " + url.trim() + "\n ```\n") this.close(); } } onClose() { const { contentEl } = this; contentEl.empty(); } } ================================================ FILE: manifest.json ================================================ { "id": "obsidian-timestamp-notes", "name": "Timestamp Notes", "version": "1.0.8", "minAppVersion": "0.12.0", "description": "This plugin allows side-by-side notetaking with videos. Annotate your notes with timestamps to directly control the video and remember where each note comes from.", "author": "Julian Grunauer", "authorUrl": "https://github.com/juliang22/ObsidianTimestampNotes", "isDesktopOnly": true } ================================================ FILE: package.json ================================================ { "name": "obsidian-timestamp-notes", "version": "1.0.8", "description": "This plugin allows side-by-side notetaking with videos. Annotate your notes with timestamps to directly control the video and remember where each note comes from.", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", "@types/react": "^18.0.9", "@types/react-dom": "^18.0.4", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "builtin-modules": "^3.2.0", "esbuild": "0.13.12", "obsidian": "latest", "tslib": "2.3.1", "typescript": "4.4.4" }, "dependencies": { "react": "^18.1.0", "react-dom": "^18.1.0", "react-player": "^2.10.1" } } ================================================ FILE: settings.ts ================================================ import { App, PluginSettingTab, Setting } from 'obsidian'; import TimestampPlugin from './main'; export interface TimestampPluginSettings { noteTitle: string; urlStartTimeMap: Map; urlColor: string; timestampColor: string; urlTextColor: string; timestampTextColor: string; forwardSeek: string; backwardsSeek: string; } export const DEFAULT_SETTINGS: TimestampPluginSettings = { noteTitle: "", urlStartTimeMap: new Map(), urlColor: 'blue', timestampColor: 'green', urlTextColor: 'white', timestampTextColor: 'white', forwardSeek: '10', backwardsSeek: '10' } const COLORS = { 'blue': 'blue', 'red': 'red', 'green': 'green', 'yellow': 'yellow', 'orange': 'orange', 'purple': 'purple', 'pink': 'pink', 'grey': 'grey', 'black': 'black', 'white': 'white' }; const TIMES = { '5': '5', '10': '10', '15': '15', '20': '20', '25': '25', '30': '30', '35': '35', '40': '40', '45': '45', '50': '50', '55': '55', '60': '60', '65': '65', '70': '70', '75': '75', '80': '80', '85': '85', '90': '90', '95': '95', '100': '100', '105': '105', '110': '110', '115': '115', '120': '120' } export class TimestampPluginSettingTab extends PluginSettingTab { plugin: TimestampPlugin; constructor(app: App, plugin: TimestampPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); containerEl.createEl('h2', { text: 'Timestamp Notes Plugin' }); // Customize title new Setting(containerEl) .setName('Title') .setDesc('This title will be printed after opening a video with the hotkey. Use
for new lines.') .addText(text => text .setPlaceholder('Enter title template.') .setValue(this.plugin.settings.noteTitle) .onChange(async (value) => { this.plugin.settings.noteTitle = value; await this.plugin.saveSettings(); })); // Customize url button color new Setting(containerEl) .setName('URL Button Color') .setDesc('Pick a color for the url button.') .addDropdown(dropdown => dropdown .addOptions(COLORS) .setValue(this.plugin.settings.urlColor) .onChange(async (value) => { this.plugin.settings.urlColor = value; await this.plugin.saveSettings(); } )); // Customize url text color new Setting(containerEl) .setName('URL Text Color') .setDesc('Pick a color for the URL text button.') .addDropdown(dropdown => dropdown .addOptions(COLORS) .setValue(this.plugin.settings.urlTextColor) .onChange(async (value) => { this.plugin.settings.urlTextColor = value; await this.plugin.saveSettings(); } )); // Customize timestamp button color new Setting(containerEl) .setName('Timestamp Button Color') .setDesc('Pick a color for the timestamp button.') .addDropdown(dropdown => dropdown .addOptions(COLORS) .setValue(this.plugin.settings.timestampColor) .onChange(async (value) => { this.plugin.settings.timestampColor = value; await this.plugin.saveSettings(); } )); // Customize timestamp text color new Setting(containerEl) .setName('Timestamp Text Color') .setDesc('Pick a color for the timestamp text.') .addDropdown(dropdown => dropdown .addOptions(COLORS) .setValue(this.plugin.settings.timestampTextColor) .onChange(async (value) => { this.plugin.settings.timestampTextColor = value; await this.plugin.saveSettings(); } )); // Customize forward seek time new Setting(containerEl) .setName('Foward time seek') .setDesc('This is the amount of seconds the video will seek forward when pressing the seek forward command.') .addDropdown(dropdown => dropdown .addOptions(TIMES) .setValue(this.plugin.settings.forwardSeek) .onChange(async (value) => { this.plugin.settings.forwardSeek = value; await this.plugin.saveSettings(); } )); // Customize backwards seek time new Setting(containerEl) .setName('Backwards time seek') .setDesc('This is the amount of seconds the video will seek backwards when pressing the seek backwards command.') .addDropdown(dropdown => dropdown .addOptions(TIMES) .setValue(this.plugin.settings.backwardsSeek) .onChange(async (value) => { this.plugin.settings.backwardsSeek = value; await this.plugin.saveSettings(); } )); } } ================================================ FILE: styles.css ================================================ ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", "target": "ES6", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", "importHelpers": true, "isolatedModules": true, "jsx": "react", "lib": [ "DOM", "ES5", "ES6", "ES7" ] }, "include": [ "**/*.ts", "view/VideoView.tsx" ] } ================================================ FILE: version-bump.mjs ================================================ import { readFileSync, writeFileSync } from "fs"; const targetVersion = process.env.npm_package_version; // read minAppVersion from manifest.json and bump version to target version let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); const { minAppVersion } = manifest; manifest.version = targetVersion; writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); // update versions.json with target version and minAppVersion from manifest.json let versions = JSON.parse(readFileSync("versions.json", "utf8")); versions[targetVersion] = minAppVersion; writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); ================================================ FILE: versions.json ================================================ { "1.0.0": "0.9.7", "1.0.1": "0.12.0" } ================================================ FILE: view/VideoContainer.tsx ================================================ import * as React from "react"; import { useRef, useState } from 'react'; import ReactPlayer from 'react-player/lazy' export interface VideoContainerProps { url: string; start: number setupPlayer: (player: ReactPlayer, setPlaying: React.Dispatch>) => void; setupError: (err: string) => void; } export const VideoContainer = ({ url, setupPlayer, start, setupError }: VideoContainerProps): JSX.Element => { // Reference to player passed back to the setupPlayer prop const playerRef = useRef(); const [playing, setPlaying] = useState(true) const onReady = () => { // Starts player at last played time if the video has been played before if (start) playerRef.current.seekTo(start); // Sets up video player to be accessed in main.ts if (playerRef) setupPlayer(playerRef.current, setPlaying); } return ( <> setupError(err ? err.message : `Video is unplayable due to privacy settings, streaming permissions, etc.`)} // Error handling for invalid URLs /> ) }; ================================================ FILE: view/VideoView.tsx ================================================ import { ItemView, WorkspaceLeaf } from 'obsidian'; import * as React from "react"; import * as ReactDOM from "react-dom"; import { createRoot, Root } from 'react-dom/client'; import { VideoContainer, VideoContainerProps } from "./VideoContainer" export interface VideoViewProps extends VideoContainerProps { saveTimeOnUnload: () => void; } export const VIDEO_VIEW = "video-view"; export class VideoView extends ItemView { component: ReactDOM.Renderer saveTimeOnUnload: () => void root: Root constructor(leaf: WorkspaceLeaf) { super(leaf); this.saveTimeOnUnload = () => { }; this.root = createRoot(this.containerEl.children[1]) } getViewType() { return VIDEO_VIEW; } getDisplayText() { return "Timestamp Video"; } getIcon(): string { return "video"; } setEphemeralState({ url, setupPlayer, setupError, saveTimeOnUnload, start }: VideoViewProps) { // Allows view to save the playback time in the setting state when the view is closed this.saveTimeOnUnload = saveTimeOnUnload; // Create a root element for the view to render into this.root.render( ); } async onClose() { if (this.saveTimeOnUnload) await this.saveTimeOnUnload(); this.root.unmount() ReactDOM.unmountComponentAtNode(this.containerEl.children[1]); } }