Просмотр исходного кода

Merge pull request #1329 from MatthewHana/master

New Web UI
Peter Bieringer 1 год назад
Родитель
Сommit
71fd91631e

+ 1 - 0
radicale/web/internal_data/css/icons/delete.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M20 9l-1.995 11.346A2 2 0 0116.035 22h-8.07a2 2 0 01-1.97-1.654L4 9M21 6h-5.625M3 6h5.625m0 0V4a2 2 0 012-2h2.75a2 2 0 012 2v2m-6.75 0h6.75" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

+ 1 - 0
radicale/web/internal_data/css/icons/download.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M6 20h12M12 4v12m0 0l3.5-3.5M12 16l-3.5-3.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

+ 1 - 0
radicale/web/internal_data/css/icons/edit.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M14.363 5.652l1.48-1.48a2 2 0 012.829 0l1.414 1.414a2 2 0 010 2.828l-1.48 1.48m-4.243-4.242l-9.616 9.615a2 2 0 00-.578 1.238l-.242 2.74a1 1 0 001.084 1.085l2.74-.242a2 2 0 001.24-.578l9.615-9.616m-4.243-4.242l4.243 4.242" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

+ 1 - 0
radicale/web/internal_data/css/icons/new.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M6 12h6m6 0h-6m0 0V6m0 6v6" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

+ 1 - 0
radicale/web/internal_data/css/icons/upload.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M6 20h12M12 16V4m0 0l3.5 3.5M12 4L8.5 7.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

+ 55 - 0
radicale/web/internal_data/css/loading.svg

