├── .eslintrc.json ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .tool-versions ├── LICENSE ├── README.md ├── bump.ts ├── esbuild.config.ts ├── images ├── banners.gif ├── embed.png ├── gradient.png ├── icon.png ├── inception.png └── solid.png ├── manifest-beta.json ├── manifest.json ├── package.json ├── pnpm-lock.yaml ├── src ├── BannerEvents.ts ├── banner │ ├── Banner.svelte │ ├── BannerImage.svelte │ ├── Error.svelte │ ├── Header.svelte │ ├── Icon.svelte │ ├── Loading.svelte │ ├── actions │ │ ├── dragBanner.ts │ │ ├── lockIcon.ts │ │ └── sizedMargin.ts │ ├── index.ts │ ├── mixins.scss │ └── utils.ts ├── bannerData │ ├── index.ts │ └── transformers.ts ├── commands │ ├── downloadBanner.ts │ ├── index.ts │ ├── pasteBanner.ts │ └── utils.ts ├── editing │ ├── extensions │ │ ├── bannerExtender.ts │ │ ├── bannerField.ts │ │ └── utils.ts │ └── index.ts ├── main.ts ├── modals │ ├── IconModal.ts │ ├── IconSuggestion.svelte │ ├── LocalImageModal.ts │ ├── LocalImageSuggestion.svelte │ ├── UpdateLegacySourceModal.ts │ ├── UpdateLegacySourcePrompt.svelte │ ├── UpsertHeaderForm.svelte │ ├── UpsertHeaderModal.ts │ └── utils.ts ├── reading │ └── index.ts ├── settings │ ├── CssSettingsHandler.ts │ ├── Settings.svelte │ ├── SettingsTab.ts │ ├── components │ │ ├── ButtonSetting.svelte │ │ ├── CSSLengthFragment.svelte │ │ ├── Depends.svelte │ │ ├── InputSetting.svelte │ │ ├── ObsidianToggle.svelte │ │ ├── SelectSetting.svelte │ │ ├── SettingHeader.svelte │ │ ├── SettingItem.svelte │ │ └── ToggleSetting.svelte │ ├── index.ts │ ├── store.ts │ ├── structure.ts │ └── updater.ts ├── types.d.ts └── utils.ts ├── tsconfig.json └── versions.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@typescript-eslint"], 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:svelte/recommended", 7 | "plugin:import/recommended", 8 | "plugin:import/typescript" 9 | ], 10 | "settings": { 11 | "import/resolver": { 12 | "typescript": { "project": "tsconfig.json" }, 13 | "node": { "project": "tsconfig.json" } 14 | } 15 | }, 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "project": "tsconfig.json", 19 | "extraFileExtensions": [".svelte"] 20 | }, 21 | "root": true, 22 | "ignorePatterns": ["/dist/**"], 23 | "rules": { 24 | "indent": ["error", 2, { "SwitchCase": 1 }], 25 | "max-len": ["error", { 26 | "code": 100, 27 | "tabWidth": 2 28 | }], 29 | "semi": ["error", "always"], 30 | "quotes": ["error", "single"], 31 | "comma-dangle": ["error", "never"], 32 | "no-multi-spaces": ["error", { "ignoreEOLComments": true }], 33 | "array-bracket-newline": ["error", { 34 | "multiline": true, 35 | "minItems": 4 36 | }], 37 | "array-element-newline": ["error", "consistent"], 38 | "object-curly-newline": ["error", { 39 | "multiline": true, 40 | "minProperties": 4 41 | }], 42 | "object-curly-spacing": ["error", "always", { 43 | "arraysInObjects": false, 44 | "objectsInObjects": false 45 | }], 46 | "object-property-newline": ["error", { "allowAllPropertiesOnSameLine": true }], 47 | "@typescript-eslint/no-explicit-any": "off", 48 | "@typescript-eslint/member-delimiter-style": "error", 49 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 50 | "import/order": ["error", { 51 | "groups": [ 52 | "builtin", 53 | "external", 54 | "internal", 55 | "parent", 56 | "sibling", 57 | "index" 58 | ], 59 | "newlines-between": "never", 60 | "alphabetize": { 61 | "order": "asc", 62 | "caseInsensitive": true 63 | } 64 | }], 65 | "import/consistent-type-specifier-style": "error", 66 | "import/no-duplicates": "off", 67 | "import/newline-after-import": "error" 68 | }, 69 | "overrides": [ 70 | { 71 | "files": ["*.svelte"], 72 | "parser": "svelte-eslint-parser", 73 | "parserOptions": { "parser": "@typescript-eslint/parser" }, 74 | "rules": { 75 | "no-undef": "off", 76 | "svelte/first-attribute-linebreak": "error", 77 | "svelte/max-attributes-per-line": ["error", { 78 | "multiline": 1, 79 | "singleline": 3 80 | }] 81 | } 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Setup Node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: "18.x" 19 | 20 | - name: Build plugin 21 | run: | 22 | npm install 23 | npm run build 24 | 25 | - name: Create release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: | 29 | tag="${GITHUB_REF#refs/tags/}" 30 | 31 | gh release create "$tag" \ 32 | --title="$tag" \ 33 | --draft \ 34 | dist/* 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .hotreload 4 | .env 5 | .vscode 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | node 18.17.1 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Danny Hernandez 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 | # Banners 2 | An [Obsidian](https://obsidian.md/) plugin to add banner images (and icons) to your notes! 3 | 4 | ![banners-demo](https://raw.githubusercontent.com/noatpad/obsidian-banners/master/images/banners.gif) 5 | 6 | ## Usage 7 | Within an open note, you can use the **Add/Change banner with local image** command to select a local image as a banner for your note; or you can copy an image URL & run the **Paste banner from clipboard** command to paste the URL as a banner image. You can also drag the banner image to reposition the image, as well as use the **Lock/Unlock banner position** command to "lock" the banner's position in place and vice versa. 8 | 9 | If you want to remove the banner, you can run the **Remove banner** command to do just that. 10 | 11 | You can also add a banner icon, using **Add/Change emoji icon** & selecting an emoji of your choice. You can also change an existing emoji by clicking on it in the preview. 12 | 13 | Similarly, you can remove the icon with the **Remove icon** command. 14 | 15 | ### Advanced 16 | Under the hood, this plugin uses your file's YAML frontmatter/metadata to store info about your banner. So you can also input it manually or use other plugins to input it for you. These are the fields you can use thus far (using the default frontmatter field prefix): 17 | 18 | ```yaml 19 | # The source path of your banner image, can be a URL or an internal link to an image. 20 | # NOTE: Make sure it's wrapped in quotes to avoid parsing errors, such as "![[file]]" 21 | banner: string 22 | 23 | # The banner's center position. A number between 0-1 24 | banner_x: number 25 | banner_y: number 26 | 27 | # Determines if the banner is locked in place or not 28 | banner_lock: boolean 29 | 30 | # The banner icon. Can be an emoji or any string really (but it'll only accept the first letter) 31 | banner_icon: string 32 | ``` 33 | 34 | ## Settings 35 | ### Banners 36 | - **Banner height**: Specify how tall the banner image should be for each note. 37 | - **Banner style**: Change how your banner looks in your notes. There are currently 2 options: 38 | - *Solid*: A simple, sharp-container banner image. 39 | - *Gradient*: A banner that fades into transparency, inspired by [this forum post](https://forum.obsidian.md/t/header-images-with-css/18917). 40 | 41 | | Solid | Gradient | 42 | | --- | --- | 43 | | ![solid-style](https://raw.githubusercontent.com/noatpad/obsidian-banners/master/images/solid.png) | ![gradient-style](https://raw.githubusercontent.com/noatpad/obsidian-banners/master/images/gradient.png) | 44 | 45 | - **Show banner in internal embed**: Choose if the banner should be displayed in the inline internal embed within a file. 46 | - **Preview internal banner height**: If **Show banner in internal embed** is on, this setting determines how tall the banner image in the embed should be. 47 | 48 | ![inception](https://raw.githubusercontent.com/noatpad/obsidian-banners/master/images/inception.png) 49 | 50 | - **Show banner in preview embed**: Choose if the banner should be displayed in the preview embed for the *Page Preview* plugin. 51 | - **Preview embed banner height**: If **Show banner in preview embed** is on, this setting determines how tall the banner image in the embed should be. 52 | 53 | ![embed](https://raw.githubusercontent.com/noatpad/obsidian-banners/master/images/embed.png) 54 | 55 | - **Frontmatter field name**: If set, use a customizable frontmatter field to use for banner data. For example, the default value `banner` will use the fields `banner_x`, `banner_y`, and so on. 56 | - **Banner drag modifier key**: Set a modifier key that must be usedto drag a banner. For example, setting it to *Shift* means you'll have to drag with Shift to move it. This can help avoid accidental banner shifts. 57 | 58 | ### Banner Icons 59 | - **Horizontal alignment**: Align the icon horizontally within the note's boundaries. If set to *Custom*, you can input a custom offset, relative to the note's left boundary. This can be any valid [CSS length value](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units#lengths). 60 | - **Vertical alignment**: Align the icon vertically relative to a banner's bottom edge, if a banner is present. If set to *Custom*, you can input a custom offset, relative to the center of a banner's lower edge if any. This can also be any valid [CSS length value](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units#lengths). 61 | - **Use Twemoji**: If set, it will use Twemoji (Twitter's emoji set) instead of the stock emoji on your device. This is on by default as there is better emoji support using this. 62 | 63 | ### Local Image Modal 64 | - **Show preview images**: Enable this to allow preview images to be seen when searching through the modal. 65 | - **Suggestions limit**: Limit the amount of suggestions the modal can display at once. 66 | - ***NOTE:** If you set this to a high number, while having **Show preview images** on, you may experience some slowdowns while searching.* 67 | - **Banners folder**: Specify a folder to exclusively search for image files within the modal. If unset, the modal will search the entire vault by default. 68 | 69 | ### Experimental 70 | - **Allow mobile drag**: Choose if you can adjust the banner positioning on mobile devices by dragging. 71 | - ***NOTE:** This setting is experimental since it acts a bit funny with the mobile app's already built-in touch gestures.* 72 | 73 | ## Compatibility 74 | This plugin has been tested on desktop from 0.12.12 onwards (previously MacOS and currently Windows) and on mobile from 1.0.4 onwards (iOS). It probably works fine on older versions, but just a heads up. 75 | 76 | ## Installation 77 | - **From the Community Plugins tab**: 78 | - Within Obsidian, search for Banners in the Community Plugins browser and install it directly 79 | - **Manual install**: 80 | - Go to the latest release [here](https://github.com/noatpad/obsidian-banners/releases/latest), & download the files listed there (`main.js`, `styles.css`, & `manifest.json`) 81 | - Go to your vault's plugin folder (`/.obsidian/plugins`), create a folder called `obsidian-banners`, and move your files in there. 82 | - Reload Obsidian & enable the plugin in Settings -> Community Plugins 83 | 84 | ## FAQ 85 | #### What are these `banner`, `banner_x`, `banner_y`, ... fields in my note's frontmatter? 86 | This plugin uses the frontmatter to store data about your note's banner, such as its positioning and such. The fields you can use are listed [here](https://github.com/noatpad/obsidian-banners#advanced) and the prefix can be customized using the **Frontmatter field name** setting. 87 | 88 | #### Is this incompatible with other plugins? 89 | There are a few cases, but it depends. Because of how it functions, any plugin that conflicts with Banners' styling may cause issues. It's rather situational, but I'm planning to address some styling fixes for those conflicts down the line. 90 | 91 | Currently some plugins reported to conflict with Banners are: 92 | - [ ] [Breadcrumbs](https://github.com/SkepticMystic/breadcrumbs) 93 | - [x] [Obsidian Code Block Copy](https://github.com/jdbrice/obsidian-code-block-copy) 94 | - *Newer versions of Obsidian have this built-in and without issue* 95 | - [ ] [Obsidian Code Block Enhancer](https://github.com/nyable/obsidian-code-block-enhancer) 96 | - [ ] [Obsidian Embedded Note Titles](https://github.com/mgmeyers/obsidian-embedded-note-titles) 97 | 98 | ## Develop 99 | Once you run `npm i`, you can build the files into `dist/` easily by running `npm run build`. 100 | 101 | You can also have it watch your files and update your plugin within your vault while you develop by running `npm run dev`. Just make sure to set `DEVDIR` in `./esbuild.config.mjs` to your testing vault beforehand. 102 | ## Things I *might* do down the road 103 | - [ ] Plugin compatibility fixes and enhancements 104 | - [ ] Note-specific settings (override global style & height settings per note) 105 | - [ ] Drag bottom of banner to determine note-specific banner height 106 | - [ ] Image icons instead of only emoji 107 | - [ ] Banner titles (a la Notion-style) 108 | - [ ] Allow content's vertical displacement height to be different than banner height (this can be nice for aesthetic choices with the *Gradient* style) 109 | - [ ] Copy image files and paste as a banner 110 | - [ ] Unsplash API integration (select from Unsplash's images straight from Obsidian) 111 | -------------------------------------------------------------------------------- /bump.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-as-default-member */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import { execSync } from 'child_process'; 4 | import { readFile, writeFile } from 'fs/promises'; 5 | import semver from 'semver'; 6 | import type { ReleaseType } from 'semver'; 7 | import { dependencies, version } from './package.json'; 8 | 9 | const [ 10 | _path, 11 | _file, 12 | argType = 'patch', 13 | argBeta 14 | ] = process.argv; 15 | 16 | const beta = (argBeta === 'beta'); 17 | const releaseType = `${beta ? 'pre' : ''}${argType}` as ReleaseType; 18 | 19 | const manifestFile = beta ? 'manifest-beta.json' : 'manifest.json'; 20 | const manifest = JSON.parse(await readFile(manifestFile, 'utf-8')); 21 | const versions = JSON.parse(await readFile('versions.json', 'utf-8')); 22 | const obsVer = semver.coerce(dependencies.obsidian)!.version; 23 | const newVer = semver.inc(semver.coerce(version)!, releaseType, 'beta', false)!; 24 | 25 | manifest.version = newVer; 26 | if (semver.gt(obsVer, manifest.minAppVersion)) { 27 | manifest.minAppVersion = obsVer; 28 | versions[newVer] = obsVer; 29 | } 30 | 31 | await writeFile(manifestFile, JSON.stringify(manifest, null, '\t')); 32 | await writeFile('versions.json', JSON.stringify(versions, null, '\t')); 33 | execSync( 34 | `npm version ${newVer} --git-tag-version=false && 35 | git commit -a -m "Prepare release" && 36 | git tag ${newVer}` 37 | ); 38 | -------------------------------------------------------------------------------- /esbuild.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import 'dotenv/config'; 3 | import fs from 'fs/promises'; 4 | import builtins from 'builtin-modules'; 5 | // @ts-ignore 6 | import copyNewer from 'copy-newer'; 7 | import esbuild from 'esbuild'; 8 | import esbuildSvelte from 'esbuild-svelte'; 9 | import sveltePreprocess from 'svelte-preprocess'; 10 | 11 | const BANNER = `/* 12 | - THIS IS A GENERATED/BUNDLED FILE BY ESBUILD - 13 | Please visit the repository linked to view the source code: 14 | https://github.com/noatpad/obsidian-banners 15 | */`; 16 | const prod = (process.argv[2] === 'prod'); 17 | const outdir = 'dist'; 18 | 19 | const obsimove: esbuild.Plugin = { 20 | name: 'obsimove', 21 | setup(build) { 22 | build.onEnd(async () => { 23 | await fs.rename(`${outdir}/main.css`, `${outdir}/styles.css`); 24 | await fs.copyFile('manifest.json', `${outdir}/manifest.json`); 25 | await fs.writeFile(`${outdir}/.hotreload`, ''); 26 | 27 | // Copy to dev vault if needed 28 | if (!prod && process.env.DEVDIR) { 29 | copyNewer('{.*,**}', process.env.DEVDIR, { cwd: outdir }); 30 | } 31 | }); 32 | } 33 | }; 34 | 35 | // eslint-disable-next-line import/no-named-as-default-member 36 | const context = await esbuild.context({ 37 | banner: { js: BANNER }, 38 | entryPoints: ['src/main.ts'], 39 | bundle: true, 40 | plugins: [ 41 | esbuildSvelte({ 42 | compilerOptions: { css: 'external' }, 43 | preprocess: sveltePreprocess() 44 | }), 45 | obsimove 46 | ], 47 | external: [ 48 | 'obsidian', 49 | 'electron', 50 | '@codemirror/autocomplete', 51 | '@codemirror/collab', 52 | '@codemirror/commands', 53 | '@codemirror/language', 54 | '@codemirror/lint', 55 | '@codemirror/search', 56 | '@codemirror/state', 57 | '@codemirror/view', 58 | '@lezer/common', 59 | '@lezer/highlight', 60 | '@lezer/lr', 61 | ...builtins 62 | ], 63 | format: 'cjs', 64 | target: 'es2020', 65 | treeShaking: true, 66 | minify: prod, 67 | sourcemap: prod ? false : 'inline', 68 | color: true, 69 | outdir, 70 | logLevel: 'info' 71 | }); 72 | 73 | if (prod) { 74 | await context.rebuild(); 75 | process.exit(0); 76 | } else { 77 | await context.watch(); 78 | } 79 | -------------------------------------------------------------------------------- /images/banners.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noatpad/obsidian-banners/f5f5aa732752124bcdde95c378a2e8ae243d5066/images/banners.gif -------------------------------------------------------------------------------- /images/embed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noatpad/obsidian-banners/f5f5aa732752124bcdde95c378a2e8ae243d5066/images/embed.png -------------------------------------------------------------------------------- /images/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noatpad/obsidian-banners/f5f5aa732752124bcdde95c378a2e8ae243d5066/images/gradient.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noatpad/obsidian-banners/f5f5aa732752124bcdde95c378a2e8ae243d5066/images/icon.png -------------------------------------------------------------------------------- /images/inception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noatpad/obsidian-banners/f5f5aa732752124bcdde95c378a2e8ae243d5066/images/inception.png -------------------------------------------------------------------------------- /images/solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noatpad/obsidian-banners/f5f5aa732752124bcdde95c378a2e8ae243d5066/images/solid.png -------------------------------------------------------------------------------- /manifest-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-banners", 3 | "name": "Banners", 4 | "description": "Add banner images to your notes!", 5 | "version": "2.0.5-beta", 6 | "minAppVersion": "1.4.11", 7 | "author": "Noatpad", 8 | "authorUrl": "https://github.com/noatpad/obsidian-banners", 9 | "isDesktopOnly": false 10 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-banners", 3 | "name": "Banners", 4 | "description": "Add banner images to your notes!", 5 | "version": "1.3.3", 6 | "minAppVersion": "0.13.21", 7 | "author": "Noatpad", 8 | "authorUrl": "https://github.com/noatpad/obsidian-banners", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-banners", 3 | "version": "2.0.5-beta", 4 | "description": "Add banners to your Obsidian notes!", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "tsx esbuild.config.ts", 8 | "build": "tsx esbuild.config.ts prod", 9 | "prepare": "husky install", 10 | "lint": "eslint .", 11 | "bump": "tsx bump.ts" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@codemirror/state": "^6.2.1", 18 | "@codemirror/view": "^6.17.0", 19 | "@twemoji/api": "^14.1.2", 20 | "lodash": "^4.17.21", 21 | "node-emoji": "^2.1.0", 22 | "obsidian": "^1.4.11" 23 | }, 24 | "devDependencies": { 25 | "@tsconfig/svelte": "^5.0.2", 26 | "@types/lodash": "^4.14.198", 27 | "@types/node": "^20.5.9", 28 | "@types/semver": "^7.5.3", 29 | "@types/twemoji-parser": "^13.1.1", 30 | "@typescript-eslint/eslint-plugin": "^6.6.0", 31 | "@typescript-eslint/parser": "^6.6.0", 32 | "builtin-modules": "^3.3.0", 33 | "copy-newer": "^2.1.2", 34 | "dotenv": "^16.3.1", 35 | "esbuild": "^0.19.2", 36 | "esbuild-svelte": "^0.7.4", 37 | "eslint": "^8.48.0", 38 | "eslint-import-resolver-typescript": "^3.6.0", 39 | "eslint-plugin-import": "^2.28.1", 40 | "eslint-plugin-svelte": "^2.33.0", 41 | "husky": "^8.0.0", 42 | "lint-staged": "^14.0.1", 43 | "sass": "^1.66.1", 44 | "semver": "^7.5.4", 45 | "svelte": "^4.2.0", 46 | "svelte-preprocess": "^5.0.4", 47 | "tsx": "^3.12.7", 48 | "typescript": "^5.2.2" 49 | }, 50 | "type": "module", 51 | "lint-staged": { 52 | "*.{ts,svelte}": "eslint" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/BannerEvents.ts: -------------------------------------------------------------------------------- 1 | import { Events } from 'obsidian'; 2 | import type { EventRef } from 'obsidian'; 3 | import { registerEditorBannerEvents } from './editing'; 4 | import { registerReadingBannerEvents } from './reading'; 5 | import type { BannerSettings } from './settings/structure'; 6 | 7 | export default class BannerEvents extends Events { 8 | loadEvents() { 9 | registerReadingBannerEvents(); 10 | registerEditorBannerEvents(); 11 | } 12 | 13 | on(name: 'setting-change', callback: (changed: Partial) => void): EventRef { 14 | return super.on(name, callback); 15 | } 16 | 17 | trigger(name: 'setting-change', data: Partial): void { 18 | super.trigger(name, data); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/banner/Banner.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 35 | 36 |
41 | 42 | {#if source} 43 | {#await fetchImage(source, file.path)} 44 | 45 | {:then src} 46 | updateBannerData(file, detail)} 53 | on:toggle-lock={toggleLock} 54 | /> 55 | {:catch error} 56 | 57 | {/await} 58 | {/if} 59 | {#if icon || header} 60 |
67 | {/if} 68 |
69 | 70 | 103 | -------------------------------------------------------------------------------- /src/banner/BannerImage.svelte: -------------------------------------------------------------------------------- 1 | 63 | 64 | Banner 82 | {#if enableLockButton} 83 | 92 | 93 | 94 | 95 | 111 | -------------------------------------------------------------------------------- /src/modals/UpsertHeaderForm.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 | {#if !useFilename} 25 |
26 | e.key === 'Enter' && upsertHeader()} 33 | /> 34 |
35 | {/if} 36 |
37 | 38 | { useFilename = !useFilename; }} 41 | id="useFilenameForHeader" 42 | /> 43 |
44 | 48 |
49 | 50 | 70 | -------------------------------------------------------------------------------- /src/modals/UpsertHeaderModal.ts: -------------------------------------------------------------------------------- 1 | import { Modal, TFile } from 'obsidian'; 2 | import { plug } from 'src/main'; 3 | import UpsertHeaderForm from './UpsertHeaderForm.svelte'; 4 | 5 | export default class UpsertHeaderModal extends Modal { 6 | activeFile: TFile; 7 | component!: UpsertHeaderForm; 8 | off!: () => void; 9 | 10 | constructor(file: TFile) { 11 | super(plug.app); 12 | this.activeFile = file; 13 | } 14 | 15 | onOpen() { 16 | this.titleEl.setText('What would you like to put on your header?'); 17 | this.component = new UpsertHeaderForm({ 18 | target: this.contentEl, 19 | props: { file: this.activeFile } 20 | }); 21 | this.off = this.component.$on('close', () => this.close()); 22 | } 23 | 24 | onClose() { 25 | this.off(); 26 | this.component.$destroy(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/modals/utils.ts: -------------------------------------------------------------------------------- 1 | import type { SearchMatches, TFile } from 'obsidian'; 2 | 3 | interface PathPart { part: string; bold: boolean } 4 | 5 | export const getPathParts = (file: TFile, matches: SearchMatches): PathPart[] => { 6 | const { path } = file; 7 | if (!matches.length) return [{ part: path.slice(0, path.length), bold: false }]; 8 | 9 | const parts: PathPart[] = []; 10 | let i = 0; 11 | for (const [start, end] of matches) { 12 | if (i !== start) parts.push({ part: path.slice(i, start), bold: false }); 13 | parts.push({ part: path.slice(start, end), bold: true }); 14 | i = end; 15 | } 16 | if (i !== path.length) parts.push({ part: path.slice(i, path.length), bold: false }); 17 | return parts; 18 | }; 19 | -------------------------------------------------------------------------------- /src/reading/index.ts: -------------------------------------------------------------------------------- 1 | import type { MarkdownPostProcessor } from 'obsidian'; 2 | import { 3 | createBanner, 4 | hasBanner, 5 | updateBanner, 6 | destroyBanner, 7 | shouldDisplayBanner 8 | } from 'src/banner'; 9 | import type { BannerProps, Embedded } from 'src/banner'; 10 | import { extractBannerData } from 'src/bannerData'; 11 | import { plug } from 'src/main'; 12 | import { getSetting } from 'src/settings'; 13 | import { iterateMarkdownLeaves, registerSettingChangeEvent } from 'src/utils'; 14 | 15 | /* BUG: This doesn't rerender banners in internal embeds properly. 16 | Reload app or manually edit the view/contents to fix */ 17 | const rerender = () => { 18 | for (const leaf of plug.app.workspace.getLeavesOfType('markdown')) { 19 | const { previewMode } = leaf.view; 20 | const sections = previewMode.renderer.sections.filter((s) => ( 21 | s.el.querySelector('pre.frontmatter, .internal-embed') 22 | )); 23 | for (const section of sections) { 24 | section.rendered = false; 25 | section.html = ''; 26 | } 27 | previewMode.renderer.queueRender(); 28 | } 29 | }; 30 | 31 | const isEmbedded = (containerEl: HTMLElement): Embedded => { 32 | if (containerEl.closest('.internal-embed')) return 'internal'; 33 | if (containerEl.closest('.popover')) return 'popover'; 34 | return false; 35 | }; 36 | 37 | const postprocessor: MarkdownPostProcessor = (el, ctx) => { 38 | const { 39 | docId, 40 | containerEl, 41 | frontmatter, 42 | sourcePath 43 | } = ctx; 44 | 45 | // Only show banners in embeds when allowed 46 | const embed = isEmbedded(containerEl); 47 | if ( 48 | (embed === 'internal' && !getSetting('showInInternalEmbed')) || 49 | (embed === 'popover' && !getSetting('showInPopover')) 50 | ) return; 51 | 52 | const file = plug.app.metadataCache.getFirstLinkpathDest(sourcePath, '/')!; 53 | const bannerData = extractBannerData(frontmatter, file); 54 | 55 | if (shouldDisplayBanner(bannerData)) { 56 | const props: BannerProps = { 57 | ...bannerData, 58 | file, 59 | embed, 60 | viewType: 'reading' 61 | }; 62 | if (hasBanner(docId)) { 63 | updateBanner(props, docId); 64 | } else { 65 | createBanner(props, containerEl.parentElement!, docId); 66 | } 67 | } else { 68 | destroyBanner(docId); 69 | } 70 | }; 71 | 72 | export const loadPostProcessor = () => { 73 | plug.registerMarkdownPostProcessor(postprocessor); 74 | rerender(); 75 | }; 76 | 77 | export const registerReadingBannerEvents = () => { 78 | registerSettingChangeEvent([ 79 | 'frontmatterField', 80 | 'showInInternalEmbed', 81 | 'useHeaderByDefault', 82 | 'defaultHeaderValue' 83 | ], rerender); 84 | plug.registerEvent(plug.app.vault.on('rename', rerender)); 85 | 86 | // Edge case when switching from a note with a banner to a banner with no data to postprocess 87 | plug.registerEvent(plug.app.workspace.on('layout-change', () => { 88 | iterateMarkdownLeaves((leaf) => { 89 | if (!leaf.view.file.stat.size) destroyBanner(leaf.view.previewMode.docId); 90 | }, 'reading'); 91 | })); 92 | }; 93 | -------------------------------------------------------------------------------- /src/settings/CssSettingsHandler.ts: -------------------------------------------------------------------------------- 1 | import { registerSettingChangeEvent } from 'src/utils'; 2 | import type { BannerSettings } from './structure'; 3 | import { getSetting, parseCssSetting } from '.'; 4 | 5 | interface CssSettingToVar { 6 | keys: keyof BannerSettings | Array; 7 | suffix: string; 8 | process?: () => string; 9 | } 10 | 11 | const BANNERS_VAR_PREFIX = '--banners'; 12 | const CSS_VAR_SETTINGS_LIST: CssSettingToVar[] = [ 13 | { keys: 'height', suffix: 'height' }, 14 | { keys: 'mobileHeight', suffix: 'mobile-height' }, 15 | { keys: 'internalEmbedHeight', suffix: 'internal-embed-height' }, 16 | { keys: 'popoverHeight', suffix: 'popover-height' }, 17 | { keys: 'headerSize', suffix: 'header-font-size' }, 18 | { keys: 'iconSize', suffix: 'icon-font-size' }, 19 | { 20 | keys: [ 21 | 'headerHorizontalAlignment', 22 | 'headerHorizontalTransform', 23 | 'headerVerticalAlignment', 24 | 'headerVerticalTransform' 25 | ], 26 | suffix: 'header-transform', 27 | process: () => { 28 | const horizontal = getSetting('headerHorizontalAlignment'); 29 | const hTransform = getSetting('headerHorizontalTransform'); 30 | const vertical = getSetting('headerVerticalAlignment'); 31 | const vTransform = getSetting('headerVerticalTransform'); 32 | const h = (horizontal === 'custom') ? hTransform : '0px'; 33 | const v = (vertical === 'custom') ? vTransform : '0px'; 34 | return `translate(${h}, ${v})`; 35 | } 36 | } 37 | ]; 38 | 39 | const setCssVars = () => { 40 | for (const { keys, suffix, process } of CSS_VAR_SETTINGS_LIST) { 41 | const value = process 42 | ? process() 43 | : parseCssSetting(getSetting(Array.isArray(keys) ? keys[0] : keys) as string); 44 | document.body.style.setProperty(`${BANNERS_VAR_PREFIX}-${suffix}`, value); 45 | } 46 | }; 47 | 48 | export const unsetCssVars = () => { 49 | for (const { suffix } of CSS_VAR_SETTINGS_LIST) { 50 | document.body.style.removeProperty(`${BANNERS_VAR_PREFIX}-${suffix}`); 51 | } 52 | }; 53 | 54 | export const prepareCssSettingListener = () => { 55 | const settings = CSS_VAR_SETTINGS_LIST.map((s) => s.keys).flat(); 56 | registerSettingChangeEvent(settings, setCssVars); 57 | setCssVars(); 58 | }; 59 | -------------------------------------------------------------------------------- /src/settings/Settings.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 |
34 | 35 | Banner style 36 | Set a style for all of your banners 37 | 38 | 39 | Banner height 40 | 41 | Set how big the banner should be in pixels. 42 | 43 | 44 | 45 | 46 | Mobile banner height 47 | 48 | Set how big the banner should be on mobile devices. 49 | 50 | 51 | 52 | 53 | Adjust width to readable line length 54 | 55 | Adjust the banner width to only be as wide as the readable line length, which is used 56 | by Obsidian's Readable line length setting. It is recommended to toggle this together 57 | with that setting. 58 | 59 | 60 | 61 | Banner drag modifier key 62 | 63 | Set a modifier key that must be used to drag a banner. 64 |
65 | For example, setting it to ⇧ Shift means you must hold down Shift in order to 66 | move a banner by dragging it. This can help avoid accidental banner movements. 67 |
68 |
69 | 70 | Property field name 71 | 72 | Set a prefix field name to be used for banner data in the frontmatter/Properties view. 73 |
74 | For example, using {frontmatterField} means that banner data will be extracted 75 | from fields like {frontmatterField}, {frontmatterField}_x, 76 | {frontmatterField}_icon, etc. 77 |
78 |
79 | 80 | 81 | 82 | Show in internal embed 83 | 84 | Display the banner in the internal file embed. This is the embed that appears when you 85 | write ![[file]] in a file. 86 |
87 | Note: You might need to reload Obsidian after toggling this setting 88 |
89 |
90 | 91 | 92 | Internal embed banner height 93 | 94 | Set how big the banner should be within an internal embed. 95 | 96 | 97 | 98 | 99 | Enable drag in internal embed 100 | Allow banner dragging from within an internal embed 101 | 102 | 103 | 104 | 105 | 106 | Show in preview popover 107 | 108 | Display the banner in the page preview popover. This is the preview that appears from the 109 | Page Preview core plugin. 110 | 111 | 112 | 113 | 114 | Preview popover banner height 115 | 116 | Set how big the banner should be within the preview popover. 117 | 118 | 119 | 120 | 121 | Enable drag in preview popover 122 | 123 | Allow banner dragging from within the preview popover. 124 | This may act a bit finicky though. 125 | 126 | 127 | 128 | 129 | 130 | Enable lock button 131 | 132 | Enable and display the lock button on the corner of a banner. When combined with the 133 | Banner drag modifier key setting, it might be desirable to disable this. 134 | 135 | 136 | 137 | 138 |
142 | 143 | Header font size 144 | 145 | Set the font size of the banner header. 146 | 147 | If left blank, it will use Obsidian's built-in font size for inline titles. 148 | Though personally, I like setting it to 2.5em 149 | 150 | 151 | 152 | Header decoration 153 | 154 | Add a shadow or border on the header elements to help with readability. 155 | 156 | 157 | 158 | Horizontal alignment 159 | Align the header horizontally. 160 | 161 | 162 | 163 | Custom horizontal alignment 164 | 165 | Set an offset relative to the left side of the note. 166 | 167 | 168 | 169 | 170 | 171 | Vertical alignment 172 | 173 | Align the header vertically relative to a banner, if any. If there's no banner, this setting 174 | has no effect. 175 | 176 | 177 | 178 | 179 | Custom vertical alignment 180 | 181 | Set an offset relative to the bottom edge of the banner, if any. 182 | 183 | 184 | 185 | 186 | 187 | Display header by default 188 | 189 | Display a banner header without having to define a {frontmatterField}_header 190 | property. This will essentially make it behave like Obsidian's native inline title feature. 191 |
192 | You can override this setting at an individual note level by having an empty 193 | {frontmatterField}_header property too. 194 |
195 |
196 | 197 | 198 | Default header value 199 | 200 | The default header text when the setting above is in effect for a given note. 201 |
202 | Any text is allowed, but you can also combine it with {'{{property}}'} to 203 | reference a property in your note, as well as {'{{filename}}'} to use 204 | the file's name. You can also set fallback keys with the 205 | {'{{property1, property2, property3}}'} syntax. 206 |
207 |
208 |
209 | 210 | 211 |
215 | 216 | Icon size 217 | 218 | Set the size of the banner icon. 219 | 220 |
221 | Note: this setting stacks with the Header font size setting above 222 |
223 |
224 | 225 | Use Twemoji 226 | 227 | Use Twemoji 228 | instead of your device's native emoji set. Makes emojis consistent across devices 229 | 230 | 231 | 232 | 233 |
239 | 240 | Show preview images 241 | Display a preview image of the suggested banner images 242 | 243 | 244 | Suggestions limit 245 | 246 | Limit how many suggestions to display in this modal. 247 |
248 | Note: setting a high number while Show preview images setting is toggled on 249 | may cause slowdowns 250 |
251 |
252 | 253 | Banners folder 254 | 255 | Select a folder to exclusively search for banner files in. If empty, it will search 256 | the entire vault for image files 257 | 258 | 259 | 260 | 261 |
265 | 266 | Auto-download pasted banners 267 | 268 | If enabled, the Paste banner from clipboard command will automatically download the 269 | image from the URL into your vault and link it as an internal file. Great if you want to 270 | keep some banners you found online! 271 |
272 | If you want to download them on a case-by-case basis though, you can use the 273 | Download banner in note to vault command for a note with a remote banner URL. 274 |
275 |
276 | 277 | Update legacy source syntax 278 | 279 | If you used Banners 1.x in the past, you may need to update the syntax for your banners' 280 | sources across your notes. This will help you do that automatically in one go. 281 | 282 | 283 | 288 | Flush banner image cache 289 | 290 | This plugin uses a cache to quickly return banner images, which is especially useful with 291 | remote images. While this cache resets whenever you reopen/reload Obsidian, you can flush 292 | the cache here if you're running into an issue with it. 293 | 294 | 295 | -------------------------------------------------------------------------------- /src/settings/SettingsTab.ts: -------------------------------------------------------------------------------- 1 | import { PluginSettingTab } from 'obsidian'; 2 | import { plug } from 'src/main'; 3 | import Settings from './Settings.svelte'; 4 | 5 | export class SettingsTab extends PluginSettingTab { 6 | component: Settings | undefined; 7 | 8 | constructor() { 9 | super(plug.app, plug); 10 | } 11 | 12 | display() { 13 | this.component = this.component || new Settings({ target: this.containerEl }); 14 | } 15 | 16 | hide() { 17 | this.component?.$destroy(); 18 | this.component = undefined; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/settings/components/ButtonSetting.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /src/settings/components/CSSLengthFragment.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | This can be any valid number or 8 | CSS length value{#if period}.{/if} 9 | {#if examples} 10 | such as 10px, -30%, calc(1em + 10px), and so on... 11 | {/if} 12 | -------------------------------------------------------------------------------- /src/settings/components/Depends.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {#if show} 13 | 14 | {/if} 15 | -------------------------------------------------------------------------------- /src/settings/components/InputSetting.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 30 | 31 | updateSetting(update, e)} 39 | /> 40 | 41 | -------------------------------------------------------------------------------- /src/settings/components/ObsidianToggle.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 |
17 | 23 |
24 | -------------------------------------------------------------------------------- /src/settings/components/SelectSetting.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 24 | 25 | -------------------------------------------------------------------------------- /src/settings/components/SettingHeader.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

{title}

9 |

{description}

10 |
11 | 12 | 33 | -------------------------------------------------------------------------------- /src/settings/components/SettingItem.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /src/settings/components/ToggleSetting.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | update(!checked)} 18 | /> 19 | 20 | -------------------------------------------------------------------------------- /src/settings/index.ts: -------------------------------------------------------------------------------- 1 | import { plug } from 'src/main'; 2 | import { prepareCssSettingListener } from './CssSettingsHandler'; 3 | import { SettingsTab } from './SettingsTab'; 4 | import { rawSettings } from './store'; 5 | import { DEFAULT_SETTINGS, LENGTH_SETTINGS, TEXT_SETTINGS } from './structure'; 6 | import type { BannerSettings, LengthValue } from './structure'; 7 | import { areSettingsOutdated, updateSettings } from './updater'; 8 | 9 | export const saveSettings = async (changed: Partial = {}) => { 10 | await plug.saveData(plug.settings); 11 | rawSettings.set(plug.settings); 12 | plug.events.trigger('setting-change', changed); 13 | console.log(plug.settings); 14 | }; 15 | 16 | export const loadSettings = async () => { 17 | // Update settings from an older version if needed 18 | const data = await plug.loadData(); 19 | if (areSettingsOutdated(data)) updateSettings(data); 20 | 21 | // Apply default settings where needed 22 | const settings = Object.assign( 23 | {}, 24 | DEFAULT_SETTINGS, 25 | data, 26 | { version: plug.manifest.version } 27 | ) as BannerSettings; 28 | 29 | for (const [key, val] of Object.entries(settings) as [keyof BannerSettings, unknown][]) { 30 | if ( 31 | DEFAULT_SETTINGS[key] === val && 32 | (typeof val === 'number' || TEXT_SETTINGS.includes(key)) 33 | ) delete settings[key]; 34 | } 35 | 36 | // Load up settings and settings tab 37 | plug.settings = settings; 38 | await saveSettings(); 39 | prepareCssSettingListener(); 40 | plug.addSettingTab(new SettingsTab()); 41 | }; 42 | 43 | export const parseCssSetting = (value: LengthValue): string => ( 44 | typeof value === 'number' ? `${value}px` : value 45 | ); 46 | 47 | export const getSetting = (key: T): BannerSettings[T] => { 48 | const value = plug.settings[key] ?? DEFAULT_SETTINGS[key]; 49 | return LENGTH_SETTINGS.includes(key) 50 | ? parseCssSetting(value as string) as BannerSettings[T] 51 | : value; 52 | }; 53 | -------------------------------------------------------------------------------- /src/settings/store.ts: -------------------------------------------------------------------------------- 1 | import { derived, writable } from 'svelte/store'; 2 | import type { Writable } from 'svelte/store'; 3 | import { plug } from 'src/main'; 4 | import { DEFAULT_SETTINGS, LENGTH_SETTINGS } from './structure'; 5 | import type { BannerSettings } from './structure'; 6 | import { parseCssSetting, saveSettings } from '.'; 7 | 8 | interface Store extends Writable { 9 | updateSetting: (key: keyof BannerSettings, value: unknown) => void; 10 | } 11 | 12 | export const rawSettings: Store = { 13 | ...writable(), 14 | updateSetting: async (key, value) => { 15 | const changed = { [key]: value }; 16 | if (value !== undefined) { 17 | plug.settings = { ...plug.settings, ...changed }; 18 | } else { 19 | delete plug.settings[key]; 20 | } 21 | await saveSettings(changed); 22 | } 23 | }; 24 | 25 | export const settings = derived(rawSettings, ($settings) => { 26 | const processed = { ...DEFAULT_SETTINGS, ...$settings } as Record; 27 | let key: keyof BannerSettings; 28 | for (key in processed) { 29 | if (LENGTH_SETTINGS.includes(key)) { 30 | processed[key] = parseCssSetting(processed[key] as string); 31 | } 32 | } 33 | return processed as BannerSettings; 34 | }); 35 | 36 | export default rawSettings; 37 | -------------------------------------------------------------------------------- /src/settings/structure.ts: -------------------------------------------------------------------------------- 1 | type StyleOption = 'solid' | 'gradient'; 2 | export type LengthValue = string | number; 3 | export type BannerDragModOption = 'None' | 'Shift' | 'Ctrl' | 'Alt' | 'Meta'; 4 | type HeaderTextDecorOption = 'none' | 'shadow' | 'border'; 5 | export type HeaderHorizontalAlignmentOption = 'left' | 'center' | 'right' | 'custom'; 6 | export type HeaderVerticalAlignmentOption = 'center' | 'above' | 'edge' | 'below' | 'custom'; 7 | 8 | export interface BannerSettings { 9 | style: StyleOption; 10 | height: LengthValue; 11 | mobileHeight: LengthValue; 12 | adjustWidthToReadableLineWidth: boolean; 13 | showInInternalEmbed: boolean; 14 | internalEmbedHeight: LengthValue; 15 | showInPopover: boolean; 16 | popoverHeight: LengthValue; 17 | bannerDragModifier: BannerDragModOption; 18 | frontmatterField: string; 19 | enableDragInInternalEmbed: boolean; 20 | enableDragInPopover: boolean; 21 | enableLockButton: boolean; 22 | headerSize: LengthValue; 23 | headerDecor: HeaderTextDecorOption; 24 | headerHorizontalAlignment: HeaderHorizontalAlignmentOption; 25 | headerHorizontalTransform: LengthValue; 26 | headerVerticalAlignment: HeaderVerticalAlignmentOption; 27 | headerVerticalTransform: LengthValue; 28 | useHeaderByDefault: boolean; 29 | defaultHeaderValue: string; 30 | iconSize: LengthValue; 31 | useTwemoji: boolean; 32 | showPreviewInLocalModal: boolean; 33 | localModalSuggestionLimit: number; 34 | bannersFolder: string; 35 | autoDownloadPastedBanners: boolean; 36 | } 37 | 38 | export const FILENAME_KEY = 'filename'; 39 | 40 | export const DEFAULT_SETTINGS: BannerSettings = { 41 | style: 'solid', 42 | height: 300, 43 | mobileHeight: 180, 44 | adjustWidthToReadableLineWidth: false, 45 | showInInternalEmbed: true, 46 | internalEmbedHeight: 200, 47 | showInPopover: true, 48 | popoverHeight: 120, 49 | bannerDragModifier: 'None', 50 | frontmatterField: 'banner', 51 | enableDragInInternalEmbed: false, 52 | enableDragInPopover: false, 53 | enableLockButton: true, 54 | headerSize: 'var(--inline-title-size)', 55 | headerDecor: 'shadow', 56 | headerHorizontalAlignment: 'left', 57 | headerHorizontalTransform: '0px', 58 | headerVerticalAlignment: 'edge', 59 | headerVerticalTransform: '0px', 60 | useHeaderByDefault: false, 61 | defaultHeaderValue: `{{${FILENAME_KEY}}}`, 62 | iconSize: '1.2em', 63 | useTwemoji: true, 64 | showPreviewInLocalModal: true, 65 | localModalSuggestionLimit: 15, 66 | bannersFolder: '/', 67 | autoDownloadPastedBanners: false 68 | }; 69 | 70 | export const LENGTH_SETTINGS: Array = [ 71 | 'height', 72 | 'mobileHeight', 73 | 'internalEmbedHeight', 74 | 'popoverHeight', 75 | 'headerSize', 76 | 'headerHorizontalTransform', 77 | 'headerVerticalTransform', 78 | 'iconSize' 79 | ]; 80 | 81 | export const TEXT_SETTINGS: Array = [ 82 | ...LENGTH_SETTINGS, 83 | 'frontmatterField', 84 | 'defaultHeaderValue', 85 | 'bannersFolder' 86 | ]; 87 | 88 | const STYLE_OPTION_LABELS: Record = { 89 | solid: 'Solid', 90 | gradient: 'Gradient' 91 | }; 92 | 93 | const BANNER_DRAG_MOD_OPION_LABELS: Record = { 94 | None: 'None', 95 | Shift: '⇧ Shift', 96 | Ctrl: '⌃ Ctrl', 97 | Alt: '⎇ Alt', 98 | Meta: '⌘ Meta' 99 | }; 100 | 101 | const HEADER_TEXT_DECOR_OPTION_LABELS: Record = { 102 | none: 'None', 103 | shadow: 'Shadow behind text', 104 | border: 'Border around text' 105 | }; 106 | 107 | const HEADER_HORIZONTAL_ALIGN_OPTION_LABELS: Record = { 108 | left: 'Left', 109 | center: 'Center', 110 | right: 'Right', 111 | custom: 'Custom' 112 | }; 113 | 114 | const HEADER_VERTICAL_ALIGN_OPTION_LABELS: Record = { 115 | center: 'Center of the banner', 116 | above: 'Just above the banner', 117 | edge: 'Edge of the banner', 118 | below: 'Just below the banner', 119 | custom: 'Custom' 120 | }; 121 | 122 | export const SELECT_OPTIONS_MAP = { 123 | style: STYLE_OPTION_LABELS, 124 | bannerDragModifier: BANNER_DRAG_MOD_OPION_LABELS, 125 | headerDecor: HEADER_TEXT_DECOR_OPTION_LABELS, 126 | headerHorizontalAlignment: HEADER_HORIZONTAL_ALIGN_OPTION_LABELS, 127 | headerVerticalAlignment: HEADER_VERTICAL_ALIGN_OPTION_LABELS 128 | }; 129 | -------------------------------------------------------------------------------- /src/settings/updater.ts: -------------------------------------------------------------------------------- 1 | import { Notice } from 'obsidian'; 2 | import { plug } from 'src/main'; 3 | 4 | type SettingsData = Record & { 5 | version: string; 6 | }; 7 | 8 | type KeyChanges = Record; 9 | type ValueChanges = Record>; 10 | type Removals = string[]; 11 | 12 | interface BreakingChanges { 13 | version: string; 14 | changes: { 15 | keys?: KeyChanges; 16 | values?: ValueChanges; 17 | remove?: Removals; 18 | callbacks?: Array<() => void>; 19 | }; 20 | } 21 | 22 | const SEMVER_REGEX = /^(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)/; 23 | 24 | const breakingChanges: BreakingChanges[] = [ 25 | { 26 | version: '2.0.0', 27 | changes: { 28 | keys: { 29 | iconHorizontalAlignment: 'headerHorizontalAlignment', 30 | iconHorizontalTransform: 'headerHorizontalTransform', 31 | iconVerticalAlignment: 'headerVerticalAlignment', 32 | iconVerticalTransform: 'headerVerticalTransform', 33 | showInPreviewEmbed: 'showInPopover', 34 | previewEmbedHeight: 'popoverHeight', 35 | localSuggestionsLimit: 'localModalSuggestionLimit' 36 | }, 37 | values: { 38 | bannerDragModifier: { 39 | none: 'None', 40 | shift: 'Shift', 41 | ctrl: 'Ctrl', 42 | alt: 'Alt', 43 | meta: 'Meta' 44 | }, 45 | headerVerticalAlignment: { center: 'edge' } 46 | }, 47 | remove: ['allowMobileDrag'], 48 | callbacks: [ 49 | () => new Notice(createFragment((el) => { 50 | el.createEl('b', { text: 'Hey! Looks like you\'ve used Banners in the past. ' }); 51 | el.createEl('br'); 52 | el.createSpan({ 53 | text: 'Banners created in 1.x use an outdated syntax for internal files that ' + 54 | 'no longer work in 2.0. To update this throughout your vault, go to the bottom ' + 55 | 'of the Banners settings tab' 56 | }); 57 | }), 0) 58 | ] 59 | } 60 | } 61 | ]; 62 | 63 | const isVersionBelow = (a: string | null, b: string): boolean => { 64 | if (!a) return true; // Edge case for 1.X versions 65 | const { major: aMajor, minor: aMinor, patch: aPatch } = a.match(SEMVER_REGEX)!.groups!; 66 | const { major: bMajor, minor: bMinor, patch: bPatch } = b.match(SEMVER_REGEX)!.groups!; 67 | return (+aMajor < +bMajor) || (+aMinor < +bMinor) || (+aPatch < +bPatch); 68 | }; 69 | 70 | const updateKeys = (data: SettingsData, keys: KeyChanges) => { 71 | for (const [oldKey, newKey] of Object.entries(keys)) { 72 | if (data[oldKey]) { 73 | data[newKey] = data[oldKey]; 74 | delete data[oldKey]; 75 | } 76 | } 77 | }; 78 | 79 | const updateValue = (data: SettingsData, keys: ValueChanges) => { 80 | for (const [key, values] of Object.entries(keys)) { 81 | const oldValue = data[key] as any; 82 | if (oldValue !== undefined && Object.hasOwn(values, oldValue)) { 83 | data[key] = values[oldValue]; 84 | } 85 | } 86 | }; 87 | 88 | const removeSetting = (data: SettingsData, keys: Removals) => { 89 | for (const key of keys) { 90 | delete data[key]; 91 | } 92 | }; 93 | 94 | export const updateSettings = (data: SettingsData) => { 95 | let madeChanges = false; 96 | for (const { version, changes } of breakingChanges) { 97 | if (isVersionBelow(data.version, version)) { 98 | madeChanges = true; 99 | if (changes.keys) updateKeys(data, changes.keys); 100 | if (changes.values) updateValue(data, changes.values); 101 | if (changes.remove) removeSetting(data, changes.remove); 102 | if (changes.callbacks) changes.callbacks.forEach((cb) => cb()); 103 | } 104 | } 105 | 106 | for (const [key, val] of Object.entries(data)) { 107 | if (val === null) { 108 | madeChanges = true; 109 | delete data[key]; 110 | } 111 | } 112 | 113 | if (madeChanges) { 114 | new Notice(`Updated Banner settings from ${data.version ?? '1.x'} to ${plug.manifest.version}`); 115 | } 116 | }; 117 | 118 | export const areSettingsOutdated = (data: SettingsData | null): boolean => { 119 | if (!data) return false; 120 | return isVersionBelow(data.version, plug.manifest.version); 121 | }; 122 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import type { EditorView } from '@codemirror/view'; 3 | import type { Editor } from 'obsidian'; 4 | import type { IconString } from './bannerData'; 5 | 6 | interface EditMode { 7 | type: 'source'; 8 | editor: Editor; 9 | } 10 | 11 | interface PreviewMode { 12 | type: 'preview'; 13 | } 14 | 15 | interface MarkdownViewState { 16 | type: 'markdown'; 17 | state: { 18 | file: string; 19 | mode: 'source' | 'preview'; 20 | source: boolean; 21 | }; 22 | } 23 | 24 | interface ImageViewState { 25 | type: 'image'; 26 | state: { file: string }; 27 | } 28 | 29 | interface RenderSection { 30 | el: HTMLElement; 31 | rendered: boolean; 32 | html: string; 33 | } 34 | 35 | interface PreviewRenderer { 36 | sections: RenderSection[]; 37 | queueRender: () => void; 38 | } 39 | 40 | declare module 'obsidian' { 41 | interface Editor { 42 | cm: EditorView; 43 | } 44 | 45 | interface Keymap { 46 | modifiers: string; 47 | } 48 | 49 | interface MarkdownPostProcessorContext { 50 | containerEl: HTMLElement; 51 | } 52 | 53 | interface MarkdownFileInfo { 54 | data: string; 55 | leaf: WorkspaceLeaf; 56 | file: TFile; 57 | } 58 | 59 | interface MarkdownPreviewView { 60 | docId: string; 61 | renderer: PreviewRenderer; 62 | } 63 | 64 | interface Vault { 65 | getAvailablePathForAttachments( 66 | base: string, 67 | ext: string | null, 68 | currentPath: string 69 | ): Promise; 70 | } 71 | 72 | interface View { 73 | currentMode: EditMode | PreviewMode; 74 | editor: Editor; 75 | file: TFile; 76 | previewMode: MarkdownPreviewView; 77 | } 78 | 79 | interface WorkspaceLeaf { 80 | containerEl: HTMLElement; 81 | getViewState(): MarkdownViewState | ImageViewState; 82 | id: string; 83 | } 84 | } 85 | 86 | // Fix typings with twemoji package 87 | declare module '@twemoji/api' { 88 | const twemoji: Twemoji; 89 | // @ts-ignore 90 | export default twemoji; 91 | } 92 | 93 | declare global { 94 | interface BannerData { 95 | source: string; 96 | x: number; 97 | y: number; 98 | icon: IconString; 99 | header: string; 100 | lock: boolean; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { EventRef, WorkspaceLeaf } from 'obsidian'; 2 | import { plug } from './main'; 3 | import type { BannerSettings } from './settings/structure'; 4 | import type { MarkdownViewState } from './types'; 5 | 6 | type Settings = keyof BannerSettings | Array; 7 | 8 | // Helper to check if a leaf is a Markdown file view, and if specified, if it's a specific mode 9 | export const doesLeafHaveMarkdownMode = (leaf: WorkspaceLeaf, mode?: 'reading' | 'editing') => { 10 | const { type, state } = leaf.getViewState(); 11 | if (type !== 'markdown') return false; 12 | if (!mode) return true; 13 | return (mode === 'reading') ? (state.mode === 'preview') : (state.mode === 'source'); 14 | }; 15 | 16 | 17 | // Helper to iterate through all markdown leaves, and if specified, those with a specific view 18 | export const iterateMarkdownLeaves = ( 19 | cb: (leaf: WorkspaceLeaf) => void, 20 | mode?: 'reading' | 'editing' 21 | ) => { 22 | let leaves = plug.app.workspace.getLeavesOfType('markdown'); 23 | if (mode) { 24 | leaves = leaves.filter((leaf) => { 25 | const { state } = leaf.getViewState() as MarkdownViewState; 26 | return (mode === 'reading') ? (state.mode === 'preview') : (state.mode === 'source'); 27 | }); 28 | } 29 | for (const leaf of leaves) cb(leaf); 30 | }; 31 | 32 | // Helper to register multiple events at once 33 | export const registerEvents = (events: EventRef[]) => { 34 | for (const event of events) plug.registerEvent(event); 35 | }; 36 | 37 | // Helper to register a `setting-change` event 38 | export const registerSettingChangeEvent = (settings: Settings, cb: CallableFunction) => { 39 | const keys = typeof settings === 'string' ? [settings] : settings; 40 | plug.registerEvent( 41 | plug.events.on('setting-change', (changed) => { 42 | if (keys.some((key) => Object.hasOwn(changed, key))) cb(); 43 | }) 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "baseUrl": ".", 7 | "esModuleInterop": true, 8 | "importHelpers": true, 9 | "lib": ["dom", "ES2022"], 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "noImplicitAny": true, 13 | "resolveJsonModule": true, 14 | "target": "ESNext" 15 | }, 16 | "types": ["svelte", "node"], 17 | "include": ["**/*.ts", "**/*.svelte"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.12.12", 3 | "1.3.0": "0.13.25", 4 | "1.3.1": "0.13.21", 5 | "2.0.0-beta": "1.4.4", 6 | "2.0.3-beta": "1.4.11" 7 | } --------------------------------------------------------------------------------