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]);
}
}
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
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.