master e1ef0c285aa5 cached
18 files
26.5 KB
7.5k tokens
28 symbols
1 requests
Download .txt
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<React.SetStateAction<boolean>>;
	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<React.SetStateAction<boolean>>) => {
					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<string, number>;
	urlColor: string;
	timestampColor: string;
	urlTextColor: string;
	timestampTextColor: string;
	forwardSeek: string;
	backwardsSeek: string;
}

export const DEFAULT_SETTINGS: TimestampPluginSettings = {
	noteTitle: "",
	urlStartTimeMap: new Map<string, number>(),
	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 <br> 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<React.SetStateAction<boolean>>) => 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<ReactPlayer>();

	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 (
		<>
			<ReactPlayer
				ref={playerRef}
				url={url}
				playing={playing}
				controls={true}
				width='100%'
				height='95%'
				onReady={onReady}
				onError={(err) => 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(
			<VideoContainer
				url={url}
				start={start}
				setupPlayer={setupPlayer}
				setupError={setupError}
			/>
		);
	}

	async onClose() {
		if (this.saveTimeOnUnload) await this.saveTimeOnUnload();
		this.root.unmount()
		ReactDOM.unmountComponentAtNode(this.containerEl.children[1]);
	}
}
Download .txt
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
Download .txt
SYMBOL INDEX (28 symbols across 4 files)

FILE: main.ts
  constant ERRORS (line 8) | const ERRORS: { [key: string]: string } = {
  class TimestampPlugin (line 13) | class TimestampPlugin extends Plugin {
    method onload (line 19) | async onload() {
    method onunload (line 166) | async onunload() {
    method activateView (line 174) | async activateView(url: string, editor: Editor) {
    method loadSettings (line 220) | async loadSettings() {
    method saveSettings (line 231) | async saveSettings() {
  class SampleModal (line 236) | class SampleModal extends Modal {
    method constructor (line 239) | constructor(app: App, activateView: (url: string, editor: Editor) => v...
    method onOpen (line 245) | onOpen() {
    method onClose (line 262) | onClose() {

FILE: settings.ts
  type TimestampPluginSettings (line 4) | interface TimestampPluginSettings {
  constant DEFAULT_SETTINGS (line 15) | const DEFAULT_SETTINGS: TimestampPluginSettings = {
  constant COLORS (line 26) | const COLORS = { 'blue': 'blue', 'red': 'red', 'green': 'green', 'yellow...
  constant TIMES (line 28) | const TIMES = { '5': '5', '10': '10', '15': '15', '20': '20', '25': '25'...
  class TimestampPluginSettingTab (line 30) | class TimestampPluginSettingTab extends PluginSettingTab {
    method constructor (line 33) | constructor(app: App, plugin: TimestampPlugin) {
    method display (line 38) | display(): void {

FILE: view/VideoContainer.tsx
  type VideoContainerProps (line 7) | interface VideoContainerProps {

FILE: view/VideoView.tsx
  type VideoViewProps (line 8) | interface VideoViewProps extends VideoContainerProps {
  constant VIDEO_VIEW (line 12) | const VIDEO_VIEW = "video-view";
  class VideoView (line 13) | class VideoView extends ItemView {
    method constructor (line 17) | constructor(leaf: WorkspaceLeaf) {
    method getViewType (line 23) | getViewType() {
    method getDisplayText (line 27) | getDisplayText() {
    method getIcon (line 31) | getIcon(): string {
    method setEphemeralState (line 35) | setEphemeralState({ url, setupPlayer, setupError, saveTimeOnUnload, st...
    method onClose (line 51) | async onClose() {
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (30K chars).
[
  {
    "path": ".editorconfig",
    "chars": 166,
    "preview": "# top-most EditorConfig file\r\nroot = true\r\n\r\n[*]\r\ncharset = utf-8\r\nend_of_line = lf\r\ninsert_final_newline = true\r\nindent"
  },
  {
    "path": ".eslintignore",
    "chars": 22,
    "preview": "npm node_modules\nbuild"
  },
  {
    "path": ".eslintrc",
    "chars": 627,
    "preview": "{\n    \"root\": true,\n    \"parser\": \"@typescript-eslint/parser\",\n    \"env\": { \"node\": true },\n    \"plugins\": [\n      \"@typ"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2597,
    "preview": "name: Release Obsidian plugin\n\non:\n  push:\n    tags:\n      - \"*\"\n\nenv:\n  PLUGIN_NAME: obsidian-timestamp-notes\n\njobs:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 316,
    "preview": "# vscode\r\n.vscode \r\n\r\n# Intellij\r\n*.iml\r\n.idea\r\n\r\n# npm\r\nnode_modules\r\n\r\n# Don't include the compiled main.js file in th"
  },
  {
    "path": ".npmrc",
    "chars": 21,
    "preview": "tag-version-prefix=\"\""
  },
  {
    "path": "README.md",
    "chars": 3612,
    "preview": "*NOTE: 1/21/24: Thank you everyone for using this plugin! Seeing the small community that has grown around this project,"
  },
  {
    "path": "esbuild.config.mjs",
    "chars": 1176,
    "preview": "import esbuild from \"esbuild\";\nimport process from \"process\";\nimport builtins from 'builtin-modules'\n\nconst banner =\n`/*"
  },
  {
    "path": "main.ts",
    "chars": 9152,
    "preview": "import { Editor, MarkdownView, Plugin, Modal, App } from 'obsidian';\nimport ReactPlayer from 'react-player/lazy'\n\nimport"
  },
  {
    "path": "manifest.json",
    "chars": 419,
    "preview": "{\n\t\"id\": \"obsidian-timestamp-notes\",\n\t\"name\": \"Timestamp Notes\",\n\t\"version\": \"1.0.8\",\n\t\"minAppVersion\": \"0.12.0\",\n\t\"desc"
  },
  {
    "path": "package.json",
    "chars": 949,
    "preview": "{\n\t\"name\": \"obsidian-timestamp-notes\",\n\t\"version\": \"1.0.8\",\n\t\"description\": \"This plugin allows side-by-side notetaking "
  },
  {
    "path": "settings.ts",
    "chars": 4357,
    "preview": "import { App, PluginSettingTab, Setting } from 'obsidian';\nimport TimestampPlugin from './main';\n\nexport interface Times"
  },
  {
    "path": "styles.css",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tsconfig.json",
    "chars": 437,
    "preview": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"inlineSourceMap\": true,\n    \"inlineSources\": true,\n    \"module\": \"ESNe"
  },
  {
    "path": "version-bump.mjs",
    "chars": 648,
    "preview": "import { readFileSync, writeFileSync } from \"fs\";\n\nconst targetVersion = process.env.npm_package_version;\n\n// read minAp"
  },
  {
    "path": "versions.json",
    "chars": 42,
    "preview": "{\n\t\"1.0.0\": \"0.9.7\",\n\t\"1.0.1\": \"0.12.0\"\n}\n"
  },
  {
    "path": "view/VideoContainer.tsx",
    "chars": 1213,
    "preview": "import * as React from \"react\";\nimport { useRef, useState } from 'react';\n\nimport ReactPlayer from 'react-player/lazy'\n\n"
  },
  {
    "path": "view/VideoView.tsx",
    "chars": 1384,
    "preview": "import { ItemView, WorkspaceLeaf } from 'obsidian';\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom"
  }
]

About this extraction

This page contains the full source code of the juliang22/ObsidianTimestampNotes GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (26.5 KB), approximately 7.5k tokens, and a symbol index with 28 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!