revamped settings frontend

This commit is contained in:
huangjx
2022-07-29 21:09:08 +08:00
parent 9075c8fb50
commit e85b8b87ba
15 changed files with 648 additions and 337 deletions

View File

@@ -9,7 +9,8 @@ use OCP\IDBConnection;
use OCP\Settings\ISettings; use OCP\Settings\ISettings;
use OCA\NCDownloader\Db\Settings; use OCA\NCDownloader\Db\Settings;
class Admin implements ISettings { class Admin implements ISettings
{
/** @var IDBConnection */ /** @var IDBConnection */
private $connection; private $connection;
@@ -18,9 +19,11 @@ class Admin implements ISettings {
/** @var IConfig */ /** @var IConfig */
private $config; private $config;
public function __construct(IDBConnection $connection, public function __construct(
IDBConnection $connection,
ITimeFactory $timeFactory, ITimeFactory $timeFactory,
IConfig $config) { IConfig $config
) {
$this->connection = $connection; $this->connection = $connection;
$this->timeFactory = $timeFactory; $this->timeFactory = $timeFactory;
$this->config = $config; $this->config = $config;
@@ -31,13 +34,18 @@ class Admin implements ISettings {
/** /**
* @return TemplateResponse * @return TemplateResponse
*/ */
public function getForm() { public function getForm()
{
$this->settings->setType($this->settings::TYPE['SYSTEM']); $this->settings->setType($this->settings::TYPE['SYSTEM']);
$parameters = [ $parameters = [
'settings' => [
"path" => "/apps/ncdownloader/admin/save", "path" => "/apps/ncdownloader/admin/save",
"ncd_yt_binary" => $this->settings->get("ncd_yt_binary"), "ncd_yt_binary" => $this->settings->get("ncd_yt_binary"),
"ncd_aria2_binary" => $this->settings->get("ncd_aria2_binary"), "ncd_aria2_binary" => $this->settings->get("ncd_aria2_binary"),
"ncd_rpctoken" => $this->settings->get("ncd_rpctoken"), "ncd_rpctoken" => $this->settings->get("ncd_rpctoken"),
"ncd_aria2_rpc_host" => $this->settings->get("ncd_aria2_rpc_host"),
"ncd_aria2_rpc_port" => $this->settings->get("ncd_aria2_rpc_port"),
]
]; ];
return new TemplateResponse('ncdownloader', 'settings/Admin', $parameters, ''); return new TemplateResponse('ncdownloader', 'settings/Admin', $parameters, '');
} }
@@ -45,7 +53,8 @@ class Admin implements ISettings {
/** /**
* @return string the section ID, e.g. 'sharing' * @return string the section ID, e.g. 'sharing'
*/ */
public function getSection(): string { public function getSection(): string
{
return 'ncdownloader'; return 'ncdownloader';
} }
@@ -56,7 +65,8 @@ class Admin implements ISettings {
* *
* E.g.: 70 * E.g.: 70
*/ */
public function getPriority(): int { public function getPriority(): int
{
return 0; return 0;
} }
} }

View File

@@ -10,7 +10,8 @@ use OCP\Settings\ISettings;
use OCA\NCDownloader\Db\Settings; use OCA\NCDownloader\Db\Settings;
use OCA\NCDownloader\Tools\Helper; use OCA\NCDownloader\Tools\Helper;
class Personal implements ISettings { class Personal implements ISettings
{
/** @var IDBConnection */ /** @var IDBConnection */
private $connection; private $connection;
@@ -19,9 +20,11 @@ class Personal implements ISettings {
/** @var IConfig */ /** @var IConfig */
private $config; private $config;
public function __construct(IDBConnection $connection, public function __construct(
IDBConnection $connection,
ITimeFactory $timeFactory, ITimeFactory $timeFactory,
IConfig $config) { IConfig $config
) {
$this->connection = $connection; $this->connection = $connection;
$this->timeFactory = $timeFactory; $this->timeFactory = $timeFactory;
$this->config = $config; $this->config = $config;
@@ -32,14 +35,17 @@ class Personal implements ISettings {
/** /**
* @return TemplateResponse * @return TemplateResponse
*/ */
public function getForm() { public function getForm()
{
$parameters = [ $parameters = [
"settings" => [
"ncd_downloader_dir" => Helper::getDownloadDir(), "ncd_downloader_dir" => Helper::getDownloadDir(),
"ncd_torrents_dir" => $this->settings->get("ncd_torrents_dir"), "ncd_torrents_dir" => $this->settings->get("ncd_torrents_dir"),
"ncd_seed_ratio" => $this->settings->get("ncd_seed_ratio"), "ncd_seed_ratio" => $this->settings->get("ncd_seed_ratio"),
'ncd_seed_time_unit' => $this->settings->get("ncd_seed_time_unit"), 'ncd_seed_time_unit' => $this->settings->get("ncd_seed_time_unit"),
'ncd_seed_time' => $this->settings->get("ncd_seed_time"), 'ncd_seed_time' => $this->settings->get("ncd_seed_time"),
"path" => '/apps/ncdownloader/personal/save', "path" => '/apps/ncdownloader/personal/save',
]
]; ];
//\OC_Util::addScript($this->appName, 'common'); //\OC_Util::addScript($this->appName, 'common');
@@ -51,7 +57,8 @@ class Personal implements ISettings {
/** /**
* @return string the section ID, e.g. 'sharing' * @return string the section ID, e.g. 'sharing'
*/ */
public function getSection(): string { public function getSection(): string
{
return 'ncdownloader'; return 'ncdownloader';
} }
@@ -62,7 +69,8 @@ class Personal implements ISettings {
* *
* E.g.: 70 * E.g.: 70
*/ */
public function getPriority(): int { public function getPriority(): int
{
return 100; return 100;
} }
} }

View File

@@ -34,7 +34,8 @@
"dependencies": { "dependencies": {
"chalk": "^5.0.0", "chalk": "^5.0.0",
"toastr": "^2.1.4", "toastr": "^2.1.4",
"v-tooltip": "^4.0.0-alpha.1" "v-tooltip": "^4.0.0-alpha.1",
"vuex": "^4.0.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0", "node": ">=14.0.0",
@@ -52,6 +53,7 @@
"css-loader": "^6.4.0", "css-loader": "^6.4.0",
"html-webpack-plugin": "^5.3.2", "html-webpack-plugin": "^5.3.2",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"mini-css-extract-plugin": "^2.6.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"sass": "^1.38.0", "sass": "^1.38.0",
"sass-loader": "^10.2.0", "sass-loader": "^10.2.0",
@@ -68,7 +70,6 @@
"vue-loader": "^16.0.0-beta.10", "vue-loader": "^16.0.0-beta.10",
"vue-svg-loader": "^0.17.0-beta.2", "vue-svg-loader": "^0.17.0-beta.2",
"webpack": "^5.69.0", "webpack": "^5.69.0",
"webpack-cli": "^4.9.0", "webpack-cli": "^4.9.0"
"mini-css-extract-plugin": "^2.6.0"
} }
} }

115
src/adminSettings.vue Normal file
View File

@@ -0,0 +1,115 @@
<template>
<div class="section">
<settingsRow
v-for="(option, key) in optionRows"
v-bind:key="key"
:value="option.value"
:id="option.id"
:label="option.label"
:placeholder="option.placeholder"
:path="option.path"
/>
</div>
<customOptions
name="admin-aria2-settings"
@mounted="render"
title="Global Aria2 Settings"
path="/apps/ncdownloader/admin/aria2/save"
:validOptions="validOptions"
>
<template #save>Save Settings</template>
</customOptions>
</template>
<script>
import customOptions from "./components/customOptions";
import helper from "./utils/helper";
import aria2Options from "./utils/aria2Options";
import settingsRow from "./components/settingsRow";
export default {
name: "adminSettings",
data() {
return {
options: [],
validOptions: aria2Options,
};
},
components: {
customOptions,
settingsRow,
},
methods: {
render(event, $vm) {
helper
.httpClient(helper.generateUrl("/apps/ncdownloader/admin/aria2/get"))
.setMethod("GET")
.setHandler((data) => {
if (!data) {
return;
}
let input = [];
for (let key in data) {
if (aria2Options.includes(key))
input.push({ name: key, value: data[key], id: key });
}
//settingsForm.getInstance($vm.container).render(input);
$vm.options = input;
})
.send();
},
},
computed: {
optionRows() {
return this.options;
},
},
mounted() {
try {
let data = this.$el.parentElement.getAttribute("data-settings");
data = JSON.parse(data);
let path = "/apps/ncdownloader/admin/save";
this.options = [
{
label: "Aria2 RPC Host",
id: "ncd_aria2_rpc_host",
value: data.ncd_aria2_rpc_host,
placeholder: "127.0.0.1",
path: path,
},
{
label: "Aria2 RPC Port",
id: "ncd_aria2_rpc_port",
value: data.ncd_aria2_rpc_port,
placeholder: "6800",
path: path,
},
{
label: "Aria2 RPC Token",
id: "ncd_aria2_rpc_token",
value: data.ncd_aria2_rpc_token,
placeholder: data.ncd_aria2_rpc_token ? data.ncd_aria2_rpc_token : "ncdownloader123",
path: path,
},
{
label: "Youtube-dl binary",
id: "ncd_yt_binary",
value: data.ncd_yt_binary,
placeholder: data.ncd_yt_binary
? data.ncd_yt_binary
: "/usr/local/bin/youtube-dl",
path: path,
},
{
label: "Aria2c binary",
id: "ncd_aria2_binary",
value: data.ncd_aria2_binary,
placeholder: "/usr/local/bin/aria2c",
path: path,
},
];
} catch (e) {
helper.error(e);
}
},
};
</script>

View File

@@ -0,0 +1,132 @@
<template>
<div class="section" :class="[classes]" :id="container">
<h3 class="title">{{ title }}</h3>
<div classs="button-container" :id="id" :path="path">
<editableRow
v-for="(option, key) in rows"
v-bind:key="key"
:value="option.value"
:name="option.name"
:placeholder="option.placeholder"
/>
<button
class="custom-settings-add-btn"
@click.prevent="newOption($event, name)"
data-tippy-content="Add new options"
>
<slot name="add">New Option</slot>
</button>
<button
class="custom-settings-save-btn"
@click.prevent="saveOptions($event)"
:data-rel="id"
>
<slot name="save">Save</slot>
</button>
</div>
</div>
</template>
<script>
import helper from "../utils/helper";
import settingsForm from "../lib/settingsForm";
import editableRow from "./editableRow";
export default {
name: "customOptions",
props: {
path: String,
name: {
type: String,
default: "settings",
},
title: {
type: String,
default: "Custom Settings",
},
classes: String,
validOptions: Array,
options: Array,
},
data() {
return {
id: "custom-" + this.name,
classes: "custom-settings-container",
container: "custom-settings-container",
validOptions: this.validOptions,
options: [],
};
},
components: {
editableRow,
},
computed: {
rows() {
return this.options;
},
},
methods: {
newOption(e, baseName) {
e.stopPropagation();
let element = e.target;
let nodeList, key, value;
nodeList = document.querySelectorAll(`[id^='${baseName}-key']`);
if (nodeList.length === 0) {
key = `${baseName}-key-1`;
value = `${baseName}-value-1`;
} else {
let index = nodeList.length + 1;
key = `${baseName}-key-${index}`;
value = `${baseName}-value-${index}`;
//selector = `[id^='${baseName}-key']`;
}
let form = settingsForm.getInstance();
element.before(form.createInputGroup(key, value));
helper.autoComplete(`[id^='${baseName}-key']`, this.validOptions);
},
saveOptions(e) {
let element = e.target;
let container = element.getAttribute("data-rel");
let data = helper.getData(container);
let url = helper.generateUrl(data._path);
data = helper.transformParams(data, this.name);
let badOptions = [];
for (let name in data) {
if (!this.validOptions.includes(name)) {
badOptions.push(name);
}
}
if (badOptions.length > 0) {
helper.error("invalid options: " + badOptions.join(","));
return;
}
helper
.httpClient(url)
.setData(data)
.setHandler((resp) => {
if (resp.error) {
helper.error(resp.error);
return;
}
this.options = [];
for (let key in data) {
this.options.push({ name: key, value: data[key] });
}
let inputDiv = element.parentElement.querySelectorAll(
`div[id^='${this.name}-key']`
);
if (inputDiv && inputDiv.length > 0) {
inputDiv.forEach((element) => {
element.remove();
});
}
helper.info(resp.message);
})
.send();
},
},
mounted() {
this.$emit("mounted", event, this);
},
};
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,34 @@
<template>
<div :id="name">
<label :for="name">{{ name }}</label>
<input
type="text"
:id="name"
:name="name"
:value="value"
placeholder="Leave empty if no value needed"
class="form-input-text"
/>
<button class="has-content icon-close" @click="remove($event)"></button>
</div>
</template>
<script>
export default {
name: "editableRow",
props: {
name: String,
value: String,
placeholder: String,
},
data() {
return {};
},
methods: {
remove(e) {
let ele = e.target;
let container = ele.closest("div");
container.remove();
},
},
};
</script>

View File

@@ -0,0 +1,72 @@
<template>
<div :class="container" :path="path" :id="container">
<label :for="id">{{ label }}</label>
<input
type="text"
:class="classes"
:id="id"
:name="id"
:value="value"
:placeholder="placeholder"
@blur="saveHandler($event)"
:data-rel="container"
/>
<input
v-if="useBtn"
type="button"
value="save"
:data-rel="container"
@click.prevent="saveHandler($event)"
/>
</div>
</template>
<script>
import helper from "../utils/helper";
export default {
name: "settingsRow",
props: {
label: String,
id: String,
value: String,
placeholder: String,
path: String,
useBtn: {
type: Boolean,
default: false,
},
},
data() {
return {
classes: this.id.replace("_", "-") + "-input",
container: this.id.replace("_", "-") + "-container",
};
},
methods: {
saveHandler(e) {
if (e.type == "blur" && this.useBtn) {
return;
}
e.stopPropagation();
let element = e.target;
let data = helper.getData(element.getAttribute("data-rel"));
let url = helper.generateUrl(data._path);
data = helper.transformParams(data);
helper
.httpClient(url)
.setData(data)
.setHandler(function (resp) {
if (resp.error) {
helper.error(resp.error);
return;
}
helper.info(resp.message);
})
.send();
},
},
computed: {},
mounted() {},
};
</script>
<style scoped></style>

View File

@@ -1,6 +1,9 @@
.ncdownloader-personal-settings, .ncdownloader-personal-settings,
.ncdownloader-admin-settings { .ncdownloader-admin-settings {
position: relative; position: relative;
display: flex;
flex-direction: column;
margin-left: 1em;
#ncdownloader-message-banner { #ncdownloader-message-banner {
position: fixed; position: fixed;
@@ -10,9 +13,9 @@
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 4px; border-radius: 4px;
text-shadow : 0 1px 0 rgba(255, 255, 255, .2); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow : inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
width: 100%; width: 100%;
background-color: #dff0d8; background-color: #dff0d8;
} }
@@ -30,5 +33,4 @@
background-color: #f2dede; background-color: #f2dede;
border-color: #ebccd1; border-color: #ebccd1;
} }
} }

View File

@@ -1,6 +1,5 @@
import helper from './utils/helper' import helper from './utils/helper'
import eventHandler from './lib/eventHandler' import eventHandler from './lib/eventHandler'
import Http from './lib/http'
import { translate as t, translatePlural as n } from '@nextcloud/l10n' import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import updatePage from './actions/updatePage' import updatePage from './actions/updatePage'
import buttonActions from './actions/buttonActions' import buttonActions from './actions/buttonActions'
@@ -10,7 +9,6 @@ import { createApp } from 'vue'
import App from './App'; import App from './App';
import tippy, { delegate } from 'tippy.js'; import tippy, { delegate } from 'tippy.js';
import 'tippy.js/dist/tippy.css'; import 'tippy.js/dist/tippy.css';
'use strict'
import settingsBar from './settingsBar'; import settingsBar from './settingsBar';
const basePath = "/apps/ncdownloader"; const basePath = "/apps/ncdownloader";

View File

@@ -8,31 +8,30 @@ type dataItems = {
type data = Array<dataItems> type data = Array<dataItems>
class settingsForm { class settingsForm {
parent = "custom-aria2-settings-container"; container;
constructor() { constructor(containerId?: string) {
this.container = containerId
} }
static getInstance() { static getInstance(containerId?: string) {
return new this(); return new this(containerId);
} }
setParent(selector: string): settingsForm { setContainer(selector: string): settingsForm {
this.parent = selector; this.container = selector;
return this; return this;
} }
create(parent: HTMLElement, element: dataItems) { create(containerEle: HTMLElement, element: dataItems) {
let label = this._createLabel(element.name, element.id) let label = this._createLabel(element.name, element.id)
let input = this._createInput(element); let input = this._createInput(element);
//let saveBtn = this._createSaveBtn(element.id); //let saveBtn = this._createSaveBtn(element.id);
let cancelBtn = this._createCancelBtn("has-content"); let cancelBtn = this._createCancelBtn("has-content");
let container = this._createContainer(element.id); let wrapper = this._createContainer(element.id);
[label, input, cancelBtn].forEach(ele => { [label, input, cancelBtn].forEach(ele => {
container.appendChild(ele); wrapper.appendChild(ele);
}) })
return containerEle.prepend(wrapper);
return parent.prepend(container);
} }
createCustomInput(keyId: string, valueId: string): HTMLElement { createInputGroup(keyId: string, valueId: string): HTMLElement {
let div = this._createContainer(keyId + "-container") let div = this._createContainer(keyId + "-container")
let items: dataItems = { let items: dataItems = {
id: keyId, id: keyId,
@@ -48,7 +47,8 @@ class settingsForm {
_createContainer(id: string): HTMLElement { _createContainer(id: string): HTMLElement {
let div = document.createElement("div"); let div = document.createElement("div");
div.classList.add(id); div.setAttribute("id",id);
div.classList.add("autocomplete-container")
return div; return div;
} }
_createCancelBtn(className = ''): HTMLElement { _createCancelBtn(className = ''): HTMLElement {
@@ -57,6 +57,10 @@ class settingsForm {
button.classList.add(className); button.classList.add(className);
//button.setAttribute("type",'button') //button.setAttribute("type",'button')
button.classList.add("icon-close"); button.classList.add("icon-close");
button.addEventListener("click", function () {
let container = this.parentNode as HTMLElement
container.remove()
})
return button; return button;
} }
_createSaveBtn(id: string): HTMLElement { _createSaveBtn(id: string): HTMLElement {
@@ -68,7 +72,7 @@ class settingsForm {
} }
_createLabel(name: string, id: string): HTMLElement { _createLabel(name: string, id: string): HTMLElement {
name = name.replace('_', '-'); name = name.replace('_', '-');
let label = document.createElement("lable"); let label = document.createElement("label");
label.setAttribute("for", id); label.setAttribute("for", id);
let text = document.createTextNode(name); let text = document.createTextNode(name);
label.appendChild(text); label.appendChild(text);
@@ -90,9 +94,12 @@ class settingsForm {
return input; return input;
} }
render(data: data) { render(data: data) {
let parent = document.getElementById(this.parent) let container = document.getElementById(this.container)
if (!container) {
throw this.container + " is not found"
}
for (const element of data) { for (const element of data) {
this.create(parent, element) this.create(container, element)
} }
} }
} }

123
src/personalSettings.vue Normal file
View File

@@ -0,0 +1,123 @@
<template>
<div class="section ncdownloader-general-settings">
<h3>General Settings</h3>
<settingsRow
v-for="(option, key) in optionRows"
v-bind:key="key"
:value="option.value"
:id="option.id"
:label="option.label"
:placeholder="option.placeholder"
:path="option.path"
:useBtn="true"
/>
</div>
<customOptions
name="custom-aria2-settings"
title="Personal Aria2 Settings"
@mounted="renderAria2"
path="/apps/ncdownloader/personal/aria2/save"
:validOptions="aria2Options"
>
<template #save>Save Aria2 Settings</template>
</customOptions>
<customOptions
name="custom-ytdl-settings"
title="Personal Youtbue-dl Settings"
@mounted="renderYtdl"
path="/apps/ncdownloader/personal/ytdl/save"
:validOptions="ytdlOptions"
>
<template #save>Save Youtube-dl Settings</template>
</customOptions>
</template>
<script>
import customOptions from "./components/customOptions";
import helper from "./utils/helper";
import aria2Options from "./utils/aria2Options";
import { options as ytdlFullOptions, names as ytdlOptions } from "./utils/ytdlOptions";
import settingsRow from "./components/settingsRow";
export default {
name: "personalSettings",
data() {
return {
options: [],
aria2Options: aria2Options,
ytdlOptions: ytdlOptions,
};
},
components: {
customOptions,
settingsRow,
},
methods: {
renderAria2(event, $vm) {
helper
.httpClient(helper.generateUrl("/apps/ncdownloader/personal/aria2/get"))
//.setMethod("GET")
.setHandler((data) => {
if (!data) {
return;
}
let input = [];
for (let key in data) {
if (aria2Options.includes(key))
input.push({ name: key, value: data[key], id: key });
}
//settingsForm.getInstance($vm.container).render(input);
$vm.options = input;
})
.send();
},
renderYtdl(event, $vm) {
helper
.httpClient(helper.generateUrl("/apps/ncdownloader/personal/ytdl/get"))
//.setMethod("GET")
.setHandler((data) => {
if (!data) {
return;
}
let input = [];
for (let key in data) {
if (ytdlOptions.includes(key))
input.push({ name: key, value: data[key], id: key });
}
//settingsForm.getInstance($vm.container).render(input);
$vm.options = input;
})
.send();
},
},
computed: {
optionRows() {
return this.options;
},
},
mounted() {
try {
let data = this.$el.parentElement.getAttribute("data-settings");
data = JSON.parse(data);
let path = "/apps/ncdownloader/personal/save";
this.options = [
{
label: "Downloads Folder ",
id: "ncd_downloader_dir",
value: data.ncd_downloader_dir,
placeholder: data.ncd_downloader_dir ? data.ncd_downloader_dir : "/downloads",
path: path,
},
{
label: "Torrents Folder",
id: "ncd_torrents_dir",
value: data.ncd_torrents_dir,
placeholder: data.ncd_torrents_dir ? data.ncd_torrents_dir : "/torrents",
path: path,
},
];
} catch (e) {
helper.error(e);
}
},
};
</script>

View File

@@ -1,143 +1,19 @@
import Http from './lib/http'
import OC_msg from './lib/msg'
import {
generateUrl
} from '@nextcloud/router'
import settingsForm from './lib/settingsForm'
import autoComplete from './lib/autoComplete';
import eventHandler from './lib/eventHandler'; import eventHandler from './lib/eventHandler';
import aria2Options from './utils/aria2Options';
import { options as ytdlFullOptions, names as ytdlOptions } from './utils/ytdlOptions';
import helper from './utils/helper'; import helper from './utils/helper';
import './css/autoComplete.css' import './css/autoComplete.css'
import './css/settings.scss' import './css/settings.scss'
'use strict';
import { delegate } from 'tippy.js'; import { delegate } from 'tippy.js';
import 'tippy.js/dist/tippy.css'; import 'tippy.js/dist/tippy.css';
import { createApp } from 'vue';
import adminSettings from './adminSettings';
import personalSettings from './personalSettings';
const customSettings = createApp(adminSettings)
const pSettings = createApp(personalSettings)
customSettings.mount('#ncdownloader-admin-settings')
pSettings.mount('#ncdownloader-personal-settings')
window.addEventListener('DOMContentLoaded', function () { window.addEventListener('DOMContentLoaded', function () {
let customOptions = ['ncd_downloader_dir', 'ncd_torrents_dir', 'ncd_seed_ratio', 'ncd_seed_time', 'ncd_rpctoken', 'ncd_yt_binary', 'ncd_aria2_binary'];
const saveHandler = (e, name) => {
e.stopImmediatePropagation();
let element = e.target;
let data = helper.getData(element.getAttribute("data-rel"));
let url = generateUrl(data.path);
delete data.path;
OC_msg.startSaving('#ncdownloader-message-banner');
helper.makePair(data, name);
let badOptions = [];
if (name === 'ytdl-settings') {
for (let key in data) {
if (!ytdlOptions.includes(key) && !customOptions.includes(key)) {
delete data[key];
badOptions.push(key)
}
}
} else {
for (let key in data) {
if (!aria2Options.includes(key) && !customOptions.includes(key)) {
delete data[key];
badOptions.push(key)
}
}
}
if (badOptions.length > 0) {
OC_msg.finishedError('#ncdownloader-message-banner', 'invalid options: ' + badOptions.join(','));
return;
}
helper.httpClient(url).setData(data).setHandler(function (data) {
if (data.hasOwnProperty("error")) {
OC_msg.finishedError('#ncdownloader-message-banner', data.error);
} else if (data.hasOwnProperty("message")) {
OC_msg.finishedSuccess('#ncdownloader-message-banner', data.message);
} else {
OC_msg.finishedSuccess('#ncdownloader-message-banner', "DONE");
}
}).send();
}
const addOption = (e, name, options) => {
e.preventDefault();
e.stopPropagation();
let baseName = `${name}-settings`;
let element = e.target;
let selector = `#${baseName}-key-1`;
let form = settingsForm.getInstance();
let nodeList, key, value;
nodeList = document.querySelectorAll(`[id^='${baseName}-key']`)
if (nodeList.length === 0) {
key = `${baseName}-key-1`;
value = `${baseName}-value-1`;
} else {
let index = nodeList.length + 1;
key = `${baseName}-key-${index}`;
value = `${baseName}-value-${index}`;
selector = `[id^='${baseName}-key']`;
}
element.before(form.createCustomInput(key, value));
try {
autoComplete.getInstance({
selector: `[id^='${baseName}-key']`,
minChars: 1,
sourceHandler: function () {
if (Array.isArray(options)) {
return options;
}
return Object.keys(options);
},
renderer: (item, search) => {
search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
let tippy;
if (options.hasOwnProperty(item)) {
tippy = options[item];
} else {
tippy = item;
}
var re = new RegExp(`(${search.split(' ').join('|')})`, "gi");
return `<div data-tippy-content="${tippy}" class="suggestion-item" data-val="${item}">${item.replace(re, "<b>$1</b>")}</div>`;
}
}).run();
} catch (error) {
console.log(error)
OC_msg.finishedError('#ncdownloader-message-banner', error);
}
}
eventHandler.add('click', '.ncdownloader-admin-settings', 'input[type="button"]', (e) => saveHandler(e));
eventHandler.add('click', '.ncdownloader-personal-settings', 'input[type="button"]', (e) => saveHandler(e));
eventHandler.add("click", "#custom-aria2-settings-container", "button.save-custom-aria2-settings", (e) => saveHandler(e))
eventHandler.add("click", "#custom-ytdl-settings-container", "button.save-custom-ytdl-settings", (e) => saveHandler(e, 'ytdl-settings'))
eventHandler.add('click', '#custom-aria2-settings-container', "button.add-custom-aria2-settings", (e) => addOption(e, 'aria2', aria2Options))
eventHandler.add('click', '#custom-ytdl-settings-container', "button.add-custom-ytdl-settings", (e) => addOption(e, 'ytdl', ytdlFullOptions))
eventHandler.add('click', '.ncdownloader-personal-settings', 'button.icon-close', function (e) {
e.stopImmediatePropagation();
e.preventDefault();
this.parentNode.remove();
})
helper.httpClient(generateUrl("/apps/ncdownloader/personal/aria2/get")).setHandler(function (data) {
if (!data) {
return;
}
let input = [];
for (let key in data) {
if (aria2Options.includes(key))
input.push({ name: key, value: data[key], id: key });
}
settingsForm.getInstance().render(input);
}).send();
helper.httpClient(generateUrl("/apps/ncdownloader/personal/ytdl/get")).setHandler(function (data) {
if (!data) {
return;
}
let input = [];
for (let key in data) {
input.push({ name: key, value: data[key], id: key });
}
settingsForm.getInstance().setParent("custom-ytdl-settings-container").render(input);
}).send();
const filepicker = function (event) { const filepicker = function (event) {
let element = event.target; let element = event.target;

View File

@@ -7,6 +7,8 @@ import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import contentTable from '../lib/contentTable'; import contentTable from '../lib/contentTable';
import Http from '../lib/http' import Http from '../lib/http'
import Polling from "../lib/polling"; import Polling from "../lib/polling";
import autoComplete from '../lib/autoComplete';
const helper = { const helper = {
vue: {}, vue: {},
addVue(name, object) { addVue(name, object) {
@@ -69,9 +71,13 @@ const helper = {
return string.charAt(0).toUpperCase() + string.slice(1) return string.charAt(0).toUpperCase() + string.slice(1)
}, },
isURL(url) { isURL(url) {
let regex = '^((https?|ftp)://)([a-z0-9-]+\.)?(?:[-a-zA-Z0-9()@:%_\+.~#?&/=]+)$'; try {
const pattern = new RegExp(regex, 'i'); new URL(url.trim());
return pattern.test(url); return true;
} catch (e) {
console.log(e.message);
return false;
}
}, },
isMagnetURI(url) { isMagnetURI(url) {
const magnetURI = /^magnet:\?xt=urn:[a-z0-9]+:[a-z0-9]{32,40}(&dn=.+&tr=.+)?$/i; const magnetURI = /^magnet:\?xt=urn:[a-z0-9]+:[a-z0-9]{32,40}(&dn=.+&tr=.+)?$/i;
@@ -154,23 +160,10 @@ const helper = {
let doc = parser.parseFromString(htmlString, "text/html") let doc = parser.parseFromString(htmlString, "text/html")
return doc.querySelector("div"); return doc.querySelector("div");
}, },
makePair: function (data, prefix = "aria2-settings") {
for (let key in data) {
let index;
if ((index = key.indexOf(prefix + "-key-")) !== -1) {
let valueKey = prefix + "-value-" + key.substring(key.lastIndexOf('-') + 1);
if (data[valueKey] === undefined) continue;
let newkey = data[key];
data[newkey] = data[valueKey];
delete data[key];
delete data[valueKey];
}
}
},
getData(selector) { getData(selector) {
const element = typeof selector === "object" ? selector : document.getElementById(selector) const element = typeof selector === "object" ? selector : document.getElementById(selector)
const data = {} const data = {}
data['path'] = element.getAttribute('path') || ''; data['_path'] = element.getAttribute('path') || '';
//if the targeted element is not of input or select type, search for such elements below it //if the targeted element is not of input or select type, search for such elements below it
if (!['SELECT', 'INPUT'].includes(element.nodeName.toUpperCase())) { if (!['SELECT', 'INPUT'].includes(element.nodeName.toUpperCase())) {
const nodeList = element.querySelectorAll('input,select') const nodeList = element.querySelectorAll('input,select')
@@ -183,11 +176,17 @@ const helper = {
const key = element.getAttribute('id') const key = element.getAttribute('id')
data[key] = element.value data[key] = element.value
for (let prop in element.dataset) { for (let prop in element.dataset) {
if (prop == "rel") {
continue;
}
data[prop] = element.dataset[prop]; data[prop] = element.dataset[prop];
} }
} }
} else { } else {
for (let prop in element.dataset) { for (let prop in element.dataset) {
if (prop == "rel") {
continue;
}
data[prop] = element.dataset[prop]; data[prop] = element.dataset[prop];
} }
const key = element.getAttribute('id') const key = element.getAttribute('id')
@@ -308,6 +307,55 @@ const helper = {
}, },
httpClient(url) { httpClient(url) {
return new Http.create(url, true) return new Http.create(url, true)
},
autoComplete(selector, options) {
try {
autoComplete.getInstance({
selector: selector,
minChars: 1,
sourceHandler: function () {
if (Array.isArray(options)) {
return options;
}
return Object.keys(options);
},
renderer: (item, search) => {
if (!item || !search) {
return;
}
search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
let tippy;
if (options.hasOwnProperty(item)) {
tippy = options[item];
} else {
tippy = item;
}
var re = new RegExp(`(${search.split(' ').join('|')})`, "gi");
return `<div data-tippy-content="${tippy}" class="suggestion-item" data-val="${item}">${item.replace(re, "<b>$1</b>")}</div>`;
}
}).run();
} catch (error) {
console.log(error)
helper.error(error);
}
},
transformParams(data, prefix = "aria2-settings") {
let index
for (let key in data) {
if (key.charAt(0) == "_") {
delete data[key]
continue
}
if ((index = key.indexOf(prefix + "-key-")) !== -1) {
let valueKey = prefix + "-value-" + key.substring(key.lastIndexOf('-') + 1);
if (data[valueKey] === undefined) continue;
let newkey = data[key];
data[newkey] = data[valueKey];
delete data[key];
delete data[valueKey];
}
}
return data
} }
} }

View File

@@ -3,36 +3,6 @@ script("ncdownloader", 'appSettings');
style("ncdownloader", 'appSettings'); style("ncdownloader", 'appSettings');
extract($_); extract($_);
?> ?>
<div class="ncdownloader-admin-settings"> <div id="ncdownloader-admin-settings" class="ncdownloader-admin-settings" data-settings='<?php print json_encode($settings);?>'>
<div id="ncdownloader-message-banner" style="display: none;"></div>
<form id="ncdownloader" class="section">
<h2>NCDownloader admin Settings</h2>
<div id="ncd_rpctoken_settings" path="<?php print $path;?>">
<label for="ncd_rpctoken">
<?php print($l->t('Aria2 RPC Token'));?>
</label>
<input type="text" class="ncd_rpctoken" id="ncd_rpctoken" name="ncd_rpctoken"
value="<?php print($ncd_rpctoken ?? 'ncdownloader123');?>"
placeholder="ncdownloader123" />
<input type="button" value="<?php print($l->t('Save'));?>" data-rel="ncd_rpctoken_settings" />
</div>
<div id="ncd_yt_binary_container" path="<?php print $path;?>">
<label for="ncd_yt_binary">
<?php print($l->t('Youtube-dl Binary Path'));?>
</label>
<input type="text" class="ncd_yt_binary" id="ncd_yt_binary" name="ncd_yt_binary"
value="<?php print($ncd_yt_binary ?? '/usr/local/bin/ytdl');?>"
placeholder='/usr/local/bin/ytdl' />
<input type="button" value="<?php print($l->t('Save'));?>" data-rel="ncd_yt_binary_container" />
</div>
<div id="ncd_aria2_binary_container" path="<?php print $path;?>">
<label for="ncd_aria2_binary">
<?php print($l->t('Aria2 Binary Path'));?>
</label>
<input type="text" class="ncd_aria2_binary" id="ncd_aria2_binary" name="ncd_aria2_binary"
value="<?php print($ncd_aria2_binary ?? '/usr/bin/aria2c');?>"
placeholder="/usr/bin/aria2c" />
<input type="button" value="<?php print($l->t('Save'));?>" data-rel="ncd_aria2_binary_container" />
</div>
</form>
</div> </div>

View File

@@ -2,93 +2,8 @@
script("ncdownloader", 'appSettings'); script("ncdownloader", 'appSettings');
style("ncdownloader", 'appSettings'); style("ncdownloader", 'appSettings');
extract($_); extract($_);
$time_map = array('i' => 'minutes', 'h' => 'hours', 'w' => 'weeks', 'd' => 'days', 'y' => 'years');
?> ?>
<div class="ncdownloader-personal-settings"> <div class="ncdownloader-personal-settings" id="ncdownloader-personal-settings" data-settings='<?php print json_encode($settings); ?>'>
<div id="ncdownloader-message-banner" style="display: none;"></div>
<div id="ncdownloader-settings-form" class="section">
<div class="ncdownloader-general-settings">
<h2 class="title">
<?php print($l->t('NCDownloader Settings'));?>
</h2>
<div id="ncd_downloader_dir_settings" path="<?php print $path;?>">
<label for="ncd_downloader_dir">
<?php print($l->t('Downloads Folder'));?>
</label>
<input type="text" class="ncd_downloader_dir" id="ncd_downloader_dir" name="ncd_downloader_dir"
value="<?php print($ncd_downloader_dir ?? '/Downloads');?>" placeholder="/Downloads" />
<input type="button" value="<?php print($l->t('Save'));?>" data-rel="ncd_downloader_dir_settings" />
</div>
<div id="ncd_torrents_dir_settings" path="<?php print $path;?>">
<label for="ncd_torrents_dir">
<?php print($l->t('Torrents Folder'));?>
</label>
<input type="text" class="ncd_torrents_dir" id="ncd_torrents_dir"
value="<?php print($ncd_torrents_dir ?? '/Downloads/Files/Torrents');?>"
placeholder="/Downloads/Files/Torrents" />
<input type="button" value="<?php print($l->t('Save'));?>" data-rel="ncd_torrents_dir_settings" />
</div>
</div>
<hr />
<div class="ncdownloader-bt-settings">
<h2>
<?php print($l->t('BT Sharing settings'));?>
</h2>
<div id="ncd_btratio_container" path="<?php print $path;?>">
<label for="ncd_seed_ratio">
<?php print($l->t('Seed ratio'));?>
</label>
<input id="ncd_seed_ratio" value="<?php print($ncd_seed_ratio ?? 1.0);?>" placeholder="1.0">
</input>
<input type="button" value="<?php print($l->t('Save'));?>" data-rel="ncd_btratio_container" />
</div>
<div>
<div id="seed_time_settings_container" path="<?php print $path;?>">
<label for="ncd_seed_time">
<?php print($l->t('Seed Time in minutes'));?>
</label>
<input id="ncd_seed_time" type="text" class="ncd_seed_time"
value="<?php print($ncd_seed_time ?? 1);?>" placeholder="1 m,h,d,w,m">
</input>
<input type="button" value="<?php print($l->t('Save'));?>"
data-rel="seed_time_settings_container" />
</div>
</div>
</div>
<hr />
<div class="advanced-settings-container">
<h2 class="title">
<?php print($l->t('Advanced Settings'));?>
</h2>
<div class="ncdownloader-aria2-settings">
<h3 class="title">
<?php print($l->t('Custom Aria2 Settings'));?>
</h3>
<div classs="section" id="custom-aria2-settings-container"
path="/apps/ncdownloader/personal/aria2/save">
<button class="add-custom-aria2-settings">
<?php print $l->t('Add Options');?>
</button>
<button class="save-custom-aria2-settings" data-rel="custom-aria2-settings-container">
<?php print $l->t('Save');?>
</button>
</div>
</div>
<div class="ncdownloader-ytdl-settings">
<h3 class="title">
<?php print($l->t('Custom Youtube-dl Settings'));?>
</h3>
<div classs="section" id="custom-ytdl-settings-container"
path="/apps/ncdownloader/personal/ytdl/save">
<button class="add-custom-ytdl-settings">
<?php print $l->t('Add Options');?>
</button>
<button class="save-custom-ytdl-settings" data-tippy-content=''
data-rel="custom-ytdl-settings-container">
<?php print $l->t('Save');?>
</button>
</div>
</div>
</div>
</div>
</div> </div>