Skip to content

Commit a85847a

Browse files
committed
Preserve stats without saved dictation history
1 parent ea1be1d commit a85847a

14 files changed

Lines changed: 244 additions & 25 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
{
2+
"formatVersion": 1,
3+
"database": {
4+
"version": 3,
5+
"identityHash": "61a361b569203bf94d8909941ab6ebfe",
6+
"entities": [
7+
{
8+
"tableName": "dictations",
9+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT NOT NULL, `rawText` TEXT NOT NULL, `wordCount` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `sourceApp` TEXT, `durationMs` INTEGER NOT NULL, `historyVisible` INTEGER NOT NULL)",
10+
"fields": [
11+
{
12+
"fieldPath": "id",
13+
"columnName": "id",
14+
"affinity": "INTEGER",
15+
"notNull": true
16+
},
17+
{
18+
"fieldPath": "text",
19+
"columnName": "text",
20+
"affinity": "TEXT",
21+
"notNull": true
22+
},
23+
{
24+
"fieldPath": "rawText",
25+
"columnName": "rawText",
26+
"affinity": "TEXT",
27+
"notNull": true
28+
},
29+
{
30+
"fieldPath": "wordCount",
31+
"columnName": "wordCount",
32+
"affinity": "INTEGER",
33+
"notNull": true
34+
},
35+
{
36+
"fieldPath": "timestamp",
37+
"columnName": "timestamp",
38+
"affinity": "INTEGER",
39+
"notNull": true
40+
},
41+
{
42+
"fieldPath": "sourceApp",
43+
"columnName": "sourceApp",
44+
"affinity": "TEXT"
45+
},
46+
{
47+
"fieldPath": "durationMs",
48+
"columnName": "durationMs",
49+
"affinity": "INTEGER",
50+
"notNull": true
51+
},
52+
{
53+
"fieldPath": "historyVisible",
54+
"columnName": "historyVisible",
55+
"affinity": "INTEGER",
56+
"notNull": true
57+
}
58+
],
59+
"primaryKey": {
60+
"autoGenerate": true,
61+
"columnNames": [
62+
"id"
63+
]
64+
},
65+
"indices": [
66+
{
67+
"name": "index_dictations_timestamp",
68+
"unique": false,
69+
"columnNames": [
70+
"timestamp"
71+
],
72+
"orders": [],
73+
"createSql": "CREATE INDEX IF NOT EXISTS `index_dictations_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
74+
}
75+
]
76+
},
77+
{
78+
"tableName": "dictionary_words",
79+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `word` TEXT NOT NULL, `category` TEXT NOT NULL)",
80+
"fields": [
81+
{
82+
"fieldPath": "id",
83+
"columnName": "id",
84+
"affinity": "INTEGER",
85+
"notNull": true
86+
},
87+
{
88+
"fieldPath": "word",
89+
"columnName": "word",
90+
"affinity": "TEXT",
91+
"notNull": true
92+
},
93+
{
94+
"fieldPath": "category",
95+
"columnName": "category",
96+
"affinity": "TEXT",
97+
"notNull": true
98+
}
99+
],
100+
"primaryKey": {
101+
"autoGenerate": true,
102+
"columnNames": [
103+
"id"
104+
]
105+
}
106+
}
107+
],
108+
"setupQueries": [
109+
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
110+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '61a361b569203bf94d8909941ab6ebfe')"
111+
]
112+
}
113+
}

app/src/main/java/com/sasayaki/data/db/SasayakiDatabase.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import com.sasayaki.data.db.entity.DictionaryWord
99

