mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 20:32:48 +00:00
frontend: Add basic information about collections
This commit is contained in:
@@ -202,14 +202,16 @@ ul.collection-list {
|
||||
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 5%);
|
||||
display: grid;
|
||||
min-height: 80px;
|
||||
height: fit-content;
|
||||
grid-template-areas:
|
||||
". . color-chip"
|
||||
"title comps color-chip"
|
||||
"description description color-chip"
|
||||
"subscription-url subscription-url color-chip"
|
||||
"metadata metadata color-chip"
|
||||
"actions actions color-chip"
|
||||
". . color-chip";
|
||||
grid-template-rows: 12px auto auto auto auto 12px;
|
||||
grid-template-rows: 12px auto auto auto auto auto 12px;
|
||||
grid-template-columns: min-content auto 80px;
|
||||
row-gap: 4px;
|
||||
color: inherit;
|
||||
@@ -251,6 +253,11 @@ ul.collection-list {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
grid-area: metadata;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subscription-url {
|
||||
grid-area: subscription-url;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<h2>{{user.id }}'s Addressbooks</h2>
|
||||
<ul class="collection-list">
|
||||
{% for addressbook in addressbooks %}
|
||||
{% for (meta, addressbook) in addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
@@ -18,6 +18,9 @@
|
||||
<delete-button trash
|
||||
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} objects, {{ meta.deleted_len }} deleted objects
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
@@ -27,7 +30,7 @@
|
||||
{%if !deleted_addressbooks.is_empty() %}
|
||||
<h3>Deleted Addressbooks</h3>
|
||||
<ul class="collection-list">
|
||||
{% for addressbook in deleted_addressbooks %}
|
||||
{% for (meta, addressbook) in deleted_addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
@@ -44,6 +47,9 @@
|
||||
</form>
|
||||
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} objects, {{ meta.deleted_len }} deleted objects
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<h2>{{ user.id }}'s Calendars</h2>
|
||||
<ul class="collection-list">
|
||||
{% for calendar in calendars %}
|
||||
{% for (meta, calendar) in calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
|
||||
@@ -27,6 +27,9 @@
|
||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} objects, {{ meta.deleted_len }} deleted objects
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -37,7 +40,7 @@
|
||||
{%if !deleted_calendars.is_empty() %}
|
||||
<h3>Deleted Calendars</h3>
|
||||
<ul class="collection-list">
|
||||
{% for calendar in deleted_calendars %}
|
||||
{% for (meta, calendar) in deleted_calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
|
||||
@@ -60,6 +63,9 @@
|
||||
</form>
|
||||
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} objects, {{ meta.deleted_len }} deleted objects
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -4,7 +4,7 @@ use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{Extension, extract::Path, response::IntoResponse};
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Addressbook, AddressbookStore, auth::Principal};
|
||||
use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal};
|
||||
|
||||
use crate::pages::user::{Section, UserPage};
|
||||
|
||||
@@ -18,8 +18,8 @@ impl Section for AddressbooksSection {
|
||||
#[template(path = "components/sections/addressbooks_section.html")]
|
||||
pub struct AddressbooksSection {
|
||||
pub user: Principal,
|
||||
pub addressbooks: Vec<Addressbook>,
|
||||
pub deleted_addressbooks: Vec<Addressbook>,
|
||||
pub addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
||||
pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
||||
}
|
||||
|
||||
pub async fn route_addressbooks<AS: AddressbookStore>(
|
||||
@@ -41,11 +41,33 @@ pub async fn route_addressbooks<AS: AddressbookStore>(
|
||||
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut addressbook_infos = vec![];
|
||||
for addressbook in addressbooks {
|
||||
addressbook_infos.push((
|
||||
addr_store
|
||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
addressbook,
|
||||
));
|
||||
}
|
||||
|
||||
let mut deleted_addressbook_infos = vec![];
|
||||
for addressbook in deleted_addressbooks {
|
||||
deleted_addressbook_infos.push((
|
||||
addr_store
|
||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
addressbook,
|
||||
));
|
||||
}
|
||||
|
||||
UserPage {
|
||||
section: AddressbooksSection {
|
||||
user: user.clone(),
|
||||
addressbooks,
|
||||
deleted_addressbooks,
|
||||
addressbooks: addressbook_infos,
|
||||
deleted_addressbooks: deleted_addressbook_infos,
|
||||
},
|
||||
user,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{Extension, extract::Path, response::IntoResponse};
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Calendar, CalendarStore, auth::Principal};
|
||||
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, auth::Principal};
|
||||
|
||||
impl Section for CalendarsSection {
|
||||
fn name() -> &'static str {
|
||||
@@ -17,8 +17,8 @@ impl Section for CalendarsSection {
|
||||
#[template(path = "components/sections/calendars_section.html")]
|
||||
pub struct CalendarsSection {
|
||||
pub user: Principal,
|
||||
pub calendars: Vec<Calendar>,
|
||||
pub deleted_calendars: Vec<Calendar>,
|
||||
pub calendars: Vec<(CollectionMetadata, Calendar)>,
|
||||
pub deleted_calendars: Vec<(CollectionMetadata, Calendar)>,
|
||||
}
|
||||
|
||||
pub async fn route_calendars<CS: CalendarStore>(
|
||||
@@ -35,16 +35,38 @@ pub async fn route_calendars<CS: CalendarStore>(
|
||||
calendars.extend(cal_store.get_calendars(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut calendar_infos = vec![];
|
||||
for calendar in calendars {
|
||||
calendar_infos.push((
|
||||
cal_store
|
||||
.calendar_metadata(&calendar.principal, &calendar.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
calendar,
|
||||
));
|
||||
}
|
||||
|
||||
let mut deleted_calendars = vec![];
|
||||
for group in user.memberships() {
|
||||
deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut deleted_calendar_infos = vec![];
|
||||
for calendar in deleted_calendars {
|
||||
deleted_calendar_infos.push((
|
||||
cal_store
|
||||
.calendar_metadata(&calendar.principal, &calendar.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
calendar,
|
||||
));
|
||||
}
|
||||
|
||||
UserPage {
|
||||
section: CalendarsSection {
|
||||
user: user.clone(),
|
||||
calendars,
|
||||
deleted_calendars,
|
||||
calendars: calendar_infos,
|
||||
deleted_calendars: deleted_calendar_infos,
|
||||
},
|
||||
user,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Error, addressbook::Addressbook};
|
||||
use crate::{CollectionMetadata, Error, addressbook::Addressbook};
|
||||
use async_trait::async_trait;
|
||||
use rustical_ical::AddressObject;
|
||||
|
||||
@@ -35,6 +35,12 @@ pub trait AddressbookStore: Send + Sync + 'static {
|
||||
synctoken: i64,
|
||||
) -> Result<(Vec<AddressObject>, Vec<String>, i64), Error>;
|
||||
|
||||
async fn addressbook_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
) -> Result<CollectionMetadata, Error>;
|
||||
|
||||
async fn get_objects(
|
||||
&self,
|
||||
principal: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Calendar, error::Error};
|
||||
use crate::{Calendar, CollectionMetadata, error::Error};
|
||||
use async_trait::async_trait;
|
||||
use chrono::NaiveDate;
|
||||
use rustical_ical::CalendarObject;
|
||||
@@ -53,6 +53,12 @@ pub trait CalendarStore: Send + Sync + 'static {
|
||||
self.get_objects(principal, cal_id).await
|
||||
}
|
||||
|
||||
async fn calendar_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<CollectionMetadata, Error>;
|
||||
|
||||
async fn get_objects(
|
||||
&self,
|
||||
principal: &str,
|
||||
|
||||
@@ -135,6 +135,20 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn calendar_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<crate::CollectionMetadata, Error> {
|
||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
||||
self.birthday_store
|
||||
.calendar_metadata(principal, cal_id)
|
||||
.await
|
||||
} else {
|
||||
self.cal_store.calendar_metadata(principal, cal_id).await
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
async fn get_objects(
|
||||
&self,
|
||||
|
||||
@@ -16,7 +16,7 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
|
||||
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
|
||||
displayname: addressbook
|
||||
.displayname
|
||||
.map(|name| format!("{} birthdays", name)),
|
||||
.map(|name| format!("{name} birthdays")),
|
||||
order: 0,
|
||||
description: None,
|
||||
color: None,
|
||||
@@ -104,6 +104,17 @@ impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
|
||||
Ok((objects, deleted_objects, new_synctoken))
|
||||
}
|
||||
|
||||
async fn calendar_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<crate::CollectionMetadata, Error> {
|
||||
let cal_id = cal_id
|
||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||
.ok_or(Error::NotFound)?;
|
||||
self.0.addressbook_metadata(principal, cal_id).await
|
||||
}
|
||||
|
||||
async fn get_objects(
|
||||
&self,
|
||||
principal: &str,
|
||||
|
||||
@@ -37,3 +37,11 @@ pub struct CollectionOperation {
|
||||
pub topic: String,
|
||||
pub data: CollectionOperationInfo,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct CollectionMetadata {
|
||||
pub len: usize,
|
||||
pub deleted_len: usize,
|
||||
pub size: u64,
|
||||
pub deleted_size: u64,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const SYNC_NAMESPACE: &str = "github.com/lennart-k/rustical/ns/";
|
||||
|
||||
pub fn format_synctoken(synctoken: i64) -> String {
|
||||
format!("{}{}", SYNC_NAMESPACE, synctoken)
|
||||
format!("{SYNC_NAMESPACE}{synctoken}")
|
||||
}
|
||||
|
||||
pub fn parse_synctoken(synctoken: &str) -> Option<i64> {
|
||||
|
||||
@@ -3,8 +3,8 @@ use async_trait::async_trait;
|
||||
use derive_more::derive::Constructor;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, CollectionOperation, CollectionOperationInfo, Error,
|
||||
synctoken::format_synctoken,
|
||||
Addressbook, AddressbookStore, CollectionMetadata, CollectionOperation,
|
||||
CollectionOperationInfo, Error, synctoken::format_synctoken,
|
||||
};
|
||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
@@ -223,6 +223,28 @@ impl SqliteAddressbookStore {
|
||||
Ok((objects, deleted_objects, new_synctoken))
|
||||
}
|
||||
|
||||
async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
) -> Result<Vec<(u64, bool)>, rustical_store::Error> {
|
||||
struct ObjectEntry {
|
||||
length: u64,
|
||||
deleted: bool,
|
||||
}
|
||||
Ok(sqlx::query_as!(
|
||||
ObjectEntry,
|
||||
"SELECT length(vcf) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM addressobjects WHERE principal = ? AND addressbook_id = ?",
|
||||
principal,
|
||||
addressbook_id
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await.map_err(crate::Error::from)?
|
||||
.into_iter()
|
||||
.map(|row| (row.length, row.deleted))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
@@ -442,6 +464,29 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
Self::_sync_changes(&self.db, principal, addressbook_id, synctoken).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn addressbook_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
) -> Result<CollectionMetadata, rustical_store::Error> {
|
||||
let mut sizes = vec![];
|
||||
let mut deleted_sizes = vec![];
|
||||
for (size, deleted) in Self::_list_objects(&self.db, principal, addressbook_id).await? {
|
||||
if deleted {
|
||||
deleted_sizes.push(size)
|
||||
} else {
|
||||
sizes.push(size)
|
||||
}
|
||||
}
|
||||
Ok(CollectionMetadata {
|
||||
len: sizes.len(),
|
||||
deleted_len: deleted_sizes.len(),
|
||||
size: sizes.iter().sum(),
|
||||
deleted_size: deleted_sizes.iter().sum(),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_objects(
|
||||
&self,
|
||||
|
||||
@@ -5,7 +5,7 @@ use derive_more::derive::Constructor;
|
||||
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
|
||||
use rustical_store::calendar_store::CalendarQuery;
|
||||
use rustical_store::synctoken::format_synctoken;
|
||||
use rustical_store::{Calendar, CalendarStore, Error};
|
||||
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, Error};
|
||||
use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||
@@ -242,6 +242,28 @@ impl SqliteCalendarStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<Vec<(u64, bool)>, rustical_store::Error> {
|
||||
struct ObjectEntry {
|
||||
length: u64,
|
||||
deleted: bool,
|
||||
}
|
||||
Ok(sqlx::query_as!(
|
||||
ObjectEntry,
|
||||
"SELECT length(ics) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM calendarobjects WHERE principal = ? AND cal_id = ?",
|
||||
principal,
|
||||
cal_id
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await.map_err(crate::Error::from)?
|
||||
.into_iter()
|
||||
.map(|row| (row.length, row.deleted))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
@@ -552,6 +574,28 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
Self::_calendar_query(&self.db, principal, cal_id, query).await
|
||||
}
|
||||
|
||||
async fn calendar_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<CollectionMetadata, Error> {
|
||||
let mut sizes = vec![];
|
||||
let mut deleted_sizes = vec![];
|
||||
for (size, deleted) in Self::_list_objects(&self.db, principal, cal_id).await? {
|
||||
if deleted {
|
||||
deleted_sizes.push(size)
|
||||
} else {
|
||||
sizes.push(size)
|
||||
}
|
||||
}
|
||||
Ok(CollectionMetadata {
|
||||
len: sizes.len(),
|
||||
deleted_len: deleted_sizes.len(),
|
||||
size: sizes.iter().sum(),
|
||||
deleted_size: deleted_sizes.iter().sum(),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_objects(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user