revamped settings frontend
This commit is contained in:
@@ -9,7 +9,8 @@ use OCP\IDBConnection;
|
||||
use OCP\Settings\ISettings;
|
||||
use OCA\NCDownloader\Db\Settings;
|
||||
|
||||
class Admin implements ISettings {
|
||||
class Admin implements ISettings
|
||||
{
|
||||
|
||||
/** @var IDBConnection */
|
||||
private $connection;
|
||||
@@ -18,9 +19,11 @@ class Admin implements ISettings {
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
|
||||
public function __construct(IDBConnection $connection,
|
||||
public function __construct(
|
||||
IDBConnection $connection,
|
||||
ITimeFactory $timeFactory,
|
||||
IConfig $config) {
|
||||
IConfig $config
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->timeFactory = $timeFactory;
|
||||
$this->config = $config;
|
||||
@@ -31,13 +34,18 @@ class Admin implements ISettings {
|
||||
/**
|
||||
* @return TemplateResponse
|
||||
*/
|
||||
public function getForm() {
|
||||
public function getForm()
|
||||
{
|
||||
$this->settings->setType($this->settings::TYPE['SYSTEM']);
|
||||
$parameters = [
|
||||
'settings' => [
|
||||
"path" => "/apps/ncdownloader/admin/save",
|
||||
"ncd_yt_binary" => $this->settings->get("ncd_yt_binary"),
|
||||
"ncd_aria2_binary" => $this->settings->get("ncd_aria2_binary"),
|
||||
"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, '');
|
||||
}
|
||||
@@ -45,7 +53,8 @@ class Admin implements ISettings {
|
||||
/**
|
||||
* @return string the section ID, e.g. 'sharing'
|
||||
*/
|
||||
public function getSection(): string {
|
||||
public function getSection(): string
|
||||
{
|
||||
return 'ncdownloader';
|
||||
}
|
||||
|
||||
@@ -56,7 +65,8 @@ class Admin implements ISettings {
|
||||
*
|
||||
* E.g.: 70
|
||||
*/
|
||||
public function getPriority(): int {
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ use OCP\Settings\ISettings;
|
||||
use OCA\NCDownloader\Db\Settings;
|
||||
use OCA\NCDownloader\Tools\Helper;
|
||||
|
||||
class Personal implements ISettings {
|
||||
class Personal implements ISettings
|
||||
{
|
||||
|
||||
/** @var IDBConnection */
|
||||
private $connection;
|
||||
@@ -19,9 +20,11 @@ class Personal implements ISettings {
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
|
||||
public function __construct(IDBConnection $connection,
|
||||
public function __construct(
|
||||
IDBConnection $connection,
|
||||
ITimeFactory $timeFactory,
|
||||
IConfig $config) {
|
||||
IConfig $config
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->timeFactory = $timeFactory;
|
||||
$this->config = $config;
|
||||
@@ -32,14 +35,17 @@ class Personal implements ISettings {
|
||||
/**
|
||||
* @return TemplateResponse
|
||||
*/
|
||||
public function getForm() {
|
||||
public function getForm()
|
||||
{
|
||||
$parameters = [
|
||||
"settings" => [
|
||||
"ncd_downloader_dir" => Helper::getDownloadDir(),
|
||||
"ncd_torrents_dir" => $this->settings->get("ncd_torrents_dir"),
|
||||
"ncd_seed_ratio" => $this->settings->get("ncd_seed_ratio"),
|
||||
'ncd_seed_time_unit' => $this->settings->get("ncd_seed_time_unit"),
|
||||
'ncd_seed_time' => $this->settings->get("ncd_seed_time"),
|
||||
"path" => '/apps/ncdownloader/personal/save',
|
||||
]
|
||||
];
|
||||
|
||||
//\OC_Util::addScript($this->appName, 'common');
|
||||
@@ -51,7 +57,8 @@ class Personal implements ISettings {
|
||||
/**
|
||||
* @return string the section ID, e.g. 'sharing'
|
||||
*/
|
||||
public function getSection(): string {
|
||||
public function getSection(): string
|
||||
{
|
||||
return 'ncdownloader';
|
||||
}
|
||||
|
||||
@@ -62,7 +69,8 @@ class Personal implements ISettings {
|
||||
*
|
||||
* E.g.: 70
|
||||
*/
|
||||
public function getPriority(): int {
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"dependencies": {
|
||||
"chalk": "^5.0.0",
|
||||
"toastr": "^2.1.4",
|
||||
"v-tooltip": "^4.0.0-alpha.1"
|
||||
"v-tooltip": "^4.0.0-alpha.1",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0",
|
||||
@@ -52,6 +53,7 @@
|
||||
"css-loader": "^6.4.0",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jquery": "^3.6.0",
|
||||
"mini-css-extract-plugin": "^2.6.0",
|
||||
"popper.js": "^1.16.1",
|
||||
"sass": "^1.38.0",
|
||||
"sass-loader": "^10.2.0",
|
||||
@@ -68,7 +70,6 @@
|
||||
"vue-loader": "^16.0.0-beta.10",
|
||||
"vue-svg-loader": "^0.17.0-beta.2",
|
||||
"webpack": "^5.69.0",
|
||||
"webpack-cli": "^4.9.0",
|
||||
"mini-css-extract-plugin": "^2.6.0"
|
||||
"webpack-cli": "^4.9.0"
|
||||
}
|
||||
}
|
||||
115
src/adminSettings.vue
Normal file
115
src/adminSettings.vue
Normal 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>
|
||||
132
src/components/customOptions.vue
Normal file
132
src/components/customOptions.vue
Normal 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>
|
||||
34
src/components/editableRow.vue
Normal file
34
src/components/editableRow.vue
Normal 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>
|
||||
72
src/components/settingsRow.vue
Normal file
72
src/components/settingsRow.vue
Normal 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>
|
||||
@@ -1,34 +1,36 @@
|
||||
.ncdownloader-personal-settings,
|
||||
.ncdownloader-admin-settings {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 1em;
|
||||
|
||||
#ncdownloader-message-banner {
|
||||
position : fixed;
|
||||
top : 50px;
|
||||
text-align : center;
|
||||
padding : 15px;
|
||||
margin-bottom : 20px;
|
||||
border : 1px solid transparent;
|
||||
border-radius : 4px;
|
||||
text-shadow : 0 1px 0 rgba(255, 255, 255, .2);
|
||||
-webkit-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, .25), 0 1px 2px rgba(0, 0, 0, .05);
|
||||
width : 100%;
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
-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, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
width: 100%;
|
||||
background-color: #dff0d8;
|
||||
}
|
||||
|
||||
#ncdownloader-message-banner.success,
|
||||
.message-banner.success {
|
||||
color : #799479;
|
||||
color: #799479;
|
||||
background-color: #dff0d8;
|
||||
border-color : #d6e9c6;
|
||||
border-color: #d6e9c6;
|
||||
}
|
||||
|
||||
#ncdownloader-message-banner.error,
|
||||
.message-banner.error {
|
||||
color : #a94442;
|
||||
color: #a94442;
|
||||
background-color: #f2dede;
|
||||
border-color : #ebccd1;
|
||||
border-color: #ebccd1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import helper from './utils/helper'
|
||||
import eventHandler from './lib/eventHandler'
|
||||
import Http from './lib/http'
|
||||
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
|
||||
import updatePage from './actions/updatePage'
|
||||
import buttonActions from './actions/buttonActions'
|
||||
@@ -10,7 +9,6 @@ import { createApp } from 'vue'
|
||||
import App from './App';
|
||||
import tippy, { delegate } from 'tippy.js';
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
'use strict'
|
||||
import settingsBar from './settingsBar';
|
||||
const basePath = "/apps/ncdownloader";
|
||||
|
||||
|
||||
@@ -8,31 +8,30 @@ type dataItems = {
|
||||
type data = Array<dataItems>
|
||||
|
||||
class settingsForm {
|
||||
parent = "custom-aria2-settings-container";
|
||||
constructor() {
|
||||
|
||||
container;
|
||||
constructor(containerId?: string) {
|
||||
this.container = containerId
|
||||
}
|
||||
static getInstance() {
|
||||
return new this();
|
||||
static getInstance(containerId?: string) {
|
||||
return new this(containerId);
|
||||
}
|
||||
setParent(selector: string): settingsForm {
|
||||
this.parent = selector;
|
||||
setContainer(selector: string): settingsForm {
|
||||
this.container = selector;
|
||||
return this;
|
||||
}
|
||||
create(parent: HTMLElement, element: dataItems) {
|
||||
create(containerEle: HTMLElement, element: dataItems) {
|
||||
let label = this._createLabel(element.name, element.id)
|
||||
let input = this._createInput(element);
|
||||
//let saveBtn = this._createSaveBtn(element.id);
|
||||
let cancelBtn = this._createCancelBtn("has-content");
|
||||
let container = this._createContainer(element.id);
|
||||
let wrapper = this._createContainer(element.id);
|
||||
[label, input, cancelBtn].forEach(ele => {
|
||||
container.appendChild(ele);
|
||||
wrapper.appendChild(ele);
|
||||
})
|
||||
|
||||
return parent.prepend(container);
|
||||
return containerEle.prepend(wrapper);
|
||||
}
|
||||
|
||||
createCustomInput(keyId: string, valueId: string): HTMLElement {
|
||||
createInputGroup(keyId: string, valueId: string): HTMLElement {
|
||||
let div = this._createContainer(keyId + "-container")
|
||||
let items: dataItems = {
|
||||
id: keyId,
|
||||
@@ -48,7 +47,8 @@ class settingsForm {
|
||||
|
||||
_createContainer(id: string): HTMLElement {
|
||||
let div = document.createElement("div");
|
||||
div.classList.add(id);
|
||||
div.setAttribute("id",id);
|
||||
div.classList.add("autocomplete-container")
|
||||
return div;
|
||||
}
|
||||
_createCancelBtn(className = ''): HTMLElement {
|
||||
@@ -57,6 +57,10 @@ class settingsForm {
|
||||
button.classList.add(className);
|
||||
//button.setAttribute("type",'button')
|
||||
button.classList.add("icon-close");
|
||||
button.addEventListener("click", function () {
|
||||
let container = this.parentNode as HTMLElement
|
||||
container.remove()
|
||||
})
|
||||
return button;
|
||||
}
|
||||
_createSaveBtn(id: string): HTMLElement {
|
||||
@@ -68,7 +72,7 @@ class settingsForm {
|
||||
}
|
||||
_createLabel(name: string, id: string): HTMLElement {
|
||||
name = name.replace('_', '-');
|
||||
let label = document.createElement("lable");
|
||||
let label = document.createElement("label");
|
||||
label.setAttribute("for", id);
|
||||
let text = document.createTextNode(name);
|
||||
label.appendChild(text);
|
||||
@@ -90,9 +94,12 @@ class settingsForm {
|
||||
return input;
|
||||
}
|
||||
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) {
|
||||
this.create(parent, element)
|
||||
this.create(container, element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
123
src/personalSettings.vue
Normal file
123
src/personalSettings.vue
Normal 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>
|
||||
140
src/settings.js
140
src/settings.js
@@ -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 aria2Options from './utils/aria2Options';
|
||||
import { options as ytdlFullOptions, names as ytdlOptions } from './utils/ytdlOptions';
|
||||
import helper from './utils/helper';
|
||||
import './css/autoComplete.css'
|
||||
import './css/settings.scss'
|
||||
'use strict';
|
||||
import { delegate } from 'tippy.js';
|
||||
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 () {
|
||||
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) {
|
||||
let element = event.target;
|
||||
|
||||
@@ -7,6 +7,8 @@ import { translate as t, translatePlural as n } from '@nextcloud/l10n'
|
||||
import contentTable from '../lib/contentTable';
|
||||
import Http from '../lib/http'
|
||||
import Polling from "../lib/polling";
|
||||
import autoComplete from '../lib/autoComplete';
|
||||
|
||||
const helper = {
|
||||
vue: {},
|
||||
addVue(name, object) {
|
||||
@@ -69,9 +71,13 @@ const helper = {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
},
|
||||
isURL(url) {
|
||||
let regex = '^((https?|ftp)://)([a-z0-9-]+\.)?(?:[-a-zA-Z0-9()@:%_\+.~#?&/=]+)$';
|
||||
const pattern = new RegExp(regex, 'i');
|
||||
return pattern.test(url);
|
||||
try {
|
||||
new URL(url.trim());
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isMagnetURI(url) {
|
||||
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")
|
||||
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) {
|
||||
const element = typeof selector === "object" ? selector : document.getElementById(selector)
|
||||
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 (!['SELECT', 'INPUT'].includes(element.nodeName.toUpperCase())) {
|
||||
const nodeList = element.querySelectorAll('input,select')
|
||||
@@ -183,11 +176,17 @@ const helper = {
|
||||
const key = element.getAttribute('id')
|
||||
data[key] = element.value
|
||||
for (let prop in element.dataset) {
|
||||
if (prop == "rel") {
|
||||
continue;
|
||||
}
|
||||
data[prop] = element.dataset[prop];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let prop in element.dataset) {
|
||||
if (prop == "rel") {
|
||||
continue;
|
||||
}
|
||||
data[prop] = element.dataset[prop];
|
||||
}
|
||||
const key = element.getAttribute('id')
|
||||
@@ -287,7 +286,7 @@ const helper = {
|
||||
container.setAttribute("type", name);
|
||||
container.className = "table " + name;
|
||||
},
|
||||
filepicker(cb,currentPath) {
|
||||
filepicker(cb, currentPath) {
|
||||
OC.dialogs.filepicker(
|
||||
t('ncdownloader', 'Select a directory'),
|
||||
cb,
|
||||
@@ -308,6 +307,55 @@ const helper = {
|
||||
},
|
||||
httpClient(url) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,36 +3,6 @@ script("ncdownloader", 'appSettings');
|
||||
style("ncdownloader", 'appSettings');
|
||||
extract($_);
|
||||
?>
|
||||
<div class="ncdownloader-admin-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 id="ncdownloader-admin-settings" class="ncdownloader-admin-settings" data-settings='<?php print json_encode($settings);?>'>
|
||||
|
||||
</div>
|
||||
@@ -2,93 +2,8 @@
|
||||
script("ncdownloader", 'appSettings');
|
||||
style("ncdownloader", 'appSettings');
|
||||
extract($_);
|
||||
$time_map = array('i' => 'minutes', 'h' => 'hours', 'w' => 'weeks', 'd' => 'days', 'y' => 'years');
|
||||
|
||||
?>
|
||||
<div class="ncdownloader-personal-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 class="ncdownloader-personal-settings" id="ncdownloader-personal-settings" data-settings='<?php print json_encode($settings); ?>'>
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user