mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-26 21:59:25 +00:00
Compare commits
4 Commits
888591c952
...
9538b68e77
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9538b68e77 | ||
|
|
ea5175387b | ||
|
|
0095491a20 | ||
|
|
e9392cc00b |
25
Cargo.lock
generated
25
Cargo.lock
generated
@@ -2974,7 +2974,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -3017,7 +3017,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_caldav"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -3057,7 +3057,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_carddav"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3089,7 +3089,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3114,7 +3114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav_push"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3139,7 +3139,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_frontend"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_web",
|
||||
@@ -3152,6 +3152,7 @@ dependencies = [
|
||||
"headers",
|
||||
"hex",
|
||||
"http",
|
||||
"itertools 0.14.0",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"rand 0.9.2",
|
||||
@@ -3160,6 +3161,7 @@ dependencies = [
|
||||
"rustical_oidc",
|
||||
"rustical_store",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tower",
|
||||
@@ -3168,11 +3170,12 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"vtimezones-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustical_ical"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -3189,7 +3192,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_oidc"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3205,7 +3208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3238,7 +3241,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store_sqlite"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3259,7 +3262,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_xml"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"thiserror 2.0.17",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
description = "A CalDAV server"
|
||||
documentation = "https://lennart-k.github.io/rustical/"
|
||||
|
||||
@@ -39,3 +39,6 @@ headers.workspace = true
|
||||
tower-sessions.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
tower-http = { workspace = true, optional = true }
|
||||
vtimezones-rs.workspace = true
|
||||
serde_json.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
@@ -7,6 +7,11 @@ import { escapeXml } from ".";
|
||||
export class CreateCalendarForm extends LitElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.fetchTimezones()
|
||||
}
|
||||
|
||||
async fetchTimezones() {
|
||||
this.timezones = await getTimezones()
|
||||
}
|
||||
|
||||
protected override createRenderRoot() {
|
||||
@@ -36,6 +41,8 @@ export class CreateCalendarForm extends LitElement {
|
||||
|
||||
dialog: Ref<HTMLDialogElement> = createRef()
|
||||
form: Ref<HTMLFormElement> = createRef()
|
||||
@property()
|
||||
timezones: Array<String> = []
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
@@ -65,7 +72,12 @@ export class CreateCalendarForm extends LitElement {
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map(timezone => html`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
|
||||
@@ -2,11 +2,17 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||
import { escapeXml } from ".";
|
||||
import { getTimezones } from "./timezones.ts";
|
||||
|
||||
@customElement("edit-calendar-form")
|
||||
export class EditCalendarForm extends LitElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.fetchTimezones()
|
||||
}
|
||||
|
||||
async fetchTimezones() {
|
||||
this.timezones = await getTimezones()
|
||||
}
|
||||
|
||||
protected override createRenderRoot() {
|
||||
@@ -36,7 +42,8 @@ export class EditCalendarForm extends LitElement {
|
||||
|
||||
dialog: Ref<HTMLDialogElement> = createRef()
|
||||
form: Ref<HTMLFormElement> = createRef()
|
||||
|
||||
@property()
|
||||
timezones: Array<String> = []
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
@@ -51,7 +58,12 @@ export class EditCalendarForm extends LitElement {
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map(timezone => html`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
|
||||
12
crates/frontend/js-components/lib/timezones.ts
Normal file
12
crates/frontend/js-components/lib/timezones.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
let timezonesPromise = null
|
||||
export async function getTimezones() {
|
||||
timezonesPromise ||= new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let response = await fetch('/frontend/_timezones.json')
|
||||
resolve(await response.json())
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
return await timezonesPromise
|
||||
}
|
||||
@@ -27,6 +27,11 @@ let CreateCalendarForm = class extends i {
|
||||
this.components = /* @__PURE__ */ new Set();
|
||||
this.dialog = e();
|
||||
this.form = e();
|
||||
this.timezones = [];
|
||||
this.fetchTimezones();
|
||||
}
|
||||
async fetchTimezones() {
|
||||
this.timezones = await getTimezones();
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -59,7 +64,12 @@ let CreateCalendarForm = class extends i {
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map((timezone) => x`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
@@ -179,6 +189,9 @@ __decorateClass([
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "components", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "timezones", 2);
|
||||
CreateCalendarForm = __decorateClass([
|
||||
t("create-calendar-form")
|
||||
], CreateCalendarForm);
|
||||
|
||||
@@ -2,6 +2,18 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
|
||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||
let timezonesPromise = null;
|
||||
async function getTimezones() {
|
||||
timezonesPromise ||= new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let response = await fetch("/frontend/_timezones.json");
|
||||
resolve(await response.json());
|
||||
} catch (e2) {
|
||||
reject(e2);
|
||||
}
|
||||
});
|
||||
return await timezonesPromise;
|
||||
}
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __decorateClass = (decorators, target, key, kind) => {
|
||||
@@ -22,6 +34,11 @@ let EditCalendarForm = class extends i {
|
||||
this.components = /* @__PURE__ */ new Set();
|
||||
this.dialog = e();
|
||||
this.form = e();
|
||||
this.timezones = [];
|
||||
this.fetchTimezones();
|
||||
}
|
||||
async fetchTimezones() {
|
||||
this.timezones = await getTimezones();
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -39,7 +56,12 @@ let EditCalendarForm = class extends i {
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map((timezone) => x`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
@@ -150,6 +172,9 @@ __decorateClass([
|
||||
}
|
||||
})
|
||||
], EditCalendarForm.prototype, "components", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], EditCalendarForm.prototype, "timezones", 2);
|
||||
EditCalendarForm = __decorateClass([
|
||||
t("edit-calendar-form")
|
||||
], EditCalendarForm);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,4 +85,3 @@
|
||||
{% endif %}
|
||||
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
||||
<import-calendar-form user="{{ user.id }}"></import-calendar-form>
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ use crate::routes::{
|
||||
app_token::{route_delete_app_token, route_post_app_token},
|
||||
calendar::{route_calendar, route_calendar_restore},
|
||||
login::{route_get_login, route_post_login, route_post_logout},
|
||||
timezones::route_timezones,
|
||||
user::{route_get_home, route_root, route_user_named},
|
||||
};
|
||||
#[cfg(not(feature = "dev"))]
|
||||
@@ -79,7 +80,11 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
||||
.route("/", get(route_root))
|
||||
.nest("/user", user_router)
|
||||
.route("/login", get(route_get_login).post(route_post_login::<AP>))
|
||||
.route("/logout", post(route_post_logout));
|
||||
.route("/logout", post(route_post_logout))
|
||||
.route(
|
||||
"/_timezones.json",
|
||||
get(route_timezones).head(route_timezones),
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
let mut router = router.route_service("/assets/{*file}", EmbedService::<Assets>::default());
|
||||
|
||||
@@ -4,4 +4,5 @@ pub mod app_token;
|
||||
pub mod calendar;
|
||||
pub mod calendars;
|
||||
pub mod login;
|
||||
pub mod timezones;
|
||||
pub mod user;
|
||||
|
||||
39
crates/frontend/src/routes/timezones.rs
Normal file
39
crates/frontend/src/routes/timezones.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use headers::{CacheControl, ContentType, HeaderMapExt};
|
||||
use http::{HeaderMap, HeaderValue, Method};
|
||||
use itertools::Itertools;
|
||||
use std::{sync::LazyLock, time::Duration};
|
||||
|
||||
static VTIMEZONES_JSON: LazyLock<String> = LazyLock::new(|| {
|
||||
serde_json::to_string(
|
||||
&vtimezones_rs::VTIMEZONES
|
||||
.keys()
|
||||
.sorted()
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub async fn route_timezones(method: Method) -> (HeaderMap, &'static str) {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.typed_insert(ContentType::json());
|
||||
headers.insert(
|
||||
"ETag",
|
||||
HeaderValue::from_static(vtimezones_rs::IANA_TZDB_VERSION),
|
||||
);
|
||||
headers.typed_insert(CacheControl::new().with_max_age(Duration::from_hours(2)));
|
||||
|
||||
if method == Method::HEAD {
|
||||
return (headers, "");
|
||||
}
|
||||
(headers, VTIMEZONES_JSON.as_str())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[tokio::test]
|
||||
async fn test_vtimezones_json() -> () {
|
||||
// Since there's an unwrap make sure this doesn't fail
|
||||
assert!(!VTIMEZONES_JSON.as_str().is_empty());
|
||||
|
||||
assert!(route_timezones(Method::HEAD).await.1.is_empty());
|
||||
assert!(!route_timezones(Method::GET).await.1.is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user