initial code commit

This commit is contained in:
Sandro Ducceschi 2025-04-27 21:33:41 +02:00
parent 3bc549e519
commit e8e21ddee8
18 changed files with 4365 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 2
tab_width = 2

1
.env.dist Normal file
View file

@ -0,0 +1 @@
VAULT_PLUGIN_PATH=/path/to/your/.obsidian/plugins/your-plugin

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules/
main.js

23
.eslintrc Normal file
View file

@ -0,0 +1,23 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

34
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
dist/main.js manifest.json dist/styles.css

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
.vscode
.idea
.DS_Store
.env
*.iml
*.map
node_modules
dist
data.json

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

30
assets/snippet.css Normal file
View file

@ -0,0 +1,30 @@
/* -------------------------------
Source / Live View Styles
------------------------------- */
/* Styles the cells of a table with calculated values */
div.markdown-rendered table.table-editor tr > td.stm-cell.stm-value,
div.markdown-rendered table.table-editor tr > td.stm-cell > .stm-value {
padding: var(--size-2-2) var(--size-4-2); /* must be the same as the original .table-cell-wrapper */
font-weight: calc(var(--font-weight) + var(--bold-modifier));
}
/* Styles the last row of a table with calculated values */
div.markdown-rendered table.table-editor tr.stm-row:last-of-type:not(.off) {
background-color: var(--background-secondary);
}
/* -------------------------------
Reading View Styles
------------------------------- */
/* Styles the cells of a table with calculated values */
div.el-table > table tr.stm-row > td.stm-cell.stm-value,
div.el-table > table tr.stm-row > td.stm-cell > .stm-value {
font-weight: calc(var(--font-weight) + var(--bold-modifier));
}
/* Styles the last row of a table with calculated values */
div.el-table > table tr.stm-row:last-of-type:not(.off) {
background-color: var(--background-secondary);
}

72
esbuild.config.mjs Normal file
View file

@ -0,0 +1,72 @@
import esbuild from 'esbuild';
import process from 'process';
import builtins from 'builtin-modules';
import { sassPlugin } from 'esbuild-sass-plugin';
import { cp } from 'node:fs/promises';
import 'dotenv/config';
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === 'production');
const vaultCopy = {
name: 'vault-copy',
setup(build) {
build.onEnd(async () => {
const vaultPluginPath = process.env.VAULT_PLUGIN_PATH;
if (vaultPluginPath) {
try {
await Promise.all([
cp('dist/main.js', `${vaultPluginPath}/main.js`, { overwrite: true }),
cp('manifest.json', `${vaultPluginPath}/manifest.json`, { overwrite: true }),
cp('dist/styles.css', `${vaultPluginPath}/styles.css`, { overwrite: true }),
]);
} catch (copyError) {
console.error('Error copying files:', copyError);
}
}
});
},
};
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ['src/main.ts', 'src/styles.scss'],
bundle: true,
external: [
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins],
format: 'cjs',
target: 'es2018',
logLevel: 'info',
sourcemap: prod ? false : 'inline',
treeShaking: true,
outdir: 'dist',
plugins: [sassPlugin(), vaultCopy],
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "simple-banner",
"name": "Simple Banner",
"version": "0.1.0",
"minAppVersion": "1.8.9",
"description": "Simply add a banner image.",
"author": "Sandro Ducceschi",
"authorUrl": "https://eatcodeplay.dev",
"isDesktopOnly": false
}

3104
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

33
package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "simple-banner",
"version": "0.1.0",
"description": "Simply add a banner image",
"main": "src/main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"test": "tsc -noEmit -skipLibCheck",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"engines": {
"node": ">=20.x"
},
"keywords": [],
"author": "Sandro Ducceschi",
"license": "MIT",
"devDependencies": {
"@types/lodash": "^4.17.16",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"dotenv": "^16.5.0",
"esbuild": "0.17.3",
"esbuild-sass-plugin": "2.4.5",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
}
}

View file

@ -0,0 +1,7 @@
import {BaseComponent} from "obsidian";
export class ContainerComponent extends BaseComponent {
constructor() {
super();
}
}

676
src/main.ts Normal file
View file

