├── .all-contributorsrc ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bun.lockb ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── index.ts │ │ └── style.css ├── add-gate.md ├── assets │ ├── img.png │ ├── img_1.png │ └── img_2.png ├── custom-css.md ├── custom-javascript.md ├── gate-link.md ├── gate-options.md ├── getting-started.md ├── index.md ├── inline-embedded.md ├── introduction.md ├── public │ ├── logo-small.webp │ └── logo.webp ├── quick-switch.md └── release.md ├── esbuild.config.mjs ├── manifest.json ├── package.json ├── src ├── GateOptions.d.ts ├── GateView.ts ├── MCPlugin.ts ├── ModalEditGate.ts ├── ModalInsertLink.ts ├── ModalListGates.ts ├── ModalOnboarding.ts ├── SetingTab.ts ├── fns │ ├── createEmptyGateOption.ts │ ├── createFormEditGate.ts │ ├── createIframe.ts │ ├── createWebviewTag.ts │ ├── fetchTitle.ts │ ├── getDefaultUserAgent.ts │ ├── getSvgIcon.ts │ ├── normalizeGateOption.ts │ ├── openView.ts │ ├── registerCodeBlockProcessor.ts │ ├── registerGate.ts │ ├── setupInsertLinkMenu.ts │ ├── setupLinkConvertMenu.ts │ └── unloadView.ts ├── main.ts └── types.d.ts ├── stuff ├── img.png └── img_3.png ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "andrewmcgivery", 12 | "name": "andrewmcgivery", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/4482878?v=4", 14 | "profile": "https://github.com/andrewmcgivery", 15 | "contributions": [ 16 | "code" 17 | ] 18 | }, 19 | { 20 | "login": "miztizm", 21 | "name": "Digital Alchemist", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/617020?v=4", 23 | "profile": "https://github.com/miztizm", 24 | "contributions": [ 25 | "code" 26 | ] 27 | }, 28 | { 29 | "login": "LiamSwayne", 30 | "name": "Liam Swayne", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/108629034?v=4", 32 | "profile": "https://github.com/LiamSwayne", 33 | "contributions": [ 34 | "code" 35 | ] 36 | } 37 | ], 38 | "contributorsPerLine": 7, 39 | "skipCi": true, 40 | "repoType": "github", 41 | "repoHost": "https://github.com", 42 | "projectName": "obsidian-open-gate", 43 | "projectOwner": "nguyenvanduocit" 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | env: 8 | PLUGIN_NAME: obsidian-open-gate 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Use bun 15 | uses: oven-sh/setup-bun@v1 16 | - name: Build 17 | id: build 18 | run: | 19 | bun install 20 | bun run build 21 | mkdir ${{ env.PLUGIN_NAME }} 22 | cp main.js manifest.json ${{ env.PLUGIN_NAME }} 23 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 24 | ls 25 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 26 | - name: Create Release 27 | id: create_release 28 | uses: actions/create-release@v1 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | VERSION: ${{ github.ref }} 32 | with: 33 | tag_name: ${{ github.ref }} 34 | release_name: ${{ github.ref }} 35 | draft: false 36 | prerelease: false 37 | - name: Upload zip file 38 | id: upload-zip 39 | uses: actions/upload-release-asset@v1 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | upload_url: ${{ steps.create_release.outputs.upload_url }} 44 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 45 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 46 | asset_content_type: application/zip 47 | - name: Upload main.js 48 | id: upload-main 49 | uses: actions/upload-release-asset@v1 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | upload_url: ${{ steps.create_release.outputs.upload_url }} 54 | asset_path: ./main.js 55 | asset_name: main.js 56 | asset_content_type: text/javascript 57 | - name: Upload manifest.json 58 | id: upload-manifest 59 | uses: actions/upload-release-asset@v1 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | with: 63 | upload_url: ${{ steps.create_release.outputs.upload_url }} 64 | asset_path: ./manifest.json 65 | asset_name: manifest.json 66 | asset_content_type: application/json 67 | 68 | - name: Upload css 69 | id: upload-css 70 | uses: actions/upload-release-asset@v1 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | with: 74 | upload_url: ${{ steps.create_release.outputs.upload_url }} 75 | asset_path: ./styles.css 76 | asset_name: styles.css 77 | asset_content_type: text/css 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | 24 | docs/.vitepress/cache/ 25 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "printWidth": 180, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Functional programming 2 | 3 | Many times, I found myself trying to reuse some of the functions that I wrote in other projects. I found that the best way to do this is to write pure functions. Pure functions are functions that do not have any side effects and always return the same output for the same input. This makes them very easy to test and reuse. 4 | 5 | A lot of function in this project are copied from another project. 6 | 7 | Even thought JavaScript is not a functional programming language, I tried to write as much pure functions as possible. So, please keep this in mind when you are contributing to this project. 8 | 9 | Thanks for your contribution! 10 | 11 | ## Git commit message 12 | 13 | I prefer to use the AngularJS Git Commit Message Conventions, but it's optional. Nowerdays, we use AI to generate the changelog, message, so it's not that important anymore. But do not get me wrong, I still think that it's a good practice to use it. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nguyen Van Duoc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./stuff/img.png) 2 | 3 | # CUSTOMER SUPPORT 4 | 5 | For the fastest and most reliable support, please use our customer support portal. This way, I can easily track your issue and make sure nothing gets overlooked. You can still reach me directly on Discord if needed. 6 | 7 | - [Discord](https://discord.gg/rxCdQ2K8M5) 8 | - [Support Portal](https://aiocean.atlassian.net/servicedesk/customer/portal/4) 9 | - [Tutorial](https://open-gate.aiocean.io/) 10 | 11 | # What is Open Gate? 12 | 13 | Obsidian Open Gate is a plugin for Obsidian. Allows you to embed any website into Obsidian, providing a seamless browsing and note-taking experience. Whether you're researching, studying, or just browsing the web, Obsidian Open Gate keeps everything you need in one place. 14 | 15 | ## Installation 16 | 17 | Click here to install the plugin: [Direct Install](https://obsidian.md/plugins?id=open-gate) 18 | 19 | ## Features 20 | 21 | - Embed any website in your Obsidian UI as a "Gate" 22 | - Open a Gate on the left, center, or right of the Obsidian UI 23 | - Embed a Gate directly within a note 24 | - Auto generate icon based on the site's favicon 25 | - Embed any site that can not be embedded by iframe 26 | - Support for mobile 27 | - Inject custom CSS to match the look and feel of Obsidian 28 | - Link to Gates from within your notes 29 | 30 | ## Tutorial 31 | 32 | We prepared a very detailed tutorial for you, don't forget to check it out: [Tutorial](https://open-gate.aiocean.io/) 33 | 34 | ## Contributors ✨ 35 | 36 | Thanks goes to these wonderful people. 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
andrewmcgivery
andrewmcgivery

💻
Digital Alchemist
Digital Alchemist

💻
Liam Swayne
Liam Swayne

