Skip to content

Commit e86d1d8

Browse files
feat: add per-folder customizable status colors
1 parent 74de67f commit e86d1d8

12 files changed

Lines changed: 295 additions & 33 deletions

File tree

186 KB
Binary file not shown.

folderview.plus.plg

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66
<!ENTITY launch "Settings/FolderViewPlus">
77
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
88
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/main/folderview.plus.plg">
9-
<!ENTITY version "2026.03.05.9">
10-
<!ENTITY md5 "1d386e5f2c1a4a90a90f5155c5fb389a">
9+
<!ENTITY version "2026.03.05.10">
10+
<!ENTITY md5 "98ff1b691d16c33d05a9c30d12e8c9c7">
1111
]>
1212

1313
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;" pluginURL="&pluginURL;" icon="folder-open-o" support="https://github.com/alexphillips-dev/FolderView-Plus/issues" min="7.0.0">
1414
<CHANGES>
1515

16+
###2026.03.05.10
17+
- Add per-folder status color controls in the folder editor for `started`, `paused`, and `stopped`.
18+
- Apply custom folder status colors to status icon + text across Docker, VMs, and Dashboard.
19+
- Add safe color normalization/default reset behavior to keep settings valid during edit/import flows.
20+
1621
###2026.03.05.9
1722
- Color-code folder state text for faster scanning:
1823
- white for `started`,

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/Folder.page

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,38 @@ Markdown="false"
366366
</li>
367367
</ul>
368368

