├── .npmrc ├── .eslintignore ├── src ├── index.ts ├── modules │ ├── SettingsPanel │ │ ├── WideTextInput.tsx │ │ ├── SettingItemControlFull.tsx │ │ └── index.tsx │ └── SetColorModalContent │ │ ├── ColorName.tsx │ │ ├── ColorGrid.tsx │ │ ├── ColorCell.tsx │ │ ├── Color.tsx │ │ └── index.tsx ├── config │ └── styles.ts ├── components │ ├── Button │ │ └── index.tsx │ ├── icons │ │ ├── AddCircleIcon.tsx │ │ └── TrashIcon.tsx │ └── SettingItem │ │ └── index.tsx ├── hooks │ ├── useModal.tsx │ ├── useFile.tsx │ └── usePlugin.tsx ├── settings.ts ├── global.d.ts └── plugin │ ├── FileColorSettingTab.tsx │ ├── SetColorModal.tsx │ └── FileColorPlugin.ts ├── .prettierrc.json ├── docs └── images │ ├── hero-rounded.png │ ├── add-color-rounded.gif │ ├── icons-notes-rounded.png │ └── set-color-rounded.gif ├── versions.json ├── .editorconfig ├── manifest.json ├── .gitignore ├── tsconfig.json ├── version-bump.mjs ├── .eslintrc ├── LICENSE ├── package.json ├── styles.css ├── esbuild.config.mjs └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { FileColorPlugin } from './plugin/FileColorPlugin' 2 | export default FileColorPlugin -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /docs/images/hero-rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecustic/obsidian-file-color/HEAD/docs/images/hero-rounded.png -------------------------------------------------------------------------------- /docs/images/add-color-rounded.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecustic/obsidian-file-color/HEAD/docs/images/add-color-rounded.gif -------------------------------------------------------------------------------- /docs/images/icons-notes-rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecustic/obsidian-file-color/HEAD/docs/images/icons-notes-rounded.png -------------------------------------------------------------------------------- /docs/images/set-color-rounded.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecustic/obsidian-file-color/HEAD/docs/images/set-color-rounded.gif -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.0.1": "0.15.0", 4 | "1.0.2": "0.15.0", 5 | "1.0.3": "1.2.7", 6 | "1.1.0": "1.2.7" 7 | } -------------------------------------------------------------------------------- /src/modules/SettingsPanel/WideTextInput.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'config/styles'; 2 | 3 | export const WideTextInput = styled('input')` 4 | display: flex; 5 | flex: 1; 6 | ` -------------------------------------------------------------------------------- /src/config/styles.ts: -------------------------------------------------------------------------------- 1 | import * as goober from "goober"; 2 | import React from "react"; 3 | 4 | goober.setup(React.createElement); 5 | 6 | export const css = goober.css 7 | export const styled = goober.styled 8 | -------------------------------------------------------------------------------- /src/modules/SetColorModalContent/ColorName.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'config/styles' 2 | 3 | export const ColorName = styled('small')` 4 | display: block; 5 | text-align: center; 6 | margin-top: 12px; 7 | ` 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /src/modules/SetColorModalContent/ColorGrid.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'config/styles' 2 | 3 | export const ColorGrid = styled('div')` 4 | display: grid; 5 | grid-template-columns: repeat(4, 1fr); 6 | place-items: center; 7 | ` 8 | -------------------------------------------------------------------------------- /src/modules/SettingsPanel/SettingItemControlFull.tsx: -------------------------------------------------------------------------------- 1 | import { SettingItemControl } from "components/SettingItem"; 2 | import { styled } from 'config/styles'; 3 | 4 | export const SettingItemControlFull = styled(SettingItemControl)` 5 | justify-content: space-between; 6 | ` 7 | 8 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'config/styles'; 2 | 3 | export const Button = styled('button')({ 4 | display: 'inline-flex', 5 | justifyContent: 'center', 6 | alignItems: 'center', 7 | 8 | '.svg-icon + *': { 9 | marginLeft: '8px', 10 | }, 11 | }) -------------------------------------------------------------------------------- /src/modules/SetColorModalContent/ColorCell.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'config/styles' 2 | 3 | export const ColorCell = styled('div')` 4 | display: flex; 5 | flex-direction: column; 6 | cursor: pointer; 7 | margin: 16px; 8 | align-items: center; 9 | justify-content: center; 10 | ` 11 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-file-color", 3 | "name": "File Color", 4 | "version": "1.1.0", 5 | "minAppVersion": "1.2.7", 6 | "description": "An Obsidian plugin for setting colors on folders and files in the file tree.", 7 | "author": "ecustic", 8 | "authorUrl": "https://github.com/ecustic", 9 | "fundingUrl": "https://www.buymeacoffee.com/ecustic", 10 | "isDesktopOnly": false 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store -------------------------------------------------------------------------------- /src/hooks/useModal.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | import { Modal } from 'obsidian' 3 | 4 | export const ModalContext = createContext( 5 | undefined 6 | ) 7 | 8 | export const useModal = () => { 9 | const context = useContext(ModalContext) 10 | 11 | if (!context) { 12 | throw new Error('Missing ModalContext provider.') 13 | } 14 | 15 | return context; 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/useFile.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | import { TAbstractFile } from 'obsidian' 3 | 4 | export const FileContext = createContext( 5 | undefined 6 | ) 7 | 8 | export const useFile = () => { 9 | const context = useContext(FileContext) 10 | 11 | if (!context) { 12 | throw new Error('Missing FileContext provider.') 13 | } 14 | 15 | return context; 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/SetColorModalContent/Color.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'config/styles' 2 | 3 | export const Color = styled<{ selected?: boolean; className?: string }>('div')` 4 | display: block; 5 | width: 32px; 6 | height: 32px; 7 | border-style: solid; 8 | border-radius: 32px; 9 | border-color: ${(props) => 10 | props.selected ? 'var(--color-base-70)' : 'var(--color-base-40)'}; 11 | background-color: var(--file-color-color); 12 | ` 13 | -------------------------------------------------------------------------------- /src/hooks/usePlugin.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | import { FileColorPlugin } from 'plugin/FileColorPlugin' 3 | 4 | export const PluginContext = createContext( 5 | undefined 6 | ) 7 | 8 | export const usePlugin = () => { 9 | const context = useContext(PluginContext) 10 | 11 | if (!context) { 12 | throw new Error('Missing PluginContext provider.') 13 | } 14 | 15 | return context; 16 | } 17 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | export type FileColorPluginSettings = { 2 | cascadeColors: boolean 3 | colorBackground: boolean 4 | palette: Array<{ 5 | id: string 6 | name: string 7 | value: string 8 | }> 9 | fileColors: Array<{ 10 | path: string 11 | color: string 12 | }> 13 | } 14 | 15 | export const defaultSettings: FileColorPluginSettings = { 16 | cascadeColors: false, 17 | colorBackground: false, 18 | palette: [], 19 | fileColors: [], 20 | } 21 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import { View, WorkspaceLeaf } from 'obsidian' 2 | 3 | declare module 'obsidian' { 4 | interface Workspace { 5 | getLeavesOfType(viewType: 'file-explorer'): ExplorerLeaf[] 6 | } 7 | } 8 | 9 | interface ExplorerLeaf extends WorkspaceLeaf { 10 | view: ExplorerView 11 | } 12 | 13 | interface ExplorerView extends View { 14 | fileItems: Record< 15 | string, 16 | { 17 | selfEl: HTMLDivElement, 18 | el: HTMLDivElement, 19 | } 20 | > 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "jsx": "react", 9 | "allowJs": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "isolatedModules": true, 15 | "strictNullChecks": true, 16 | "allowSyntheticDefaultImports": true, 17 | "lib": ["DOM", "ES5", "ES6", "ES7"] 18 | }, 19 | "include": ["**/*.ts", "**/*.tsx"] 20 | } 21 | -------------------------------------------------------------------------------- /src/components/icons/AddCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const AddCircleIcon = () => ( 4 | 16 | 17 | 18 | 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /src/components/icons/TrashIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const TrashIcon = () => ( 4 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier" 13 | ], 14 | "parserOptions": { 15 | "sourceType": "module" 16 | }, 17 | "rules": { 18 | "no-unused-vars": "off", 19 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 20 | "@typescript-eslint/ban-ts-comment": "off", 21 | "no-prototype-builtins": "off", 22 | "@typescript-eslint/no-empty-function": "off" 23 | } 24 | } -------------------------------------------------------------------------------- /src/plugin/FileColorSettingTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FileColorPlugin } from 'plugin/FileColorPlugin' 3 | import { App, PluginSettingTab } from 'obsidian' 4 | import { createRoot, Root } from 'react-dom/client' 5 | import { PluginContext } from 'hooks/usePlugin' 6 | import { SettingsPanel } from 'modules/SettingsPanel' 7 | 8 | export class FileColorSettingTab extends PluginSettingTab { 9 | plugin: FileColorPlugin 10 | root: Root 11 | 12 | constructor(app: App, plugin: FileColorPlugin) { 13 | super(app, plugin) 14 | this.plugin = plugin 15 | this.root = createRoot(this.containerEl) 16 | } 17 | 18 | display(): void { 19 | this.root.render( 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Emil Custic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/SettingItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HTMLProps } from 'react' 3 | 4 | export const SettingItem = ({ 5 | className, 6 | ...props 7 | }: HTMLProps) => ( 8 |
9 | ) 10 | export const SettingItemName = ({ 11 | className, 12 | ...props 13 | }: HTMLProps) => ( 14 |
15 | ) 16 | export const SettingItemControl = ({ 17 | className, 18 | ...props 19 | }: HTMLProps) => ( 20 |
21 | ) 22 | export const SettingItemInfo = ({ 23 | className, 24 | ...props 25 | }: HTMLProps) => ( 26 |
27 | ) 28 | export const SettingItemDescription = ({ 29 | className, 30 | ...props 31 | }: HTMLProps) => ( 32 |
33 | ) 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-file-color-plugin", 3 | "version": "1.1.0", 4 | "description": "An Obsidian (https://obsidian.md) plugin for setting colors on folders and files in the file tree.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "ecustic", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@types/react": "^18.0.26", 17 | "@types/react-dom": "^18.0.10", 18 | "@typescript-eslint/eslint-plugin": "5.29.0", 19 | "@typescript-eslint/parser": "5.29.0", 20 | "builtin-modules": "3.3.0", 21 | "esbuild": "0.14.47", 22 | "esbuild-plugin-babel": "^0.2.3", 23 | "eslint": "^8.29.0", 24 | "eslint-config-prettier": "^8.5.0", 25 | "obsidian": "latest", 26 | "prettier": "^2.8.1", 27 | "tslib": "2.4.0", 28 | "typescript": "4.7.4" 29 | }, 30 | "dependencies": { 31 | "goober": "^2.1.12", 32 | "nanoid": "^4.0.0", 33 | "react": "^18.2.0", 34 | "react-dom": "^18.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/plugin/SetColorModal.tsx: -------------------------------------------------------------------------------- 1 | import { FileColorPlugin } from 'plugin/FileColorPlugin' 2 | import { Modal, TAbstractFile } from 'obsidian' 3 | import * as React from 'react' 4 | import { createRoot, Root } from "react-dom/client"; 5 | import { PluginContext } from 'hooks/usePlugin'; 6 | import { FileContext } from 'hooks/useFile'; 7 | import { SetColorModalContent } from 'modules/SetColorModalContent'; 8 | import { ModalContext } from 'hooks/useModal'; 9 | 10 | export class SetColorModal extends Modal { 11 | plugin: FileColorPlugin 12 | file: TAbstractFile 13 | root?: Root; 14 | 15 | constructor(plugin: FileColorPlugin, file: TAbstractFile) { 16 | super(plugin.app) 17 | this.plugin = plugin 18 | this.file = file 19 | } 20 | 21 | onOpen(): void { 22 | this.titleEl.innerText = 'Set color' 23 | this.root = createRoot(this.contentEl); 24 | this.root.render( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | onClose(): void { 38 | this.root?.unmount(); 39 | } 40 | } -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* TEXT COLORING, NO CASCADE */ 2 | .workspace-leaf-content[data-type="file-explorer"] .nav-files-container .file-color-file.file-color-type-text > .nav-folder-title, 3 | .workspace-leaf-content[data-type="file-explorer"] .nav-files-container .file-color-file.file-color-type-text > .nav-file-title 4 | { 5 | color: var(--file-color-color); 6 | } 7 | 8 | /* TEXT COLORING, WITH CASCADE */ 9 | .workspace-leaf-content[data-type="file-explorer"] .nav-files-container .file-color-file.file-color-type-text.file-color-cascade .nav-folder-title, 10 | .workspace-leaf-content[data-type="file-explorer"] .nav-files-container .file-color-file.file-color-type-text.file-color-cascade .nav-file-title 11 | { 12 | color: var(--file-color-color); 13 | } 14 | 15 | /* BACKGROUND COLORING, NO CASCADE */ 16 | .workspace-leaf-content[data-type="file-explorer"] .nav-files-container .file-color-file.file-color-type-background > .nav-folder-title, 17 | .workspace-leaf-content[data-type="file-explorer"] .nav-files-container .file-color-file.file-color-type-background > .nav-file-title 18 | { 19 | background-color: color-mix(in srgb, var(--file-color-color) 15%, transparent); 20 | } 21 | 22 | /* BACKGROUND COLORING, WITH CASCADE */ 23 | .workspace-leaf-content[data-type="file-explorer"] .nav-files-container .file-color-file.file-color-type-background.file-color-cascade .nav-folder-title, 24 | .workspace-leaf-content[data-type="file-explorer"] .nav-files-container .file-color-file.file-color-type-background.file-color-cascade .nav-file-title 25 | { 26 | background-color: color-mix(in srgb, var(--file-color-color) 15%, transparent); 27 | } 28 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | import process from 'process' 3 | import builtins from 'builtin-modules' 4 | import { readFile } from 'fs/promises' 5 | 6 | const banner = `/* 7 | ${new Date()} 8 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 9 | if you want to view the source, please visit the github repository of this plugin 10 | */ 11 | ` 12 | 13 | const prod = process.argv[2] === 'production' 14 | 15 | const replaceGoober = async (path, encoding) => { 16 | let contents = await readFile(path, encoding) 17 | return contents.replace(/_goober/g, 'fileColorPluginGooberStyles') 18 | } 19 | 20 | esbuild 21 | .build({ 22 | banner: { 23 | js: banner, 24 | }, 25 | entryPoints: ['src/index.ts'], 26 | bundle: true, 27 | external: [ 28 | 'obsidian', 29 | 'electron', 30 | '@codemirror/autocomplete', 31 | '@codemirror/collab', 32 | '@codemirror/commands', 33 | '@codemirror/language', 34 | '@codemirror/lint', 35 | '@codemirror/search', 36 | '@codemirror/state', 37 | '@codemirror/view', 38 | '@lezer/common', 39 | '@lezer/highlight', 40 | '@lezer/lr', 41 | ...builtins, 42 | ], 43 | format: 'cjs', 44 | watch: !prod, 45 | target: 'es2018', 46 | logLevel: 'info', 47 | sourcemap: prod ? false : 'inline', 48 | treeShaking: true, 49 | outfile: 'main.js', 50 | plugins: [ 51 | { 52 | name: 'replacer', 53 | setup(build) { 54 | build.onLoad({ filter: /goober/ }, async ({ path }) => { 55 | return { 56 | contents: await replaceGoober(path, 'utf-8'), 57 | loader: 'tsx', 58 | } 59 | }) 60 | }, 61 | }, 62 | ], 63 | }) 64 | .catch(() => process.exit(1)) 65 | -------------------------------------------------------------------------------- /src/modules/SetColorModalContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useFile } from 'hooks/useFile' 3 | import { useModal } from 'hooks/useModal' 4 | import { usePlugin } from 'hooks/usePlugin' 5 | import { Color } from './Color' 6 | import { ColorCell } from './ColorCell' 7 | import { ColorGrid } from './ColorGrid' 8 | import { ColorName } from './ColorName' 9 | 10 | export const SetColorModalContent = () => { 11 | const plugin = usePlugin() 12 | const { path } = useFile() 13 | const modal = useModal() 14 | const selectedColor = plugin.settings.fileColors.find( 15 | (file) => file.path === path 16 | )?.color 17 | 18 | const handleSelectColor = (color: string | undefined) => { 19 | const fileIndex = plugin.settings.fileColors.findIndex( 20 | (file) => file.path === path 21 | ) 22 | 23 | const file = 24 | fileIndex > -1 ? plugin.settings.fileColors[fileIndex] : undefined 25 | 26 | if (!color && file) { 27 | plugin.settings.fileColors.splice(fileIndex, 1) 28 | } 29 | 30 | if (color && file) { 31 | file.color = color 32 | } 33 | 34 | if (color && !file) { 35 | plugin.settings.fileColors.push({ 36 | path, 37 | color, 38 | }) 39 | } 40 | 41 | plugin.saveSettings() 42 | plugin.applyColorStyles() 43 | modal.close() 44 | } 45 | 46 | return ( 47 | 48 | handleSelectColor(undefined)}> 49 | 50 | None 51 | 52 | {plugin.settings.palette.map((color) => ( 53 | handleSelectColor(color.id)}> 54 | 58 | {color.name} 59 | 60 | ))} 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian File Color 2 | 3 | ![Obsidian File Color Banner](./docs/images/hero-rounded.png) 4 | 5 | ## What is this? 6 | This is a plugin for [Obsidian](https://obsidian.md), which allows you to select colors for your files and folders in the file explorer. 7 | 8 | ## Usage 9 | 10 | Setting the color for a file or folder is done by right clicking on the file in the file explorer and selecting `Set color`. This opens a modal where you can select all the colors defined in the plugin palette. 11 | 12 | ![Setting a color](./docs/images/set-color-rounded.gif) 13 | 14 | ### Changing the palette 15 | 16 | To add colors to the palette open the plugin settings, and click the `+` button. Then use the color picker to select the color, and input a name for the color. Afterwards it will appear in the `Set color` modal. You can add as many colors as you need. 17 | 18 | ![Adding a color](./docs/images/add-color-rounded.gif) 19 | 20 | ### Options 21 | 22 | - Cascade Colors: if turned on, colors on folders will cascade to their children. Set colors on children to override parents. 23 | - Background Colors: if turned on, the background is colored instead of the text. 24 | 25 | ## Compatibility 26 | 27 | This plugin has been tested with a few other community plugins: 28 | 29 | * [Icon Folder](https://github.com/FlorianWoelki/obsidian-icon-folder) 30 | * Should work out of the box, and colors should automatically be applied to the icons as well. 31 | * [Folder Note](https://github.com/aidenlx/folder-note-core) 32 | * Will also work out of the box, however the underlines added to the files will not be colored. 33 | * Personally I use the following CSS snippet locally: 34 | 35 | ```css 36 | .nav-folder.alx-folder-with-note>.nav-folder-title>.nav-folder-title-content { 37 | text-decoration-style: dotted; 38 | text-decoration-color: inherit; 39 | } 40 | ``` 41 | Which will make make the underline use the colors defined by this plugin. The result looks something like this: 42 | 43 | ![Example with icons and folder notes](./docs/images/icons-notes-rounded.png) 44 | --- 45 | 46 |
47 | 48 | 49 | 50 |
-------------------------------------------------------------------------------- /src/plugin/FileColorPlugin.ts: -------------------------------------------------------------------------------- 1 | import { debounce, MenuItem, Plugin } from 'obsidian' 2 | import { SetColorModal } from 'plugin/SetColorModal' 3 | import { FileColorSettingTab } from 'plugin/FileColorSettingTab' 4 | 5 | import type { FileColorPluginSettings } from 'settings' 6 | import { defaultSettings } from 'settings' 7 | 8 | export class FileColorPlugin extends Plugin { 9 | settings: FileColorPluginSettings = defaultSettings 10 | saveSettingsInternalDebounced = debounce(this.saveSettingsInternal, 3000, true); 11 | 12 | async onload() { 13 | await this.loadSettings() 14 | 15 | this.registerEvent( 16 | this.app.workspace.on('file-menu', (menu, file) => { 17 | const addFileColorMenuItem = (item: MenuItem) => { 18 | item.setTitle('Set color') 19 | item.setIcon('palette') 20 | item.onClick(() => { 21 | new SetColorModal(this, file).open() 22 | }) 23 | } 24 | 25 | menu.addItem(addFileColorMenuItem) 26 | }) 27 | ) 28 | 29 | this.app.workspace.onLayoutReady(async () => { 30 | this.generateColorStyles() 31 | this.applyColorStyles() 32 | }) 33 | 34 | this.registerEvent( 35 | this.app.workspace.on('layout-change', () => this.applyColorStyles()) 36 | ) 37 | 38 | this.registerEvent( 39 | this.app.vault.on('rename', async (newFile, oldPath) => { 40 | this.settings.fileColors 41 | .filter((fileColor) => fileColor.path === oldPath) 42 | .forEach((fileColor) => { 43 | fileColor.path = newFile.path 44 | }) 45 | this.saveSettings() 46 | this.applyColorStyles() 47 | }) 48 | ) 49 | 50 | this.registerEvent( 51 | this.app.vault.on('delete', async (file) => { 52 | this.settings.fileColors = this.settings.fileColors.filter( 53 | (fileColor) => !fileColor.path.startsWith(file.path) 54 | ) 55 | this.saveSettings() 56 | }) 57 | ) 58 | 59 | this.addSettingTab(new FileColorSettingTab(this.app, this)) 60 | } 61 | 62 | onunload() { 63 | document.getElementById('fileColorPluginStyles')?.remove(); 64 | document.getElementById('fileColorPluginGooberStyles')?.remove(); 65 | } 66 | 67 | async loadSettings() { 68 | this.settings = Object.assign({}, defaultSettings, await this.loadData()) 69 | } 70 | 71 | async saveSettings(immediate?: boolean) { 72 | if (immediate) { 73 | return this.saveSettingsInternal(); 74 | } 75 | return this.saveSettingsInternalDebounced(); 76 | } 77 | 78 | private saveSettingsInternal() { 79 | return this.saveData(this.settings) 80 | } 81 | 82 | generateColorStyles() { 83 | let colorStyleEl = document.getElementById('fileColorPluginStyles') 84 | 85 | if (!colorStyleEl) { 86 | colorStyleEl = this.app.workspace.containerEl.createEl('style') 87 | colorStyleEl.id = 'fileColorPluginStyles' 88 | } 89 | 90 | colorStyleEl.innerHTML = this.settings.palette 91 | .map( 92 | (color) => 93 | `.file-color-color-${color.id} { --file-color-color: ${color.value}; }` 94 | ) 95 | .join('\n') 96 | } 97 | applyColorStyles = debounce(this.applyColorStylesInternal, 50, true); 98 | 99 | private applyColorStylesInternal() { 100 | const cssType = this.settings.colorBackground ? 'background' : 'text' 101 | 102 | const fileExplorers = this.app.workspace.getLeavesOfType('file-explorer') 103 | fileExplorers.forEach((fileExplorer) => { 104 | Object.entries(fileExplorer.view.fileItems).forEach( 105 | ([path, fileItem]) => { 106 | const itemClasses = fileItem.el.classList.value 107 | .split(' ') 108 | .filter((cls) => !cls.startsWith('file-color')) 109 | 110 | const file = this.settings.fileColors.find( 111 | (file) => file.path === path 112 | ) 113 | 114 | if (file) { 115 | itemClasses.push('file-color-file') 116 | itemClasses.push('file-color-color-' + file.color) 117 | itemClasses.push('file-color-type-' + cssType) 118 | if (this.settings.cascadeColors) { 119 | itemClasses.push('file-color-cascade') 120 | } 121 | } 122 | 123 | fileItem.el.classList.value = itemClasses.join(' ') 124 | } 125 | ) 126 | }) 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/modules/SettingsPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'components/Button' 2 | import { AddCircleIcon } from 'components/icons/AddCircleIcon' 3 | import { TrashIcon } from 'components/icons/TrashIcon' 4 | import { usePlugin } from 'hooks/usePlugin' 5 | import { nanoid } from 'nanoid' 6 | import React, { useEffect, useState } from 'react' 7 | import type { FileColorPluginSettings } from 'settings' 8 | import { 9 | SettingItem, 10 | SettingItemName, 11 | SettingItemControl, 12 | SettingItemInfo, 13 | SettingItemDescription 14 | } from 'components/SettingItem' 15 | import { SettingItemControlFull } from './SettingItemControlFull' 16 | import { WideTextInput } from './WideTextInput' 17 | 18 | type Color = FileColorPluginSettings['palette'][number] 19 | 20 | export const SettingsPanel = () => { 21 | const plugin = usePlugin() 22 | const [palette, setPalette] = useState( 23 | plugin.settings.palette 24 | ) 25 | const [cascadeColors, setCascadeColors] = useState( 26 | plugin.settings.cascadeColors 27 | ) 28 | const [colorBackground, setColorBackground] = useState( 29 | plugin.settings.colorBackground 30 | ) 31 | const [changed, setChanged] = useState(false) 32 | 33 | useEffect(() => { 34 | if (palette.length !== plugin.settings.palette.length) { 35 | setChanged(true) 36 | return 37 | } 38 | 39 | setChanged( 40 | palette.some((color) => { 41 | const settingsColor = plugin.settings.palette.find( 42 | (settingsColor) => settingsColor.id === color.id 43 | ) 44 | 45 | if ( 46 | !settingsColor || 47 | settingsColor.name !== color.name || 48 | settingsColor.value !== color.value 49 | ) { 50 | return true 51 | } 52 | }) 53 | ) 54 | }, [plugin, palette]) 55 | 56 | const onRemoveColor = (color: Color, colorIndex: number) => { 57 | setPalette(palette.filter((paletteColor) => paletteColor.id !== color.id)) 58 | } 59 | 60 | const onColorValueChange = (color: Color, value: string) => { 61 | setPalette( 62 | palette.map((paletteColor) => { 63 | if (paletteColor.id === color.id) { 64 | return { ...color, value } 65 | } 66 | return paletteColor 67 | }) 68 | ) 69 | } 70 | 71 | const onColorNameChange = (color: Color, name: string) => { 72 | setPalette( 73 | palette.map((paletteColor) => { 74 | if (paletteColor.id === color.id) { 75 | return { ...color, name } 76 | } 77 | return paletteColor 78 | }) 79 | ) 80 | } 81 | 82 | const onAddColor = () => { 83 | setPalette([ 84 | ...palette, 85 | { 86 | id: nanoid(), 87 | name: '', 88 | value: '#ffffff', 89 | }, 90 | ]) 91 | } 92 | 93 | const onSave = () => { 94 | plugin.settings.palette = palette 95 | plugin.settings.fileColors = plugin.settings.fileColors.filter( 96 | (fileColor) => palette.find((color) => fileColor.color === color.id) 97 | ) 98 | plugin.saveSettings() 99 | plugin.generateColorStyles() 100 | plugin.applyColorStyles() 101 | setChanged(false) 102 | } 103 | 104 | const onRevert = () => { 105 | setPalette(plugin.settings.palette) 106 | setChanged(false) 107 | } 108 | 109 | const onChangeCascadeColors = () => { 110 | setCascadeColors(!cascadeColors) 111 | plugin.settings.cascadeColors = !plugin.settings.cascadeColors 112 | plugin.saveSettings() 113 | plugin.applyColorStyles() 114 | } 115 | 116 | const onChangeColorBackground = () => { 117 | setColorBackground(!colorBackground) 118 | plugin.settings.colorBackground = !plugin.settings.colorBackground 119 | plugin.saveSettings() 120 | plugin.applyColorStyles() 121 | } 122 | 123 | return ( 124 |
125 |

Palette

126 | {palette.length < 1 && No colors in the palette} 127 | {palette.map((color, colorIndex) => ( 128 | 129 | 130 | onColorValueChange(color, e.target.value)} 134 | /> 135 | onColorNameChange(color, e.target.value)} 140 | /> 141 | 144 | 145 | 146 | ))} 147 | 148 | 149 | 153 | 154 | 155 | {changed && ( 156 | 157 | 158 | You have unsaved palette changes. 159 | 160 | 161 | 162 | 163 | 164 | 165 | )} 166 | 167 |

Options

168 | 169 | 170 | Cascade Colors 171 | Folders will cascade their colors to sub-folders and notes, unless their colors are explicitly set. 172 | 173 | 174 | 175 |
176 | 177 |
178 |
179 |
180 | 181 | 182 | 183 | Color Background 184 | Color the background instead of the text. 185 | 186 | 187 | 188 |
189 | 190 |
191 |
192 |
193 | 194 | 195 |
196 | ) 197 | } 198 | --------------------------------------------------------------------------------