1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-07-02 16:58:30 +00:00

Merge pull request #1329 from MatthewHana/master

New Web UI
This commit is contained in:
Peter Bieringer 2024-03-05 20:55:46 +01:00 committed by GitHub
commit 71fd91631e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 730 additions and 153 deletions

View file

@ -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>

After

Width:  |  Height:  |  Size: 418 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 322 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 499 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 305 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 320 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -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>

After

Width:  |  Height:  |  Size: 564 B

View file

@ -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;
}
}

View file

@ -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; color_form.style.background = collection.color;
} else {
color_form.classList.add("hidden");
} }
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.value = href;
url_form.textContent = 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;

View file

@ -1,138 +1,179 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head> <meta charset="utf-8">
<meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Radicale Web Interface</title>
<script src="fn.js"></script> <link href="css/main.css" type="text/css" media="screen" rel="stylesheet">
<title>Radicale Web Interface</title> <link href="css/icon.png" type="image/png" rel="icon">
<link href="css/main.css" media="screen" rel="stylesheet"> <style>.hidden {display: none !important;}</style>
<link href="css/icon.png" type="image/png" rel="icon"> <script src="fn.js"></script>
<style> </head>
.hidden {display:none;}
</style> <body>
</head> <nav id="logoutview" class="hidden">
<span data-name="user" style="word-wrap:break-word;"></span>
<body> <a href="" class="red" data-name="link" title="Logout">Logout</a>
<nav> </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> <main>
</ul> <section id="loadingscene">
</nav> <img src="css/loading.svg" alt="Loading">
<h2>Loading</h2>
<main> <p>Please wait...</p>
<section id="loadingscene"> <noscript>JavaScript is required</noscript>
<h1>Loading</h1> </section>
<p>Please wait...</p>
<noscript>JavaScript is required</noscript> <section id="loginscene" class="container hidden">
</section> <div class="logocontainer">
<img src="css/logo.svg" alt="Radicale">
<section id="loginscene" class="hidden"> </div>
<h1>Login</h1> <h1>Sign in</h1>
<form data-name="form"> <br>
<input data-name="user" type="text" placeholder="Username"><br> <form data-name="form">
<input data-name="password" type="password" placeholder="Password"><br> <input data-name="user" type="text" placeholder="Username">
<span style="color: #A40000;" data-name="error"></span><br> <input data-name="password" type="password" placeholder="Password">
<button type="submit">Next</button> <button class="green" type="submit">Next</button>
</form> <span class="error" data-name="error"></span>
</section> </form>
</section>
<section id="collectionsscene" class="hidden">
<h1>Collections</h1> <section id="collectionsscene" class="hidden">
<ul> <div class="fabcontainer">
<li><a href="" data-name="new">Create new addressbook or calendar</a></li> <a href="" class="green" data-name="new" title="Create a new addressbook or calendar">
<li><a href="" data-name="upload">Upload addressbook or calendar</a></li> <img src="css/icons/new.svg" class="icon" alt="">
</ul> </a>
<article data-name="collectiontemplate" class="hidden"> <a href="" class="blue" data-name="upload" title="Upload an addressbook or calendar">
<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> <img src="css/icons/upload.svg" class="icon" alt="⬆️">
<span data-name="description" style="word-wrap:break-word;">Description</span> </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="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 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> <ul>
<li>URL: <a data-name="url" style="word-wrap:break-word;">url</a></li> <li data-name="filetemplate" class="hidden"> Uploading <span data-name="name">name</span>
<li><a href="" data-name="edit">Edit</a></li> <br>
<li><a href="" data-name="delete">Delete</a></li> <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> </ul>
</article> <form>
</section> <button type="button" class="red" data-name="close">Close</button>
</form>
<section id="editcollectionscene" class="hidden"> </section>
<h1>Edit collection</h1>
<h2>Edit <span data-name="title" style="word-wrap:break-word;font-weight:bold;">title</span>:</h2> <section id="deletecollectionscene" class="container hidden">
<form> <h1>Delete Collection</h1>
Title:<br> <p>Do you want to delete the collection <span class="title" data-name="title">title</span>? </p>
<input data-name="displayname" type="text"><br> <p class="red">WARNING: This action cannot be reversed.</p>
Description:<br> <form>
<input data-name="description" type="text"><br> <button type="button" class="red" data-name="delete">Delete</button>
Type:<br> <button type="button" class="blue" data-name="cancel">Cancel</button>
<select data-name="type"> </form>
<option value="ADDRESSBOOK">addressbook</option> <span class="error hidden" data-name="error"></span>
<option value="CALENDAR_JOURNAL_TASKS">calendar, journal and tasks</option> <br>
<option value="CALENDAR_JOURNAL">calendar and journal</option> </section>
<option value="CALENDAR_TASKS">calendar and tasks</option>
<option value="JOURNAL_TASKS">journal and tasks</option> </main>
<option value="CALENDAR">calendar</option> </body>
<option value="JOURNAL">journal</option> </html>
<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">
<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">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>

View file

@ -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