secubox-openwrt/package/secubox/luci-app-photoprism/htdocs/luci-static/resources/view/photoprism/overview.js
CyberMind-FR 1bbd345cee refactor(luci): Mass KissTheme UI rework across all LuCI apps
Convert 90+ LuCI view files from legacy cbi-button-* classes to
KissTheme kiss-btn-* classes for consistent dark theme styling.

Pattern conversions applied:
- cbi-button-positive → kiss-btn-green
- cbi-button-negative/remove → kiss-btn-red
- cbi-button-apply → kiss-btn-cyan
- cbi-button-action → kiss-btn-blue
- cbi-button (plain) → kiss-btn

Also replaced hardcoded colors (#080, #c00, #888, etc.) with
CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.)
for proper dark theme compatibility.

Apps updated include: ai-gateway, auth-guardian, bandwidth-manager,
cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure,
glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager,
mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer,
metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status,
secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats,
service-registry, simplex, streamlit, system-hub, tor-shield,
traffic-shaper, vhost-manager, vortex-dns, vortex-firewall,
webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-12 11:09:34 +01:00

436 lines
15 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require ui';
'require poll';
'require secubox/kiss-theme';
var callStatus = rpc.declare({
object: 'luci.photoprism',
method: 'status',
expect: {}
});
var callGetStats = rpc.declare({
object: 'luci.photoprism',
method: 'get_stats',
expect: {}
});
var callGetIndexProgress = rpc.declare({
object: 'luci.photoprism',
method: 'get_index_progress',
expect: {}
});
var callStart = rpc.declare({
object: 'luci.photoprism',
method: 'start',
expect: {}
});
var callStop = rpc.declare({
object: 'luci.photoprism',
method: 'stop',
expect: {}
});
var callInstall = rpc.declare({
object: 'luci.photoprism',
method: 'install',
expect: {}
});
var callUninstall = rpc.declare({
object: 'luci.photoprism',
method: 'uninstall',
expect: {}
});
var callIndex = rpc.declare({
object: 'luci.photoprism',
method: 'index',
expect: {}
});
var callImport = rpc.declare({
object: 'luci.photoprism',
method: 'import',
expect: {}
});
var callEmancipate = rpc.declare({
object: 'luci.photoprism',
method: 'emancipate',
params: ['domain'],
expect: {}
});
var callGetConfig = rpc.declare({
object: 'luci.photoprism',
method: 'get_config',
expect: {}
});
var callSetConfig = rpc.declare({
object: 'luci.photoprism',
method: 'set_config',
params: ['originals_path'],
expect: {}
});
return view.extend({
status: null,
stats: null,
config: null,
indexProgress: null,
load: function() {
return Promise.all([
callStatus(),
callGetStats(),
callGetConfig(),
callGetIndexProgress()
]);
},
renderNav: function(active) {
var tabs = [
{ name: 'Overview', path: 'admin/services/photoprism/overview' },
{ name: 'Settings', path: 'admin/services/photoprism/settings' }
];
return E('div', { 'class': 'kiss-tabs' }, tabs.map(function(tab) {
var isActive = tab.path.indexOf(active) !== -1;
return E('a', {
'href': L.url(tab.path),
'class': 'kiss-tab' + (isActive ? ' active' : '')
}, tab.name);
}));
},
formatNumber: function(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return String(n);
},
renderStats: function(stats, indexProgress) {
var c = KissTheme.colors;
return [
KissTheme.stat(this.formatNumber(indexProgress.sidecar_count || 0), 'Indexed', c.blue),
KissTheme.stat(this.formatNumber(indexProgress.thumbnail_count || 0), 'Thumbnails', c.purple),
KissTheme.stat(this.formatNumber(stats.photo_count || 0), 'Photos', c.green),
KissTheme.stat(this.formatNumber(stats.video_count || 0), 'Videos', c.orange)
];
},
renderInstallWizard: function() {
var self = this;
return KissTheme.card('Install PhotoPrism', E('div', {}, [
E('p', { 'style': 'color: var(--kiss-muted); margin-bottom: 16px;' }, 'Self-hosted Google Photos alternative with AI-powered face recognition, search, and albums.'),
E('div', { 'style': 'background: var(--kiss-bg2); padding: 16px; border-radius: 8px; margin-bottom: 16px;' }, [
E('div', { 'style': 'font-weight: 600; margin-bottom: 12px;' }, 'Features'),
E('ul', { 'style': 'color: var(--kiss-muted); margin: 0; padding-left: 20px;' }, [
E('li', {}, 'AI-powered face recognition and clustering'),
E('li', {}, 'Automatic image classification'),
E('li', {}, 'Location mapping and search'),
E('li', {}, 'RAW photo support and conversion'),
E('li', {}, 'Video transcoding and playback'),
E('li', {}, 'Albums and sharing')
])
]),
E('button', {
'class': 'kiss-btn kiss-btn-green',
'click': function() {
ui.showModal('Installing...', [E('p', { 'class': 'spinning' }, 'Installing PhotoPrism...')]);
callInstall().then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', 'PhotoPrism installed successfully!'), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Installation failed: ' + (res.output || 'Unknown error')), 'error');
}
});
}
}, 'Install PhotoPrism')
]));
},
renderHealth: function(status, stats) {
var checks = [
{ label: 'Container', ok: status.installed, value: status.installed ? (status.running ? 'Running' : 'Stopped') : 'Not Installed' },
{ label: 'Face Recognition', ok: status.face_recognition, value: status.face_recognition ? 'Enabled' : 'Disabled' },
{ label: 'Object Detection', ok: status.object_detection, value: status.object_detection ? 'Enabled' : 'Disabled' },
{ label: 'Places/Maps', ok: status.places, value: status.places ? 'Enabled' : 'Disabled' },
{ label: 'Domain', ok: !!status.domain, value: status.domain || 'Not configured' }
];
return E('div', { 'style': 'display: flex; flex-direction: column; gap: 8px;' }, checks.map(function(c) {
return E('div', { 'style': 'display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--kiss-line);' }, [
E('div', { 'style': 'width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; ' +
(c.ok ? 'background: rgba(0,200,83,0.15); color: var(--kiss-green);' : 'background: rgba(255,23,68,0.15); color: var(--kiss-red);') },
c.ok ? '\u2713' : '\u2717'),
E('div', { 'style': 'flex: 1;' }, [
E('div', { 'style': 'font-size: 13px; color: var(--kiss-text);' }, c.label),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, c.value)
])
]);
}));
},
renderControls: function(status) {
var self = this;
var running = status.running;
return E('div', { 'style': 'display: flex; flex-direction: column; gap: 16px;' }, [
// Service controls
E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap;' }, [
running ? E('button', {
'class': 'kiss-btn kiss-btn-red',
'click': function() {
callStop().then(function() { window.location.reload(); });
}
}, 'Stop') : E('button', {
'class': 'kiss-btn kiss-btn-green',
'click': function() {
callStart().then(function() { window.location.reload(); });
}
}, 'Start'),
E('button', {
'class': 'kiss-btn',
'disabled': !running,
'click': function() {
ui.showModal('Indexing...', [E('p', { 'class': 'spinning' }, 'Starting index...')]);
callIndex().then(function(res) {
ui.hideModal();
ui.addNotification(null, E('p', 'Indexing started'), 'success');
});
}
}, 'Index Photos'),
E('button', {
'class': 'kiss-btn',
'disabled': !running,
'click': function() {
ui.showModal('Importing...', [E('p', { 'class': 'spinning' }, 'Importing photos...')]);
callImport().then(function(res) {
ui.hideModal();
ui.addNotification(null, E('p', 'Import complete'), 'success');
window.location.reload();
});
}
}, 'Import')
]),
// Open Gallery
running ? E('a', {
'href': 'http://' + window.location.hostname + ':' + (status.port || 2342),
'target': '_blank',
'class': 'kiss-btn kiss-btn-blue',
'style': 'text-decoration: none; text-align: center;'
}, 'Open Gallery') : '',
// Uninstall
E('button', {
'class': 'kiss-btn kiss-btn-red',
'click': function() {
if (confirm('Remove PhotoPrism container? Photos will be preserved.')) {
callUninstall().then(function() {
ui.addNotification(null, E('p', 'PhotoPrism uninstalled'), 'success');
window.location.reload();
});
}
}
}, 'Uninstall')
]);
},
renderIndexProgress: function(indexProgress) {
if (!indexProgress.indexing) {
return E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
E('span', { 'style': 'color: var(--kiss-green);' }, '\u2713 Ready'),
E('span', { 'style': 'font-family: monospace; font-size: 12px; color: var(--kiss-muted);' }, 'DB: ' + (indexProgress.db_size || '0'))
]);
}
return E('div', {}, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 8px;' }, [
E('span', { 'style': 'color: var(--kiss-orange);' }, 'Indexing...'),
E('span', { 'style': 'font-family: monospace; font-size: 11px; color: var(--kiss-muted);' }, indexProgress.current_file || 'Processing...')
]),
E('div', { 'style': 'height: 6px; background: var(--kiss-bg); border-radius: 3px; overflow: hidden;' }, [
E('div', { 'style': 'width: 100%; height: 100%; background: linear-gradient(90deg, var(--kiss-orange), var(--kiss-purple)); animation: pulse 1.5s infinite;' })
])
]);
},
renderStorage: function(stats, config) {
var self = this;
return E('div', { 'style': 'display: flex; flex-direction: column; gap: 16px;' }, [
E('div', { 'class': 'kiss-grid kiss-grid-2' }, [
E('div', { 'style': 'background: var(--kiss-bg2); padding: 16px; border-radius: 8px; text-align: center;' }, [
E('div', { 'style': 'font-size: 24px; font-weight: 700; color: var(--kiss-cyan);' }, stats.originals_size || '0'),
E('div', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Originals')
]),
E('div', { 'style': 'background: var(--kiss-bg2); padding: 16px; border-radius: 8px; text-align: center;' }, [
E('div', { 'style': 'font-size: 24px; font-weight: 700; color: var(--kiss-purple);' }, stats.storage_size || '0'),
E('div', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Cache')
])
]),
E('div', { 'style': 'display: flex; gap: 8px;' }, [
E('input', {
'type': 'text',
'id': 'originals-path',
'value': config.originals_path || '/srv/photoprism/originals',
'placeholder': '/mnt/PHOTO',
'style': 'flex: 1; background: var(--kiss-bg2); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 8px 12px; border-radius: 6px;'
}),
E('button', {
'class': 'kiss-btn',
'click': function() {
var path = document.getElementById('originals-path').value;
if (!path) {
ui.addNotification(null, E('p', 'Please enter a path'), 'warning');
return;
}
callSetConfig(path).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', 'Path updated. Restart to apply.'), 'success');
}
});
}
}, 'Save Path')
])
]);
},
renderEmancipate: function(status) {
var self = this;
var domain = status.domain || '';
return E('div', { 'style': 'display: flex; flex-direction: column; gap: 12px;' }, [
E('p', { 'style': 'color: var(--kiss-muted); font-size: 12px; margin: 0;' }, 'Expose your photo gallery publicly with SSL.'),
domain ? E('div', { 'style': 'padding: 12px; background: rgba(0,200,83,0.1); border-radius: 6px;' }, [
E('span', {}, 'Gallery at: '),
E('a', { 'href': 'https://' + domain, 'target': '_blank', 'style': 'color: var(--kiss-cyan);' }, 'https://' + domain)
]) : E('div', { 'style': 'display: flex; gap: 8px;' }, [
E('input', {
'type': 'text',
'id': 'emancipate-domain',
'placeholder': 'photos.example.com',
'style': 'flex: 1; background: var(--kiss-bg2); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 8px 12px; border-radius: 6px;'
}),
E('button', {
'class': 'kiss-btn kiss-btn-green',
'click': function() {
var d = document.getElementById('emancipate-domain').value;
if (!d) {
ui.addNotification(null, E('p', 'Please enter a domain'), 'warning');
return;
}
ui.showModal('Configuring...', [E('p', { 'class': 'spinning' }, 'Setting up public exposure...')]);
callEmancipate(d).then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', 'Gallery exposed at ' + res.url), 'success');
window.location.reload();
}
});
}
}, 'Emancipate')
])
]);
},
render: function(data) {
var self = this;
this.status = data[0] || {};
this.stats = data[1] || {};
this.config = data[2] || {};
this.indexProgress = data[3] || {};
var status = this.status;
var stats = this.stats;
var config = this.config;
var indexProgress = this.indexProgress;
var c = KissTheme.colors;
if (!status.installed) {
var content = [
E('div', { 'style': 'margin-bottom: 24px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'PhotoPrism'),
KissTheme.badge('NOT INSTALLED', 'yellow')
]),
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, 'AI-powered photo gallery')
]),
this.renderNav('overview'),
this.renderInstallWizard()
];
return KissTheme.wrap(content, 'admin/services/photoprism/overview');
}
var content = [
// Header
E('div', { 'style': 'margin-bottom: 24px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'PhotoPrism'),
KissTheme.badge(status.running ? 'RUNNING' : 'STOPPED', status.running ? 'green' : 'red')
]),
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, 'AI-powered photo gallery')
]),
// Navigation
this.renderNav('overview'),
// Stats
E('div', { 'class': 'kiss-grid kiss-grid-4', 'id': 'photoprism-stats', 'style': 'margin: 20px 0;' }, this.renderStats(stats, indexProgress)),
// Index progress
KissTheme.card('Index Status', E('div', { 'id': 'index-progress' }, this.renderIndexProgress(indexProgress))),
// Two column layout
E('div', { 'class': 'kiss-grid kiss-grid-2' }, [
KissTheme.card('AI Features', this.renderHealth(status, stats)),
KissTheme.card('Controls', this.renderControls(status))
]),
// Storage and Emancipate
E('div', { 'class': 'kiss-grid kiss-grid-2' }, [
KissTheme.card('Storage', this.renderStorage(stats, config)),
KissTheme.card('Public Exposure', this.renderEmancipate(status))
])
];
poll.add(function() {
return Promise.all([callStatus(), callGetStats(), callGetConfig(), callGetIndexProgress()]).then(function(results) {
self.status = results[0] || {};
self.stats = results[1] || {};
self.config = results[2] || {};
self.indexProgress = results[3] || {};
var statsEl = document.getElementById('photoprism-stats');
if (statsEl) {
statsEl.innerHTML = '';
self.renderStats(self.stats, self.indexProgress).forEach(function(el) { statsEl.appendChild(el); });
}
var indexEl = document.getElementById('index-progress');
if (indexEl) {
indexEl.innerHTML = '';
indexEl.appendChild(self.renderIndexProgress(self.indexProgress));
}
});
}, 5);
return KissTheme.wrap(content, 'admin/services/photoprism/overview');
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});