1use std::collections::BTreeSet;
2
3use anyhow::Result;
4
5use crate::db::{Database, begin_immediate, get_meta, set_meta};
6use crate::ids::WorkspaceId;
7
8const IOS_QUEUE_WORKSPACE_META_KEY: &str = "ios_queue_workspace_id";
9const ONBOARDING_META_KEY: &str = "tui_onboarding_version";
10const FIRST_LAUNCH_ONBOARDING_VERSION: u32 = 1;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum OnboardingStatus {
14 Due,
15 Complete,
16 Established,
17}
18
19impl Database {
20 pub async fn restore_ios_queue_workspace(&self) -> Result<crate::workspaces::Workspace> {
21 let mut conn = self.acquire_writer().await?;
22 let mut tx = begin_immediate(&mut conn).await?;
23 let workspaces = crate::workspaces::list_workspaces(&mut tx).await?;
24 let stored_id = get_meta(&mut tx, IOS_QUEUE_WORKSPACE_META_KEY)
25 .await?
26 .and_then(|value| value.parse::<WorkspaceId>().ok());
27 let selected = stored_id
28 .and_then(|id| workspaces.iter().find(|workspace| workspace.id == id))
29 .or_else(|| workspaces.first())
30 .cloned()
31 .ok_or_else(|| anyhow::anyhow!("no available workspace"))?;
32 set_meta(&mut tx, IOS_QUEUE_WORKSPACE_META_KEY, selected.id.as_str()).await?;
33 tx.commit().await?;
34 Ok(selected)
35 }
36
37 pub async fn select_ios_queue_workspace(
38 &self,
39 workspace_id: &WorkspaceId,
40 ) -> Result<crate::workspaces::Workspace> {
41 let mut conn = self.acquire_writer().await?;
42 let mut tx = begin_immediate(&mut conn).await?;
43 let workspace = crate::workspaces::workspace_for_id(&mut tx, workspace_id).await?;
44 set_meta(&mut tx, IOS_QUEUE_WORKSPACE_META_KEY, workspace.id.as_str()).await?;
45 tx.commit().await?;
46 Ok(workspace)
47 }
48
49 pub async fn onboarding_status(&self) -> Result<OnboardingStatus> {
50 let mut conn = self.acquire_reader().await?;
51 let marker = get_meta(&mut conn, ONBOARDING_META_KEY).await?;
52 if marker_version(marker.as_deref())
53 .is_some_and(|version| version >= FIRST_LAUNCH_ONBOARDING_VERSION)
54 {
55 return Ok(OnboardingStatus::Complete);
56 }
57
58 let has_tasks: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks)")
59 .fetch_one(&mut *conn)
60 .await?;
61 Ok(if has_tasks {
62 OnboardingStatus::Established
63 } else {
64 OnboardingStatus::Due
65 })
66 }
67
68 pub async fn complete_onboarding(&self) -> Result<()> {
69 let mut conn = self.acquire_writer().await?;
70 let mut tx = begin_immediate(&mut conn).await?;
71 let marker = get_meta(&mut tx, ONBOARDING_META_KEY).await?;
72 if marker_version(marker.as_deref())
73 .is_none_or(|version| version < FIRST_LAUNCH_ONBOARDING_VERSION)
74 {
75 set_meta(
76 &mut tx,
77 ONBOARDING_META_KEY,
78 &FIRST_LAUNCH_ONBOARDING_VERSION.to_string(),
79 )
80 .await?;
81 }
82 tx.commit().await?;
83 Ok(())
84 }
85}
86
87fn marker_version(value: Option<&str>) -> Option<u32> {
88 value?.trim().parse().ok()
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
93pub enum SidebarSection {
94 Views,
95 Scope,
96 Projects,
97}
98
99impl SidebarSection {
100 fn meta_key(self) -> &'static str {
101 match self {
102 Self::Views => "tui_sidebar_views_collapsed",
103 Self::Scope => "tui_sidebar_scope_collapsed",
104 Self::Projects => "tui_sidebar_projects_collapsed",
105 }
106 }
107}
108
109impl Database {
110 pub async fn collapsed_sidebar_sections(&self) -> Result<BTreeSet<SidebarSection>> {
111 let mut conn = self.acquire_reader().await?;
112 let mut sections = BTreeSet::new();
113 for section in [
114 SidebarSection::Views,
115 SidebarSection::Scope,
116 SidebarSection::Projects,
117 ] {
118 if get_meta(&mut conn, section.meta_key())
119 .await?
120 .as_deref()
121 .map(str::trim)
122 == Some("true")
123 {
124 sections.insert(section);
125 }
126 }
127 Ok(sections)
128 }
129
130 pub async fn set_sidebar_section_collapsed(
131 &self,
132 section: SidebarSection,
133 collapsed: bool,
134 ) -> Result<()> {
135 let mut conn = self.acquire_writer().await?;
136 let mut tx = begin_immediate(&mut conn).await?;
137 set_meta(
138 &mut tx,
139 section.meta_key(),
140 if collapsed { "true" } else { "false" },
141 )
142 .await?;
143 tx.commit().await?;
144 Ok(())
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[tokio::test]
153 async fn sidebar_preferences_are_local_and_invalid_values_default_to_expanded() {
154 let database = Database::open(std::path::Path::new(":memory:"))
155 .await
156 .unwrap();
157 let before: i64 = {
158 let mut conn = database.acquire_reader().await.unwrap();
159 sqlx::query_scalar("SELECT COUNT(*) FROM changes")
160 .fetch_one(&mut *conn)
161 .await
162 .unwrap()
163 };
164 database
165 .set_sidebar_section_collapsed(SidebarSection::Scope, true)
166 .await
167 .unwrap();
168 {
169 let mut conn = database.acquire_writer().await.unwrap();
170 set_meta(&mut conn, SidebarSection::Views.meta_key(), "invalid")
171 .await
172 .unwrap();
173 }
174 assert_eq!(
175 database.collapsed_sidebar_sections().await.unwrap(),
176 BTreeSet::from([SidebarSection::Scope])
177 );
178 let mut conn = database.acquire_reader().await.unwrap();
179 let after: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM changes")
180 .fetch_one(&mut *conn)
181 .await
182 .unwrap();
183 assert_eq!(before, after);
184 }
185}