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