mirror of
https://github.com/marc-f/obsidian-file-name-styler.git
synced 2026-07-22 05:46:09 +00:00
Release v1.0.0 – initial plugin version
This commit is contained in:
commit
60ca770a86
12 changed files with 825 additions and 0 deletions
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Compiled output
|
||||
main.js
|
||||
main.js.map
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Node.js dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# Build output (ZIPs and temporary folders)
|
||||
build/
|
||||
*.zip
|
||||
|
||||
# OS-specific files
|
||||
.DS_Store
|
||||
|
||||
# PhpStorm project files
|
||||
.idea/
|
||||
|
||||
# Optional: IDE-specific workspace files
|
||||
*.iml
|
||||
|
||||
# Optional: Logs and misc
|
||||
*.log
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Marc Feininger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
88
README.md
Normal file
88
README.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# FileName Styler
|
||||
|
||||
**FileName Styler** is a plugin for [Obsidian](https://obsidian.md) that allows you to hide, modify, and decorate parts of file names in the sidebar based on patterns or folder locations.
|
||||
|
||||
Originally created as simple “Zettelkasten ID Hider,” this plugin has evolved into a flexible tool for styling file names based on regex or timestamp-based IDs.
|
||||
|
||||
✨ **Special thanks to Simon for the original idea and inspiration behind this plugin.**
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- Hide timestamp-based IDs (8-, 12-, or 14-digit) from file names
|
||||
- Support for **custom regular expressions** with 3 capture groups
|
||||
- Add custom **prefixes** and **suffixes** to matching file names
|
||||
- Apply **custom colors** and **custom file icons** (e.g. 🧠 or 📖)
|
||||
- Restrict styling to specific folders only
|
||||
- Combine multiple styling **profiles** – different styles for different folders or file types
|
||||
- Automatically restore original file names and styles when unloading
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
This plugin searches for matching file names based on selected ID formats:
|
||||
|
||||
- `yyyyMMddHHmmss` → 14-digit
|
||||
- `yyMMddHHmmss` → 12-digit
|
||||
- `yyyyMMdd` → 8-digit
|
||||
- or a **custom regular expression**, such as:
|
||||
|
||||
```regex
|
||||
^(\d{6})([-_ ]?)(.+)$
|
||||
```
|
||||
|
||||
It then applies the configured styling to each match within the File Explorer (without changing the actual file name on disk).
|
||||
|
||||
---
|
||||
|
||||
## Settings
|
||||
|
||||
You can configure and combine **multiple profiles**. Each profile supports:
|
||||
|
||||
- **ID format** – Choose from predefined timestamp formats or define your own regex
|
||||
- **Move ID to end** – Display the ID at the end of the file name instead of the start
|
||||
- **Custom prefix** – Text to prepend to the displayed name
|
||||
- **Custom suffix** – Text to append to the displayed name
|
||||
- **Custom text color** – Optional color override
|
||||
- **Custom icon** – Emoji or Unicode (e.g. `🧠`, `📖`)
|
||||
- **Folder filter** – Apply profile only in certain folders
|
||||
|
||||
All profiles can be toggled independently and are applied in order.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download or clone this repository.
|
||||
2. Copy the following files into your Obsidian vault under:
|
||||
|
||||
```
|
||||
.obsidian/plugins/filename-styler/
|
||||
```
|
||||
|
||||
Required files:
|
||||
- `main.js`
|
||||
- `manifest.json`
|
||||
- (optional) `styles.css`
|
||||
|
||||
3. Enable the plugin in Obsidian under
|
||||
`Settings → Community Plugins`.
|
||||
|
||||
---
|
||||
|
||||
## Compatibility
|
||||
|
||||
Compatible with Obsidian v1.8.9 and higher.
|
||||
Earlier versions might also work, but have not been tested.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License.
|
||||
|
||||
---
|
||||
|
||||
**Created with ❤️ by and for the Obsidian community.**
|
||||
49
esbuild.config.mjs
Normal file
49
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
29
eslint.config.js
Normal file
29
eslint.config.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import js from '@eslint/js';
|
||||
import ts from '@typescript-eslint/eslint-plugin';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': ts
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { args: 'none' }],
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off'
|
||||
}
|
||||
}
|
||||
];
|
||||
535
main.ts
Normal file
535
main.ts
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
import { App, Plugin, PluginSettingTab, Setting, Modal } from "obsidian";
|
||||
|
||||
interface FileNameStylerProfileSettings {
|
||||
idFormat: string;
|
||||
customIdRegex?: string;
|
||||
moveIdToEnd: boolean;
|
||||
showCustomSuffix: boolean;
|
||||
customSuffix: string;
|
||||
customColor: string;
|
||||
enableCustomColor: boolean;
|
||||
onlyEnabledFolders: boolean;
|
||||
enabledFolders: string[];
|
||||
showCustomPrefix: boolean;
|
||||
customPrefix: string;
|
||||
showCustomFileIcon: boolean;
|
||||
customFileIcon: string;
|
||||
}
|
||||
|
||||
interface FileNameStylerGlobalSettings {
|
||||
profiles: Record<string, Partial<FileNameStylerProfileSettings>>;
|
||||
activeProfiles: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_PROFILE_SETTINGS: FileNameStylerProfileSettings = {
|
||||
idFormat: "12",
|
||||
moveIdToEnd: false,
|
||||
showCustomSuffix: false,
|
||||
customSuffix: " (z)",
|
||||
customColor: "#4b0082",
|
||||
enableCustomColor: false,
|
||||
onlyEnabledFolders: false,
|
||||
enabledFolders: [],
|
||||
showCustomPrefix: false,
|
||||
customPrefix: "[Z] ",
|
||||
showCustomFileIcon: false,
|
||||
customFileIcon: "🧠",
|
||||
};
|
||||
|
||||
export default class FileNameStylerPlugin extends Plugin {
|
||||
settings!: FileNameStylerGlobalSettings;
|
||||
observer!: MutationObserver;
|
||||
|
||||
async onload() {
|
||||
console.log("FileName Styler Plugin loaded");
|
||||
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new FileNameStylerSettingTab(this.app, this));
|
||||
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
if (mutations.some(m => m.target instanceof HTMLElement && m.target.closest(".nav-file-title"))) {
|
||||
setTimeout(() => this.refreshAll(), 50);
|
||||
}
|
||||
});
|
||||
|
||||
this.refreshAll();
|
||||
|
||||
this.observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
refreshAll() {
|
||||
if (this.observer) this.observer.disconnect();
|
||||
|
||||
this.restoreOriginalFileTitles();
|
||||
this.applyStylingToFileNames();
|
||||
|
||||
this.observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log("FileName Styler Plugin unloaded");
|
||||
this.observer.disconnect();
|
||||
this.restoreOriginalFileTitles();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign(
|
||||
{ profiles: {}, activeProfiles: [] },
|
||||
await this.loadData()
|
||||
);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
applyStylingToFileNames() {
|
||||
const fileTitles = document.querySelectorAll(".nav-file-title-content");
|
||||
|
||||
fileTitles.forEach((el: Element) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const parent = el.closest(".tree-item-self");
|
||||
const path = parent?.getAttribute("data-path") || "";
|
||||
const originalText = htmlEl.dataset.originalTitle || htmlEl.textContent || "";
|
||||
|
||||
for (const profileName of this.settings.activeProfiles) {
|
||||
const profile = this.settings.profiles[profileName];
|
||||
if (!profile) continue;
|
||||
|
||||
if (
|
||||
profile.onlyEnabledFolders &&
|
||||
!(profile.enabledFolders?.some(folder => path.startsWith(folder)))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let regex: RegExp | null = null;
|
||||
if (profile.idFormat === "custom" && profile.customIdRegex) {
|
||||
try {
|
||||
regex = new RegExp(profile.customIdRegex);
|
||||
} catch (e) {
|
||||
console.warn(`Invalid custom regex in profile '${profileName}':`, e);
|
||||
continue;
|
||||
}
|
||||
} else if (profile.idFormat) {
|
||||
regex = new RegExp(`^(\\d{${profile.idFormat}})([-_ ]?)(.+)`);
|
||||
}
|
||||
|
||||
if (!regex) continue;
|
||||
|
||||
const match = originalText.match(regex);
|
||||
if (!match) continue;
|
||||
|
||||
this.applyIdHiding(htmlEl, match, originalText, profile);
|
||||
this.applyProfileStyling(htmlEl, profile);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyIdHiding(el: HTMLElement, match: RegExpMatchArray, originalText: string, profile: Partial<FileNameStylerProfileSettings>) {
|
||||
if (el.dataset.originalTitle) return;
|
||||
|
||||
el.dataset.originalTitle = originalText;
|
||||
|
||||
const moveToEnd = profile.moveIdToEnd ?? false;
|
||||
|
||||
el.textContent = moveToEnd
|
||||
? `${match[3]} ${match[1]}`
|
||||
: match[3];
|
||||
}
|
||||
|
||||
|
||||
applyProfileStyling(el: HTMLElement, profile: Partial<FileNameStylerProfileSettings>) {
|
||||
if (profile.showCustomPrefix && profile.customPrefix && !el.textContent?.startsWith(profile.customPrefix)) {
|
||||
el.textContent = `${profile.customPrefix}${el.textContent}`;
|
||||
}
|
||||
|
||||
if (profile.showCustomSuffix && profile.customSuffix && !el.textContent?.endsWith(profile.customSuffix)) {
|
||||
el.textContent = `${el.textContent}${profile.customSuffix}`;
|
||||
}
|
||||
|
||||
if (profile.enableCustomColor && profile.customColor) {
|
||||
el.style.color = profile.customColor;
|
||||
}
|
||||
|
||||
if (profile.showCustomFileIcon && profile.customFileIcon) {
|
||||
el.style.setProperty("--file-icon", `"${profile.customFileIcon}"`);
|
||||
}
|
||||
}
|
||||
|
||||
restoreOriginalFileTitles() {
|
||||
const fileTitles = document.querySelectorAll(".nav-file-title-content");
|
||||
|
||||
fileTitles.forEach((el: Element) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
|
||||
if (htmlEl.dataset.originalTitle) {
|
||||
htmlEl.textContent = htmlEl.dataset.originalTitle;
|
||||
delete htmlEl.dataset.originalTitle;
|
||||
}
|
||||
|
||||
htmlEl.style.color = "";
|
||||
htmlEl.style.removeProperty("--file-icon");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class FileNameStylerSettingTab extends PluginSettingTab {
|
||||
plugin: FileNameStylerPlugin;
|
||||
newProfileName: string = "";
|
||||
|
||||
constructor(app: App, plugin: FileNameStylerPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
containerEl.empty();
|
||||
|
||||
const profileHeading = containerEl.createDiv("setting-item setting-item-heading");
|
||||
const profileInfo = profileHeading.createDiv("setting-item-info");
|
||||
profileInfo.createDiv({cls: "setting-item-name", text: "Filename styling profiles"});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("New Profile")
|
||||
.setDesc("Enter a profile name and add it")
|
||||
.addText(text => {
|
||||
text.setPlaceholder("e.g. Focus Mode")
|
||||
.onChange(value => this.newProfileName = value);
|
||||
})
|
||||
.addExtraButton(button => {
|
||||
button.setIcon("circle-plus")
|
||||
.setTooltip("Add profile")
|
||||
.onClick(async () => {
|
||||
if (!this.newProfileName) return;
|
||||
this.plugin.settings.profiles[this.newProfileName] = this.getCleanSettings();
|
||||
await this.plugin.saveSettings();
|
||||
this.newProfileName = "";
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(this.plugin.settings.profiles).forEach(profileName => {
|
||||
const isActive = this.plugin.settings.activeProfiles.includes(profileName);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(profileName)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(isActive)
|
||||
.onChange(async (value) => {
|
||||
const idx = this.plugin.settings.activeProfiles.indexOf(profileName);
|
||||
if (value && idx === -1) {
|
||||
this.plugin.settings.activeProfiles.push(profileName);
|
||||
} else if (!value && idx > -1) {
|
||||
this.plugin.settings.activeProfiles.splice(idx, 1);
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAll();
|
||||
this.display();
|
||||
}))
|
||||
.addExtraButton(btn => {
|
||||
btn.setIcon("settings")
|
||||
.setTooltip("Show profile details")
|
||||
.onClick(() => {
|
||||
new ProfileModal(this.app, profileName, this.plugin.settings.profiles[profileName], this.plugin).open();
|
||||
});
|
||||
})
|
||||
.addExtraButton(btn => {
|
||||
btn.setIcon("x")
|
||||
.setTooltip("Remove profile")
|
||||
.onClick(async () => {
|
||||
delete this.plugin.settings.profiles[profileName];
|
||||
this.plugin.settings.activeProfiles = this.plugin.settings.activeProfiles.filter(p => p !== profileName);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getCleanSettings(): Partial<FileNameStylerProfileSettings> {
|
||||
return { ...DEFAULT_PROFILE_SETTINGS };
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileModal extends Modal {
|
||||
constructor(
|
||||
app: App,
|
||||
private profileName: string,
|
||||
private profileData: Partial<FileNameStylerProfileSettings>,
|
||||
private plugin: FileNameStylerPlugin
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
this.modalEl.style.maxWidth = "70vw";
|
||||
this.modalEl.style.width = "700px";
|
||||
|
||||
contentEl.createEl("h2", { text: `Profile: ${this.profileName}` });
|
||||
|
||||
const isActive = this.plugin.settings.activeProfiles.includes(this.profileName);
|
||||
|
||||
if (!isActive) {
|
||||
contentEl.createEl("p", {
|
||||
text: "This profile is currently inactive. Changes will take effect when it's active.",
|
||||
cls: "mod-warning"
|
||||
});
|
||||
}
|
||||
|
||||
this.createSectionHeading(contentEl, "Filename ID format");
|
||||
new Setting(contentEl)
|
||||
.setName("ID Format")
|
||||
.setDesc("Select the ID format used at the beginning of filenames.")
|
||||
.addDropdown(drop => {
|
||||
drop.addOption("14", "14-digit (yyyyMMddHHmmss)");
|
||||
drop.addOption("12", "12-digit (yyMMddHHmmss)");
|
||||
drop.addOption("8", "8-digit (yyyyMMdd)");
|
||||
drop.addOption("custom", "Custom regex");
|
||||
|
||||
drop.setValue(this.profileData.idFormat ?? "12");
|
||||
|
||||
drop.onChange(async (value) => {
|
||||
this.profileData.idFormat = value;
|
||||
this.plugin.settings.profiles[this.profileName] = this.profileData;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAll();
|
||||
this.onOpen();
|
||||
});
|
||||
});
|
||||
|
||||
if (this.profileData.idFormat === "custom") {
|
||||
new Setting(contentEl)
|
||||
.setName("Custom ID Regex")
|
||||
.setDesc(
|
||||
"Enter a custom regular expression to extract the ID and filename\n" +
|
||||
"The custom regex must contain exactly three capturing groups:\n" +
|
||||
" (1) the ID\n" +
|
||||
" (2) an optional separator (e.g. -, _, space)\n" +
|
||||
" (3) the remaining file name\n" +
|
||||
"Example: ^(\\d{6})([-_ ]?)(.+)$"
|
||||
)
|
||||
.addText(text => {
|
||||
text.setPlaceholder("e.g. ^(\\d{6})([-_ ]?)(.+)$")
|
||||
.setValue(this.profileData.customIdRegex ?? "")
|
||||
.onChange(async (value) => {
|
||||
this.profileData.customIdRegex = value;
|
||||
this.plugin.settings.profiles[this.profileName] = this.profileData;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.createSectionHeading(contentEl, "ID position in filename");
|
||||
this.addProfileToggleSetting(
|
||||
contentEl,
|
||||
"Move ID to end",
|
||||
"Moves the ID to the end of the filename",
|
||||
"moveIdToEnd"
|
||||
);
|
||||
|
||||
this.createSectionHeading(contentEl, "Folder filter");
|
||||
this.addProfileToggleSetting(
|
||||
contentEl,
|
||||
"Restrict to folders",
|
||||
"Only apply formatting in specified folders",
|
||||
"onlyEnabledFolders"
|
||||
);
|
||||
const folderList = contentEl.createDiv();
|
||||
const renderFolderList = () => {
|
||||
folderList.empty();
|
||||
const folders = this.profileData.enabledFolders ?? [];
|
||||
|
||||
folders.forEach((folder, index) => {
|
||||
new Setting(folderList)
|
||||
.setName(folder)
|
||||
.addExtraButton(button => {
|
||||
button.setIcon("x")
|
||||
.setTooltip("Remove folder")
|
||||
.onClick(async () => {
|
||||
folders.splice(index, 1);
|
||||
this.profileData.enabledFolders = folders;
|
||||
this.plugin.settings.profiles[this.profileName] = this.profileData;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAll();
|
||||
renderFolderList();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
renderFolderList();
|
||||
let newFolderPath = "";
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Add folder")
|
||||
.setDesc("Add a folder prefix (e.g. 'projects/', 'notes/', or 'inbox'). Files in these folders will be styled.")
|
||||
.addText(text => {
|
||||
text.setPlaceholder("folder or prefix")
|
||||
.onChange(value => {
|
||||
newFolderPath = value;
|
||||
});
|
||||
})
|
||||
.addButton(btn => {
|
||||
btn.setButtonText("Add")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
if (!newFolderPath) return;
|
||||
|
||||
const folders = this.profileData.enabledFolders ?? [];
|
||||
if (!folders.includes(newFolderPath)) {
|
||||
folders.push(newFolderPath);
|
||||
this.profileData.enabledFolders = folders;
|
||||
this.plugin.settings.profiles[this.profileName] = this.profileData;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAll();
|
||||
renderFolderList();
|
||||
}
|
||||
newFolderPath = "";
|
||||
});
|
||||
});
|
||||
|
||||
this.createSectionHeading(contentEl, "Text color styling");
|
||||
this.addProfileToggleSetting(
|
||||
contentEl,
|
||||
"Enable custom text color",
|
||||
"Turn on/off custom color for this profile",
|
||||
"enableCustomColor"
|
||||
);
|
||||
new Setting(contentEl)
|
||||
.setName("Custom text color")
|
||||
.setDesc("Color used when color is enabled")
|
||||
.addColorPicker(picker => {
|
||||
picker.setValue(this.profileData.customColor || "#4b0082");
|
||||
picker.onChange(async (value) => {
|
||||
this.profileData.customColor = value;
|
||||
this.plugin.settings.profiles[this.profileName] = this.profileData;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAll();
|
||||
});
|
||||
});
|
||||
|
||||
this.createSectionHeading(contentEl, "Custom file icon options");
|
||||
this.addProfileToggleSetting(
|
||||
contentEl,
|
||||
"Show custom file icon",
|
||||
"Apply a custom file icon such as 🧠.",
|
||||
"showCustomFileIcon"
|
||||
);
|
||||
this.addProfileTextSetting(
|
||||
contentEl,
|
||||
"Custom file icon",
|
||||
"Enter an emoji (e.g. 🧠) or Unicode like \\1F4D6 for 📖.",
|
||||
"customFileIcon",
|
||||
);
|
||||
|
||||
this.createSectionHeading(contentEl, "Prefix styling");
|
||||
this.addProfileToggleSetting(
|
||||
contentEl,
|
||||
"Show prefix styling",
|
||||
"Add a prefix like '[Z]' at the start of file names.",
|
||||
"showCustomPrefix"
|
||||
);
|
||||
this.addProfileTextSetting(
|
||||
contentEl,
|
||||
"Prefix",
|
||||
"Text to prepend to file names, e.g. [Z]",
|
||||
"customPrefix",
|
||||
);
|
||||
|
||||
this.createSectionHeading(contentEl, "Suffix styling");
|
||||
this.addProfileToggleSetting(
|
||||
contentEl,
|
||||
"Show suffix styling",
|
||||
"Display a suffix like '(z)' at the end of file names.",
|
||||
"showCustomSuffix"
|
||||
);
|
||||
this.addProfileTextSetting(
|
||||
contentEl,
|
||||
"Suffix",
|
||||
"Text to append to file names, e.g. [Z]",
|
||||
"customSuffix",
|
||||
);
|
||||
|
||||
const closeBtn = contentEl.createEl("button", { text: "Close" });
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
|
||||
private createSectionHeading(container: HTMLElement, title: string, description?: string): void {
|
||||
const heading = container.createDiv("setting-item setting-item-heading");
|
||||
const info = heading.createDiv("setting-item-info");
|
||||
info.createDiv({ cls: "setting-item-name", text: title });
|
||||
if (description) {
|
||||
info.createDiv({ cls: "setting-item-description", text: description });
|
||||
}
|
||||
}
|
||||
|
||||
private addProfileToggleSetting(
|
||||
container: HTMLElement,
|
||||
label: string,
|
||||
description: string,
|
||||
key: keyof FileNameStylerProfileSettings
|
||||
): void {
|
||||
new Setting(container)
|
||||
.setName(label)
|
||||
.setDesc(description)
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(this.profileData[key] as boolean ?? false);
|
||||
toggle.onChange(async (value) => {
|
||||
this.profileData[key] = value as any;
|
||||
Object.assign(this.plugin.settings.profiles[this.profileName], this.profileData);
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private addProfileTextSetting(
|
||||
container: HTMLElement,
|
||||
label: string,
|
||||
description: string,
|
||||
key: keyof FileNameStylerProfileSettings
|
||||
): void {
|
||||
new Setting(container)
|
||||
.setName(label)
|
||||
.setDesc(description)
|
||||
.addText(text => {
|
||||
text.setPlaceholder("")
|
||||
.setValue((this.profileData[key] as string) ?? DEFAULT_PROFILE_SETTINGS[key] as string)
|
||||
.onChange(async (value) => {
|
||||
this.profileData[key] = value as any;
|
||||
Object.assign(this.plugin.settings.profiles[this.profileName], this.profileData);
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAll();
|
||||
});
|
||||
})
|
||||
.addExtraButton(button => {
|
||||
button.setIcon("reset")
|
||||
.setTooltip("Reset to default")
|
||||
.onClick(async () => {
|
||||
this.profileData[key] = DEFAULT_PROFILE_SETTINGS[key] as any;
|
||||
Object.assign(this.plugin.settings.profiles[this.profileName], this.profileData);
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAll();
|
||||
this.onOpen();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "filename-styler",
|
||||
"name": "FileName Styler",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "1.8.9",
|
||||
"description": "Customize and style file names in the file explorer using patterns, prefixes, suffixes, colors, and icons.",
|
||||
"author": "Marc Feininger",
|
||||
"authorUrl": "https://github.com/marc-f/obsidian-file-name-styler",
|
||||
"main": "main.js"
|
||||
}
|
||||
35
package.json
Normal file
35
package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "filename-styler",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "An Obsidian plugin to style filenames based on ID patterns, colors, icons, and more.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"lint": "eslint . --ext .ts"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
"plugin",
|
||||
"filename",
|
||||
"styling",
|
||||
"zettelkasten",
|
||||
"customization"
|
||||
],
|
||||
"author": "Marc Feininger",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@types/node": "^22.14.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
||||
"@typescript-eslint/parser": "^8.29.1",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"esbuild": "^0.25.2",
|
||||
"eslint": "^9.24.0",
|
||||
"globals": "^16.0.0",
|
||||
"obsidian": "latest",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
1
styles.css
Normal file
1
styles.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* Placeholder style sheet */
|
||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "CommonJS",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "node",
|
||||
"outDir": ".",
|
||||
"baseUrl": ".",
|
||||
"types": ["obsidian", "node"]
|
||||
},
|
||||
"files": ["main.ts"]
|
||||
}
|
||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "1.8.9"
|
||||
}
|
||||
Loading…
Reference in a new issue