@ -0,0 +1,676 @@
import {
App,
debounce,
MarkdownView,
Platform,
Plugin,
PluginSettingTab,
Setting,
TFile,
Workspace,
WorkspaceLeaf,
} from 'obsidian';
//----------------------------------
// Interfaces
//----------------------------------
interface SimpleBannerSettings {
offset: number;
radius: Array<number>;
padding: number;
fade: boolean;
desktopHeight: number;
tabletHeight: number;
mobileHeight: number;
propertyName: string;
}
const DEFAULT_SETTINGS: SimpleBannerSettings = {
offset: -32,
radius: [8, 8, 8, 8],
padding: 8,
fade: true,
desktopHeight: 240,
mobileHeight: 160,
tabletHeight: 190,
propertyName: 'banner',
}
interface BannerData {
path: string | null;
value: string | null;
isNewValue: boolean;
isUpdate: boolean;
viewMode: ViewMode | null;
lastViewMode: ViewMode | null;
container: HTMLElement | null;
}
interface BannerOptions {
x: number;
y: number;
repeatable: boolean;
}
enum ViewMode {
Source = 'source',
Reading = 'preview',
}
export default class SimpleBanner extends Plugin {
//---------------------------------------------------
//
// Variables
//
//---------------------------------------------------
workspace: Workspace;
settings: SimpleBannerSettings;
boundProcess: () => void;
//---------------------------------------------------
//
// Plugin Lifecycle
//
//---------------------------------------------------
async onload() {
this.workspace = this.app.workspace;
await this.loadSettings();
this.addSettingTab(new SettingTab(this.app, this));
this.app.workspace.onLayoutReady(() => {
this.applySettings();
this.boundProcess = debounce(this.process.bind(this), 50);
this.registerEvent(this.workspace.on('layout-change', this.boundProcess));
this.registerEvent(this.workspace.on('file-open', this.handleFileEvents.bind(this)));
this.registerEvent(this.app.metadataCache.on('changed', this.handleMetaEvents.bind(this)));
this.processAll();
});
}
onunload() {
}
//---------------------------------------------------
//
// Methods
//
//---------------------------------------------------
processAll() {
this.app.workspace.iterateRootLeaves((leaf: WorkspaceLeaf) => {
const view = leaf.view as MarkdownView;
if (view) {
const file = view.file as TFile || null;
const options = this.computeBannerData(file, view);
this.process(options || null);
}
});
}
process(options?: BannerData | null) {
const opts = options || this.computeBannerData();
if (!opts) {
return;
}
if (!opts.value) {
this.removeBanner();
return;
}
this.updateBanner(opts);
}
updateBanner(options: BannerData) {
const propsContainer = document.querySelector('.metadata-container');
if (propsContainer) {
const parent = propsContainer.parentNode;
if (parent && parent.firstChild !== propsContainer) {
parent.prepend(propsContainer);
}
}
const { value, isNewValue, isUpdate, viewMode, lastViewMode, container } = options;
if (lastViewMode !== viewMode || isNewValue) {
if (container && isNewValue) {
const view = this.getActiveView();
const containers = container.querySelectorAll('.markdown-preview-view, .cm-scroller');
containers.forEach((c) => {
let element = (c.querySelector('.simple-banner') || document.createElement('div')) as HTMLElement;
element.classList.add('simple-banner');
if (isNewValue) {
const { url, repeatable, x, y } = this.parseLinkString(value || '', view);
element.classList.remove('static');
this.setCSSVariable('--smpbn-url', `url(${url}`, container);
this.setCSSVariable('--smpbn-img-x', `${x}px`, container);
this.setCSSVariable('--smpbn-img-y', `${y}px`, container);
this.setCSSVariable('--smpbn-size', repeatable ? 'auto' : 'cover', container);
this.setCSSVariable('--smpbn-repeat', repeatable ? 'repeat' : 'no-repeat', container);
if (isUpdate) {
element.classList.add('static');
} else {
c.prepend(element);
element.onanimationend = () => {
const banners = document.querySelectorAll('.simple-banner');
banners.forEach(b => b.classList.add('static'));
}
}
}
});
options.isNewValue = false;
options.lastViewMode = viewMode;
container.dataset.sb = JSON.stringify(options);
}
}
}
removeBanner() {
const options = this.computeBannerData();
const container = options?.container;
if (options && container) {
const targets = container.querySelectorAll('.simple-banner');
targets.forEach((t) => { t.remove() });
options.lastViewMode = null;
options.value = null;
delete container.dataset.sb;
}
}
computeBannerData(newfile?: TFile, targetView?: MarkdownView): BannerData | null {
const view = targetView || this.getActiveView();
if (view instanceof MarkdownView) {
const viewMode = view.getMode() || null;
const defaultOptions = this.createDefaultBannerData();
let oldopt: BannerData | null = defaultOptions;
const container = view.containerEl;
if (container?.dataset.sb) {
oldopt = JSON.parse(container.dataset.sb) as BannerData || defaultOptions;
}
if (newfile && newfile.path !== view?.file?.path) {
oldopt = defaultOptions;
}
const opt = {
path: null,
value: null,
isNewValue: false,
isUpdate: false,
viewMode: (viewMode === ViewMode.Reading) ? ViewMode.Reading : ViewMode.Source,
lastViewMode: oldopt.viewMode,
container: view.containerEl,
} as BannerData;
const file = view?.file || null;
if (file) {
const cachedMetadata = this.app.metadataCache.getFileCache(file);
const propName = this.settings.propertyName;
const frontmatter = cachedMetadata?.frontmatter;
if (frontmatter && frontmatter[propName]) {
opt.value = frontmatter[propName];
opt.path = file.path;
if (oldopt.value !== opt.value) {
opt.isNewValue = true;
if (this.isOptionsUpdate(oldopt.value, opt.value, view)) {
opt.isUpdate = true;
}
}
}
}
return opt;
}
return null;
}
//----------------------------------
// Settings Methods
//----------------------------------
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
this.applySettings();
}
applySettings() {
let height = this.settings.desktopHeight;
if (Platform.isTablet) {
height = this.settings.tabletHeight;
} else if (Platform.isMobile) {
height = this.settings.mobileHeight;
}
const offset = this.settings.offset;
const radius = this.settings.radius;
const padding = this.settings.padding;
const fade = this.settings.fade;
this.setCSSVariable('--smpbn-height', `${height}px`);
this.setCSSVariable('--smpbn-note-offset', `${offset}px`);
this.setCSSVariable('--smpbn-radius', `${radius[0]}px ${radius[1]}px ${radius[2]}px ${radius[3]}px`);
this.setCSSVariable('--smpbn-padding', `${padding}px`);
this.setCSSVariable('--smpbn-fade', 'none');
if (fade) {
this.setCSSVariable(
'--smpbn-fade',
`linear-gradient(to bottom, black calc(100% + -75%), transparent)`
);
}
}
//----------------------------------
// Event Handlers
//----------------------------------
handleFileEvents(file: TFile) {
const options = this.computeBannerData(file);
const container = options?.container;
if (options && container && file.path !== options.path) {
options.lastViewMode = null;
delete container.dataset.sb;
this.boundProcess();
}
}
async handleMetaEvents(file: TFile) {
this.app.workspace.iterateRootLeaves((leaf: WorkspaceLeaf) => {
const view = leaf.view as MarkdownView;
if (view.file === file) {
const options = this.computeBannerData(file, view);
this.process(options || null);
}
});
}
//----------------------------------
// Helper Methods
//----------------------------------
getActiveView(): MarkdownView | null {
return this.workspace.getActiveViewOfType(MarkdownView) || null;
}
parseLinkString(str: string, view?: MarkdownView | null) {
let url: string | null = null;
let displayText: string | null = null;
let external: boolean = false;
let obsidianUrl: boolean = false;
let options = { x: 0, y: 0, repeatable: false };
const wikilinkRegex = /^!?\[\[([^\]]+?)(\|([^\]]+?))?\]\]$/;
const wikilinkMatch = str.match(wikilinkRegex);
if (wikilinkMatch) {
url = wikilinkMatch[1].trim();
displayText = wikilinkMatch[3] ? wikilinkMatch[3].trim() : null;
}
const markdownLinkRegex = /^!?\[([^\]]*)\]\(([^)]+?)\)$/;
const markdownBareLinkRegex = /^!?<([^>]+)>$/;
const markdownMatch = str.match(markdownLinkRegex);
const markdownBareMatch = str.match(markdownBareLinkRegex);
if (markdownMatch) {
displayText = markdownMatch[1].trim();
url = markdownMatch[2].trim();
} else if (markdownBareMatch) {
url = markdownBareMatch[1].trim();
displayText = null;
}
if (!url) {
url = str;
displayText = null;
}
external = /^https?:\/\//i.test(url);
if (url.startsWith('obsidian://open')) {
const str = url.replace('obsidian://open', '');
const params = new URLSearchParams(str);
let file = params.get('file');
if (file) {
url = file;
obsidianUrl = true;
external = false;
displayText = null;
}
}
const hashIndex = url.indexOf('#');
if ((external || obsidianUrl) && hashIndex !== -1) {
options = this.parseBannerOptions(url.substring(hashIndex + 1));
url = url.replace(/#.*/, '').trim();
}
if (displayText) {
options = this.parseBannerOptions(displayText);
}
if (!external) {
const vault = this.app.vault;
const files = vault.getFiles().filter(f => f.path === url || f.name === url);
let file = files.find(f => f.path === url);
if (file) {
url = vault.getResourcePath(file);
}
if (!file) {
file = files.find(f => f.name === url);
if (file) {
url = vault.getResourcePath(file);
}
}
if (obsidianUrl && file && view) {
const activeFile = this.workspace.getActiveFile();
if (activeFile) {
this.app.fileManager.processFrontMatter(activeFile, (frontmatter) => {
const propName = this.settings.propertyName;
frontmatter[propName] = `[[${file?.path}]]`
});
}
}
}
return {
url: url.trim(),
external,
...options,
};
}
parseBannerOptions(str: string): BannerOptions {
let repeatable: boolean;
let x = 0;
let y = 0;
const values = str.toLowerCase();
repeatable = values.includes('repeat');
const sizes = str.split(/x|,/);
const numbers = sizes.filter(v => !isNaN(parseInt(v.trim(), 10)));
if (numbers.length === 2) {
x = parseInt(numbers[0].trim(), 10);
y = parseInt(numbers[1].trim(), 10);
} else if (numbers.length === 1) {
y = parseInt(numbers[0].trim(), 10);
}
return { x, y, repeatable };
}
isOptionsUpdate(oldstr?: string | null, newstr?: string | null, view?: MarkdownView | null): boolean {
if (!oldstr || !newstr) {
return false;
}
const oldopt = this.parseLinkString(oldstr, view);
const newopt = this.parseLinkString(newstr, view);
return oldopt.url === newopt.url;
}
setCSSVariable(name: string, value: string, target: HTMLElement = document.documentElement) {
target.style.setProperty(name, value);
}
createDefaultBannerData(): BannerData {
return {
path: null,
value: null,
isNewValue: false,
isUpdate: false,
viewMode: null,
lastViewMode: null,
container: null,
}
}
}
/**
* Represents the settings tab for configuring the SimpleBanner plugin.
*
*/
class SettingTab extends PluginSettingTab {
plugin: SimpleBanner;
constructor(app: App, plugin: SimpleBanner) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
// GLOBALS
new Setting(containerEl)
.setHeading()
.setName('Global Settings');
new Setting(containerEl)
.setName('Note Offset')
.setDesc('Move the position of the notes content in pixels.')
.addExtraButton(button => button
.setIcon('rotate-ccw')
.setTooltip('Restore default')
.onClick(async () => {
this.plugin.settings.offset = DEFAULT_SETTINGS.offset;
await this.plugin.saveSettings();
this.display();
})
)
.addText(text => text
.setPlaceholder('Enter a number')
.setValue(this.plugin.settings.offset.toString())
.onChange(async (value) => {
let num = parseInt(value, 10);
if (isNaN(num)) {
num = DEFAULT_SETTINGS.offset;
}
this.plugin.settings.offset = num;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Border Radius')
.setDesc('Size of the border radius in pixels.')
.addExtraButton(button => button
.setIcon('rotate-ccw')
.setTooltip('Restore default')
.onClick(async () => {
this.plugin.settings.radius = DEFAULT_SETTINGS.radius;
await this.plugin.saveSettings();
this.display();
})
)
.addText(text => text
.setPlaceholder('8')
.setValue(this.plugin.settings.radius[0].toString())
.onChange(async (value) => {
let num = parseInt(value, 10);
if (isNaN(num)) {
num = DEFAULT_SETTINGS.radius[0];
}
this.plugin.settings.radius[0] = num;
await this.plugin.saveSettings();
})
)
.addText(text => text
.setPlaceholder('8')
.setValue(this.plugin.settings.radius[1].toString())
.onChange(async (value) => {
let num = parseInt(value, 10);
if (isNaN(num)) {
num = DEFAULT_SETTINGS.radius[1];
}
this.plugin.settings.radius[1] = num;
await this.plugin.saveSettings();
})
)
.addText(text => text
.setPlaceholder('8')
.setValue(this.plugin.settings.radius[2].toString())
.onChange(async (value) => {
let num = parseInt(value, 10);
if (isNaN(num)) {
num = DEFAULT_SETTINGS.radius[2];
}
this.plugin.settings.radius[2] = num;
await this.plugin.saveSettings();
})
)
.addText((text) => {
text.setPlaceholder('8')
.setValue(this.plugin.settings.radius[3].toString())
.onChange(async (value) => {
let num = parseInt(value, 10);
if (isNaN(num)) {
num = DEFAULT_SETTINGS.radius[3];
}
this.plugin.settings.radius[3] = num;
await this.plugin.saveSettings();
});
text?.inputEl?.parentElement?.classList.add('smpbn-radii');
return text;
});
new Setting(containerEl)
.setName('Padding')
.setDesc('Padding of the image from the edges of the note in pixels.')
.addExtraButton(button => button
.setIcon('rotate-ccw')
.setTooltip('Restore default')
.onClick(async () => {
this.plugin.settings.padding = DEFAULT_SETTINGS.padding;
await this.plugin.saveSettings();
this.display();
})
)
.addText(text => text
.setPlaceholder('Enter a number')
.setValue(this.plugin.settings.padding.toString())
.onChange(async (value) => {
let num = parseInt(value, 10);
if (isNaN(num)) {
num = DEFAULT_SETTINGS.padding;
}
this.plugin.settings.padding = num;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Fade')
.setDesc('Fade the image out towards the content.')
.addToggle(component => component
.setValue(this.plugin.settings.fade)
.onChange(async (value) => {
this.plugin.settings.fade = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setHeading()
.setName('Height Settings');
new Setting(containerEl)
.setName('Desktop')
.setDesc('Height of the Banner on desktop devices (in pixels).')
.addExtraButton(button => button
.setIcon('rotate-ccw')
.setTooltip('Restore default')
.onClick(async () => {
this.plugin.settings.desktopHeight = DEFAULT_SETTINGS.desktopHeight;
await this.plugin.saveSettings();
this.display();
})
)
.addText(text => text
.setPlaceholder('Enter a number')
.setValue(this.plugin.settings.desktopHeight.toString())
.onChange(async (value) => {
let num = parseInt(value, 10);
if (isNaN(num)) {
num = DEFAULT_SETTINGS.desktopHeight;
}
this.plugin.settings.desktopHeight = num;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Tablet')
.setDesc('Height of the Banner on tablet devices (in pixels).')
.addExtraButton(button => button
.setIcon('rotate-ccw')
.setTooltip('Restore default')
.onClick(async () => {
this.plugin.settings.tabletHeight = DEFAULT_SETTINGS.tabletHeight;
await this.plugin.saveSettings();
this.display();
})
)
.addText(text => text
.setPlaceholder('Enter a number')
.setValue(this.plugin.settings.tabletHeight.toString())
.onChange(async (value) => {
let num = parseInt(value, 10);
if (isNaN(num)) {
num = DEFAULT_SETTINGS.tabletHeight;
}
this.plugin.settings.tabletHeight = num;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Mobile')
.setDesc('Height of the Banner on mobile devices (in pixels).')
.addExtraButton(button => button
.setIcon('rotate-ccw')
.setTooltip('Restore default')
.onClick(async () => {
this.plugin.settings.mobileHeight = DEFAULT_SETTINGS.mobileHeight;
await this.plugin.saveSettings();
this.display();
})
)
.addText(text => text
.setPlaceholder('Enter a number')
.setValue(this.plugin.settings.mobileHeight.toString())
.onChange(async (value) => {
let num = parseInt(value, 10);
if (isNaN(num)) {
num = DEFAULT_SETTINGS.mobileHeight;
}
this.plugin.settings.mobileHeight = num;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setHeading()
.setName('Frontmatter Settings');
new Setting(containerEl)
.setName('Property Name')
.setDesc('Name of the property this plugin should look for in the frontmatter.')
.addExtraButton(button => button
.setIcon('rotate-ccw')
.setTooltip('Restore default')
.onClick(async () => {
this.plugin.settings.propertyName = DEFAULT_SETTINGS.propertyName;
await this.plugin.saveSettings();
this.display();
})
)
.addText(text => text
.setPlaceholder('Default: banner')
.setValue(this.plugin.settings.propertyName.toString())
.onChange(async (value) => {
this.plugin.settings.propertyName = (value !== '') ? value : DEFAULT_SETTINGS.propertyName;
await this.plugin.saveSettings();
})
);
}
}

307
src/styles.scss Normal file
View file

@ -0,0 +1,307 @@
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes move {
from {
background-position-y: calc(50% + (var(--smpbn-img-y, 0) - 10px));
}
to {
background-position-y: calc(50% + var(--smpbn-img-y, 0));
}
}
.view-content {
.cm-scroller, .markdown-preview-view {
padding-top: 0;
.cm-contentContainer {
position: relative;
z-index: 1;
}
.el-pre.mod-frontmatter.mod-ui {
padding-top: 0;
}
}
}
.view-content > .markdown-source-view.mod-cm6 > .cm-editor > .cm-scroller,
.view-content .markdown-preview-view {
.cm-contentContainer {
position: relative;
z-index: 1;
}
.el-pre.mod-frontmatter.mod-ui {
padding-top: 0;
}
&:has(.simple-banner) {
padding-top: 0;
.metadata-container {
position: absolute;
container-type: normal;
margin-left: calc(-1 * var(--size-4-5));
padding: var(--size-4-2) var(--size-4-6) var(--size-4-6) var(--size-4-6);
z-index: 2;
width: 100%;
max-width: calc(var(--file-line-width) + (var(--size-4-6) * 2));
&:before {
content: '';
position: absolute;
inset: 0;
width: 100%;
height: 100%;
left: 0;
background-color: color-mix(in srgb, var(--background-primary) 90%, transparent);
backdrop-filter: blur(5px);
opacity: 1;
z-index: -1;
border-radius: 0 0 var(--radius-m) var(--radius-m);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.65);
transition: opacity 0.2s linear;
}
&.is-collapsed {
.metadata-properties-heading {
opacity: 0.5;
cursor: pointer;
transition: opacity 0.2s linear;
& > .metadata-properties-title {
opacity: 0;
transition: opacity 0.2s linear;
}
}
&:before {
background-color: rgba(var(--background-primary), 0);
box-shadow: 0 4px 0 rgba(0, 0, 0, 0);
opacity: 0;
}
}
&:hover, &:focus, &:focus-within {
.metadata-properties-heading.is-collapsed {
opacity: 1;
&:before {
opacity: 0;
}
& > .metadata-properties-title {
opacity: 1;
}
}
}
}
.cm-contentContainer {
position: relative;
z-index: 1;
padding-top: 190px;
}
.el-pre.mod-frontmatter.mod-ui {
padding-top: 210px;
}
}
}
.workspace-leaf-content[data-sb] .view-content > .markdown-source-view.mod-cm6 > .cm-editor > .cm-scroller,
.workspace-leaf-content[data-sb] .view-content .markdown-preview-view {
padding-top: 0;
.metadata-container {
position: absolute;
container-type: normal;
left: calc(50% + (var(--size-4-5) * 1));
transform: translateX(-50%);
padding: var(--size-4-2) var(--size-4-6) var(--size-4-6) var(--size-4-6);
z-index: 2;
width: 100%;
.metadata-properties-heading {
width: 100%;
transform: translateY(0);
transition: opacity 0.2s linear, transform 0.2s ease-in-out;
cursor: pointer;
}
&:after {
content: '';
position: absolute;
inset: 0;
height: 2px;
width: 100%;
opacity: 0.1;
background-color: black;
box-shadow: 0 2px 2px black;
transition: all 0.2s linear;
}
&:before {
content: '';
position: absolute;
inset: 0;
width: 100%;
height: 100%;
left: 0;
background-color: color-mix(in srgb, var(--background-primary) 90%, transparent);
backdrop-filter: blur(5px);
opacity: 1;
z-index: -1;
border-radius: 0 0 var(--radius-m) var(--radius-m);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.4);
transform: translateY(0);
transition: opacity 0.2s linear, transform 0.2s ease-in-out;
}
&.is-collapsed {
.metadata-properties-heading {
opacity: 0.5;
transform: translateY(-70px);
& > .metadata-properties-title {
opacity: 0;
transition: opacity 0.2s linear;
}
}
&:after {
opacity: 0;
box-shadow: 0 2px 2px black;
}
&:before {
opacity: 0;
transform: translateY(-70px);
}
}
&:hover, &:focus, &:focus-within {
&.is-collapsed {
&:before {
transform: translateY(-26px);
opacity: 0.85;
}
&:after {
opacity: 0.1;
}
}
.metadata-properties-heading.is-collapsed {
opacity: 1;
transform: translateY(0);
&:before {
opacity: 0;
}
& > .metadata-properties-title {
opacity: 1;
}
}
}
}
.cm-contentContainer {
position: relative;
z-index: 1;
padding-top: calc(var(--smpbn-height, 240px) + (var(--smpbn-note-offset) - 6px));
}
.el-pre.mod-frontmatter.mod-ui {
padding-top: calc(var(--smpbn-height, 240px) + (var(--smpbn-note-offset) + 12px));
}
}
body.show-inline-title .workspace-leaf-content[data-sb] .view-content > .markdown-source-view.mod-cm6 > .cm-editor > .cm-scroller,
body.show-inline-title .workspace-leaf-content[data-sb] .view-content .markdown-preview-view {
.cm-contentContainer {
padding-top: 0;
}
.inline-title {
padding-top: calc(var(--smpbn-height, 240px) + (var(--smpbn-note-offset) + 12px));
}
.el-pre.mod-frontmatter.mod-ui {
padding-top: 0;
}
}
div.simple-banner {
position: absolute;
inset: 0;
z-index: 1;
width: calc(100% - (var(--smpbn-padding, 8px) * 2));
height: 100%;
margin-left: var(--smpbn-padding, 8px);
max-height: var(--smpbn-height, 240px);
border-radius: var(--smpbn-radius, 0px);
mask-image: var(--smpbn-fade);
user-select: none;
overflow: hidden;
&:before {
position: absolute;
inset: 0;
z-index: -1;
width: 100%;
height: 100%;
content: '';
box-shadow: inset 0 2px 4px 1px rgba(0, 0, 0, 0.1);
border-radius: var(--smpbn-radius, 0px);
background-color: rgba(0,0,0,.2);
}
&:after {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
content: '';
background-image: var(--smpbn-url);
background-repeat: var(--smpbn-repeat, no-repeat);
background-size: var(--smpbn-size, cover);
background-position-x: calc(50% + var(--smpbn-img-x, 0));
background-position-y: calc(50% + var(--smpbn-img-y, 0));
animation: fade 0.6s linear forwards, move 0.8s ease-out;
transition: background-position-x 0.5s ease-in-out, background-position-y 0.5s ease-in-out;
will-change: transform, background-position-x, background-position-y;
transform-style: preserve-3d;
image-rendering: crisp-edges;
backface-visibility: hidden;
}
&.static:after {
animation: none;
}
& + div {
position: relative;
z-index: 2;
}
}
.setting-item-control.smpbn-radii {
display: grid;
grid-template-columns: auto auto auto; /* Three columns */
grid-template-rows: auto auto; /* Two rows */
& > *:nth-child(1) {
grid-column: 1; /* Spans the first column */
grid-row: 1 / 3; /* Spans from the first to the third row */
}
& > *:nth-child(2) {
grid-column: 2;
grid-row: 1;
}
& > *:nth-child(3) {
grid-column: 3;
grid-row: 1;
}
& > *:nth-child(4) {
grid-column: 2;
grid-row: 2;
}
& > *:nth-child(5) {
grid-column: 3;
grid-row: 2;
}
& > input {
max-width: 78.2px;
text-align: center;
}
}

28
tsconfig.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
],
"exclude": [
"node_modules"
],
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"0.1.0": "1.8.9"
}