/', views.ajax_delete_folder, name='ajax_delete_folder'),
path(r'', views.index, name='home')
]
diff --git a/YtManagerApp/management.py b/YtManagerApp/management.py
new file mode 100644
index 0000000..acfeb9e
--- /dev/null
+++ b/YtManagerApp/management.py
@@ -0,0 +1,44 @@
+from .models import SubscriptionFolder, Subscription, Video
+
+
+class FolderManager(object):
+
+ @staticmethod
+ def create_or_edit(fid, name, parent_id):
+ # Create or edit
+ if fid == '#':
+ folder = SubscriptionFolder()
+ else:
+ folder = SubscriptionFolder.objects.get(id=int(fid))
+
+ # Set attributes
+ folder.name = name
+ if parent_id == '#':
+ folder.parent = None
+ else:
+ folder.parent = SubscriptionFolder.objects.get(id=int(parent_id))
+
+ FolderManager.__validate(folder)
+ folder.save()
+
+ @staticmethod
+ def __validate(folder):
+ # Make sure folder name is unique in the parent folder
+ for dbFolder in SubscriptionFolder.objects.filter(parent_id=folder.parent_id):
+ if dbFolder.id != folder.id and dbFolder.name == folder.name:
+ raise ValueError('Folder name is not unique!')
+
+ # Prevent parenting loops
+ current = folder
+ visited = []
+
+ while not (current is None):
+ if current in visited:
+ raise ValueError('Parenting cycle detected!')
+ visited.append(current)
+ current = current.parent
+
+ @staticmethod
+ def delete(fid: int):
+ folder = SubscriptionFolder.objects.get(id=fid)
+ folder.delete()
diff --git a/YtManagerApp/migrations/0004_auto_20181005_1626.py b/YtManagerApp/migrations/0004_auto_20181005_1626.py
new file mode 100644
index 0000000..fcbfa88
--- /dev/null
+++ b/YtManagerApp/migrations/0004_auto_20181005_1626.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.2 on 2018-10-05 13:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('YtManagerApp', '0003_auto_20181003_1825'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='video',
+ name='downloaded_path',
+ field=models.TextField(blank=True, null=True),
+ ),
+ ]
diff --git a/YtManagerApp/static/YtManagerApp/css/style.css b/YtManagerApp/static/YtManagerApp/css/style.css
index 48aa54c..579ac76 100644
--- a/YtManagerApp/static/YtManagerApp/css/style.css
+++ b/YtManagerApp/static/YtManagerApp/css/style.css
@@ -1,9 +1,31 @@
/* Some material font helpers */
-
.material-folder::before {
- content: "\e2c7";
-}
+ content: "\e2c7"; }
.material-person::before {
- content: "\e7fd";
-}
+ content: "\e7fd"; }
+
+/* Loading animation */
+.loading-dual-ring {
+ display: inline-block;
+ width: 64px;
+ height: 64px; }
+
+.loading-dual-ring:after {
+ content: " ";
+ display: block;
+ width: 46px;
+ height: 46px;
+ margin: 1px;
+ border-radius: 50%;
+ border: 5px solid #007bff;
+ border-color: #007bff transparent #007bff transparent;
+ animation: loading-dual-ring 1.2s linear infinite; }
+
+@keyframes loading-dual-ring {
+ 0% {
+ transform: rotate(0deg); }
+ 100% {
+ transform: rotate(360deg); } }
+
+/*# sourceMappingURL=style.css.map */
diff --git a/YtManagerApp/static/YtManagerApp/css/style.css.map b/YtManagerApp/static/YtManagerApp/css/style.css.map
new file mode 100644
index 0000000..dec9131
--- /dev/null
+++ b/YtManagerApp/static/YtManagerApp/css/style.css.map
@@ -0,0 +1,7 @@
+{
+"version": 3,
+"mappings": "AAEA,gCAAgC;AAChC,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,uBAAuB;AACvB,kBAAmB;EACf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAGhB,wBAAyB;EACrB,OAAO,EAAE,GAAG;EACZ,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,GAAG;EACX,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,iBAAuB;EAC/B,YAAY,EAAE,uCAAmD;EACjE,SAAS,EAAE,sCAAsC;;AAGrD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc",
+"sources": ["style.scss"],
+"names": [],
+"file": "style.css"
+}
\ No newline at end of file
diff --git a/YtManagerApp/static/YtManagerApp/css/style.scss b/YtManagerApp/static/YtManagerApp/css/style.scss
new file mode 100644
index 0000000..30b17dd
--- /dev/null
+++ b/YtManagerApp/static/YtManagerApp/css/style.scss
@@ -0,0 +1,38 @@
+$accent-color: #007bff;
+
+/* Some material font helpers */
+.material-folder::before {
+ content: "\e2c7";
+}
+
+.material-person::before {
+ content: "\e7fd";
+}
+
+/* Loading animation */
+.loading-dual-ring {
+ display: inline-block;
+ width: 64px;
+ height: 64px;
+}
+
+.loading-dual-ring:after {
+ content: " ";
+ display: block;
+ width: 46px;
+ height: 46px;
+ margin: 1px;
+ border-radius: 50%;
+ border: 5px solid $accent-color;
+ border-color: $accent-color transparent $accent-color transparent;
+ animation: loading-dual-ring 1.2s linear infinite;
+}
+
+@keyframes loading-dual-ring {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
\ No newline at end of file
diff --git a/YtManagerApp/static/YtManagerApp/import/jstree/jstree.js b/YtManagerApp/static/YtManagerApp/import/jstree/jstree.js
index ed7c316..851a15f 100644
--- a/YtManagerApp/static/YtManagerApp/import/jstree/jstree.js
+++ b/YtManagerApp/static/YtManagerApp/import/jstree/jstree.js
@@ -4592,7 +4592,7 @@
if(theme_url === true) {
var dir = this.settings.core.themes.dir;
if(!dir) { dir = $.jstree.path + '/themes'; }
- theme_url = dir + '/' + theme_name + '/style.css';
+ theme_url = dir + '/' + theme_name + '/_style.css';
}
if(theme_url && $.inArray(theme_url, themes_loaded) === -1) {
$('head').append('<'+'link rel="stylesheet" href="' + theme_url + '" type="text/css" />');
diff --git a/YtManagerApp/static/YtManagerApp/js/subtree.js b/YtManagerApp/static/YtManagerApp/js/subtree.js
deleted file mode 100644
index 230d06f..0000000
--- a/YtManagerApp/static/YtManagerApp/js/subtree.js
+++ /dev/null
@@ -1,50 +0,0 @@
-function onSelectionChanged(e, data)
-{
- node = data.instance.get_selected(true)[0];
-}
-
-function validateChange(operation, node, parent, position, more)
-{
- if (more.dnd)
- {
- // create_node, rename_node, delete_node, move_node and copy_node
- if (operation === "copy_node" || operation === "move_node")
- {
- if (more.ref.type === "sub")
- return false;
- }
- }
-
- return true;
-}
-
-function setupTree(dataIn)
-{
- $("#tree-wrapper").jstree({
- core : {
- data : {
- url : 'ajax/get_children'
- },
- check_callback : validateChange,
- themes : {
- dots : false
- },
- },
- types : {
- folder : {
- icon : "material-icons material-folder"
- },
- sub : {
- icon : "material-icons material-person",
- max_depth : 0
- }
- },
- plugins : [ "types", "wholerow", "dnd" ]
- });
- $("#tree-wrapper").on("changed.jstree", onSelectionChanged);
-}
-
-$(document).ready(function ()
-{
- setupTree();
-})
diff --git a/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html b/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html
index 8fb52ce..d71897e 100644
--- a/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html
+++ b/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html
@@ -2,30 +2,38 @@
\ No newline at end of file
+
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/subscription_edit_dialog.html b/YtManagerApp/templates/YtManagerApp/controls/subscription_edit_dialog.html
new file mode 100644
index 0000000..acd1b54
--- /dev/null
+++ b/YtManagerApp/templates/YtManagerApp/controls/subscription_edit_dialog.html
@@ -0,0 +1,39 @@
+
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/index.html b/YtManagerApp/templates/YtManagerApp/index.html
index 3c85c9e..029287a 100644
--- a/YtManagerApp/templates/YtManagerApp/index.html
+++ b/YtManagerApp/templates/YtManagerApp/index.html
@@ -2,25 +2,38 @@
{% load static %}
{% block stylesheets %}
-
+
{% endblock %}
{% block scripts %}
-
-
+
+
+
+ {% include 'YtManagerApp/controls/folder_edit_dialog.html' %}
+
{% endblock %}
{% block master %}
diff --git a/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js b/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js
new file mode 100644
index 0000000..33ba124
--- /dev/null
+++ b/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js
@@ -0,0 +1,174 @@
+function folderEditDialog_Show(isNew, editNode)
+{
+ let dialog = $("#folder_edit_dialog");
+ dialog.find('#folder_edit_dialog_title').text(isNew ? "New folder" : "Edit folder");
+ dialog.find("#folder_edit_dialog_loading").show();
+ dialog.find("#folder_edit_dialog_error").hide();
+ dialog.find("#folder_edit_dialog_form").hide();
+ dialog.modal();
+
+ $.get("{% url 'ajax_get_folders' %}")
+ .done(function(folders)
+ {
+ // Populate list of folders
+ let selParent = dialog.find("#folder_edit_dialog_parent");
+ selParent.empty();
+ selParent.append(new Option('(None)', '#'));
+
+ let parentId = null;
+ if (!isNew) {
+ parentId = editNode.parent.replace('folder', '');
+ }
+
+ for (let folder of folders)
+ {
+ let o = new Option(folder.text, folder.id);
+ if (!isNew && folder.id.toString() === parentId.toString())
+ o.selected = true;
+
+ selParent.append(o);
+ }
+
+ // Show form
+ dialog.find("#folder_edit_dialog_loading").hide();
+ dialog.find("#folder_edit_dialog_form").show();
+ dialog.find("#folder_edit_dialog_submit").text(isNew ? "Create" : "Save");
+
+ if (isNew)
+ {
+ dialog.find("#folder_edit_dialog_id").val('#');
+ dialog.find("#folder_edit_dialog_name").val('');
+ }
+ if (!isNew)
+ {
+ idTrimmed = editNode.id.replace('folder', '');
+ dialog.find("#folder_edit_dialog_id").val(idTrimmed);
+ dialog.find("#folder_edit_dialog_name").val(editNode.text);
+ }
+ })
+ .fail(function() {
+ let msgError = dialog.find("#folder_edit_dialog_error");
+ msgError.show();
+ msgError.text("An error occurred!");
+ });
+}
+
+function folderEditDialog_ShowNew()
+{
+ folderEditDialog_Show(true, null);
+}
+
+function folderEditDialog_Close()
+{
+ $("#folder_edit_dialog").modal('hide');
+}
+
+function folderEditDialog_Submit(e)
+{
+ let form = $(this);
+ let url = form.attr('action');
+
+ $.post(url, form.serialize())
+ .done(tree_Refresh);
+
+ folderEditDialog_Close();
+ e.preventDefault();
+}
+
+function treeNode_Edit()
+{
+ let selectedNodes = $("#tree-wrapper").jstree('get_selected', true);
+ if (selectedNodes.length === 1)
+ {
+ let node = selectedNodes[0];
+ if (node.type === 'folder') {
+ folderEditDialog_Show(false, node);
+ }
+ else {
+ // TODO...
+ }
+ }
+}
+
+function treeNode_Delete()
+{
+ let selectedNodes = $("#tree-wrapper").jstree('get_selected', true);
+ if (selectedNodes.length === 1)
+ {
+ let node = selectedNodes[0];
+ if (node.type === 'folder') {
+ let folderId = node.id.toString().replace('folder', '');
+ if (confirm('Are you sure you want to delete folder "' + node.text + '" and all its descendants?\nNote: the subscriptions won\'t be deleted, they will only be moved outside.'))
+ {
+ $.post("{% url 'ajax_delete_folder' 99999 %}".replace('99999', folderId), {
+ csrfmiddlewaretoken: '{{ csrf_token }}'
+ }).done(tree_Refresh);
+ }
+ }
+ else {
+ // TODO...
+ }
+ }
+}
+
+function tree_Initialize()
+{
+ let treeWrapper = $("#tree-wrapper");
+ treeWrapper.jstree({
+ core : {
+ data : {
+ url : "{% url 'ajax_get_children' %}"
+ },
+ check_callback : tree_ValidateChange,
+ themes : {
+ dots : false
+ },
+ },
+ types : {
+ folder : {
+ icon : "material-icons material-folder"
+ },
+ sub : {
+ icon : "material-icons material-person",
+ max_depth : 0
+ }
+ },
+ plugins : [ "types", "wholerow", "dnd" ]
+ });
+ treeWrapper.on("changed.jstree", tree_OnSelectionChanged);
+}
+
+function tree_Refresh()
+{
+ $("#tree-wrapper").jstree("refresh");
+}
+
+function tree_ValidateChange(operation, node, parent, position, more)
+{
+ if (more.dnd)
+ {
+ // create_node, rename_node, delete_node, move_node and copy_node
+ if (operation === "copy_node" || operation === "move_node")
+ {
+ if (more.ref.type === "sub")
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function tree_OnSelectionChanged(e, data)
+{
+ node = data.instance.get_selected(true)[0];
+}
+
+$(document).ready(function ()
+{
+ tree_Initialize();
+ $("#btn_create_folder").on("click", folderEditDialog_ShowNew);
+ $("#btn_edit_node").on("click", treeNode_Edit);
+ $("#btn_delete_node").on("click", treeNode_Delete);
+
+ $("#folder_edit_dialog_form").submit(folderEditDialog_Submit);
+});
diff --git a/YtManagerApp/views.py b/YtManagerApp/views.py
index 5e097c5..8b7ee03 100644
--- a/YtManagerApp/views.py
+++ b/YtManagerApp/views.py
@@ -1,36 +1,42 @@
from django.shortcuts import render
from django.http import HttpResponse, HttpRequest, JsonResponse
from .models import SubscriptionFolder, Subscription
+from .management import FolderManager
+
def get_children_recurse(parent_id):
children = []
for folder in SubscriptionFolder.objects.filter(parent_id=parent_id).order_by('name'):
children.append({
- "id" : "folder" + str(folder.id),
- "text" : folder.name,
- "type" : "folder",
- "children" : get_children_recurse(folder.id)
+ "id": "folder" + str(folder.id),
+ "text": folder.name,
+ "type": "folder",
+ "state": {"opened": True},
+ "children": get_children_recurse(folder.id)
})
for sub in Subscription.objects.filter(parent_folder_id=parent_id).order_by('name'):
children.append({
- "id" : "sub" + str(sub.id),
- "type" : "sub",
- "text" : sub.name
+ "id": "sub" + str(sub.id),
+ "type": "sub",
+ "text": sub.name
})
return children
-def get_folders(parent_id, path = ""):
+def get_folders(parent_id, path=""):
folders = []
+ prefix = path + "/"
+ if len(path) == 0:
+ prefix = ""
for folder in SubscriptionFolder.objects.filter(parent_id=parent_id).order_by('name'):
- folder_path = path + "/" + folder.name
+ folder_path = prefix + folder.name
folders.append({
- "id" : "folder" + str(folder.id),
- "text" : folder_path
+ "id": folder.id,
+ "text": folder_path
})
folders.extend(get_folders(folder.id, folder_path))
@@ -40,9 +46,26 @@ def get_folders(parent_id, path = ""):
def ajax_get_children(request: HttpRequest):
return JsonResponse(get_children_recurse(None), safe=False)
+
def ajax_get_folders(request: HttpRequest):
return JsonResponse(get_folders(None), safe=False)
+
+def ajax_edit_folder(request: HttpRequest):
+ if request.method == 'POST':
+ fid = request.POST['id']
+ name = request.POST['name']
+ parent_id = request.POST['parent']
+ FolderManager.create_or_edit(fid, name, parent_id)
+
+ return HttpResponse()
+
+
+def ajax_delete_folder(request: HttpRequest, fid):
+ FolderManager.delete(fid)
+ return HttpResponse()
+
+
def index(request: HttpRequest):
context = {}
- return render(request, 'YtManagerApp/index.html', context)
\ No newline at end of file
+ return render(request, 'YtManagerApp/index.html', context)