@@ -0,0 +1,55 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; display: block;" width="264px" height="264px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
+<g transform="rotate(0 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#4e9a06">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.8026755852842808s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(27.692307692307693 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#71cc1a">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.7357859531772575s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(55.38461538461539 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#8ce139">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.6688963210702341s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(83.07692307692308 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#cdff9c">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.6020066889632106s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(110.76923076923077 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#cdf7a6">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.5351170568561873s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(138.46153846153845 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#fcfcfc">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.46822742474916385s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(166.15384615384616 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#fefefe">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.4013377926421404s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(193.84615384615384 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#f4f4f4">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.33444816053511706s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(221.53846153846155 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#ffd6d6">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.26755852842809363s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(249.23076923076923 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#f86f6f">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.2006688963210702s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(276.9230769230769 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#e73c3c">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.13377926421404682s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(304.61538461538464 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#da2121">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="-0.06688963210702341s" repeatCount="indefinite"></animate>
+  </rect>
+</g><g transform="rotate(332.3076923076923 50 50)">
+  <rect x="45.5" y="32" rx="0" ry="0" width="9" height="4" fill="#a40000">
+    <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="0.8695652173913042s" begin="0s" repeatCount="indefinite"></animate>
+  </rect>
+</g>
+</svg>

+ 10 - 0
radicale/web/internal_data/css/logo.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="200" height="300" xmlns="http://www.w3.org/2000/svg">
+  <path fill="#a40000" d="M 186,188 C 184,98 34,105 47,192 C 59,279 130,296 130,296 C 130,296 189,277 186,188 z" />
+  <path fill="#ffffff" d="M 73,238 C 119,242 140,241 177,222 C 172,270 131,288 131,288 C 131,288 88,276 74,238 z" />
+  <g fill="none" stroke="#4e9a06" stroke-width="15">
+    <path d="M 103,137 C 77,69 13,62 13,62" />
+    <path d="M 105,136 C 105,86 37,20 37,20" />
+    <path d="M 105,135 C 112,73 83,17 83,17" />
+  </g>
+</svg>

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
radicale/web/internal_data/css/main.css


+ 85 - 18
radicale/web/internal_data/fn.js

@@ -1,6 +1,6 @@
 /**
 /**
  * This file is part of Radicale Server - Calendar Server
  * This file is part of Radicale Server - Calendar Server
- * Copyright © 2017-2018 Unrud <unrud@outlook.com>
+ * Copyright © 2017-2024 Unrud <unrud@outlook.com>
  *
  *
  * This program is free software: you can redistribute it and/or modify
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * it under the terms of the GNU General Public License as published by
@@ -28,7 +28,7 @@ const SERVER = location.origin;
  * @const
  * @const
  * @type {string}
  * @type {string}
  */
  */
-const ROOT_PATH = (new URL("..", location.href)).pathname;
+const ROOT_PATH = location.pathname.replace(new RegExp("/+[^/]+/*(/index\\.html?)?$"), "") + '/';
 
 
 /**
 /**
  * Regex to match and normalize color
  * Regex to match and normalize color
@@ -63,6 +63,7 @@ const CollectionType = {
     CALENDAR: "CALENDAR",
     CALENDAR: "CALENDAR",
     JOURNAL: "JOURNAL",
     JOURNAL: "JOURNAL",
     TASKS: "TASKS",
     TASKS: "TASKS",
+    WEBCAL: "WEBCAL",
     is_subset: function(a, b) {
     is_subset: function(a, b) {
         let components = a.split("_");
         let components = a.split("_");
         for (let i = 0; i < components.length; i++) {
         for (let i = 0; i < components.length; i++) {
@@ -89,6 +90,9 @@ const CollectionType = {
         if (a.search(this.TASKS) !== -1 || b.search(this.TASKS) !== -1) {
         if (a.search(this.TASKS) !== -1 || b.search(this.TASKS) !== -1) {
             union.push(this.TASKS);
             union.push(this.TASKS);
         }
         }
+        if (a.search(this.WEBCAL) !== -1 || b.search(this.WEBCAL) !== -1) {
+            union.push(this.WEBCAL);
+        }
         return union.join("_");
         return union.join("_");
     }
     }
 };
 };
@@ -102,12 +106,13 @@ const CollectionType = {
  * @param {string} description
  * @param {string} description
  * @param {string} color
  * @param {string} color
  */
  */
-function Collection(href, type, displayname, description, color) {
+function Collection(href, type, displayname, description, color, source) {
     this.href = href;
     this.href = href;
     this.type = type;
     this.type = type;
     this.displayname = displayname;
     this.displayname = displayname;
     this.color = color;
     this.color = color;
     this.description = description;
     this.description = description;
+    this.source = source;
 }
 }
 
 
 /**
 /**
@@ -183,6 +188,7 @@ function get_collections(user, password, collection, callback) {
                 let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
                 let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
                 let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
                 let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
                 let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
                 let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
+                let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source");
                 let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
                 let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
                 let components_element = response.querySelector(components_query);
                 let components_element = response.querySelector(components_query);
                 let href = href_element ? href_element.textContent : "";
                 let href = href_element ? href_element.textContent : "";
@@ -190,11 +196,17 @@ function get_collections(user, password, collection, callback) {
                 let type = "";
                 let type = "";
                 let color = "";
                 let color = "";
                 let description = "";
                 let description = "";
+                let source = "";
                 if (resourcetype_element) {
                 if (resourcetype_element) {
                     if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
                     if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
                         type = CollectionType.ADDRESSBOOK;
                         type = CollectionType.ADDRESSBOOK;
                         color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
                         color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
                         description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
                         description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
+                    } else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) {
+                        type = CollectionType.WEBCAL;
+                        source = webcalsource_element ? webcalsource_element.textContent : "";
+                        color = calendarcolor_element ? calendarcolor_element.textContent : "";
+                        description = calendardesc_element ? calendardesc_element.textContent : "";
                     } else if (resourcetype_element.querySelector(resourcetype_query + " > *|calendar")) {
                     } else if (resourcetype_element.querySelector(resourcetype_query + " > *|calendar")) {
                         if (components_element) {
                         if (components_element) {
                             if (components_element.querySelector(components_query + " > *|comp[name=VEVENT]")) {
                             if (components_element.querySelector(components_query + " > *|comp[name=VEVENT]")) {
@@ -221,7 +233,7 @@ function get_collections(user, password, collection, callback) {
                     }
                     }
                 }
                 }
                 if (href.substr(-1) === "/" && href !== collection.href && type) {
                 if (href.substr(-1) === "/" && href !== collection.href && type) {
-                    collections.push(new Collection(href, type, displayname, description, sane_color));
+                    collections.push(new Collection(href, type, displayname, description, sane_color, source));
                 }
                 }
             }
             }
             collections.sort(function(a, b) {
             collections.sort(function(a, b) {
@@ -235,11 +247,15 @@ function get_collections(user, password, collection, callback) {
         }
         }
     };
     };
     request.send('<?xml version="1.0" encoding="utf-8" ?>' +
     request.send('<?xml version="1.0" encoding="utf-8" ?>' +
-                 '<propfind xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" ' +
+                 '<propfind ' + 
+                         'xmlns="DAV:" ' + 
+                         'xmlns:C="urn:ietf:params:xml:ns:caldav" ' +
                          'xmlns:CR="urn:ietf:params:xml:ns:carddav" ' +
                          'xmlns:CR="urn:ietf:params:xml:ns:carddav" ' +
+                         'xmlns:CS="http://calendarserver.org/ns/" ' +
                          'xmlns:I="http://apple.com/ns/ical/" ' +
                          'xmlns:I="http://apple.com/ns/ical/" ' +
                          'xmlns:INF="http://inf-it.com/ns/ab/" ' +
                          'xmlns:INF="http://inf-it.com/ns/ab/" ' +
-                         'xmlns:RADICALE="http://radicale.org/ns/">' +
+                         'xmlns:RADICALE="http://radicale.org/ns/"' + 
+                         '>' +
                      '<prop>' +
                      '<prop>' +
                          '<resourcetype />' +
                          '<resourcetype />' +
                          '<RADICALE:displayname />' +
                          '<RADICALE:displayname />' +
@@ -248,6 +264,7 @@ function get_collections(user, password, collection, callback) {
                          '<C:calendar-description />' +
                          '<C:calendar-description />' +
                          '<C:supported-calendar-component-set />' +
                          '<C:supported-calendar-component-set />' +
                          '<CR:addressbook-description />' +
                          '<CR:addressbook-description />' +
+                         '<CS:source />' +
                      '</prop>' +
                      '</prop>' +
                  '</propfind>');
                  '</propfind>');
     return request;
     return request;
@@ -329,12 +346,18 @@ function create_edit_collection(user, password, collection, create, callback) {
     let addressbook_color = "";
     let addressbook_color = "";
     let calendar_description = "";
     let calendar_description = "";
     let addressbook_description = "";
     let addressbook_description = "";
+    let calendar_source = "";
     let resourcetype;
     let resourcetype;
     let components = "";
     let components = "";
     if (collection.type === CollectionType.ADDRESSBOOK) {
     if (collection.type === CollectionType.ADDRESSBOOK) {
         addressbook_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
         addressbook_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
         addressbook_description = escape_xml(collection.description);
         addressbook_description = escape_xml(collection.description);
         resourcetype = '<CR:addressbook />';
         resourcetype = '<CR:addressbook />';
+    } else if (collection.type === CollectionType.WEBCAL) {
+        calendar_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
+        calendar_description = escape_xml(collection.description);
+        resourcetype = '<CS:subscribed />';
+        calendar_source = collection.source;
     } else {
     } else {
         calendar_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
         calendar_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
         calendar_description = escape_xml(collection.description);
         calendar_description = escape_xml(collection.description);
@@ -351,7 +374,7 @@ function create_edit_collection(user, password, collection, create, callback) {
     }
     }
     let xml_request = create ? "mkcol" : "propertyupdate";
     let xml_request = create ? "mkcol" : "propertyupdate";
     request.send('<?xml version="1.0" encoding="UTF-8" ?>' +
     request.send('<?xml version="1.0" encoding="UTF-8" ?>' +
-                 '<' + xml_request + ' xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:CR="urn:ietf:params:xml:ns:carddav" xmlns:I="http://apple.com/ns/ical/" xmlns:INF="http://inf-it.com/ns/ab/">' +
+                 '<' + xml_request + ' xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:CR="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:I="http://apple.com/ns/ical/" xmlns:INF="http://inf-it.com/ns/ab/">' +
                      '<set>' +
                      '<set>' +
                          '<prop>' +
                          '<prop>' +
                              (create ? '<resourcetype><collection />' + resourcetype + '</resourcetype>' : '') +
                              (create ? '<resourcetype><collection />' + resourcetype + '</resourcetype>' : '') +
@@ -361,6 +384,7 @@ function create_edit_collection(user, password, collection, create, callback) {
                              (addressbook_color ? '<INF:addressbook-color>' + addressbook_color + '</INF:addressbook-color>' : '') +
                              (addressbook_color ? '<INF:addressbook-color>' + addressbook_color + '</INF:addressbook-color>' : '') +
                              (addressbook_description ? '<CR:addressbook-description>' + addressbook_description + '</CR:addressbook-description>' : '') +
                              (addressbook_description ? '<CR:addressbook-description>' + addressbook_description + '</CR:addressbook-description>' : '') +
                              (calendar_description ? '<C:calendar-description>' + calendar_description + '</C:calendar-description>' : '') +
                              (calendar_description ? '<C:calendar-description>' + calendar_description + '</C:calendar-description>' : '') +
+                             (calendar_source ? '<CS:source>' + calendar_source + '</CS:source>' : '') +
                          '</prop>' +
                          '</prop>' +
                      '</set>' +
                      '</set>' +
                      (!create ? ('<remove>' +
                      (!create ? ('<remove>' +
@@ -495,7 +519,12 @@ function LoginScene() {
     function fill_form() {
     function fill_form() {
         user_form.value = user;
         user_form.value = user;
         password_form.value = "";
         password_form.value = "";
-        error_form.textContent = error ? "Error: " + error : "";
+        if(error){
+            error_form.textContent = "Error: " + error;
+            error_form.classList.remove("hidden");
+        }else{
+            error_form.classList.add("hidden");
+        }
     }
     }
 
 
     function onlogin() {
     function onlogin() {
@@ -507,7 +536,7 @@ function LoginScene() {
                 // setup logout
                 // setup logout
                 logout_view.classList.remove("hidden");
                 logout_view.classList.remove("hidden");
                 logout_btn.onclick = onlogout;
                 logout_btn.onclick = onlogout;
-                logout_user_form.textContent = user;
+                logout_user_form.textContent = user + "'s Collections";
                 // Fetch principal
                 // Fetch principal
                 let loading_scene = new LoadingScene();
                 let loading_scene = new LoadingScene();
                 push_scene(loading_scene, false);
                 push_scene(loading_scene, false);
@@ -683,12 +712,11 @@ function CollectionsScene(user, password, collection, onerror) {
             let color_form = node.querySelector("[data-name=color]");
             let color_form = node.querySelector("[data-name=color]");
             let delete_btn = node.querySelector("[data-name=delete]");
             let delete_btn = node.querySelector("[data-name=delete]");
             let edit_btn = node.querySelector("[data-name=edit]");
             let edit_btn = node.querySelector("[data-name=edit]");
+            let download_btn = node.querySelector("[data-name=download]");
             if (collection.color) {
             if (collection.color) {
-                color_form.style.color = collection.color;
-            } else {
-                color_form.classList.add("hidden");
+                color_form.style.background = collection.color;
             }
             }
-            let possible_types = [CollectionType.ADDRESSBOOK];
+            let possible_types = [CollectionType.ADDRESSBOOK, CollectionType.WEBCAL];
             [CollectionType.CALENDAR, ""].forEach(function(e) {
             [CollectionType.CALENDAR, ""].forEach(function(e) {
                 [CollectionType.union(e, CollectionType.JOURNAL), e].forEach(function(e) {
                 [CollectionType.union(e, CollectionType.JOURNAL), e].forEach(function(e) {
                     [CollectionType.union(e, CollectionType.TASKS), e].forEach(function(e) {
                     [CollectionType.union(e, CollectionType.TASKS), e].forEach(function(e) {
@@ -704,10 +732,16 @@ function CollectionsScene(user, password, collection, onerror) {
                 }
                 }
             });
             });
             title_form.textContent = collection.displayname || collection.href;
             title_form.textContent = collection.displayname || collection.href;
+            if(title_form.textContent.length > 30){
+                title_form.classList.add("smalltext");
+            }
             description_form.textContent = collection.description;
             description_form.textContent = collection.description;
+            if(description_form.textContent.length > 150){
+                description_form.classList.add("smalltext");
+            }
             let href = SERVER + collection.href;
             let href = SERVER + collection.href;
-            url_form.href = href;
-            url_form.textContent = href;
+            url_form.value = href;
+            download_btn.href = href;
             delete_btn.onclick = function() {return ondelete(collection);};
             delete_btn.onclick = function() {return ondelete(collection);};
             edit_btn.onclick = function() {return onedit(collection);};
             edit_btn.onclick = function() {return onedit(collection);};
             node.classList.remove("hidden");
             node.classList.remove("hidden");
@@ -945,9 +979,15 @@ function DeleteCollectionScene(user, password, collection) {
         scene_index = scene_stack.length - 1;
         scene_index = scene_stack.length - 1;
         html_scene.classList.remove("hidden");
         html_scene.classList.remove("hidden");
         title_form.textContent = collection.displayname || collection.href;
         title_form.textContent = collection.displayname || collection.href;
-        error_form.textContent = error ? "Error: " + error : "";
         delete_btn.onclick = ondelete;
         delete_btn.onclick = ondelete;
         cancel_btn.onclick = oncancel;
         cancel_btn.onclick = oncancel;
+        if(error){
+            error_form.textContent = "Error: " + error;
+            error_form.classList.remove("hidden");
+        }else{
+            error_form.classList.add("hidden");
+        }
+
     };
     };
     this.hide = function() {
     this.hide = function() {
         html_scene.classList.add("hidden");
         html_scene.classList.add("hidden");
@@ -989,12 +1029,19 @@ function CreateEditCollectionScene(user, password, collection) {
     let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
     let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
     let error_form = html_scene.querySelector("[data-name=error]");
     let error_form = html_scene.querySelector("[data-name=error]");
     let displayname_form = html_scene.querySelector("[data-name=displayname]");
     let displayname_form = html_scene.querySelector("[data-name=displayname]");
+    let displayname_label = html_scene.querySelector("label[for=displayname]");
     let description_form = html_scene.querySelector("[data-name=description]");
     let description_form = html_scene.querySelector("[data-name=description]");
+    let description_label = html_scene.querySelector("label[for=description]");
+    let source_form = html_scene.querySelector("[data-name=source]");
+    let source_label = html_scene.querySelector("label[for=source]");
     let type_form = html_scene.querySelector("[data-name=type]");
     let type_form = html_scene.querySelector("[data-name=type]");
+    let type_label = html_scene.querySelector("label[for=type]");
     let color_form = html_scene.querySelector("[data-name=color]");
     let color_form = html_scene.querySelector("[data-name=color]");
+    let color_label = html_scene.querySelector("label[for=color]");
     let submit_btn = html_scene.querySelector("[data-name=submit]");
     let submit_btn = html_scene.querySelector("[data-name=submit]");
     let cancel_btn = html_scene.querySelector("[data-name=cancel]");
     let cancel_btn = html_scene.querySelector("[data-name=cancel]");
 
 
+
     /** @type {?number} */ let scene_index = null;
     /** @type {?number} */ let scene_index = null;
     /** @type {?XMLHttpRequest} */ let create_edit_req = null;
     /** @type {?XMLHttpRequest} */ let create_edit_req = null;
     let error = "";
     let error = "";
@@ -1003,6 +1050,7 @@ function CreateEditCollectionScene(user, password, collection) {
     let href = edit ? collection.href : collection.href + random_uuid() + "/";
     let href = edit ? collection.href : collection.href + random_uuid() + "/";
     let displayname = edit ? collection.displayname : "";
     let displayname = edit ? collection.displayname : "";
     let description = edit ? collection.description : "";
     let description = edit ? collection.description : "";
+    let source = edit ? collection.source : "";
     let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
     let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
     let color = edit && collection.color ? collection.color : "#" + random_hex(6);
     let color = edit && collection.color ? collection.color : "#" + random_hex(6);
 
 
@@ -1022,6 +1070,7 @@ function CreateEditCollectionScene(user, password, collection) {
     function read_form() {
     function read_form() {
         displayname = displayname_form.value;
         displayname = displayname_form.value;
         description = description_form.value;
         description = description_form.value;
+        source = source_form.value;
         type = type_form.value;
         type = type_form.value;
         color = color_form.value;
         color = color_form.value;
     }
     }
@@ -1029,9 +1078,17 @@ function CreateEditCollectionScene(user, password, collection) {
     function fill_form() {
     function fill_form() {
         displayname_form.value = displayname;
         displayname_form.value = displayname;
         description_form.value = description;
         description_form.value = description;
+        source_form.value = source;
         type_form.value = type;
         type_form.value = type;
         color_form.value = color;
         color_form.value = color;
-        error_form.textContent = error ? "Error: " + error : "";
+        if(error){
+            error_form.textContent = "Error: " + error;
+            error_form.classList.remove("hidden");
+        }
+        error_form.classList.add("hidden");
+        
+        onTypeChange();
+        type_form.addEventListener("change", onTypeChange);
     }
     }
 
 
     function onsubmit() {
     function onsubmit() {
@@ -1049,7 +1106,7 @@ function CreateEditCollectionScene(user, password, collection) {
             }
             }
             let loading_scene = new LoadingScene();
             let loading_scene = new LoadingScene();
             push_scene(loading_scene);
             push_scene(loading_scene);
-            let collection = new Collection(href, type, displayname, description, sane_color);
+            let collection = new Collection(href, type, displayname, description, sane_color, source);
             let callback = function(error1) {
             let callback = function(error1) {
                 if (scene_index === null) {
                 if (scene_index === null) {
                     return;
                     return;
@@ -1082,6 +1139,16 @@ function CreateEditCollectionScene(user, password, collection) {
         return false;
         return false;
     }
     }
 
 
+    function onTypeChange(e){
+        if(type_form.value == CollectionType.WEBCAL){
+            source_label.classList.remove("hidden");
+            source_form.classList.remove("hidden");
+        }else{
+            source_label.classList.add("hidden");
+            source_form.classList.add("hidden");
+        }
+    }
+
     this.show = function() {
     this.show = function() {
         this.release();
         this.release();
         scene_index = scene_stack.length - 1;
         scene_index = scene_stack.length - 1;

+ 169 - 128
radicale/web/internal_data/index.html

@@ -1,96 +1,95 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html lang="en">
 <html lang="en">
-
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<meta http-equiv="X-UA-Compatible" content="IE=edge">
-<script src="fn.js"></script>
-<title>Radicale Web Interface</title>
-<link href="css/main.css" media="screen" rel="stylesheet">
-<link href="css/icon.png" type="image/png" rel="icon">
-<style>
-    .hidden {display:none;}
-</style>
-</head>
-
-<body>
-<nav>
-    <ul>
-        <li id="logoutview" class="hidden"><a href="" data-name="link">Logout [<span data-name="user" style="word-wrap:break-word;"></span>]</a></li>
-    </ul>
-</nav>
-
-<main>
-<section id="loadingscene">
-    <h1>Loading</h1>
-    <p>Please wait...</p>
-    <noscript>JavaScript is required</noscript>
-</section>
-
-<section id="loginscene" class="hidden">
-    <h1>Login</h1>
-    <form data-name="form">
-        <input data-name="user" type="text" placeholder="Username"><br>
-        <input data-name="password" type="password" placeholder="Password"><br>
-        <span style="color: #A40000;" data-name="error"></span><br>
-        <button type="submit">Next</button>
-    </form>
-</section>
-
-<section id="collectionsscene" class="hidden">
-    <h1>Collections</h1>
-    <ul>
-        <li><a href="" data-name="new">Create new addressbook or calendar</a></li>
-        <li><a href="" data-name="upload">Upload addressbook or calendar</a></li>
-    </ul>
-    <article data-name="collectiontemplate" class="hidden">
-        <h2><span data-name="color">█ </span><span data-name="title" style="word-wrap:break-word;">Title</span> <small>[<span data-name="ADDRESSBOOK">addressbook</span><span data-name="CALENDAR_JOURNAL_TASKS">calendar, journal and tasks</span><span data-name="CALENDAR_JOURNAL">calendar and journal</span><span data-name="CALENDAR_TASKS">calendar and tasks</span><span data-name="JOURNAL_TASKS">journal and tasks</span><span data-name="CALENDAR">calendar</span><span data-name="JOURNAL">journal</span><span data-name="TASKS">tasks</span>]</small></h2>
-        <span data-name="description" style="word-wrap:break-word;">Description</span>
-        <ul>
-            <li>URL: <a data-name="url" style="word-wrap:break-word;">url</a></li>
-            <li><a href="" data-name="edit">Edit</a></li>
-            <li><a href="" data-name="delete">Delete</a></li>
-        </ul>
-    </article>
-</section>
-
-<section id="editcollectionscene" class="hidden">
-    <h1>Edit collection</h1>
-    <h2>Edit <span data-name="title" style="word-wrap:break-word;font-weight:bold;">title</span>:</h2>
-    <form>
-        Title:<br>
-        <input data-name="displayname" type="text"><br>
-        Description:<br>
-        <input data-name="description" type="text"><br>
-        Type:<br>
-        <select data-name="type">
-            <option value="ADDRESSBOOK">addressbook</option>
-            <option value="CALENDAR_JOURNAL_TASKS">calendar, journal and tasks</option>
-            <option value="CALENDAR_JOURNAL">calendar and journal</option>
-            <option value="CALENDAR_TASKS">calendar and tasks</option>
-            <option value="JOURNAL_TASKS">journal and tasks</option>
-            <option value="CALENDAR">calendar</option>
-            <option value="JOURNAL">journal</option>
-            <option value="TASKS">tasks</option>
-        </select><br>
-        Color:<br>
-        <input data-name="color" type="color"><br>
-        <span style="color: #A40000;" data-name="error"></span><br>
-        <button type="submit" data-name="submit">Save</button>
-        <button type="button" data-name="cancel">Cancel</button>
-    </form>
-</section>
-
-<section id="createcollectionscene" class="hidden">
-    <h1>Create new collection</h1>
-    <form>
-        Title:<br>
-        <input data-name="displayname" type="text"><br>
-        Description:<br>
-        <input data-name="description" type="text"><br>
-        Type:<br>
-        <select data-name="type">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>Radicale Web Interface</title>
+    <link href="css/main.css" type="text/css" media="screen" rel="stylesheet">
+    <link href="css/icon.png" type="image/png" rel="icon">
+    <style>.hidden {display: none !important;}</style>
+    <script src="fn.js"></script>
+  </head>
+  
+  <body>
+    <nav id="logoutview" class="hidden">
+      <span data-name="user" style="word-wrap:break-word;"></span>
+      <a href="" class="red" data-name="link" title="Logout">Logout</a>
+    </nav>
+	
+    <main>
+      <section id="loadingscene">
+        <img src="css/loading.svg" alt="Loading">
+        <h2>Loading</h2>
+        <p>Please wait...</p>
+        <noscript>JavaScript is required</noscript>
+      </section>
+	  
+      <section id="loginscene" class="container hidden">
+        <div class="logocontainer">
+          <img src="css/logo.svg" alt="Radicale">
+        </div>
+        <h1>Sign in</h1>
+        <br>
+        <form data-name="form">
+          <input data-name="user" type="text" placeholder="Username">
+          <input data-name="password" type="password" placeholder="Password">
+          <button class="green" type="submit">Next</button>
+          <span class="error" data-name="error"></span>
+        </form>
+      </section>
+	  
+      <section id="collectionsscene" class="hidden">
+        <div class="fabcontainer">
+          <a href="" class="green" data-name="new" title="Create a new addressbook or calendar">
+            <img src="css/icons/new.svg" class="icon" alt="➕">
+          </a>
+          <a href="" class="blue" data-name="upload" title="Upload an addressbook or calendar">
+            <img src="css/icons/upload.svg" class="icon" alt="⬆️">
+          </a>
+        </div>
+        <article data-name="collectiontemplate" class="hidden">
+          <div class="colorbar" data-name="color"></div>
+          <h3 class="title" data-name="title">Title</h3>
+          <small>
+            <span data-name="ADDRESSBOOK">Address book</span>
+            <span data-name="CALENDAR_JOURNAL_TASKS">Calendar, journal and tasks</span>
+            <span data-name="CALENDAR_JOURNAL">Calendar and journal</span>
+            <span data-name="CALENDAR_TASKS">Calendar and tasks</span>
+            <span data-name="JOURNAL_TASKS">Journal and tasks</span>
+            <span data-name="CALENDAR">Calendar</span>
+            <span data-name="JOURNAL">Journal</span>
+            <span data-name="TASKS">Tasks</span>
+            <span data-name="WEBCAL">Webcal</span>
+          </small>
+          <input type="text" data-name="url" value="" readonly="" onfocus="this.setSelectionRange(0, 99999);">
+          <p data-name="description" style="word-wrap:break-word;">Description</p>
+          <ul>
+            <li>
+              <a href="" title="Download" class="green" data-name="download">
+                <img src="css/icons/download.svg" class="icon" alt="🔗">
+              </a>
+            </li>
+            <li>
+              <a href="" title="Edit" class="blue" data-name="edit">
+                <img src="css/icons/edit.svg" class="icon" alt="✏️">
+              </a>
+            </li>
+            <li>
+              <a href="" title="Delete" class="red" data-name="delete">
+                <img src="css/icons/delete.svg" class="icon" alt="❌">
+              </a>
+            </li>
+          </ul>
+        </article>
+      </section>
+	  
+      <section id="editcollectionscene" class="container hidden">
+        <h1>Edit Collection</h1>
+        <p>Editing collection <span class="title" data-name="title">title</span>
+        </p>
+        <form> Type: <br>
+          <select data-name="type">
             <option value="ADDRESSBOOK">addressbook</option>
             <option value="ADDRESSBOOK">addressbook</option>
             <option value="CALENDAR_JOURNAL_TASKS">calendar, journal and tasks</option>
             <option value="CALENDAR_JOURNAL_TASKS">calendar, journal and tasks</option>
             <option value="CALENDAR_JOURNAL">calendar and journal</option>
             <option value="CALENDAR_JOURNAL">calendar and journal</option>
@@ -99,40 +98,82 @@
             <option value="CALENDAR">calendar</option>
             <option value="CALENDAR">calendar</option>
             <option value="JOURNAL">journal</option>
             <option value="JOURNAL">journal</option>
             <option value="TASKS">tasks</option>
             <option value="TASKS">tasks</option>
-        </select><br>
-        Color:<br>
-        <input data-name="color" type="color"><br>
-        <span style="color: #A40000;" data-name="error"></span><br>
-        <button type="submit" data-name="submit">Create</button>
-        <button type="button" data-name="cancel">Cancel</button>
-    </form>
-</section>
-
-<section id="uploadcollectionscene" class="hidden">
-    <h1>Upload collection</h1>
-    <ul>
-        <li data-name="filetemplate" class="hidden">
-                Upload <span data-name="name" style="word-wrap:break-word;font-weight:bold;">name</span>:<br>
-                <span data-name="pending">Please wait...</span>
-                <span style="color: #00A400;" data-name="success">Finished</span>
-                <span style="color: #A40000;" data-name="error"></span>
-        </li>
-    </ul>
-    <form>
-        <button type="button" data-name="close">Close</button>
-    </form>
-</section>
-
-<section id="deletecollectionscene" class="hidden">
-    <h1>Delete collection</h1>
-    <h2>Delete <span data-name="title" style="word-wrap:break-word;font-weight:bold;">title</span>?</h2>
-    <span style="color: #A40000;" data-name="error"></span><br>
-    <form>
-        <button type="button" data-name="delete">Yes</button>
-        <button type="button" data-name="cancel">No</button>
-    </form>
-</section>
-</main>
-</body>
-
-</html>
+            <option value="WEBCAL">webcal</option>
+          </select>
+          <label for="displayname">Title:</label>
+          <input data-name="displayname" type="text">
+          <label for="description">Description:</label>
+          <input data-name="description" type="text">
+          <label for="source">Source:</label>
+          <input data-name="source" type="url">
+          <label for="color">Color:</label>
+          <input data-name="color" type="color">
+          <br>
+          <span class="error hidden" data-name="error"></span>
+          <br>
+          <button type="submit" class="green" data-name="submit">Save</button>
+          <button type="button" class="red" data-name="cancel">Cancel</button>
+        </form>
+      </section>
+	  
+      <section id="createcollectionscene" class="container hidden">
+        <h1>Create a new Collection</h1>
+        <p>Enter the details of your new collection.</p>
+        <form> Type: <br>
+          <select data-name="type">
+            <option value="ADDRESSBOOK">Address book</option>
+            <option value="CALENDAR_JOURNAL_TASKS">Calendar, journal and tasks</option>
+            <option value="CALENDAR_JOURNAL">Calendar and journal</option>
+            <option value="CALENDAR_TASKS">Calendar and tasks</option>
+            <option value="JOURNAL_TASKS">Journal and tasks</option>
+            <option value="CALENDAR">Calendar</option>
+            <option value="JOURNAL">Journal</option>
+            <option value="TASKS">Tasks</option>
+            <option value="WEBCAL">Webcal</option>
+          </select>
+          <label for="displayname">Title:</label>
+          <input data-name="displayname" type="text">
+          <label for="description">Description:</label>
+          <input data-name="description" type="text">
+          <label for="source">Source:</label>
+          <input data-name="source" type="url">
+          <label for="color">Color:</label>
+          <input data-name="color" type="color">
+          <br>
+          <span class="error" data-name="error"></span>
+          <br>
+          <button type="submit" class="green" data-name="submit">Create</button>
+          <button type="button" class="red" data-name="cancel">Cancel</button>
+        </form>
+      </section>
+	  
+      <section id="uploadcollectionscene" class="container hidden">
+        <h1>Upload Collection</h1>
+        <ul>
+          <li data-name="filetemplate" class="hidden"> Uploading <span data-name="name">name</span>
+            <br>
+            <img data-name="pending" src="css/loading.svg" alt="Please wait...">
+            <span class="successmessage" data-name="success">Uploaded Successfully!</span>
+            <span class="error" data-name="error"></span>
+          </li>
+        </ul>
+        <form>
+          <button type="button" class="red" data-name="close">Close</button>
+        </form>
+      </section>
+	  
+      <section id="deletecollectionscene" class="container hidden">
+        <h1>Delete Collection</h1>
+        <p>Do you want to delete the collection <span class="title" data-name="title">title</span>? </p>
+        <p class="red">WARNING: This action cannot be reversed.</p>
+        <form>
+          <button type="button" class="red" data-name="delete">Delete</button>
+          <button type="button" class="blue" data-name="cancel">Cancel</button>
+        </form>
+        <span class="error hidden" data-name="error"></span>
+        <br>
+      </section>
+	  
+    </main>
+  </body>
+</html>

+ 3 - 0
radicale/xmlutils.py

@@ -178,6 +178,9 @@ def props_from_request(xml_request: Optional[ET.Element]
                     if resource_type.tag == make_clark("C:calendar"):
                     if resource_type.tag == make_clark("C:calendar"):
                         value = "VCALENDAR"
                         value = "VCALENDAR"
                         break
                         break
+                    if resource_type.tag == make_clark("CS:subscribed"):
+                        value = "VSUBSCRIBED"
+                        break
                     if resource_type.tag == make_clark("CR:addressbook"):
                     if resource_type.tag == make_clark("CR:addressbook"):
                         value = "VADDRESSBOOK"
                         value = "VADDRESSBOOK"
                         break
                         break

Некоторые файлы не были показаны из-за большого количества измененных файлов