Skip to content

Commit

Permalink
Merge pull request #65 from crowdproj/wip-32
Browse files Browse the repository at this point in the history
cards page
  • Loading branch information
sszuev authored Jan 4, 2025
2 parents 163304e + b7c2503 commit 0c8ddc4
Show file tree
Hide file tree
Showing 19 changed files with 984 additions and 129 deletions.
1 change: 1 addition & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.navigation)
implementation(libs.ktor.client.android)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
Expand Down
6 changes: 3 additions & 3 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="com.github.sszuev.flashcards.android"
android:host="oauth2redirect"
android:path="/"
/>
android:scheme="com.github.sszuev.flashcards.android" />
</intent-filter>
</activity>
<activity
android:name="com.github.sszuev.flashcards.android.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.App.Splash">
android:theme="@style/Theme.App.Splash"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.github.sszuev.flashcards.android

data class Card (
val cardId: String,
val dictionaryId: String,
val word: String,
val translation: String,
val answered: Int
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import androidx.activity.viewModels
import androidx.compose.material3.MaterialTheme
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import com.github.sszuev.flashcards.android.models.CardViewModel
import com.github.sszuev.flashcards.android.models.CardsViewModelFactory
import com.github.sszuev.flashcards.android.models.DictionariesViewModelFactory
import com.github.sszuev.flashcards.android.models.DictionaryViewModel
import com.github.sszuev.flashcards.android.models.ViewModelFactory
import com.github.sszuev.flashcards.android.repositories.CardsRepository
import com.github.sszuev.flashcards.android.repositories.DictionaryRepository
import com.github.sszuev.flashcards.android.ui.MainDictionariesScreen
import com.github.sszuev.flashcards.android.ui.MainNavigation
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode
Expand All @@ -24,8 +27,11 @@ import kotlinx.coroutines.withContext
class MainActivity : ComponentActivity() {
private val tag = "MainActivity"

private val viewModel: DictionaryViewModel by viewModels {
ViewModelFactory(DictionaryRepository(AppConfig.serverUri))
private val dictionaryViewModel: DictionaryViewModel by viewModels {
DictionariesViewModelFactory(DictionaryRepository(AppConfig.serverUri))
}
private val cardViewModel: CardViewModel by viewModels {
CardsViewModelFactory(CardsRepository(AppConfig.serverUri))
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -46,9 +52,10 @@ class MainActivity : ComponentActivity() {

setContent {
MaterialTheme {
MainDictionariesScreen(
MainNavigation(
onSignOut = { onSignOut() },
viewModel = viewModel,
dictionariesViewModel = dictionaryViewModel,
cardViewModel = cardViewModel,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.sszuev.flashcards.android

import com.github.sszuev.flashcards.android.repositories.CardResource
import com.github.sszuev.flashcards.android.repositories.DictionaryResource

fun DictionaryResource.toDictionary() = Dictionary(
Expand All @@ -9,4 +10,16 @@ fun DictionaryResource.toDictionary() = Dictionary(
targetLanguage = checkNotNull(this.targetLang),
totalWords = checkNotNull(this.total),
learnedWords = checkNotNull(this.learned),
)

fun CardResource.toCard() = Card(
dictionaryId = checkNotNull(this.dictionaryId),
cardId = checkNotNull(this.cardId),
word = checkNotNull(this.words) { "No words" }
.map { checkNotNull(it.word) { "No word" } }
.firstOrNull() ?: throw IllegalArgumentException("Can't find field 'word' for card = $cardId"),
translation = checkNotNull(this.words).firstNotNullOfOrNull {
checkNotNull(it.translations) { "No translation" }.flatten().firstOrNull()
} ?: throw IllegalArgumentException("Can't find field 'translation' for card = $cardId"),
answered = checkNotNull(answered),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.github.sszuev.flashcards.android.models

import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.github.sszuev.flashcards.android.Card
import com.github.sszuev.flashcards.android.repositories.CardsRepository
import com.github.sszuev.flashcards.android.toCard
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class CardViewModel(
private val repository: CardsRepository
) : ViewModel() {

private val tag = "CardViewModel"

val cads = mutableStateOf<List<Card>>(emptyList())
val isLoading = mutableStateOf(true)
val errorMessage = mutableStateOf<String?>(null)

val selectedCardId = mutableStateOf<String?>(null)

var sortField = mutableStateOf<String?>("name")
var isAscending = mutableStateOf(true)

suspend fun loadCards(dictionaryId: String) = withContext(Dispatchers.IO) {
Log.d(tag, "load cards for dictionary = $dictionaryId")
isLoading.value = true
errorMessage.value = null
cads.value = emptyList()
try {
cads.value = repository.getAll(dictionaryId).map { it.toCard() }
} catch (e: Exception) {
errorMessage.value = "Failed to load cards: ${e.localizedMessage}"
Log.e(tag, "Failed to load cards", e)
} finally {
isLoading.value = false
}
}

fun sortBy(field: String) {
if (sortField.value == field) {
isAscending.value = !isAscending.value
} else {
sortField.value = field
isAscending.value = true
}
applySorting()
}

private fun applySorting() {
cads.value = cads.value.sortedWith { a, b ->
val result = when (sortField.value) {
"word" -> a.word.compareTo(b.word)
"translation" -> a.translation.compareTo(b.translation)
"status" -> a.answered.compareTo(b.answered)
else -> 0
}
if (isAscending.value) result else -result
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class DictionaryViewModel(
dictionaries.value = repository.getAll().map { it.toDictionary() }
} catch (e: Exception) {
errorMessage.value = "Failed to load dictionaries: ${e.localizedMessage}"
Log.e(tag, "Failed to load dictionaries", e)
} finally {
isLoading.value = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package com.github.sszuev.flashcards.android.models

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.github.sszuev.flashcards.android.repositories.CardsRepository
import com.github.sszuev.flashcards.android.repositories.DictionaryRepository

class ViewModelFactory(
class DictionariesViewModelFactory(
private val repository: DictionaryRepository
) : ViewModelProvider.Factory {

Expand All @@ -15,4 +16,17 @@ class ViewModelFactory(
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

class CardsViewModelFactory(
private val repository: CardsRepository
) : ViewModelProvider.Factory {

@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(CardViewModel::class.java)) {
return CardViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.github.sszuev.flashcards.android.repositories

import com.github.sszuev.flashcards.android.utils.MapStringAnySerializer
import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable

@Serializable
data class CardResource(
val cardId: String,
val dictionaryId: String? = null,
val words: List<CardWordResource>? = null,
val stats: Map<String, Long>? = null,
@Serializable(with = MapStringAnySerializer::class)
val details: Map<String, @Polymorphic Any>? = null,
val answered: Int? = null,
)

@Serializable
data class CardWordResource(
val word: String? = null,
val transcription: String? = null,
val partOfSpeech: String? = null,
val translations: List<List<String>>? = null,
val examples: List<CardWordExampleResource>? = null,
val sound: String? = null,
val primary: Boolean? = null
)

@Serializable
data class CardWordExampleResource(
val example: String? = null,
val translation: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.github.sszuev.flashcards.android.repositories

import android.util.Log
import io.ktor.client.request.setBody
import kotlinx.serialization.Serializable
import java.util.UUID

class CardsRepository(
private val serverUri: String,
) {
private val tag = "CardsRepository"

suspend fun getAll(dictionaryId: String): List<CardResource> {
val requestId = UUID.randomUUID().toString()
Log.d(tag, "Get all cards with requestId=$requestId and dictionaryId=$dictionaryId")
val container = authPost<GetAllCardsResponse>("$serverUri/v1/api/cards/get-all") {
setBody(
GetAllCardsRequest(
requestType = "getAllCards",
requestId = requestId,
dictionaryId = dictionaryId,
)
)
}
Log.d(
tag,
"Received response for requestId: $requestId, cards count: ${container.cards.size}"
)
return container.cards
}

@Serializable
private data class GetAllCardsResponse(
val responseType: String? = null,
val requestId: String? = null,
val cards: List<CardResource> = emptyList()
)

@Suppress("unused")
@Serializable
data class GetAllCardsRequest(
val requestType: String? = null,
val requestId: String? = null,
val dictionaryId: String? = null
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ import kotlinx.serialization.Serializable
import java.util.UUID

class DictionaryRepository(
private val host: String,
private val serverUri: String,
) {
private val tag = "DictionaryRepository"

suspend fun getAll(): List<DictionaryResource> {
val requestId = UUID.randomUUID().toString()
Log.i(tag, "Get all dictionaries with requestId: $requestId")
val container = authPost<GetAllDictionariesResponse>("$host/v1/api/dictionaries/get-all") {
Log.d(tag, "Get all dictionaries with requestId=$requestId")
val container = authPost<GetAllDictionariesResponse>("$serverUri/v1/api/dictionaries/get-all") {
setBody(
GetAllDictionariesRequest(
requestType = "getAllDictionaries",
requestId = requestId,
)
)
}
Log.i(
Log.d(
tag,
"Received response for requestId: $requestId, dictionaries count: ${container.dictionaries.size}"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ suspend inline fun <reified T> authPost(
Log.d("HttpApi", "second attempt succeeds")
res
} else {
// TODO: handle invalid: 400 Bad Request. Text: "{"error":"invalid_grant","error_description":"Session not active"}"
throw e
}
}
Expand Down
Loading

0 comments on commit 0c8ddc4

Please sign in to comment.