1010
@Database(
1111
entities = [Dictation::class, DictionaryWord::class],
12-
version = 2,
12+
version = 3,
1313
exportSchema = true
1414
)
1515
abstract class SasayakiDatabase : RoomDatabase() {

app/src/main/java/com/sasayaki/data/db/dao/DictationDao.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@ interface DictationDao {
1212
@Insert
1313
suspend fun insert(dictation: com.sasayaki.data.db.entity.Dictation): Long
1414

15-
@Query("SELECT id, text, wordCount, timestamp, sourceApp, durationMs FROM dictations ORDER BY timestamp DESC LIMIT :limit")
15+
@Query("SELECT id, text, wordCount, timestamp, sourceApp, durationMs FROM dictations WHERE historyVisible = 1 ORDER BY timestamp DESC LIMIT :limit")
1616
fun getRecent(limit: Int = 500): Flow<List<DictationSummary>>
1717

1818
@Query("SELECT rawText FROM dictations WHERE id = :id")
1919
suspend fun getRawText(id: Long): String?
2020

21-
@Query("DELETE FROM dictations WHERE id = :id")
21+
@Query("UPDATE dictations SET text = '', rawText = '', sourceApp = NULL, historyVisible = 0 WHERE id = :id")
2222
suspend fun delete(id: Long)
2323

24+
@Query("UPDATE dictations SET text = '', rawText = '', sourceApp = NULL, historyVisible = 0 WHERE historyVisible = 1")
25+
suspend fun clearHistory()
26+
2427
@Query("DELETE FROM dictations WHERE id NOT IN (SELECT id FROM dictations ORDER BY timestamp DESC LIMIT :keep)")
2528
suspend fun pruneOldEntries(keep: Int)
2629

app/src/main/java/com/sasayaki/data/db/entity/Dictation.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ data class Dictation(
1414
val wordCount: Int,
1515
val timestamp: Long = System.currentTimeMillis(),
1616
val sourceApp: String? = null,
17-
val durationMs: Long = 0
17+
val durationMs: Long = 0,
18+
val historyVisible: Boolean = true
1819
)

app/src/main/java/com/sasayaki/data/preferences/PreferencesDataStore.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class PreferencesDataStore @Inject constructor(
4343
val PREFERRED_LANGUAGES = stringPreferencesKey("preferred_languages")
4444
val ACTIVE_LANGUAGE = stringPreferencesKey("active_language")
4545
val HISTORY_ENABLED = booleanPreferencesKey("history_enabled")
46+
val KEEP_STATS_WITHOUT_HISTORY = booleanPreferencesKey("keep_stats_without_history")
4647
}
4748

4849
val preferences: Flow<UserPreferences> = context.dataStore.data
@@ -70,7 +71,8 @@ class PreferencesDataStore @Inject constructor(
7071
silenceThresholdMs = prefs[Keys.SILENCE_THRESHOLD_MS] ?: 2000L,
7172
preferredLanguages = parsePreferredLanguages(prefs),
7273
activeLanguage = resolveActiveLanguage(prefs),
73-
historyEnabled = prefs[Keys.HISTORY_ENABLED] ?: true
74+
historyEnabled = prefs[Keys.HISTORY_ENABLED] ?: true,
75+
keepStatsWithoutHistory = prefs[Keys.KEEP_STATS_WITHOUT_HISTORY] ?: false
7476
)
7577
}
7678
.distinctUntilChanged()
@@ -99,13 +101,15 @@ class PreferencesDataStore @Inject constructor(
99101
autoClipboard: Boolean,
100102
vibrateOnRecord: Boolean,
101103
silenceThresholdMs: Long,
102-
historyEnabled: Boolean
104+
historyEnabled: Boolean,
105+
keepStatsWithoutHistory: Boolean
103106
) {
104107
context.dataStore.edit { prefs ->
105108
prefs[Keys.AUTO_CLIPBOARD] = autoClipboard
106109
prefs[Keys.VIBRATE_ON_RECORD] = vibrateOnRecord
107110
prefs[Keys.SILENCE_THRESHOLD_MS] = silenceThresholdMs
108111
prefs[Keys.HISTORY_ENABLED] = historyEnabled
112+
prefs[Keys.KEEP_STATS_WITHOUT_HISTORY] = keepStatsWithoutHistory
109113
}
110114
}
111115

app/src/main/java/com/sasayaki/data/preferences/UserPreferences.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ data class UserPreferences(
1313
val silenceThresholdMs: Long = 2000,
1414
val preferredLanguages: List<String> = emptyList(),
1515
val activeLanguage: String? = null,
16-
val historyEnabled: Boolean = true
16+
val historyEnabled: Boolean = true,
17+
val keepStatsWithoutHistory: Boolean = false
1718
)

app/src/main/java/com/sasayaki/di/DatabaseModule.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,22 @@ object DatabaseModule {
2525
}
2626
}
2727

28+
private val migration2To3 = object : Migration(2, 3) {
29+
override fun migrate(database: SupportSQLiteDatabase) {
30+
database.execSQL(
31+
"ALTER TABLE `dictations` ADD COLUMN `historyVisible` INTEGER NOT NULL DEFAULT 1"
32+
)
33+
}
34+
}
35+
2836
@Provides
2937
@Singleton
3038
fun provideDatabase(@ApplicationContext context: Context): SasayakiDatabase {
3139
return Room.databaseBuilder(
3240
context,
3341
SasayakiDatabase::class.java,
3442
"sasayaki.db"
35-
).addMigrations(migration1To2)
43+
).addMigrations(migration1To2, migration2To3)
3644
.fallbackToDestructiveMigrationOnDowngrade()
3745
.build()
3846
}