369+
<div class="basic">
370+
<dl>
371+
<dt>Folder status colors:</dt>
372+
<dd class="folder-status-colors-dd">
373+
<div class="folder-status-colors">
374+
<label class="folder-status-color-item">
375+
<span>Started</span>
376+
<input type="color" name="status_color_started" style="height:28px;vertical-align:middle;">
377+
</label>
378+
<label class="folder-status-color-item">
379+
<span>Paused</span>
380+
<input type="color" name="status_color_paused" style="height:28px;vertical-align:middle;">
381+
</label>
382+
<label class="folder-status-color-item">
383+
<span>Stopped</span>
384+
<input type="color" name="status_color_stopped" style="height:28px;vertical-align:middle;">
385+
</label>
386+
</div>
387+
<button onclick="event.preventDefault(); resetStatusColorDefaults();" style="width:44px;height:28px;min-width:0;padding:0;margin:0;margin-left:0.5em;vertical-align:middle;">
388+
<i class="fa fa-repeat" aria-hidden="true"></i>
389+
</button>
390+
</dd>
391+
</dl>
392+
<blockquote class="inline_help">
393+
<p>
394+
Set custom colors for this folder's status icon and text across Docker, VMs, and Dashboard views.
395+
<br>
396+
The <i class="fa fa-repeat" aria-hidden="true"></i> button resets to defaults.
397+
</p>
398+
</blockquote>
399+
</div>
400+
369401
<div class="basic" constraint="docker">
370402
<dl>
371403
<dt data-i18n="update-column">Hide update column:</dt>
@@ -605,4 +637,4 @@ Markdown="false"
605637
<input type="text" name="action_script_icon">
606638
</dd>
607639
</dl>
608-
</div>
640+
</div>

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/dashboard.js

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
1+
const localDefaultFolderStatusColors = {
2+
started: '#ffffff',
3+
paused: '#b8860b',
4+
stopped: '#ff4d4d'
5+
};
6+
const normalizeStatusHexColor = (value, fallback) => {
7+
if (typeof value !== 'string') {
8+
return fallback;
9+
}
10+
const trimmed = value.trim();
11+
if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(trimmed)) {
12+
return fallback;
13+
}
14+
if (trimmed.length === 4) {
15+
return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`.toLowerCase();
16+
}
17+
return trimmed.toLowerCase();
18+
};
119
const utils = window.FolderViewPlusUtils || {
220
normalizePrefs: () => ({ sortMode: 'created', manualOrder: [], autoRules: [] }),
3-
getAutoRuleMatches: () => []
21+
getAutoRuleMatches: () => [],
22+
DEFAULT_FOLDER_STATUS_COLORS: localDefaultFolderStatusColors,
23+
getFolderStatusColors: (settings) => {
24+
const incoming = settings && typeof settings === 'object' ? settings : {};
25+
return {
26+
started: normalizeStatusHexColor(incoming.status_color_started, localDefaultFolderStatusColors.started),
27+
paused: normalizeStatusHexColor(incoming.status_color_paused, localDefaultFolderStatusColors.paused),
28+
stopped: normalizeStatusHexColor(incoming.status_color_stopped, localDefaultFolderStatusColors.stopped)
29+
};
30+
}
431
};
532

633
/**
@@ -291,7 +318,7 @@ const createFolderDocker = (folder, id, position, order, containersInfo, folders
291318
}).filter((name) => !folder.containers.includes(name)));
292319

293320
// the HTML template for the folder
294-
const fld = `<div class="folder-showcase-outer-${id} folder-showcase-outer"><span class="outer solid apps stopped folder-docker"><span id="folder-id-${id}" onclick='addDockerFolderContext("${id}")' class="hand docker folder-hand-docker"><img src="${folder.icon}" class="img folder-img-docker" onerror="this.src='/plugins/dynamix.docker.manager/images/question.png';"></span><span class="inner folder-inner-docker"><span class="folder-appname-docker">${folder.name}</span><br><i class="fa fa-square stopped red-text folder-load-status-docker"></i><span class="state folder-state-docker">${$.i18n('stopped')}</span></span><div class="folder-storage"></div></span><div class="folder-showcase-${id} folder-showcase"></div></div>`;
321+
const fld = `<div class="folder-showcase-outer-${id} folder-showcase-outer"><span class="outer solid apps stopped folder-docker"><span id="folder-id-${id}" onclick='addDockerFolderContext("${id}")' class="hand docker folder-hand-docker"><img src="${folder.icon}" class="img folder-img-docker" onerror="this.src='/plugins/dynamix.docker.manager/images/question.png';"></span><span class="inner folder-inner-docker"><span class="folder-appname-docker">${folder.name}</span><br><i class="fa fa-square stopped folder-load-status-docker"></i><span class="state folder-state-docker">${$.i18n('stopped')}</span></span><div class="folder-storage"></div></span><div class="folder-showcase-${id} folder-showcase"></div></div>`;
295322

296323
// insertion at position of the folder
297324
if (position === 0) {
@@ -403,7 +430,14 @@ const createFolderDocker = (folder, id, position, order, containersInfo, folders
403430
folder.containers = newFolder;
404431

405432
//temp var
406-
const sel = $(`tbody#docker_view span#folder-id-${id}`)
433+
const sel = $(`tbody#docker_view span#folder-id-${id}`);
434+
const statusColors = typeof utils.getFolderStatusColors === 'function'
435+
? utils.getFolderStatusColors(folder.settings)
436+
: localDefaultFolderStatusColors;
437+
const $statusIcon = sel.next('span.inner').children('i');
438+
const $statusText = sel.next('span.inner').children('span.state');
439+
$statusIcon.css('color', statusColors.stopped);
440+
$statusText.css('color', statusColors.stopped);
407441

408442
//set the status of a folder
409443

@@ -413,8 +447,8 @@ const createFolderDocker = (folder, id, position, order, containersInfo, folders
413447

414448
if (started) {
415449
sel.parent().removeClass('stopped').addClass('started');
416-
sel.next('span.inner').children('i').replaceWith($('<i class="fa fa-play started green-text"></i>'));
417-
sel.next('span.inner').children('span.state').text(`${started}/${Object.entries(folder.containers).length} ${$.i18n('started')}`);
450+
$statusIcon.replaceWith($(`<i class="fa fa-play started folder-load-status-docker" style="color:${statusColors.started}"></i>`));
451+
$statusText.text(`${started}/${Object.entries(folder.containers).length} ${$.i18n('started')}`).css('color', statusColors.started);
418452
}
419453

420454
if(autostart === 0) {
@@ -499,7 +533,7 @@ const createFolderVM = (folder, id, position, order, vmInfo, foldersDone) => {
499533
}).filter((name) => !folder.containers.includes(name)));
500534

501535
// the HTML template for the folder
502-
const fld = `<div class="folder-showcase-outer-${id} folder-showcase-outer"><span class="outer solid vms stopped folder-vm"><span id="folder-id-${id}" onclick='addVMFolderContext("${id}")' class="hand vm folder-hand-vm"><img src="${folder.icon}" class="img folder-img-vm" onerror='this.src="/plugins/dynamix.docker.manager/images/question.png"'></span><span class="inner folder-inner-vm"><span class="folder-appname-vm">${folder.name}</span><br><i class="fa fa-square stopped red-text folder-load-status-vm"></i><span class="state folder-state-vm">${$.i18n('stopped')}</span></span><div class="folder-storage" style="display:none"></div></span><div class="folder-showcase-${id} folder-showcase"></div></div>`;
536+
const fld = `<div class="folder-showcase-outer-${id} folder-showcase-outer"><span class="outer solid vms stopped folder-vm"><span id="folder-id-${id}" onclick='addVMFolderContext("${id}")' class="hand vm folder-hand-vm"><img src="${folder.icon}" class="img folder-img-vm" onerror='this.src="/plugins/dynamix.docker.manager/images/question.png"'></span><span class="inner folder-inner-vm"><span class="folder-appname-vm">${folder.name}</span><br><i class="fa fa-square stopped folder-load-status-vm"></i><span class="state folder-state-vm">${$.i18n('stopped')}</span></span><div class="folder-storage" style="display:none"></div></span><div class="folder-showcase-${id} folder-showcase"></div></div>`;
503537

504538
// insertion at position of the folder
505539
if (position === 0) {
@@ -596,11 +630,18 @@ const createFolderVM = (folder, id, position, order, vmInfo, foldersDone) => {
596630

597631

598632
//set tehe status of a folder
633+
const sel = $(`tbody#vm_view span#folder-id-${id}`);
634+
const statusColors = typeof utils.getFolderStatusColors === 'function'
635+
? utils.getFolderStatusColors(folder.settings)
636+
: localDefaultFolderStatusColors;
637+
const $statusIcon = sel.next('span.inner').children('i');
638+
const $statusText = sel.next('span.inner').children('span.state');
639+
$statusIcon.css('color', statusColors.stopped);
640+
$statusText.css('color', statusColors.stopped);
599641
if (started) {
600-
const sel = $(`tbody#vm_view span#folder-id-${id}`);
601642
sel.parent().removeClass('stopped').addClass('started');
602-
sel.next('span.inner').children('i').replaceWith($('<i class="fa fa-play started green-text"></i>'));
603-
sel.next('span.inner').children('span.state').text(`${started}/${Object.entries(folder.containers).length} ${$.i18n('started')}`);
643+
$statusIcon.replaceWith($(`<i class="fa fa-play started folder-load-status-vm" style="color:${statusColors.started}"></i>`));
644+
$statusText.text(`${started}/${Object.entries(folder.containers).length} ${$.i18n('started')}`).css('color', statusColors.started);
604645
}
605646

606647
if(autostart === 0) {

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.js

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,39 @@
11
const FOLDER_VIEW_DEBUG_MODE = false;
2+
const localDefaultFolderStatusColors = {
3+
started: '#ffffff',
4+
paused: '#b8860b',
5+
stopped: '#ff4d4d'
6+
};
7+
const normalizeStatusHexColor = (value, fallback) => {
8+
if (typeof value !== 'string') {
9+
return fallback;
10+
}
11+
const trimmed = value.trim();
12+
if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(trimmed)) {
13+
return fallback;
14+
}
15+
if (trimmed.length === 4) {
16+
return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`.toLowerCase();
17+
}
18+
return trimmed.toLowerCase();
19+
};
220
const utils = window.FolderViewPlusUtils || {
321
normalizePrefs: () => ({
422
sortMode: 'created',
523
manualOrder: [],
624
autoRules: [],
725
badges: { running: true, stopped: false, updates: true }
826
}),
9-
getAutoRuleMatches: () => []
27+
getAutoRuleMatches: () => [],
28+
DEFAULT_FOLDER_STATUS_COLORS: localDefaultFolderStatusColors,
29+
getFolderStatusColors: (settings) => {
30+
const incoming = settings && typeof settings === 'object' ? settings : {};
31+
return {
32+
started: normalizeStatusHexColor(incoming.status_color_started, localDefaultFolderStatusColors.started),
33+
paused: normalizeStatusHexColor(incoming.status_color_paused, localDefaultFolderStatusColors.paused),
34+
stopped: normalizeStatusHexColor(incoming.status_color_stopped, localDefaultFolderStatusColors.stopped)
35+
};
36+
}
1037
};
1138

1239
if (FOLDER_VIEW_DEBUG_MODE) {
@@ -1022,21 +1049,27 @@ const createFolder = (folder, id, positionInMainOrder, liveOrderArray, container
10221049
if (FOLDER_VIEW_DEBUG_MODE) console.log(`[FV3_DEBUG] createFolder (id: ${id}): Set 'update ready' status in update column.`);
10231050
}
10241051
const total = Object.entries(folder.containers).length;
1052+
const statusColors = typeof utils.getFolderStatusColors === 'function'
1053+
? utils.getFolderStatusColors(folder.settings)
1054+
: localDefaultFolderStatusColors;
1055+
const $folderIcon = $(`tr.folder-id-${id} i#load-folder-${id}`);
10251056
const $folderState = $(`tr.folder-id-${id} span.folder-state`);
10261057
$folderState.removeClass('fv-folder-state-started fv-folder-state-paused fv-folder-state-stopped');
1058+
$folderState.css('color', '');
1059+
$folderIcon.show().css('color', '');
10271060
let folderStatusKind = 'stopped';
10281061
if (started > 0) {
10291062
folderStatusKind = 'running';
1030-
$(`tr.folder-id-${id} i#load-folder-${id}`).attr('class', 'fa fa-play started green-text folder-load-status');
1031-
$folderState.text(`${started}/${total} ${$.i18n('started')}`).addClass('fv-folder-state-started');
1063+
$folderIcon.attr('class', 'fa fa-play started folder-load-status').css('color', statusColors.started);
1064+
$folderState.text(`${started}/${total} ${$.i18n('started')}`).addClass('fv-folder-state-started').css('color', statusColors.started);
10321065
} else if (paused > 0) {
10331066
folderStatusKind = 'paused';
1034-
$(`tr.folder-id-${id} i#load-folder-${id}`).attr('class', 'fa fa-pause paused orange-text folder-load-status');
1035-
$folderState.text(`${paused}/${total} ${$.i18n('paused')}`).addClass('fv-folder-state-paused');
1067+
$folderIcon.attr('class', 'fa fa-pause paused folder-load-status').css('color', statusColors.paused);
1068+
$folderState.text(`${paused}/${total} ${$.i18n('paused')}`).addClass('fv-folder-state-paused').css('color', statusColors.paused);
10361069
} else {
10371070
folderStatusKind = 'stopped';
1038-
$(`tr.folder-id-${id} i#load-folder-${id}`).attr('class', 'fa fa-square stopped red-text folder-load-status');
1039-
$folderState.text(`${stopped}/${total} ${$.i18n('stopped')}`).addClass('fv-folder-state-stopped');
1071+
$folderIcon.attr('class', 'fa fa-square stopped folder-load-status').css('color', statusColors.stopped);
1072+
$folderState.text(`${stopped}/${total} ${$.i18n('stopped')}`).addClass('fv-folder-state-stopped').css('color', statusColors.stopped);
10401073
}
10411074
const badgePrefs = folderTypePrefs?.badges || {};
10421075
const showRunningBadge = badgePrefs.running !== false;

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folder.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,42 @@ let selected = [];
88
const type = new URLSearchParams(location.search).get('type');
99
//id of the folder if present
1010
const folderId = new URLSearchParams(location.search).get('id');
11+
const DEFAULT_FOLDER_STATUS_COLORS = {
12+
started: '#ffffff',
13+
paused: '#b8860b',
14+
stopped: '#ff4d4d'
15+
};
1116

1217
const rgbToHex = (rgb) => {
1318
rgb = rgb.slice(4, -1).split(', ');
1419
return "#" + (1 << 24 | rgb[0] << 16 | rgb[1] << 8 | rgb[2]).toString(16).slice(1);
1520
}
1621

22+
const normalizeHexColor = (value, fallback) => {
23+
if (typeof value !== 'string') {
24+
return fallback;
25+
}
26+
const trimmed = value.trim();
27+
if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(trimmed)) {
28+
return fallback;
29+
}
30+
if (trimmed.length === 4) {
31+
return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`.toLowerCase();
32+
}
33+
return trimmed.toLowerCase();
34+
};
35+
36+
const resetStatusColorDefaults = () => {
37+
const form = $('div.canvas > form')[0];
38+
form.status_color_started.value = DEFAULT_FOLDER_STATUS_COLORS.started;
39+
form.status_color_paused.value = DEFAULT_FOLDER_STATUS_COLORS.paused;
40+
form.status_color_stopped.value = DEFAULT_FOLDER_STATUS_COLORS.stopped;
41+
};
42+
window.resetStatusColorDefaults = resetStatusColorDefaults;
43+
1744
$('div.canvas > form')[0].preview_border_color.value = rgbToHex($('body').css('color'));
1845
$('div.canvas > form')[0].preview_vertical_bars_color.value = rgbToHex($('body').css('color'));
46+
resetStatusColorDefaults();
1947

2048
(async () => {
2149
// if editing a vm hide docker related settings
@@ -74,6 +102,9 @@ $('div.canvas > form')[0].preview_vertical_bars_color.value = rgbToHex($('body')
74102
form.preview_border.checked = currFolder.settings.preview_border || false;
75103
form.preview_border_color.value = currFolder.settings.preview_border_color || rgbToHex($('body').css('color'));
76104
form.preview_vertical_bars_color.value = currFolder.settings.preview_vertical_bars_color || currFolder.settings.preview_border_color || rgbToHex($('body').css('color'));
105+
form.status_color_started.value = normalizeHexColor(currFolder.settings.status_color_started, DEFAULT_FOLDER_STATUS_COLORS.started);
106+
form.status_color_paused.value = normalizeHexColor(currFolder.settings.status_color_paused, DEFAULT_FOLDER_STATUS_COLORS.paused);
107+
form.status_color_stopped.value = normalizeHexColor(currFolder.settings.status_color_stopped, DEFAULT_FOLDER_STATUS_COLORS.stopped);
77108
form.update_column.checked = currFolder.settings.update_column || false;
78109
form.default_action.checked = currFolder.settings.default_action || false;
79110
form.expand_tab.checked = currFolder.settings.expand_tab;
@@ -282,6 +313,9 @@ const submitForm = async (e) => {
282313
preview_border: e.preview_border.checked,
283314
preview_border_color: e.preview_border_color.value.toString(),
284315
preview_vertical_bars_color: e.preview_vertical_bars_color.value.toString(),
316+
status_color_started: normalizeHexColor(e.status_color_started.value.toString(), DEFAULT_FOLDER_STATUS_COLORS.started),
317+
status_color_paused: normalizeHexColor(e.status_color_paused.value.toString(), DEFAULT_FOLDER_STATUS_COLORS.paused),
318+
status_color_stopped: normalizeHexColor(e.status_color_stopped.value.toString(), DEFAULT_FOLDER_STATUS_COLORS.stopped),
285319
update_column: e.update_column.checked,
286320
default_action: e.default_action.checked,
287321
expand_tab: e.expand_tab.checked,

0 commit comments

Comments
 (0)