mirror of
https://github.com/eatcodeplay/obsidian-simple-banner.git
synced 2026-07-22 12:40:33 +00:00
major refactor
- added data store for banners - better css variable handling - more modularity - reduced redundancy - better performance
This commit is contained in:
parent
83a66ec233
commit
4374ba0b35
21 changed files with 1572 additions and 1438 deletions
|
|
@ -2,16 +2,17 @@ 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 { cp, readFile } 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 getBanner = async () => {
|
||||
const manifest = JSON.parse(await readFile('./manifest.json', 'utf-8'));
|
||||
return `/**
|
||||
* Simple Banner v${manifest.version}
|
||||
* @author ${manifest.author}
|
||||
* @url ${manifest.authorUrl}
|
||||
*/`;
|
||||
};
|
||||
const prod = (process.argv[2] === 'production');
|
||||
const vaultCopy = {
|
||||
name: 'vault-copy',
|
||||
|
|
@ -21,9 +22,9 @@ const vaultCopy = {
|
|||
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 }),
|
||||
cp('dist/main.js', `${vaultPluginPath}/main.js`, { overwrite: true }),
|
||||
cp('dist/main.css', `${vaultPluginPath}/styles.css`, { overwrite: true }),
|
||||
]);
|
||||
} catch (copyError) {
|
||||
console.error('Error copying files:', copyError);
|
||||
|
|
@ -35,9 +36,12 @@ const vaultCopy = {
|
|||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
js: await getBanner(),
|
||||
},
|
||||
entryPoints: ['src/main.ts', 'src/styles.scss'],
|
||||
entryPoints: [
|
||||
'src/main.ts',
|
||||
'src/main.scss',
|
||||
],
|
||||
bundle: true,
|
||||
external: [
|
||||
'obsidian',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "simple-banner",
|
||||
"version": "0.2.0",
|
||||
"description": "Simply add a banner image and an icon",
|
||||
"main": "src/main.js",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"test": "tsc -noEmit -skipLibCheck",
|
||||
|
|
|
|||
33
src/data/store.ts
Normal file
33
src/data/store.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { BannerData, Datastore } from '../types/interfaces';
|
||||
|
||||
|
||||
|
||||
const storage = {} as Datastore;
|
||||
|
||||
export default class Store {
|
||||
static get(id: string): BannerData | null {
|
||||
return storage[id] || null;
|
||||
}
|
||||
|
||||
static set(id: string, data: BannerData) {
|
||||
storage[id] = data;
|
||||
}
|
||||
|
||||
static delete(id: string | null = null) {
|
||||
if (id) {
|
||||
delete storage[id];
|
||||
}
|
||||
}
|
||||
|
||||
static exists(id: string): boolean {
|
||||
return storage[id] !== undefined;
|
||||
}
|
||||
|
||||
static getAll(): Datastore {
|
||||
return storage;
|
||||
}
|
||||
|
||||
static getIds(): string[] {
|
||||
return Object.keys(storage);
|
||||
}
|
||||
}
|
||||
50
src/feature/banner.ts
Normal file
50
src/feature/banner.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { BannerData, ImageOptions } from '../types/interfaces';
|
||||
import { CSSClasses, CSSValue } from '../types/enums';
|
||||
import DomUtils from '../utils/domutils';
|
||||
|
||||
const MAIN_SELECTOR = `.${CSSClasses.Main}`;
|
||||
|
||||
export default class Banner {
|
||||
static update(data: BannerData, imgOptions: ImageOptions, containers: NodeListOf<HTMLElement>): HTMLElement[] {
|
||||
const { isImageChange, isImagePropsUpdate } = data;
|
||||
const banners: HTMLElement[] = [];
|
||||
|
||||
containers.forEach(container => {
|
||||
let element = (container.querySelector(MAIN_SELECTOR) || document.createElement('div')) as HTMLElement;
|
||||
element.classList.add(CSSClasses.Main);
|
||||
banners.push(element);
|
||||
|
||||
if (isImageChange || isImagePropsUpdate) {
|
||||
if (isImageChange) {
|
||||
element.classList.remove(CSSClasses.Static);
|
||||
}
|
||||
|
||||
DomUtils.setCSSVariables({
|
||||
'url': `url(${imgOptions.url})`,
|
||||
'img-x': `${imgOptions.x}px`,
|
||||
'img-y': `${imgOptions.y}px`,
|
||||
'size': imgOptions.repeatable ? CSSValue.Auto : CSSValue.RevertLayer,
|
||||
'repeat': imgOptions.repeatable ? CSSValue.Repeat : CSSValue.RevertLayer,
|
||||
}, container);
|
||||
}
|
||||
});
|
||||
return banners;
|
||||
}
|
||||
|
||||
static insert(banners: HTMLElement[], containers: NodeListOf<HTMLElement>): void {
|
||||
containers.forEach((container, index) => {
|
||||
const banner: HTMLElement = banners[index];
|
||||
container.prepend(banner);
|
||||
banner.onanimationend = () => {
|
||||
const instances = document.querySelectorAll(MAIN_SELECTOR);
|
||||
instances.forEach(i => i.classList.add(CSSClasses.Static));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static replace(banners: HTMLElement[]): void {
|
||||
banners.forEach((banner) => {
|
||||
banner.classList.add(CSSClasses.Static);
|
||||
})
|
||||
}
|
||||
}
|
||||
51
src/feature/icon.ts
Normal file
51
src/feature/icon.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { Platform } from 'obsidian';
|
||||
import { BannerData, DeviceSettings } from '../types/interfaces';
|
||||
import { CSSClasses, IconType } from '../types/enums';
|
||||
import DomUtils from '../utils/domutils';
|
||||
import Parse from '../utils/parse';
|
||||
|
||||
export default class Icon {
|
||||
static update(data: BannerData, banners: HTMLElement[], settings: DeviceSettings) {
|
||||
let calculatedFontSize: string | null = null;
|
||||
banners.forEach((banner) => {
|
||||
const { icon, view } = data;
|
||||
let iconContainer = banner.querySelector(`.${CSSClasses.Icon}`) || null;
|
||||
const hadIconContainer = iconContainer !== null;
|
||||
if (hadIconContainer) {
|
||||
iconContainer?.classList.add(CSSClasses.Static);
|
||||
}
|
||||
if (settings.iconEnabled && icon) {
|
||||
if (!hadIconContainer) {
|
||||
iconContainer = document.createElement('div');
|
||||
iconContainer.classList.add(CSSClasses.Icon);
|
||||
if (Platform.isWin) {
|
||||
iconContainer.classList.add(CSSClasses.IsWindows);
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
iconContainer.appendChild(div);
|
||||
banner.prepend(iconContainer);
|
||||
}
|
||||
|
||||
if (iconContainer) {
|
||||
const iconelement = iconContainer.querySelector('div') as HTMLElement;
|
||||
|
||||
let { value, type } = Parse.icon(icon, view);
|
||||
value = value?.replace(/([#.:[\\]"])/g, '\\$1') || '';
|
||||
iconelement.dataset.type = type;
|
||||
|
||||
const iconVars = {} as any;
|
||||
iconVars['icon-value'] = type === IconType.Link ? `url(${value})` : `"${value}"`;
|
||||
|
||||
if (type === IconType.Text) {
|
||||
calculatedFontSize = calculatedFontSize ? calculatedFontSize : DomUtils.calculateFontsize(value);
|
||||
iconVars['icon-fontsize'] = calculatedFontSize;
|
||||
}
|
||||
DomUtils.setCSSVariables(iconVars, iconelement);
|
||||
}
|
||||
} else if (iconContainer) {
|
||||
data.icon = null;
|
||||
iconContainer.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
6
src/main.scss
Normal file
6
src/main.scss
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
@use 'scss/variables';
|
||||
@use 'scss/animations';
|
||||
@use 'scss/banner';
|
||||
@use 'scss/icon';
|
||||
@use 'scss/frontmatter';
|
||||
@use 'scss/settings';
|
||||
569
src/main.ts
569
src/main.ts
|
|
@ -1,62 +1,13 @@
|
|||
import {
|
||||
MarkdownView,
|
||||
Platform,
|
||||
Plugin,
|
||||
TFile,
|
||||
Workspace,
|
||||
WorkspaceLeaf,
|
||||
} from 'obsidian';
|
||||
import {
|
||||
Settings,
|
||||
SettingsMigrator,
|
||||
SimpleBannerSettings,
|
||||
PropertySettings,
|
||||
DeviceSettings,
|
||||
} from "./settings";
|
||||
|
||||
//----------------------------------
|
||||
// Interfaces
|
||||
//----------------------------------
|
||||
interface BannerData {
|
||||
filepath: string | null;
|
||||
image: string | null;
|
||||
icon: string | null,
|
||||
viewMode: ViewMode | null;
|
||||
lastViewMode: ViewMode | null;
|
||||
isImageChange: boolean;
|
||||
isImagePropsUpdate: boolean;
|
||||
isIconChange: boolean;
|
||||
needsUpdate: boolean;
|
||||
container: HTMLElement | null;
|
||||
}
|
||||
|
||||
interface ImageProperties {
|
||||
x: number;
|
||||
y: number;
|
||||
repeatable: boolean;
|
||||
}
|
||||
|
||||
interface IconData {
|
||||
value: string | null,
|
||||
type: IconType,
|
||||
}
|
||||
|
||||
enum IconType {
|
||||
Link = 'link',
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
enum ViewMode {
|
||||
Source = 'source',
|
||||
Reading = 'preview',
|
||||
}
|
||||
|
||||
const RegExpression = {
|
||||
Wikilink: /^!?\[\[([^\]]+?)(\|([^\]]+?))?\]\]$/,
|
||||
Markdown: /^!?\[([^\]]*)\]\(([^)]+?)\)$/,
|
||||
MarkdownBare: /^!?<([^>]+)>$/,
|
||||
Weblink: /^https?:\/\//i,
|
||||
};
|
||||
import { MarkdownView, Plugin, TFile, WorkspaceLeaf, } from 'obsidian';
|
||||
import { BannerData, DeviceSettings, PropertySettings, SimpleBannerSettings } from './types/interfaces';
|
||||
import { CSSClasses, CSSValue, ViewMode } from './types/enums';
|
||||
import Settings from './settings/core';
|
||||
import SettingsMigrator from './settings/migrator';
|
||||
import Store from './data/store';
|
||||
import DomUtils from './utils/domutils';
|
||||
import Parse from './utils/parse';
|
||||
import Banner from './feature/banner';
|
||||
import Icon from './feature/icon';
|
||||
|
||||
export default class SimpleBanner extends Plugin {
|
||||
//---------------------------------------------------
|
||||
|
|
@ -64,27 +15,29 @@ export default class SimpleBanner extends Plugin {
|
|||
// Variables
|
||||
//
|
||||
//---------------------------------------------------
|
||||
workspace: Workspace;
|
||||
settings: SimpleBannerSettings;
|
||||
deviceSettings: DeviceSettings;
|
||||
settingProperties: PropertySettings;
|
||||
|
||||
|
||||
//---------------------------------------------------
|
||||
//
|
||||
// Plugin Lifecycle
|
||||
//
|
||||
//---------------------------------------------------
|
||||
async onload() {
|
||||
this.workspace = this.app.workspace;
|
||||
const app = this.app;
|
||||
const workspace = app.workspace;
|
||||
|
||||
Parse.init(this);
|
||||
DomUtils.init(this);
|
||||
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new Settings(this.app, this));
|
||||
this.addSettingTab(new Settings(app, this));
|
||||
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.registerEvent(this.workspace.on('layout-change', this.process.bind(this)));
|
||||
this.registerEvent(this.workspace.on('file-open', this.handleFileEvents.bind(this)));
|
||||
this.registerEvent(this.app.metadataCache.on('changed', this.handleMetaEvents.bind(this)));
|
||||
workspace.onLayoutReady(() => {
|
||||
this.registerEvent(workspace.on('layout-change', this.handleLayoutChange.bind(this)));
|
||||
this.registerEvent(workspace.on('file-open', this.handleFileOpen.bind(this)));
|
||||
this.registerEvent(app.metadataCache.on('changed', this.handleMetaChange.bind(this)));
|
||||
this.applySettings();
|
||||
});
|
||||
}
|
||||
|
|
@ -97,53 +50,93 @@ export default class SimpleBanner extends Plugin {
|
|||
// Methods
|
||||
//
|
||||
//---------------------------------------------------
|
||||
|
||||
processAll() {
|
||||
this.app.workspace.iterateRootLeaves((leaf: WorkspaceLeaf) => {
|
||||
const view = leaf.view as MarkdownView;
|
||||
if (view) {
|
||||
if (this.deviceSettings.bannerEnabled) {
|
||||
const file = view?.file || null;
|
||||
const options = this.computeBannerData(file, view);
|
||||
this.process(options || null);
|
||||
this.process(file, view);
|
||||
} else {
|
||||
this.removeBanner();
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
process(options?: BannerData | null) {
|
||||
const opts = options || this.computeBannerData();
|
||||
if (!opts) {
|
||||
process(file?: TFile | null, view?: MarkdownView) {
|
||||
const data = this.compute(file, view);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (!opts.image) {
|
||||
this.removeBanner();
|
||||
if (!data.image) {
|
||||
this.remove(data);
|
||||
return;
|
||||
}
|
||||
if (!opts.icon) {
|
||||
opts.needsUpdate = true;
|
||||
if (!data.icon) {
|
||||
data.needsUpdate = true;
|
||||
}
|
||||
if (this.deviceSettings.bannerEnabled) {
|
||||
this.updateBanner(opts);
|
||||
this.render(data);
|
||||
}
|
||||
}
|
||||
|
||||
updateBanner(options: BannerData) {
|
||||
const {
|
||||
image,
|
||||
icon,
|
||||
viewMode,
|
||||
lastViewMode,
|
||||
container,
|
||||
needsUpdate,
|
||||
isImageChange,
|
||||
isImagePropsUpdate,
|
||||
isIconChange,
|
||||
} = options;
|
||||
compute(file?: TFile | null, targetView?: MarkdownView): BannerData | null {
|
||||
const view = targetView || this.getActiveView();
|
||||
if (file && view instanceof MarkdownView) {
|
||||
const defaultData = this.createDefaultBannerData();
|
||||
// @ts-ignore
|
||||
const olddata: BannerData | null = Store.get(view?.leaf.id) || defaultData;
|
||||
const newdata = this.createDefaultBannerData(view, olddata.viewMode);
|
||||
const cachedMetadata = this.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
|
||||
|
||||
if (frontmatter) {
|
||||
const settingProps = this.settingProperties;
|
||||
const imageProp = settingProps.image;
|
||||
const iconProp = settingProps.icon;
|
||||
|
||||
// parse for image property
|
||||
if (frontmatter[imageProp]) {
|
||||
newdata.image = frontmatter[imageProp];
|
||||
newdata.filepath = file.path;
|
||||
if (olddata.filepath !== newdata.filepath) {
|
||||
newdata.needsUpdate = true;
|
||||
newdata.isImageChange = true;
|
||||
} else if (olddata.image !== newdata.image) {
|
||||
newdata.needsUpdate = true;
|
||||
newdata.isImageChange = true;
|
||||
if (Parse.isImagePropertiesUpdate(olddata.image, newdata.image, view)) {
|
||||
newdata.isImagePropsUpdate = true;
|
||||
newdata.isImageChange = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse for icon property if enabled
|
||||
if (this.deviceSettings.iconEnabled) {
|
||||
if (frontmatter[iconProp]) {
|
||||
newdata.icon = frontmatter[iconProp];
|
||||
newdata.filepath = file.path;
|
||||
if (olddata.icon !== newdata.icon) {
|
||||
newdata.needsUpdate = true;
|
||||
}
|
||||
} else if (olddata.icon) {
|
||||
newdata.icon = null;
|
||||
newdata.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return newdata;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render(data: BannerData) {
|
||||
const { image, viewMode, lastViewMode, view, needsUpdate, isImageChange } = data;
|
||||
const propsContainer = document.querySelector('.metadata-container');
|
||||
|
||||
if (propsContainer) {
|
||||
const parent = propsContainer.parentNode;
|
||||
if (parent && parent.firstChild !== propsContainer) {
|
||||
|
|
@ -151,153 +144,45 @@ export default class SimpleBanner extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
const container = view?.containerEl;
|
||||
if (container && (lastViewMode !== viewMode || needsUpdate)) {
|
||||
const view = this.getActiveView();
|
||||
let calculatedFontSize: string | null = null;
|
||||
const { url, repeatable, x, y } = this.parseLinkString(image || '', view);
|
||||
const containers = container.querySelectorAll('.markdown-reading-view > .markdown-preview-view, .cm-scroller');
|
||||
containers.forEach((c) => {
|
||||
let element = (c.querySelector('.simple-banner') || document.createElement('div')) as HTMLElement;
|
||||
element.classList.add('simple-banner');
|
||||
const containers = container.querySelectorAll('.cm-scroller, .markdown-reading-view > .markdown-preview-view') as NodeListOf<HTMLElement>;
|
||||
const imageOptions = Parse.link(image || '', view);
|
||||
const settings = this.deviceSettings;
|
||||
|
||||
const STATIC_CSS = 'static';
|
||||
// process features
|
||||
const banners = Banner.update(data, imageOptions, containers);
|
||||
Icon.update(data, banners, settings);
|
||||
|
||||
if (isImageChange || isImagePropsUpdate) {
|
||||
if (isImageChange) {
|
||||
element.classList.remove(STATIC_CSS);
|
||||
}
|
||||
// apply changes
|
||||
if (!isImageChange) {
|
||||
Banner.replace(banners);
|
||||
} else {
|
||||
Banner.insert(banners, containers);
|
||||
}
|
||||
|
||||
this.setCSSVariables({
|
||||
'url': `url(${url})`,
|
||||
'img-x': `${x}px`,
|
||||
'img-y': `${y}px`,
|
||||
'size': repeatable ? 'auto' : 'cover',
|
||||
'repeat': repeatable ? 'repeat' : 'no-repeat',
|
||||
}, container);
|
||||
}
|
||||
|
||||
let iconContainer = element.querySelector('.icon');
|
||||
const hadIconContainer = iconContainer !== null;
|
||||
if (hadIconContainer) {
|
||||
iconContainer?.classList.add(STATIC_CSS);
|
||||
}
|
||||
if (this.deviceSettings.iconEnabled && icon) {
|
||||
if (!hadIconContainer) {
|
||||
iconContainer = document.createElement('div');
|
||||
iconContainer.classList.add('icon');
|
||||
if (Platform.isWin) {
|
||||
iconContainer.classList.add('is-windows');
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
iconContainer.appendChild(div);
|
||||
element.prepend(iconContainer);
|
||||
}
|
||||
|
||||
if (iconContainer) {
|
||||
const iconelement = iconContainer.querySelector('div') as HTMLElement;
|
||||
|
||||
let { value, type } = this.getIconData(icon, view);
|
||||
value = value?.replace(/([#.:[\\]"])/g, '\\$1') || '';
|
||||
iconelement.dataset.type = type;
|
||||
|
||||
const iconVars = {} as any;
|
||||
iconVars['icon'] = type === IconType.Link ? `url(${value})` : `"${value}"`;
|
||||
|
||||
if (type === IconType.Text) {
|
||||
calculatedFontSize = calculatedFontSize !== null ? calculatedFontSize : this.getFontsize(value);
|
||||
iconVars['icon-fontsize'] = calculatedFontSize;
|
||||
}
|
||||
this.setCSSVariables(iconVars, iconelement);
|
||||
}
|
||||
} else if (iconContainer) {
|
||||
options.icon = null;
|
||||
iconContainer.remove();
|
||||
}
|
||||
|
||||
if (!isImageChange) {
|
||||
element.classList.add(STATIC_CSS);
|
||||
} else {
|
||||
c.prepend(element);
|
||||
element.onanimationend = () => {
|
||||
const banners = document.querySelectorAll('.simple-banner');
|
||||
banners.forEach(b => b.classList.add(STATIC_CSS));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
options.lastViewMode = viewMode;
|
||||
container.dataset.sb = JSON.stringify(options);
|
||||
// Store new state
|
||||
data.lastViewMode = viewMode;
|
||||
container.dataset.sb = '';
|
||||
// @ts-ignore
|
||||
Store.set(view?.leaf.id, data);
|
||||
}
|
||||
}
|
||||
|
||||
removeBanner() {
|
||||
const options = this.computeBannerData();
|
||||
const container = options?.container;
|
||||
if (options && container) {
|
||||
const targets = container.querySelectorAll('.simple-banner');
|
||||
targets.forEach((t) => { t.remove() });
|
||||
delete container.dataset.sb;
|
||||
}
|
||||
}
|
||||
|
||||
computeBannerData(newfile?: TFile | null, targetView?: MarkdownView): BannerData | null {
|
||||
const view = targetView || this.getActiveView();
|
||||
remove(data?: BannerData) {
|
||||
const view = data?.view || this.getActiveView();
|
||||
if (view instanceof MarkdownView) {
|
||||
const defaultOptions = this.createDefaultBannerData();
|
||||
let oldopt: BannerData | null = defaultOptions;
|
||||
const container = view.containerEl;
|
||||
|
||||
if (newfile && newfile.path !== view?.file?.path) {
|
||||
oldopt = defaultOptions;
|
||||
} else if (container?.dataset.sb) {
|
||||
oldopt = JSON.parse(container.dataset.sb) as BannerData || defaultOptions;
|
||||
const container = view?.containerEl;
|
||||
if (container) {
|
||||
const targets = container.querySelectorAll(`.${CSSClasses.Main}`);
|
||||
targets.forEach((t) => {
|
||||
t.remove()
|
||||
});
|
||||
// @ts-ignore
|
||||
Store.delete(view?.leaf.id);
|
||||
delete container.dataset.sb;
|
||||
}
|
||||
|
||||
const opt = this.createDefaultBannerData(view, oldopt.viewMode);
|
||||
const file = view?.file || null;
|
||||
if (file) {
|
||||
const cachedMetadata = this.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
|
||||
if (frontmatter) {
|
||||
const settingProps = this.settingProperties;
|
||||
const imageProp = settingProps.image;
|
||||
const iconProp = settingProps.icon;
|
||||
|
||||
// parse for image property
|
||||
if (frontmatter[imageProp]) {
|
||||
opt.image = frontmatter[imageProp];
|
||||
opt.filepath = file.path;
|
||||
if (oldopt.image !== opt.image) {
|
||||
opt.needsUpdate = true;
|
||||
opt.isImageChange = true;
|
||||
if (this.isImagePropertiesUpdate(oldopt.image, opt.image, view)) {
|
||||
opt.isImagePropsUpdate = true;
|
||||
opt.isImageChange = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse for icon property if enabled
|
||||
if (this.deviceSettings.iconEnabled) {
|
||||
if (frontmatter[iconProp]) {
|
||||
opt.icon = frontmatter[iconProp];
|
||||
opt.filepath = file.path;
|
||||
if (oldopt.icon !== opt.icon) {
|
||||
opt.needsUpdate = true;
|
||||
opt.isIconChange = true;
|
||||
}
|
||||
} else if (oldopt.icon) {
|
||||
opt.icon = null;
|
||||
opt.needsUpdate = true;
|
||||
opt.isIconChange = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//----------------------------------
|
||||
|
|
@ -328,7 +213,7 @@ export default class SimpleBanner extends Plugin {
|
|||
vars['note-offset'] = `${offset}px`;
|
||||
vars['radius'] = `${radius[0]}px ${radius[1]}px ${radius[2]}px ${radius[3]}px`;
|
||||
vars['padding'] = `${padding}px`;
|
||||
vars['fade'] = (fade) ? 'linear-gradient(180deg, var(--background-primary) 25%, transparent)' : 'none';
|
||||
vars['mask'] = (fade) ? CSSValue.RevertLayer : CSSValue.Initial;
|
||||
|
||||
if (settings.iconEnabled) {
|
||||
const iconSize = settings.iconSize;
|
||||
|
|
@ -338,23 +223,23 @@ export default class SimpleBanner extends Plugin {
|
|||
const iconAlignment = settings.iconAlignment;
|
||||
const iconOffset = settings.iconOffset;
|
||||
|
||||
vars['icon-size'] = `${iconSize}px`;
|
||||
vars['icon-size-w'] = `${iconSize}px`;
|
||||
vars['icon-size-h'] = `${iconSize}px`;
|
||||
vars['icon-radius'] = `${iconRadius}px`;
|
||||
vars['icon-background'] = iconBackground ? 'var(--background-primary)' : 'transparent';
|
||||
vars['icon-border'] = `${iconBorder}px`;
|
||||
vars['icon-alignh'] = iconAlignment[0];
|
||||
vars['icon-alignv'] = iconAlignment[1];
|
||||
vars['icon-align-h'] = iconAlignment[0];
|
||||
vars['icon-align-v'] = iconAlignment[1];
|
||||
vars['icon-offset-x'] = `${iconOffset[0]}px`;
|
||||
vars['icon-offset-y'] = `${iconOffset[1]}px`;
|
||||
vars['icon-border'] = `${iconBorder}px`;
|
||||
vars['icon-background'] = iconBackground ? CSSValue.RevertLayer : CSSValue.Transparent;
|
||||
}
|
||||
|
||||
this.setCSSVariables(vars);
|
||||
DomUtils.setCSSVariables(vars);
|
||||
|
||||
const AUTOHIDE_CLASS = 'smpbn-autohide';
|
||||
if (this.settings.properties.autohide) {
|
||||
document.body.classList.add(AUTOHIDE_CLASS);
|
||||
document.body.classList.add(CSSClasses.Autohide);
|
||||
} else {
|
||||
document.body.classList.remove(AUTOHIDE_CLASS);
|
||||
document.body.classList.remove(CSSClasses.Autohide);
|
||||
}
|
||||
|
||||
this.processAll();
|
||||
|
|
@ -363,21 +248,30 @@ export default class SimpleBanner extends Plugin {
|
|||
//----------------------------------
|
||||
// Event Handlers
|
||||
//----------------------------------
|
||||
handleFileEvents(file: TFile) {
|
||||
const options = this.computeBannerData(file);
|
||||
const container = options?.container;
|
||||
if (options && container && file.path !== options.filepath) {
|
||||
delete container.dataset.sb;
|
||||
this.process();
|
||||
handleLayoutChange() {
|
||||
const view = this.getActiveView();
|
||||
if (view) {
|
||||
// @ts-ignore
|
||||
if (!Store.exists(view?.leaf.id)) {
|
||||
this.process(view.file, view);
|
||||
}
|
||||
} else {
|
||||
Store.delete(this.getMostRecentLeafId());
|
||||
}
|
||||
}
|
||||
|
||||
async handleMetaEvents(file: TFile) {
|
||||
handleFileOpen(file: TFile) {
|
||||
const view = this.getActiveView();
|
||||
if (view instanceof MarkdownView) {
|
||||
this.process(file, view);
|
||||
}
|
||||
}
|
||||
|
||||
handleMetaChange(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);
|
||||
this.process(file, view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -386,180 +280,19 @@ export default class SimpleBanner extends Plugin {
|
|||
// Helper Methods
|
||||
//----------------------------------
|
||||
getActiveView(): MarkdownView | null {
|
||||
return this.workspace.getActiveViewOfType(MarkdownView) || null;
|
||||
return this.app.workspace.getActiveViewOfType(MarkdownView) || null;
|
||||
}
|
||||
|
||||
parseLinkString(str: string, view?: MarkdownView | null, settingProperty?: string | null) {
|
||||
let url: string | null = null;
|
||||
let displayText: string | null = null;
|
||||
let external: boolean;
|
||||
let obsidianUrl: boolean = false;
|
||||
let options = { x: 0, y: 0, repeatable: false };
|
||||
|
||||
const wikilinkMatch = str.match(RegExpression.Wikilink);
|
||||
if (wikilinkMatch) {
|
||||
url = wikilinkMatch[1].trim();
|
||||
displayText = wikilinkMatch[3] ? wikilinkMatch[3].trim() : null;
|
||||
}
|
||||
|
||||
const markdownMatch = str.match(RegExpression.Markdown);
|
||||
const markdownBareMatch = str.match(RegExpression.MarkdownBare);
|
||||
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 = RegExpression.Weblink.test(url);
|
||||
|
||||
if (this.isObsidianUrl(url)) {
|
||||
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.parseImageProperties(url.substring(hashIndex + 1));
|
||||
url = url.replace(/#.*/, '').trim();
|
||||
}
|
||||
|
||||
if (displayText) {
|
||||
options = this.parseImageProperties(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) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.app.fileManager.processFrontMatter(activeFile, (frontmatter) => {
|
||||
const propName = settingProperty || this.settingProperties.image;
|
||||
frontmatter[propName] = `[[${file?.path}]]`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: url.trim(),
|
||||
external,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
isObsidianUrl(url: string): boolean {
|
||||
return url.startsWith('obsidian://open');
|
||||
}
|
||||
|
||||
parseImageProperties(str: string): ImageProperties {
|
||||
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 };
|
||||
}
|
||||
|
||||
isImagePropertiesUpdate(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;
|
||||
}
|
||||
|
||||
getIconData(icon: string, view?: MarkdownView | null): IconData {
|
||||
const str = icon || '';
|
||||
const out = { value: null, type: IconType.Text } as IconData;
|
||||
|
||||
if (RegExpression.Wikilink.test(str)) {
|
||||
out.type = IconType.Link;
|
||||
} else if (RegExpression.Markdown.test(str) || RegExpression.MarkdownBare.test(str)) {
|
||||
out.type = IconType.Link;
|
||||
} else if (RegExpression.Weblink.test(icon)) {
|
||||
out.type = IconType.Link;
|
||||
} else if (this.isObsidianUrl(icon)) {
|
||||
out.type = IconType.Link;
|
||||
}
|
||||
|
||||
if (out.type === IconType.Link) {
|
||||
const data = this.parseLinkString(str, view, this.settingProperties.icon);
|
||||
out.value = data.url;
|
||||
} else {
|
||||
out.value = str;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
getFontsize(textContent: string) {
|
||||
const temp = document.createElement('span');
|
||||
temp.setAttribute('style', 'position: absolute; visibility: hidden; white-space: nowrap;');
|
||||
temp.style.padding = '0';
|
||||
temp.style.margin = '0';
|
||||
temp.style.left = '-9999px';
|
||||
temp.textContent = textContent.toUpperCase();
|
||||
document.body.appendChild(temp);
|
||||
const size = this.deviceSettings.iconSize;
|
||||
const checkWidth = size - 16;
|
||||
|
||||
let fontSize = size; // Start big
|
||||
temp.style.fontSize = fontSize + 'px';
|
||||
|
||||
while (temp.offsetWidth > checkWidth && fontSize > 1) {
|
||||
fontSize -= 1;
|
||||
temp.style.fontSize = fontSize + 'px';
|
||||
}
|
||||
|
||||
document.body.removeChild(temp);
|
||||
return `${fontSize}px`;
|
||||
getMostRecentLeafId(): string | null {
|
||||
const leaf = this.app.workspace.getMostRecentLeaf();
|
||||
// @ts-ignore
|
||||
return leaf.id;
|
||||
}
|
||||
|
||||
createDefaultBannerData(view?: MarkdownView | null, lastViewMode?: ViewMode | null): BannerData {
|
||||
let viewMode = null;
|
||||
let container = null;
|
||||
if (view) {
|
||||
viewMode = ((view.getMode() || null) === ViewMode.Reading) ? ViewMode.Reading : ViewMode.Source;
|
||||
container = view.containerEl;
|
||||
}
|
||||
return {
|
||||
filepath: null,
|
||||
|
|
@ -569,16 +302,8 @@ export default class SimpleBanner extends Plugin {
|
|||
lastViewMode: lastViewMode || null,
|
||||
isImagePropsUpdate: false,
|
||||
isImageChange: false,
|
||||
isIconChange: false,
|
||||
needsUpdate: false,
|
||||
container,
|
||||
view,
|
||||
}
|
||||
}
|
||||
|
||||
setCSSVariables(variables: Record<string, string>, target: HTMLElement = document.body) {
|
||||
const style = target.style;
|
||||
Object.keys(variables).forEach(k => {
|
||||
style.setProperty(`--smpbn-${k}`, variables[k]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/scss/animations.scss
Normal file
15
src/scss/animations.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* -------------------------------
|
||||
Animations
|
||||
------------------------------- */
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes banner-anim {
|
||||
from { background-position-y: calc(50% + (var(--sb-img-y, 0) - 10px)); }
|
||||
to { background-position-y: calc(50% + var(--sb-img-y, 0)); }
|
||||
}
|
||||
@keyframes icon-anim {
|
||||
from { transform: translateX(10px) }
|
||||
to { transform: translateX(0) }
|
||||
}
|
||||
66
src/scss/banner.scss
Normal file
66
src/scss/banner.scss
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/* -------------------------------
|
||||
Banner
|
||||
------------------------------- */
|
||||
div.simple-banner {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: calc(100% - (var(--sb-padding) * 2));
|
||||
height: 100%;
|
||||
margin-left: var(--sb-padding);
|
||||
max-height: var(--sb-height);
|
||||
padding: var(--size-4-3) var(--size-4-3) 0 var(--size-4-3);
|
||||
user-select: none;
|
||||
|
||||
&: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(--sb-radius);
|
||||
background-color: rgba(0,0,0,.2);
|
||||
mask-image: var(--sb-mask);
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
border-radius: var(--sb-radius);
|
||||
background-image: var(--sb-url);
|
||||
background-repeat: var(--sb-repeat);
|
||||
background-size: var(--sb-size);
|
||||
background-position-x: calc(50% + var(--sb-img-x));
|
||||
background-position-y: calc(50% + var(--sb-img-y));
|
||||
animation: fade-in 0.7s linear forwards, banner-anim 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;
|
||||
backface-visibility: hidden;
|
||||
overflow: hidden;
|
||||
mask-image: var(--sb-mask);
|
||||
}
|
||||
|
||||
&.static:after {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
& + div {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
&.is-phone, &.is-tablet {
|
||||
div.simple-banner {
|
||||
width: calc(100% - (var(--sb-padding) - 2px));
|
||||
}
|
||||
}
|
||||
}
|
||||
143
src/scss/frontmatter.scss
Normal file
143
src/scss/frontmatter.scss
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
/* -------------------------------
|
||||
Frontmatter Overrides
|
||||
------------------------------- */
|
||||
.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: var(--sb-height, 240px);
|
||||
|
||||
.inline-embed .el-pre.mod-frontmatter.mod-ui,
|
||||
.inline-embed .markdown-preview-view {
|
||||
padding-top: initial;
|
||||
}
|
||||
}
|
||||
|
||||
body.sb-autohide {
|
||||
.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: 50%;
|
||||
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(-3px);
|
||||
transition: opacity 0.2s linear, transform 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
opacity: 0.1;
|
||||
background-color: black;
|
||||
box-shadow: 0 2px 2px black;
|
||||
transition: box-shadow 0.2s linear, opacity 0.2s linear;
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
.metadata-properties-heading {
|
||||
opacity: 0.5;
|
||||
transform: translateY(-70px);
|
||||
& > .metadata-properties-title {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
}
|
||||
&:before {
|
||||
opacity: 0;
|
||||
transform: translateY(-70px);
|
||||
}
|
||||
&:after {
|
||||
opacity: 0;
|
||||
box-shadow: 0 2px 2px black;
|
||||
}
|
||||
}
|
||||
|
||||
&: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(-3px);
|
||||
&:before {
|
||||
opacity: 0;
|
||||
}
|
||||
& > .metadata-properties-title {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-contentContainer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-top: calc(var(--sb-height, 240px) + (var(--sb-note-offset) - 6px));
|
||||
}
|
||||
|
||||
.el-pre.mod-frontmatter.mod-ui {
|
||||
padding-top: calc(var(--sb-height, 240px) + (var(--sb-note-offset) + 12px));
|
||||
}
|
||||
|
||||
.inline-embed .el-pre.mod-frontmatter.mod-ui {
|
||||
padding-top: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&.show-inline-title .workspace-leaf-content[data-sb] .view-content > .markdown-source-view.mod-cm6 > .cm-editor > .cm-scroller,
|
||||
&.show-inline-title .workspace-leaf-content[data-sb] .view-content > .markdown-preview-view {
|
||||
.cm-contentContainer {
|
||||
padding-top: 0;
|
||||
}
|
||||
.inline-title {
|
||||
padding-top: calc(var(--sb-height, 240px) + (var(--sb-note-offset) + 12px));
|
||||
}
|
||||
.el-pre.mod-frontmatter.mod-ui {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-mobile {
|
||||
.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 {
|
||||
.metadata-container {
|
||||
left: calc(50% + (var(--size-4-1) * 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/scss/icon.scss
Normal file
91
src/scss/icon.scss
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/* -------------------------------
|
||||
Icon
|
||||
------------------------------- */
|
||||
div.simple-banner {
|
||||
& > div.icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: var(--sb-icon-align-h);
|
||||
align-items: var(--sb-icon-align-v);
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
z-index: 2;
|
||||
margin: 0 auto;
|
||||
max-width: var(--file-line-width);
|
||||
transform: translate(0, 0);
|
||||
animation: fade-in 0.7s linear forwards, icon-anim 0.8s ease-out;
|
||||
will-change: transform, opacity;
|
||||
|
||||
& > div {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: var(--sb-icon-size-w);
|
||||
height: var(--sb-icon-size-h);
|
||||
background-color: var(--sb-icon-background);
|
||||
transform: translate(var(--sb-icon-offset-x), var(--sb-icon-offset-y));
|
||||
border-radius: var(--sb-icon-radius);
|
||||
border: var(--sb-icon-border) solid var(--background-primary);
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
|
||||
&[data-type="text"]:after {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
text-transform: uppercase;
|
||||
content: var(--sb-icon-value);
|
||||
color: var(--text-normal);
|
||||
line-height: 0;
|
||||
font-size: var(--sb-icon-fontsize, inherit);
|
||||
user-select: none;
|
||||
}
|
||||
&[data-type="link"]:after {
|
||||
position: absolute;
|
||||
text-transform: uppercase;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
transform: translate(0, 0);
|
||||
content: '';
|
||||
background-image: var(--sb-icon-value);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-windows {
|
||||
& > div {
|
||||
&[data-type="text"]:after {
|
||||
top: calc(0% - (var(--sb-icon-fontsize) * 0.05));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.static > div.icon {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
&.is-tablet {
|
||||
div.simple-banner > div.icon {
|
||||
left: calc(var(--size-4-2) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-phone {
|
||||
div.simple-banner > div.icon {
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/scss/settings.scss
Normal file
65
src/scss/settings.scss
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/* -------------------------------
|
||||
Settings
|
||||
------------------------------- */
|
||||
.setting-item {
|
||||
&.sbs-grid-radius > .setting-item-control {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
grid-template-rows: auto auto;
|
||||
|
||||
& > *:nth-child(1) {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
& > *:nth-child(2) {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
& > *:nth-child(3) {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
}
|
||||
& > *:nth-child(5) {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
& > *:nth-child(4) {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
& > input {
|
||||
max-width: 78.2px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.sbs-grid-xy > .setting-item-control {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
grid-template-rows: auto;
|
||||
|
||||
& > *:nth-child(1) {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
& > *:nth-child(2) {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
& > *:nth-child(3) {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
}
|
||||
& > input {
|
||||
max-width: 78.2px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.sbs-spacer {
|
||||
padding-bottom: calc(var(--size-4-1) * 10);
|
||||
&:last-of-type {
|
||||
padding-bottom: revert;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/scss/variables.scss
Normal file
39
src/scss/variables.scss
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
body {
|
||||
/* Banner Variables */
|
||||
--sb-padding: 8px;
|
||||
--sb-radius: 0px;
|
||||
--sb-height: 240px;
|
||||
--sb-url: none;
|
||||
--sb-repeat: no-repeat;
|
||||
--sb-size: cover;
|
||||
--sb-img-x: 0;
|
||||
--sb-img-y: 0;
|
||||
--sb-mask: linear-gradient(180deg, var(--background-primary) 25%, transparent);
|
||||
|
||||
/* Icon Variables */
|
||||
--sb-icon-align-h: flex-start;
|
||||
--sb-icon-align-v: flex-end;
|
||||
--sb-icon-size-w: 96px;
|
||||
--sb-icon-size-h: 96px;
|
||||
--sb-icon-background: rgb(from var(--background-primary) r g b / 80%);
|
||||
--sb-icon-border: 0;
|
||||
--sb-icon-offset-x: 0;
|
||||
--sb-icon-offset-y: 0;
|
||||
--sb-icon-radius: 8px;
|
||||
--sb-icon-value: '';
|
||||
--sb-icon-fontsize: inherit;
|
||||
|
||||
/* Datetime Variables */
|
||||
--sb-dt-align-h: flex-start;
|
||||
--sb-dt-align-v: flex-end;
|
||||
--sb-dt-gap: var(--size-4-2);
|
||||
--sb-dt-offset-x: 0;
|
||||
--sb-dt-offset-y: 0;
|
||||
--sb-dt-fontfamily-time: inherit;
|
||||
--sb-dt-fontweight-time: inherit;
|
||||
--sb-dt-fontsize-time: initial;
|
||||
--sb-dt-fontfamily-date: inherit;
|
||||
--sb-dt-fontweight-date: inherit;
|
||||
--sb-dt-fontsize-date: initial;
|
||||
--sb-dt-textshadow: initial;
|
||||
}
|
||||
625
src/settings.ts
625
src/settings.ts
|
|
@ -1,625 +0,0 @@
|
|||
import {App, Platform, PluginSettingTab, Setting} from 'obsidian';
|
||||
import SimpleBanner from "./main";
|
||||
|
||||
//----------------------------------
|
||||
// Interfaces
|
||||
//----------------------------------
|
||||
export interface DeviceSettings {
|
||||
bannerEnabled: boolean;
|
||||
height: number;
|
||||
noteOffset: number;
|
||||
bannerRadius: Array<number>;
|
||||
bannerPadding: number;
|
||||
bannerFade: boolean;
|
||||
|
||||
iconEnabled: boolean,
|
||||
iconSize: number;
|
||||
iconRadius: number;
|
||||
iconBackground: boolean;
|
||||
iconBorder: number;
|
||||
iconAlignment: Array<string>;
|
||||
iconOffset: Array<number>;
|
||||
}
|
||||
|
||||
export interface PropertySettings {
|
||||
autohide: boolean;
|
||||
image: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface SimpleBannerSettings {
|
||||
desktop: DeviceSettings
|
||||
tablet: DeviceSettings
|
||||
phone: DeviceSettings
|
||||
properties: PropertySettings;
|
||||
}
|
||||
|
||||
export enum DeviceType {
|
||||
Desktop = 'desktop',
|
||||
Tablet = 'tablet',
|
||||
Phone = 'phone',
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: SimpleBannerSettings = {
|
||||
desktop: {
|
||||
bannerEnabled: true,
|
||||
height: 240,
|
||||
noteOffset: -32,
|
||||
bannerRadius: [8, 8, 8, 8],
|
||||
bannerPadding: 8,
|
||||
bannerFade: true,
|
||||
|
||||
iconEnabled: false,
|
||||
iconSize: 96,
|
||||
iconRadius: 8,
|
||||
iconBackground: true,
|
||||
iconBorder: 2,
|
||||
iconAlignment: ['flex-start', 'flex-end'],
|
||||
iconOffset: [0, -24],
|
||||
},
|
||||
|
||||
tablet: {
|
||||
bannerEnabled: true,
|
||||
height: 190,
|
||||
noteOffset: -32,
|
||||
bannerRadius: [8, 8, 8, 8],
|
||||
bannerPadding: 8,
|
||||
bannerFade: true,
|
||||
|
||||
iconEnabled: false,
|
||||
iconSize: 96,
|
||||
iconRadius: 8,
|
||||
iconBackground: true,
|
||||
iconBorder: 2,
|
||||
iconAlignment: ['flex-start', 'flex-end'],
|
||||
iconOffset: [0, -24],
|
||||
},
|
||||
|
||||
phone: {
|
||||
bannerEnabled: true,
|
||||
height: 160,
|
||||
noteOffset: -32,
|
||||
bannerRadius: [8, 8, 8, 8],
|
||||
bannerPadding: 8,
|
||||
bannerFade: true,
|
||||
|
||||
iconEnabled: false,
|
||||
iconSize: 56,
|
||||
iconRadius: 8,
|
||||
iconBackground: true,
|
||||
iconBorder: 2,
|
||||
iconAlignment: ['flex-start', 'flex-end'],
|
||||
iconOffset: [0, -24],
|
||||
},
|
||||
|
||||
properties: {
|
||||
autohide: true,
|
||||
image: 'banner',
|
||||
icon: 'icon',
|
||||
},
|
||||
}
|
||||
const ICON_RESET = 'rotate-ccw';
|
||||
const TEXT_RESET = 'Restore default';
|
||||
|
||||
/**
|
||||
* Represents the settings tab for configuring the SimpleBanner plugin.
|
||||
*
|
||||
*/
|
||||
export class Settings extends PluginSettingTab {
|
||||
plugin: SimpleBanner;
|
||||
|
||||
constructor(app: App, plugin: SimpleBanner) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
static get currentDevice(): DeviceType {
|
||||
if (Platform.isPhone) {
|
||||
return DeviceType.Phone;
|
||||
}
|
||||
if (Platform.isTablet) {
|
||||
return DeviceType.Tablet;
|
||||
}
|
||||
return DeviceType.Desktop;
|
||||
}
|
||||
|
||||
static get DEFAULT_SETTINGS(): SimpleBannerSettings {
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
containerEl.empty();
|
||||
|
||||
this.createBannerSettings(containerEl);
|
||||
this.createIconSettings(containerEl);
|
||||
this.createFrontmatterSettings(containerEl);
|
||||
}
|
||||
|
||||
createBannerSettings(containerEl: HTMLElement) {
|
||||
const plugin = this.plugin;
|
||||
const currentDevice = Settings.currentDevice;
|
||||
const settings = plugin.settings[currentDevice];
|
||||
const defaultSettings = DEFAULT_SETTINGS[currentDevice];
|
||||
const prettyDevice = currentDevice.charAt(0).toUpperCase() + currentDevice.slice(1);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setHeading()
|
||||
.setName(`Simple Banner - ${prettyDevice} Settings`)
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Show Simple Banner')
|
||||
.setDesc(`Enable or disable Simple Banner on your ${currentDevice} device.`)
|
||||
.addToggle(component => component
|
||||
.setValue(settings.bannerEnabled)
|
||||
.onChange(async (value) => {
|
||||
settings.bannerEnabled = value;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
}));
|
||||
|
||||
if (settings.bannerEnabled) {
|
||||
new Setting(containerEl)
|
||||
.setName('Note Offset')
|
||||
.setDesc('Move the position of the notes content in pixels.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.noteOffset = defaultSettings.noteOffset;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter a number')
|
||||
.setValue(settings.noteOffset.toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.noteOffset;
|
||||
}
|
||||
settings.noteOffset = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Border Radius')
|
||||
.setDesc('Size of the border radius in pixels.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.bannerRadius = defaultSettings.bannerRadius;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('8')
|
||||
.setValue(settings.bannerRadius[0].toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.bannerRadius[0];
|
||||
}
|
||||
settings.bannerRadius[0] = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('8')
|
||||
.setValue(settings.bannerRadius[1].toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.bannerRadius[1];
|
||||
}
|
||||
settings.bannerRadius[1] = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('8')
|
||||
.setValue(settings.bannerRadius[2].toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.bannerRadius[2];
|
||||
}
|
||||
settings.bannerRadius[2] = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('8')
|
||||
.setValue(settings.bannerRadius[3].toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.bannerRadius[3];
|
||||
}
|
||||
settings.bannerRadius[3] = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.setClass('smpbn-banner-radii');
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Padding')
|
||||
.setDesc('Padding of the banner from the edges of the note in pixels.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.bannerPadding = defaultSettings.bannerPadding;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter a number')
|
||||
.setValue(settings.bannerPadding.toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.bannerPadding;
|
||||
}
|
||||
settings.bannerPadding = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Fade')
|
||||
.setDesc('Fade the image out towards the content.')
|
||||
.addToggle(component => component
|
||||
.setValue(settings.bannerFade)
|
||||
.onChange(async (value) => {
|
||||
settings.bannerFade = value;
|
||||
await plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Height')
|
||||
.setDesc(`Height of the Banner on your ${currentDevice} device (in pixels).`)
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.height = defaultSettings.height;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter a number')
|
||||
.setValue(settings.height.toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.height;
|
||||
}
|
||||
settings.height = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
createIconSettings(containerEl: HTMLElement) {
|
||||
const plugin = this.plugin;
|
||||
const currentDevice = Settings.currentDevice;
|
||||
const settings = plugin.settings[currentDevice];
|
||||
const defaultSettings = DEFAULT_SETTINGS[currentDevice];
|
||||
|
||||
if (settings.bannerEnabled) {
|
||||
new Setting(containerEl)
|
||||
.setHeading()
|
||||
.setName('Icon Settings');
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Show Icon')
|
||||
.setDesc('Enable or disable the icon.')
|
||||
.addToggle(component => component
|
||||
.setValue(settings.iconEnabled)
|
||||
.onChange(async (value) => {
|
||||
settings.iconEnabled = value;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
}));
|
||||
|
||||
if (settings.iconEnabled) {
|
||||
new Setting(containerEl)
|
||||
.setName('Icon Size')
|
||||
.setDesc('Size of the icon in pixels.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.iconSize = defaultSettings.iconSize;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter a number')
|
||||
.setValue(settings.iconSize.toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.iconSize;
|
||||
}
|
||||
settings.iconSize = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Border Radius')
|
||||
.setDesc('Size of the border radius in pixels.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.iconRadius = defaultSettings.iconRadius;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter a number')
|
||||
.setValue(settings.iconRadius.toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.iconRadius;
|
||||
}
|
||||
settings.iconRadius = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Icon Background')
|
||||
.setDesc('Enable or disable the icon background.')
|
||||
.addToggle(component => component
|
||||
.setValue(settings.iconBackground)
|
||||
.onChange(async (value) => {
|
||||
settings.iconBackground = value;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Icon Border Size')
|
||||
.setDesc('Size of the border in pixels.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.iconBorder = defaultSettings.iconBorder;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter a number')
|
||||
.setValue(settings.iconBorder.toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.iconBorder;
|
||||
}
|
||||
settings.iconBorder = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Icon Alignment - Horizontal')
|
||||
.setDesc('Horizontal alignment of the icon.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.iconAlignment[0] = defaultSettings.iconAlignment[0];
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('flex-start', 'Left');
|
||||
dropdown.addOption('center', 'Middle');
|
||||
dropdown.addOption('flex-end', 'Right');
|
||||
dropdown.setValue(settings.iconAlignment[0]);
|
||||
dropdown.onChange(async (value) => {
|
||||
settings.iconAlignment[0] = value;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Icon Alignment - Vertical')
|
||||
.setDesc('Vertical alignment of the icon.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.iconAlignment[1] = defaultSettings.iconAlignment[1];
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('flex-start', 'Top');
|
||||
dropdown.addOption('center', 'Middle');
|
||||
dropdown.addOption('flex-end', 'Bottom');
|
||||
dropdown.setValue(settings.iconAlignment[1]);
|
||||
dropdown.onChange(async (value) => {
|
||||
settings.iconAlignment[1] = value;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Icon Offset')
|
||||
.setDesc('Offset the X and Y position of the icon in pixels')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.iconOffset = defaultSettings.iconOffset;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('8')
|
||||
.setValue(settings.iconOffset[0].toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.iconOffset[0];
|
||||
}
|
||||
settings.iconOffset[0] = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('8')
|
||||
.setValue(settings.iconOffset[1].toString())
|
||||
.onChange(async (value) => {
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
num = defaultSettings.iconOffset[1];
|
||||
}
|
||||
settings.iconOffset[1] = num;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.setClass('smpbn-banner-offset');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createFrontmatterSettings(containerEl: HTMLElement) {
|
||||
const plugin = this.plugin;
|
||||
const settings = plugin.settings;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setHeading()
|
||||
.setName('Frontmatter Settings (Global)');
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Autohide Frontmatter/Properties')
|
||||
.setDesc(`Enable or disables the frontmatter/properties autohide feature.`)
|
||||
.addToggle(component => component
|
||||
.setValue(settings.properties.autohide)
|
||||
.onChange(async (value) => {
|
||||
settings.properties.autohide = value;
|
||||
await plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Banner Property')
|
||||
.setDesc('Name of the banner property this plugin will look for in the frontmatter.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.properties.image = DEFAULT_SETTINGS.properties.image;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('Default: banner')
|
||||
.setValue(settings.properties.image.toString())
|
||||
.onChange(async (value) => {
|
||||
settings.properties.image = (value !== '') ? value : DEFAULT_SETTINGS.properties.image;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Icon Property')
|
||||
.setDesc('Name of the icon property this plugin will look for in the frontmatter.')
|
||||
.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
settings.properties.icon = DEFAULT_SETTINGS.properties.icon;
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.addText(text => text
|
||||
.setPlaceholder('Default: icon')
|
||||
.setValue(settings.properties.icon.toString())
|
||||
.onChange(async (value) => {
|
||||
settings.properties.icon = (value !== '') ? value : DEFAULT_SETTINGS.properties.icon;
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingsMigrator {
|
||||
static async migrate(data: any, plugin: SimpleBanner): Promise<SimpleBannerSettings> {
|
||||
const DESKTOP = DeviceType.Desktop;
|
||||
const TABLET = DeviceType.Tablet;
|
||||
const PHONE = DeviceType.Phone;
|
||||
const migrationMap: { [oldKey: string]: { target: string; devices?: string[] } } = {
|
||||
desktopHeight: { target: 'desktop.height' },
|
||||
tabletHeight: { target: 'tablet.height' },
|
||||
mobileHeight: { target: 'phone.height' },
|
||||
offset: { target: 'noteOffset', devices: [DESKTOP, TABLET, PHONE] },
|
||||
fade: { target: 'bannerFade', devices: [DESKTOP, TABLET, PHONE] },
|
||||
radius: { target: 'bannerRadius', devices: [DESKTOP, TABLET, PHONE] },
|
||||
padding: { target: 'bannerPadding', devices: [DESKTOP, TABLET, PHONE] },
|
||||
propertyName: { target: 'properties.image' },
|
||||
};
|
||||
|
||||
let neededMigration = false;
|
||||
const newData = { ...data };
|
||||
|
||||
for (const oldKey in migrationMap) {
|
||||
if (newData.hasOwnProperty(oldKey) && newData[oldKey] !== undefined) {
|
||||
neededMigration = true;
|
||||
const migration = migrationMap[oldKey];
|
||||
|
||||
if (migration.devices) {
|
||||
migration.devices.forEach((device) => {
|
||||
const path = device + '.' + migration.target;
|
||||
this.setValueByPath(newData, path, newData[oldKey]);
|
||||
});
|
||||
} else {
|
||||
this.setValueByPath(newData, migration.target, newData[oldKey]);
|
||||
}
|
||||
delete newData[oldKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (neededMigration) {
|
||||
console.log('migrated settings');
|
||||
await plugin.saveData(newData);
|
||||
}
|
||||
|
||||
return newData;
|
||||
}
|
||||
|
||||
static setValueByPath(obj: any, path: string, value: any) {
|
||||
const parts = path.split('.');
|
||||
let current = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
if (!current[part] || typeof current[part] !== 'object') {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
}
|
||||
}
|
||||
465
src/settings/core.ts
Normal file
465
src/settings/core.ts
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
import { App, Platform, PluginSettingTab, Setting } from 'obsidian';
|
||||
import SimpleBanner from '../main';
|
||||
import { DeviceType } from '../types/enums';
|
||||
import { SettingCompOptions, SimpleBannerSettings } from '../types/interfaces';
|
||||
|
||||
const DEFAULT_SETTINGS: SimpleBannerSettings = {
|
||||
desktop: {
|
||||
bannerEnabled: true,
|
||||
height: 240,
|
||||
noteOffset: -32,
|
||||
bannerRadius: [8, 8, 8, 8],
|
||||
bannerPadding: 8,
|
||||
bannerFade: true,
|
||||
|
||||
iconEnabled: false,
|
||||
iconSize: 96,
|
||||
iconRadius: 8,
|
||||
iconBackground: true,
|
||||
iconBorder: 2,
|
||||
iconAlignment: ['flex-start', 'flex-end'],
|
||||
iconOffset: [0, -24],
|
||||
},
|
||||
|
||||
tablet: {
|
||||
bannerEnabled: true,
|
||||
height: 190,
|
||||
noteOffset: -32,
|
||||
bannerRadius: [8, 8, 8, 8],
|
||||
bannerPadding: 8,
|
||||
bannerFade: true,
|
||||
|
||||
iconEnabled: false,
|
||||
iconSize: 96,
|
||||
iconRadius: 8,
|
||||
iconBackground: true,
|
||||
iconBorder: 2,
|
||||
iconAlignment: ['flex-start', 'flex-end'],
|
||||
iconOffset: [0, -24],
|
||||
},
|
||||
|
||||
phone: {
|
||||
bannerEnabled: true,
|
||||
height: 160,
|
||||
noteOffset: -32,
|
||||
bannerRadius: [8, 8, 8, 8],
|
||||
bannerPadding: 8,
|
||||
bannerFade: true,
|
||||
|
||||
iconEnabled: false,
|
||||
iconSize: 56,
|
||||
iconRadius: 8,
|
||||
iconBackground: true,
|
||||
iconBorder: 2,
|
||||
iconAlignment: ['flex-start', 'flex-end'],
|
||||
iconOffset: [0, -24],
|
||||
},
|
||||
|
||||
properties: {
|
||||
autohide: true,
|
||||
image: 'banner',
|
||||
icon: 'icon',
|
||||
},
|
||||
}
|
||||
const ICON_RESET = 'rotate-ccw';
|
||||
const TEXT_RESET = 'Restore default';
|
||||
|
||||
/**
|
||||
* Represents the settings tab for configuring the SimpleBanner plugin.
|
||||
*
|
||||
*/
|
||||
export default class Settings extends PluginSettingTab {
|
||||
plugin: SimpleBanner;
|
||||
|
||||
constructor(app: App, plugin: SimpleBanner) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
static get currentDevice(): DeviceType {
|
||||
if (Platform.isPhone) {
|
||||
return DeviceType.Phone;
|
||||
}
|
||||
if (Platform.isTablet) {
|
||||
return DeviceType.Tablet;
|
||||
}
|
||||
return DeviceType.Desktop;
|
||||
}
|
||||
|
||||
static get DEFAULT_SETTINGS(): SimpleBannerSettings {
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
const currentDevice = Settings.currentDevice;
|
||||
const settings = this.plugin.settings[currentDevice];
|
||||
|
||||
this.createBannerSettings();
|
||||
if (settings.bannerEnabled) {
|
||||
this.createFrontmatterSettings();
|
||||
this.createIconSettings();
|
||||
}
|
||||
}
|
||||
|
||||
createBannerSettings() {
|
||||
const currentDevice = Settings.currentDevice;
|
||||
const prettyDevice = currentDevice.charAt(0).toUpperCase() + currentDevice.slice(1);
|
||||
const settings = this.plugin.settings[currentDevice];
|
||||
const defaultSettings = DEFAULT_SETTINGS[currentDevice];
|
||||
|
||||
this.addHeading(`Simple Banner - ${prettyDevice} Settings`);
|
||||
this.addToggle({
|
||||
title: 'Show Simple Banner',
|
||||
description: `Enable or disable Simple Banner on your ${currentDevice} device.`,
|
||||
refreshOnUpdate: true,
|
||||
}, settings, 'bannerEnabled');
|
||||
|
||||
if (settings.bannerEnabled) {
|
||||
this.addNumber({
|
||||
title: 'Height',
|
||||
description: `Height of the Banner on your ${currentDevice} device (in pixels).`,
|
||||
placeholder: 'Enter a number',
|
||||
resetValue: defaultSettings.height,
|
||||
}, settings, 'height');
|
||||
|
||||
this.addNumber({
|
||||
title: 'Padding',
|
||||
description: `Padding of the banner from the edges of the note in pixels.`,
|
||||
placeholder: 'Enter a number',
|
||||
resetValue: defaultSettings.bannerPadding,
|
||||
}, settings, 'bannerPadding');
|
||||
|
||||
this.addNumber({
|
||||
title: 'Note Offset',
|
||||
description: `Move the position of the notes content in pixels.`,
|
||||
placeholder: 'Enter a number',
|
||||
resetValue: defaultSettings.noteOffset,
|
||||
}, settings, 'noteOffset');
|
||||
|
||||
|
||||
this.addNumber({
|
||||
title: 'Border Radius',
|
||||
description: `Size of the border radius in pixels.`,
|
||||
placeholder: '8',
|
||||
isValueArray: true,
|
||||
length: 4,
|
||||
resetValue: defaultSettings.bannerRadius,
|
||||
classes: ['sbs-grid-radius'],
|
||||
}, settings, 'bannerRadius');
|
||||
|
||||
this.addToggle({
|
||||
title: 'Fade',
|
||||
description: `Fade the image out towards the content.`,
|
||||
classes: ['sbs-spacer'],
|
||||
}, settings, 'bannerFade');
|
||||
}
|
||||
}
|
||||
|
||||
createFrontmatterSettings() {
|
||||
const plugin = this.plugin;
|
||||
const settings = plugin.settings;
|
||||
|
||||
this.addHeading('Frontmatter Settings (Global)');
|
||||
this.addToggle({
|
||||
title: 'Autohide Frontmatter/Properties',
|
||||
description: `Enable or disables the frontmatter/properties autohide feature.`,
|
||||
}, settings.properties, 'autohide');
|
||||
|
||||
this.addText({
|
||||
title: 'Banner Property',
|
||||
description: 'Name of the banner property this plugin will look for in the frontmatter.',
|
||||
placeholder: 'Default: banner',
|
||||
resetValue: DEFAULT_SETTINGS.properties.image,
|
||||
}, settings.properties, 'image');
|
||||
|
||||
this.addText({
|
||||
title: 'Icon Property',
|
||||
description: 'Name of the icon property this plugin will look for in the frontmatter.',
|
||||
placeholder: 'Default: icon',
|
||||
resetValue: DEFAULT_SETTINGS.properties.icon,
|
||||
classes: ['sbs-spacer'],
|
||||
}, settings.properties, 'icon');
|
||||
}
|
||||
|
||||
createIconSettings() {
|
||||
const currentDevice = Settings.currentDevice;
|
||||
const settings = this.plugin.settings[currentDevice];
|
||||
const defaultSettings = DEFAULT_SETTINGS[currentDevice];
|
||||
|
||||
this.addHeading(`Icon Settings`);
|
||||
this.addToggle({
|
||||
title: 'Show Icon',
|
||||
description: `Enable or disable the icon.`,
|
||||
refreshOnUpdate: true,
|
||||
}, settings, 'iconEnabled');
|
||||
|
||||
if (settings.iconEnabled) {
|
||||
this.addNumber({
|
||||
title: 'Icon Size',
|
||||
description: `Size of the icon in pixels.`,
|
||||
placeholder: 'Enter a number',
|
||||
resetValue: defaultSettings.iconSize,
|
||||
}, settings, 'iconSize');
|
||||
|
||||
this.addToggle({
|
||||
title: 'Icon Background',
|
||||
description: `Enable or disable the icon background.`,
|
||||
}, settings, 'iconBackground');
|
||||
|
||||
this.addNumber({
|
||||
title: 'Border Size',
|
||||
description: `Size of the border in pixels.`,
|
||||
placeholder: 'Enter a number',
|
||||
resetValue: defaultSettings.iconBorder,
|
||||
}, settings, 'iconBorder');
|
||||
|
||||
this.addNumber({
|
||||
title: 'Border Radius',
|
||||
description: `Size of the border radius in pixels.`,
|
||||
placeholder: 'Enter a number',
|
||||
resetValue: defaultSettings.iconRadius,
|
||||
}, settings, 'iconRadius');
|
||||
|
||||
this.addDropdown({
|
||||
title: 'Icon Alignment - Horizontal',
|
||||
description: `Horizontal alignment of the icon.`,
|
||||
choices: [
|
||||
{ label: 'Left', value: 'flex-start' },
|
||||
{ label: 'Middle', value: 'center' },
|
||||
{ label: 'Right', value: 'flex-end' },
|
||||
],
|
||||
resetValue: defaultSettings.iconAlignment[0],
|
||||
}, settings, 'iconAlignment', 0);
|
||||
|
||||
this.addDropdown({
|
||||
title: 'Icon Alignment - Vertical',
|
||||
description: 'Vertical alignment of the icon.',
|
||||
choices: [
|
||||
{ label: 'Top', value: 'flex-start' },
|
||||
{ label: 'Middle', value: 'center' },
|
||||
{ label: 'Bottom', value: 'flex-end' },
|
||||
],
|
||||
resetValue: defaultSettings.iconAlignment[1],
|
||||
}, settings, 'iconAlignment', 1);
|
||||
|
||||
this.addNumber({
|
||||
title: 'Icon Offset',
|
||||
description: 'Offset the X and Y position of the icon in pixels',
|
||||
placeholder: '0',
|
||||
isValueArray: true,
|
||||
length: 2,
|
||||
resetValue: defaultSettings.iconOffset,
|
||||
classes: ['sbs-grid-xy', 'sbs-spacer'],
|
||||
}, settings, 'iconOffset');
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------
|
||||
// Helper Methods
|
||||
//----------------------------------
|
||||
addHeading(text: string) {
|
||||
new Setting(this.containerEl).setHeading().setName(text);
|
||||
}
|
||||
|
||||
addToggle(options: SettingCompOptions, obj: any, prop: string) {
|
||||
const instance = new Setting(this.containerEl);
|
||||
if (options.title) {
|
||||
instance.setName(options.title);
|
||||
}
|
||||
if (options.description) {
|
||||
instance.setDesc(options.description);
|
||||
}
|
||||
|
||||
this.setClasses(instance, options.classes);
|
||||
|
||||
instance.addToggle(component => component
|
||||
.setValue(obj[prop])
|
||||
.onChange(async (value) => {
|
||||
obj[prop] = value;
|
||||
await this.plugin.saveSettings();
|
||||
if (options.refreshOnUpdate) {
|
||||
this.display();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addDropdown(options: SettingCompOptions, obj: any, prop: string, index?: number) {
|
||||
const isResettable = options.resetValue !== undefined;
|
||||
const resetValue = options.resetValue;
|
||||
const instance = new Setting(this.containerEl);
|
||||
const hasIndex = index !== undefined;
|
||||
|
||||
if (options.title) {
|
||||
instance.setName(options.title);
|
||||
}
|
||||
if (options.description) {
|
||||
instance.setDesc(options.description);
|
||||
}
|
||||
|
||||
this.setClasses(instance, options.classes);
|
||||
|
||||
if (isResettable) {
|
||||
instance.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
if (hasIndex) {
|
||||
obj[prop][index] = resetValue;
|
||||
} else {
|
||||
obj[prop] = resetValue;
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
instance.addDropdown((dropdown) => {
|
||||
const choices = options?.choices || [];
|
||||
choices.forEach((choice) => {
|
||||
dropdown.addOption(choice.value, choice.label);
|
||||
})
|
||||
if (hasIndex) {
|
||||
dropdown.setValue(obj[prop][index]);
|
||||
} else {
|
||||
dropdown.setValue(obj[prop]);
|
||||
}
|
||||
|
||||
dropdown.onChange(async (v) => {
|
||||
if (hasIndex) {
|
||||
obj[prop][index] = v;
|
||||
} else {
|
||||
obj[prop] = v;
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
return dropdown;
|
||||
})
|
||||
}
|
||||
|
||||
addText(options: SettingCompOptions, obj: any, prop: string) {
|
||||
const isResettable = options.resetValue !== undefined;
|
||||
const resetValue = options.resetValue;
|
||||
const instance = new Setting(this.containerEl);
|
||||
|
||||
if (options.title) {
|
||||
instance.setName(options.title);
|
||||
}
|
||||
if (options.description) {
|
||||
instance.setDesc(options.description);
|
||||
}
|
||||
|
||||
this.setClasses(instance, options.classes);
|
||||
|
||||
if (isResettable) {
|
||||
instance.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
obj[prop] = resetValue;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
instance.addText((text) => {
|
||||
if (options.placeholder) {
|
||||
text.setPlaceholder(options.placeholder);
|
||||
}
|
||||
text.setValue(obj[prop].toString())
|
||||
.onChange(async (value) => {
|
||||
obj[prop] = (value !== '') ? value : resetValue || '';
|
||||
await this.plugin.saveSettings();
|
||||
if (options.refreshOnUpdate) {
|
||||
this.display();
|
||||
}
|
||||
});
|
||||
return text;
|
||||
});
|
||||
}
|
||||
|
||||
addNumber(options: SettingCompOptions, obj: any, prop: string) {
|
||||
const asFloat = options.float;
|
||||
const isResettable = options.resetValue !== undefined;
|
||||
const resetValue = options.resetValue;
|
||||
const instance = new Setting(this.containerEl);
|
||||
|
||||
if (options.title) {
|
||||
instance.setName(options.title);
|
||||
}
|
||||
if (options.description) {
|
||||
instance.setDesc(options.description);
|
||||
}
|
||||
|
||||
this.setClasses(instance, options.classes);
|
||||
|
||||
if (isResettable) {
|
||||
instance.addExtraButton(button => button
|
||||
.setIcon(ICON_RESET)
|
||||
.setTooltip(TEXT_RESET)
|
||||
.onClick(async () => {
|
||||
obj[prop] = (options.isValueArray) ? [...resetValue] : resetValue;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (options.isValueArray && options.length !== undefined) {
|
||||
const hasPlaceholders = options.placeholders !== undefined;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
instance.addText((text) => {
|
||||
if (hasPlaceholders && options?.placeholders) {
|
||||
text.setPlaceholder(options.placeholders[i] || '');
|
||||
} else if (options.placeholder) {
|
||||
text.setPlaceholder(options.placeholder);
|
||||
}
|
||||
text.setValue(obj[prop][i].toString())
|
||||
.onChange(async (value) => {
|
||||
let num = (asFloat) ? parseFloat(value) : parseInt(value, 10);
|
||||
if (isNaN(num) && isResettable) {
|
||||
num = resetValue[i] || 0;
|
||||
}
|
||||
obj[prop][i] = num;
|
||||
await this.plugin.saveSettings();
|
||||
if (options.refreshOnUpdate) {
|
||||
this.display();
|
||||
}
|
||||
});
|
||||
return text;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
instance.addText((text) => {
|
||||
if (options.placeholder) {
|
||||
text.setPlaceholder(options.placeholder);
|
||||
}
|
||||
text.setValue(obj[prop].toString())
|
||||
.onChange(async (value) => {
|
||||
let num = (asFloat) ? parseFloat(value) : parseInt(value, 10);
|
||||
if (isNaN(num) && isResettable) {
|
||||
num = resetValue;
|
||||
}
|
||||
obj[prop] = num;
|
||||
await this.plugin.saveSettings();
|
||||
if (options.refreshOnUpdate) {
|
||||
this.display();
|
||||
}
|
||||
});
|
||||
return text;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setClasses(instance: Setting, classes?: Array<string>) {
|
||||
if (classes) {
|
||||
classes.forEach((c) => instance.setClass(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
60
src/settings/migrator.ts
Normal file
60
src/settings/migrator.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import SimpleBanner from '../main';
|
||||
import { DeviceType } from '../types/enums';
|
||||
import { SimpleBannerSettings } from '../types/interfaces';
|
||||
|
||||
export default class SettingsMigrator {
|
||||
static async migrate(data: any, plugin: SimpleBanner): Promise<SimpleBannerSettings> {
|
||||
const DESKTOP = DeviceType.Desktop;
|
||||
const TABLET = DeviceType.Tablet;
|
||||
const PHONE = DeviceType.Phone;
|
||||
const migrationMap: { [oldKey: string]: { target: string; devices?: string[] } } = {
|
||||
desktopHeight: { target: 'desktop.height' },
|
||||
tabletHeight: { target: 'tablet.height' },
|
||||
mobileHeight: { target: 'phone.height' },
|
||||
offset: { target: 'noteOffset', devices: [DESKTOP, TABLET, PHONE] },
|
||||
fade: { target: 'bannerFade', devices: [DESKTOP, TABLET, PHONE] },
|
||||
radius: { target: 'bannerRadius', devices: [DESKTOP, TABLET, PHONE] },
|
||||
padding: { target: 'bannerPadding', devices: [DESKTOP, TABLET, PHONE] },
|
||||
propertyName: { target: 'properties.image' },
|
||||
};
|
||||
|
||||
let neededMigration = false;
|
||||
const newData = { ...data };
|
||||
|
||||
for (const oldKey in migrationMap) {
|
||||
if (newData.hasOwnProperty(oldKey) && newData[oldKey] !== undefined) {
|
||||
neededMigration = true;
|
||||
const migration = migrationMap[oldKey];
|
||||
|
||||
if (migration.devices) {
|
||||
migration.devices.forEach((device) => {
|
||||
const path = device + '.' + migration.target;
|
||||
this.setValueByPath(newData, path, newData[oldKey]);
|
||||
});
|
||||
} else {
|
||||
this.setValueByPath(newData, migration.target, newData[oldKey]);
|
||||
}
|
||||
delete newData[oldKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (neededMigration) {
|
||||
await plugin.saveData(newData);
|
||||
}
|
||||
|
||||
return newData;
|
||||
}
|
||||
|
||||
static setValueByPath(obj: any, path: string, value: any) {
|
||||
const parts = path.split('.');
|
||||
let current = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
if (!current[part] || typeof current[part] !== 'object') {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
}
|
||||
}
|
||||
378
src/styles.scss
378
src/styles.scss
|
|
@ -1,378 +0,0 @@
|
|||
/* -------------------------------
|
||||
Animations
|
||||
------------------------------- */
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes movedown {
|
||||
from { background-position-y: calc(50% + (var(--smpbn-img-y, 0) - 10px)); }
|
||||
to { background-position-y: calc(50% + var(--smpbn-img-y, 0)); }
|
||||
}
|
||||
@keyframes moveup {
|
||||
from { transform: translateX(10px) }
|
||||
to { transform: translateX(0) }
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
Simple Banner Styles
|
||||
------------------------------- */
|
||||
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);
|
||||
padding: var(--size-4-3) var(--size-4-3) 0 var(--size-4-3);
|
||||
user-select: none;
|
||||
|
||||
&: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);
|
||||
mask-image: var(--smpbn-fade, none);
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
border-radius: var(--smpbn-radius, 0px);
|
||||
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: fadein 0.7s linear forwards, movedown 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;
|
||||
overflow: hidden;
|
||||
mask-image: var(--smpbn-fade, none);
|
||||
}
|
||||
|
||||
& > div.icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: var(--smpbn-icon-alignh);
|
||||
align-items: var(--smpbn-icon-alignv);
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
z-index: 2;
|
||||
margin: 0 auto;
|
||||
max-width: var(--file-line-width);
|
||||
transform: translate(0, 0);
|
||||
animation: fadein 0.7s linear forwards, moveup 0.8s ease-out;
|
||||
will-change: transform, opacity;
|
||||
|
||||
& > div {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: var(--smpbn-icon-size, 96px);
|
||||
height: var(--smpbn-icon-size, 96px);
|
||||
background-color: color-mix(in srgb, var(--smpbn-icon-background) 80%, transparent);
|
||||
transform: translate(var(--smpbn-icon-offset-x), var(--smpbn-icon-offset-y));
|
||||
border-radius: var(--smpbn-icon-radius, 8px);
|
||||
border: var(--smpbn-icon-border, 0px) solid var(--background-primary);
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
|
||||
&[data-type="text"]:after {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
text-transform: uppercase;
|
||||
content: var(--smpbn-icon);
|
||||
color: var(--text-normal);
|
||||
line-height: 0;
|
||||
font-size: var(--smpbn-icon-fontsize, inherit);
|
||||
user-select: none;
|
||||
}
|
||||
&[data-type="link"]:after {
|
||||
position: absolute;
|
||||
text-transform: uppercase;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
transform: translate(0, 0);
|
||||
content: '';
|
||||
background-image: var(--smpbn-icon);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-windows {
|
||||
& > div {
|
||||
&[data-type="text"]:after {
|
||||
top: calc(0% - (var(--smpbn-icon-fontsize, 0) * 0.05));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.static {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.static:after {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
& + div {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
&.is-phone, &.is-tablet {
|
||||
div.simple-banner {
|
||||
width: calc(100% - (var(--smpbn-padding, 8px) - 2px));
|
||||
}
|
||||
}
|
||||
|
||||
&.is-tablet {
|
||||
div.simple-banner {
|
||||
& > div.icon {
|
||||
left: calc(var(--size-4-2) * -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-phone {
|
||||
div.simple-banner {
|
||||
& > div.icon {
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
Settings
|
||||
------------------------------- */
|
||||
.setting-item {
|
||||
&.smpbn-banner-radii > .setting-item-control {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
grid-template-rows: auto auto;
|
||||
|
||||
& > *:nth-child(1) {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
& > *: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;
|
||||
}
|
||||
}
|
||||
|
||||
&.smpbn-banner-offset > .setting-item-control {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
grid-template-rows: auto;
|
||||
|
||||
& > *:nth-child(1) {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
& > *:nth-child(2) {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
& > *:nth-child(3) {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
}
|
||||
& > input {
|
||||
max-width: 78.2px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
Obsidian Properties Overrides
|
||||
------------------------------- */
|
||||
.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: var(--smpbn-height, 240px);
|
||||
|
||||
.inline-embed .el-pre.mod-frontmatter.mod-ui,
|
||||
.inline-embed .markdown-preview-view {
|
||||
padding-top: initial;
|
||||
}
|
||||
}
|
||||
|
||||
body.smpbn-autohide {
|
||||
.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: 50%;
|
||||
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(-3px);
|
||||
transition: opacity 0.2s linear, transform 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
opacity: 0.1;
|
||||
background-color: black;
|
||||
box-shadow: 0 2px 2px black;
|
||||
transition: box-shadow 0.2s linear, opacity 0.2s linear;
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
.metadata-properties-heading {
|
||||
opacity: 0.5;
|
||||
transform: translateY(-70px);
|
||||
& > .metadata-properties-title {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
}
|
||||
&:before {
|
||||
opacity: 0;
|
||||
transform: translateY(-70px);
|
||||
}
|
||||
&:after {
|
||||
opacity: 0;
|
||||
box-shadow: 0 2px 2px black;
|
||||
}
|
||||
}
|
||||
|
||||
&: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(-3px);
|
||||
&: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));
|
||||
}
|
||||
|
||||
.inline-embed .el-pre.mod-frontmatter.mod-ui {
|
||||
padding-top: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&.show-inline-title .workspace-leaf-content[data-sb] .view-content > .markdown-source-view.mod-cm6 > .cm-editor > .cm-scroller,
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-mobile {
|
||||
.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 {
|
||||
.metadata-container {
|
||||
left: calc(50% + (var(--size-4-1) * 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
39
src/types/enums.ts
Normal file
39
src/types/enums.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
//----------------------------------
|
||||
// Plugin Enums
|
||||
//----------------------------------
|
||||
export enum IconType {
|
||||
Link = 'link',
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
export enum ViewMode {
|
||||
Source = 'source',
|
||||
Reading = 'preview',
|
||||
}
|
||||
|
||||
//----------------------------------
|
||||
// Settings Enums
|
||||
//----------------------------------
|
||||
export enum DeviceType {
|
||||
Desktop = 'desktop',
|
||||
Tablet = 'tablet',
|
||||
Phone = 'phone',
|
||||
}
|
||||
|
||||
export enum CSSValue {
|
||||
RevertLayer = 'revert-layer',
|
||||
Auto = 'auto',
|
||||
Initial = 'initial',
|
||||
// Inherit = 'inherit',
|
||||
Transparent = 'transparent',
|
||||
Repeat = 'repeat',
|
||||
}
|
||||
|
||||
export enum CSSClasses {
|
||||
Main = 'simple-banner',
|
||||
Icon = 'icon',
|
||||
Datetime = 'date-time',
|
||||
Static = 'static',
|
||||
Autohide = 'sb-autohide',
|
||||
IsWindows = 'is-windows',
|
||||
}
|
||||
87
src/types/interfaces.ts
Normal file
87
src/types/interfaces.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { MarkdownView } from 'obsidian';
|
||||
import { IconType, ViewMode } from './enums';
|
||||
|
||||
//----------------------------------
|
||||
// Plugin Interfaces
|
||||
//----------------------------------
|
||||
export interface BannerData {
|
||||
filepath: string | null;
|
||||
image: string | null;
|
||||
icon: string | null,
|
||||
viewMode: ViewMode | null;
|
||||
lastViewMode: ViewMode | null;
|
||||
isImageChange: boolean;
|
||||
isImagePropsUpdate: boolean;
|
||||
needsUpdate: boolean;
|
||||
view: MarkdownView | null | undefined;
|
||||
}
|
||||
|
||||
export interface ImageOptions {
|
||||
url: string,
|
||||
external: boolean,
|
||||
x: number;
|
||||
y: number;
|
||||
repeatable: boolean;
|
||||
}
|
||||
|
||||
export interface IconData {
|
||||
value: string | null,
|
||||
type: IconType,
|
||||
}
|
||||
|
||||
export interface Datastore {
|
||||
[key: string]: BannerData;
|
||||
}
|
||||
|
||||
//----------------------------------
|
||||
// Settings Interfaces
|
||||
//----------------------------------
|
||||
export interface SimpleBannerSettings {
|
||||
desktop: DeviceSettings
|
||||
tablet: DeviceSettings
|
||||
phone: DeviceSettings
|
||||
properties: PropertySettings;
|
||||
}
|
||||
|
||||
export interface DeviceSettings {
|
||||
bannerEnabled: boolean;
|
||||
height: number;
|
||||
noteOffset: number;
|
||||
bannerRadius: Array<number>;
|
||||
bannerPadding: number;
|
||||
bannerFade: boolean;
|
||||
|
||||
iconEnabled: boolean,
|
||||
iconSize: number;
|
||||
iconRadius: number;
|
||||
iconBackground: boolean;
|
||||
iconBorder: number;
|
||||
iconAlignment: Array<string>;
|
||||
iconOffset: Array<number>;
|
||||
}
|
||||
|
||||
export interface PropertySettings {
|
||||
autohide: boolean;
|
||||
image: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface SettingCompOptions {
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
placeholders?: Array<string>;
|
||||
choices?: ValueLabelPair[];
|
||||
classes?: Array<string>;
|
||||
resettable?: boolean;
|
||||
float?: boolean,
|
||||
isValueArray?: boolean,
|
||||
length?: number,
|
||||
resetValue?: any,
|
||||
refreshOnUpdate?: boolean;
|
||||
}
|
||||
|
||||
export interface ValueLabelPair {
|
||||
value: any,
|
||||
label: string,
|
||||
}
|
||||
37
src/utils/domutils.ts
Normal file
37
src/utils/domutils.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import SimpleBanner from '../main';
|
||||
|
||||
let instance: SimpleBanner;
|
||||
export default class DomUtils {
|
||||
static init(plugin: SimpleBanner) {
|
||||
instance = plugin;
|
||||
}
|
||||
static calculateFontsize(textContent: string) {
|
||||
const temp = document.createElement('span');
|
||||
temp.setAttribute('style', 'position: absolute; visibility: hidden; white-space: nowrap;');
|
||||
temp.style.padding = '0';
|
||||
temp.style.margin = '0';
|
||||
temp.style.left = '-9999px';
|
||||
temp.textContent = textContent.toUpperCase();
|
||||
document.body.appendChild(temp);
|
||||
const size = instance.deviceSettings.iconSize;
|
||||
const checkWidth = size - 16;
|
||||
|
||||
let fontSize = size; // Start big
|
||||
temp.style.fontSize = fontSize + 'px';
|
||||
|
||||
while (temp.offsetWidth > checkWidth && fontSize > 1) {
|
||||
fontSize -= 1;
|
||||
temp.style.fontSize = fontSize + 'px';
|
||||
}
|
||||
|
||||
document.body.removeChild(temp);
|
||||
return `${fontSize}px`;
|
||||
}
|
||||
|
||||
static setCSSVariables(variables: Record<string, string>, target: HTMLElement = document.body) {
|
||||
const style = target.style;
|
||||
Object.keys(variables).forEach(k => {
|
||||
style.setProperty(`--sb-${k}`, variables[k]);
|
||||
});
|
||||
}
|
||||
}
|
||||
161
src/utils/parse.ts
Normal file
161
src/utils/parse.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { MarkdownView } from 'obsidian';
|
||||
import SimpleBanner from '../main';
|
||||
import { IconData, ImageOptions } from '../types/interfaces';
|
||||
import { IconType } from '../types/enums';
|
||||
|
||||
let instance: SimpleBanner;
|
||||
const RegExpression = {
|
||||
Wikilink: /^!?\[\[([^\]]+?)(\|([^\]]+?))?\]\]$/,
|
||||
Markdown: /^!?\[([^\]]*)\]\(([^)]+?)\)$/,
|
||||
MarkdownBare: /^!?<([^>]+)>$/,
|
||||
Weblink: /^https?:\/\//i,
|
||||
};
|
||||
|
||||
export default class Parse {
|
||||
|
||||
static init(plugin: SimpleBanner) {
|
||||
instance = plugin;
|
||||
}
|
||||
|
||||
static link(str: string, view?: MarkdownView | null, settingProperty?: string | null): ImageOptions {
|
||||
let url: string | null = null;
|
||||
let displayText: string | null = null;
|
||||
let external: boolean;
|
||||
let obsidianUrl: boolean = false;
|
||||
let options = { x: 0, y: 0, repeatable: false };
|
||||
|
||||
const wikilinkMatch = str.match(RegExpression.Wikilink);
|
||||
if (wikilinkMatch) {
|
||||
url = wikilinkMatch[1].trim();
|
||||
displayText = wikilinkMatch[3] ? wikilinkMatch[3].trim() : null;
|
||||
}
|
||||
|
||||
const markdownMatch = str.match(RegExpression.Markdown);
|
||||
const markdownBareMatch = str.match(RegExpression.MarkdownBare);
|
||||
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 = RegExpression.Weblink.test(url);
|
||||
|
||||
if (this.isObsidianUrl(url)) {
|
||||
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.imageProperties(url.substring(hashIndex + 1));
|
||||
url = url.replace(/#.*/, '').trim();
|
||||
}
|
||||
|
||||
if (displayText) {
|
||||
options = this.imageProperties(displayText);
|
||||
}
|
||||
|
||||
if (!external) {
|
||||
const vault = instance.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 = instance.app.workspace.getActiveFile();
|
||||
if (activeFile) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
instance.app.fileManager.processFrontMatter(activeFile, (frontmatter) => {
|
||||
const propName = settingProperty || instance.settingProperties.image;
|
||||
frontmatter[propName] = `[[${file?.path}]]`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: url.trim(),
|
||||
external,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
static imageProperties(str: string): { x: number, y: number, repeatable: boolean } {
|
||||
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 };
|
||||
}
|
||||
|
||||
static icon(icon: string, view?: MarkdownView | null): IconData {
|
||||
const str = icon || '';
|
||||
const out = { value: null, type: IconType.Text } as IconData;
|
||||
|
||||
if (RegExpression.Wikilink.test(str)) {
|
||||
out.type = IconType.Link;
|
||||
} else if (RegExpression.Markdown.test(str) || RegExpression.MarkdownBare.test(str)) {
|
||||
out.type = IconType.Link;
|
||||
} else if (RegExpression.Weblink.test(icon)) {
|
||||
out.type = IconType.Link;
|
||||
} else if (this.isObsidianUrl(icon)) {
|
||||
out.type = IconType.Link;
|
||||
}
|
||||
|
||||
if (out.type === IconType.Link) {
|
||||
const data = this.link(str, view, instance.settingProperties.icon);
|
||||
out.value = data.url;
|
||||
} else {
|
||||
out.value = str;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static isImagePropertiesUpdate(oldstr?: string | null, newstr?: string | null, view?: MarkdownView | null): boolean {
|
||||
if (!oldstr || !newstr) {
|
||||
return false;
|
||||
}
|
||||
const oldopt = this.link(oldstr, view);
|
||||
const newopt = this.link(newstr, view);
|
||||
return oldopt.url === newopt.url;
|
||||
}
|
||||
|
||||
static isObsidianUrl(url: string): boolean {
|
||||
return url.startsWith('obsidian://open');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue