ticks/
projects.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{TickTick, TickTickError};
4
5use super::{builders::ProjectBuilder, tasks::Task};
6
7/// ID used to identify Projects from TickTick.
8#[derive(Serialize, Deserialize, Default, Debug, Clone)]
9#[serde(transparent)]
10pub struct ProjectID(pub String);
11
12impl ProjectID {
13    pub fn is_empty(&self) -> bool {
14        self.0.is_empty()
15    }
16}
17
18/// ID used to identify Project Groups from TickTick.
19#[derive(Serialize, Deserialize, Default, Debug, Clone)]
20#[serde(transparent)]
21pub struct GroupID(pub String);
22
23impl GroupID {
24    pub fn is_empty(&self) -> bool {
25        self.0.is_empty()
26    }
27}
28
29/// TickTick Project info
30/// [API Reference](https://developer.ticktick.com/docs/index.html#/openapi?id=project-1)
31#[derive(Serialize, Deserialize, Default, Debug)]
32#[serde(rename_all = "camelCase", default)]
33pub struct Project {
34    #[serde(skip)]
35    pub(crate) http_client: reqwest::Client,
36    pub(crate) id: ProjectID,
37    pub name: String,
38    pub color: String,
39    pub sort_order: i64,
40    #[serde(default)]
41    pub closed: bool,
42    pub group_id: GroupID,
43    pub view_mode: ProjectViewMode,
44    pub permission: ProjectUserPermissions,
45    pub kind: ProjectKind,
46}
47
48impl Project {
49    pub fn builder(ticktick: &TickTick, name: String) -> ProjectBuilder {
50        ProjectBuilder::new(ticktick, name)
51    }
52    pub fn get_id(self) -> ProjectID {
53        self.id
54    }
55    pub async fn get_data(&self) -> Result<ProjectData, TickTickError> {
56        let resp = self
57            .http_client
58            .get(format!(
59                "https://ticktick.com/open/v1/project/{}/data",
60                self.id.0
61            ))
62            .send()
63            .await?
64            .error_for_status()?;
65        let mut project_data = resp.json::<ProjectData>().await?;
66        project_data
67            .tasks
68            .iter_mut()
69            .for_each(|task| task.http_client = self.http_client.clone());
70        Ok(project_data)
71    }
72    pub async fn get_all(ticktick: &TickTick) -> Result<Vec<Project>, TickTickError> {
73        ticktick.get_all_projects().await
74    }
75    pub async fn get_tasks(&self) -> Result<Vec<Task>, TickTickError> {
76        Ok(self.get_data().await?.tasks)
77    }
78    pub async fn get_columns(&self) -> Result<Vec<Column>, TickTickError> {
79        Ok(self.get_data().await?.columns)
80    }
81    pub async fn get(ticktick: &TickTick, id: &ProjectID) -> Result<Project, TickTickError> {
82        ticktick.get_project(id).await
83    }
84    /// Send changes made to this project to the TickTick API. Clients will require a refresh/sync for changes to take effect.
85    /// [API Reference](https://developer.ticktick.com/docs/index.html#/openapi?id=update-project)
86    pub async fn publish_changes(&self) -> Result<(), reqwest::Error> {
87        self.http_client
88            .post(format!(
89                "https://ticktick.com/open/v1/project/{}",
90                self.id.0
91            ))
92            .json(self)
93            .send()
94            .await?
95            .text()
96            .await?;
97        Ok(())
98    }
99}
100
101#[derive(Serialize, Deserialize, Default, Debug)]
102#[serde(from = "String", rename_all = "lowercase")]
103pub enum ProjectViewMode {
104    #[default]
105    List,
106    Kanban,
107    Timeline,
108}
109
110impl From<String> for ProjectViewMode {
111    fn from(value: String) -> Self {
112        match value.as_str() {
113            "list" => Self::List,
114            "kanban" => Self::Kanban,
115            "timeline" => Self::Timeline,
116            _ => Self::List,
117        }
118    }
119}
120
121#[derive(Serialize, Deserialize, Default, Debug)]
122#[serde(from = "String", rename_all = "lowercase")]
123pub enum ProjectUserPermissions {
124    #[default]
125    Read,
126    Write,
127    Comment,
128}
129
130impl From<String> for ProjectUserPermissions {
131    fn from(value: String) -> Self {
132        match value.as_str() {
133            "read" => Self::Read,
134            "write" => Self::Write,
135            "comment" => Self::Comment,
136            _ => Self::Read,
137        }
138    }
139}
140
141#[derive(Serialize, Deserialize, Default, Debug)]
142#[serde(from = "String", rename_all = "UPPERCASE")]
143pub enum ProjectKind {
144    #[default]
145    Task,
146    Note,
147}
148
149impl From<String> for ProjectKind {
150    fn from(value: String) -> Self {
151        match value.as_str() {
152            "TASK" => Self::Task,
153            "NOTE" => Self::Note,
154            _ => Self::Task,
155        }
156    }
157}
158
159/// TickTick ProjectData
160/// [API Reference](https://developer.ticktick.com/docs/index.html#/openapi?id=projectdata)
161#[derive(Serialize, Deserialize, Default, Debug)]
162#[serde(rename_all = "camelCase")]
163pub struct ProjectData {
164    pub tasks: Vec<Task>,
165    pub columns: Vec<Column>,
166}
167
168#[derive(Serialize, Deserialize, Default, Debug, Clone)]
169#[serde(transparent)]
170pub struct ColumnID(pub String);
171
172impl ColumnID {
173    pub fn is_empty(&self) -> bool {
174        self.0.is_empty()
175    }
176}
177
178#[derive(Serialize, Deserialize, Default, Debug)]
179#[serde(rename_all = "camelCase")]
180pub struct Column {
181    id: ColumnID,
182    project_id: ProjectID,
183    name: String,
184    sort_order: i64,
185}