🦄 Sync 2026-11-13 03:09:08

This commit is contained in:
github-actions[bot] committed 2026-11-13 03:09:08 +08:00
1 parent b8837465cd
commit be1c08bda8
255 files changed
+45916 -22815

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
#
# Copyright 2025 Rafał Wabik - IceG - From eko.one.pl forum
#
# MIT License
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-crontab-wizard
LUCI_TITLE:=Crontab configurator for LuCI JS
MAINTAINER:=Rafał Wabik <4Rafal@gmail.com>
LUCI_DESCRIPTION:=Super simple graphical configurator for crontab.
LUCI_DEPENDS:=
PKG_VERSION:=1.0.4
PKG_RELEASE:=1
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,750 @@
'use strict';
'require baseclass';
'require form';
'require ui';
'require view';
'require fs';
'require tools.widgets as widgets';
/*
Copyright 2025 Rafał Wabik - IceG - From eko.one.pl forum
MIT License
*/
function popTimeout(a, message, timeout, severity) {
ui.addTimeLimitedNotification(a, message, timeout, severity)
}
return view.extend({
colors: {
light: {
minute: '#fdd2d6',
hour: '#cfead1',
day: '#ffd8ad',
month: '#bbdefb',
weekday:'#e1bee7'
},
dark: {
minute: '#5b2f35',
hour: '#2f4732',
day: '#4a3828',
month: '#30475e',
weekday:'#3a2f41'
}
},
getCurrentColors: function() {
const isDarkMode = document.documentElement.getAttribute('data-darkmode') === 'true';
return isDarkMode ? this.colors.dark : this.colors.light;
},
getBadgeTextColor: function() {
const isDarkMode = document.documentElement.getAttribute('data-darkmode') === 'true';
return isDarkMode ? '#ffffff' : '#111111';
},
_weekdayNames: function(){
return [
_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')
];
},
_monthNames: function(){
return [
'', _('January'), _('February'), _('March'), _('April'), _('May'), _('June'),
_('July'), _('August'), _('September'), _('October'), _('November'), _('December')
];
},
_formatRange: function(a, b){
return a + '-' + b;
},
_parseSegments: function(expr, base) {
if (!expr || expr === '*') return { type: 'any' };
if (/^\*\/\d+$/.test(expr)) return { type: 'step', step: parseInt(expr.slice(2), 10) };
const segs = [];
expr.split(',').forEach(function(part){
const m = part.match(/^(\d+)-(\d+)$/);
if (m) {
const a = parseInt(m[1],10), b = parseInt(m[2],10);
segs.push({ kind:'range', a:a, b:b });
} else {
const v = parseInt(part,10);
if (!isNaN(v)) segs.push({ kind:'single', v:v });
}
});
return { type:'list', segs:segs };
},
_expand: function(parsed, minVal, maxVal) {
if (parsed.type === 'any') return null;
const vals = new Set();
if (parsed.type === 'step') {
const n = parsed.step;
for (let i=minVal; i<=maxVal; i+=n) vals.add(i);
return Array.from(vals).sort(function(a,b){ return a-b; });
}
parsed.segs.forEach(function(s){
if (s.kind==='single') { if (s.v>=minVal && s.v<=maxVal) vals.add(s.v); }
else { for (let i=s.a; i<=s.b; i++) if (i>=minVal && i<=maxVal) vals.add(i); }
});
return Array.from(vals).sort(function(a,b){ return a-b; });
},
_humanList: function(parsed, labeller) {
if (parsed.type === 'any') return null;
if (parsed.type === 'step') return null;
const self = this;
const items = parsed.segs.map(function(s){
if (s.kind==='single') return labeller(s.v, false);
return self._formatRange(labeller(s.a,false), labeller(s.b,true));
});
return items.join(', ');
},
_pluralMin: function(n){ return (n===1) ? _('minute') : _('minutes'); },
_pluralHour: function(n){ return (n===1) ? _('hour') : _('hours'); },
_pluralDay: function(n){ return (n===1) ? _('day') : _('days'); },
_pad2: function(n){ return (n<10 ? '0'+n : ''+n); },
_enumerateTimes: function(minExpr, hourExpr) {
const pm = this._parseSegments(minExpr,0), ph = this._parseSegments(hourExpr,0);
const mins = (pm.type==='any' || pm.type==='step') ? null : this._expand(pm,0,59);
const hours = (ph.type==='any' || ph.type==='step') ? null : this._expand(ph,0,23);
if (!mins || !hours) return null;
const combos = [];
for (let i=0;i<hours.length;i++){
for (let j=0;j<mins.length;j++){
combos.push(this._pad2(hours[i])+':'+this._pad2(mins[j]));
}
}
if (combos.length===1) return { single: combos[0], list: null };
if (combos.length>0 && combos.length<=12) return { single: null, list: combos.join(', ') };
return null;
},
_timeOfDayPhrase: function(minExpr, hourExpr) {
const pm = this._parseSegments(minExpr,0), ph = this._parseSegments(hourExpr,0);
const times = this._enumerateTimes(minExpr, hourExpr);
if (times) {
if (times.single) return _('at time: %s').format(times.single);
return _('at times: %s').format(times.list);
}
let parts = [];
// Minutes
if (pm.type==='step') {
parts.push(_('runs every %d %s').format(pm.step, this._pluralMin(pm.step)));
} else if (pm.type==='list') {
const list = this._humanList(pm, function(n){ return String(n); });
if (list) parts.push(_('in minutes: %s').format(list));
}
// Hours
if (ph.type==='step') {
parts.push(_('runs every %d %s').format(ph.step, this._pluralHour(ph.step)));
} else if (ph.type==='list') {
const listH = this._humanList(ph, function(n){ return String(n); });
if (listH) parts.push(_('in hours: %s').format(listH));
}
return parts.length > 0 ? parts.join('; ') : null;
},
_descDayOfMonth: function(expr){
if (expr === '*') return null;
const p = this._parseSegments(expr,1);
if (p.type==='step') {
return _('runs every %d %s of the month').format(p.step, this._pluralDay(p.step));
}
const listStr = this._humanList(p, function(n){ return String(n); });
if (!listStr) return null;
return _('on day(s): %s').format(listStr);
},
_descMonth: function(expr){
if (expr === '*') return null;
const p = this._parseSegments(expr,1);
if (p.type==='step') return _('runs every %d month(s)').format(p.step);
const months = this._monthNames();
const list = this._humanList(p, function(n){ return months[n]; });
return list ? _('in month(s): %s').format(list) : null;
},
_descWeekday: function(expr){
if (expr === '*') return null;
const p = this._parseSegments(expr,0);
if (p.type==='step') return _('runs every %d day of the week').format(p.step);
const days = this._weekdayNames();
const list = this._humanList(p, function(n){ return days[n===7?0:n]; });
return list ? _('on weekday(s): %s').format(list) : null;
},
_describeCron: function(minute, hour, day, month, weekday, command){
const parts = [];
const isMinuteChanged = minute !== '*';
const isHourChanged = hour !== '*';
const isDayChanged = day !== '*';
const isMonthChanged = month !== '*';
const isWeekdayChanged = weekday !== '*';
// Nothing changed
if (!isMinuteChanged && !isHourChanged && !isDayChanged && !isMonthChanged && !isWeekdayChanged) {
const prefix = command ? _('Command "%s" ').format(command) : _('Command');
return prefix + ' ';
}
// Time (hour and minute)
const timePart = this._timeOfDayPhrase(minute, hour);
if (timePart) parts.push(timePart);
// Month
const monthPart = this._descMonth(month);
if (monthPart) parts.push(monthPart);
// Day of month
const dayPart = this._descDayOfMonth(day);
if (dayPart) parts.push(dayPart);
// Day of week
const weekdayPart = this._descWeekday(weekday);
if (weekdayPart) parts.push(weekdayPart);
const prefix = command ? _('Command "%s" will run').format(command) : _('Command will run');
if (parts.length === 0) {
return prefix + '.';
}
return prefix + ' ' + parts.join('; ') + '.';
},
addStyles: function() {
const style = document.createElement('style');
style.type = 'text/css';
style.textContent = `
.ifacebox .span-40 { grid-column: span 2; }
.ifacebox .span-80 { grid-column: span 4; }
.ifacebox .span-160 { grid-column: span 8; }
.ifacebox .span-320 { grid-column: span 8; }
@media (max-width: 560px) {
.ifacebox .span-320 { grid-column: span 8 !important; }
}
:root {
--tile-free: #eceff1;
--tile-active: #34c759;
--tile-text: #263238;
--tile-text-on-active: #ffffff;
--iface-head-bg: #f8f8f8;
--app-id-font-color: #454545;
--border-color-soft: #eceff1;
--border-color-medium: #cfd8dc;
--border-color-strong: #90a4ae;
--tile-border: #b0bec5;
--cron-minute: #fdd2d6;
--cron-hour: #cfead1;
--cron-day: #ffd8ad;
--cron-month: #bbdefb;
--cron-weekday: #e1bee7;
--badge-text: #111111;
--badge-border: rgba(0,0,0,.15);
--badge-shadow: rgba(0,0,0,.06);
--multiselect-bg: var(--border-color-low);
--multiselect-text: #263238;
--multiselect-border: #cfd8dc;
--multiselect-selected-bg: #e3f2fd;
--multiselect-selected-text: #1565c0;
--multiselect-hover-bg: #f5f5f5;
}
:root[data-darkmode="true"] {
--tile-free: #2a2f34;
--tile-active: #2ecc71;
--tile-text: #e5e7eb;
--tile-text-on-active: #ffffff;
--iface-head-bg: #1f2327;
--border-color-soft: #2a2f34;
--border-color-medium: #3a4146;
--border-color-strong: #6b737a;
--app-id-font-color: #f6f6f6;
--tile-border: #54616b;
--cron-minute: #8b4f5a;
--cron-hour: #4f6b52;
--cron-day: #7a5838;
--cron-month: #4a6b8e;
--cron-weekday:#5a4f71;
--badge-text: #ffffff;
--badge-border: rgba(255,255,255,.2);
--badge-shadow: rgba(0,0,0,.35);
--multiselect-bg: var(--border-color-low);
--multiselect-text: #e5e7eb;
--multiselect-border: #3a4146;
--multiselect-hover-bg: #3a4146;
}
.cron-minute-head { background: var(--cron-minute) !important; }
.cron-hour-head { background: var(--cron-hour) !important; }
.cron-day-head { background: var(--cron-day) !important; }
.cron-month-head { background: var(--cron-month) !important; }
.cron-weekday-head{ background: var(--cron-weekday)!important; }
:root .cron-minute-head { color: #263238 !important; }
:root .cron-hour-head { color: #263238 !important; }
:root .cron-day-head { color: #263238 !important; }
:root .cron-month-head { color: #263238 !important; }
:root .cron-weekday-head{ color: #263238 !important; }
:root[data-darkmode="true"] .cron-minute-head { color: #ffcdd2 !important; }
:root[data-darkmode="true"] .cron-hour-head { color: #c8e6c9 !important; }
:root[data-darkmode="true"] .cron-day-head { color: #ffe0b2 !important; }
:root[data-darkmode="true"] .cron-month-head { color: #bbdefb !important; }
:root[data-darkmode="true"] .cron-weekday-head{ color: #e1bee7 !important; }
.ifacebox .tile { font-weight: normal; }
.cron-badge {
display: inline-block;
padding: 3px 6px;
border-radius: 6px;
border: 1px solid var(--badge-border);
box-shadow: 0 1px 1px var(--badge-shadow), inset 0 1px 0 rgba(255,255,255,.08);
font-weight: 600;
letter-spacing: .2px;
}
.cbi-input-text { text-align: left !important; }
#cron_preview_text { text-align: left !important; }
#cron_preview {
height: 40px !important;
min-height: 40px !important;
max-height: 40px !important;
background-color: var(--multiselect-bg) !important;
border: 1px solid var(--badge-border);
line-height: 26px;
display: flex;
align-items: center;
}
.cron-actions {
margin-top: 1em;
display: flex;
justify-content: flex-end;
gap: .6em;
}
.cron-multiselect {
width: 100%;
min-height: 140px;
background-color: var(--multiselect-bg) !important;
color: var(--multiselect-text) !important;
border: 1px solid var(--multiselect-border) !important;
border-radius: 4px;
padding: 4px;
font-family: inherit;
font-size: 13px;
line-height: 1.4;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.cron-multiselect:focus {
border-color: var(--tile-active) !important;
box-shadow: 0 0 0 2px rgba(52, 199, 89, 0.2) !important;
}
:root:not([data-darkmode="true"]) .cron-multiselect option {
padding: 4px 8px; border: none; outline: none;
background-color: revert !important; color: revert !important;
}
:root:not([data-darkmode="true"]) .cron-multiselect option:not(:checked):hover {
background-color: var(--multiselect-hover-bg) !important;
}
/* Dark mode */
:root[data-darkmode="true"] .cron-multiselect option {
background-color: var(--multiselect-bg) !important;
color: var(--multiselect-text) !important;
padding: 4px 8px; border: none; outline: none;
}
:root[data-darkmode="true"] .cron-multiselect option:hover {
background-color: var(--multiselect-hover-bg) !important;
}
:root[data-darkmode="true"] #minute_select option:checked { background-color: var(--cron-minute) !important; color: #ffffff !important; font-weight: 600; }
:root[data-darkmode="true"] #hour_select option:checked { background-color: var(--cron-hour) !important; color: #ffffff !important; font-weight: 600; }
:root[data-darkmode="true"] #day_select option:checked { background-color: var(--cron-day) !important; color: #ffffff !important; font-weight: 600; }
:root[data-darkmode="true"] #month_select option:checked { background-color: var(--cron-month) !important; color: #ffffff !important; font-weight: 600; }
:root[data-darkmode="true"] #weekday_select option:checked { background-color: var(--cron-weekday) !important; color: #ffffff !important; font-weight: 600; }
:root[data-darkmode="true"] #minute_select option:checked:hover { background-color: var(--cron-minute) !important; color: #ffffff !important; }
:root[data-darkmode="true"] #hour_select option:checked:hover { background-color: var(--cron-hour) !important; color: #ffffff !important; }
:root[data-darkmode="true"] #day_select option:checked:hover { background-color: var(--cron-day) !important; color: #ffffff !important; }
:root[data-darkmode="true"] #month_select option:checked:hover { background-color: var(--cron-month) !important; color: #ffffff !important; }
:root[data-darkmode="true"] #weekday_select option:checked:hover { background-color: var(--cron-weekday) !important; color: #ffffff !important; }
:root[data-darkmode="true"] .cron-multiselect::-webkit-scrollbar { width: 8px; }
:root[data-darkmode="true"] .cron-multiselect::-webkit-scrollbar-track { background: #1a1e22; border-radius: 4px; }
:root[data-darkmode="true"] .cron-multiselect::-webkit-scrollbar-thumb { background: #4a5158; border-radius: 4px; }
:root[data-darkmode="true"] .cron-multiselect::-webkit-scrollbar-thumb:hover { background: #5a6168; }
:root .cron-multiselect::-webkit-scrollbar { width: 8px; }
:root .cron-multiselect::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
:root .cron-multiselect::-webkit-scrollbar-thumb { background: #c1c1c1; }
:root .cron-multiselect::-webkit-scrollbar-thumb:hover { background: #a8a8a8; }
`;
document.head.appendChild(style);
},
badge: function(bg, content){
const color = this.getBadgeTextColor();
return '<span class="cron-badge" style="background:' + bg + '; color:' + color + '">' + content + '</span>';
},
getSelectedValues: function(selectId) {
const sel = document.getElementById(selectId);
if (!sel) return [];
const vals = Array.from(sel.selectedOptions || [])
.map(function(o){ return o.value; })
.filter(function(v){ return v !== undefined && v !== null; });
return vals;
},
normalizeSelection: function(values) {
if (!values || values.length === 0) return ['*'];
if (values.includes('*')) return ['*'];
const uniq = Array.from(new Set(values.map(function(v){ return parseInt(v, 10); })));
return uniq.sort(function(a,b){ return a-b; }).map(function(x){ return String(x); });
},
compressValues: function(values) {
if (!values || values.length === 0) return '*';
if (values.length === 1) return values[0];
const nums = values.map(function(v){ return parseInt(v,10); });
let parts = [];
let start = nums[0];
let prev = nums[0];
for (let i = 1; i < nums.length; i++) {
const cur = nums[i];
if (cur === prev + 1) { prev = cur; continue; }
parts.push(start === prev ? String(start) : (start + '-' + prev));
start = prev = cur;
}
parts.push(start === prev ? String(start) : (start + '-' + prev));
return parts.join(',');
},
buildCronField: function(selectId, checkboxId /*, isPeriod*/) {
const valuesRaw = this.getSelectedValues(selectId);
const values = this.normalizeSelection(valuesRaw);
if (checkboxId) {
const cb = document.getElementById(checkboxId);
if (cb && cb.checked) {
const sel = values[0];
if (sel === '*' || sel === undefined) return '*';
return '*/' + sel;
}
}
if (values.length === 0 || values[0] === '*') return '*';
return this.compressValues(values);
},
updateCronPreview: function() {
const minute = this.buildCronField('minute_select', 'minute_checkbox', true);
const hour = this.buildCronField('hour_select', 'hour_checkbox', true);
const day = this.buildCronField('day_select', 'day_checkbox', true);
const month = this.buildCronField('month_select');
const weekday = this.buildCronField('weekday_select');
const cmdInput = document.getElementById('cron_command_input');
const command = cmdInput ? (cmdInput.value || '') : '';
const isMinuteChanged = minute !== '*';
const isHourChanged = hour !== '*';
const isDayChanged = day !== '*';
const isMonthChanged = month !== '*';
const isWeekdayChanged = weekday !== '*';
const isCommandChanged = command !== '';
const colors = this.getCurrentColors();
let cronHtml = '';
cronHtml += isMinuteChanged ? this.badge(colors.minute, minute) : minute; cronHtml += ' ';
cronHtml += isHourChanged ? this.badge(colors.hour, hour) : hour; cronHtml += ' ';
cronHtml += isDayChanged ? this.badge(colors.day, day) : day; cronHtml += ' ';
cronHtml += isMonthChanged ? this.badge(colors.month, month) : month; cronHtml += ' ';
cronHtml += isWeekdayChanged ? this.badge(colors.weekday, weekday) : weekday; cronHtml += ' ';
cronHtml += isCommandChanged ? '<span class="cron-badge" style="background: var(--border-color-medium, var(--border-color-strong)); color:var(--badge-text)">' + command + '</span>'
: command
document.getElementById('cron_preview').innerHTML = cronHtml;
const plain = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + weekday + ' ' + command;
document.getElementById('cron_preview_text').value = plain;
const human = this._describeCron(minute, hour, day, month, weekday, command);
const humanEl = document.getElementById('cron_human_text');
if (humanEl) humanEl.value = human;
},
appendCronLine: function() {
const input = document.getElementById('cron_preview_text');
if (!input) {
ui.addNotification(null, E('p', _('Please enter the command')), 'info');
return;
}
const cronLine = (input.value || '').trim();
const isDefault = cronLine === '* * * * *' || cronLine === '';
const parts = cronLine.split(/\s+/);
if (isDefault) {
ui.addNotification(null, E('p', _('Check the cron entry, it must contain (time + command)')), 'info');
return;
}
if (parts.length < 6) {
ui.addNotification(null, E('p', _('Please enter the command')), 'info');
return;
}
return fs.read('/etc/crontabs/root').then(function(content) {
let current = (content || '').replace(/\r\n/g, '\n');
const haystack = '\n' + current + (current.endsWith('\n') ? '' : '\n');
const needle = '\n' + cronLine + '\n';
if (haystack.indexOf(needle) !== -1) {
ui.addNotification(null, E('p', _('This entry already exists in cron') + ': ' + cronLine));
return Promise.resolve('duplicate');
}
if (!current.endsWith('\n') && current.length > 0) current += '\n';
current += cronLine + '\n';
return fs.write('/etc/crontabs/root', current).then(function(){ return 'written'; });
}).then(function(state) {
if (state === 'duplicate') return;
return fs.exec('/etc/init.d/cron', ['restart']).then(function() {
popTimeout(null, E('p', _('Added entry to cron') + ': ' + cronLine), 5000, 'info');
});
}).catch(function(e) {
ui.addNotification(null, E('p', _('Error: %s').format(e.message)));
});
},
generateOptions: function(start, end, labels) {
const opts = [];
opts.push(E('option', { value: '*', selected: true }, '*'));
if (labels) {
for (let i = 0; i < labels.length; i++) {
opts.push(E('option', { value: (start + i).toString() }, labels[i]));
}
} else {
for (let i = start; i <= end; i++) {
opts.push(E('option', { value: i.toString() }, i.toString()));
}
}
return opts;
},
resetMulti: function(id) {
const sel = document.getElementById(id);
if (!sel) return;
Array.from(sel.options).forEach(function(o){ o.selected = (o.value === '*'); });
},
render: function(){
this.addStyles();
const root = E('div', { class:'cbi-map' }, [
E('h4', {}, [ _('Graphical Crontab Configurator') ]),
E('div', { 'class': 'cbi-section-descr fade-in' }, [_('A super simple graphical configurator for crontab.') ]),
E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));margin-bottom:1em;gap:1em' }, [
E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100%' }, [
E('div', { 'class': 'ifacebox-head cron-minute-head', 'style': 'font-weight:bold;padding:8px;color:#ffffff;' }, [ _('Minute') ]),
E('div', { 'class': 'ifacebox-body', 'style': 'padding:8px' }, [
E('select', {
'id': 'minute_select',
'class': 'cbi-input-select cron-multiselect',
'multiple': true,
'size': 8,
'change': ui.createHandlerFn(this, 'updateCronPreview')
}, this.generateOptions(0, 59)),
E('div', { 'style': 'display:flex; align-items:center; gap:5px; margin-top:6px' }, [
E('input', {
'type': 'checkbox',
'id': 'minute_checkbox',
'change': ui.createHandlerFn(this, 'updateCronPreview')
}),
E('label', { 'for': 'minute_checkbox', 'style': 'font-size:12px' }, _('every specified minute period'))
])
])
]),
E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100%' }, [
E('div', { 'class': 'ifacebox-head cron-hour-head', 'style': 'font-weight:bold;padding:8px;color:#ffffff;' }, [ _('Hour') ]),
E('div', { 'class': 'ifacebox-body', 'style': 'padding:8px' }, [
E('select', {
'id': 'hour_select',
'class': 'cbi-input-select cron-multiselect',
'multiple': true,
'size': 8,
'change': ui.createHandlerFn(this, 'updateCronPreview')
}, this.generateOptions(0, 23)),
E('div', { 'style': 'display:flex; align-items:center; gap:5px; margin-top:6px' }, [
E('input', {
'type': 'checkbox',
'id': 'hour_checkbox',
'change': ui.createHandlerFn(this, 'updateCronPreview')
}),
E('label', { 'for': 'hour_checkbox', 'style': 'font-size:12px' }, _('every specified hour period'))
])
])
]),
E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100%' }, [
E('div', { 'class': 'ifacebox-head cron-day-head', 'style': 'font-weight:bold;padding:8px;color:#ffffff;' }, [ _('Day') ]),
E('div', { 'class': 'ifacebox-body', 'style': 'padding:8px' }, [
E('select', {
'id': 'day_select',
'class': 'cbi-input-select cron-multiselect',
'multiple': true,
'size': 8,
'change': ui.createHandlerFn(this, 'updateCronPreview')
}, this.generateOptions(1, 31)),
E('div', { 'style': 'display:flex; align-items:center; gap:5px; margin-top:6px' }, [
E('input', {
'type': 'checkbox',
'id': 'day_checkbox',
'change': ui.createHandlerFn(this, 'updateCronPreview')
}),
E('label', { 'for': 'day_checkbox', 'style': 'font-size:12px' }, _('every specified day period'))
])
])
]),
E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100%' }, [
E('div', { 'class': 'ifacebox-head cron-month-head', 'style': 'font-weight:bold;padding:8px;color:#ffffff;' }, [ _('Month') ]),
E('div', { 'class': 'ifacebox-body', 'style': 'padding:8px' }, [
E('select', {
'id': 'month_select',
'class': 'cbi-input-select cron-multiselect',
'multiple': true,
'size': 8,
'change': ui.createHandlerFn(this, 'updateCronPreview')
}, this.generateOptions(1, 12, [
_('January'), _('February'), _('March'), _('April'), _('May'), _('June'),
_('July'), _('August'), _('September'), _('October'), _('November'), _('December')
]))
])
]),
E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100%' }, [
E('div', { 'class': 'ifacebox-head cron-weekday-head', 'style': 'font-weight:bold;padding:8px;color:#ffffff;' }, [ _('Weekday') ]),
E('div', { 'class': 'ifacebox-body', 'style': 'padding:8px' }, [
E('select', {
'id': 'weekday_select',
'class': 'cbi-input-select cron-multiselect',
'multiple': true,
'size': 8,
'change': ui.createHandlerFn(this, 'updateCronPreview')
}, this.generateOptions(0, 6, [
_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')
]))
])
])
]),
E('label', { 'style': 'font-weight:bold; display:block; margin-bottom:5px;' }, _('Command to execute:')),
E('input', {
'id': 'cron_command_input',
'class': 'cbi-input-text',
'style': 'width:100%; margin-bottom:10px; text-align:center;',
'blur': ui.createHandlerFn(this, 'updateCronPreview')
}),
E('div', { 'style': 'margin-top:1em;' }, [
E('label', { 'style': 'font-weight:bold; display:block; margin-bottom:5px;' }, _('Command preview:')),
E('div', {
'id': 'cron_preview',
'style': 'width:100%; margin-bottom:10px; font-family:monospace; border:1px solid var(--border-color-medium); padding:8px; background:var(--tile-free); border-radius:6px; color:var(--tile-text);'
}),
]),
// Desc
E('div', { 'style': 'margin-top:1em;' }, [
E('label', { 'style': 'font-weight:bold; display:block; margin-bottom:5px;' }, _('Description:')),
E('textarea', {
'id': 'cron_human_text',
'class': 'cbi-input-textarea',
'style': 'width:100%; min-height:60px; resize:vertical;',
'readonly': true
})
]),
E('div', { 'style': 'margin-top:1em;' }, [
E('label', { 'style': 'font-weight:bold; display:block; margin-bottom:5px;' }, _('Generated cron command:')),
E('input', {
'id': 'cron_preview_text',
'class': 'cbi-input-text',
'style': 'width:100%; margin-bottom:10px; font-family:monospace;',
'readonly': true,
'data-tooltip': _('Cron command for copying'),
'value': '* * * * * '
})
]),
E('div', { 'class': 'cron-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, 'appendCronLine')
}, _('Add to cron')),
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.createHandlerFn(this, function(){
this.resetMulti('minute_select');
this.resetMulti('hour_select');
this.resetMulti('day_select');
this.resetMulti('month_select');
this.resetMulti('weekday_select');
document.getElementById('cron_command_input').value = '';
const minuteCheckbox = document.getElementById('minute_checkbox');
const hourCheckbox = document.getElementById('hour_checkbox');
const dayCheckbox = document.getElementById('day_checkbox');
if (minuteCheckbox) minuteCheckbox.checked = false;
if (hourCheckbox) hourCheckbox.checked = false;
if (dayCheckbox) dayCheckbox.checked = false;
this.updateCronPreview();
})
}, _('Reset changes'))
])
]);
setTimeout(() => this.updateCronPreview(), 0);
return root;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,200 @@
msgid "Graphical Crontab Configurator"
msgstr "Graficzny konfigurator crontab"
msgid "Crontab Configurator"
msgstr "Konfigurator crontab"
msgid "Minute"
msgstr "Minuta"
msgid "Hour"
msgstr "Godzina"
msgid "Day"
msgstr "Dzień"
msgid "Month"
msgstr "Miesiąc"
msgid "Weekday"
msgstr "Dzień tygodnia"
msgid "Command to execute:"
msgstr "Polecenie do wykonania:"
msgid "Command preview:"
msgstr "Wizualizacja polecenia:"
msgid "Generated cron command:"
msgstr "Wygenerowane polecenie cron:"
msgid "Move to crontab"
msgstr "Przenieś do crontab"
msgid "Reset changes"
msgstr "Resetuj zmiany"
msgid "every specified minute period"
msgstr "co podany okres minut"
msgid "every specified hour period"
msgstr "co podany okres godzin"
msgid "every specified day period"
msgstr "co podany okres dni"
msgid "January"
msgstr "Styczeń"
msgid "February"
msgstr "Luty"
msgid "March"
msgstr "Marzec"
msgid "April"
msgstr "Kwiecień"
msgid "May"
msgstr "Maj"
msgid "June"
msgstr "Czerwiec"
msgid "July"
msgstr "Lipiec"
msgid "August"
msgstr "Sierpień"
msgid "September"
msgstr "Wrzesień"
msgid "October"
msgstr "Październik"
msgid "November"
msgstr "Listopad"
msgid "December"
msgstr "Grudzień"
msgid "Sunday"
msgstr "Niedziela"
msgid "Monday"
msgstr "Poniedziałek"
msgid "Tuesday"
msgstr "Wtorek"
msgid "Wednesday"
msgstr "Środa"
msgid "Thursday"
msgstr "Czwartek"
msgid "Friday"
msgstr "Piątek"
msgid "Saturday"
msgstr "Sobota"
msgid "A super simple graphical configurator for crontab."
msgstr "Bardzo prosty graficzny konfigurator dla crontab."
msgid "Please enter the command"
msgstr "Proszę podać polecenie"
msgid "Check the cron entry, it must contain (time + command)"
msgstr "Sprawdź wpis crona, musi zawierać (czas + polecenie)"
msgid "This entry already exists in cron"
msgstr "Wpis już istnieje w cronie"
msgid "This entry already exists in cron"
msgstr "Wpis już istnieje w cronie"
msgid "Added entry to cron"
msgstr "Dodano wpis do crona"
msgid "Error: %s"
msgstr "Błąd: %s"
msgid "Add to cron"
msgstr "Dodaj do crona"
msgid "Cron command for copying"
msgstr "Polecenie cron do skopiowania"
msgid "minute"
msgstr "minuta"
msgid "minutes"
msgstr "minut(-y)"
msgid "hour"
msgstr "godzina"
msgid "hours"
msgstr "godzin(-y)"
msgid "day"
msgstr "dzień"
msgid "days"
msgstr "dni(-ach)"
msgid "month"
msgstr "miesiąc"
msgid "months"
msgstr "miesiące(-ęcy)"
msgid "Description:"
msgstr "Opis:"
msgid "at time: %s"
msgstr "o godzinie: %s"
msgid "at times: %s"
msgstr "o czasie: %s"
msgid "runs every %d %s"
msgstr "działa z okresem %d %s"
msgid "in minutes: %s"
msgstr "w minutach: %s"
msgid "in hours: %s"
msgstr "w godzinach: %s"
msgid "runs every %d %s of the month"
msgstr "z okresem co %d %s dni w miesiącu"
msgid "on day(s): %s"
msgstr "w dniu(-ach): %s"
msgid "runs every %d month(s)"
msgstr "z okresem co %d miesiąc(-ęcy)"
msgid "in month(s): %s"
msgstr "w miesiącu(-ach): %s"
msgid "runs every %d day of the week"
msgstr "działa każdego %d dnia tygodnia"
msgid "on weekday(s): %s"
msgstr "w dni(-u) tygodnia: %s"
msgid "Command \"%s\" "
msgstr "Polecenie \"%s\" "
msgid "Command"
msgstr "Polecenie"
msgid "Command will run"
msgstr "Polecenie zostanie wykonane"
msgid "Command \"%s\" will run"
msgstr "Polecenie \"%s\" zostanie wykonane"
+1
View File
@@ -0,0 +1 @@
zh_Hans
@@ -0,0 +1,202 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: luci-app-crontab-wizard\n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: luci-app-crontab-wizard/root/usr/share/luci/menu.d/luci-app-crontab-wizard.json
msgid "Scheduled Tasks"
msgstr "计划任务"
msgid "Crontab Configurator"
msgstr "Crontab 配置向导"
#: luci-app-crontab-wizard/htdocs/luci-static/resources/view/system/crontab_wizard.js
msgid "Graphical Crontab Configurator"
msgstr "图形化 Crontab 配置向导"
msgid "A super simple graphical configurator for crontab."
msgstr "一个超级简单的图形化 Crontab 定时任务配置工具。"
msgid "Minute"
msgstr "分钟"
msgid "every specified minute period"
msgstr "每隔指定分钟周期"
msgid "Hour"
msgstr "小时"
msgid "every specified hour period"
msgstr "每隔指定小时周期"
msgid "Day"
msgstr "日期"
msgid "every specified day period"
msgstr "每隔指定天数周期"
msgid "Month"
msgstr "月份"
msgid "Weekday"
msgstr "星期"
msgid "Command to execute:"
msgstr "要执行的命令:"
msgid "Command preview:"
msgstr "命令预览:"
msgid "Description:"
msgstr "规则说明:"
msgid "Generated cron command:"
msgstr "生成的 Cron 命令:"
msgid "Cron command for copying"
msgstr "用于复制的 Cron 命令"
msgid "Add to cron"
msgstr "添加到 Crontab"
msgid "Reset changes"
msgstr "重置更改"
msgid "Please enter the command"
msgstr "请输入命令"
msgid "Check the cron entry, it must contain (time + command)"
msgstr "请检查 Cron 条目,必须包含(时间 + 命令)"
msgid "This entry already exists in cron"
msgstr "此条目已存在于 Crontab 中"
msgid "Added entry to cron"
msgstr "已将条目添加到 Crontab"
msgid "Error: %s"
msgstr "错误:%s"
msgid "Command \"%s\" "
msgstr "命令 \"%s\" "
msgid "Command"
msgstr "命令"
msgid "Command \"%s\" will run"
msgstr "命令 \"%s\" 将运行"
msgid "Command will run"
msgstr "命令将运行"
msgid "at time: %s"
msgstr "时间:%s"
msgid "at times: %s"
msgstr "时间:%s"
msgid "runs every %d %s"
msgstr "每 %d %s运行一次"
msgid "in minutes: %s"
msgstr "在指定分钟:%s"
msgid "in hours: %s"
msgstr "在指定小时:%s"
msgid "runs every %d %s of the month"
msgstr "每月每 %d %s运行一次"
msgid "on day(s): %s"
msgstr "在日期:%s"
msgid "runs every %d month(s)"
msgstr "每 %d 个月运行一次"
msgid "in month(s): %s"
msgstr "在月份:%s"
msgid "runs every %d day of the week"
msgstr "每周每 %d 天运行一次"
msgid "on weekday(s): %s"
msgstr "在星期:%s"
msgid "minute"
msgstr "分钟"
msgid "minutes"
msgstr "分钟"
msgid "hour"
msgstr "小时"
msgid "hours"
msgstr "小时"
msgid "day"
msgstr "天"
msgid "days"
msgstr "天"
msgid "Sunday"
msgstr "星期日"
msgid "Monday"
msgstr "星期一"
msgid "Tuesday"
msgstr "星期二"
msgid "Wednesday"
msgstr "星期三"
msgid "Thursday"
msgstr "星期四"
msgid "Friday"
msgstr "星期五"
msgid "Saturday"
msgstr "星期六"
msgid "January"
msgstr "一月"
msgid "February"
msgstr "二月"
msgid "March"
msgstr "三月"
msgid "April"
msgstr "四月"
msgid "May"
msgstr "五月"
msgid "June"
msgstr "六月"
msgid "July"
msgstr "七月"
msgid "August"
msgstr "八月"
msgid "September"
msgstr "九月"
msgid "October"
msgstr "十月"
msgid "November"
msgstr "十一月"
msgid "December"
msgstr "十二月"
@@ -0,0 +1,37 @@
{
"admin/system/crontab": {
"title": "Scheduled Tasks",
"order": 46,
"action": {
"type": "view",
"path": "system/crontab"
},
"depends": {
"acl": [ "luci-mod-system-cron" ]
}
},
"admin/system/crontab/crontab": {
"title": "Scheduled Tasks",
"order": 46,
"action": {
"type": "view",
"path": "system/crontab"
},
"depends": {
"acl": [ "luci-mod-system-cron" ]
}
},
"admin/system/crontab/wizard": {
"title": "Crontab Configurator",
"order": 47,
"action": {
"type": "view",
"path": "system/crontab_wizard"
},
"depends": {
"acl": [ "luci-app-crontab-wizard" ]
}
}
}
@@ -0,0 +1,22 @@
{
"luci-app-crontab-wizard": {
"description": "Grant access to luci-app-crontab-wizard",
"read": {
"file": {
"/etc/crontabs/root": [ "read" ]
},
"ubus": {
"file": [ "read" ]
}
},
"write": {
"file": {
"/etc/crontabs/root": [ "write" ],
"/etc/init.d/cron reload": [ "exec" ]
},
"ubus": {
"file": [ "write" ]
}
}
}
}