app/src/main/java/com/sasayaki/domain/transcription/TranscriptionManager.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,16 @@ class TranscriptionManager @Inject constructor(
5555
val processedText = textProcessor.process(rawText, dictionaryWords, sourceApp)
5656
val wordCount = processedText.split("\\s+".toRegex()).filter { it.isNotBlank() }.size
5757

58-
if (prefs.historyEnabled) {
58+
if (prefs.historyEnabled || prefs.keepStatsWithoutHistory) {
59+
val saveFullHistory = prefs.historyEnabled
5960
dictationDao.insert(
6061
Dictation(
61-
text = processedText,
62-
rawText = rawText,
62+
text = if (saveFullHistory) processedText else "",
63+
rawText = if (saveFullHistory) rawText else "",
6364
wordCount = wordCount,
64-
sourceApp = sourceApp,
65-
durationMs = durationMs
65+
sourceApp = if (saveFullHistory) sourceApp else null,
66+
durationMs = durationMs,
67+
historyVisible = saveFullHistory
6668
)
6769
)
6870
dictationDao.pruneOldEntries(MAX_HISTORY_ENTRIES)

app/src/main/java/com/sasayaki/ui/common/AppChrome.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
66
import androidx.compose.foundation.layout.ColumnScope
77
import androidx.compose.foundation.layout.PaddingValues
88
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.RowScope
910
import androidx.compose.foundation.layout.fillMaxWidth
1011
import androidx.compose.foundation.layout.padding
1112
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -52,7 +53,8 @@ fun SasayakiScaffold(
5253
fun SasayakiTopBar(
5354
title: String,
5455
subtitle: String? = null,
55-
onBack: (() -> Unit)? = null
56+
onBack: (() -> Unit)? = null,
57+
actions: @Composable RowScope.() -> Unit = {}
5658
) {
5759
TopAppBar(
5860
title = {
@@ -76,6 +78,7 @@ fun SasayakiTopBar(
7678
}
7779
}
7880
},
81+
actions = actions,
7982
colors = TopAppBarDefaults.topAppBarColors(
8083
containerColor = MaterialTheme.colorScheme.background,
8184
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer

app/src/main/java/com/sasayaki/ui/history/HistoryScreen.kt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import androidx.compose.material.icons.filled.Delete
2121
import com.sasayaki.ui.theme.SasayakiIcons
2222
import androidx.compose.material3.Icon
2323
import androidx.compose.material3.IconButton
24+
import androidx.compose.material3.AlertDialog
2425
import androidx.compose.material3.MaterialTheme
2526
import androidx.compose.material3.Text
27+
import androidx.compose.material3.TextButton
2628
import androidx.compose.runtime.Composable
2729
import androidx.compose.runtime.getValue
2830
import androidx.compose.runtime.mutableStateOf
@@ -54,13 +56,25 @@ fun HistoryScreen(
5456
) {
5557
val dayGroups by viewModel.dayGroups.collectAsStateWithLifecycle()
5658
val context = LocalContext.current
59+
var showClearHistoryDialog by remember { mutableStateOf(false) }
5760

5861
SasayakiScaffold(
5962
topBar = {
6063
SasayakiTopBar(
6164
title = "History",
6265
subtitle = "Review, copy, or clear previous dictations stored on this device.",
63-
onBack = onBack
66+
onBack = onBack,
67+
actions = {
68+
if (dayGroups.isNotEmpty()) {
69+
IconButton(onClick = { showClearHistoryDialog = true }) {
70+
Icon(
71+
imageVector = Icons.Default.Delete,
72+
contentDescription = "Clear history",
73+
tint = MaterialTheme.colorScheme.error
74+
)
75+
}
76+
}
77+
}
6478
)
6579
}
6680
) { padding ->
@@ -99,6 +113,29 @@ fun HistoryScreen(
99113
}
100114
}
101115
}
116+
117+
if (showClearHistoryDialog) {
118+
AlertDialog(
119+
onDismissRequest = { showClearHistoryDialog = false },
120+
title = { Text("Clear saved history?") },
121+
text = { Text("This removes saved dictation text from the history screen but keeps your usage stats.") },
122+
confirmButton = {
123+
TextButton(
124+
onClick = {
125+
viewModel.clearHistory()
126+
showClearHistoryDialog = false
127+
}
128+
) {
129+
Text("Clear")
130+
}
131+
},
132+
dismissButton = {
133+
TextButton(onClick = { showClearHistoryDialog = false }) {
134+
Text("Cancel")
135+
}
136+
}
137+
)
138+
}
102139
}
103140

104141
@Composable

0 commit comments

Comments
 (0)