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>
382 lines
13 KiB
JavaScript
382 lines
13 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
'require form';
|
|
'require uci';
|
|
'require secubox/kiss-theme';
|
|
|
|
var callSchedules = rpc.declare({
|
|
object: 'luci.webradio',
|
|
method: 'schedules',
|
|
expect: {}
|
|
});
|
|
|
|
var callCurrentShow = rpc.declare({
|
|
object: 'luci.webradio',
|
|
method: 'current_show',
|
|
expect: {}
|
|
});
|
|
|
|
var callAddSchedule = rpc.declare({
|
|
object: 'luci.webradio',
|
|
method: 'add_schedule',
|
|
params: ['name', 'start_time', 'end_time', 'days', 'playlist', 'jingle_before'],
|
|
expect: {}
|
|
});
|
|
|
|
var callUpdateSchedule = rpc.declare({
|
|
object: 'luci.webradio',
|
|
method: 'update_schedule',
|
|
params: ['slot', 'enabled', 'name', 'start_time', 'end_time', 'days', 'playlist', 'jingle_before'],
|
|
expect: {}
|
|
});
|
|
|
|
var callDeleteSchedule = rpc.declare({
|
|
object: 'luci.webradio',
|
|
method: 'delete_schedule',
|
|
params: ['slot'],
|
|
expect: {}
|
|
});
|
|
|
|
var callGenerateCron = rpc.declare({
|
|
object: 'luci.webradio',
|
|
method: 'generate_cron',
|
|
expect: {}
|
|
});
|
|
|
|
var DAYS = {
|
|
'0': 'Sun',
|
|
'1': 'Mon',
|
|
'2': 'Tue',
|
|
'3': 'Wed',
|
|
'4': 'Thu',
|
|
'5': 'Fri',
|
|
'6': 'Sat'
|
|
};
|
|
|
|
function formatDays(days) {
|
|
if (!days) return 'Every day';
|
|
if (days === '0123456') return 'Every day';
|
|
if (days === '12345') return 'Weekdays';
|
|
if (days === '06') return 'Weekends';
|
|
|
|
return days.split('').map(function(d) {
|
|
return DAYS[d] || d;
|
|
}).join(', ');
|
|
}
|
|
|
|
return view.extend({
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null,
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
callSchedules(),
|
|
callCurrentShow(),
|
|
uci.load('webradio')
|
|
]);
|
|
},
|
|
|
|
renderStats: function(scheduleCount, currentShow, schedulingEnabled) {
|
|
var c = KissTheme.colors;
|
|
return [
|
|
KissTheme.stat(currentShow.name || 'Default', 'Now Playing', c.green),
|
|
KissTheme.stat(scheduleCount, 'Schedules', c.blue),
|
|
KissTheme.stat(schedulingEnabled ? 'On' : 'Off', 'Auto-Switch', schedulingEnabled ? c.purple : c.muted)
|
|
];
|
|
},
|
|
|
|
renderScheduleTable: function(schedules) {
|
|
if (!schedules || schedules.length === 0) {
|
|
return E('p', { 'style': 'color: var(--kiss-muted);' },
|
|
'No schedules configured. Add a schedule above to create a programming grid.');
|
|
}
|
|
|
|
var self = this;
|
|
var rows = schedules.map(function(sched) {
|
|
return E('tr', {}, [
|
|
E('td', { 'style': 'width: 40px;' }, [
|
|
E('input', {
|
|
'type': 'checkbox',
|
|
'checked': sched.enabled,
|
|
'data-slot': sched.slot,
|
|
'change': function(ev) {
|
|
self.handleToggleEnabled(sched.slot, ev.target.checked);
|
|
}
|
|
})
|
|
]),
|
|
E('td', { 'style': 'font-weight: 600;' }, sched.name),
|
|
E('td', {}, sched.start_time + ' - ' + (sched.end_time || '...')),
|
|
E('td', { 'style': 'color: var(--kiss-muted);' }, formatDays(sched.days)),
|
|
E('td', {}, sched.playlist || '-'),
|
|
E('td', { 'style': 'color: var(--kiss-muted);' }, sched.jingle_before || '-'),
|
|
E('td', { 'style': 'width: 80px;' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-red',
|
|
'style': 'padding: 4px 10px; font-size: 11px;',
|
|
'click': ui.createHandlerFn(self, 'handleDelete', sched.slot)
|
|
}, 'Delete')
|
|
])
|
|
]);
|
|
});
|
|
|
|
return E('table', { 'class': 'kiss-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', {}, [
|
|
E('th', {}, 'On'),
|
|
E('th', {}, 'Name'),
|
|
E('th', {}, 'Time'),
|
|
E('th', {}, 'Days'),
|
|
E('th', {}, 'Playlist'),
|
|
E('th', {}, 'Jingle'),
|
|
E('th', {}, 'Action')
|
|
])
|
|
]),
|
|
E('tbody', {}, rows)
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
var scheduleData = data[0] || {};
|
|
var currentShow = data[1] || {};
|
|
var schedules = scheduleData.schedules || [];
|
|
var schedulingEnabled = scheduleData.scheduling_enabled;
|
|
|
|
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;' }, 'Programming Schedule'),
|
|
KissTheme.badge(schedules.length + ' Shows', 'cyan')
|
|
]),
|
|
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' },
|
|
'Schedule automated show changes and playlist rotations')
|
|
]),
|
|
|
|
// Stats
|
|
E('div', { 'class': 'kiss-grid kiss-grid-3', 'style': 'margin: 20px 0;' },
|
|
this.renderStats(schedules.length, currentShow, schedulingEnabled)),
|
|
|
|
// Current show info
|
|
KissTheme.card('Now Playing',
|
|
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px;' }, [
|
|
E('div', {}, [
|
|
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); margin-bottom: 4px;' }, 'Show'),
|
|
E('div', { 'style': 'font-weight: 600; font-size: 16px;' }, currentShow.name || 'Default')
|
|
]),
|
|
currentShow.playlist ? E('div', {}, [
|
|
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); margin-bottom: 4px;' }, 'Playlist'),
|
|
E('div', {}, currentShow.playlist)
|
|
]) : '',
|
|
currentShow.start ? E('div', {}, [
|
|
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); margin-bottom: 4px;' }, 'Started'),
|
|
E('div', {}, currentShow.start)
|
|
]) : ''
|
|
])
|
|
),
|
|
|
|
// Scheduling settings
|
|
KissTheme.card('Scheduling Settings',
|
|
E('div', { 'style': 'display: flex; flex-direction: column; gap: 16px;' }, [
|
|
E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [
|
|
E('label', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
|
E('input', {
|
|
'type': 'checkbox',
|
|
'id': 'scheduling-enabled',
|
|
'checked': schedulingEnabled
|
|
}),
|
|
E('span', {}, 'Automatically switch shows based on schedule')
|
|
])
|
|
]),
|
|
E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [
|
|
E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Timezone'),
|
|
E('select', {
|
|
'id': 'timezone',
|
|
'style': 'background: var(--kiss-bg); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 10px 12px; border-radius: 6px; max-width: 250px;'
|
|
}, [
|
|
E('option', { 'value': 'UTC', 'selected': scheduleData.timezone === 'UTC' }, 'UTC'),
|
|
E('option', { 'value': 'Europe/Paris', 'selected': scheduleData.timezone === 'Europe/Paris' }, 'Europe/Paris'),
|
|
E('option', { 'value': 'Europe/London', 'selected': scheduleData.timezone === 'Europe/London' }, 'Europe/London'),
|
|
E('option', { 'value': 'America/New_York', 'selected': scheduleData.timezone === 'America/New_York' }, 'America/New_York'),
|
|
E('option', { 'value': 'America/Los_Angeles', 'selected': scheduleData.timezone === 'America/Los_Angeles' }, 'America/Los_Angeles'),
|
|
E('option', { 'value': 'Asia/Tokyo', 'selected': scheduleData.timezone === 'Asia/Tokyo' }, 'Asia/Tokyo')
|
|
])
|
|
]),
|
|
E('div', { 'style': 'display: flex; gap: 12px;' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-blue',
|
|
'click': ui.createHandlerFn(this, 'handleSaveSettings')
|
|
}, 'Save Settings'),
|
|
E('button', {
|
|
'class': 'kiss-btn',
|
|
'click': ui.createHandlerFn(this, 'handleGenerateCron')
|
|
}, 'Regenerate Cron')
|
|
])
|
|
])
|
|
),
|
|
|
|
// Add new schedule
|
|
KissTheme.card('Add New Schedule',
|
|
E('div', { 'style': 'display: flex; flex-direction: column; gap: 16px;' }, [
|
|
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;' }, [
|
|
E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [
|
|
E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Show Name'),
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'new-name',
|
|
'placeholder': 'Morning Show',
|
|
'style': 'background: var(--kiss-bg); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 10px 12px; border-radius: 6px;'
|
|
})
|
|
]),
|
|
E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [
|
|
E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Start Time'),
|
|
E('input', {
|
|
'type': 'time',
|
|
'id': 'new-start',
|
|
'style': 'background: var(--kiss-bg); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 10px 12px; border-radius: 6px;'
|
|
})
|
|
]),
|
|
E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [
|
|
E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'End Time'),
|
|
E('input', {
|
|
'type': 'time',
|
|
'id': 'new-end',
|
|
'style': 'background: var(--kiss-bg); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 10px 12px; border-radius: 6px;'
|
|
})
|
|
]),
|
|
E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [
|
|
E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Playlist'),
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'new-playlist',
|
|
'placeholder': 'morning_mix',
|
|
'style': 'background: var(--kiss-bg); border: 1px solid var(--kiss-line); color: var(--kiss-text); padding: 10px 12px; border-radius: 6px;'
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'style': 'display: flex; flex-direction: column; gap: 6px;' }, [
|
|
E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'Days'),
|
|
E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' },
|
|
Object.keys(DAYS).map(function(d) {
|
|
return E('label', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [
|
|
E('input', {
|
|
'type': 'checkbox',
|
|
'class': 'day-checkbox',
|
|
'data-day': d,
|
|
'checked': true
|
|
}),
|
|
E('span', { 'style': 'font-size: 13px;' }, DAYS[d])
|
|
]);
|
|
})
|
|
)
|
|
]),
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-green',
|
|
'style': 'align-self: flex-start;',
|
|
'click': ui.createHandlerFn(this, 'handleAddSchedule')
|
|
}, 'Add Schedule')
|
|
])
|
|
),
|
|
|
|
// Schedule list
|
|
KissTheme.card('Scheduled Shows (' + schedules.length + ')', this.renderScheduleTable(schedules))
|
|
];
|
|
|
|
return KissTheme.wrap(content, 'admin/services/webradio/schedule');
|
|
},
|
|
|
|
handleSaveSettings: function() {
|
|
var enabled = document.getElementById('scheduling-enabled').checked;
|
|
var timezone = document.getElementById('timezone').value;
|
|
|
|
uci.set('webradio', 'scheduling', 'scheduling');
|
|
uci.set('webradio', 'scheduling', 'enabled', enabled ? '1' : '0');
|
|
uci.set('webradio', 'scheduling', 'timezone', timezone);
|
|
|
|
return uci.save().then(function() {
|
|
return uci.apply();
|
|
}).then(function() {
|
|
if (enabled) {
|
|
return callGenerateCron();
|
|
}
|
|
}).then(function() {
|
|
ui.addNotification(null, E('p', 'Settings saved'));
|
|
});
|
|
},
|
|
|
|
handleGenerateCron: function() {
|
|
ui.showModal('Generating Cron', [
|
|
E('p', { 'class': 'spinning' }, 'Generating cron schedule...')
|
|
]);
|
|
|
|
return callGenerateCron().then(function(res) {
|
|
ui.hideModal();
|
|
if (res.result === 'ok') {
|
|
ui.addNotification(null, E('p', 'Cron schedule regenerated'));
|
|
} else {
|
|
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'unknown')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
handleAddSchedule: function() {
|
|
var name = document.getElementById('new-name').value;
|
|
var start_time = document.getElementById('new-start').value;
|
|
var end_time = document.getElementById('new-end').value;
|
|
var playlist = document.getElementById('new-playlist').value;
|
|
|
|
if (!name || !start_time) {
|
|
ui.addNotification(null, E('p', 'Name and start time are required'), 'warning');
|
|
return;
|
|
}
|
|
|
|
var days = '';
|
|
document.querySelectorAll('.day-checkbox:checked').forEach(function(cb) {
|
|
days += cb.dataset.day;
|
|
});
|
|
|
|
ui.showModal('Adding Schedule', [
|
|
E('p', { 'class': 'spinning' }, 'Creating schedule...')
|
|
]);
|
|
|
|
return callAddSchedule(name, start_time, end_time, days, playlist, '').then(function(res) {
|
|
ui.hideModal();
|
|
if (res.result === 'ok') {
|
|
ui.addNotification(null, E('p', 'Schedule added: ' + name));
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'unknown')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
handleToggleEnabled: function(slot, enabled) {
|
|
return callUpdateSchedule(slot, enabled, null, null, null, null, null, null).then(function(res) {
|
|
if (res.result === 'ok') {
|
|
ui.addNotification(null, E('p', 'Schedule ' + (enabled ? 'enabled' : 'disabled')));
|
|
}
|
|
});
|
|
},
|
|
|
|
handleDelete: function(slot) {
|
|
if (!confirm('Delete this schedule?')) return;
|
|
|
|
ui.showModal('Deleting', [
|
|
E('p', { 'class': 'spinning' }, 'Removing schedule...')
|
|
]);
|
|
|
|
return callDeleteSchedule(slot).then(function(res) {
|
|
ui.hideModal();
|
|
if (res.result === 'ok') {
|
|
ui.addNotification(null, E('p', 'Schedule deleted'));
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'unknown')), 'error');
|
|
}
|
|
});
|
|
}
|
|
});
|