Add events
parent
2281c34529
commit
644800907d
|
|
@ -7,6 +7,7 @@ use kitchen_fridge::traits::CalDavSource;
|
||||||
use kitchen_fridge::calendar::SupportedComponents;
|
use kitchen_fridge::calendar::SupportedComponents;
|
||||||
use kitchen_fridge::Item;
|
use kitchen_fridge::Item;
|
||||||
use kitchen_fridge::Task;
|
use kitchen_fridge::Task;
|
||||||
|
use kitchen_fridge::Event;
|
||||||
use kitchen_fridge::task::CompletionStatus;
|
use kitchen_fridge::task::CompletionStatus;
|
||||||
use kitchen_fridge::CalDavProvider;
|
use kitchen_fridge::CalDavProvider;
|
||||||
use kitchen_fridge::traits::BaseCalendar;
|
use kitchen_fridge::traits::BaseCalendar;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,27 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use kitchen_fridge::cache::Cache;
|
||||||
use kitchen_fridge::client::Client;
|
use kitchen_fridge::client::Client;
|
||||||
use kitchen_fridge::traits::CalDavSource;
|
use kitchen_fridge::traits::CalDavSource;
|
||||||
use kitchen_fridge::CalDavProvider;
|
use kitchen_fridge::CalDavProvider;
|
||||||
use kitchen_fridge::cache::Cache;
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: change these values with yours
|
// TODO: change these values with yours
|
||||||
pub const URL: &str = "https://my.server.com/remote.php/dav/files/john";
|
pub const URL: &str = "https://next.anshorei.me/remote.php/dav/files/test";
|
||||||
pub const USERNAME: &str = "username";
|
pub const USERNAME: &str = "test";
|
||||||
pub const PASSWORD: &str = "secret_password";
|
pub const PASSWORD: &str = "1ETsJD6BPTIMLhmVQcCK";
|
||||||
|
|
||||||
pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/";
|
// pub const EXAMPLE_EXISTING_CALENDAR_URL: &str =
|
||||||
pub const EXAMPLE_CREATED_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_that_we_have_created/";
|
// "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/";
|
||||||
|
pub const EXAMPLE_CREATED_CALENDAR_URL: &str =
|
||||||
|
"https://next.anshorei.me/remote.php/dav/calendars/test/a_calendar_that_we_have_created/";
|
||||||
|
// https://next.anshorei.me/remote.php/dav
|
||||||
|
pub const EXAMPLE_EXISTING_CALENDAR_URL: &str =
|
||||||
|
"https://next.anshorei.me/remote.php/dav/calendars/test/personal/";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
panic!("This file is not supposed to be executed");
|
panic!("This file is not supposed to be executed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Initializes a Provider, and run an initial sync from the server
|
/// Initializes a Provider, and run an initial sync from the server
|
||||||
pub async fn initial_sync(cache_folder: &str) -> CalDavProvider {
|
pub async fn initial_sync(cache_folder: &str) -> CalDavProvider {
|
||||||
let cache_path = Path::new(cache_folder);
|
let cache_path = Path::new(cache_folder);
|
||||||
|
|
@ -33,13 +36,14 @@ pub async fn initial_sync(cache_folder: &str) -> CalDavProvider {
|
||||||
};
|
};
|
||||||
let mut provider = CalDavProvider::new(client, cache);
|
let mut provider = CalDavProvider::new(client, cache);
|
||||||
|
|
||||||
|
|
||||||
let cals = provider.local().get_calendars().await.unwrap();
|
let cals = provider.local().get_calendars().await.unwrap();
|
||||||
println!("---- Local items, before sync -----");
|
println!("---- Local items, before sync -----");
|
||||||
kitchen_fridge::utils::print_calendar_list(&cals).await;
|
kitchen_fridge::utils::print_calendar_list(&cals).await;
|
||||||
|
|
||||||
println!("Starting a sync...");
|
println!("Starting a sync...");
|
||||||
println!("Depending on your RUST_LOG value, you may see more or less details about the progress.");
|
println!(
|
||||||
|
"Depending on your RUST_LOG value, you may see more or less details about the progress."
|
||||||
|
);
|
||||||
// Note that we could use sync_with_feedback() to have better and formatted feedback
|
// Note that we could use sync_with_feedback() to have better and formatted feedback
|
||||||
if provider.sync().await == false {
|
if provider.sync().await == false {
|
||||||
log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync.");
|
log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync.");
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ pub enum SearchFilter {
|
||||||
// /// Return only completed tasks
|
// /// Return only completed tasks
|
||||||
// CompletedTasks,
|
// CompletedTasks,
|
||||||
// /// Return only calendar events
|
// /// Return only calendar events
|
||||||
// Events,
|
Events,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SearchFilter {
|
impl Default for SearchFilter {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,19 @@ use crate::item::SyncStatus;
|
||||||
use crate::resource::Resource;
|
use crate::resource::Resource;
|
||||||
use crate::utils::find_elem;
|
use crate::utils::find_elem;
|
||||||
|
|
||||||
|
// static TASKS_BODY: &str = r#"
|
||||||
|
// <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||||
|
// <d:prop>
|
||||||
|
// <d:getetag />
|
||||||
|
// </d:prop>
|
||||||
|
// <c:filter>
|
||||||
|
// <c:comp-filter name="VCALENDAR">
|
||||||
|
// <c:comp-filter name="VTODO" />
|
||||||
|
// </c:comp-filter>
|
||||||
|
// </c:filter>
|
||||||
|
// </c:calendar-query>
|
||||||
|
// "#;
|
||||||
|
|
||||||
static TASKS_BODY: &str = r#"
|
static TASKS_BODY: &str = r#"
|
||||||
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||||
<d:prop>
|
<d:prop>
|
||||||
|
|
@ -23,7 +36,6 @@ static TASKS_BODY: &str = r#"
|
||||||
</d:prop>
|
</d:prop>
|
||||||
<c:filter>
|
<c:filter>
|
||||||
<c:comp-filter name="VCALENDAR">
|
<c:comp-filter name="VCALENDAR">
|
||||||
<c:comp-filter name="VTODO" />
|
|
||||||
</c:comp-filter>
|
</c:comp-filter>
|
||||||
</c:filter>
|
</c:filter>
|
||||||
</c:calendar-query>
|
</c:calendar-query>
|
||||||
|
|
@ -249,4 +261,3 @@ impl DavCalendar for RemoteCalendar {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
103
src/event.rs
103
src/event.rs
|
|
@ -1,27 +1,100 @@
|
||||||
//! Calendar events (iCal `VEVENT` items)
|
//! Calendar events (iCal `VEVENT` items)
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use ical::property::Property;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
|
use crate::utils::random_url;
|
||||||
|
|
||||||
/// TODO: implement `Event` one day.
|
/// An event
|
||||||
/// This crate currently only supports tasks, not calendar events.
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
|
/// The event URL
|
||||||
|
url: Url,
|
||||||
|
|
||||||
|
/// Persistent, globally unique identifier for the calendar component.
|
||||||
uid: String,
|
uid: String,
|
||||||
name: String,
|
|
||||||
|
/// The sync status of this item
|
||||||
sync_status: SyncStatus,
|
sync_status: SyncStatus,
|
||||||
|
/// The time this event was created.
|
||||||
|
/// This is not required by RFC5545. This will be populated in events created by this crate, but can be None for events coming from a server
|
||||||
|
creation_date: Option<DateTime<Utc>>,
|
||||||
|
last_modified: DateTime<Utc>,
|
||||||
|
|
||||||
|
start_date: DateTime<Utc>,
|
||||||
|
end_date: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// The display name of the event
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// The PRODID, as defined in iCal files
|
||||||
|
ical_prod_id: String,
|
||||||
|
|
||||||
|
/// Extra parameters that have not been parsed from the iCal file (because they're not supported (yet) by this crate).
|
||||||
|
/// They are needed to serialize this item into an equivalent iCal file
|
||||||
|
extra_parameters: Vec<Property>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
pub fn new() -> Self {
|
pub fn new(
|
||||||
unimplemented!();
|
name: String,
|
||||||
|
parent_calendar_url: &Url,
|
||||||
|
start_date: DateTime<Utc>,
|
||||||
|
end_date: DateTime<Utc>,
|
||||||
|
) -> Self {
|
||||||
|
let new_url = random_url(parent_calendar_url);
|
||||||
|
let new_sync_status = SyncStatus::NotSynced;
|
||||||
|
let new_uid = Uuid::new_v4().to_hyphenated().to_string();
|
||||||
|
let new_creation_date = Some(Utc::now());
|
||||||
|
let new_last_modified = Utc::now();
|
||||||
|
let ical_prod_id = crate::ical::default_prod_id();
|
||||||
|
let extra_parameters = Vec::new();
|
||||||
|
Self::new_with_parameters(
|
||||||
|
name,
|
||||||
|
new_uid,
|
||||||
|
new_url,
|
||||||
|
new_sync_status,
|
||||||
|
new_creation_date,
|
||||||
|
new_last_modified,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
ical_prod_id,
|
||||||
|
extra_parameters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_parameters(
|
||||||
|
name: String,
|
||||||
|
uid: String,
|
||||||
|
new_url: Url,
|
||||||
|
sync_status: SyncStatus,
|
||||||
|
creation_date: Option<DateTime<Utc>>,
|
||||||
|
last_modified: DateTime<Utc>,
|
||||||
|
start_date: DateTime<Utc>,
|
||||||
|
end_date: DateTime<Utc>,
|
||||||
|
ical_prod_id: String,
|
||||||
|
extra_parameters: Vec<Property>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
url: new_url,
|
||||||
|
uid,
|
||||||
|
name,
|
||||||
|
sync_status,
|
||||||
|
creation_date,
|
||||||
|
last_modified,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
ical_prod_id,
|
||||||
|
extra_parameters,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn url(&self) -> &Url {
|
pub fn url(&self) -> &Url {
|
||||||
unimplemented!();
|
&self.url
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uid(&self) -> &str {
|
pub fn uid(&self) -> &str {
|
||||||
|
|
@ -33,15 +106,23 @@ impl Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ical_prod_id(&self) -> &str {
|
pub fn ical_prod_id(&self) -> &str {
|
||||||
unimplemented!()
|
&self.ical_prod_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
|
pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
|
||||||
unimplemented!()
|
self.creation_date.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn last_modified(&self) -> &DateTime<Utc> {
|
pub fn last_modified(&self) -> &DateTime<Utc> {
|
||||||
unimplemented!()
|
&self.last_modified
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_date(&self) -> &DateTime<Utc> {
|
||||||
|
&self.start_date
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_date(&self) -> &DateTime<Utc> {
|
||||||
|
&self.end_date
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_status(&self) -> &SyncStatus {
|
pub fn sync_status(&self) -> &SyncStatus {
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,33 @@
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
|
use chrono::{Date, DateTime, NaiveDate, NaiveTime, TimeZone, Utc};
|
||||||
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
|
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::Item;
|
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
use crate::Task;
|
|
||||||
use crate::task::CompletionStatus;
|
use crate::task::CompletionStatus;
|
||||||
use crate::Event;
|
use crate::Event;
|
||||||
|
use crate::Item;
|
||||||
|
use crate::Task;
|
||||||
|
|
||||||
/// Parse an iCal file into the internal representation [`crate::Item`]
|
/// Parse an iCal file into the internal representation [`crate::Item`]
|
||||||
pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<Item, Box<dyn Error>> {
|
pub fn parse(
|
||||||
|
content: &str,
|
||||||
|
item_url: Url,
|
||||||
|
sync_status: SyncStatus,
|
||||||
|
) -> Result<Item, Box<dyn Error>> {
|
||||||
let mut reader = ical::IcalParser::new(content.as_bytes());
|
let mut reader = ical::IcalParser::new(content.as_bytes());
|
||||||
let parsed_item = match reader.next() {
|
let parsed_item = match reader.next() {
|
||||||
None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()),
|
None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()),
|
||||||
Some(item) => match item {
|
Some(item) => match item {
|
||||||
Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()),
|
Err(err) => {
|
||||||
Ok(item) => item,
|
return Err(
|
||||||
|
format!("Unable to parse iCal data for item {}: {}", item_url, err).into(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
Ok(item) => item,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let ical_prod_id = extract_ical_prod_id(&parsed_item)
|
let ical_prod_id = extract_ical_prod_id(&parsed_item)
|
||||||
|
|
@ -29,9 +36,93 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
||||||
.unwrap_or_else(|| super::default_prod_id());
|
.unwrap_or_else(|| super::default_prod_id());
|
||||||
|
|
||||||
let item = match assert_single_type(&parsed_item)? {
|
let item = match assert_single_type(&parsed_item)? {
|
||||||
CurrentType::Event(_) => {
|
CurrentType::Event(event) => {
|
||||||
Item::Event(Event::new())
|
let mut name = None;
|
||||||
},
|
let mut uid = None;
|
||||||
|
let mut last_modified = None;
|
||||||
|
let mut creation_date = None;
|
||||||
|
let mut start_date = None;
|
||||||
|
let mut end_date = None;
|
||||||
|
let mut extra_parameters = Vec::new();
|
||||||
|
|
||||||
|
for prop in &event.properties {
|
||||||
|
match prop.name.as_str() {
|
||||||
|
"SUMMARY" => name = prop.value.clone(),
|
||||||
|
"UID" => uid = prop.value.clone(),
|
||||||
|
"DTSTART" => {
|
||||||
|
start_date = parse_date_time_from_property(&prop.value);
|
||||||
|
}
|
||||||
|
"DTEND" => {
|
||||||
|
end_date = parse_date_time_from_property(&prop.value);
|
||||||
|
}
|
||||||
|
"DTSTAMP" => {
|
||||||
|
last_modified = parse_date_time_from_property(&prop.value);
|
||||||
|
}
|
||||||
|
"LAST-MODIFIED" => {
|
||||||
|
last_modified = parse_date_time_from_property(&prop.value);
|
||||||
|
}
|
||||||
|
"CREATED" => {
|
||||||
|
// The property can be specified once, but is not mandatory
|
||||||
|
creation_date = parse_date_time_from_property(&prop.value)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// This field is not supported. Let's store it anyway, so that we are able to re-create an identical iCal file
|
||||||
|
extra_parameters.push(prop.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let name = match name {
|
||||||
|
Some(name) => name,
|
||||||
|
None => return Err(format!("Missing name for item {}", item_url).into()),
|
||||||
|
};
|
||||||
|
let uid = match uid {
|
||||||
|
Some(uid) => uid,
|
||||||
|
None => return Err(format!("Missing UID for item {}", item_url).into()),
|
||||||
|
};
|
||||||
|
let last_modified = match last_modified {
|
||||||
|
Some(dt) => dt,
|
||||||
|
None => {
|
||||||
|
return Err(format!(
|
||||||
|
"Missing DTSTAMP for item {}, but this is required by RFC5545",
|
||||||
|
item_url
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let start_date = match start_date {
|
||||||
|
Some(dt) => dt,
|
||||||
|
None => {
|
||||||
|
return Err(format!(
|
||||||
|
"Missing DTSTART for item {}, but this is required by RFC5545",
|
||||||
|
item_url
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let end_date = match end_date {
|
||||||
|
Some(dt) => dt,
|
||||||
|
None => {
|
||||||
|
return Err(format!(
|
||||||
|
"Missing DTEND for item {}, but this is required by RFC5545",
|
||||||
|
item_url
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Item::Event(Event::new_with_parameters(
|
||||||
|
name,
|
||||||
|
uid,
|
||||||
|
item_url,
|
||||||
|
sync_status,
|
||||||
|
creation_date,
|
||||||
|
last_modified,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
ical_prod_id,
|
||||||
|
extra_parameters,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
CurrentType::Todo(todo) => {
|
CurrentType::Todo(todo) => {
|
||||||
let mut name = None;
|
let mut name = None;
|
||||||
|
|
@ -39,13 +130,14 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
||||||
let mut completed = false;
|
let mut completed = false;
|
||||||
let mut last_modified = None;
|
let mut last_modified = None;
|
||||||
let mut completion_date = None;
|
let mut completion_date = None;
|
||||||
|
let mut completion_percent = None;
|
||||||
let mut creation_date = None;
|
let mut creation_date = None;
|
||||||
let mut extra_parameters = Vec::new();
|
let mut extra_parameters = Vec::new();
|
||||||
|
|
||||||
for prop in &todo.properties {
|
for prop in &todo.properties {
|
||||||
match prop.name.as_str() {
|
match prop.name.as_str() {
|
||||||
"SUMMARY" => { name = prop.value.clone() },
|
"SUMMARY" => name = prop.value.clone(),
|
||||||
"UID" => { uid = prop.value.clone() },
|
"UID" => uid = prop.value.clone(),
|
||||||
"DTSTAMP" => {
|
"DTSTAMP" => {
|
||||||
// The property can be specified once, but is not mandatory
|
// The property can be specified once, but is not mandatory
|
||||||
// "This property specifies the date and time that the information associated with
|
// "This property specifies the date and time that the information associated with
|
||||||
|
|
@ -53,7 +145,7 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
||||||
// "In the case of an iCalendar object that doesn't specify a "METHOD"
|
// "In the case of an iCalendar object that doesn't specify a "METHOD"
|
||||||
// property [e.g.: VTODO and VEVENT], this property is equivalent to the "LAST-MODIFIED" property".
|
// property [e.g.: VTODO and VEVENT], this property is equivalent to the "LAST-MODIFIED" property".
|
||||||
last_modified = parse_date_time_from_property(&prop.value);
|
last_modified = parse_date_time_from_property(&prop.value);
|
||||||
},
|
}
|
||||||
"LAST-MODIFIED" => {
|
"LAST-MODIFIED" => {
|
||||||
// The property can be specified once, but is not mandatory
|
// The property can be specified once, but is not mandatory
|
||||||
// "This property specifies the date and time that the information associated with
|
// "This property specifies the date and time that the information associated with
|
||||||
|
|
@ -66,11 +158,16 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
||||||
// "This property defines the date and time that a to-do was
|
// "This property defines the date and time that a to-do was
|
||||||
// actually completed."
|
// actually completed."
|
||||||
completion_date = parse_date_time_from_property(&prop.value)
|
completion_date = parse_date_time_from_property(&prop.value)
|
||||||
},
|
}
|
||||||
|
"PERCENT-COMPLETE" => {
|
||||||
|
if let Some(value) = &prop.value {
|
||||||
|
completion_percent = value.parse().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
"CREATED" => {
|
"CREATED" => {
|
||||||
// The property can be specified once, but is not mandatory
|
// The property can be specified once, but is not mandatory
|
||||||
creation_date = parse_date_time_from_property(&prop.value)
|
creation_date = parse_date_time_from_property(&prop.value)
|
||||||
},
|
}
|
||||||
"STATUS" => {
|
"STATUS" => {
|
||||||
// Possible values:
|
// Possible values:
|
||||||
// "NEEDS-ACTION" ;Indicates to-do needs action.
|
// "NEEDS-ACTION" ;Indicates to-do needs action.
|
||||||
|
|
@ -97,7 +194,13 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
||||||
};
|
};
|
||||||
let last_modified = match last_modified {
|
let last_modified = match last_modified {
|
||||||
Some(dt) => dt,
|
Some(dt) => dt,
|
||||||
None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_url).into()),
|
None => {
|
||||||
|
return Err(format!(
|
||||||
|
"Missing DTSTAMP for item {}, but this is required by RFC5545",
|
||||||
|
item_url
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let completion_status = match completed {
|
let completion_status = match completed {
|
||||||
false => {
|
false => {
|
||||||
|
|
@ -105,15 +208,29 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
||||||
log::warn!("Task {:?} has an inconsistent content: its STATUS is not completed, yet it has a COMPLETED timestamp at {:?}", uid, completion_date);
|
log::warn!("Task {:?} has an inconsistent content: its STATUS is not completed, yet it has a COMPLETED timestamp at {:?}", uid, completion_date);
|
||||||
}
|
}
|
||||||
CompletionStatus::Uncompleted
|
CompletionStatus::Uncompleted
|
||||||
},
|
}
|
||||||
true => CompletionStatus::Completed(completion_date),
|
true => CompletionStatus::Completed(completion_date),
|
||||||
};
|
};
|
||||||
|
let completion_percent = completion_percent.unwrap_or(match completed {
|
||||||
|
false => 0f32,
|
||||||
|
true => 100f32,
|
||||||
|
});
|
||||||
|
|
||||||
Item::Task(Task::new_with_parameters(name, uid, item_url, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters))
|
Item::Task(Task::new_with_parameters(
|
||||||
},
|
name,
|
||||||
|
uid,
|
||||||
|
item_url,
|
||||||
|
completion_status,
|
||||||
|
completion_percent,
|
||||||
|
sync_status,
|
||||||
|
creation_date,
|
||||||
|
last_modified,
|
||||||
|
ical_prod_id,
|
||||||
|
extra_parameters,
|
||||||
|
))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// What to do with multiple items?
|
// What to do with multiple items?
|
||||||
if reader.next().map(|r| r.is_ok()) == Some(true) {
|
if reader.next().map(|r| r.is_ok()) == Some(true) {
|
||||||
return Err("Parsing multiple items are not supported".into());
|
return Err("Parsing multiple items are not supported".into());
|
||||||
|
|
@ -122,34 +239,38 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
||||||
Ok(item)
|
Ok(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_date(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
|
||||||
|
NaiveDate::parse_from_str("20220413", "%Y%m%d")
|
||||||
|
.map(|dt| dt.and_time(NaiveTime::from_hms(0, 0, 0)))
|
||||||
|
.map(|dt| DateTime::from_utc(dt, Utc))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_date_time(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
|
fn parse_date_time(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
|
||||||
Utc.datetime_from_str(dt, "%Y%m%dT%H%M%SZ")
|
Utc.datetime_from_str(dt, "%Y%m%dT%H%M%SZ")
|
||||||
.or_else(|_err| Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S") )
|
.or_else(|_err| Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S"))
|
||||||
|
.or_else(|_err| parse_date(dt))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>> {
|
fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>> {
|
||||||
value.as_ref()
|
value.as_ref().and_then(|s| {
|
||||||
.and_then(|s| {
|
|
||||||
parse_date_time(s)
|
parse_date_time(s)
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
log::warn!("Invalid timestamp: {}", s);
|
log::warn!("Invalid timestamp: '{}'", s);
|
||||||
err
|
err
|
||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn extract_ical_prod_id(item: &IcalCalendar) -> Option<&str> {
|
fn extract_ical_prod_id(item: &IcalCalendar) -> Option<&str> {
|
||||||
for prop in &item.properties {
|
for prop in &item.properties {
|
||||||
if &prop.name == "PRODID" {
|
if &prop.name == "PRODID" {
|
||||||
return prop.value.as_ref().map(|s| s.as_str())
|
return prop.value.as_ref().map(|s| s.as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum CurrentType<'a> {
|
enum CurrentType<'a> {
|
||||||
Event(&'a IcalEvent),
|
Event(&'a IcalEvent),
|
||||||
Todo(&'a IcalTodo),
|
Todo(&'a IcalTodo),
|
||||||
|
|
@ -179,7 +300,6 @@ fn assert_single_type<'a>(item: &'a IcalCalendar) -> Result<CurrentType<'a>, Box
|
||||||
return Err("Only a single TODO or a single EVENT is supported".into());
|
return Err("Only a single TODO or a single EVENT is supported".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR
|
const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR
|
||||||
|
|
@ -195,7 +315,7 @@ END:VTODO
|
||||||
END:VCALENDAR
|
END:VCALENDAR
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const EXAMPLE_ICAL_COMPLETED: &str = r#"BEGIN:VCALENDAR
|
const EXAMPLE_ICAL_COMPLETED: &str = r#"BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
PRODID:-//Nextcloud Tasks v0.13.6
|
PRODID:-//Nextcloud Tasks v0.13.6
|
||||||
BEGIN:VTODO
|
BEGIN:VTODO
|
||||||
|
|
@ -211,7 +331,7 @@ END:VTODO
|
||||||
END:VCALENDAR
|
END:VCALENDAR
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE: &str = r#"BEGIN:VCALENDAR
|
const EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE: &str = r#"BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
PRODID:-//Nextcloud Tasks v0.13.6
|
PRODID:-//Nextcloud Tasks v0.13.6
|
||||||
BEGIN:VTODO
|
BEGIN:VTODO
|
||||||
|
|
@ -261,11 +381,17 @@ END:VCALENDAR
|
||||||
|
|
||||||
assert_eq!(task.name(), "Do not forget to do this");
|
assert_eq!(task.name(), "Do not forget to do this");
|
||||||
assert_eq!(task.url(), &item_url);
|
assert_eq!(task.url(), &item_url);
|
||||||
assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com");
|
assert_eq!(
|
||||||
|
task.uid(),
|
||||||
|
"0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com"
|
||||||
|
);
|
||||||
assert_eq!(task.completed(), false);
|
assert_eq!(task.completed(), false);
|
||||||
assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted);
|
assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted);
|
||||||
assert_eq!(task.sync_status(), &sync_status);
|
assert_eq!(task.sync_status(), &sync_status);
|
||||||
assert_eq!(task.last_modified(), &Utc.ymd(2021, 03, 21).and_hms(0, 16, 0));
|
assert_eq!(
|
||||||
|
task.last_modified(),
|
||||||
|
&Utc.ymd(2021, 03, 21).and_hms(0, 16, 0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -274,11 +400,19 @@ END:VCALENDAR
|
||||||
let sync_status = SyncStatus::Synced(version_tag);
|
let sync_status = SyncStatus::Synced(version_tag);
|
||||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||||
|
|
||||||
let item = parse(EXAMPLE_ICAL_COMPLETED, item_url.clone(), sync_status.clone()).unwrap();
|
let item = parse(
|
||||||
|
EXAMPLE_ICAL_COMPLETED,
|
||||||
|
item_url.clone(),
|
||||||
|
sync_status.clone(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let task = item.unwrap_task();
|
let task = item.unwrap_task();
|
||||||
|
|
||||||
assert_eq!(task.completed(), true);
|
assert_eq!(task.completed(), true);
|
||||||
assert_eq!(task.completion_status(), &CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57))));
|
assert_eq!(
|
||||||
|
task.completion_status(),
|
||||||
|
&CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -287,7 +421,12 @@ END:VCALENDAR
|
||||||
let sync_status = SyncStatus::Synced(version_tag);
|
let sync_status = SyncStatus::Synced(version_tag);
|
||||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||||
|
|
||||||
let item = parse(EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, item_url.clone(), sync_status.clone()).unwrap();
|
let item = parse(
|
||||||
|
EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE,
|
||||||
|
item_url.clone(),
|
||||||
|
sync_status.clone(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let task = item.unwrap_task();
|
let task = item.unwrap_task();
|
||||||
|
|
||||||
assert_eq!(task.completed(), true);
|
assert_eq!(task.completed(), true);
|
||||||
|
|
|
||||||
93
src/task.rs
93
src/task.rs
|
|
@ -1,10 +1,10 @@
|
||||||
//! To-do tasks (iCal `VTODO` item)
|
//! To-do tasks (iCal `VTODO` item)
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use ical::property::Property;
|
use ical::property::Property;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
use crate::utils::random_url;
|
use crate::utils::random_url;
|
||||||
|
|
@ -49,11 +49,11 @@ pub struct Task {
|
||||||
last_modified: DateTime<Utc>,
|
last_modified: DateTime<Utc>,
|
||||||
/// The completion status of this task
|
/// The completion status of this task
|
||||||
completion_status: CompletionStatus,
|
completion_status: CompletionStatus,
|
||||||
|
completion_percent: f32,
|
||||||
|
|
||||||
/// The display name of the task
|
/// The display name of the task
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
|
|
||||||
/// The PRODID, as defined in iCal files
|
/// The PRODID, as defined in iCal files
|
||||||
ical_prod_id: String,
|
ical_prod_id: String,
|
||||||
|
|
||||||
|
|
@ -62,7 +62,6 @@ pub struct Task {
|
||||||
extra_parameters: Vec<Property>,
|
extra_parameters: Vec<Property>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Task {
|
impl Task {
|
||||||
/// Create a brand new Task that is not on a server yet.
|
/// Create a brand new Task that is not on a server yet.
|
||||||
/// This will pick a new (random) task ID.
|
/// This will pick a new (random) task ID.
|
||||||
|
|
@ -74,24 +73,45 @@ impl Task {
|
||||||
let new_last_modified = Utc::now();
|
let new_last_modified = Utc::now();
|
||||||
let new_completion_status = if completed {
|
let new_completion_status = if completed {
|
||||||
CompletionStatus::Completed(Some(Utc::now()))
|
CompletionStatus::Completed(Some(Utc::now()))
|
||||||
} else { CompletionStatus::Uncompleted };
|
} else {
|
||||||
|
CompletionStatus::Uncompleted
|
||||||
|
};
|
||||||
|
let new_completion_percent = if completed { 100f32 } else { 0f32 };
|
||||||
let ical_prod_id = crate::ical::default_prod_id();
|
let ical_prod_id = crate::ical::default_prod_id();
|
||||||
let extra_parameters = Vec::new();
|
let extra_parameters = Vec::new();
|
||||||
Self::new_with_parameters(name, new_uid, new_url, new_completion_status, new_sync_status, new_creation_date, new_last_modified, ical_prod_id, extra_parameters)
|
Self::new_with_parameters(
|
||||||
|
name,
|
||||||
|
new_uid,
|
||||||
|
new_url,
|
||||||
|
new_completion_status,
|
||||||
|
new_completion_percent,
|
||||||
|
new_sync_status,
|
||||||
|
new_creation_date,
|
||||||
|
new_last_modified,
|
||||||
|
ical_prod_id,
|
||||||
|
extra_parameters,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new Task instance, that may be synced on the server already
|
/// Create a new Task instance, that may be synced on the server already
|
||||||
pub fn new_with_parameters(name: String, uid: String, new_url: Url,
|
pub fn new_with_parameters(
|
||||||
|
name: String,
|
||||||
|
uid: String,
|
||||||
|
new_url: Url,
|
||||||
completion_status: CompletionStatus,
|
completion_status: CompletionStatus,
|
||||||
sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>,
|
completion_percent: f32,
|
||||||
ical_prod_id: String, extra_parameters: Vec<Property>,
|
sync_status: SyncStatus,
|
||||||
) -> Self
|
creation_date: Option<DateTime<Utc>>,
|
||||||
{
|
last_modified: DateTime<Utc>,
|
||||||
|
ical_prod_id: String,
|
||||||
|
extra_parameters: Vec<Property>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
url: new_url,
|
url: new_url,
|
||||||
uid,
|
uid,
|
||||||
name,
|
name,
|
||||||
completion_status,
|
completion_status,
|
||||||
|
completion_percent,
|
||||||
sync_status,
|
sync_status,
|
||||||
creation_date,
|
creation_date,
|
||||||
last_modified,
|
last_modified,
|
||||||
|
|
@ -100,16 +120,39 @@ impl Task {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn url(&self) -> &Url { &self.url }
|
pub fn url(&self) -> &Url {
|
||||||
pub fn uid(&self) -> &str { &self.uid }
|
&self.url
|
||||||
pub fn name(&self) -> &str { &self.name }
|
}
|
||||||
pub fn completed(&self) -> bool { self.completion_status.is_completed() }
|
pub fn uid(&self) -> &str {
|
||||||
pub fn ical_prod_id(&self) -> &str { &self.ical_prod_id }
|
&self.uid
|
||||||
pub fn sync_status(&self) -> &SyncStatus { &self.sync_status }
|
}
|
||||||
pub fn last_modified(&self) -> &DateTime<Utc> { &self.last_modified }
|
pub fn name(&self) -> &str {
|
||||||
pub fn creation_date(&self) -> Option<&DateTime<Utc>> { self.creation_date.as_ref() }
|
&self.name
|
||||||
pub fn completion_status(&self) -> &CompletionStatus { &self.completion_status }
|
}
|
||||||
pub fn extra_parameters(&self) -> &[Property] { &self.extra_parameters }
|
pub fn completed(&self) -> bool {
|
||||||
|
self.completion_status.is_completed()
|
||||||
|
}
|
||||||
|
pub fn completion_percent(&self) -> f32 {
|
||||||
|
self.completion_percent
|
||||||
|
}
|
||||||
|
pub fn ical_prod_id(&self) -> &str {
|
||||||
|
&self.ical_prod_id
|
||||||
|
}
|
||||||
|
pub fn sync_status(&self) -> &SyncStatus {
|
||||||
|
&self.sync_status
|
||||||
|
}
|
||||||
|
pub fn last_modified(&self) -> &DateTime<Utc> {
|
||||||
|
&self.last_modified
|
||||||
|
}
|
||||||
|
pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
|
||||||
|
self.creation_date.as_ref()
|
||||||
|
}
|
||||||
|
pub fn completion_status(&self) -> &CompletionStatus {
|
||||||
|
&self.completion_status
|
||||||
|
}
|
||||||
|
pub fn extra_parameters(&self) -> &[Property] {
|
||||||
|
&self.extra_parameters
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "integration_tests"))]
|
#[cfg(any(test, feature = "integration_tests"))]
|
||||||
pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
|
pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
|
||||||
|
|
@ -137,7 +180,7 @@ impl Task {
|
||||||
SyncStatus::LocallyDeleted(_) => {
|
SyncStatus::LocallyDeleted(_) => {
|
||||||
log::warn!("Trying to update an item that has previously been deleted. These changes will probably be ignored at next sync.");
|
log::warn!("Trying to update an item that has previously been deleted. These changes will probably be ignored at next sync.");
|
||||||
return;
|
return;
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +188,6 @@ impl Task {
|
||||||
self.last_modified = Utc::now();
|
self.last_modified = Utc::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Rename a task.
|
/// Rename a task.
|
||||||
/// This updates its "last modified" field
|
/// This updates its "last modified" field
|
||||||
pub fn set_name(&mut self, new_name: String) {
|
pub fn set_name(&mut self, new_name: String) {
|
||||||
|
|
@ -169,7 +211,10 @@ impl Task {
|
||||||
}
|
}
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||||
/// Set the completion status, but forces a "master" SyncStatus, just like CalDAV servers are always "masters"
|
/// Set the completion status, but forces a "master" SyncStatus, just like CalDAV servers are always "masters"
|
||||||
pub fn mock_remote_calendar_set_completion_status(&mut self, new_completion_status: CompletionStatus) {
|
pub fn mock_remote_calendar_set_completion_status(
|
||||||
|
&mut self,
|
||||||
|
new_completion_status: CompletionStatus,
|
||||||
|
) {
|
||||||
self.sync_status = SyncStatus::random_synced();
|
self.sync_status = SyncStatus::random_synced();
|
||||||
self.completion_status = new_completion_status;
|
self.completion_status = new_completion_status;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
//! Some utility functions
|
//! Some utility functions
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::io::{stdin, stdout, Read, Write};
|
use std::io::{stdin, stdout, Read, Write};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use minidom::Element;
|
use minidom::Element;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::item::SyncStatus;
|
||||||
use crate::traits::CompleteCalendar;
|
use crate::traits::CompleteCalendar;
|
||||||
use crate::traits::DavCalendar;
|
use crate::traits::DavCalendar;
|
||||||
use crate::Item;
|
use crate::Item;
|
||||||
use crate::item::SyncStatus;
|
|
||||||
|
|
||||||
/// Walks an XML tree and returns every element that has the given name
|
/// Walks an XML tree and returns every element that has the given name
|
||||||
pub fn find_elems<S: AsRef<str>>(root: &Element, searched_name: S) -> Vec<&Element> {
|
pub fn find_elems<S: AsRef<str>>(root: &Element, searched_name: S) -> Vec<&Element> {
|
||||||
|
|
@ -49,14 +49,10 @@ pub fn find_elem<S: AsRef<str>>(root: &Element, searched_name: S) -> Option<&Ele
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn print_xml(element: &Element) {
|
pub fn print_xml(element: &Element) {
|
||||||
let mut writer = std::io::stdout();
|
let mut writer = std::io::stdout();
|
||||||
|
|
||||||
let mut xml_writer = minidom::quick_xml::Writer::new_with_indent(
|
let mut xml_writer = minidom::quick_xml::Writer::new_with_indent(std::io::stdout(), 0x20, 4);
|
||||||
std::io::stdout(),
|
|
||||||
0x20, 4
|
|
||||||
);
|
|
||||||
let _ = element.to_writer(&mut xml_writer);
|
let _ = element.to_writer(&mut xml_writer);
|
||||||
let _ = writer.write(&[0x0a]);
|
let _ = writer.write(&[0x0a]);
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +70,7 @@ where
|
||||||
for (_, item) in map {
|
for (_, item) in map {
|
||||||
print_task(item);
|
print_task(item);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +88,7 @@ where
|
||||||
for (url, version_tag) in map {
|
for (url, version_tag) in map {
|
||||||
println!(" * {} (version {:?})", url, version_tag);
|
println!(" * {} (version {:?})", url, version_tag);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +96,14 @@ where
|
||||||
pub fn print_task(item: &Item) {
|
pub fn print_task(item: &Item) {
|
||||||
match item {
|
match item {
|
||||||
Item::Task(task) => {
|
Item::Task(task) => {
|
||||||
let completion = if task.completed() { "✓" } else { " " };
|
let completion = if task.completed() {
|
||||||
|
String::from("✓")
|
||||||
|
} else if task.completion_percent() > 0f32 {
|
||||||
|
format!("{}%", task.completion_percent())
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let completion = format!("{completion:>width$}", completion = completion, width = 4);
|
||||||
let sync = match task.sync_status() {
|
let sync = match task.sync_status() {
|
||||||
SyncStatus::NotSynced => ".",
|
SyncStatus::NotSynced => ".",
|
||||||
SyncStatus::Synced(_) => "=",
|
SyncStatus::Synced(_) => "=",
|
||||||
|
|
@ -108,12 +111,27 @@ pub fn print_task(item: &Item) {
|
||||||
SyncStatus::LocallyDeleted(_) => "x",
|
SyncStatus::LocallyDeleted(_) => "x",
|
||||||
};
|
};
|
||||||
println!(" {}{} {}\t{}", completion, sync, task.name(), task.url());
|
println!(" {}{} {}\t{}", completion, sync, task.name(), task.url());
|
||||||
},
|
}
|
||||||
|
Item::Event(event) => {
|
||||||
|
let sync = match event.sync_status() {
|
||||||
|
SyncStatus::NotSynced => ".",
|
||||||
|
SyncStatus::Synced(_) => "=",
|
||||||
|
SyncStatus::LocallyModified(_) => "~",
|
||||||
|
SyncStatus::LocallyDeleted(_) => "x",
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
" {}-{}{} {}\t{}",
|
||||||
|
event.start_date(),
|
||||||
|
event.end_date(),
|
||||||
|
sync,
|
||||||
|
event.name(),
|
||||||
|
event.url()
|
||||||
|
)
|
||||||
|
}
|
||||||
_ => return,
|
_ => return,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Compare keys of two hashmaps for equality
|
/// Compare keys of two hashmaps for equality
|
||||||
pub fn keys_are_the_same<T, U, V>(left: &HashMap<T, U>, right: &HashMap<T, V>) -> bool
|
pub fn keys_are_the_same<T, U, V>(left: &HashMap<T, U>, right: &HashMap<T, V>) -> bool
|
||||||
where
|
where
|
||||||
|
|
@ -140,7 +158,6 @@ where
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Wait for the user to press enter
|
/// Wait for the user to press enter
|
||||||
pub fn pause() {
|
pub fn pause() {
|
||||||
let mut stdout = stdout();
|
let mut stdout = stdout();
|
||||||
|
|
@ -149,7 +166,6 @@ pub fn pause() {
|
||||||
stdin().read_exact(&mut [0]).unwrap();
|
stdin().read_exact(&mut [0]).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Generate a random URL with a given prefix
|
/// Generate a random URL with a given prefix
|
||||||
pub fn random_url(parent_calendar: &Url) -> Url {
|
pub fn random_url(parent_calendar: &Url) -> Url {
|
||||||
let random = uuid::Uuid::new_v4().to_hyphenated().to_string();
|
let random = uuid::Uuid::new_v4().to_hyphenated().to_string();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue