Skip to content

Commit e5f7996

Browse files
committed
fix: corrected DTOs
1 parent 23dde58 commit e5f7996

12 files changed

Lines changed: 572 additions & 254 deletions

File tree

mobile/androidApp/src/main/kotlin/app/myfaq/android/screens/SettingsScreen.kt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,63 @@ import android.widget.Toast
44
import androidx.compose.foundation.clickable
55
import androidx.compose.foundation.layout.Column
66
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.material3.AlertDialog
78
import androidx.compose.material3.CenterAlignedTopAppBar
89
import androidx.compose.material3.ExperimentalMaterial3Api
910
import androidx.compose.material3.HorizontalDivider
1011
import androidx.compose.material3.ListItem
1112
import androidx.compose.material3.MaterialTheme
1213
import androidx.compose.material3.Scaffold
1314
import androidx.compose.material3.Text
15+
import androidx.compose.material3.TextButton
1416
import androidx.compose.runtime.Composable
1517
import androidx.compose.runtime.collectAsState
1618
import androidx.compose.runtime.getValue
19+
import androidx.compose.runtime.mutableStateOf
20+
import androidx.compose.runtime.remember
21+
import androidx.compose.runtime.setValue
1722
import androidx.compose.ui.Modifier
1823
import androidx.compose.ui.platform.LocalContext
1924
import app.myfaq.shared.data.ActiveInstanceManager
25+
import app.myfaq.shared.data.MyFaqDatabase
2026
import org.koin.compose.koinInject
2127

2228
@OptIn(ExperimentalMaterial3Api::class)
2329
@Composable
2430
fun SettingsScreen(
2531
onSwitchInstance: () -> Unit,
2632
aim: ActiveInstanceManager = koinInject(),
33+
db: MyFaqDatabase = koinInject(),
2734
) {
2835
val context = LocalContext.current
2936
val activeInstance by aim.activeInstance.collectAsState()
37+
var showDeleteConfirmation by remember { mutableStateOf(false) }
38+
39+
// Delete confirmation dialog
40+
if (showDeleteConfirmation) {
41+
activeInstance?.let { instance ->
42+
AlertDialog(
43+
onDismissRequest = { showDeleteConfirmation = false },
44+
title = { Text("Delete Instance") },
45+
text = { Text("Are you sure you want to delete \"${instance.displayName}\"? This cannot be undone.") },
46+
confirmButton = {
47+
TextButton(onClick = {
48+
db.instancesQueries.deleteById(instance.id)
49+
aim.clear()
50+
showDeleteConfirmation = false
51+
onSwitchInstance()
52+
}) {
53+
Text("Delete", color = MaterialTheme.colorScheme.error)
54+
}
55+
},
56+
dismissButton = {
57+
TextButton(onClick = { showDeleteConfirmation = false }) {
58+
Text("Cancel")
59+
}
60+
},
61+
)
62+
}
63+
}
3064

3165
Scaffold(
3266
topBar = {
@@ -65,6 +99,20 @@ fun SettingsScreen(
6599
)
66100
HorizontalDivider()
67101

102+
// Delete instance
103+
if (activeInstance != null) {
104+
ListItem(
105+
headlineContent = {
106+
Text(
107+
text = "Delete instance",
108+
color = MaterialTheme.colorScheme.error,
109+
)
110+
},
111+
modifier = Modifier.clickable { showDeleteConfirmation = true },
112+
)
113+
HorizontalDivider()
114+
}
115+
68116
// App version
69117
ListItem(
70118
headlineContent = { Text("App version") },

mobile/androidApp/src/main/kotlin/app/myfaq/android/screens/WorkspacesScreen.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.items
1616
import androidx.compose.material.icons.Icons
1717
import androidx.compose.material.icons.filled.Add
1818
import androidx.compose.material.icons.filled.Delete
19+
import androidx.compose.material3.AlertDialog
1920
import androidx.compose.material3.Card
2021
import androidx.compose.material3.CenterAlignedTopAppBar
2122
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -25,9 +26,13 @@ import androidx.compose.material3.IconButton
2526
import androidx.compose.material3.MaterialTheme
2627
import androidx.compose.material3.Scaffold
2728
import androidx.compose.material3.Text
29+
import androidx.compose.material3.TextButton
2830
import androidx.compose.runtime.Composable
2931
import androidx.compose.runtime.collectAsState
3032
import androidx.compose.runtime.getValue
33+
import androidx.compose.runtime.mutableStateOf
34+
import androidx.compose.runtime.remember
35+
import androidx.compose.runtime.setValue
3136
import androidx.compose.ui.Alignment
3237
import androidx.compose.ui.Modifier
3338
import androidx.compose.ui.unit.dp
@@ -46,6 +51,29 @@ fun WorkspacesScreen(
4651
) {
4752
val vm = WorkspacesViewModel(db, aim)
4853
val instances by vm.instances.collectAsState()
54+
var instanceToDelete by remember { mutableStateOf<Instance?>(null) }
55+
56+
// Confirmation dialog
57+
instanceToDelete?.let { instance ->
58+
AlertDialog(
59+
onDismissRequest = { instanceToDelete = null },
60+
title = { Text("Delete Instance") },
61+
text = { Text("Are you sure you want to delete \"${instance.displayName}\"? This cannot be undone.") },
62+
confirmButton = {
63+
TextButton(onClick = {
64+
vm.deleteInstance(instance.id)
65+
instanceToDelete = null
66+
}) {
67+
Text("Delete", color = MaterialTheme.colorScheme.error)
68+
}
69+
},
70+
dismissButton = {
71+
TextButton(onClick = { instanceToDelete = null }) {
72+
Text("Cancel")
73+
}
74+
},
75+
)
76+
}
4977

5078
Scaffold(
5179
topBar = {
@@ -75,7 +103,7 @@ fun WorkspacesScreen(
75103
vm.selectInstance(instance)
76104
onInstanceSelected()
77105
},
78-
onDelete = { vm.deleteInstance(instance.id) },
106+
onDelete = { instanceToDelete = instance },
79107
)
80108
}
81109
}

mobile/iosApp/iosApp/Screens/SettingsScreen.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Shared
44
struct SettingsScreen: View {
55
let onSwitchInstance: () -> Void
66
@State private var showCacheCleared = false
7+
@State private var showDeleteConfirmation = false
78

89
private var aim: ActiveInstanceManager { KoinHelper.activeInstanceManager }
910

@@ -30,6 +31,12 @@ struct SettingsScreen: View {
3031
aim.repository.clearCache()
3132
showCacheCleared = true
3233
}
34+
35+
if aim.activeInstance.value != nil {
36+
Button("Delete instance", role: .destructive) {
37+
showDeleteConfirmation = true
38+
}
39+
}
3340
}
3441

3542
Section("About") {
@@ -59,6 +66,20 @@ struct SettingsScreen: View {
5966
}
6067
}
6168
.animation(.easeInOut, value: showCacheCleared)
69+
.alert("Delete Instance", isPresented: $showDeleteConfirmation) {
70+
Button("Delete", role: .destructive) {
71+
guard let instance = aim.activeInstance.value as? Instance else { return }
72+
let db = KoinHelper.database
73+
db.instancesQueries.deleteById(id: instance.id)
74+
aim.clear()
75+
onSwitchInstance()
76+
}
77+
Button("Cancel", role: .cancel) {}
78+
} message: {
79+
if let instance = aim.activeInstance.value as? Instance {
80+
Text("Are you sure you want to delete \"\(instance.displayName)\"? This cannot be undone.")
81+
}
82+
}
6283
}
6384

6485
private var appVersion: String {

mobile/iosApp/iosApp/Screens/WorkspacesScreen.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ final class WorkspacesStore: ObservableObject {
4444
struct WorkspacesScreen: View {
4545
@StateObject private var store = WorkspacesStore()
4646
@State private var showAddSheet = false
47+
@State private var instanceToDelete: Instance?
4748
let onInstanceSelected: () -> Void
4849

4950
var body: some View {
@@ -70,6 +71,24 @@ struct WorkspacesScreen: View {
7071
onInstanceSelected()
7172
}
7273
}
74+
.alert(
75+
"Delete Instance",
76+
isPresented: Binding(
77+
get: { instanceToDelete != nil },
78+
set: { if !$0 { instanceToDelete = nil } }
79+
),
80+
presenting: instanceToDelete
81+
) { instance in
82+
Button("Delete", role: .destructive) {
83+
store.vm.deleteInstance(instanceId: instance.id)
84+
instanceToDelete = nil
85+
}
86+
Button("Cancel", role: .cancel) {
87+
instanceToDelete = nil
88+
}
89+
} message: { instance in
90+
Text("Are you sure you want to delete \"\(instance.displayName)\"? This cannot be undone.")
91+
}
7392
}
7493

7594
private var emptyState: some View {
@@ -106,11 +125,19 @@ struct WorkspacesScreen: View {
106125
}
107126
}
108127
.foregroundStyle(.primary)
109-
}
110-
.onDelete { offsets in
111-
for index in offsets {
112-
let instance = store.instances[index]
113-
store.vm.deleteInstance(instanceId: instance.id)
128+
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
129+
Button(role: .destructive) {
130+
instanceToDelete = instance
131+
} label: {
132+
Label("Delete", systemImage: "trash")
133+
}
134+
}
135+
.contextMenu {
136+
Button(role: .destructive) {
137+
instanceToDelete = instance
138+
} label: {
139+
Label("Delete Instance", systemImage: "trash")
140+
}
114141
}
115142
}
116143
}

mobile/shared/src/commonMain/kotlin/app/myfaq/shared/api/MyFaqApi.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import app.myfaq.shared.api.dto.Comment
55
import app.myfaq.shared.api.dto.FaqDetail
66
import app.myfaq.shared.api.dto.FaqPopularItem
77
import app.myfaq.shared.api.dto.FaqSummary
8+
import app.myfaq.shared.api.dto.GlossaryItem
89
import app.myfaq.shared.api.dto.Meta
910
import app.myfaq.shared.api.dto.NewsItem
1011
import app.myfaq.shared.api.dto.OpenQuestion
@@ -27,7 +28,7 @@ import io.ktor.client.request.parameter
2728
* v4.0 changes:
2829
* - Most list endpoints return a paginated wrapper: `{ success, data, meta }`.
2930
* - Popular/latest/trending/sticky FAQs and popular searches remain plain arrays.
30-
* - `/meta` stays at the v3.2 path (not bumped in v4.0 spec).
31+
* - `/meta` now at v4.0 path with updated field names (availableLanguages, enabledFeatures, etc.).
3132
*/
3233
interface MyFaqApi {
3334
// Bootstrap
@@ -57,6 +58,9 @@ interface MyFaqApi {
5758
// Comments (paginated)
5859
suspend fun comments(recordId: Int): List<Comment>
5960

61+
// Glossary (paginated)
62+
suspend fun glossary(): List<GlossaryItem>
63+
6064
// Open questions (paginated)
6165
suspend fun openQuestions(): List<OpenQuestion>
6266
}
@@ -69,9 +73,8 @@ class MyFaqApiImpl(
6973

7074
private val api get() = "$baseUrl/api/v4.0"
7175

72-
// /meta stays at v3.2 — it was not bumped in the v4.0 spec
7376
override suspend fun meta(): Meta =
74-
http.get("$baseUrl/api/v3.2/meta").body()
77+
http.get("$api/meta").body()
7578

7679
// --- Paginated endpoints: unwrap { success, data, meta } ---
7780

@@ -123,6 +126,10 @@ class MyFaqApiImpl(
123126
http.get("$api/comments/$recordId") { header("Accept-Language", language) }
124127
.body<PaginatedResponse<List<Comment>>>().data
125128

129+
override suspend fun glossary(): List<GlossaryItem> =
130+
http.get("$api/glossary") { header("Accept-Language", language) }
131+
.body<PaginatedResponse<List<GlossaryItem>>>().data
132+
126133
override suspend fun openQuestions(): List<OpenQuestion> =
127134
http.get("$api/open-questions") { header("Accept-Language", language) }
128135
.body<PaginatedResponse<List<OpenQuestion>>>().data
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package app.myfaq.shared.api.dto
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* Glossary item from `GET /api/v4.0/glossary` (paginated).
7+
*/
8+
@Serializable
9+
data class GlossaryItem(
10+
val id: Int = 0,
11+
val language: String? = null,
12+
val item: String = "",
13+
val definition: String = "",
14+
)
15+

mobile/shared/src/commonMain/kotlin/app/myfaq/shared/api/dto/Meta.kt

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,32 @@ import kotlinx.serialization.SerialName
44
import kotlinx.serialization.Serializable
55

66
/**
7-
* Response payload from `GET /api/v3.2/meta`.
7+
* Response payload from `GET /api/v4.0/meta`.
88
*
99
* Unknown or missing fields are tolerated — the shared `Json`
1010
* instance is configured with `ignoreUnknownKeys = true` so future
1111
* phpMyFAQ releases can add fields without breaking the client.
12+
*
13+
* v4.0 field names:
14+
* availableLanguages (object, e.g. {"de":"German","en":"English"})
15+
* enabledFeatures (object)
16+
* publicLogoUrl (string)
17+
* oauthDiscovery (object)
1218
*/
1319
@Serializable
1420
data class Meta(
1521
val version: String,
1622
val title: String,
1723
val language: String,
18-
@SerialName("available_languages")
19-
val availableLanguages: List<String> = emptyList(),
20-
val features: Map<String, Boolean> = emptyMap(),
21-
@SerialName("logo_url")
22-
val logoUrl: String? = null,
23-
val oauth: OAuthDiscovery? = null,
24-
)
24+
/** v4.0: object map e.g. {"de":"German","en":"English"} */
25+
val availableLanguages: Map<String, String> = emptyMap(),
26+
val enabledFeatures: Map<String, Boolean> = emptyMap(),
27+
val publicLogoUrl: String? = null,
28+
val oauthDiscovery: OAuthDiscovery? = null,
29+
) {
30+
/** Convenience: list of language codes (keys). */
31+
val languageCodes: List<String> get() = availableLanguages.keys.toList()
32+
}
2533

2634
@Serializable
2735
data class OAuthDiscovery(

mobile/shared/src/commonMain/kotlin/app/myfaq/shared/data/FaqRepository.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import app.myfaq.shared.api.dto.Comment
77
import app.myfaq.shared.api.dto.FaqDetail
88
import app.myfaq.shared.api.dto.FaqPopularItem
99
import app.myfaq.shared.api.dto.FaqSummary
10+
import app.myfaq.shared.api.dto.GlossaryItem
1011
import app.myfaq.shared.api.dto.Meta
1112
import app.myfaq.shared.api.dto.NewsItem
1213
import app.myfaq.shared.api.dto.OpenQuestion
@@ -90,6 +91,11 @@ class FaqRepository(
9091
suspend fun comments(recordId: Int): List<Comment> =
9192
cachedList("comments/$recordId", CacheTtl.COMMENTS) { api.comments(recordId) }
9293

94+
// --- Glossary ---
95+
96+
suspend fun glossary(): List<GlossaryItem> =
97+
cachedList("glossary", CacheTtl.FAQS) { api.glossary() }
98+
9399
// --- Open questions ---
94100

95101
suspend fun openQuestions(): List<OpenQuestion> =

mobile/shared/src/commonMain/kotlin/app/myfaq/shared/ui/WorkspacesViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class WorkspacesViewModel(
8888
}
8989

9090
fun deleteInstance(instanceId: String) {
91+
db.cacheEntriesQueries.deleteByInstance(instanceId)
9192
db.instancesQueries.deleteById(instanceId)
9293
if (activeInstanceManager.activeInstance.value?.id == instanceId) {
9394
activeInstanceManager.clear()

0 commit comments

Comments
 (0)