[
  {
    "path": ".editorconfig",
    "content": "# 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_style = tab\r\nindent_size = 4\r\ntab_width = 4\r\n"
  },
  {
    "path": ".eslintignore",
    "content": "npm node_modules\nbuild"
  },
  {
    "path": ".eslintrc",
    "content": "{\n    \"root\": true,\n    \"parser\": \"@typescript-eslint/parser\",\n    \"env\": { \"node\": true },\n    \"plugins\": [\n      \"@typescript-eslint\"\n    ],\n    \"extends\": [\n      \"eslint:recommended\",\n      \"plugin:@typescript-eslint/eslint-recommended\",\n      \"plugin:@typescript-eslint/recommended\"\n    ], \n    \"parserOptions\": {\n        \"sourceType\": \"module\"\n    },\n    \"rules\": {\n      \"no-unused-vars\": \"off\",\n      \"@typescript-eslint/no-unused-vars\": [\"error\", { \"args\": \"none\" }],\n      \"@typescript-eslint/ban-ts-comment\": \"off\",\n      \"no-prototype-builtins\": \"off\",\n      \"@typescript-eslint/no-empty-function\": \"off\"\n    } \n  }"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release Obsidian plugin\n\non:\n  push:\n    tags:\n      - \"*\"\n\nenv:\n  PLUGIN_NAME: obsidian-timestamp-notes\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions: write-all\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js\n        uses: actions/setup-node@v1\n        with:\n          node-version: \"14.x\"\n\n      - name: Build\n        id: build\n        run: |\n          npm install\n          npm run build\n          mkdir ${{ env.PLUGIN_NAME }}\n          cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }}\n          zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}\n          ls\n          echo \"::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)\"\n\n      - name: Create Release\n        id: create_release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          VERSION: ${{ github.ref }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: ${{ github.ref }}\n          draft: false\n          prerelease: false\n\n      - name: Upload zip file\n        id: upload-zip\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./${{ env.PLUGIN_NAME }}.zip\n          asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip\n          asset_content_type: application/zip\n\n      - name: Upload main.js\n        id: upload-main\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./main.js\n          asset_name: main.js\n          asset_content_type: text/javascript\n\n      - name: Upload manifest.json\n        id: upload-manifest\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ./manifest.json\n          asset_name: manifest.json\n          asset_content_type: application/json\n\n      # - name: Upload styles.css\n      #   id: upload-css\n      #   uses: actions/upload-release-asset@v1\n      #   env:\n      #     GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      #   with:\n      #     upload_url: ${{ steps.create_release.outputs.upload_url }}\n      #     asset_path: ./styles.css\n      #     asset_name: styles.css\n      #     asset_content_type: text/css"
  },
  {
    "path": ".gitignore",
    "content": "# 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 the repo.\r\n# They should be uploaded to GitHub releases instead.\r\nmain.js\r\n\r\n# Exclude sourcemaps\r\n*.map\r\n\r\n# obsidian\r\ndata.json\r\n\r\n# Exclude macOS Finder (System Explorer) View States\r\n.DS_Store\r\n"
  },
  {
    "path": ".npmrc",
    "content": "tag-version-prefix=\"\""
  },
  {
    "path": "README.md",
    "content": "*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!*\n\n## Obsidian Timestamp Notes\n\n\n### Use Case\nHello 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. \n\n## Solution\nThis plugin solves this issue by allowing you to:\n- Open up a video player in Obsidian's sidebar\n- Insert timestamps with a hotkey\n- Select timestamps to navigate to that place in the video\n\n## Setup \n- Download and enable the plugin\n- Set the hotkeys for\n  - Opening the video player (my default is cmnd-shift-y)\n  - Opening a local video (my defauly is cmnd-shift-l)\n  - Inserting timestamps (my default is cmnd-y)\n  - Playing/pausing video (my default is cntrl-space)\n  - Seeking forward/back (my default is cntrl-arrows)\n- Set options for\n  - Colors of the url, url text, timestamp button, and timestamp text\n  - Title that is pasted when 'Open Video Player' hotkey is used\n  - How far you want to seek forward/back\n\n## Usage\n- 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)\n- Jot down notes and anytime you want to insert a timestamp, press the registered hotkey\n- Toggle pausing/playing the video by using hotkey (my default is option space)\n- Open videos at the timestamp you left off on (this is reset if plugin is disabled)\n- Close the player by right-clicking the icon above the video player and selecting close \n\n## Valid Video Players\nThis plugin should work with:\n- youtube\n- vimeo\n- facebook\n- soundcloud\n- wistia\t\n- mixcloud\n- dailymotion\n- twitch\n- local videos\n\n## Demo\n\nhttps://user-images.githubusercontent.com/39292521/167230491-f5439a62-a3f7-445c-a208-839c804953d7.mov\n\n\n## Known Issues\n1. 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.\n2. 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.\n3. If your timestamp/video button dont work, simply switch between live-editing and viewing modes.\n4. Local videos currently cannot generate buttons. It's probably doable, but I couldn't find a way to make it work without glitching. \n\n\n## Other Authors\nThis plugin uses the react-player npm package: https://www.npmjs.com/package/react-player.\n"
  },
  {
    "path": "esbuild.config.mjs",
    "content": "import esbuild from \"esbuild\";\nimport process from \"process\";\nimport builtins from 'builtin-modules'\n\nconst banner =\n`/*\nTHIS IS A GENERATED/BUNDLED FILE BY ESBUILD\nif you want to view the source, please visit the github repository of this plugin\n*/\n`;\n\nconst prod = (process.argv[2] === 'production');\n\nesbuild.build({\n\tbanner: {\n\t\tjs: banner,\n\t},\n\tentryPoints: ['main.ts'],\n\tbundle: true,\n\texternal: [\n\t\t'obsidian',\n\t\t'electron',\n\t\t'@codemirror/autocomplete',\n\t\t'@codemirror/closebrackets',\n\t\t'@codemirror/collab',\n\t\t'@codemirror/commands',\n\t\t'@codemirror/comment',\n\t\t'@codemirror/fold',\n\t\t'@codemirror/gutter',\n\t\t'@codemirror/highlight',\n\t\t'@codemirror/history',\n\t\t'@codemirror/language',\n\t\t'@codemirror/lint',\n\t\t'@codemirror/matchbrackets',\n\t\t'@codemirror/panel',\n\t\t'@codemirror/rangeset',\n\t\t'@codemirror/rectangular-selection',\n\t\t'@codemirror/search',\n\t\t'@codemirror/state',\n\t\t'@codemirror/stream-parser',\n\t\t'@codemirror/text',\n\t\t'@codemirror/tooltip',\n\t\t'@codemirror/view',\n\t\t...builtins],\n\tformat: 'cjs',\n\twatch: !prod,\n\ttarget: 'es2016',\n\tlogLevel: \"info\",\n\tsourcemap: prod ? false : 'inline',\n\ttreeShaking: true,\n\toutfile: 'main.js',\n}).catch(() => process.exit(1));\n"
  },
  {
    "path": "main.ts",
    "content": "import { Editor, MarkdownView, Plugin, Modal, App } from 'obsidian';\nimport ReactPlayer from 'react-player/lazy'\n\nimport { VideoView, VIDEO_VIEW } from './view/VideoView';\nimport { TimestampPluginSettings, TimestampPluginSettingTab, DEFAULT_SETTINGS } from 'settings';\n\n\nconst ERRORS: { [key: string]: string } = {\n\t\"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\",\n\t\"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\",\n}\n\nexport default class TimestampPlugin extends Plugin {\n\tsettings: TimestampPluginSettings;\n\tplayer: ReactPlayer;\n\tsetPlaying: React.Dispatch<React.SetStateAction<boolean>>;\n\teditor: Editor;\n\n\tasync onload() {\n\t\t// Register view\n\t\tthis.registerView(\n\t\t\tVIDEO_VIEW,\n\t\t\t(leaf) => new VideoView(leaf)\n\t\t);\n\n\t\t// Register settings\n\t\tawait this.loadSettings();\n\n\t\t// Markdown processor that turns timestamps into buttons\n\t\tthis.registerMarkdownCodeBlockProcessor(\"timestamp\", (source, el, ctx) => {\n\t\t\t// Match mm:ss or hh:mm:ss timestamp format\n\t\t\tconst regExp = /\\d+:\\d+:\\d+|\\d+:\\d+/g;\n\t\t\tconst rows = source.split(\"\\n\").filter((row) => row.length > 0);\n\t\t\trows.forEach((row) => {\n\t\t\t\tconst match = row.match(regExp);\n\t\t\t\tif (match) {\n\t\t\t\t\t//create button for each timestamp\n\t\t\t\t\tconst div = el.createEl(\"div\");\n\t\t\t\t\tconst button = div.createEl(\"button\");\n\t\t\t\t\tbutton.innerText = match[0];\n\t\t\t\t\tbutton.style.backgroundColor = this.settings.timestampColor;\n\t\t\t\t\tbutton.style.color = this.settings.timestampTextColor;\n\n\t\t\t\t\t// convert timestamp to seconds and seek to that position when clicked\n\t\t\t\t\tbutton.addEventListener(\"click\", () => {\n\t\t\t\t\t\tconst timeArr = match[0].split(\":\").map((v) => parseInt(v));\n\t\t\t\t\t\tconst [hh, mm, ss] = timeArr.length === 2 ? [0, ...timeArr] : timeArr;\n\t\t\t\t\t\tconst seconds = (hh || 0) * 3600 + (mm || 0) * 60 + (ss || 0);\n\t\t\t\t\t\tif (this.player) this.player.seekTo(seconds);\n\t\t\t\t\t});\n\t\t\t\t\tdiv.appendChild(button);\n\t\t\t\t}\n\t\t\t})\n\t\t});\n\n\n\t\t// Markdown processor that turns video urls into buttons to open views of the video\n\t\tthis.registerMarkdownCodeBlockProcessor(\"timestamp-url\", (source, el, ctx) => {\n\t\t\tconst url = source.trim();\n\t\t\tif (ReactPlayer.canPlay(url)) {\n\t\t\t\t// Creates button for video url\n\t\t\t\tconst div = el.createEl(\"div\");\n\t\t\t\tconst button = div.createEl(\"button\");\n\t\t\t\tbutton.innerText = url;\n\t\t\t\tbutton.style.backgroundColor = this.settings.urlColor;\n\t\t\t\tbutton.style.color = this.settings.urlTextColor;\n\n\t\t\t\tbutton.addEventListener(\"click\", () => {\n\t\t\t\t\tthis.activateView(url, this.editor);\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tif (this.editor) {\n\t\t\t\t\tthis.editor.replaceSelection(this.editor.getSelection() + \"\\n\" + ERRORS[\"INVALID_URL\"]);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\t// Command that gets selected video link and sends it to view which passes it to React component\n\t\tthis.addCommand({\n\t\t\tid: 'trigger-player',\n\t\t\tname: 'Open video player (copy video url and use hotkey)',\n\t\t\teditorCallback: (editor: Editor, view: MarkdownView) => {\n\t\t\t\t// Get selected text and match against video url to convert link to video video id\n\t\t\t\tconst url = editor.getSelection().trim();\n\n\t\t\t\t// Activate the view with the valid link\n\t\t\t\tif (ReactPlayer.canPlay(url)) {\n\t\t\t\t\tthis.activateView(url, editor);\n\t\t\t\t\tthis.settings.noteTitle ?\n\t\t\t\t\t\teditor.replaceSelection(\"\\n\" + this.settings.noteTitle + \"\\n\" + \"```timestamp-url \\n \" + url + \"\\n ```\\n\") :\n\t\t\t\t\t\teditor.replaceSelection(\"```timestamp-url \\n \" + url + \"\\n ```\\n\")\n\t\t\t\t\tthis.editor = editor;\n\t\t\t\t} else {\n\t\t\t\t\teditor.replaceSelection(ERRORS[\"INVALID_URL\"])\n\t\t\t\t}\n\t\t\t\teditor.setCursor(editor.getCursor().line + 1)\n\t\t\t}\n\t\t});\n\n\t\t// This command inserts the timestamp of the playing video into the editor\n\t\tthis.addCommand({\n\t\t\tid: 'timestamp-insert',\n\t\t\tname: 'Insert timestamp based on videos current play time',\n\t\t\teditorCallback: (editor: Editor, view: MarkdownView) => {\n\t\t\t\tif (!this.player) {\n\t\t\t\t\teditor.replaceSelection(ERRORS[\"NO_ACTIVE_VIDEO\"])\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// convert current video time into timestamp\n\t\t\t\tconst leadingZero = (num: number) => num < 10 ? \"0\" + num.toFixed(0) : num.toFixed(0);\n\t\t\t\tconst totalSeconds = Number(this.player.getCurrentTime().toFixed(2));\n\t\t\t\tconst hours = Math.floor(totalSeconds / 3600);\n\t\t\t\tconst minutes = Math.floor((totalSeconds - (hours * 3600)) / 60);\n\t\t\t\tconst seconds = totalSeconds - (hours * 3600) - (minutes * 60);\n\t\t\t\tconst time = (hours > 0 ? leadingZero(hours) + \":\" : \"\") + leadingZero(minutes) + \":\" + leadingZero(seconds);\n\n\t\t\t\t// insert timestamp into editor\n\t\t\t\teditor.replaceSelection(\"```timestamp \\n \" + time + \"\\n ```\\n\")\n\t\t\t}\n\t\t});\n\n\t\t//Command that play/pauses the video\n\t\tthis.addCommand({\n\t\t\tid: 'pause-player',\n\t\t\tname: 'Pause player',\n\t\t\teditorCallback: (editor: Editor, view: MarkdownView) => {\n\t\t\t\tthis.setPlaying(!this.player.props.playing)\n\t\t\t}\n\t\t});\n\n\t\t// Seek forward by set amount of seconds\n\t\tthis.addCommand({\n\t\t\tid: 'seek-forward',\n\t\t\tname: 'Seek Forward',\n\t\t\teditorCallback: (editor: Editor, view: MarkdownView) => {\n\t\t\t\tif (this.player) this.player.seekTo(this.player.getCurrentTime() + parseInt(this.settings.forwardSeek));\n\t\t\t}\n\t\t});\n\n\t\t// Seek backwards by set amount of seconds\n\t\tthis.addCommand({\n\t\t\tid: 'seek-backward',\n\t\t\tname: 'Seek Backward',\n\t\t\teditorCallback: (editor: Editor, view: MarkdownView) => {\n\t\t\t\tif (this.player) this.player.seekTo(this.player.getCurrentTime() - parseInt(this.settings.backwardsSeek));\n\t\t\t}\n\t\t});\n\n\t\t// This adds a complex command that can check whether the current state of the app allows execution of the command\n\t\tthis.addCommand({\n\t\t\tid: 'open-sample-modal-complex',\n\t\t\tname: 'Open sample modal (complex)',\n\t\t\teditorCallback: (editor: Editor, view: MarkdownView) => {\n\t\t\t\tthis.editor = editor;\n\t\t\t\tnew SampleModal(this.app, this.activateView.bind(this), editor).open();\n\t\t\t\t// This command will only show up in Command Palette when the check function returns true\n\t\t\t\treturn true;\n\t\t\t}\n\t\t});\n\n\t\t// This adds a settings tab so the user can configure various aspects of the plugin\n\t\tthis.addSettingTab(new TimestampPluginSettingTab(this.app, this));\n\t}\n\n\tasync onunload() {\n\t\tthis.player = null;\n\t\tthis.editor = null;\n\t\tthis.setPlaying = null;\n\t\tthis.app.workspace.detachLeavesOfType(VIDEO_VIEW);\n\t}\n\n\t// This is called when a valid url is found => it activates the View which loads the React view\n\tasync activateView(url: string, editor: Editor) {\n\t\tthis.app.workspace.detachLeavesOfType(VIDEO_VIEW);\n\n\t\tawait this.app.workspace.getRightLeaf(false).setViewState({\n\t\t\ttype: VIDEO_VIEW,\n\t\t\tactive: true,\n\t\t});\n\n\t\tthis.app.workspace.revealLeaf(\n\t\t\tthis.app.workspace.getLeavesOfType(VIDEO_VIEW)[0]\n\t\t);\n\n\t\t// This triggers the React component to be loaded\n\t\tthis.app.workspace.getLeavesOfType(VIDEO_VIEW).forEach(async (leaf) => {\n\t\t\tif (leaf.view instanceof VideoView) {\n\n\t\t\t\tconst setupPlayer = (player: ReactPlayer, setPlaying: React.Dispatch<React.SetStateAction<boolean>>) => {\n\t\t\t\t\tthis.player = player;\n\t\t\t\t\tthis.setPlaying = setPlaying;\n\t\t\t\t}\n\n\t\t\t\tconst setupError = (err: string) => {\n\t\t\t\t\teditor.replaceSelection(editor.getSelection() + `\\n> [!error] Streaming Error \\n> ${err}\\n`);\n\t\t\t\t}\n\n\t\t\t\tconst saveTimeOnUnload = async () => {\n\t\t\t\t\tif (this.player) {\n\t\t\t\t\t\tthis.settings.urlStartTimeMap.set(url, Number(this.player.getCurrentTime().toFixed(0)));\n\t\t\t\t\t}\n\t\t\t\t\tawait this.saveSettings();\n\t\t\t\t}\n\n\t\t\t\t// create a new video instance, sets up state/unload functionality, and passes in a start time if available else 0\n\t\t\t\tleaf.setEphemeralState({\n\t\t\t\t\turl,\n\t\t\t\t\tsetupPlayer,\n\t\t\t\t\tsetupError,\n\t\t\t\t\tsaveTimeOnUnload,\n\t\t\t\t\tstart: ~~this.settings.urlStartTimeMap.get(url)\n\t\t\t\t});\n\n\t\t\t\tawait this.saveSettings();\n\t\t\t}\n\t\t});\n\t}\n\n\tasync loadSettings() {\n\t\t// Fix for a weird bug that turns default map into a normal object when loaded\n\t\tconst data = await this.loadData()\n\t\tif (data) {\n\t\t\tconst map = new Map(Object.keys(data.urlStartTimeMap).map(k => [k, data.urlStartTimeMap[k]]))\n\t\t\tthis.settings = { ...DEFAULT_SETTINGS, ...data, urlStartTimeMap: map };\n\t\t} else {\n\t\t\tthis.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());\n\t\t}\n\t}\n\n\tasync saveSettings() {\n\t\tawait this.saveData(this.settings);\n\t}\n}\n\nclass SampleModal extends Modal {\n\teditor: Editor;\n\tactivateView: (url: string, editor: Editor) => void;\n\tconstructor(app: App, activateView: (url: string, editor: Editor) => void, editor: Editor) {\n\t\tsuper(app);\n\t\tthis.activateView = activateView;\n\t\tthis.editor = editor;\n\t}\n\n\tonOpen() {\n\t\tconst { contentEl } = this;\n\t\t// add an input field to contentEl\n\n\t\tconst input = contentEl.createEl('input');\n\t\tinput.setAttribute(\"type\", \"file\");\n\t\tinput.onchange = (e: any) => {\n\t\t\t// accept local video input and make a url from input\n\t\t\tconst url = URL.createObjectURL(e.target.files[0]);\n\t\t\tthis.activateView(url, this.editor);\n\n\t\t\t// Can't get the buttons to work with local videos unfortunately\n\t\t\t// this.editor.replaceSelection(\"\\n\" + \"```timestamp-url \\n \" + url.trim() + \"\\n ```\\n\")\n\t\t\tthis.close();\n\t\t}\n\t}\n\n\tonClose() {\n\t\tconst { contentEl } = this;\n\t\tcontentEl.empty();\n\t}\n}\n"
  },
  {
    "path": "manifest.json",
    "content": "{\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\"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.\",\n\t\"author\": \"Julian Grunauer\",\n\t\"authorUrl\": \"https://github.com/juliang22/ObsidianTimestampNotes\",\n\t\"isDesktopOnly\": true\n}"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"obsidian-timestamp-notes\",\n\t\"version\": \"1.0.8\",\n\t\"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.\",\n\t\"main\": \"main.js\",\n\t\"scripts\": {\n\t\t\"dev\": \"node esbuild.config.mjs\",\n\t\t\"build\": \"tsc -noEmit -skipLibCheck && node esbuild.config.mjs production\",\n\t\t\"version\": \"node version-bump.mjs && git add manifest.json versions.json\"\n\t},\n\t\"keywords\": [],\n\t\"author\": \"\",\n\t\"license\": \"MIT\",\n\t\"devDependencies\": {\n\t\t\"@types/node\": \"^16.11.6\",\n\t\t\"@types/react\": \"^18.0.9\",\n\t\t\"@types/react-dom\": \"^18.0.4\",\n\t\t\"@typescript-eslint/eslint-plugin\": \"^5.2.0\",\n\t\t\"@typescript-eslint/parser\": \"^5.2.0\",\n\t\t\"builtin-modules\": \"^3.2.0\",\n\t\t\"esbuild\": \"0.13.12\",\n\t\t\"obsidian\": \"latest\",\n\t\t\"tslib\": \"2.3.1\",\n\t\t\"typescript\": \"4.4.4\"\n\t},\n\t\"dependencies\": {\n\t\t\"react\": \"^18.1.0\",\n\t\t\"react-dom\": \"^18.1.0\",\n\t\t\"react-player\": \"^2.10.1\"\n\t}\n}"
  },
  {
    "path": "settings.ts",
    "content": "import { App, PluginSettingTab, Setting } from 'obsidian';\nimport TimestampPlugin from './main';\n\nexport interface TimestampPluginSettings {\n\tnoteTitle: string;\n\turlStartTimeMap: Map<string, number>;\n\turlColor: string;\n\ttimestampColor: string;\n\turlTextColor: string;\n\ttimestampTextColor: string;\n\tforwardSeek: string;\n\tbackwardsSeek: string;\n}\n\nexport const DEFAULT_SETTINGS: TimestampPluginSettings = {\n\tnoteTitle: \"\",\n\turlStartTimeMap: new Map<string, number>(),\n\turlColor: 'blue',\n\ttimestampColor: 'green',\n\turlTextColor: 'white',\n\ttimestampTextColor: 'white',\n\tforwardSeek: '10',\n\tbackwardsSeek: '10'\n}\n\nconst COLORS = { 'blue': 'blue', 'red': 'red', 'green': 'green', 'yellow': 'yellow', 'orange': 'orange', 'purple': 'purple', 'pink': 'pink', 'grey': 'grey', 'black': 'black', 'white': 'white' };\n\nconst 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' }\n\nexport class TimestampPluginSettingTab extends PluginSettingTab {\n\tplugin: TimestampPlugin;\n\n\tconstructor(app: App, plugin: TimestampPlugin) {\n\t\tsuper(app, plugin);\n\t\tthis.plugin = plugin;\n\t}\n\n\tdisplay(): void {\n\t\tconst { containerEl } = this;\n\n\t\tcontainerEl.empty();\n\n\t\tcontainerEl.createEl('h2', { text: 'Timestamp Notes Plugin' });\n\n\t\t// Customize title\n\t\tnew Setting(containerEl)\n\t\t\t.setName('Title')\n\t\t\t.setDesc('This title will be printed after opening a video with the hotkey. Use <br> for new lines.')\n\t\t\t.addText(text => text\n\t\t\t\t.setPlaceholder('Enter title template.')\n\t\t\t\t.setValue(this.plugin.settings.noteTitle)\n\t\t\t\t.onChange(async (value) => {\n\t\t\t\t\tthis.plugin.settings.noteTitle = value;\n\t\t\t\t\tawait this.plugin.saveSettings();\n\t\t\t\t}));\n\n\t\t// Customize  url button color\n\t\tnew Setting(containerEl)\n\t\t\t.setName('URL Button Color')\n\t\t\t.setDesc('Pick a color for the url button.')\n\t\t\t.addDropdown(dropdown => dropdown\n\t\t\t\t.addOptions(COLORS)\n\t\t\t\t.setValue(this.plugin.settings.urlColor)\n\t\t\t\t.onChange(async (value) => {\n\t\t\t\t\tthis.plugin.settings.urlColor = value;\n\t\t\t\t\tawait this.plugin.saveSettings();\n\t\t\t\t}\n\t\t\t\t));\n\n\t\t// Customize url text color\n\t\tnew Setting(containerEl)\n\t\t\t.setName('URL Text Color')\n\t\t\t.setDesc('Pick a color for the URL text button.')\n\t\t\t.addDropdown(dropdown => dropdown\n\t\t\t\t.addOptions(COLORS)\n\t\t\t\t.setValue(this.plugin.settings.urlTextColor)\n\t\t\t\t.onChange(async (value) => {\n\t\t\t\t\tthis.plugin.settings.urlTextColor = value;\n\t\t\t\t\tawait this.plugin.saveSettings();\n\t\t\t\t}\n\t\t\t\t));\n\n\t\t// Customize timestamp button color\n\t\tnew Setting(containerEl)\n\t\t\t.setName('Timestamp Button Color')\n\t\t\t.setDesc('Pick a color for the timestamp button.')\n\t\t\t.addDropdown(dropdown => dropdown\n\t\t\t\t.addOptions(COLORS)\n\t\t\t\t.setValue(this.plugin.settings.timestampColor)\n\t\t\t\t.onChange(async (value) => {\n\t\t\t\t\tthis.plugin.settings.timestampColor = value;\n\t\t\t\t\tawait this.plugin.saveSettings();\n\t\t\t\t}\n\t\t\t\t));\n\n\t\t// Customize timestamp text color\n\t\tnew Setting(containerEl)\n\t\t\t.setName('Timestamp Text Color')\n\t\t\t.setDesc('Pick a color for the timestamp text.')\n\t\t\t.addDropdown(dropdown => dropdown\n\t\t\t\t.addOptions(COLORS)\n\t\t\t\t.setValue(this.plugin.settings.timestampTextColor)\n\t\t\t\t.onChange(async (value) => {\n\t\t\t\t\tthis.plugin.settings.timestampTextColor = value;\n\t\t\t\t\tawait this.plugin.saveSettings();\n\t\t\t\t}\n\t\t\t\t));\n\n\t\t// Customize forward seek time\n\t\tnew Setting(containerEl)\n\t\t\t.setName('Foward time seek')\n\t\t\t.setDesc('This is the amount of seconds the video will seek forward when pressing the seek forward command.')\n\t\t\t.addDropdown(dropdown => dropdown\n\t\t\t\t.addOptions(TIMES)\n\t\t\t\t.setValue(this.plugin.settings.forwardSeek)\n\t\t\t\t.onChange(async (value) => {\n\t\t\t\t\tthis.plugin.settings.forwardSeek = value;\n\t\t\t\t\tawait this.plugin.saveSettings();\n\t\t\t\t}\n\t\t\t\t));\n\n\t\t// Customize backwards seek time\n\t\tnew Setting(containerEl)\n\t\t\t.setName('Backwards time seek')\n\t\t\t.setDesc('This is the amount of seconds the video will seek backwards when pressing the seek backwards command.')\n\t\t\t.addDropdown(dropdown => dropdown\n\t\t\t\t.addOptions(TIMES)\n\t\t\t\t.setValue(this.plugin.settings.backwardsSeek)\n\t\t\t\t.onChange(async (value) => {\n\t\t\t\t\tthis.plugin.settings.backwardsSeek = value;\n\t\t\t\t\tawait this.plugin.saveSettings();\n\t\t\t\t}\n\t\t\t\t));\n\t}\n}"
  },
  {
    "path": "styles.css",
    "content": ""
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"inlineSourceMap\": true,\n    \"inlineSources\": true,\n    \"module\": \"ESNext\",\n    \"target\": \"ES6\",\n    \"allowJs\": true,\n    \"noImplicitAny\": true,\n    \"moduleResolution\": \"node\",\n    \"importHelpers\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react\",\n    \"lib\": [\n      \"DOM\",\n      \"ES5\",\n      \"ES6\",\n      \"ES7\"\n    ]\n  },\n  \"include\": [\n    \"**/*.ts\",\n    \"view/VideoView.tsx\"\n  ]\n}"
  },
  {
    "path": "version-bump.mjs",
    "content": "import { readFileSync, writeFileSync } from \"fs\";\n\nconst targetVersion = process.env.npm_package_version;\n\n// read minAppVersion from manifest.json and bump version to target version\nlet manifest = JSON.parse(readFileSync(\"manifest.json\", \"utf8\"));\nconst { minAppVersion } = manifest;\nmanifest.version = targetVersion;\nwriteFileSync(\"manifest.json\", JSON.stringify(manifest, null, \"\\t\"));\n\n// update versions.json with target version and minAppVersion from manifest.json\nlet versions = JSON.parse(readFileSync(\"versions.json\", \"utf8\"));\nversions[targetVersion] = minAppVersion;\nwriteFileSync(\"versions.json\", JSON.stringify(versions, null, \"\\t\"));\n"
  },
  {
    "path": "versions.json",
    "content": "{\n\t\"1.0.0\": \"0.9.7\",\n\t\"1.0.1\": \"0.12.0\"\n}\n"
  },
  {
    "path": "view/VideoContainer.tsx",
    "content": "import * as React from \"react\";\nimport { useRef, useState } from 'react';\n\nimport ReactPlayer from 'react-player/lazy'\n\n\nexport interface VideoContainerProps {\n\turl: string;\n\tstart: number\n\tsetupPlayer: (player: ReactPlayer, setPlaying: React.Dispatch<React.SetStateAction<boolean>>) => void;\n\tsetupError: (err: string) => void;\n}\n\nexport const VideoContainer = ({ url, setupPlayer, start, setupError }: VideoContainerProps): JSX.Element => {\n\t// Reference to player passed back to the setupPlayer prop\n\tconst playerRef = useRef<ReactPlayer>();\n\n\tconst [playing, setPlaying] = useState(true)\n\n\tconst onReady = () => {\n\t\t// Starts player at last played time if the video has been played before\n\t\tif (start) playerRef.current.seekTo(start);\n\n\t\t// Sets up video player to be accessed in main.ts\n\t\tif (playerRef) setupPlayer(playerRef.current, setPlaying);\n\t}\n\n\treturn (\n\t\t<>\n\t\t\t<ReactPlayer\n\t\t\t\tref={playerRef}\n\t\t\t\turl={url}\n\t\t\t\tplaying={playing}\n\t\t\t\tcontrols={true}\n\t\t\t\twidth='100%'\n\t\t\t\theight='95%'\n\t\t\t\tonReady={onReady}\n\t\t\t\tonError={(err) => setupError(err ?\n\t\t\t\t\terr.message :\n\t\t\t\t\t`Video is unplayable due to privacy settings, streaming permissions, etc.`)} // Error handling for invalid URLs\n\t\t\t/>\n\t\t</>\n\t)\n};\n"
  },
  {
    "path": "view/VideoView.tsx",
    "content": "import { ItemView, WorkspaceLeaf } from 'obsidian';\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { createRoot, Root } from 'react-dom/client';\n\nimport { VideoContainer, VideoContainerProps } from \"./VideoContainer\"\n\nexport interface VideoViewProps extends VideoContainerProps {\n\tsaveTimeOnUnload: () => void;\n}\n\nexport const VIDEO_VIEW = \"video-view\";\nexport class VideoView extends ItemView {\n\tcomponent: ReactDOM.Renderer\n\tsaveTimeOnUnload: () => void\n\troot: Root\n\tconstructor(leaf: WorkspaceLeaf) {\n\t\tsuper(leaf);\n\t\tthis.saveTimeOnUnload = () => { };\n\t\tthis.root = createRoot(this.containerEl.children[1])\n\t}\n\n\tgetViewType() {\n\t\treturn VIDEO_VIEW;\n\t}\n\n\tgetDisplayText() {\n\t\treturn \"Timestamp Video\";\n\t}\n\n\tgetIcon(): string {\n\t\treturn \"video\";\n\t}\n\n\tsetEphemeralState({ url, setupPlayer, setupError, saveTimeOnUnload, start }: VideoViewProps) {\n\n\t\t// Allows view to save the playback time in the setting state when the view is closed \n\t\tthis.saveTimeOnUnload = saveTimeOnUnload;\n\n\t\t// Create a root element for the view to render into\n\t\tthis.root.render(\n\t\t\t<VideoContainer\n\t\t\t\turl={url}\n\t\t\t\tstart={start}\n\t\t\t\tsetupPlayer={setupPlayer}\n\t\t\t\tsetupError={setupError}\n\t\t\t/>\n\t\t);\n\t}\n\n\tasync onClose() {\n\t\tif (this.saveTimeOnUnload) await this.saveTimeOnUnload();\n\t\tthis.root.unmount()\n\t\tReactDOM.unmountComponentAtNode(this.containerEl.children[1]);\n\t}\n}\n"
  }
]