mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
Merge pull request #79 from devonthesofa/feature/file-explorer-rainbow
[feature] Add advanced file explorer status visualizations
This commit is contained in:
commit
edacb2a9fa
8 changed files with 396 additions and 12 deletions
|
|
@ -2,6 +2,11 @@ import { StatusIconPreview } from "@/components/atoms/StatusIconPreview";
|
|||
import { GroupedStatuses } from "@/types/noteStatus";
|
||||
import React, { FC, memo } from "react";
|
||||
import { StatusIcon } from "@/components/atoms/StatusIcon";
|
||||
import {
|
||||
getPrimaryStatus,
|
||||
getUnknownStatusColor,
|
||||
resolveStatusColor,
|
||||
} from "@/utils/statusColor";
|
||||
|
||||
type Props = {
|
||||
statuses: GroupedStatuses;
|
||||
|
|
@ -35,15 +40,12 @@ export const FileExplorerIcon: FC<Props> = memo(
|
|||
|
||||
const useStatusColors = iconColorMode === "status";
|
||||
|
||||
const getStatusColor = (color?: string) =>
|
||||
(color && color.trim()) || "var(--text-accent)";
|
||||
|
||||
if (totalStatuses === 0) {
|
||||
if (hideUnknownStatus) return null;
|
||||
|
||||
const icon = unknownStatusConfig?.icon || "❓";
|
||||
const color = useStatusColors
|
||||
? unknownStatusConfig?.color?.trim() || "#8b949e"
|
||||
? unknownStatusConfig?.color?.trim() || getUnknownStatusColor()
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
|
|
@ -64,10 +66,10 @@ export const FileExplorerIcon: FC<Props> = memo(
|
|||
);
|
||||
}
|
||||
|
||||
const primaryStatus = statusEntries[0]?.[1]?.[0];
|
||||
const primaryStatus = getPrimaryStatus(statuses);
|
||||
if (!primaryStatus) return null;
|
||||
const iconColor = useStatusColors
|
||||
? getStatusColor(primaryStatus.color)
|
||||
? resolveStatusColor(primaryStatus)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -225,6 +225,63 @@ export const UISettings: React.FC<Props> = ({ settings, onChange }) => {
|
|||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Color filename text using status color"
|
||||
description="Apply the active status color to the filename text inside the file explorer."
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.fileExplorerColorFileName || false}
|
||||
onChange={handleChange("fileExplorerColorFileName")}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Color the file explorer block based on status"
|
||||
description="Tint the entire file explorer list item with the active status color."
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.fileExplorerColorBlock || false}
|
||||
onChange={handleChange("fileExplorerColorBlock")}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<h4>Additional status visualization in file explorer</h4>
|
||||
|
||||
<SettingItem
|
||||
name="Show colored left border"
|
||||
description="Render a subtle colored strip on the left edge of the file explorer item."
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.fileExplorerLeftBorder || false}
|
||||
onChange={handleChange("fileExplorerLeftBorder")}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Show colored status dot"
|
||||
description="Append a compact colored dot next to the filename as an extra status indicator."
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.fileExplorerStatusDot || false}
|
||||
onChange={handleChange("fileExplorerStatusDot")}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Underline filename with status color"
|
||||
description="Adds a colored underline below the filename without changing the text color."
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.fileExplorerUnderlineFileName || false}
|
||||
onChange={handleChange("fileExplorerUnderlineFileName")}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Status icon frame"
|
||||
description="Choose whether to display a frame around the status icon inside the file explorer."
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
|
|||
customStatuses: [],
|
||||
showStatusIconsInExplorer: true,
|
||||
hideUnknownStatusInExplorer: true, // Default to hide unknown status
|
||||
fileExplorerColorFileName: false,
|
||||
fileExplorerColorBlock: false,
|
||||
fileExplorerLeftBorder: false,
|
||||
fileExplorerStatusDot: false,
|
||||
fileExplorerUnderlineFileName: false,
|
||||
enabledTemplates: DEFAULT_ENABLED_TEMPLATES,
|
||||
useCustomStatusesOnly: false,
|
||||
useMultipleStatuses: true,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ import {
|
|||
import { NoteStatusService } from "@/core/noteStatusService";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import { GroupedStatuses } from "@/types/noteStatus";
|
||||
import {
|
||||
getPrimaryStatus,
|
||||
getUnknownStatusColor,
|
||||
resolveStatusColor,
|
||||
} from "@/utils/statusColor";
|
||||
import { Plugin, TFile, View } from "obsidian";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { StatusesInfoPopup } from "../popups/statusesInfoPopupIntegration";
|
||||
|
|
@ -20,6 +25,15 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
"file-explorer-integration-subscription1";
|
||||
private readonly FILE_EXPLORER_TYPE = "file-explorer";
|
||||
private readonly CONTAINER_SELECTOR = ".nav-files-container";
|
||||
private readonly FILE_NAME_COLORIZED_ATTR = "noteStatusColorized";
|
||||
private readonly FILE_NAME_ORIGINAL_COLOR_ATTR = "noteStatusOriginalColor";
|
||||
private readonly FILE_BLOCK_CLASS = "note-status-colored-block";
|
||||
private readonly FILE_BLOCK_COLOR_VAR = "--note-status-block-color";
|
||||
private readonly FILE_BORDER_CLASS = "note-status-border-left";
|
||||
private readonly FILE_BORDER_COLOR_VAR = "--note-status-border-color";
|
||||
private readonly FILE_UNDERLINE_CLASS = "note-status-underline";
|
||||
private readonly FILE_UNDERLINE_COLOR_VAR = "--note-status-underline-color";
|
||||
private readonly STATUS_DOT_CLASS = "note-status-dot-badge";
|
||||
|
||||
constructor(plugin: Plugin) {
|
||||
this.plugin = plugin;
|
||||
|
|
@ -58,6 +72,11 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
key === "fileExplorerIconPosition" ||
|
||||
key === "fileExplorerIconFrame" ||
|
||||
key === "fileExplorerIconColorMode" ||
|
||||
key === "fileExplorerColorFileName" ||
|
||||
key === "fileExplorerColorBlock" ||
|
||||
key === "fileExplorerLeftBorder" ||
|
||||
key === "fileExplorerStatusDot" ||
|
||||
key === "fileExplorerUnderlineFileName" ||
|
||||
key === "unknownStatusIcon" ||
|
||||
key === "unknownStatusLucideIcon" ||
|
||||
key === "unknownStatusColor" ||
|
||||
|
|
@ -101,8 +120,22 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
}
|
||||
|
||||
const noteStatusService = this.getFileNoteStatusService(dataPath);
|
||||
const statuses = noteStatusService?.statuses ?? null;
|
||||
const primaryStatus = getPrimaryStatus(statuses);
|
||||
const hasStatus = Boolean(primaryStatus);
|
||||
const fallbackColor = getUnknownStatusColor();
|
||||
const statusColor = primaryStatus
|
||||
? resolveStatusColor(primaryStatus, fallbackColor)
|
||||
: undefined;
|
||||
const textElement = textEl as HTMLElement;
|
||||
const navItem = this.getNavItemElement(textElement);
|
||||
|
||||
this.applyFileNameColor(textElement, statusColor, hasStatus);
|
||||
this.applyFileNameUnderline(textElement, statusColor, hasStatus);
|
||||
this.applyStatusDot(textElement, statusColor, hasStatus);
|
||||
this.applyFileBlockColor(navItem, statusColor, hasStatus);
|
||||
this.applyLeftBorder(navItem, statusColor, hasStatus);
|
||||
|
||||
// Only render icons for markdown files
|
||||
if (noteStatusService) {
|
||||
this.render(textEl, noteStatusService.statuses);
|
||||
}
|
||||
|
|
@ -110,11 +143,10 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
}
|
||||
|
||||
private getUnknownStatusConfig() {
|
||||
const settings = settingsService.settings;
|
||||
return {
|
||||
icon: settings.unknownStatusIcon || "❓",
|
||||
lucideIcon: settings.unknownStatusLucideIcon || "",
|
||||
color: settings.unknownStatusColor || "#8b949e",
|
||||
icon: settingsService.settings.unknownStatusIcon || "❓",
|
||||
lucideIcon: settingsService.settings.unknownStatusLucideIcon || "",
|
||||
color: getUnknownStatusColor(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -270,4 +302,181 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
this.CONTAINER_SELECTOR,
|
||||
);
|
||||
}
|
||||
|
||||
private applyFileNameColor(
|
||||
element?: HTMLElement | null,
|
||||
color?: string,
|
||||
hasStatus?: boolean,
|
||||
): void {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!settingsService.settings.fileExplorerColorFileName ||
|
||||
!hasStatus ||
|
||||
!color
|
||||
) {
|
||||
this.clearFileNameColor(element);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!element.dataset[this.FILE_NAME_ORIGINAL_COLOR_ATTR]) {
|
||||
element.dataset[this.FILE_NAME_ORIGINAL_COLOR_ATTR] =
|
||||
element.style.color || "";
|
||||
}
|
||||
|
||||
element.dataset[this.FILE_NAME_COLORIZED_ATTR] = "true";
|
||||
element.style.color = color;
|
||||
}
|
||||
|
||||
private applyFileBlockColor(
|
||||
navItem?: HTMLElement | null,
|
||||
color?: string,
|
||||
hasStatus?: boolean,
|
||||
): void {
|
||||
if (!navItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!settingsService.settings.fileExplorerColorBlock ||
|
||||
!hasStatus ||
|
||||
!color
|
||||
) {
|
||||
this.clearFileBlockColor(navItem);
|
||||
return;
|
||||
}
|
||||
|
||||
navItem.classList.add(this.FILE_BLOCK_CLASS);
|
||||
navItem.style.setProperty(this.FILE_BLOCK_COLOR_VAR, color);
|
||||
}
|
||||
|
||||
private applyLeftBorder(
|
||||
navItem?: HTMLElement | null,
|
||||
color?: string,
|
||||
hasStatus?: boolean,
|
||||
): void {
|
||||
if (!navItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!settingsService.settings.fileExplorerLeftBorder ||
|
||||
!hasStatus ||
|
||||
!color
|
||||
) {
|
||||
this.clearFileLeftBorder(navItem);
|
||||
return;
|
||||
}
|
||||
|
||||
navItem.classList.add(this.FILE_BORDER_CLASS);
|
||||
navItem.style.setProperty(this.FILE_BORDER_COLOR_VAR, color);
|
||||
}
|
||||
|
||||
private applyFileNameUnderline(
|
||||
element?: HTMLElement | null,
|
||||
color?: string,
|
||||
hasStatus?: boolean,
|
||||
): void {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!settingsService.settings.fileExplorerUnderlineFileName ||
|
||||
!hasStatus ||
|
||||
!color
|
||||
) {
|
||||
this.clearFileNameUnderline(element);
|
||||
return;
|
||||
}
|
||||
|
||||
element.classList.add(this.FILE_UNDERLINE_CLASS);
|
||||
element.style.setProperty(this.FILE_UNDERLINE_COLOR_VAR, color);
|
||||
}
|
||||
|
||||
private applyStatusDot(
|
||||
element?: HTMLElement | null,
|
||||
color?: string,
|
||||
hasStatus?: boolean,
|
||||
): void {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingDot = element.querySelector(
|
||||
`.${this.STATUS_DOT_CLASS}`,
|
||||
) as HTMLElement | null;
|
||||
if (
|
||||
!settingsService.settings.fileExplorerStatusDot ||
|
||||
!hasStatus ||
|
||||
!color
|
||||
) {
|
||||
if (existingDot) {
|
||||
existingDot.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const dot =
|
||||
existingDot ||
|
||||
createSpan({
|
||||
cls: this.STATUS_DOT_CLASS,
|
||||
});
|
||||
|
||||
dot.setAttribute("aria-hidden", "true");
|
||||
dot.style.backgroundColor = color;
|
||||
|
||||
if (!existingDot) {
|
||||
element.appendChild(dot);
|
||||
}
|
||||
}
|
||||
|
||||
private getNavItemElement(
|
||||
element?: HTMLElement | null,
|
||||
): HTMLElement | null {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
return element.closest(".nav-file, .nav-folder") as HTMLElement | null;
|
||||
}
|
||||
|
||||
private clearFileNameColor(element: HTMLElement): void {
|
||||
if (!element.dataset[this.FILE_NAME_COLORIZED_ATTR]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousColor =
|
||||
element.dataset[this.FILE_NAME_ORIGINAL_COLOR_ATTR] || "";
|
||||
if (previousColor) {
|
||||
element.style.color = previousColor;
|
||||
} else {
|
||||
element.style.removeProperty("color");
|
||||
}
|
||||
|
||||
delete element.dataset[this.FILE_NAME_ORIGINAL_COLOR_ATTR];
|
||||
delete element.dataset[this.FILE_NAME_COLORIZED_ATTR];
|
||||
}
|
||||
|
||||
private clearFileBlockColor(navItem?: HTMLElement | null): void {
|
||||
if (!navItem) {
|
||||
return;
|
||||
}
|
||||
navItem.classList.remove(this.FILE_BLOCK_CLASS);
|
||||
navItem.style.removeProperty(this.FILE_BLOCK_COLOR_VAR);
|
||||
}
|
||||
|
||||
private clearFileLeftBorder(navItem?: HTMLElement | null): void {
|
||||
if (!navItem) {
|
||||
return;
|
||||
}
|
||||
navItem.classList.remove(this.FILE_BORDER_CLASS);
|
||||
navItem.style.removeProperty(this.FILE_BORDER_COLOR_VAR);
|
||||
}
|
||||
|
||||
private clearFileNameUnderline(element: HTMLElement): void {
|
||||
element.classList.remove(this.FILE_UNDERLINE_CLASS);
|
||||
element.style.removeProperty(this.FILE_UNDERLINE_COLOR_VAR);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -11,6 +11,62 @@
|
|||
right: 0;
|
||||
}
|
||||
|
||||
.note-status-colored-block {
|
||||
position: relative;
|
||||
border-radius: var(--radius-s);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.note-status-colored-block::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background-color: var(--note-status-block-color, transparent);
|
||||
opacity: 0.18;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
transition:
|
||||
background-color var(--anim-duration-fast) ease,
|
||||
opacity var(--anim-duration-fast) ease;
|
||||
}
|
||||
|
||||
.note-status-border-left {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-status-border-left::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
width: 3px;
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--note-status-border-color, transparent);
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.note-status-underline {
|
||||
text-decoration-line: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: var(--note-status-underline-color, currentColor);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.note-status-dot-badge {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-left: var(--size-2-1);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 0 1px var(--background-modifier-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ export type PluginSettings = {
|
|||
statusBarShowTemplateName: "always" | "never" | "auto"; // How to show template names in status bar
|
||||
showStatusIconsInExplorer: boolean;
|
||||
hideUnknownStatusInExplorer: boolean;
|
||||
fileExplorerColorFileName: boolean; // Whether to color the file explorer filename text using the current status color
|
||||
fileExplorerColorBlock: boolean; // Whether to tint the entire explorer list item background using the status color
|
||||
fileExplorerLeftBorder: boolean; // Whether to display a colored border on the explorer item
|
||||
fileExplorerStatusDot: boolean; // Whether to append a small colored dot next to the filename
|
||||
fileExplorerUnderlineFileName: boolean; // Whether to underline the filename using the status color
|
||||
enabledTemplates: string[]; // IDs of enabled templates
|
||||
useCustomStatusesOnly: boolean; // Whether to use only custom statuses or include templates
|
||||
useMultipleStatuses: boolean; // Whether to allow multiple statuses per note
|
||||
|
|
|
|||
50
utils/statusColor.ts
Normal file
50
utils/statusColor.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import settingsService from "@/core/settingsService";
|
||||
import { GroupedStatuses, NoteStatus } from "@/types/noteStatus";
|
||||
|
||||
export const DEFAULT_STATUS_ACCENT_COLOR = "var(--text-accent)";
|
||||
const DEFAULT_UNKNOWN_STATUS_COLOR = "#8b949e";
|
||||
|
||||
export function getPrimaryStatus(
|
||||
statuses?: GroupedStatuses | null,
|
||||
): NoteStatus | null {
|
||||
if (!statuses) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const list of Object.values(statuses)) {
|
||||
if (list.length) {
|
||||
return list[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getUnknownStatusColor(): string {
|
||||
return (
|
||||
settingsService.settings.unknownStatusColor?.trim() ||
|
||||
DEFAULT_UNKNOWN_STATUS_COLOR
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveStatusColor(
|
||||
status?: NoteStatus | null,
|
||||
fallbackColor?: string,
|
||||
defaultColor: string | null = DEFAULT_STATUS_ACCENT_COLOR,
|
||||
): string | undefined {
|
||||
const statusColor = status?.color?.trim();
|
||||
if (statusColor) {
|
||||
return statusColor;
|
||||
}
|
||||
|
||||
const fallback = fallbackColor?.trim();
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (defaultColor === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return defaultColor || undefined;
|
||||
}
|
||||
Loading…
Reference in a new issue