💻
50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/068f703988c911b845b7756a8543c48747978694/bun.lockb -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | export default defineConfig({ 4 | title: 'OpenGate', 5 | description: 'Embed any website into Obsidian - The ultimate plugin for seamless web integration in your notes', 6 | lastUpdated: true, 7 | cleanUrls: true, 8 | metaChunk: true, 9 | locales: { 10 | root: { 11 | label: 'English', 12 | lang: 'en' 13 | }, 14 | fr: { 15 | label: 'Vietnamese', 16 | lang: 'vi', 17 | link: '/vi' 18 | } 19 | }, 20 | head: [ 21 | // Basic meta tags 22 | ['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }], 23 | ['meta', { name: 'robots', content: 'index, follow' }], 24 | ['meta', { name: 'keywords', content: 'obsidian, plugin, web embed, productivity, note-taking, open gate, iframe' }], 25 | ['meta', { name: 'author', content: 'Duoc NV' }], 26 | 27 | // Open Graph meta tags 28 | ['meta', { property: 'og:type', content: 'website' }], 29 | ['meta', { property: 'og:locale', content: 'en' }], 30 | ['meta', { property: 'og:title', content: 'OpenGate - Embed any website into Obsidian' }], 31 | [ 32 | 'meta', 33 | { 34 | property: 'og:description', 35 | content: 'The ultimate Obsidian plugin for seamless web integration in your notes. Embed any website, customize appearance, and boost your productivity.' 36 | } 37 | ], 38 | ['meta', { property: 'og:site_name', content: 'OpenGate' }], 39 | ['meta', { property: 'og:image', content: '/logo.webp' }], 40 | 41 | // Twitter Card meta tags 42 | ['meta', { name: 'twitter:card', content: 'summary_large_image' }], 43 | ['meta', { name: 'twitter:site', content: '@duocdev' }], 44 | ['meta', { name: 'twitter:creator', content: '@duocdev' }], 45 | ['meta', { name: 'twitter:title', content: 'OpenGate - Embed any website into Obsidian' }], 46 | [ 47 | 'meta', 48 | { 49 | name: 'twitter:description', 50 | content: 'The ultimate Obsidian plugin for seamless web integration in your notes. Embed any website, customize appearance, and boost your productivity.' 51 | } 52 | ], 53 | ['meta', { name: 'twitter:image', content: '/logo.webp' }], 54 | 55 | // Favicon 56 | ['link', { rel: 'icon', type: 'image/webp', href: '/logo-small.webp' }] 57 | ], 58 | themeConfig: { 59 | logo: { src: '/logo-small.webp', width: 24, height: 24 }, 60 | nav: [ 61 | { text: 'Install', link: 'https://obsidian.md/plugins?id=open-gate' }, 62 | { text: 'Tutorial', link: '/introduction' }, 63 | { text: 'Community', link: 'https://community.aiocean.io/' } 64 | ], 65 | editLink: { 66 | pattern: 'https://github.com/nguyenvanduocit/obsidian-open-gate/edit/main/docs/:path' 67 | }, 68 | search: { 69 | provider: 'local' 70 | }, 71 | footer: { 72 | message: 'Released under the MIT License.', 73 | copyright: 'Copyright © 2019-present Duoc NV' 74 | }, 75 | sidebar: [ 76 | { 77 | text: 'Introduction', 78 | collapsed: false, 79 | items: [ 80 | { text: 'What is Open Gate?', link: '/introduction' }, 81 | { text: 'Getting Started', link: '/getting-started' } 82 | ] 83 | }, 84 | { 85 | text: 'Tutorial', 86 | collapsed: false, 87 | items: [ 88 | { text: 'Add Gate', link: '/add-gate' }, 89 | { text: 'Quick Switch', link: '/quick-switch' }, 90 | { text: 'Inline Embedded', link: '/inline-embedded' }, 91 | { text: 'Gate Link', link: '/gate-link' }, 92 | { text: 'Custom CSS', link: '/custom-css' }, 93 | { text: 'Custom JavaScript', link: '/custom-javascript' } 94 | ] 95 | }, 96 | { 97 | text: 'Preferences', 98 | collapsed: false, 99 | items: [{ text: 'Gate Options', link: '/gate-options' }] 100 | } 101 | ], 102 | 103 | socialLinks: [{ icon: 'github', link: 'https://github.com/nguyenvanduocit/obsidian-open-gate' }] 104 | } 105 | }) 106 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | // @ts-ignore 3 | if (!import.meta.env.SSR) { 4 | import('quicklink').then((module) => { 5 | module.listen() 6 | }) 7 | } 8 | import './style.css' 9 | 10 | export default DefaultTheme 11 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | .aspect-ratio-16-9 { 2 | position: relative; 3 | padding-bottom: 56.25%; 4 | } 5 | 6 | .aspect-ratio-16-9 iframe { 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /docs/add-gate.md: -------------------------------------------------------------------------------- 1 | # Add Gate 2 | 3 | There are two ways to add a new gate to your Obsidian vault: using the command palette or setting screen. This guide will walk you through both methods. 4 | 5 | ## Command Palette 6 | 7 | To add a new gate to your Obsidian vault, follow these simple steps: 8 | 9 | 1. Open the command palette (`Ctrl+P` on Windows/Linux, `Cmd+P` on macOS). 10 | 2. Type `Create new gate` and press `Enter`. 11 | 3. Enter the desired URL and title for the website you wish to embed. 12 | 4. Your new gate will now appear in the left sidebar of Obsidian. Click on its icon to view the embedded website within Obsidian. 13 | 14 |
15 | 16 |
17 | 18 | ## Setting Screen 19 | 20 | To add a new gate using the setting screen, follow these simple steps: 21 | 22 | 1. Open the settings 23 | 2. Navigate to the `Open Gate` tab. 24 | 3. Click on the `Add Gate` button. 25 | 4. Enter the desired URL and title for the website you wish to embed. 26 | 5. Your new gate will now appear in the left sidebar of Obsidian. Click on its icon to view the embedded website within Obsidian. 27 | 28 |
29 | 30 |
31 | 32 | 33 | ## Gate Options 34 | 35 | Please refer to the [Gate Options](gate-options.md) documentation for more information on customizing your gates. 36 | 37 | ## Custom icon 38 | 39 | The app will automatically generate an icon for your gate based on the site's favicon. However, you can also set a custom icon for your gate. You can fill svg code in the icon field to set a custom icon. Remember to remove the `width` and `height` attributes from the svg code. 40 | -------------------------------------------------------------------------------- /docs/assets/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/068f703988c911b845b7756a8543c48747978694/docs/assets/img.png -------------------------------------------------------------------------------- /docs/assets/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/068f703988c911b845b7756a8543c48747978694/docs/assets/img_1.png -------------------------------------------------------------------------------- /docs/assets/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/068f703988c911b845b7756a8543c48747978694/docs/assets/img_2.png -------------------------------------------------------------------------------- /docs/custom-css.md: -------------------------------------------------------------------------------- 1 | # Custom CSS 2 | 3 | Every website has its own style, and you can customize the appearance of embedded websites in Open Gate by adding custom CSS. This can help match the embedded content with your Obsidian theme or make it more readable. 4 | 5 | ## How to Add Custom CSS 6 | 7 | You can add custom CSS in two ways: 8 | 9 | 1. Through the Gate Settings: 10 | - Open Gate Settings 11 | - Edit a gate 12 | - Enable Advanced Options 13 | - Add your CSS in the CSS field 14 | 15 | 2. In Markdown Code Blocks: 16 | 17 | If you use [inline embedded](inline-embedded.md), you can add custom CSS in the code block like this: 18 | 19 | ~~~ 20 | ```gate 21 | url: https://example.com 22 | css: | 23 | html { font-size: 20px; } 24 | ``` 25 | ~~~ 26 | 27 | ## Useful Snippets 28 | 29 | ### Dark Mode 30 | Convert light websites to dark mode: 31 | ```css 32 | html { 33 | filter: invert(90%) hue-rotate(180deg)!important; 34 | } 35 | ``` 36 | 37 | ### Readability Improvements 38 | Make text more readable: 39 | ```css 40 | html { 41 | font-size: 20px; 42 | } 43 | 44 | body { 45 | line-height: 1.6; 46 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 47 | } 48 | ``` 49 | 50 | ### Hide Elements 51 | Remove unwanted elements: 52 | ```css 53 | .advertisement, 54 | .cookie-banner, 55 | .popup-overlay { 56 | display: none !important; 57 | } 58 | ``` 59 | 60 | ### Custom Scrollbar 61 | Style the scrollbar: 62 | ```css 63 | ::-webkit-scrollbar { 64 | width: 8px; 65 | } 66 | 67 | ::-webkit-scrollbar-track { 68 | background: var(--background-primary); 69 | } 70 | 71 | ::-webkit-scrollbar-thumb { 72 | background: var(--text-muted); 73 | border-radius: 4px; 74 | } 75 | ``` 76 | 77 | ## Tips 78 | - Use `!important` when your styles aren't being applied due to specificity conflicts 79 | - Test CSS changes incrementally to avoid breaking the layout 80 | - Use browser dev tools to inspect elements and find the right selectors 81 | - Consider using Obsidian CSS variables for better theme compatibility 82 | 83 | ## Resources 84 | - [MDN CSS Reference](https://developer.mozilla.org/en-US/docs/Web/CSS) 85 | - [CSS Tricks](https://css-tricks.com/) 86 | - Share your snippets in our [GitHub Discussions](https://github.com/nguyenvanduocit/obsidian-open-gate/discussions/categories/snippets) -------------------------------------------------------------------------------- /docs/custom-javascript.md: -------------------------------------------------------------------------------- 1 | # Custom JavaScript 2 | 3 | The same with custom CSS, not much to say here, if you want to custom JS, it's mean you know what you are doing. 4 | 5 | But be careful, custom JS is very powerful, it can allow you to change the behavior of the website, or even leak your data. So only do it if you know what you are doing. 6 | 7 | In some use cases, I use custom JS to remove the annoying cookie banner, ads, popup, .... 8 | 9 | The best case is allows another plugin to interact with the embedded website. 10 | -------------------------------------------------------------------------------- /docs/gate-link.md: -------------------------------------------------------------------------------- 1 | # Gate Link 2 | 3 | This feature allows you to create a link that opens a URL within the gate view of Obsidian, rather than navigating away in an external browser 4 | 5 | ## Usage 6 | 7 | ```md 8 | [Open Google Gate](obsidian://opengate?title=google&url=https%3A%2F%2Fdocs.google.com%2Fdocument%2Fd%2Fabc123%2Fedit) 9 | ``` 10 | 11 | Please notice that the URL must be encoded. You can use [this tool](https://www.urlencoder.org/) to encode your URL. 12 | 13 | You may want to read [Gate Options](gate-options.md) to learn more about the options you can use. Of course, the are no space for custom css or javascript in the gate view. 14 | 15 | ## Editor context menu 16 | 17 | We know that encoding URLs can be a hassle, so we've added a context menu option to make it easier. Just right-click on the link, and select `Insert Gate Link`. 18 | 19 | A popup will appear, asking for the title and URL of the gate. The URL will be automatically encoded for you. 20 | 21 | ![img.png](assets/img_2.png) 22 | 23 | ## Convert to gate link 24 | 25 | If you right-click on a normal link, you will see an option to convert it to a gate link. The URL will be automatically encoded for you. 26 | 27 | ## Reuse gates 28 | 29 | Once again, to simplify the reuse of gates configured in the settings, the plugin automatically matches the title or URL with existing gates. If a match is found, the options from the gate configured in the settings are merged with the options specified in the note. 30 | -------------------------------------------------------------------------------- /docs/gate-options.md: -------------------------------------------------------------------------------- 1 | # Gate Options 2 | 3 | If you can read code, here is what we have under the hood. The `GateFrameOption` type is used to define the options for a gate frame. 4 | 5 | <<< @/../src/GateOptions.d.ts 6 | 7 | ## User Agent 8 | 9 | Usually, you won't need to change the user agent. However, in some case, the website you are trying to embed may require a specific user agent to work correctly. For example, some websites may block requests from bots or crawlers. In this case, you can set the user agent to a common browser user agent to bypass this restriction. 10 | 11 | Currently, default value is: 12 | 13 | <<< @/../src/fns/getDefaultUserAgent.ts {2} 14 | 15 | ## profileKey 16 | 17 | This property is intriguing as it allows you to embed multiple emails in your vault using the `profileKey` for differentiation. The `profileKey` acts as a namespace, enabling gates with the same `profileKey` to share storage space. This facilitates using a single sign-on (SSO) to log into a website and maintaining the same session across all gates sharing that `profileKey`. 18 | 19 | In other words, `profileKey` is like profile on Chrome. 20 | 21 | ## zoomFactor 22 | 23 | The `zoomFactor` in the `GateFrameOption` determines the magnification level of the content within a "Gate" frame in Obsidian. A value of 1 means 100% zoom (normal size), 0.5 means the content is reduced to 50% of its normal size, and a value of 2 means the content is enlarged to 200% of its normal size. 24 | 25 | ## Css & Js Injection 26 | 27 | `css` and `js` allows you to customize the appearance and functionality of embedded websites. 28 | 29 | ### Example 30 | 31 | - Use CSS to modify styles for a consistent look with Obsidian. 32 | - Example: `html { font-family: 'Arial', sans-serif; }` changes the font. 33 | - Use JS to add interactivity or modify web content. 34 | - Example: `document.body.style.backgroundColor = "lightblue";` changes the background color. 35 | 36 | ### Warning 37 | 38 | - Incorrect CSS/JS may break the appearance/functionality of the gate. 39 | - Be cautious with JS that interacts with external websites to avoid security risks. 40 | 41 | You may want to read [Gate Options](gate-options.md) to learn more about the options you can use. Of course, the are no space for custom css or javascript in the gate view. 42 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Getting Started 6 | 7 | To begin using the Obsidian Open Gate plugin, follow these simple steps to install it: 8 | 9 | 1. Direct Install: Navigate to the [Direct Install Link](https://obsidian.md/plugins?id=open-gate). This will take you to the plugin's page in the Obsidian app. Follow the instructions to install. 10 | 2. Once installed, access the plugin settings in Obsidian to configure it according to your preferences. 11 | 3. To start embedding websites, open the command palette (`Ctrl+P` on Windows/Linux, `Cmd+P` on macOS) and type `Create new gate`. Enter the desired URL and title for the website you wish to embed. 12 | 4. Your new gate will now appear in the left sidebar of Obsidian. Click on its icon to view the embedded website within Obsidian. 13 | 14 | You may notice that there are several options available for customizing your gates. We will deep dive into these features in the next section. For now, feel free to explore the plugin and experiment with embedding different websites to enhance your note-taking experience. 15 | 16 | ![img_1.png](assets/img_1.png) 17 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: "Open Gate" 6 | text: "Embed any website in to Obsidian" 7 | tagline: "Anything you need, right where you need it" 8 | actions: 9 | - theme: brand 10 | text: Direct Install 11 | link: https://obsidian.md/plugins?id=open-gate 12 | - theme: alt 13 | text: Tutorial 14 | link: /introduction 15 | 16 | image: 17 | src: /logo.webp 18 | alt: VitePress 19 | 20 | features: 21 | - title: Embed Any Website 22 | icon: 🖼️ 23 | details: Create "Gates" that display websites directly in Obsidian's interface 24 | - title: Flexible Options 25 | icon: 📄 26 | details: Open websites in dedicated views or embed them inline within your notes 27 | - title: Profile Management 28 | icon: 🔗 29 | details: Utilize profile keys to share storage between different gates, similar to Chrome profiles 30 | - title: Customizable Experience 31 | icon: 🎨 32 | details: Inject custom CSS and JavaScript to tailor the appearance and functionality of embedded websites 33 | --- 34 | 35 | 78 | 79 | ## Contributers 80 | 81 | Thanks to all the people who have contributed! 82 | 83 | 84 | 85 | 86 | 87 | 88 | 112 | -------------------------------------------------------------------------------- /docs/inline-embedded.md: -------------------------------------------------------------------------------- 1 | # Inline Embedded 2 | 3 | You can embed a view directly in a note: 4 | 5 | ~~~md 6 | ```gate 7 | url: https://12bit.vn 8 | height: 300 9 | zoomFactor: 1 10 | css: | 11 | html { filter: invert(90%) hue-rotate(180deg)!important; } 12 | ``` 13 | ~~~ 14 | 15 | Here is the result: 16 | 17 | ![img.png](assets/img.png) 18 | 19 | You can use any property from the [Gate Options](gate-options.md) documentation. 20 | 21 | ## Reuse gates 22 | 23 | To simplify the reuse of gates configured in the settings, the plugin automatically matches the title or URL with existing gates. If a match is found, the options from the gate configured in the settings are merged with the options specified in the note. 24 | 25 | For instance, if there is a gate titled `12bit` configured in the settings, you can easily reuse it by only specifying the `title` and any additional option like `height` in your note as shown below: 26 | 27 | ~~~md 28 | ```gate 29 | title: 12bit 30 | height: 300 31 | ``` 32 | ~~~ 33 | 34 | This will open the `12bit` gate with a height of `300px`. 35 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # What is Open Gate? 2 | 3 | **Obsidian Open Gate** seamlessly integrates web browsing into your Obsidian note-taking experience. Embed any website as a "Gate" within the Obsidian interface or directly within notes. Customize the layout, inject custom CSS, and access Gates quickly via hotkeys or a command palette. Whether you're researching, studying, or just browsing, Obsidian Open Gate keeps everything you need in one place, enhancing your productivity and streamlining your workflow. 4 | 5 | ## How does it work? 6 | 7 | Under the hood, Obsidian Open Gate uses webview (mobile) and iframe (desktop) technologies to display websites within the app. 8 | 9 | ## Why use Open Gate? 10 | 11 | - **Improved productivity:** Reduces context switching, allowing you to focus on your tasks. 12 | - **Interactive notes:** Embeds websites directly into notes, making them more engaging and interactive. 13 | - **Real-time updates:** Provides live updates from embedded websites. 14 | - **Enhanced aesthetics:** Brings life to websites by incorporating images, audio, and other elements. 15 | 16 | ## Usage cases 17 | 18 | Obsidian Open Gate transforms Obsidian into a more versatile workspace by embedding various web applications directly within. Here are 10 use cases: 19 | 20 | 1. **Calendar Management**: Embed your Google Calendar or other web-based calendars to stay organized and manage appointments directly in Obsidian. 21 | 2. **Collaborative Workspace**: Integrate Notion or other collaborative tools to work on shared projects and documents with colleagues or friends. 22 | 3. **Real-Time Document Editing**: Embed Google Docs or Microsoft Word Online to seamlessly edit documents and collaborate with others in real time. 23 | 4. **Interactive Learning**: Insert YouTube videos or other interactive web content to enhance your notes or learning materials. 24 | 5. **Project Management**: Embed Trello or Asana boards to manage tasks, track progress, and collaborate on projects. 25 | 6. **Financial Tracking**: Integrate Google Sheets or Excel Online to monitor financial data, create budgets, and track expenses. 26 | 7. **Interactive Whiteboarding**: Embed Miro or other online whiteboards for brainstorming, mind mapping, and collaborative idea generation. 27 | 8. **Content Curation**: Insert Pinterest or Pocket to collect and organize your favorite articles, images, or other online content. 28 | 9. **Research and Analysis**: Embed Google Scholar or JSTOR to access academic databases and research materials. 29 | 10. **Social Media Integration**: Connect with Twitter or LinkedIn to stay updated on industry trends or engage with professional networks. 30 | 31 | ## Why I created Open Gate? 32 | 33 | I created Open Gate because other plugins I tried were either too complicated or lacked features I needed. My goal was to simplify the user experience and include only essential functionalities. I use Open Gate daily and continuously update it to ensure it meets user needs effectively. 34 | -------------------------------------------------------------------------------- /docs/public/logo-small.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/068f703988c911b845b7756a8543c48747978694/docs/public/logo-small.webp -------------------------------------------------------------------------------- /docs/public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/068f703988c911b845b7756a8543c48747978694/docs/public/logo.webp -------------------------------------------------------------------------------- /docs/quick-switch.md: -------------------------------------------------------------------------------- 1 | # Quick switch 2 | 3 | You dont want every gates to show up in the left sidebar. 4 | 5 | You will turn off the "Ribbon" options of the gates. Then how to access them quickly? We have two ways to do that. 6 | 7 | ## Command Palette 8 | 9 | 1. Open the command palette (`Ctrl+P` on Windows/Linux, `Cmd+P` on macOS). 10 | 2. Type `Open gate` and you will see the list of gates. 11 | 3. Select the gate you want to open. 12 | 4. Press `Enter`. 13 | 14 | ## Hotkey 15 | 16 | By default, the hotkey to open the last gate is `Ctrl+Shift+G` on Windows/Linux, `Cmd+Shift+G` on macOS (You can change it in the settings.), a popup will show up with the list of gates. Click on the gate you want to open. 17 | 18 | 19 |
20 | 21 |
22 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | # Hướng dẫn Release Phiên Bản Mới 2 | 3 | Tài liệu này hướng dẫn chi tiết quy trình release một phiên bản mới của plugin Open Gate. Vui lòng tuân thủ từng bước để đảm bảo process release diễn ra suôn sẻ. 4 | 5 | ## Các bước release 6 | 7 | ### 1. Cập nhật code và kiểm tra trạng thái 8 | 9 | ```bash 10 | # Fetch tất cả các thay đổi mới nhất từ repository 11 | git fetch --all 12 | 13 | # Kiểm tra trạng thái hiện tại của repository 14 | git status 15 | ``` 16 | 17 | Đảm bảo working tree của bạn sạch sẽ (clean) trước khi bắt đầu quy trình release. 18 | 19 | ### 2. Kiểm tra phiên bản hiện tại 20 | 21 | ```bash 22 | # Xem các tag gần đây để xác định phiên bản hiện tại 23 | git tag -l --sort=-v:refname | head -5 24 | ``` 25 | 26 | ### 3. Xem các commit gần đây 27 | 28 | ```bash 29 | # Xem 5 commit gần nhất để hiểu những thay đổi sẽ được bao gồm trong phiên bản mới 30 | git log -5 --pretty=format:"%h - %s (%cr)" | cat 31 | ``` 32 | 33 | ### 4. Cập nhật phiên bản trong các file 34 | 35 | Cần cập nhật số phiên bản trong 3 file: 36 | 37 | 1. `manifest.json` 38 | 2. `package.json` 39 | 3. `versions.json` 40 | 41 | Ví dụ, để cập nhật từ phiên bản 1.11.8 lên 1.11.9: 42 | 43 | #### Cập nhật manifest.json 44 | ```json 45 | { 46 | "id": "open-gate", 47 | "name": "Open Gate", 48 | "version": "1.11.9", // Cập nhật phiên bản tại đây 49 | "minAppVersion": "0.15.0", 50 | // ... 51 | } 52 | ``` 53 | 54 | #### Cập nhật package.json 55 | ```json 56 | { 57 | "name": "obsidian-open-gate", 58 | "version": "1.11.9", // Cập nhật phiên bản tại đây 59 | // ... 60 | } 61 | ``` 62 | 63 | #### Cập nhật versions.json 64 | ```json 65 | { 66 | // ... các phiên bản trước ... 67 | "1.11.8": "0.15.0", 68 | "1.11.9": "0.15.0" // Thêm phiên bản mới và minAppVersion 69 | } 70 | ``` 71 | 72 | ### 5. Commit các thay đổi 73 | 74 | ```bash 75 | # Add các file đã thay đổi 76 | git add manifest.json package.json versions.json 77 | 78 | # Commit với thông điệp mô tả rõ ràng 79 | git commit -m "chore: release version 1.11.9" 80 | ``` 81 | 82 | ### 6. Tạo tag cho phiên bản mới 83 | 84 | ```bash 85 | # Tạo annotated tag với message 86 | git tag -a "1.11.9" -m "Release version 1.11.9" 87 | 88 | # Kiểm tra lại tag vừa tạo 89 | git tag -l --sort=-v:refname | head -3 90 | ``` 91 | 92 | ### 7. Push commit và tag lên repository 93 | 94 | ```bash 95 | # Push commit lên nhánh chính (thường là main) 96 | git push 97 | 98 | # Push tag lên repository 99 | git push --tags 100 | ``` 101 | 102 | ## Lưu ý 103 | 104 | - Luôn đảm bảo bạn đã test kỹ tất cả các tính năng trước khi thực hiện release 105 | - Tuân thủ quy tắc semantic versioning (SemVer): 106 | - MAJOR: thay đổi không tương thích với API cũ 107 | - MINOR: thêm tính năng mới nhưng vẫn tương thích ngược 108 | - PATCH: sửa lỗi, cải thiện hiệu suất mà không thay đổi API 109 | - Nếu có nhiều thay đổi lớn, cân nhắc cập nhật changelog trong README hoặc tạo file CHANGELOG.md 110 | 111 | ## Quy trình sau khi release 112 | 113 | Sau khi release thành công, bạn nên: 114 | 115 | 1. Kiểm tra repository trên GitHub để xác nhận tag mới đã xuất hiện 116 | 2. Xác minh rằng phiên bản mới được hiển thị trong Obsidian Community Plugins 117 | 3. Nếu có bất kỳ vấn đề nào, phản hồi nhanh chóng và cân nhắc việc release hotfix nếu cần thiết -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | import process from 'process' 3 | import builtins from 'builtin-modules' 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | ` 10 | 11 | const prod = process.argv[2] === 'production' 12 | 13 | esbuild 14 | .build({ 15 | banner: { 16 | js: banner 17 | }, 18 | entryPoints: ['src/main.ts'], 19 | bundle: true, 20 | external: [ 21 | 'obsidian', 22 | 'electron', 23 | '@codemirror/autocomplete', 24 | '@codemirror/collab', 25 | '@codemirror/commands', 26 | '@codemirror/language', 27 | '@codemirror/lint', 28 | '@codemirror/search', 29 | '@codemirror/state', 30 | '@codemirror/view', 31 | '@lezer/common', 32 | '@lezer/highlight', 33 | '@lezer/lr', 34 | ...builtins 35 | ], 36 | format: 'cjs', 37 | watch: !prod, 38 | target: 'es2018', 39 | logLevel: 'info', 40 | sourcemap: prod ? false : 'inline', 41 | treeShaking: true, 42 | outfile: 'main.js' 43 | }) 44 | .catch(() => process.exit(1)) 45 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "open-gate", 3 | "name": "Open Gate", 4 | "version": "1.11.10", 5 | "minAppVersion": "0.15.0", 6 | "description": "Embed any website to Obsidian, you have anything you need in one place. You can browse website and take notes at the same time. e.g. Ask ChatGPT and copy the answer directly to your note.", 7 | "author": "duocnv", 8 | "authorUrl": "https://twitter.com/duocdev", 9 | "fundingUrl": { 10 | "Buy Me a Coffee": "https://paypal.me/duocnguyen", 11 | "Follow me": "https://twitter.com/duocdev" 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-open-gate", 3 | "version": "1.11.10", 4 | "description": "Embedding any website to Obsidian, from now all, you have anything you need in one place.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json", 10 | "format": "prettier --write .", 11 | "docs:dev": "vitepress dev docs", 12 | "docs:build": "vitepress build docs", 13 | "docs:preview": "vitepress preview docs" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@codemirror/view": "^6.35.0", 20 | "@types/chrome": "^0.0.203", 21 | "@types/node": "^16.18.121", 22 | "@typescript-eslint/eslint-plugin": "5.29.0", 23 | "@typescript-eslint/parser": "5.29.0", 24 | "builtin-modules": "3.3.0", 25 | "electron": "^23.3.13", 26 | "esbuild": "0.14.47", 27 | "obsidian": "latest", 28 | "prettier": "^2.8.8", 29 | "quicklink": "^2.3.0", 30 | "tslib": "2.4.0", 31 | "typescript": "4.7.4", 32 | "vitepress": "^1.5.0" 33 | }, 34 | "dependencies": { 35 | "yaml": "^2.6.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/GateOptions.d.ts: -------------------------------------------------------------------------------- 1 | export type GateFrameOptionType = 'left' | 'center' | 'right' 2 | 3 | export type GateFrameOption = { 4 | id: string 5 | icon: string // SVG code or icon id from lucide.dev 6 | title: string // Gate frame title 7 | url: string // Gate frame URL 8 | profileKey?: string // Similar to a Chrome profile 9 | hasRibbon?: boolean // If true, icon appears in the left sidebar 10 | position?: GateFrameOptionType // 'left', 'center', or 'right' 11 | userAgent?: string // User agent for the gate frame 12 | zoomFactor?: number // Zoom factor (0.5 = 50%, 1.0 = 100%, 2.0 = 200%, etc.) 13 | css?: string // Custom CSS for the gate frame 14 | js?: string // Custom JavaScript for the gate frame 15 | } 16 | -------------------------------------------------------------------------------- /src/GateView.ts: -------------------------------------------------------------------------------- 1 | import { ItemView, WorkspaceLeaf, Menu } from 'obsidian' 2 | import { createWebviewTag } from './fns/createWebviewTag' 3 | import { Platform } from 'obsidian' 4 | import { createIframe } from './fns/createIframe' 5 | import { clipboard } from 'electron' 6 | import WebviewTag = Electron.WebviewTag 7 | import { GateFrameOption } from './GateOptions' 8 | import { callbackify } from 'util' 9 | 10 | export class GateView extends ItemView { 11 | private readonly options: GateFrameOption 12 | private frame: WebviewTag | HTMLIFrameElement 13 | private readonly useIframe: boolean = false 14 | private frameReadyCallbacks: Function[] 15 | private isFrameReady: boolean = false 16 | private frameDoc: Document 17 | 18 | constructor(leaf: WorkspaceLeaf, options: GateFrameOption) { 19 | super(leaf) 20 | this.navigation = false 21 | this.options = options 22 | this.useIframe = Platform.isMobileApp 23 | this.frameReadyCallbacks = [] 24 | } 25 | 26 | addActions(): void { 27 | this.addAction('refresh-ccw', 'Reload', () => { 28 | if (this.frame instanceof HTMLIFrameElement) { 29 | this.frame.contentWindow?.location.reload() 30 | } else { 31 | this.frame.reload() 32 | } 33 | }) 34 | 35 | this.addAction('home', 'Home page', () => { 36 | if (this.frame instanceof HTMLIFrameElement) { 37 | this.frame.src = this.options?.url ?? 'about:blank' 38 | } else { 39 | this.frame.loadURL(this.options?.url ?? 'about:blank') 40 | } 41 | }) 42 | } 43 | 44 | isWebviewFrame(): boolean { 45 | return this.frame! instanceof HTMLIFrameElement 46 | } 47 | 48 | onload(): void { 49 | super.onload() 50 | this.addActions() 51 | 52 | this.contentEl.empty() 53 | this.contentEl.addClass('open-gate-view') 54 | 55 | this.frameDoc = this.contentEl.doc 56 | this.createFrame() 57 | } 58 | 59 | private createFrame(): void { 60 | const onReady = () => { 61 | if(!this.isFrameReady){ 62 | this.isFrameReady = true 63 | this.frameReadyCallbacks.forEach((callback) => callback()) 64 | } 65 | } 66 | 67 | if(this.useIframe){ 68 | this.frame = createIframe(this.options, onReady) 69 | }else{ 70 | this.frame = createWebviewTag(this.options, onReady, this.frameDoc) 71 | 72 | this.frame.addEventListener('destroyed', () => { 73 | 74 | if(this.frameDoc != this.contentEl.doc){ 75 | if(this.frame){ 76 | this.frame.remove() 77 | } 78 | this.frameDoc = this.contentEl.doc 79 | this.createFrame() 80 | } 81 | }) 82 | } 83 | 84 | this.contentEl.appendChild(this.frame as unknown as HTMLElement) 85 | } 86 | 87 | 88 | 89 | onunload(): void { 90 | 91 | if(this.frame){ 92 | this.frame.remove() 93 | } 94 | super.onunload() 95 | 96 | } 97 | 98 | onPaneMenu(menu: Menu, source: string): void { 99 | super.onPaneMenu(menu, source) 100 | menu.addItem((item) => { 101 | item.setTitle('Reload') 102 | item.setIcon('refresh-ccw') 103 | item.onClick(() => { 104 | if (this.frame instanceof HTMLIFrameElement) { 105 | this.frame.contentWindow?.location.reload() 106 | } else { 107 | this.frame.reload() 108 | } 109 | }) 110 | }) 111 | menu.addItem((item) => { 112 | item.setTitle('Home page') 113 | item.setIcon('home') 114 | item.onClick(async () => { 115 | if (this.frame instanceof HTMLIFrameElement) { 116 | this.frame.src = this.options?.url ?? 'about:blank' 117 | } else { 118 | await this.frame.loadURL(this.options?.url ?? 'about:blank') 119 | } 120 | }) 121 | }) 122 | menu.addItem((item) => { 123 | item.setTitle('Toggle DevTools') 124 | item.setIcon('file-cog') 125 | item.onClick(() => { 126 | if (this.frame instanceof HTMLIFrameElement) { 127 | return 128 | } 129 | 130 | if (this.frame.isDevToolsOpened()) { 131 | this.frame.closeDevTools() 132 | } else { 133 | this.frame.openDevTools() 134 | } 135 | }) 136 | }) 137 | 138 | menu.addItem((item) => { 139 | item.setTitle('Copy Page URL') 140 | item.setIcon('clipboard-copy') 141 | item.onClick(() => { 142 | if (this.frame instanceof HTMLIFrameElement) { 143 | clipboard.writeText(this.frame.src) 144 | return 145 | } 146 | 147 | clipboard.writeText(this.frame.getURL()) 148 | }) 149 | }) 150 | 151 | // Open in Default Browser 152 | menu.addItem((item) => { 153 | item.setTitle('Open in browser') 154 | item.setIcon('globe') 155 | item.onClick(() => { 156 | if (this.frame instanceof HTMLIFrameElement) { 157 | window.open(this.frame.src) 158 | return 159 | } 160 | 161 | window.open(this.frame.getURL()) 162 | }) 163 | }) 164 | } 165 | 166 | getViewType(): string { 167 | return this.options?.id ?? 'gate' 168 | } 169 | 170 | getDisplayText(): string { 171 | return this.options?.title ?? 'Gate' 172 | } 173 | 174 | getIcon(): string { 175 | if (this.options?.icon.startsWith(' void 8 | constructor(app: App, gateOptions: GateFrameOption, onSubmit: (result: GateFrameOption) => void) { 9 | super(app) 10 | this.onSubmit = onSubmit 11 | this.gateOptions = gateOptions 12 | } 13 | 14 | onOpen() { 15 | const { contentEl } = this 16 | contentEl.createEl('h3', { text: 'Open Gate' }) 17 | createFormEditGate(contentEl, this.gateOptions, (result) => { 18 | this.onSubmit(result) 19 | this.close() 20 | }) 21 | } 22 | 23 | onClose() { 24 | const { contentEl } = this 25 | contentEl.empty() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ModalInsertLink.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, Setting } from 'obsidian' 2 | import { createEmptyGateOption } from './fns/createEmptyGateOption' 3 | import { normalizeGateOption } from './fns/normalizeGateOption' 4 | import { GateFrameOption } from './GateOptions' 5 | 6 | export class ModalInsertLink extends Modal { 7 | onSubmit: (result: GateFrameOption) => void 8 | constructor(app: App, onSubmit: (result: GateFrameOption) => void) { 9 | super(app) 10 | this.onSubmit = onSubmit 11 | } 12 | 13 | onOpen() { 14 | this.titleEl.setText('Insert Link') 15 | this.createFormInsertLink() 16 | } 17 | 18 | onClose() { 19 | const { contentEl } = this 20 | contentEl.empty() 21 | } 22 | 23 | createFormInsertLink() { 24 | let gateOptions = createEmptyGateOption() 25 | new Setting(this.contentEl) 26 | .setName('URL') 27 | .setClass('open-gate--form-field') 28 | .addText((text) => 29 | text.setPlaceholder('https://example.com').onChange(async (value) => { 30 | gateOptions.url = value 31 | }) 32 | ) 33 | 34 | new Setting(this.contentEl) 35 | .setName('Title') 36 | .setClass('open-gate--form-field') 37 | .addText((text) => 38 | text.onChange(async (value) => { 39 | gateOptions.title = value 40 | }) 41 | ) 42 | 43 | new Setting(this.contentEl).addButton((btn) => 44 | btn 45 | .setButtonText('Insert Link') 46 | .setCta() 47 | .onClick(async () => { 48 | gateOptions = normalizeGateOption(gateOptions) 49 | this.onSubmit(gateOptions) 50 | }) 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ModalListGates.ts: -------------------------------------------------------------------------------- 1 | import { App, getIcon, Modal } from 'obsidian' 2 | import { createFormEditGate } from './fns/createFormEditGate' 3 | import { openView } from './fns/openView' 4 | import { GateFrameOption } from './GateOptions' 5 | 6 | export class ModalListGates extends Modal { 7 | gates: Record 8 | onSubmit: (result: GateFrameOption) => void 9 | constructor(app: App, gates: Record, onSubmit: (result: GateFrameOption) => void) { 10 | super(app) 11 | this.onSubmit = onSubmit 12 | this.gates = gates 13 | } 14 | 15 | onOpen() { 16 | const { contentEl } = this 17 | 18 | for (const gateId in this.gates) { 19 | const gate = this.gates[gateId] 20 | // create svg icon 21 | const container = contentEl.createEl('div', { 22 | cls: 'open-gate--quick-list-item' 23 | }) 24 | 25 | if (!gate.icon.startsWith(' { 37 | await openView(this.app.workspace, gate.id, gate.position) 38 | this.close() 39 | }) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ModalOnboarding.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from 'obsidian' 2 | import { createFormEditGate } from './fns/createFormEditGate' 3 | import { GateFrameOption } from './GateOptions' 4 | 5 | export class ModalOnBoarding extends Modal { 6 | gateOptions: GateFrameOption 7 | onSubmit: (result: GateFrameOption) => void 8 | constructor(app: App, gateOptions: GateFrameOption, onSubmit: (result: GateFrameOption) => void) { 9 | super(app) 10 | this.onSubmit = onSubmit 11 | this.gateOptions = gateOptions 12 | } 13 | 14 | onOpen() { 15 | const { contentEl } = this 16 | contentEl.createEl('h3', { text: 'Welcome to OpenGate' }) 17 | contentEl.createEl('p', { 18 | text: 'OpenGate is a plugin that allows you to embed any website in Obsidian. You will never have to leave Obsidian again!' 19 | }) 20 | 21 | contentEl.createEl('p', { 22 | text: 'If you need help, please join our community.' 23 | }) 24 | 25 | contentEl.createEl('a', { 26 | cls: 'community-link', 27 | text: 'Community', 28 | attr: { href: 'https://community.aiocean.io/' } 29 | }) 30 | 31 | contentEl.createEl('p', { 32 | text: 'But now you have to create your first gate.' 33 | }) 34 | 35 | createFormEditGate(contentEl, this.gateOptions, (result) => { 36 | this.onSubmit(result) 37 | this.close() 38 | }) 39 | } 40 | 41 | onClose() { 42 | const { contentEl } = this 43 | contentEl.empty() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/SetingTab.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting, Platform } from 'obsidian' 2 | import OpenGatePlugin from './main' 3 | import { ModalEditGate } from './ModalEditGate' 4 | import { createEmptyGateOption } from './fns/createEmptyGateOption' 5 | import { GateFrameOption } from './GateOptions' 6 | 7 | export class SettingTab extends PluginSettingTab { 8 | plugin: OpenGatePlugin 9 | shouldNotify: boolean 10 | 11 | constructor(app: App, plugin: OpenGatePlugin) { 12 | super(app, plugin) 13 | this.plugin = plugin 14 | } 15 | 16 | async updateGate(gate: GateFrameOption) { 17 | await this.plugin.addGate(gate) 18 | this.display() 19 | } 20 | 21 | display(): void { 22 | this.shouldNotify = false 23 | const { containerEl } = this 24 | containerEl.empty() 25 | 26 | if (Platform.isMobileApp) { 27 | containerEl 28 | .createEl('div', { 29 | text: 'On mobile, some websites may not work. it is a limitation of Obsidian Mobile. Please use Obsidian Desktop instead. Follow me on Twitter to get the latest updates about the issue: ', 30 | cls: 'open-gate-mobile-warning' 31 | }) 32 | .createEl('a', { 33 | text: '@duocdev', 34 | cls: 'open-gate-mobile-link', 35 | href: 'https://twitter.com/duocdev' 36 | }) 37 | } 38 | 39 | containerEl.createEl('button', { text: 'New gate', cls: 'mod-cta' }).addEventListener('click', () => { 40 | new ModalEditGate(this.app, createEmptyGateOption(), this.updateGate.bind(this)).open() 41 | }) 42 | 43 | containerEl.createEl('hr') 44 | 45 | const settingContainerEl = containerEl.createDiv('setting-container') 46 | 47 | for (const gateId in this.plugin.settings.gates) { 48 | const gate = this.plugin.settings.gates[gateId] 49 | const gateEl = settingContainerEl.createEl('div', { 50 | attr: { 51 | 'data-gate-id': gate.id, 52 | class: 'open-gate--setting--gate' 53 | } 54 | }) 55 | 56 | new Setting(gateEl) 57 | .setName(gate.title) 58 | .setDesc(gate.url) 59 | .addButton((button) => { 60 | button.setButtonText('Delete').onClick(async () => { 61 | await this.plugin.removeGate(gateId) 62 | gateEl.remove() 63 | }) 64 | }) 65 | .addButton((button) => { 66 | button.setButtonText('Edit').onClick(() => { 67 | new ModalEditGate(this.app, gate, this.updateGate.bind(this)).open() 68 | }) 69 | }) 70 | } 71 | 72 | containerEl.createEl('h3', { text: 'Help' }) 73 | 74 | containerEl.createEl('small', { 75 | attr: { 76 | style: 'display: block; margin-bottom: 5px' 77 | }, 78 | text: 'When you delete or edit a gate, you need to reload Obsidian to see the changes.' 79 | }) 80 | 81 | containerEl.createEl('small', { 82 | attr: { 83 | style: 'display: block; margin-bottom: 1em;' 84 | }, 85 | text: `To reload Obsidian, you can use the menu "view -> Force reload" or "Reload App" in the command palette.` 86 | }) 87 | 88 | new Setting(containerEl) 89 | .setName('Follow me on Twitter') 90 | .setDesc('@duocdev') 91 | .addButton((button) => { 92 | button.setCta() 93 | button.setButtonText('Join Community').onClick(() => { 94 | window.open('https://community.aiocean.io/') 95 | }) 96 | }) 97 | .addButton((button) => { 98 | button.setCta() 99 | button.setButtonText('Follow for update').onClick(() => { 100 | window.open('https://twitter.com/duocdev') 101 | }) 102 | }) 103 | .addButton((button) => { 104 | button.buttonEl.outerHTML = 105 | "" 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/fns/createEmptyGateOption.ts: -------------------------------------------------------------------------------- 1 | import getDefaultUserAgent from './getDefaultUserAgent' 2 | import { GateFrameOption } from '../GateOptions' 3 | 4 | export const createEmptyGateOption = (): GateFrameOption => { 5 | return { 6 | id: '', 7 | title: '', 8 | icon: '', 9 | hasRibbon: true, 10 | position: 'right', 11 | profileKey: 'open-gate', 12 | url: '', 13 | zoomFactor: 1.0, 14 | userAgent: getDefaultUserAgent() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/fns/createFormEditGate.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from 'obsidian' 2 | import { normalizeGateOption } from './normalizeGateOption' 3 | import { GateFrameOption, GateFrameOptionType } from '../GateOptions' 4 | 5 | export const createFormEditGate = (contentEl: HTMLElement, gateOptions: GateFrameOption, onSubmit?: (result: GateFrameOption) => void) => { 6 | new Setting(contentEl) 7 | .setName('URL') 8 | .setClass('open-gate--form-field') 9 | .addText((text) => 10 | text 11 | .setPlaceholder('https://example.com') 12 | .setValue(gateOptions.url) 13 | .onChange(async (value) => { 14 | gateOptions.url = value 15 | }) 16 | ) 17 | 18 | new Setting(contentEl) 19 | .setName('Name') 20 | .setClass('open-gate--form-field') 21 | .addText((text) => 22 | text.setValue(gateOptions.title).onChange(async (value) => { 23 | gateOptions.title = value 24 | }) 25 | ) 26 | 27 | new Setting(contentEl) 28 | .setName('Pin to menu') 29 | .setClass('open-gate--form-field') 30 | .setDesc('If enabled, the gate will be pinned to the left bar') 31 | .addToggle((text) => 32 | text.setValue(gateOptions.hasRibbon === true).onChange(async (value) => { 33 | gateOptions.hasRibbon = value 34 | }) 35 | ) 36 | 37 | new Setting(contentEl) 38 | .setName('Position') 39 | .setClass('open-gate--form-field') 40 | .setDesc('What banner do you want to show?') 41 | .addDropdown((text) => 42 | text 43 | .addOption('left', 'Left') 44 | .addOption('right', 'Right') 45 | .addOption('center', 'Center') 46 | .setValue(gateOptions.position ?? 'right') 47 | .onChange(async (value) => { 48 | gateOptions.position = value as GateFrameOptionType 49 | }) 50 | ) 51 | 52 | new Setting(contentEl) 53 | .setName('Advanced Options') 54 | .setClass('open-gate--form-field') 55 | .addToggle((text) => 56 | text.setValue(false).onChange(async (value) => { 57 | if (value) { 58 | advancedOptions.addClass('open-gate--advanced-options--show') 59 | } else { 60 | advancedOptions.removeClass('open-gate--advanced-options--show') 61 | } 62 | }) 63 | ) 64 | 65 | const advancedOptions = contentEl.createDiv({ 66 | cls: 'open-gate--advanced-options' 67 | }) 68 | 69 | new Setting(advancedOptions) 70 | .setName('Icon') 71 | .setClass('open-gate--form-field--column') 72 | .setDesc('Leave it blank to enable auto-detect') 73 | .addTextArea((text) => 74 | text.setValue(gateOptions.icon).onChange(async (value) => { 75 | gateOptions.icon = value 76 | }) 77 | ) 78 | 79 | new Setting(advancedOptions) 80 | .setName('User Agent') 81 | .setClass('open-gate--form-field--column') 82 | .setDesc('Leave it blank if you are not sure') 83 | .addTextArea((text) => 84 | text.setValue(gateOptions.userAgent ?? '').onChange(async (value) => { 85 | gateOptions.userAgent = value 86 | }) 87 | ) 88 | 89 | new Setting(advancedOptions) 90 | .setName('Profile Key') 91 | .setClass('open-gate--form-field') 92 | .setDesc("It's like profiles in Chrome, gates with the same profile can share storage") 93 | .addText((text) => 94 | text.setValue(gateOptions.profileKey ?? '').onChange(async (value) => { 95 | if (value === '') { 96 | value = 'open-gate' 97 | } 98 | 99 | gateOptions.profileKey = value 100 | }) 101 | ) 102 | 103 | //zoomFactor 104 | new Setting(advancedOptions) 105 | .setName('Zoom Factor') 106 | .setClass('open-gate--form-field') 107 | .setDesc('Leave it blank if you are not sure') 108 | .addText((text) => 109 | text.setValue(gateOptions.zoomFactor?.toString() ?? '0.0').onChange(async (value) => { 110 | gateOptions.zoomFactor = parseFloat(value) 111 | }) 112 | ) 113 | 114 | const cssFieldDesc = new DocumentFragment() 115 | 116 | // Create a new element to hold the description and the link 117 | const descLink = document.createElement('a') 118 | descLink.href = 'https://github.com/nguyenvanduocit/obsidian-open-gate/discussions/categories/snippets' 119 | descLink.textContent = 'Check out the snippet library here' 120 | cssFieldDesc.appendChild(descLink) 121 | 122 | new Setting(advancedOptions) 123 | .setName('CSS') 124 | .setClass('open-gate--form-field--column') 125 | .setDesc(cssFieldDesc) 126 | .addTextArea((text) => 127 | text.setValue(gateOptions.css ?? '').onChange(async (value) => { 128 | gateOptions.css = value 129 | }) 130 | ) 131 | 132 | new Setting(advancedOptions) 133 | .setName('JavaScript') 134 | .setClass('open-gate--form-field--column') 135 | .setDesc('Leave it blank if you are not sure') 136 | .addTextArea((text) => 137 | text.setValue(gateOptions.js ?? '').onChange(async (value) => { 138 | gateOptions.js = value 139 | }) 140 | ) 141 | 142 | new Setting(contentEl).addButton((btn) => 143 | btn 144 | .setButtonText(gateOptions.id ? 'Update the gate' : 'Create new gate') 145 | .setCta() 146 | .onClick(async () => { 147 | gateOptions = normalizeGateOption(gateOptions) 148 | onSubmit && onSubmit(gateOptions) 149 | }) 150 | ) 151 | } 152 | -------------------------------------------------------------------------------- /src/fns/createIframe.ts: -------------------------------------------------------------------------------- 1 | import { GateFrameOption } from '../GateOptions' 2 | 3 | export const createIframe = (params: Partial, onReady?: () => void): HTMLIFrameElement => { 4 | const iframe = document.createElement('iframe') 5 | 6 | iframe.setAttribute('allowpopups', '') 7 | iframe.setAttribute('credentialless', 'true') 8 | iframe.setAttribute('crossorigin', 'anonymous') 9 | iframe.setAttribute('src', params.url ?? 'about:blank') 10 | iframe.setAttribute('sandbox', 'allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-top-navigation-by-user-activation') 11 | iframe.setAttribute('allow', 'encrypted-media; fullscreen; oversized-images; picture-in-picture; sync-xhr; geolocation') 12 | iframe.addClass('open-gate-iframe') 13 | 14 | iframe.addEventListener('load', () => { 15 | onReady?.call(null) 16 | 17 | if (params?.css) { 18 | const style = document.createElement('style') 19 | style.textContent = params.css 20 | iframe.contentDocument?.head.appendChild(style) 21 | } 22 | 23 | if (params?.js) { 24 | const script = document.createElement('script') 25 | script.textContent = params.js 26 | iframe.contentDocument?.head.appendChild(script) 27 | } 28 | }) 29 | 30 | return iframe 31 | } 32 | -------------------------------------------------------------------------------- /src/fns/createWebviewTag.ts: -------------------------------------------------------------------------------- 1 | import WebviewTag = Electron.WebviewTag 2 | import { GateFrameOption } from '../GateOptions' 3 | 4 | // Constants for repeated strings 5 | const DEFAULT_URL = 'about:blank' 6 | const GOOGLE_URL = 'https://google.com' 7 | const OPEN_GATE_WEBVIEW_CLASS = 'open-gate-webview' 8 | 9 | export const createWebviewTag = (params: Partial, onReady?: () => void, parentDoc?: Document): WebviewTag => { 10 | // Create a new webview tag using the parent document context 11 | const webviewTag = (parentDoc || document).createElement('webview') as unknown as WebviewTag 12 | 13 | // Set attributes for the webview tag 14 | webviewTag.setAttribute('partition', 'persist:' + params.profileKey) 15 | webviewTag.setAttribute('src', params.url ?? DEFAULT_URL) 16 | webviewTag.setAttribute('httpreferrer', params.url ?? GOOGLE_URL) 17 | webviewTag.setAttribute('crossorigin', 'anonymous') 18 | webviewTag.setAttribute('allowpopups', 'true') 19 | webviewTag.setAttribute('disablewebsecurity', 'true') 20 | webviewTag.addClass(OPEN_GATE_WEBVIEW_CLASS) 21 | 22 | // Set user agent if provided 23 | if (params.userAgent && params.userAgent !== '') { 24 | webviewTag.setAttribute('useragent', params.userAgent) 25 | } 26 | 27 | webviewTag.addEventListener('dom-ready', async () => { 28 | // Set zoom factor if provided 29 | if (params.zoomFactor) { 30 | webviewTag.setZoomFactor(params.zoomFactor) 31 | } 32 | 33 | if (params?.css) { 34 | await webviewTag.insertCSS(params.css) 35 | } 36 | 37 | if (params?.js) { 38 | await webviewTag.executeJavaScript(params.js) 39 | } 40 | 41 | onReady?.call(null) 42 | }) 43 | 44 | return webviewTag 45 | } 46 | -------------------------------------------------------------------------------- /src/fns/fetchTitle.ts: -------------------------------------------------------------------------------- 1 | export const fetchTitle = async (url: string): Promise => { 2 | let response = await fetch(url) 3 | let text = await response.text() 4 | let doc = new DOMParser().parseFromString(text, 'text/html') 5 | return doc.title 6 | } 7 | -------------------------------------------------------------------------------- /src/fns/getDefaultUserAgent.ts: -------------------------------------------------------------------------------- 1 | export default function getDefaultUserAgent() { 2 | return `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0/HBpt3US8-18` 3 | } 4 | -------------------------------------------------------------------------------- /src/fns/getSvgIcon.ts: -------------------------------------------------------------------------------- 1 | export const getSvgIcon = (siteUrl: string): string => { 2 | const domain = new URL(siteUrl).hostname 3 | return `` 4 | } -------------------------------------------------------------------------------- /src/fns/normalizeGateOption.ts: -------------------------------------------------------------------------------- 1 | import { getSvgIcon } from './getSvgIcon' 2 | import { GateFrameOption } from '../GateOptions' 3 | 4 | export const normalizeGateOption = (gate: Partial): GateFrameOption => { 5 | if (gate.url === '' || gate.url === undefined) { 6 | throw new Error('URL is required') 7 | } 8 | 9 | if (gate.id === '' || gate.id === undefined) { 10 | let seedString = gate.url! 11 | if (gate.profileKey != undefined && gate.profileKey !== 'open-gate' && gate.profileKey !== '') { 12 | seedString += gate.profileKey 13 | } 14 | gate.id = btoa(seedString) 15 | } 16 | 17 | if (gate.profileKey === '' || gate.profileKey === undefined) { 18 | gate.profileKey = 'open-gate' 19 | } 20 | 21 | if (gate.zoomFactor === 0 || gate.zoomFactor === undefined) { 22 | gate.zoomFactor = 1 23 | } 24 | 25 | if (gate.icon === '' || gate.icon === undefined) { 26 | gate.icon = gate.url?.startsWith('http') ? getSvgIcon(gate.url) : 'globe' 27 | } 28 | 29 | if (gate.title === '' || gate.title === undefined) { 30 | gate.title = gate.url 31 | } 32 | 33 | return gate 34 | } 35 | -------------------------------------------------------------------------------- /src/fns/openView.ts: -------------------------------------------------------------------------------- 1 | import { Workspace, WorkspaceLeaf } from 'obsidian' 2 | import { GateFrameOptionType } from '../GateOptions' 3 | 4 | export const openView = async (workspace: Workspace, id: string, position?: GateFrameOptionType): Promise => { 5 | let leafs = workspace.getLeavesOfType(id) 6 | if (leafs.length > 0) { 7 | workspace.revealLeaf(leafs[0]) 8 | return leafs[0] 9 | } 10 | 11 | const leaf = await createView(workspace, id, position) 12 | if (!leaf) { 13 | throw new Error(`Failed to create view with id: ${id}`) 14 | } 15 | workspace.revealLeaf(leaf) 16 | 17 | return leaf 18 | } 19 | 20 | export const isViewExist = (workspace: Workspace, id: string): boolean => { 21 | let leafs = workspace.getLeavesOfType(id) 22 | return leafs.length > 0 23 | } 24 | 25 | const createView = async (workspace: Workspace, id: string, position?: GateFrameOptionType): Promise => { 26 | let leaf: WorkspaceLeaf | null = null 27 | switch (position) { 28 | case 'left': 29 | leaf = workspace.getLeftLeaf(false) 30 | break 31 | case 'center': 32 | leaf = workspace.getLeaf(true) 33 | break 34 | case 'right': 35 | default: 36 | leaf = workspace.getRightLeaf(false) 37 | break 38 | } 39 | 40 | if (leaf) { 41 | await leaf.setViewState({ type: id, active: true }) 42 | return leaf 43 | } 44 | return undefined 45 | } 46 | -------------------------------------------------------------------------------- /src/fns/registerCodeBlockProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'obsidian' 2 | import { parse } from 'yaml' 3 | import { createIframe } from './createIframe' 4 | import { createWebviewTag } from './createWebviewTag' 5 | import WebviewTag = Electron.WebviewTag 6 | import { createEmptyGateOption } from './createEmptyGateOption' 7 | import OpenGatePlugin from '../main' 8 | import { normalizeGateOption } from './normalizeGateOption' 9 | import { GateFrameOption } from '../GateOptions' 10 | 11 | type CodeBlockOption = GateFrameOption & { 12 | height?: string | number 13 | } 14 | 15 | function processNewSyntax(plugin: OpenGatePlugin, sourceCode: string): Node { 16 | // we want user follow the yaml format, but sometime, it's easier for user to just type the url in the first line, so we support that 17 | const firstLineUrl = sourceCode.split('\n')[0] 18 | if (firstLineUrl.startsWith('http')) { 19 | sourceCode = sourceCode.replace(firstLineUrl, '').trim() 20 | } 21 | // Replace tabs with spaces at the start of each line, because YAML doesn't support tabs 22 | sourceCode = sourceCode.replace(/^\t+/gm, (match) => ' '.repeat(match.length)) 23 | 24 | if (sourceCode.length === 0) { 25 | return createFrame(createEmptyGateOption(), '800px') 26 | } 27 | 28 | let data: Partial = {} 29 | 30 | if (firstLineUrl.startsWith('http')) { 31 | data.url = firstLineUrl 32 | } 33 | 34 | try { 35 | data = Object.assign(data, parse(sourceCode)) 36 | } catch (error) { 37 | return createErrorMessage(error) 38 | } 39 | 40 | if (typeof data !== 'object' || data === null || Object.keys(data).length === 0) { 41 | return createErrorMessage() 42 | } 43 | 44 | let height = '800px' 45 | if (data.height) { 46 | height = typeof data.height === 'number' ? `${data.height}px` : data.height 47 | delete data.height 48 | } 49 | 50 | let prefill: GateFrameOption | undefined 51 | 52 | if (data.title) { 53 | prefill = plugin.findGateBy('title', data.title) 54 | } else if (data.url) { 55 | prefill = plugin.findGateBy('url', data.url) 56 | } 57 | 58 | if (prefill) { 59 | data = Object.assign(prefill, data) 60 | } 61 | 62 | return createFrame(normalizeGateOption(data), height) 63 | } 64 | 65 | function createErrorMessage(error?: Error): Node { 66 | const div = document.createElement('div') 67 | 68 | const messageText = 'The syntax has been updated. Please use the YAML format.' 69 | const messageTextNode = document.createTextNode(messageText) 70 | div.appendChild(messageTextNode) 71 | 72 | if (error) { 73 | const errorDetailsText = `\nError details: ${error.message}` 74 | const errorDetailsTextNode = document.createTextNode(errorDetailsText) 75 | div.appendChild(errorDetailsTextNode) 76 | } 77 | 78 | const linkText = '\nRead more about YAML here.' 79 | const linkTextNode = document.createTextNode(linkText) 80 | const linkNode = document.createElement('a') 81 | linkNode.href = 'https://yaml.org/spec/1.2/spec.html' 82 | linkNode.textContent = 'YAML Syntax' 83 | div.appendChild(linkTextNode) 84 | div.appendChild(linkNode) 85 | 86 | return div 87 | } 88 | 89 | function createFrame(options: GateFrameOption, height: string): HTMLIFrameElement | WebviewTag { 90 | let frame: HTMLIFrameElement | WebviewTag 91 | 92 | if (Platform.isMobileApp) { 93 | frame = createIframe(options) 94 | } else { 95 | frame = createWebviewTag(options) 96 | } 97 | 98 | frame.style.height = height 99 | 100 | return frame 101 | } 102 | 103 | export function registerCodeBlockProcessor(plugin: OpenGatePlugin) { 104 | plugin.registerMarkdownCodeBlockProcessor('gate', (sourceCode, el, _ctx) => { 105 | el.addClass('open-gate-view') 106 | const frame = processNewSyntax(plugin, sourceCode) 107 | el.appendChild(frame) 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /src/fns/registerGate.ts: -------------------------------------------------------------------------------- 1 | import { GateView } from '../GateView' 2 | import { openView } from './openView' 3 | import { addIcon, Plugin } from 'obsidian' 4 | import { GateFrameOption } from '../GateOptions' 5 | 6 | export const registerGate = (plugin: Plugin, options: GateFrameOption) => { 7 | plugin.registerView(options.id, (leaf) => { 8 | return new GateView(leaf, options) 9 | }) 10 | 11 | let iconName = options.icon 12 | 13 | if (options.icon.startsWith(' openView(plugin.app.workspace, options.id, options.position)) 20 | } 21 | 22 | plugin.addCommand({ 23 | id: `open-gate-${btoa(options.url)}`, 24 | name: `Open gate ${options.title}`, 25 | callback: async () => await openView(plugin.app.workspace, options.id, options.position) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/fns/setupInsertLinkMenu.ts: -------------------------------------------------------------------------------- 1 | import { App, Editor, Menu, Notice, Plugin } from 'obsidian' 2 | import { ModalInsertLink } from '../ModalInsertLink' 3 | import { GateFrameOption } from '../GateOptions' 4 | 5 | export const setupInsertLinkMenu = (plugin: Plugin) => { 6 | plugin.registerEvent(plugin.app.workspace.on('editor-menu', (menu, editor) => createMenu(plugin.app, menu, editor))) 7 | } 8 | 9 | const createMenu = (app: App, menu: Menu, editor: Editor) => { 10 | menu.addItem((item) => { 11 | item.setTitle('Insert Gate Link').onClick(async () => { 12 | const modal = new ModalInsertLink(app, async (gate: GateFrameOption) => { 13 | const gateLink = `[${gate.title}](obsidian://opengate?title=${encodeURIComponent(gate.title)}&url=${encodeURIComponent(gate.url)})` 14 | editor.replaceSelection(gateLink) 15 | modal.close() 16 | }) 17 | modal.open() 18 | }) 19 | }) 20 | } 21 | 22 | const getDialog = (plugin: Plugin) => {} 23 | -------------------------------------------------------------------------------- /src/fns/setupLinkConvertMenu.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Menu, Notice, Plugin } from 'obsidian' 2 | import { MarkdownLink } from '../types' 3 | 4 | export const setupLinkConvertMenu = (plugin: Plugin) => { 5 | plugin.registerEvent(plugin.app.workspace.on('editor-menu', createMenu)) 6 | } 7 | 8 | const parseLink = (text: string): MarkdownLink | undefined => { 9 | const markdownLinkMatch = text.match(/\[([^\]]+)\]\(([^)]+)\)/) 10 | if (markdownLinkMatch) { 11 | return { 12 | title: markdownLinkMatch[1], 13 | url: markdownLinkMatch[2] 14 | } 15 | } 16 | 17 | const urlMatch = text.match(/https?:\/\/[^ ]+/) 18 | if (urlMatch) { 19 | return { 20 | title: urlMatch[0], 21 | url: urlMatch[0] 22 | } 23 | } 24 | } 25 | 26 | const createMenu = (menu: Menu, editor: Editor) => { 27 | const selection = editor.getSelection() 28 | if (selection.length === 0) return 29 | 30 | const parsedLink = parseLink(selection) 31 | if (!parsedLink) return 32 | 33 | if (parsedLink.url.startsWith('obsidian://opengate')) { 34 | menu.addItem((item) => { 35 | item.setTitle('Convert to normal link').onClick(async () => { 36 | // get the url parameter from the link 37 | const urlMatch = parsedLink.url.match(/url=([^&]+)/) 38 | if (!urlMatch) { 39 | new Notice('Can not convert the pre-configured gate link to normal link.') 40 | return 41 | } 42 | 43 | const url = decodeURIComponent(urlMatch[1]) 44 | const normalLink = `[${parsedLink.title}](${url})` 45 | editor.replaceSelection(normalLink) 46 | }) 47 | }) 48 | } else { 49 | menu.addItem((item) => { 50 | item.setTitle('Convert to Gate Link').onClick(async () => { 51 | const gateLink = `[${parsedLink.title}](obsidian://opengate?title=${encodeURIComponent(parsedLink.title)}&url=${encodeURIComponent(parsedLink.url)})` 52 | editor.replaceSelection(gateLink) 53 | }) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/fns/unloadView.ts: -------------------------------------------------------------------------------- 1 | import { removeIcon, Workspace } from 'obsidian' 2 | import { GateFrameOption } from '../GateOptions' 3 | 4 | export const unloadView = async (workspace: Workspace, gate: GateFrameOption): Promise => { 5 | workspace.detachLeavesOfType(gate.id) 6 | const ribbonIcons = workspace.containerEl.querySelector(`div[aria-label="${gate.title}"]`) 7 | if (ribbonIcons) { 8 | ribbonIcons.remove() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Notice, ObsidianProtocolData, Plugin } from 'obsidian' 2 | import { SettingTab } from './SetingTab' 3 | import { registerGate } from './fns/registerGate' 4 | import { ModalEditGate } from './ModalEditGate' 5 | import { ModalOnBoarding } from './ModalOnboarding' 6 | import { unloadView } from './fns/unloadView' 7 | import { createEmptyGateOption } from './fns/createEmptyGateOption' 8 | import { normalizeGateOption } from './fns/normalizeGateOption' 9 | import { ModalListGates } from './ModalListGates' 10 | import { registerCodeBlockProcessor } from './fns/registerCodeBlockProcessor' 11 | import { isViewExist, openView } from './fns/openView' 12 | import { GateView } from './GateView' 13 | import { setupLinkConvertMenu } from './fns/setupLinkConvertMenu' 14 | import { setupInsertLinkMenu } from './fns/setupInsertLinkMenu' 15 | import { PluginSetting } from './types' 16 | import { GateFrameOption, GateFrameOptionType } from './GateOptions' 17 | 18 | const DEFAULT_SETTINGS: PluginSetting = { 19 | uuid: '', 20 | gates: {} 21 | } 22 | 23 | export default class OpenGatePlugin extends Plugin { 24 | settings: PluginSetting 25 | 26 | async onload() { 27 | await this.loadSettings() 28 | await this.mayShowOnboardingDialog() 29 | await this.initGates() 30 | this.addSettingTab(new SettingTab(this.app, this)) 31 | this.registerCommands() 32 | this.registerProtocol() 33 | setupLinkConvertMenu(this) 34 | setupInsertLinkMenu(this) 35 | registerCodeBlockProcessor(this) 36 | } 37 | 38 | async mayShowOnboardingDialog() { 39 | // Check if the UUID in the settings is empty 40 | if (this.settings.uuid === '') { 41 | // Generate a new UUID and assign it to the settings 42 | this.settings.uuid = this.generateUuid() 43 | // Save the updated settings 44 | await this.saveSettings() 45 | 46 | // Check if there are no gates in the settings 47 | if (Object.keys(this.settings.gates).length === 0) { 48 | // Open the onboarding modal to create a new gate 49 | new ModalOnBoarding(this.app, createEmptyGateOption(), async (gate: GateFrameOption) => { 50 | // Add the created gate to the settings 51 | await this.addGate(gate) 52 | }).open() 53 | } 54 | } 55 | } 56 | 57 | private async initGates() { 58 | // Iterate over all the gates in the settings 59 | for (const gateId in this.settings.gates) { 60 | // Get the gate with the current ID 61 | const gate = this.settings.gates[gateId] 62 | // Register the gate 63 | registerGate(this, gate) 64 | } 65 | 66 | // this view is used to open gates from the protocol handler 67 | registerGate( 68 | this, 69 | normalizeGateOption({ 70 | id: 'temp-gate', 71 | title: 'Temp Gate', 72 | icon: 'globe', 73 | url: 'about:blank' 74 | }) 75 | ) 76 | } 77 | 78 | private registerCommands() { 79 | this.addCommand({ 80 | id: `open-gate-create-new`, 81 | name: `Create new gate`, 82 | callback: async () => { 83 | new ModalEditGate(this.app, createEmptyGateOption(), async (gate: GateFrameOption) => { 84 | await this.addGate(gate) 85 | }).open() 86 | } 87 | }) 88 | 89 | this.addCommand({ 90 | id: `open-list-gates-modal`, 91 | name: `List Gates`, 92 | hotkeys: [{ modifiers: ['Mod', 'Shift'], key: 'g' }], 93 | callback: async () => { 94 | new ModalListGates(this.app, this.settings.gates, async (gate: GateFrameOption) => { 95 | await this.addGate(gate) 96 | }).open() 97 | } 98 | }) 99 | } 100 | 101 | /** 102 | * Register the "opengate" action to Obsidian. 103 | * 104 | * We will attempt to open a gate based on the provided title and navigate to the provided URL 105 | */ 106 | private registerProtocol() { 107 | this.registerObsidianProtocolHandler('opengate', this.handleCustomProtocol.bind(this)) 108 | } 109 | 110 | getGateOptionFromProtocolData(data: ObsidianProtocolData): GateFrameOption | undefined { 111 | const { title, url, id, position } = data 112 | 113 | // Initialize targetGate as undefined 114 | let targetGate: GateFrameOption | undefined 115 | 116 | // Search for the gate by id first 117 | if (id && this.settings.gates[id]) { 118 | targetGate = this.settings.gates[id] 119 | } else { 120 | // Search for the gate by title or url if id is not found 121 | targetGate = Object.values(this.settings.gates).find( 122 | (gate) => (title && gate.title.toLowerCase() === title.toLowerCase()) || (url && gate.url.toLowerCase() === url.toLowerCase()) 123 | ) 124 | } 125 | 126 | // If no gate is found, create a new empty gate option 127 | if (!targetGate) { 128 | targetGate = createEmptyGateOption() 129 | } 130 | 131 | // Update the url and position if needed 132 | if (url) { 133 | targetGate.url = url 134 | } 135 | 136 | if (position) { 137 | targetGate.position = position as GateFrameOptionType 138 | } 139 | 140 | return targetGate 141 | } 142 | 143 | findGateBy(field: 'title' | 'url', value: string): GateFrameOption | undefined { 144 | return Object.values(this.settings.gates).find((gate) => gate[field].toLowerCase() === value.toLowerCase()) 145 | } 146 | 147 | async handleCustomProtocol(data: ObsidianProtocolData) { 148 | let targetGate = this.getGateOptionFromProtocolData(data) 149 | if (targetGate === undefined) { 150 | if (!data.url) { 151 | new Notice('Missing url parameter') 152 | return 153 | } 154 | } 155 | 156 | console.log(targetGate) 157 | 158 | const gate = await openView(this.app.workspace, targetGate?.id || 'temp-gate', targetGate?.position) 159 | const gateView = gate.view as GateView 160 | gateView?.onFrameReady(() => { 161 | gateView.setUrl(data.url) 162 | }) 163 | } 164 | 165 | async addGate(gate: GateFrameOption) { 166 | const normalizedGate = normalizeGateOption(gate) 167 | 168 | if (!this.settings.gates.hasOwnProperty(normalizedGate.id)) { 169 | registerGate(this, normalizedGate) 170 | } else { 171 | new Notice('This change will take effect after you reload Obsidian.') 172 | } 173 | 174 | this.settings.gates[normalizedGate.id] = normalizedGate 175 | 176 | await this.saveSettings() 177 | } 178 | 179 | async removeGate(gateId: string) { 180 | if (!this.settings.gates[gateId]) { 181 | new Notice('Gate not found') 182 | } 183 | 184 | const gate = this.settings.gates[gateId] 185 | 186 | await unloadView(this.app.workspace, gate) 187 | delete this.settings.gates[gateId] 188 | await this.saveSettings() 189 | new Notice('This change will take effect after you reload Obsidian.') 190 | } 191 | 192 | async loadSettings() { 193 | this.settings = await this.loadData() 194 | // merge default settings 195 | this.settings = { 196 | ...DEFAULT_SETTINGS, 197 | ...this.settings 198 | } 199 | 200 | if (!this.settings.gates) { 201 | this.settings.gates = {} 202 | } 203 | 204 | for (const gateId in this.settings.gates) { 205 | this.settings.gates[gateId] = normalizeGateOption(this.settings.gates[gateId]) 206 | } 207 | } 208 | 209 | async saveSettings() { 210 | await this.saveData(this.settings) 211 | } 212 | 213 | private generateUuid() { 214 | // generate uuid 215 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import { GateFrameOption } from './GateOptions' 2 | 3 | export interface PluginSetting { 4 | uuid: string 5 | gates: Record 6 | } 7 | 8 | export interface MarkdownLink { 9 | title: string 10 | url: string 11 | } 12 | -------------------------------------------------------------------------------- /stuff/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/068f703988c911b845b7756a8543c48747978694/stuff/img.png -------------------------------------------------------------------------------- /stuff/img_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/068f703988c911b845b7756a8543c48747978694/stuff/img_3.png -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .open-gate-view { 2 | padding: 0 !important; 3 | overflow: hidden !important; 4 | } 5 | 6 | .open-gate-view .open-gate-iframe, 7 | .open-gate-view .open-gate-webview { 8 | width: 100%; 9 | height: 100%; 10 | border: none; 11 | background-color: #fff; 12 | } 13 | 14 | .gate-header { 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | } 19 | 20 | .open-gate-mobile-warning { 21 | background: #b38c1d; 22 | padding: 5px 10px; 23 | border-radius: 5px; 24 | margin-bottom: 20px; 25 | font-size: 14px; 26 | color: #fff; 27 | } 28 | 29 | .open-gate-mobile-link { 30 | color: #fff; 31 | text-decoration: underline; 32 | } 33 | 34 | .open-gate--form-field--column .setting-item-control, 35 | .open-gate--form-field--column .setting-item-control textarea, 36 | .open-gate--form-field .setting-item-control input[type='text'] { 37 | width: 100%; 38 | } 39 | 40 | .open-gate--form-field--column .setting-item-control textarea{ 41 | min-height: 80px; 42 | } 43 | 44 | .open-gate--form-field--column { 45 | display: flex; 46 | flex-direction: column; 47 | gap: 10px; 48 | align-items: flex-start; 49 | } 50 | 51 | .open-gate--advanced-options { 52 | border-top: 1px solid var(--background-modifier-border); 53 | padding: 0.75em 0; 54 | display: none; 55 | } 56 | 57 | .open-gate--advanced-options--show { 58 | display: block; 59 | } 60 | 61 | .open-gate--quick-list-item { 62 | padding: 10px; 63 | display: flex; 64 | justify-content: flex-start; 65 | align-items: center; 66 | gap: 10px; 67 | cursor: pointer; 68 | } 69 | 70 | .open-gate--quick-list-item.active, 71 | .open-gate--quick-list-item:hover { 72 | background: var(--background-modifier-hover); 73 | } 74 | 75 | div[data-gate-id] .setting-item { 76 | overflow: hidden; 77 | } 78 | 79 | div[data-gate-id] .setting-item-name { 80 | white-space: nowrap; 81 | overflow: hidden; 82 | text-overflow: ellipsis; 83 | } 84 | 85 | .open-gate--setting--gate .setting-item-info { 86 | width: 100%; 87 | overflow: hidden; 88 | } 89 | .open-gate--setting--gate .setting-item-description{ 90 | white-space: nowrap; 91 | overflow: hidden; 92 | text-overflow: ellipsis; 93 | } 94 | 95 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": ["DOM", "ES5", "ES6", "ES7"] 15 | }, 16 | "include": ["src", "env.d.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs' 2 | 3 | const targetVersion = process.env.npm_package_version 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync('manifest.json', 'utf8')) 7 | const { minAppVersion } = manifest 8 | manifest.version = targetVersion 9 | writeFileSync('manifest.json', JSON.stringify(manifest, null, '\t')) 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync('versions.json', 'utf8')) 13 | versions[targetVersion] = minAppVersion 14 | writeFileSync('versions.json', JSON.stringify(versions, null, '\t')) 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.0.1": "0.15.0", 4 | "1.0.2": "0.15.0", 5 | "1.0.4": "0.15.0", 6 | "1.0.5": "0.15.0", 7 | "1.0.6": "0.15.0", 8 | "1.0.7": "0.15.0", 9 | "1.0.8": "0.15.0", 10 | "1.1.1": "0.15.0", 11 | "1.1.2": "0.15.0", 12 | "1.1.3": "0.15.0", 13 | "1.1.4": "0.15.0", 14 | "1.1.5": "0.15.0", 15 | "1.1.6": "0.15.0", 16 | "1.1.7": "0.15.0", 17 | "1.2.1": "0.15.0", 18 | "1.2.3": "0.15.0", 19 | "1.2.4": "0.15.0", 20 | "1.2.5": "0.15.0", 21 | "1.2.6": "0.15.0", 22 | "1.2.7": "0.15.0", 23 | "1.2.8": "0.15.0", 24 | "1.2.9": "0.15.0", 25 | "1.2.20": "0.15.0", 26 | "1.2.21": "0.15.0", 27 | "1.3.0": "0.15.0", 28 | "1.3.1": "0.15.0", 29 | "1.3.2": "0.15.0", 30 | "1.3.3": "0.15.0", 31 | "1.3.4": "0.15.0", 32 | "1.3.5": "0.15.0", 33 | "1.4.5": "0.15.0", 34 | "1.7.1": "0.15.0", 35 | "1.8.0": "0.15.0", 36 | "1.8.1": "0.15.0", 37 | "1.8.4": "0.15.0", 38 | "1.8.5": "0.15.0", 39 | "1.9.0": "0.15.0", 40 | "1.9.1": "0.15.0", 41 | "1.9.2": "0.15.0", 42 | "1.9.3": "0.15.0", 43 | "1.9.4": "0.15.0", 44 | "1.9.5": "0.15.0", 45 | "1.9.6": "0.15.0", 46 | "1.9.7": "0.15.0", 47 | "1.9.8": "0.15.0", 48 | "1.10.0": "0.15.0", 49 | "1.10.1": "0.15.0", 50 | "1.10.2": "0.15.0", 51 | "1.10.3": "0.15.0", 52 | "1.10.4": "0.15.0", 53 | "1.10.5": "0.15.0", 54 | "1.10.6": "0.15.0", 55 | "1.10.7": "0.15.0", 56 | "1.10.8": "0.15.0", 57 | "1.11.0": "0.15.0", 58 | "1.11.1": "0.15.0", 59 | "1.11.2": "0.15.0", 60 | "1.11.3": "0.15.0", 61 | "1.11.4": "0.15.0", 62 | "1.11.5": "0.15.0", 63 | "1.11.6": "0.15.0", 64 | "1.11.7": "0.15.0", 65 | "1.11.8": "0.15.0", 66 | "1.11.9": "0.15.0", 67 | "1.11.10": "0.15.0" 68 | } --------------------------------------------------------------------------------