diff --git a/luci-app-openlist/Makefile b/luci-app-openlist/Makefile
new file mode 100644
index 0000000..181bccc
--- /dev/null
+++ b/luci-app-openlist/Makefile
@@ -0,0 +1,17 @@
+# Copyright (C) 2016 Openwrt.org
+#
+# This is free software, licensed under the Apache License, Version 2.0 .
+#
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=luci-app-openlist
+PKG_VERSION:=1.0.0
+PKG_RELEASE:=1
+
+LUCI_TITLE:=LuCI support for openlist
+LUCI_DEPENDS:=+openlist
+
+include $(TOPDIR)/feeds/luci/luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/luci-app-openlist/htdocs/luci-static/resources/view/openlist/basic.js b/luci-app-openlist/htdocs/luci-static/resources/view/openlist/basic.js
new file mode 100644
index 0000000..401f3c3
--- /dev/null
+++ b/luci-app-openlist/htdocs/luci-static/resources/view/openlist/basic.js
@@ -0,0 +1,384 @@
+'use strict';
+'require form';
+'require fs';
+'require poll';
+'require rpc';
+'require uci';
+'require view';
+
+var callServiceList = rpc.declare({
+ object: 'service',
+ method: 'list',
+ params: ['name'],
+ expect: { '': {} }
+});
+
+function getServiceStatus() {
+ return L.resolveDefault(callServiceList('openlist'), {}).then(function (res) {
+ var isRunning = false;
+ try {
+ isRunning = res['openlist']['instances']['openlist']['running'];
+ } catch (e) { }
+ return isRunning;
+ });
+}
+
+function renderStatus(isRunning, protocol, webport) {
+ var spanTemp = '%s %s';
+ var renderHTML;
+ if (isRunning) {
+ var button = String.format('',
+ _('Open Web Interface'), protocol, window.location.hostname, webport);
+ renderHTML = spanTemp.format('green', 'OpenList', _('RUNNING')) + button;
+ } else {
+ renderHTML = spanTemp.format('red', 'OpenList', _('NOT RUNNING'));
+ }
+
+ return renderHTML;
+}
+
+return view.extend({
+ load: function () {
+ return Promise.all([
+ uci.load('openlist')
+ ]);
+ },
+
+ handleResetPassword: async function (data) {
+ var data_dir = uci.get(data[0], '@openlist[0]', 'data_dir') || '/etc/openlist';
+ try {
+ var newpassword = await fs.exec('/usr/bin/openlist', ['admin', 'random', '--data', data_dir]);
+ var new_password = newpassword.stderr.match(/password:\s*(\S+)/)[1];
+ const textArea = document.createElement('textarea');
+ textArea.value = new_password;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ alert(_('Username:') + 'admin\n' + _('New Password:') + new_password + '\n\n' + _('New password has been copied to clipboard.'));
+ } catch (error) {
+ console.error('Failed to reset password: ', error);
+ }
+ },
+
+ render: function (data) {
+ var m, s, o;
+ var webport = uci.get(data[0], '@openlist[0]', 'port') || '5244';
+ var ssl = uci.get(data[0], '@openlist[0]', 'ssl') || '0';
+ var protocol;
+ if (ssl === '0') {
+ protocol = 'http:';
+ } else if (ssl === '1') {
+ protocol = 'https:';
+ }
+
+ m = new form.Map('openlist', _('OpenList'),
+ _('A file list program that supports multiple storage.'));
+
+ s = m.section(form.TypedSection);
+ s.anonymous = true;
+ s.addremove = false;
+
+ s.render = function () {
+ poll.add(function () {
+ return L.resolveDefault(getServiceStatus()).then(function (res) {
+ var view = document.getElementById('service_status');
+ view.innerHTML = renderStatus(res, protocol, webport);
+ });
+ });
+
+ return E('div', { class: 'cbi-section', id: 'status_bar' }, [
+ E('p', { id: 'service_status' }, _('Collecting data...'))
+ ]);
+ }
+
+ s = m.section(form.NamedSection, '@openlist[0]', 'openlist');
+
+ s.tab('basic', _('Basic Settings'));
+ s.tab('global', _('Global Settings'));
+ s.tab("log", _("Logs"));
+ s.tab("database", _("Database"));
+ s.tab("scheme", _("Web Protocol"));
+ s.tab('tasks', _('Task threads'));
+ s.tab('cors', _('CORS Settings'));
+ s.tab('s3', _('Object Storage'));
+ s.tab('ftp', _('FTP'));
+ s.tab('sftp', _('SFTP'));
+
+ // init
+ o = s.taboption('basic', form.Flag, 'enabled', _('Enabled'));
+ o.default = o.disabled;
+ o.rmempty = false;
+
+ o = s.taboption('basic', form.Value, 'port', _('Port'));
+ o.datatype = 'and(port,min(1))';
+ o.default = '5244';
+ o.rmempty = false;
+
+ o = s.taboption('basic', form.Value, 'delayed_start', _('Delayed Start (seconds)'));
+ o.datatype = 'uinteger';
+ o.default = '0';
+ o.rmempty = false;
+
+ o = s.taboption('basic', form.Flag, 'allow_wan', _('Open firewall port'));
+ o.rmempty = false;
+
+ o = s.taboption('basic', form.Value, 'data_dir', _('Data directory'));
+ o.default = '/etc/openlist';
+
+ o = s.taboption('basic', form.Value, 'temp_dir', _('Cache directory'));
+ o.default = '/tmp/openlist';
+ o.rmempty = false;
+
+ o = s.taboption('basic', form.Button, '_newpassword', _('Reset Password'),
+ _('Generate a new random password.'));
+ o.inputtitle = _('Reset Password');
+ o.inputstyle = 'apply';
+ o.onclick = L.bind(this.handleResetPassword, this, data);
+
+ // global
+ o = s.taboption('global', form.Flag, 'force', _('Force read config'),
+ _('Setting this to true will force the program to read the configuration file, ignoring environment variables.'));
+ o.default = true;
+ o.rmempty = false;
+
+ o = s.taboption('global', form.Value, 'site_url', _('Site URL'),
+ _('When the web is reverse proxied to a subdirectory, this option must be filled out to ensure proper functioning of the web. Do not include \'/\' at the end of the URL'));
+
+ o = s.taboption('global', form.Value, 'cdn', _('CDN URL'));
+ o.default = '';
+
+ o = s.taboption('global', form.Value, 'jwt_secret', _('JWT Key'));
+ o.default = '';
+
+ o = s.taboption('global', form.Value, 'token_expires_in', _('Login Validity Period (hours)'));
+ o.datatype = 'uinteger';
+ o.default = '48';
+ o.rmempty = false;
+
+ o = s.taboption('global', form.Value, 'max_connections', _('Max Connections'),
+ _('0 is unlimited, It is recommend to set a low number of concurrency (10-20) for poor performance device'));
+ o.default = '0';
+ o.datatype = 'uinteger';
+ o.rmempty = false;
+
+ o = s.taboption('global', form.Value, 'max_concurrency', _('Max concurrency of local proxies'),
+ _('0 is unlimited, Limit the maximum concurrency of local agents. The default value is 64'));
+ o.default = '0';
+ o.datatype = 'uinteger';
+ o.rmempty = false;
+
+ o = s.taboption('global', form.Flag, 'tls_insecure_skip_verify', _('Disable TLS Verify'));
+ o.default = true;
+ o.rmempty = false;
+
+ // Logs
+ o = s.taboption('log', form.Flag, 'log', _('Enable Logs'));
+ o.default = 1;
+ o.rmempty = false;
+
+ o = s.taboption('log', form.Value, 'log_path', _('Log path'));
+ o.default = '/var/log/openlist.log';
+ o.rmempty = false;
+ o.depends('log', '1');
+
+ o = s.taboption('log', form.Value, 'log_max_size', _('Max Size (MB)'));
+ o.datatype = 'uinteger';
+ o.default = '10';
+ o.rmempty = false;
+ o.depends('log', '1');
+
+ o = s.taboption('log', form.Value, 'log_max_backups', _('Max backups'));
+ o.datatype = 'uinteger';
+ o.default = '5';
+ o.rmempty = false;
+ o.depends('log', '1');
+
+ o = s.taboption('log', form.Value, 'log_max_age', _('Max age'));
+ o.datatype = 'uinteger';
+ o.default = '28';
+ o.rmempty = false;
+ o.depends('log', '1');
+
+ o = s.taboption('log', form.Flag, 'log_compress', _('Log Compress'));
+ o.default = 'false';
+ o.rmempty = false;
+ o.depends('log', '1');
+
+ // database
+ o = s.taboption('database', form.ListValue, 'database_type', _('Database Type'));
+ o.default = 'sqlite3';
+ o.value('sqlite3', _('SQLite'));
+ o.value('mysql', _('MySQL'));
+ o.value('postgres', _('PostgreSQL'));
+
+ o = s.taboption('database', form.Value, 'mysql_host', _('Database Host'));
+ o.depends('database_type','mysql');
+ o.depends('database_type','postgres');
+
+ o = s.taboption('database', form.Value, 'mysql_port', _('Database Port'));
+ o.datatype = 'port';
+ o.default = '3306';
+ o.depends('database_type','mysql');
+ o.depends('database_type','postgres');
+
+ o = s.taboption('database', form.Value, 'mysql_username', _('Database Username'));
+ o.depends('database_type','mysql');
+ o.depends('database_type','postgres');
+
+ o = s.taboption('database', form.Value, 'mysql_password', _('Database Password'));
+ o.depends('database_type','mysql');
+ o.depends('database_type','postgres');
+
+ o = s.taboption('database', form.Value, 'mysql_database', _('Database Name'));
+ o.depends('database_type','mysql');
+ o.depends('database_type','postgres');
+
+ o = s.taboption('database', form.Value, 'mysql_table_prefix', _('Database Table Prefix'));
+ o.default = 'x_';
+ o.depends('database_type','mysql');
+ o.depends('database_type','postgres');
+
+ o = s.taboption('database', form.Value, 'mysql_ssl_mode', _('Database SSL Mode'));
+ o.depends('database_type','mysql');
+ o.depends('database_type','postgres');
+
+ o = s.taboption('database', form.Value, 'mysql_dsn', _('Database DSN'));
+ o.depends('database_type','mysql');
+ o.depends('database_type','postgres');
+
+ // scheme
+ o = s.taboption('scheme', form.Flag, 'ssl', _('Enable SSL'));
+ o.rmempty = false;
+
+ o = s.taboption('scheme', form.Flag, 'force_https', _('Force HTTPS'));
+ o.rmempty = false;
+ o.depends('ssl', '1');
+
+ o = s.taboption('scheme', form.Value, 'ssl_cert', _('SSL cert'),
+ _('SSL certificate file path'));
+ o.rmempty = false;
+ o.depends('ssl', '1');
+
+ o = s.taboption('scheme', form.Value, 'ssl_key', _('SSL key'),
+ _('SSL key file path'));
+ o.rmempty = false;
+ o.depends('ssl', '1');
+
+ // tasks
+ o = s.taboption('tasks', form.Value, 'download_workers', _('Download Workers'));
+ o.datatype = 'uinteger';
+ o.default = '5';
+ o.rmempty = false;
+
+ o = s.taboption('tasks', form.Value, 'download_max_retry', _('Download Max Retry'));
+ o.datatype = 'uinteger';
+ o.default = '1';
+ o.rmempty = false;
+
+ o = s.taboption('tasks', form.Value, 'transfer_workers', _('Transfer Workers'));
+ o.datatype = 'uinteger';
+ o.default = '5';
+ o.rmempty = false;
+
+ o = s.taboption('tasks', form.Value, 'transfer_max_retry', _('Transfer Max Retry'));
+ o.datatype = 'uinteger';
+ o.default = '2';
+ o.rmempty = false;
+
+ o = s.taboption('tasks', form.Value, 'upload_workers', _('Upload Workers'));
+ o.datatype = 'uinteger';
+ o.default = '5';
+ o.rmempty = false;
+
+ o = s.taboption('tasks', form.Value, 'upload_max_retry', _('Upload Max Retry'));
+ o.datatype = 'uinteger';
+ o.default = '0';
+ o.rmempty = false;
+
+ o = s.taboption('tasks', form.Value, 'copy_workers', _('Copy Workers'));
+ o.datatype = 'uinteger';
+ o.default = '5';
+ o.rmempty = false;
+
+ o = s.taboption('tasks', form.Value, 'copy_max_retry', _('Copy Max Retry'));
+ o.datatype = 'uinteger';
+ o.default = '2';
+ o.rmempty = false;
+
+ // cors
+ o = s.taboption('cors', form.Value, 'cors_allow_origins', _('Allow Origins'));
+ o.default = '*';
+ o.rmempty = false;
+
+ o = s.taboption('cors', form.Value, 'cors_allow_methods', _('Allow Methods'));
+ o.default = '*';
+ o.rmempty = false;
+
+ o = s.taboption('cors', form.Value, 'cors_allow_headers', _('Allow Headers'));
+ o.default = '*';
+ o.rmempty = false;
+
+ // s3
+ o = s.taboption('s3', form.Flag, 's3', _('Enabled S3'));
+ o.rmempty = false;
+
+ o = s.taboption('s3', form.Value, 's3_port', _('Port'));
+ o.datatype = 'and(port,min(1))';
+ o.default = 5246;
+ o.rmempty = false;
+
+ o = s.taboption('s3', form.Flag, 's3_ssl', _('Enable SSL'));
+ o.rmempty = false;
+
+ // ftp
+ o = s.taboption('ftp', form.Flag, 'ftp', _('Enabled FTP'));
+ o.rmempty = false;
+
+ o = s.taboption('ftp', form.Value, 'ftp_port', _('FTP Port'));
+ o.datatype = 'and(port,min(1))';
+ o.default = 5221;
+ o.rmempty = false;
+
+ o = s.taboption('ftp', form.Value, 'find_pasv_port_attempts', _('Max retries on port conflict during passive transfer'));
+ o.datatype = 'uinteger';
+ o.default = '50';
+ o.rmempty = false;
+
+ o = s.taboption('ftp', form.Flag, 'active_transfer_port_non_20', _('Enable non-20 port for active transfer'));
+ o.rmempty = false;
+
+ o = s.taboption('ftp', form.Value, 'idle_timeout', _('Client idle timeout (seconds)'));
+ o.datatype = 'uinteger';
+ o.default = '900';
+ o.rmempty = false;
+
+ o = s.taboption('ftp', form.Value, 'connection_timeout', _('Connection timeout (seconds)'));
+ o.datatype = 'uinteger';
+ o.default = '900';
+ o.rmempty = false;
+
+ o = s.taboption('ftp', form.Flag, 'disable_active_mode', _('Disable active transfer mode'));
+ o.rmempty = false;
+
+ o = s.taboption('ftp', form.Flag, 'default_transfer_binary', _('Enable binary transfer mode'));
+ o.rmempty = false;
+
+ o = s.taboption('ftp', form.Flag, 'enable_active_conn_ip_check', _('Client IP check in active transfer mode'));
+ o.rmempty = false;
+
+ o = s.taboption('ftp', form.Flag, 'enable_pasv_conn_ip_check', _('Client IP check in passive transfer mode'));
+ o.rmempty = false;
+
+ // sftp
+ o = s.taboption('sftp', form.Flag, 'sftp', _('Enabled SFTP'));
+ o.rmempty = false;
+
+ o = s.taboption('sftp', form.Value, 'sftp_port', _('SFTP Port'));
+ o.datatype = 'and(port,min(1))';
+ o.default = 5222;
+ o.rmempty = false;
+
+ return m.render();
+ }
+});
diff --git a/luci-app-openlist/htdocs/luci-static/resources/view/openlist/logs.js b/luci-app-openlist/htdocs/luci-static/resources/view/openlist/logs.js
new file mode 100644
index 0000000..3895258
--- /dev/null
+++ b/luci-app-openlist/htdocs/luci-static/resources/view/openlist/logs.js
@@ -0,0 +1,77 @@
+'use strict';
+'require dom';
+'require fs';
+'require poll';
+'require uci';
+'require view';
+
+var scrollPosition = 0;
+var userScrolled = false;
+var logTextarea;
+var log_path;
+
+uci.load('openlist').then(function() {
+ log_path = uci.get('openlist', '@openlist[0]', 'log_path') || '/var/log/openlist.log';
+});
+
+function pollLog() {
+ return Promise.all([
+ fs.read_direct(log_path, 'text').then(function (res) {
+ return res.trim().split(/\n/).join('\n').replace(/\u001b\[33mWARN\u001b\[0m/g, '').replace(/\u001b\[36mINFO\u001b\[0m/g, '').replace(/\u001b\[31mERRO\u001b\[0m/g, '');
+ }),
+ ]).then(function (data) {
+ logTextarea.value = data[0] || _('No log data.');
+
+ if (!userScrolled) {
+ logTextarea.scrollTop = logTextarea.scrollHeight;
+ } else {
+ logTextarea.scrollTop = scrollPosition;
+ }
+ });
+};
+
+return view.extend({
+ handleCleanLogs: function () {
+ return fs.write(log_path, '')
+ .catch(function (e) { ui.addNotification(null, E('p', e.message)) });
+ },
+
+ render: function () {
+ logTextarea = E('textarea', {
+ 'class': 'cbi-input-textarea',
+ 'wrap': 'off',
+ 'readonly': 'readonly',
+ 'style': 'width: calc(100% - 20px);height: 535px;margin: 10px;overflow-y: scroll;',
+ });
+
+ logTextarea.addEventListener('scroll', function () {
+ userScrolled = true;
+ scrollPosition = logTextarea.scrollTop;
+ });
+
+ var log_textarea_wrapper = E('div', { 'id': 'log_textarea' }, logTextarea);
+
+ setTimeout(function () {
+ poll.add(pollLog);
+ }, 100);
+
+ var clear_logs_button = E('input', { 'class': 'btn cbi-button-action', 'type': 'button', 'style': 'margin-left: 10px; margin-top: 10px;', 'value': _('Clear logs') });
+ clear_logs_button.addEventListener('click', this.handleCleanLogs.bind(this));
+
+ return E([
+ E('div', { 'class': 'cbi-map' }, [
+ E('div', { 'class': 'cbi-section' }, [
+ clear_logs_button,
+ log_textarea_wrapper,
+ E('div', { 'style': 'text-align:right' },
+ E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
+ )
+ ])
+ ])
+ ]);
+ },
+
+ handleSave: null,
+ handleSaveApply: null,
+ handleReset: null
+});
diff --git a/luci-app-openlist/po/zh_Hans/openlist.po b/luci-app-openlist/po/zh_Hans/openlist.po
new file mode 100644
index 0000000..e03487d
--- /dev/null
+++ b/luci-app-openlist/po/zh_Hans/openlist.po
@@ -0,0 +1,267 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Project-Id-Version: PACKAGE VERSION\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: zh_Hans\n"
+"MIME-Version: 1.0\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "OpenList"
+msgstr "OpenList"
+
+msgid "Open Web Interface"
+msgstr "打开 Web 界面"
+
+msgid "A file list program that supports multiple storage."
+msgstr "一款支持多种存储的目录文件列表程序。"
+
+msgid "Setting"
+msgstr "设置"
+
+msgid "Basic Setting"
+msgstr "基本设置"
+
+msgid "Global Settings"
+msgstr "全局设置"
+
+msgid "Logs"
+msgstr "日志"
+
+msgid "Enabled"
+msgstr "启用"
+
+msgid "Port"
+msgstr "端口"
+
+msgid "Web Protocol"
+msgstr "Web 协议"
+
+msgid "Enable SSL"
+msgstr "启用 SSL"
+
+msgid "Force HTTPS"
+msgstr "强制 HTTPS"
+
+msgid "SSL cert"
+msgstr "SSL 证书"
+
+msgid "SSL certificate file path"
+msgstr "SSL 证书文件路径"
+
+msgid "SSL key"
+msgstr "SSL 密钥"
+
+msgid "SSL key file path"
+msgstr "SSL 密钥文件路径"
+
+msgid "Data directory"
+msgstr "数据目录"
+
+msgid "Cache directory"
+msgstr "缓存目录"
+
+msgid "RUNNING"
+msgstr "运行中"
+
+msgid "NOT RUNNING"
+msgstr "未运行"
+
+msgid "Collecting data..."
+msgstr "收集数据..."
+
+msgid "NAS"
+msgstr "网络存储"
+
+msgid "User Manual"
+msgstr "用户手册"
+
+msgid "Open firewall port"
+msgstr "打开防火墙端口"
+
+msgid "Enable Logs"
+msgstr "启用日志"
+
+msgid "Log path"
+msgstr "日志文件路径"
+
+msgid "Max Size (MB)"
+msgstr "日志文件大小(MB)"
+
+msgid "Max backups"
+msgstr "日志备份数量"
+
+msgid "Max age"
+msgstr "日志保存天数"
+
+msgid "Log Compress"
+msgstr "日志压缩"
+
+msgid "Clear logs"
+msgstr "清空日志"
+
+msgid "Reset Password"
+msgstr "重置密码"
+
+msgid "Generate a new random password."
+msgstr "随机生成一个新密码。"
+
+msgid "Username:"
+msgstr "用户名:"
+
+msgid "New Password:"
+msgstr "新密码:"
+
+msgid "New password has been copied to clipboard."
+msgstr "新密码已复制到剪贴板。"
+
+msgid "Force read config"
+msgstr "强制读取配置"
+
+msgid "Setting this to true will force the program to read the configuration file, ignoring environment variables."
+msgstr "将此设置为 true 可以强制程序读取配置文件,而忽略环境变量"
+
+msgid "Login Validity Period (hours)"
+msgstr "登录有效期(小时)"
+
+msgid "Max Connections"
+msgstr "最大并发连接数"
+
+msgid "0 is unlimited, It is recommend to set a low number of concurrency (10-20) for poor performance device"
+msgstr "默认 0 不限制,低性能设备建议设置较低的并发数(10-20)"
+
+msgid "Max concurrency of local proxies"
+msgstr "本地代理最大并发数"
+
+msgid "0 is unlimited, Limit the maximum concurrency of local agents. The default value is 64"
+msgstr "限制本地代理最大并发数,值为 0 表示不限制,默认值 64"
+
+msgid "Site URL"
+msgstr "站点地址"
+
+msgid "CDN URL"
+msgstr "CDN 地址"
+
+msgid "JWT Key"
+msgstr "JWT 密钥"
+
+msgid "Disable TLS Verify"
+msgstr "禁用 TLS 验证"
+
+msgid "When the web is reverse proxied to a subdirectory, this option must be filled out to ensure proper functioning of the web. Do not include '/' at the end of the URL"
+msgstr "Web 被反向代理到二级目录时,必须填写该选项以确保 Web 正常工作。URL 结尾请勿携带 '/'"
+
+msgid "Delayed Start (seconds)"
+msgstr "开机延时启动(秒)"
+
+msgid "Database"
+msgstr "数据库"
+
+msgid "Database Type"
+msgstr "数据库类型"
+
+msgid "Database Host"
+msgstr "主机"
+
+msgid "Database Port"
+msgstr "端口"
+
+msgid "Database Username"
+msgstr "用户名"
+
+msgid "Database Password"
+msgstr "密码"
+
+msgid "Database Name"
+msgstr "数据库名"
+
+msgid "Database Table Prefix"
+msgstr "数据库表前缀"
+
+msgid "Database SSL Mode"
+msgstr "SSL模式"
+
+msgid "Database DSN"
+msgstr "DSN"
+
+msgid "Task threads"
+msgstr "任务线程"
+
+msgid "Download Workers"
+msgstr "下载任务线程数"
+
+msgid "Download Max Retry"
+msgstr "下载任务重试次数"
+
+msgid "Transfer Workers"
+msgstr "中转任务线程数"
+
+msgid "Transfer Max Retry"
+msgstr "中转任务下载重试次数"
+
+msgid "Upload Workers"
+msgstr "上传任务线程数"
+
+msgid "Upload Max Retry"
+msgstr "上传任务重试次数"
+
+msgid "Copy Workers"
+msgstr "复制任务线程数"
+
+msgid "Copy Max Retry"
+msgstr "复制任务重试次数"
+
+msgid "CORS Settings"
+msgstr "跨域设置"
+
+msgid "Allow Origins"
+msgstr "允许来源(Origins)"
+
+msgid "Allow Methods"
+msgstr "允许方法(Methods)"
+
+msgid "Allow Headers"
+msgstr "允许标头(Headers)"
+
+msgid "Object Storage"
+msgstr "对象存储"
+
+msgid "Enabled S3"
+msgstr "启用 S3"
+
+msgid "Enabled FTP"
+msgstr "启用 FTP"
+
+msgid "FTP Port"
+msgstr "FTP 端口"
+
+msgid "Max retries on port conflict during passive transfer"
+msgstr "被动传输端口冲突时最大重试次数"
+
+msgid "Enable non-20 port for active transfer"
+msgstr "启用非 20 端口进行主动传输"
+
+msgid "Client idle timeout (seconds)"
+msgstr "客户端空闲超时(秒)"
+
+msgid "Connection timeout (seconds)"
+msgstr "连接超时(秒)"
+
+msgid "Disable active transfer mode"
+msgstr "禁用主动传输模式"
+
+msgid "Enable binary transfer mode"
+msgstr "启用二进制传输模式"
+
+msgid "Client IP check in active transfer mode"
+msgstr "主动传输模式下对客户端进行 IP 检查"
+
+msgid "Client IP check in passive transfer mode"
+msgstr "被动传输模式下对客户端进行 IP 检查"
+
+msgid "Enabled SFTP"
+msgstr "启用 SFTP"
+
+msgid "SFTP Port"
+msgstr "SFTP 端口"
diff --git a/luci-app-openlist/root/etc/uci-defaults/luci-app-openlist b/luci-app-openlist/root/etc/uci-defaults/luci-app-openlist
new file mode 100755
index 0000000..73bd43f
--- /dev/null
+++ b/luci-app-openlist/root/etc/uci-defaults/luci-app-openlist
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+[ -f "/etc/config/ucitrack" ] && {
+uci -q batch <<-EOF >/dev/null
+ delete ucitrack.@openlist[-1]
+ add ucitrack openlist
+ set ucitrack.@openlist[-1].init=openlist
+ commit ucitrack
+EOF
+}
+
+rm -rf /tmp/luci-*
+exit 0
diff --git a/luci-app-openlist/root/usr/share/luci/menu.d/luci-app-openlist.json b/luci-app-openlist/root/usr/share/luci/menu.d/luci-app-openlist.json
new file mode 100644
index 0000000..63c3ef5
--- /dev/null
+++ b/luci-app-openlist/root/usr/share/luci/menu.d/luci-app-openlist.json
@@ -0,0 +1,28 @@
+{
+ "admin/services/openlist": {
+ "title": "OpenList",
+ "action": {
+ "type": "firstchild"
+ },
+ "depends": {
+ "acl": [ "luci-app-openlist" ],
+ "uci": { "openlist": true }
+ }
+ },
+ "admin/services/openlist/basic": {
+ "title": "Setting",
+ "order": 10,
+ "action": {
+ "type": "view",
+ "path": "openlist/basic"
+ }
+ },
+ "admin/services/openlist/logs": {
+ "title": "Logs",
+ "order": 20,
+ "action": {
+ "type": "view",
+ "path": "openlist/logs"
+ }
+ }
+}
diff --git a/luci-app-openlist/root/usr/share/rpcd/acl.d/luci-app-openlist.json b/luci-app-openlist/root/usr/share/rpcd/acl.d/luci-app-openlist.json
new file mode 100644
index 0000000..f024065
--- /dev/null
+++ b/luci-app-openlist/root/usr/share/rpcd/acl.d/luci-app-openlist.json
@@ -0,0 +1,21 @@
+{
+ "luci-app-openlist": {
+ "description": "Grant UCI access for luci-app-openlist",
+ "read": {
+ "file": {
+ "/usr/bin/openlist": [ "exec" ],
+ "/*": [ "read" ]
+ },
+ "ubus": {
+ "service": [ "list" ]
+ },
+ "uci": [ "openlist" ]
+ },
+ "write": {
+ "file": {
+ "/*": [ "write" ]
+ },
+ "uci": [ "openlist" ]
+ }
+ }
+}
diff --git a/luci-app-openlist/root/usr/share/ucitrack/luci-app-openlist.json b/luci-app-openlist/root/usr/share/ucitrack/luci-app-openlist.json
new file mode 100644
index 0000000..1dda9de
--- /dev/null
+++ b/luci-app-openlist/root/usr/share/ucitrack/luci-app-openlist.json
@@ -0,0 +1,4 @@
+{
+ "config": "openlist",
+ "init": "openlist"
+}