diff --git a/radicale/web/internal_data/css/icons/delete.svg b/radicale/web/internal_data/css/icons/delete.svg new file mode 100644 index 00000000..f8aa7856 --- /dev/null +++ b/radicale/web/internal_data/css/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/radicale/web/internal_data/css/icons/download.svg b/radicale/web/internal_data/css/icons/download.svg new file mode 100644 index 00000000..1ee311b5 --- /dev/null +++ b/radicale/web/internal_data/css/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/radicale/web/internal_data/css/icons/edit.svg b/radicale/web/internal_data/css/icons/edit.svg new file mode 100644 index 00000000..0cfe935e --- /dev/null +++ b/radicale/web/internal_data/css/icons/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/radicale/web/internal_data/css/icons/new.svg b/radicale/web/internal_data/css/icons/new.svg new file mode 100644 index 00000000..d8448b8e --- /dev/null +++ b/radicale/web/internal_data/css/icons/new.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/radicale/web/internal_data/css/icons/upload.svg b/radicale/web/internal_data/css/icons/upload.svg new file mode 100644 index 00000000..2e05b18c --- /dev/null +++ b/radicale/web/internal_data/css/icons/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/radicale/web/internal_data/css/loading.svg b/radicale/web/internal_data/css/loading.svg new file mode 100644 index 00000000..5151c1b6 --- /dev/null +++ b/radicale/web/internal_data/css/loading.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/radicale/web/internal_data/css/logo.svg b/radicale/web/internal_data/css/logo.svg new file mode 100644 index 00000000..546d3d10 --- /dev/null +++ b/radicale/web/internal_data/css/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/radicale/web/internal_data/css/main.css b/radicale/web/internal_data/css/main.css index 726b9a19..ae132f51 100644 --- a/radicale/web/internal_data/css/main.css +++ b/radicale/web/internal_data/css/main.css @@ -1 +1,397 @@ -body{background:#e4e9f6;color:#424247;display:flex;flex-direction:column;font-family:sans;font-size:14pt;line-height:1.4;margin:0;min-height:100vh}a{color:inherit}nav,footer{background:#a40000;color:#fff;padding:0 20%}nav ul,footer ul{display:flex;flex-wrap:wrap;margin:0;padding:0}nav ul li,footer ul li{display:block;padding:0 1em 0 0}nav ul li a,footer ul li a{color:inherit;display:block;padding:1em .5em 1em 0;text-decoration:inherit;transition:.2s}nav ul li a:hover,nav ul li a:focus,footer ul li a:hover,footer ul li a:focus{color:#000;outline:none}header{background:url(logo.svg),linear-gradient(to bottom right, #050a02, #000);background-position:22% 45%;background-repeat:no-repeat;color:#efdddd;font-size:1.5em;min-height:250px;overflow:auto;padding:3em 22%;text-shadow:.2em .2em .2em rgba(0,0,0,0.5)}header>*{padding-left:220px}header h1{font-size:2.5em;font-weight:lighter;margin:.5em 0}main{flex:1}section{padding:0 20% 2em}section:not(:last-child){border-bottom:1px dashed #ccc}section h1{background:linear-gradient(to bottom right, #050a02, #000);color:#e5dddd;font-size:2.5em;margin:0 -33.33% 1em;padding:1em 33.33%}section h2,section h3,section h4{font-weight:lighter;margin:1.5em 0 1em}article{border-top:1px solid transparent;position:relative;margin:3em 0}article aside{box-sizing:border-box;color:#aaa;font-size:.8em;right:-30%;top:.5em;position:absolute}article:before{border-top:1px dashed #ccc;content:"";display:block;left:-33.33%;position:absolute;right:-33.33%}pre{border-radius:3px;background:#000;color:#d3d5db;margin:0 -1em;overflow-x:auto;padding:1em}table{border-collapse:collapse;font-size:.8em;margin:auto}table td{border:1px solid #ccc;padding:.5em}dl dt{margin-bottom:.5em;margin-top:1em}p>code,li>code,dt>code{background:#d1daf0}@media (max-width: 800px){body{font-size:12pt}header,section{padding-left:2em;padding-right:2em}nav,footer{padding-left:0;padding-right:0}nav ul,footer ul{justify-content:center}nav ul li,footer ul li{padding:0 .5em}nav ul li a,footer ul li a{padding:1em 0}header{background-position:50% 30px,0 0;padding-bottom:0;padding-top:330px;text-align:center}header>*{margin:0;padding-left:0}section h1{margin:0 -.8em 1.3em;padding:.5em 0;text-align:center}article aside{top:.5em;right:-1.5em}article:before{left:-2em;right:-2em}} +body{ + background: #ffffff; + color: #424247; + font-family: sans-serif; + font-size: 14pt; + margin: 0; + min-height: 100vh; + display: flex; + flex-wrap: wrap; + flex-direction: row; + align-content: center; + align-items: flex-start; + justify-content: space-around; +} + +main{ + width: 100%; +} + +.container{ + height: auto; + min-height: 450px; + width: 350px; + transition: .2s; + overflow: hidden; + padding: 20px 40px; + background: #fff; + border: 1px solid #dadce0; + border-radius: 8px; + display: block; + flex-shrink: 0; + margin: 0 auto; +} + +.container h1{ + margin: 0; + width: 100%; + text-align: center; + color: #484848; +} + +#loginscene input{ +} + + +#loginscene .logocontainer{ + width: 100%; + text-align: center; +} + +#loginscene .logocontainer img{ + width: 75px; +} + +#loginscene h1{ + text-align: center; + font-family: sans-serif; + font-weight: normal; +} + +#loginscene button{ + float: right; +} + +#loadingscene{ + width: 100%; + height: 100%; + background: rgb(237 237 237); + position: absolute; + top: 0; + left: 0; + display: flex; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + flex-direction: column; + overflow: hidden; + z-index: 999; +} + +#loadingscene h2{ + font-size: 2em; + font-weight: bold; +} + +#logoutview{ + width: 100%; + display: block; + background: white; + text-align: center; + padding: 10px 0px; + color: #666; + border-bottom: 2px solid #dadce0; + position: fixed; +} + +#logoutview span{ + width: calc(100% - 60px); + display: inline-block; +} + +#logoutview a{ + color: white; + text-decoration: none; + padding: 3px 10px; + position: absolute; + right: 25px; + border-radius: 4px; +} + +#collectionsscene{ + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-content: flex-start; + align-items: center; + margin-top: 50px; + width: 100%; + height: calc(100vh - 50px); +} + +#collectionsscene article{ + width: 250px; + background: rgb(250, 250, 250); + border-radius: 8px; + box-shadow: 2px 2px 3px #0000001a; + border: 1px solid #dadce0; + padding: 5px 10px; + padding-top: 0; + margin: 10px; + float: left; + height: 350px; + overflow: hidden; +} + +#collectionsscene article .colorbar{ + width: 500%; + height: 15px; + margin: 0px -100%; + background: #000000; +} + +#collectionsscene article .title{ + width: 100%; + text-align: center; + font-size: 1.5em; + display: block; + padding: 10px 0; + margin: 0; +} + +#collectionsscene article small{ + font-size: 15px; + float: left; + font-weight: normal; + font-style: italic; + padding-bottom: 10px; + width: 100%; + text-align: center; +} + +#collectionsscene article input[type=text]{ + margin-bottom: 0 !important; +} + +#collectionsscene article p{ + font-size: 1em; + max-height: 130px; + overflow: overlay; +} + +#collectionsscene article:hover ul{ + display: flex !important; +} + +#collectionsscene ul{ + display: none; + justify-content: space-evenly; + width: 60%; + margin: 0 20%; + padding: 0; +} + +#collectionsscene li{ + list-style: none; + display: block; +} + +#collectionsscene li a{ + text-decoration: none !important; + padding: 5px; + float: left; + border-radius: 5px; + width: 25px; + height: 25px; + text-align: center; +} + +#editcollectionscene p span{ + word-wrap:break-word; + font-weight: bold; + color: #4e9a06; +} + +#deletecollectionscene p span{ + word-wrap:break-word; + font-weight: bold; + color: #a40000; +} + +#uploadcollectionscene ul{ + margin: 10px -30px; + max-height: 600px; + overflow: overlay; +} + +#uploadcollectionscene li{ + border-bottom: 1px dashed #d5d5d5; + margin-bottom: 10px; + padding-bottom: 10px; +} + +#uploadcollectionscene .successmessage{ + color: #4e9a06; + width: 100%; + text-align: center; + display: block; + margin-top: 15px; +} + +.fabcontainer{ + display: flex; + flex-direction: column-reverse; + position: fixed; + bottom: 5px; + right: 0; +} + +.fabcontainer a{ + width: 30px; + height: 30px; + text-decoration: none; + color: white; + border: none !important; + border-radius: 100%; + margin: 5px 10px; + background: black; + text-align: center; + display: flex; + align-content: center; + justify-content: center; + align-items: center; + font-size: 30px; + padding: 10px; + box-shadow: 2px 2px 7px #000000d6; +} + +.title{ + word-wrap: break-word; + font-weight: bold; +} + +.icon{ + width: 100%; + height: 100%; + filter: invert(1); +} + +.smalltext{ + font-size: 75% !important; +} + +.error{ + width: 100%; + display: block; + text-align: center; + color: rgb(217,48,37); + font-family: sans-serif; + clear: both; + padding-top: 15px; +} + +.error::before{ + content: "!"; + height: 1em; + color: white; + background: rgb(217,48,37); + font-weight: bold; + border-radius: 100%; + display: inline-block; + width: 1.1em; + margin-right: 5px; + font-size: 1em; + text-align: center; +} + +button{ + font-size: 1em; + padding: 7px 21px; + color: white; + border-radius: 4px; + float: right; + margin-left: 10px; + background: black; + cursor: pointer; +} + +input, select{ + width: 100%; + height: 3em; + border-style: solid; + border-color: #e6e6e6; + border-width: 1px; + border-radius: 7px; + margin-bottom: 25px; + padding-left: 15px; + padding-right: 15px; + outline: none !important; +} + +input[type=text], input[type=password]{ + width: calc(100% - 30px); +} + +input:active, input:focus, input:focus-visible{ + border-color: #2494fe !important; + border-width: 1px !important; +} + +p.red, span.red{ + color: #b50202; +} + +button.red, a.red{ + background: #b50202; + border: 1px solid #a40000; +} + +button.red:hover, a.red:hover{ + background: #a40000; +} + +button.red:active, a.red:active{ + background: #8f0000; +} + +button.green, a.green{ + background: #4e9a06; + border: 1px solid #377200; +} + +button.green:hover, a.green:hover{ + background: #377200; +} + +button.green:active, a.green:active{ + background: #285200; +} + +button.blue, a.blue{ + background: #2494fe; + border: 1px solid #055fb5; +} + +button.blue:hover, a.blue:hover{ + background: #1578d6; + cursor: pointer !important; +} + +button.blue:active, a.blue:active{ + background: #055fb5; + cursor: pointer !important; +} + +@media only screen and (max-width: 600px) { + #collectionsscene{ + flex-direction: column !important; + flex-wrap: nowrap; + } + + #collectionsscene article{ + height: auto; + min-height: 350px; + } + + .container{ + max-width: 280px !important; + } + + #collectionsscene ul{ + display: flex !important; + } + + #logoutview span{ + text-align: left; + } +} \ No newline at end of file diff --git a/radicale/web/internal_data/fn.js b/radicale/web/internal_data/fn.js index 82651a36..3b3d6d6d 100644 --- a/radicale/web/internal_data/fn.js +++ b/radicale/web/internal_data/fn.js @@ -1,6 +1,6 @@ /** * This file is part of Radicale Server - Calendar Server - * Copyright © 2017-2018 Unrud + * Copyright © 2017-2024 Unrud * * 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 @@ -28,7 +28,7 @@ const SERVER = location.origin; * @const * @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 @@ -63,6 +63,7 @@ const CollectionType = { CALENDAR: "CALENDAR", JOURNAL: "JOURNAL", TASKS: "TASKS", + WEBCAL: "WEBCAL", is_subset: function(a, b) { let components = a.split("_"); 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) { union.push(this.TASKS); } + if (a.search(this.WEBCAL) !== -1 || b.search(this.WEBCAL) !== -1) { + union.push(this.WEBCAL); + } return union.join("_"); } }; @@ -102,12 +106,13 @@ const CollectionType = { * @param {string} description * @param {string} color */ -function Collection(href, type, displayname, description, color) { +function Collection(href, type, displayname, description, color, source) { this.href = href; this.type = type; this.displayname = displayname; this.color = color; 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 calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-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_element = response.querySelector(components_query); let href = href_element ? href_element.textContent : ""; @@ -190,11 +196,17 @@ function get_collections(user, password, collection, callback) { let type = ""; let color = ""; let description = ""; + let source = ""; if (resourcetype_element) { if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) { type = CollectionType.ADDRESSBOOK; color = addressbookcolor_element ? addressbookcolor_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")) { if (components_element) { 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) { - 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) { @@ -235,11 +247,15 @@ function get_collections(user, password, collection, callback) { } }; request.send('' + - '' + + 'xmlns:RADICALE="http://radicale.org/ns/"' + + '>' + '' + '' + '' + @@ -248,6 +264,7 @@ function get_collections(user, password, collection, callback) { '' + '' + '' + + '' + '' + ''); return request; @@ -329,12 +346,18 @@ function create_edit_collection(user, password, collection, create, callback) { let addressbook_color = ""; let calendar_description = ""; let addressbook_description = ""; + let calendar_source = ""; let resourcetype; let components = ""; if (collection.type === CollectionType.ADDRESSBOOK) { addressbook_color = escape_xml(collection.color + (collection.color ? "ff" : "")); addressbook_description = escape_xml(collection.description); resourcetype = ''; + } else if (collection.type === CollectionType.WEBCAL) { + calendar_color = escape_xml(collection.color + (collection.color ? "ff" : "")); + calendar_description = escape_xml(collection.description); + resourcetype = ''; + calendar_source = collection.source; } else { calendar_color = escape_xml(collection.color + (collection.color ? "ff" : "")); 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"; request.send('' + - '<' + 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/">' + '' + '' + (create ? '' + resourcetype + '' : '') + @@ -361,6 +384,7 @@ function create_edit_collection(user, password, collection, create, callback) { (addressbook_color ? '' + addressbook_color + '' : '') + (addressbook_description ? '' + addressbook_description + '' : '') + (calendar_description ? '' + calendar_description + '' : '') + + (calendar_source ? '' + calendar_source + '' : '') + '' + '' + (!create ? ('' + @@ -495,7 +519,12 @@ function LoginScene() { function fill_form() { user_form.value = user; 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() { @@ -507,7 +536,7 @@ function LoginScene() { // setup logout logout_view.classList.remove("hidden"); logout_btn.onclick = onlogout; - logout_user_form.textContent = user; + logout_user_form.textContent = user + "'s Collections"; // Fetch principal let loading_scene = new LoadingScene(); push_scene(loading_scene, false); @@ -683,12 +712,11 @@ function CollectionsScene(user, password, collection, onerror) { let color_form = node.querySelector("[data-name=color]"); let delete_btn = node.querySelector("[data-name=delete]"); let edit_btn = node.querySelector("[data-name=edit]"); + let download_btn = node.querySelector("[data-name=download]"); 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.union(e, CollectionType.JOURNAL), 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; + if(title_form.textContent.length > 30){ + title_form.classList.add("smalltext"); + } description_form.textContent = collection.description; + if(description_form.textContent.length > 150){ + description_form.classList.add("smalltext"); + } 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);}; edit_btn.onclick = function() {return onedit(collection);}; node.classList.remove("hidden"); @@ -945,9 +979,15 @@ function DeleteCollectionScene(user, password, collection) { scene_index = scene_stack.length - 1; html_scene.classList.remove("hidden"); title_form.textContent = collection.displayname || collection.href; - error_form.textContent = error ? "Error: " + error : ""; delete_btn.onclick = ondelete; 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() { 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 error_form = html_scene.querySelector("[data-name=error]"); 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_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_label = html_scene.querySelector("label[for=type]"); 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 cancel_btn = html_scene.querySelector("[data-name=cancel]"); + /** @type {?number} */ let scene_index = null; /** @type {?XMLHttpRequest} */ let create_edit_req = null; let error = ""; @@ -1003,6 +1050,7 @@ function CreateEditCollectionScene(user, password, collection) { let href = edit ? collection.href : collection.href + random_uuid() + "/"; let displayname = edit ? collection.displayname : ""; let description = edit ? collection.description : ""; + let source = edit ? collection.source : ""; let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS; let color = edit && collection.color ? collection.color : "#" + random_hex(6); @@ -1022,6 +1070,7 @@ function CreateEditCollectionScene(user, password, collection) { function read_form() { displayname = displayname_form.value; description = description_form.value; + source = source_form.value; type = type_form.value; color = color_form.value; } @@ -1029,9 +1078,17 @@ function CreateEditCollectionScene(user, password, collection) { function fill_form() { displayname_form.value = displayname; description_form.value = description; + source_form.value = source; type_form.value = type; 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() { @@ -1049,7 +1106,7 @@ function CreateEditCollectionScene(user, password, collection) { } let loading_scene = new LoadingScene(); 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) { if (scene_index === null) { return; @@ -1082,6 +1139,16 @@ function CreateEditCollectionScene(user, password, collection) { 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.release(); scene_index = scene_stack.length - 1; diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html index ea294266..fce880d3 100644 --- a/radicale/web/internal_data/index.html +++ b/radicale/web/internal_data/index.html @@ -1,138 +1,179 @@ - - - - - - -Radicale Web Interface - - - - - - - - -
-
-

Loading

-

Please wait...

- -
- - - - - - - - - - - - -
- - - +
+ +
+ + + + + + + \ No newline at end of file diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py index da59c08b..4b9c51bf 100644 --- a/radicale/xmlutils.py +++ b/radicale/xmlutils.py @@ -178,6 +178,9 @@ def props_from_request(xml_request: Optional[ET.Element] if resource_type.tag == make_clark("C:calendar"): value = "VCALENDAR" break + if resource_type.tag == make_clark("CS:subscribed"): + value = "VSUBSCRIBED" + break if resource_type.tag == make_clark("CR:addressbook"): value = "VADDRESSBOOK" break