Skip to main content

aven_core/
local_state.rs

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_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_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/// Database-local sidebar preferences shared across workspaces and projects.
92#[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}