Compare commits
20 Commits
ebfd097bf8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
199f5ae33f | ||
|
|
cfd71162a0 | ||
|
|
c94b29073f | ||
|
|
95dfd3c7eb | ||
|
|
d6a9ccf4e3 | ||
|
|
863920143d | ||
|
|
15d03ef57f | ||
|
|
f737657cdb | ||
|
|
b75f5f32a0 | ||
|
|
0f8d605df7 | ||
|
|
0a202191eb | ||
|
|
d12a21909c | ||
|
|
37d8c2a6c5 | ||
|
|
8f42fa79ef | ||
|
|
9600ef84ae | ||
|
|
c81e0886b8 | ||
|
|
9db538bf0a | ||
|
|
4cd014957f | ||
|
|
4b572f8773 | ||
|
|
c4fbfdf0ed |
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2026-02-16T10:13:39.492968600Z">
|
<DropdownSelection timestamp="2026-02-20T17:14:10.736481200Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=RFCW11ML1WV" />
|
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\jonas\.android\avd\Pixel_6.avd" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
implementation(libs.core.ktx)
|
implementation(libs.core.ktx)
|
||||||
|
implementation(libs.androidx.compose.runtime)
|
||||||
ksp(libs.room.compiler)
|
ksp(libs.room.compiler)
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
|
|||||||
24
app/src/main/assets/hints/explore_packs_hint.md
Normal file
24
app/src/main/assets/hints/explore_packs_hint.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
All vocabulary lists in this section were generated automatically. While I strive for accuracy and quality, please keep in mind that these are machine-generated collections.
|
||||||
|
|
||||||
|
I'm a single developer building and maintaining this app in my spare time. I'm passionate about creating tools that help people learn languages, but I have limited resources and time.
|
||||||
|
|
||||||
|
### Your Feedback Matters
|
||||||
|
|
||||||
|
I greatly appreciate any feedback, suggestions, or ideas you might have! If you:
|
||||||
|
|
||||||
|
- Find errors in any vocabulary pack
|
||||||
|
- Have ideas for new topics, languages, or categories
|
||||||
|
- Want to request a specific vocabulary pack
|
||||||
|
- Have suggestions for improving existing packs
|
||||||
|
|
||||||
|
Please don't hesitate to reach out through the Request feature or contact me directly. Your input helps make this app better for everyone!
|
||||||
|
|
||||||
|
### How Packs Work
|
||||||
|
|
||||||
|
- **Download** packs that interest you
|
||||||
|
- **Preview** the words before adding them
|
||||||
|
- **Import** them into your library with options to handle duplicates
|
||||||
|
- **Organize** them into categories which are created automatically
|
||||||
|
|
||||||
|
|
||||||
|
Thank you for using this app and your feedback!
|
||||||
@@ -57,6 +57,10 @@ data class VocabularyItem(
|
|||||||
features = switchedFeaturesJson
|
features = switchedFeaturesJson
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasFeatures(): Boolean {
|
||||||
|
return !features.isNullOrBlank() && features != "{}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import eu.gaudian.translator.model.repository.SettingsRepository
|
|||||||
import eu.gaudian.translator.utils.ApiCallback
|
import eu.gaudian.translator.utils.ApiCallback
|
||||||
import eu.gaudian.translator.utils.Log
|
import eu.gaudian.translator.utils.Log
|
||||||
import eu.gaudian.translator.utils.StatusAction
|
import eu.gaudian.translator.utils.StatusAction
|
||||||
|
import eu.gaudian.translator.utils.StatusMessageId
|
||||||
import eu.gaudian.translator.utils.StatusMessageService
|
import eu.gaudian.translator.utils.StatusMessageService
|
||||||
import eu.gaudian.translator.viewmodel.MessageAction
|
import eu.gaudian.translator.viewmodel.MessageAction
|
||||||
import eu.gaudian.translator.viewmodel.MessageDisplayType
|
import eu.gaudian.translator.viewmodel.MessageDisplayType
|
||||||
@@ -403,11 +404,7 @@ class ApiManager(private val context: Context) {
|
|||||||
|
|
||||||
if (languageModel == null) {
|
if (languageModel == null) {
|
||||||
val errorMsg = context.getString(R.string.no_model_selected_for_the_task, modelType.getLocalizedName(context))
|
val errorMsg = context.getString(R.string.no_model_selected_for_the_task, modelType.getLocalizedName(context))
|
||||||
StatusMessageService.trigger(StatusAction.ShowActionableMessage(
|
StatusMessageService.showErrorById(StatusMessageId.ERROR_NO_MODEL_CONFIGURED)
|
||||||
text = errorMsg,
|
|
||||||
type = MessageDisplayType.ACTIONABLE_ERROR,
|
|
||||||
action = MessageAction.NAVIGATE_TO_API_KEYS
|
|
||||||
))
|
|
||||||
callback.onFailure(errorMsg)
|
callback.onFailure(errorMsg)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
|
package eu.gaudian.translator.model.communication.files_download
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized configuration for all download-related URLs and paths.
|
||||||
|
* Update this file when server configuration changes.
|
||||||
|
*/
|
||||||
|
object DownloadConfig {
|
||||||
|
|
||||||
|
// ===== BASE URLS =====
|
||||||
|
|
||||||
|
/** Base URL for all polly content */
|
||||||
|
const val POLLY_BASE_URL = "http://pollyapp.b-cdn.net/"
|
||||||
|
|
||||||
|
// ===== DICTIONARIES CONFIG =====
|
||||||
|
|
||||||
|
/** Subdirectory for dictionary files on the server */
|
||||||
|
const val DICTIONARIES_SUBDIRECTORY = "dictionaries"
|
||||||
|
|
||||||
|
/** Full URL for dictionary files (baseUrl + subdirectory) */
|
||||||
|
const val DICTIONARIES_BASE_URL = "$POLLY_BASE_URL$DICTIONARIES_SUBDIRECTORY/"
|
||||||
|
|
||||||
|
/** Manifest file name for dictionaries */
|
||||||
|
const val DICTIONARIES_MANIFEST_FILE = "manifest.json"
|
||||||
|
|
||||||
|
/** Full URL for the dictionary manifest */
|
||||||
|
const val DICTIONARIES_MANIFEST_URL = "$DICTIONARIES_BASE_URL$DICTIONARIES_MANIFEST_FILE"
|
||||||
|
|
||||||
|
// ===== FLASHCARDS CONFIG =====
|
||||||
|
|
||||||
|
/** Subdirectory for flashcard/vocab files on the server */
|
||||||
|
const val FLASHCARDS_SUBDIRECTORY = "flashcards"
|
||||||
|
|
||||||
|
/** Full URL for flashcard files (baseUrl + subdirectory) */
|
||||||
|
const val FLASHCARDS_BASE_URL = "$POLLY_BASE_URL$FLASHCARDS_SUBDIRECTORY/"
|
||||||
|
|
||||||
|
/** Manifest file name for flashcards/vocab packs */
|
||||||
|
const val FLASHCARDS_MANIFEST_FILE = "vocab_manifest.json"
|
||||||
|
|
||||||
|
/** Full URL for the flashcard manifest */
|
||||||
|
const val FLASHCARDS_MANIFEST_URL = "$FLASHCARDS_BASE_URL$FLASHCARDS_MANIFEST_FILE"
|
||||||
|
|
||||||
|
// ===== LOCAL STORAGE PATHS =====
|
||||||
|
|
||||||
|
/** Local subdirectory for storing flashcard files (relative to filesDir) */
|
||||||
|
const val LOCAL_FLASHCARDS_PATH = FLASHCARDS_SUBDIRECTORY
|
||||||
|
|
||||||
|
// ===== HELPER METHODS =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full remote URL for a dictionary asset.
|
||||||
|
* @param filename The asset filename (e.g., "dictionary_de.db")
|
||||||
|
*/
|
||||||
|
fun getDictionaryAssetUrl(filename: String): String = "$DICTIONARIES_BASE_URL$filename"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full remote URL for a flashcard asset.
|
||||||
|
* @param filename The asset filename (e.g., "2026_02_20_verbs_beginners_zh_pl_A1.json")
|
||||||
|
*/
|
||||||
|
fun getFlashcardAssetUrl(filename: String): String = "$FLASHCARDS_BASE_URL$filename"
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
package eu.gaudian.translator.model.communication
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
|
package eu.gaudian.translator.model.communication.files_download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
|
import eu.gaudian.translator.model.communication.Asset
|
||||||
|
import eu.gaudian.translator.model.communication.FileInfo
|
||||||
|
import eu.gaudian.translator.model.communication.ManifestResponse
|
||||||
import eu.gaudian.translator.utils.Log
|
import eu.gaudian.translator.utils.Log
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -15,91 +20,122 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum representing different download sources.
|
|
||||||
*/
|
|
||||||
enum class DownloadSource(val baseUrl: String, val subdirectory: String) {
|
|
||||||
DICTIONARIES("http://23.88.48.47/", "dictionaries"),
|
|
||||||
FLASHCARDS("http://23.88.48.47/", "flashcard-collections")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages downloading files from the server, verifying checksums, and checking versions.
|
* Manages downloading files from the server, verifying checksums, and checking versions.
|
||||||
|
* All URLs and paths are centralized in [DownloadConfig].
|
||||||
*/
|
*/
|
||||||
class FileDownloadManager(private val context: Context) {
|
class FileDownloadManager(private val context: Context) {
|
||||||
|
|
||||||
private val baseUrl = "http://23.88.48.47/"
|
init {
|
||||||
|
Log.d("FileDownloadManager", "=== FileDownloadManager initialized ===")
|
||||||
|
Log.d("FileDownloadManager", "Context filesDir: ${context.filesDir.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "Polly base URL: ${DownloadConfig.POLLY_BASE_URL}")
|
||||||
|
Log.d("FileDownloadManager", "Dictionaries URL: ${DownloadConfig.DICTIONARIES_BASE_URL}")
|
||||||
|
Log.d("FileDownloadManager", "Flashcards URL: ${DownloadConfig.FLASHCARDS_BASE_URL}")
|
||||||
|
}
|
||||||
|
|
||||||
private val retrofit = Retrofit.Builder()
|
// ===== Retrofit Services =====
|
||||||
.baseUrl(baseUrl)
|
|
||||||
|
private val dictionaryRetrofit = Retrofit.Builder()
|
||||||
|
.baseUrl(DownloadConfig.DICTIONARIES_BASE_URL)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.client(OkHttpClient.Builder().build())
|
.client(OkHttpClient.Builder().build())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val manifestApiService = retrofit.create<ManifestApiService>()
|
private val manifestApiService = dictionaryRetrofit.create<ManifestApiService>()
|
||||||
|
|
||||||
|
private val flashcardRetrofit = Retrofit.Builder()
|
||||||
|
.baseUrl(DownloadConfig.POLLY_BASE_URL)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.client(OkHttpClient.Builder().build())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val flashcardApiService = flashcardRetrofit.create<FlashcardApiService>()
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
private val sharedPreferences = context.getSharedPreferences("file_versions", Context.MODE_PRIVATE)
|
private val sharedPreferences = context.getSharedPreferences("file_versions", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// ===== Dictionary Manifest =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the manifest from the server.
|
* Fetches the manifest from the server.
|
||||||
*/
|
*/
|
||||||
suspend fun fetchManifest(): ManifestResponse? = withContext(Dispatchers.IO) {
|
suspend fun fetchManifest(): ManifestResponse? = withContext(Dispatchers.IO) {
|
||||||
|
Log.d("FileDownloadManager", "=== fetchManifest() called ===")
|
||||||
|
Log.d("FileDownloadManager", "Fetching manifest from: ${DownloadConfig.DICTIONARIES_MANIFEST_URL}")
|
||||||
try {
|
try {
|
||||||
val response = manifestApiService.getManifest().execute()
|
val response = manifestApiService.getManifest().execute()
|
||||||
|
Log.d("FileDownloadManager", "Manifest response received - isSuccessful: ${response.isSuccessful}, code: ${response.code()}")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
response.body()
|
val manifest = response.body()
|
||||||
|
Log.d("FileDownloadManager", "Manifest parsed successfully, files count: ${manifest?.files?.size ?: 0}")
|
||||||
|
manifest?.files?.forEach { file ->
|
||||||
|
Log.d("FileDownloadManager", " - File: ${file.id}, name: ${file.name}, version: ${file.version}, assets: ${file.assets.size}")
|
||||||
|
}
|
||||||
|
manifest
|
||||||
} else {
|
} else {
|
||||||
@Suppress("HardCodedStringLiteral") val errorMessage = response.errorBody()?.string() ?: "HTTP ${response.code()}"
|
val errorMessage = response.errorBody()?.string() ?: "HTTP ${response.code()}"
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
Log.e("FileDownloadManager", "Failed to fetch manifest: $errorMessage")
|
Log.e("FileDownloadManager", "Failed to fetch manifest: $errorMessage")
|
||||||
throw Exception(context.getString(R.string.failed_to_fetch_manifest, errorMessage))
|
throw Exception(context.getString(R.string.failed_to_fetch_manifest, errorMessage))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
Log.e("FileDownloadManager", "Error fetching manifest", e)
|
Log.e("FileDownloadManager", "Error fetching manifest", e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Dictionary Downloads =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads all assets for a file and verifies their checksums.
|
* Downloads all assets for a file and verifies their checksums.
|
||||||
*/
|
*/
|
||||||
suspend fun downloadFile(fileInfo: FileInfo, onProgress: (Float) -> Unit = {}): Boolean = withContext(Dispatchers.IO) {
|
suspend fun downloadFile(fileInfo: FileInfo, onProgress: (Float) -> Unit = {}): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
Log.d("FileDownloadManager", "=== downloadFile() called ===")
|
||||||
|
Log.d("FileDownloadManager", "File info - id: ${fileInfo.id}, name: ${fileInfo.name}, version: ${fileInfo.version}")
|
||||||
|
Log.d("FileDownloadManager", "Total assets to download: ${fileInfo.assets.size}")
|
||||||
val totalAssets = fileInfo.assets.size
|
val totalAssets = fileInfo.assets.size
|
||||||
|
|
||||||
for ((completedAssets, asset) in fileInfo.assets.withIndex()) {
|
for ((completedAssets, asset) in fileInfo.assets.withIndex()) {
|
||||||
val success = downloadAsset(asset) { assetProgress ->
|
Log.d("FileDownloadManager", "Processing asset ${completedAssets + 1}/$totalAssets: ${asset.filename}")
|
||||||
|
val success = downloadDictionaryAsset(asset) { assetProgress ->
|
||||||
// Calculate overall progress
|
// Calculate overall progress
|
||||||
val assetContribution = assetProgress / totalAssets
|
val assetContribution = assetProgress / totalAssets
|
||||||
val previousAssetsProgress = completedAssets.toFloat() / totalAssets
|
val previousAssetsProgress = completedAssets.toFloat() / totalAssets
|
||||||
onProgress(previousAssetsProgress + assetContribution)
|
onProgress(previousAssetsProgress + assetContribution)
|
||||||
}
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
Log.e("FileDownloadManager", "Failed to download asset: ${asset.filename}")
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save version after all assets are downloaded successfully
|
// Save version after all assets are downloaded successfully
|
||||||
sharedPreferences.edit { putString(fileInfo.id, fileInfo.version) }
|
sharedPreferences.edit { putString(fileInfo.id, fileInfo.version) }
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
Log.d("FileDownloadManager", "Download successful for all assets of ${fileInfo.name}")
|
Log.d("FileDownloadManager", "Download successful for all assets of ${fileInfo.name}")
|
||||||
|
Log.d("FileDownloadManager", "Saved version ${fileInfo.version} for id ${fileInfo.id}")
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a specific asset and verifies its checksum.
|
* Downloads a specific dictionary asset and verifies its checksum.
|
||||||
*/
|
*/
|
||||||
private suspend fun downloadAsset(asset: Asset, onProgress: (Float) -> Unit = {}): Boolean = withContext(Dispatchers.IO) {
|
private suspend fun downloadDictionaryAsset(asset: Asset, onProgress: (Float) -> Unit = {}): Boolean = withContext(Dispatchers.IO) {
|
||||||
val fileUrl = "${baseUrl}${asset.filename}"
|
Log.d("FileDownloadManager", "=== downloadDictionaryAsset() called ===")
|
||||||
|
val fileUrl = DownloadConfig.getDictionaryAssetUrl(asset.filename)
|
||||||
val localFile = File(context.filesDir, asset.filename)
|
val localFile = File(context.filesDir, asset.filename)
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Remote URL: $fileUrl")
|
||||||
|
Log.d("FileDownloadManager", "Local file path: ${localFile.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "Local file parent: ${localFile.parentFile?.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "Expected checksum: ${asset.checksumSha256}")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Log.d("FileDownloadManager", "Creating HTTP request...")
|
||||||
val client = OkHttpClient()
|
val client = OkHttpClient()
|
||||||
val request = Request.Builder().url(fileUrl).build()
|
val request = Request.Builder().url(fileUrl).build()
|
||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Response received - code: ${response.code}, message: ${response.message}")
|
||||||
|
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val errorMessage = context.getString(
|
val errorMessage = context.getString(
|
||||||
R.string.text_download_failed_http,
|
R.string.text_download_failed_http,
|
||||||
@@ -113,16 +149,19 @@ class FileDownloadManager(private val context: Context) {
|
|||||||
val body = response.body
|
val body = response.body
|
||||||
|
|
||||||
val contentLength = body.contentLength()
|
val contentLength = body.contentLength()
|
||||||
|
Log.d("FileDownloadManager", "Content length: $contentLength bytes")
|
||||||
if (contentLength <= 0) {
|
if (contentLength <= 0) {
|
||||||
|
Log.e("FileDownloadManager", "Invalid file size: $contentLength")
|
||||||
throw Exception("Invalid file size: $contentLength")
|
throw Exception("Invalid file size: $contentLength")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Starting file download to: ${localFile.absolutePath}")
|
||||||
FileOutputStream(localFile).use { output ->
|
FileOutputStream(localFile).use { output ->
|
||||||
body.byteStream().use { input ->
|
body.byteStream().use { input ->
|
||||||
val buffer = ByteArray(8192)
|
val buffer = ByteArray(8192)
|
||||||
var bytesRead: Int
|
var bytesRead: Int
|
||||||
var totalBytesRead: Long = 0
|
var totalBytesRead: Long = 0
|
||||||
@Suppress("HardCodedStringLiteral") val digest = MessageDigest.getInstance("SHA-256")
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
|
||||||
while (input.read(buffer).also { bytesRead = it } != -1) {
|
while (input.read(buffer).also { bytesRead = it } != -1) {
|
||||||
output.write(buffer, 0, bytesRead)
|
output.write(buffer, 0, bytesRead)
|
||||||
@@ -134,16 +173,19 @@ class FileDownloadManager(private val context: Context) {
|
|||||||
output.flush()
|
output.flush()
|
||||||
|
|
||||||
// Compute checksum
|
// Compute checksum
|
||||||
val computedChecksum = digest.digest().joinToString("") {
|
val computedChecksum = digest.digest().joinToString("") { "%02X".format(it) }
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
"%02X".format(it)
|
Log.d("FileDownloadManager", "Download complete. Total bytes read: $totalBytesRead")
|
||||||
}
|
Log.d("FileDownloadManager", "Computed checksum: $computedChecksum")
|
||||||
|
Log.d("FileDownloadManager", "Expected checksum: ${asset.checksumSha256}")
|
||||||
|
|
||||||
if (computedChecksum.equals(asset.checksumSha256, ignoreCase = true)) {
|
if (computedChecksum.equals(asset.checksumSha256, ignoreCase = true)) {
|
||||||
@Suppress("HardCodedStringLiteral")
|
Log.d("FileDownloadManager", "Checksum VERIFIED for ${asset.filename}")
|
||||||
Log.d("FileDownloadManager", "Download successful for ${asset.filename}")
|
Log.d("FileDownloadManager", "File saved successfully at: ${localFile.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "File exists: ${localFile.exists()}, size: ${localFile.length()} bytes")
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
Log.e("FileDownloadManager", "Checksum MISMATCH for ${asset.filename}")
|
||||||
Log.e("FileDownloadManager",
|
Log.e("FileDownloadManager",
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.text_checksum_mismatch_for_expected_got,
|
R.string.text_checksum_mismatch_for_expected_got,
|
||||||
@@ -151,34 +193,44 @@ class FileDownloadManager(private val context: Context) {
|
|||||||
asset.checksumSha256,
|
asset.checksumSha256,
|
||||||
computedChecksum
|
computedChecksum
|
||||||
))
|
))
|
||||||
|
Log.d("FileDownloadManager", "Deleting corrupted file: ${localFile.absolutePath}")
|
||||||
localFile.delete() // Delete corrupted file
|
localFile.delete() // Delete corrupted file
|
||||||
throw Exception("Checksum verification failed for ${asset.filename}")
|
throw Exception("Checksum verification failed for ${asset.filename}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@Suppress("HardCodedStringLiteral")
|
Log.e("FileDownloadManager", "Error downloading asset from $fileUrl", e)
|
||||||
Log.e("FileDownloadManager", "Error downloading asset", e)
|
Log.e("FileDownloadManager", "Target path was: ${localFile.absolutePath}")
|
||||||
// Clean up partial download
|
// Clean up partial download
|
||||||
if (localFile.exists()) {
|
if (localFile.exists()) {
|
||||||
|
Log.d("FileDownloadManager", "Cleaning up partial download: ${localFile.absolutePath}")
|
||||||
localFile.delete()
|
localFile.delete()
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Version Management =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a newer version is available for a file.
|
* Checks if a newer version is available for a file.
|
||||||
*/
|
*/
|
||||||
fun isNewerVersionAvailable(fileInfo: FileInfo): Boolean {
|
fun isNewerVersionAvailable(fileInfo: FileInfo): Boolean {
|
||||||
|
Log.d("FileDownloadManager", "=== isNewerVersionAvailable() called ===")
|
||||||
|
Log.d("FileDownloadManager", "Checking file: ${fileInfo.id} (${fileInfo.name})")
|
||||||
val localVersion = sharedPreferences.getString(fileInfo.id, "0.0.0") ?: "0.0.0"
|
val localVersion = sharedPreferences.getString(fileInfo.id, "0.0.0") ?: "0.0.0"
|
||||||
return compareVersions(fileInfo.version, localVersion) > 0
|
Log.d("FileDownloadManager", "Local version: $localVersion, Server version: ${fileInfo.version}")
|
||||||
|
val result = compareVersions(fileInfo.version, localVersion) > 0
|
||||||
|
Log.d("FileDownloadManager", "Newer version available: $result")
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares two version strings (assuming semantic versioning).
|
* Compares two version strings (assuming semantic versioning).
|
||||||
*/
|
*/
|
||||||
private fun compareVersions(version1: String, version2: String): Int {
|
private fun compareVersions(version1: String, version2: String): Int {
|
||||||
|
Log.d("FileDownloadManager", "Comparing versions: '$version1' vs '$version2'")
|
||||||
val parts1 = version1.split(".").map { it.toIntOrNull() ?: 0 }
|
val parts1 = version1.split(".").map { it.toIntOrNull() ?: 0 }
|
||||||
val parts2 = version2.split(".").map { it.toIntOrNull() ?: 0 }
|
val parts2 = version2.split(".").map { it.toIntOrNull() ?: 0 }
|
||||||
|
|
||||||
@@ -186,9 +238,12 @@ class FileDownloadManager(private val context: Context) {
|
|||||||
val part1 = parts1.getOrElse(i) { 0 }
|
val part1 = parts1.getOrElse(i) { 0 }
|
||||||
val part2 = parts2.getOrElse(i) { 0 }
|
val part2 = parts2.getOrElse(i) { 0 }
|
||||||
if (part1 != part2) {
|
if (part1 != part2) {
|
||||||
return part1.compareTo(part2)
|
val result = part1.compareTo(part2)
|
||||||
|
Log.d("FileDownloadManager", "Version comparison result: $result (at part $i: $part1 vs $part2)")
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Log.d("FileDownloadManager", "Versions are equal, returning 0")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,37 +251,12 @@ class FileDownloadManager(private val context: Context) {
|
|||||||
* Gets the local version of a file.
|
* Gets the local version of a file.
|
||||||
*/
|
*/
|
||||||
fun getLocalVersion(fileId: String): String {
|
fun getLocalVersion(fileId: String): String {
|
||||||
return sharedPreferences.getString(fileId, "0.0.0") ?: "0.0.0"
|
val version = sharedPreferences.getString(fileId, "0.0.0") ?: "0.0.0"
|
||||||
|
Log.d("FileDownloadManager", "getLocalVersion($fileId) = $version")
|
||||||
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Flashcard Collections Support =====
|
// ===== Flashcard Collections =====
|
||||||
|
|
||||||
private val flashcardRetrofit = Retrofit.Builder()
|
|
||||||
.baseUrl(DownloadSource.FLASHCARDS.baseUrl)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.client(OkHttpClient.Builder().build())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val flashcardApiService = flashcardRetrofit.create<FlashcardApiService>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the flashcard collection manifest from the server.
|
|
||||||
*/
|
|
||||||
suspend fun fetchFlashcardManifest(): FlashcardManifestResponse? = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val response = flashcardApiService.getFlashcardManifest().execute()
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
response.body()
|
|
||||||
} else {
|
|
||||||
val errorMessage = response.errorBody()?.string() ?: "HTTP ${response.code()}"
|
|
||||||
Log.e("FileDownloadManager", "Failed to fetch flashcard manifest: $errorMessage")
|
|
||||||
throw Exception(context.getString(R.string.failed_to_fetch_manifest, errorMessage))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("FileDownloadManager", "Error fetching flashcard manifest", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a flashcard collection file with checksum verification.
|
* Downloads a flashcard collection file with checksum verification.
|
||||||
@@ -235,19 +265,34 @@ class FileDownloadManager(private val context: Context) {
|
|||||||
flashcardInfo: FlashcardCollectionInfo,
|
flashcardInfo: FlashcardCollectionInfo,
|
||||||
onProgress: (Float) -> Unit = {}
|
onProgress: (Float) -> Unit = {}
|
||||||
): Boolean = withContext(Dispatchers.IO) {
|
): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
Log.d("FileDownloadManager", "=== downloadFlashcardCollection() called ===")
|
||||||
|
Log.d("FileDownloadManager", "Flashcard info - id: ${flashcardInfo.id}, version: ${flashcardInfo.version}")
|
||||||
|
|
||||||
val asset = flashcardInfo.asset
|
val asset = flashcardInfo.asset
|
||||||
val source = DownloadSource.FLASHCARDS
|
val fileUrl = DownloadConfig.getFlashcardAssetUrl(asset.filename)
|
||||||
val fileUrl = "${source.baseUrl}${source.subdirectory}/${asset.filename}"
|
val localFile = File(context.filesDir, "${DownloadConfig.LOCAL_FLASHCARDS_PATH}/${asset.filename}")
|
||||||
val localFile = File(context.filesDir, "${source.subdirectory}/${asset.filename}")
|
|
||||||
|
Log.d("FileDownloadManager", "Remote URL: $fileUrl")
|
||||||
|
Log.d("FileDownloadManager", "Local file path: ${localFile.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "Local file parent: ${localFile.parentFile?.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "Expected checksum: ${asset.checksumSha256}")
|
||||||
|
|
||||||
// Create subdirectory if it doesn't exist
|
// Create subdirectory if it doesn't exist
|
||||||
localFile.parentFile?.mkdirs()
|
val parentDir = localFile.parentFile
|
||||||
|
if (parentDir != null && !parentDir.exists()) {
|
||||||
|
Log.d("FileDownloadManager", "Creating subdirectory: ${parentDir.absolutePath}")
|
||||||
|
val created = parentDir.mkdirs()
|
||||||
|
Log.d("FileDownloadManager", "Subdirectory created: $created")
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Log.d("FileDownloadManager", "Creating HTTP request for flashcard...")
|
||||||
val client = OkHttpClient()
|
val client = OkHttpClient()
|
||||||
val request = Request.Builder().url(fileUrl).build()
|
val request = Request.Builder().url(fileUrl).build()
|
||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Response received - code: ${response.code}, message: ${response.message}")
|
||||||
|
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val errorMessage = context.getString(
|
val errorMessage = context.getString(
|
||||||
R.string.text_download_failed_http,
|
R.string.text_download_failed_http,
|
||||||
@@ -260,10 +305,13 @@ class FileDownloadManager(private val context: Context) {
|
|||||||
|
|
||||||
val body = response.body
|
val body = response.body
|
||||||
val contentLength = body.contentLength()
|
val contentLength = body.contentLength()
|
||||||
|
Log.d("FileDownloadManager", "Content length: $contentLength bytes")
|
||||||
if (contentLength <= 0) {
|
if (contentLength <= 0) {
|
||||||
|
Log.e("FileDownloadManager", "Invalid file size: $contentLength")
|
||||||
throw Exception("Invalid file size: $contentLength")
|
throw Exception("Invalid file size: $contentLength")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Starting flashcard download to: ${localFile.absolutePath}")
|
||||||
FileOutputStream(localFile).use { output ->
|
FileOutputStream(localFile).use { output ->
|
||||||
body.byteStream().use { input ->
|
body.byteStream().use { input ->
|
||||||
val buffer = ByteArray(8192)
|
val buffer = ByteArray(8192)
|
||||||
@@ -282,26 +330,37 @@ class FileDownloadManager(private val context: Context) {
|
|||||||
|
|
||||||
val computedChecksum = digest.digest().joinToString("") { "%02X".format(it) }
|
val computedChecksum = digest.digest().joinToString("") { "%02X".format(it) }
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Download complete. Total bytes read: $totalBytesRead")
|
||||||
|
Log.d("FileDownloadManager", "Computed checksum: $computedChecksum")
|
||||||
|
Log.d("FileDownloadManager", "Expected checksum: ${asset.checksumSha256}")
|
||||||
|
|
||||||
if (computedChecksum.equals(asset.checksumSha256, ignoreCase = true)) {
|
if (computedChecksum.equals(asset.checksumSha256, ignoreCase = true)) {
|
||||||
Log.d("FileDownloadManager", "Flashcard download successful for ${asset.filename}")
|
Log.d("FileDownloadManager", "Checksum VERIFIED for flashcard ${asset.filename}")
|
||||||
|
Log.d("FileDownloadManager", "File saved successfully at: ${localFile.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "File exists: ${localFile.exists()}, size: ${localFile.length()} bytes")
|
||||||
// Save version
|
// Save version
|
||||||
sharedPreferences.edit { putString(flashcardInfo.id, flashcardInfo.version) }
|
sharedPreferences.edit { putString(flashcardInfo.id, flashcardInfo.version) }
|
||||||
|
Log.d("FileDownloadManager", "Saved version ${flashcardInfo.version} for id ${flashcardInfo.id}")
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
Log.e("FileDownloadManager", "Checksum MISMATCH for flashcard ${asset.filename}")
|
||||||
Log.e("FileDownloadManager", context.getString(
|
Log.e("FileDownloadManager", context.getString(
|
||||||
R.string.text_checksum_mismatch_for_expected_got,
|
R.string.text_checksum_mismatch_for_expected_got,
|
||||||
asset.filename,
|
asset.filename,
|
||||||
asset.checksumSha256,
|
asset.checksumSha256,
|
||||||
computedChecksum
|
computedChecksum
|
||||||
))
|
))
|
||||||
|
Log.d("FileDownloadManager", "Deleting corrupted file: ${localFile.absolutePath}")
|
||||||
localFile.delete()
|
localFile.delete()
|
||||||
throw Exception("Checksum verification failed for ${asset.filename}")
|
throw Exception("Checksum verification failed for ${asset.filename}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("FileDownloadManager", "Error downloading flashcard collection", e)
|
Log.e("FileDownloadManager", "Error downloading flashcard collection from $fileUrl", e)
|
||||||
|
Log.e("FileDownloadManager", "Target path was: ${localFile.absolutePath}")
|
||||||
if (localFile.exists()) {
|
if (localFile.exists()) {
|
||||||
|
Log.d("FileDownloadManager", "Cleaning up partial download: ${localFile.absolutePath}")
|
||||||
localFile.delete()
|
localFile.delete()
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
@@ -312,14 +371,190 @@ class FileDownloadManager(private val context: Context) {
|
|||||||
* Checks if a newer version is available for a flashcard collection.
|
* Checks if a newer version is available for a flashcard collection.
|
||||||
*/
|
*/
|
||||||
fun isNewerFlashcardVersionAvailable(flashcardInfo: FlashcardCollectionInfo): Boolean {
|
fun isNewerFlashcardVersionAvailable(flashcardInfo: FlashcardCollectionInfo): Boolean {
|
||||||
|
Log.d("FileDownloadManager", "=== isNewerFlashcardVersionAvailable() called ===")
|
||||||
|
Log.d("FileDownloadManager", "Checking flashcard: ${flashcardInfo.id}")
|
||||||
val localVersion = sharedPreferences.getString(flashcardInfo.id, "0.0.0") ?: "0.0.0"
|
val localVersion = sharedPreferences.getString(flashcardInfo.id, "0.0.0") ?: "0.0.0"
|
||||||
return compareVersions(flashcardInfo.version, localVersion) > 0
|
Log.d("FileDownloadManager", "Local version: $localVersion, Server version: ${flashcardInfo.version}")
|
||||||
|
val result = compareVersions(flashcardInfo.version, localVersion) > 0
|
||||||
|
Log.d("FileDownloadManager", "Newer version available: $result")
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the local version of a flashcard collection.
|
* Gets the local version of a flashcard collection.
|
||||||
*/
|
*/
|
||||||
fun getFlashcardLocalVersion(collectionId: String): String {
|
fun getFlashcardLocalVersion(collectionId: String): String {
|
||||||
return sharedPreferences.getString(collectionId, "0.0.0") ?: "0.0.0"
|
val version = sharedPreferences.getString(collectionId, "0.0.0") ?: "0.0.0"
|
||||||
|
Log.d("FileDownloadManager", "getFlashcardLocalVersion($collectionId) = $version")
|
||||||
|
return version
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// ===== Vocab Packs =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the vocabulary-pack manifest (vocab_manifest.json).
|
||||||
|
* Unwraps the top-level [VocabManifestResponse] and returns the `lists` array.
|
||||||
|
*/
|
||||||
|
suspend fun fetchVocabManifest(): List<VocabCollectionInfo>? = withContext(Dispatchers.IO) {
|
||||||
|
Log.d("FileDownloadManager", "=== fetchVocabManifest() called ===")
|
||||||
|
Log.d("FileDownloadManager", "Fetching vocab manifest from: ${DownloadConfig.FLASHCARDS_MANIFEST_URL}")
|
||||||
|
try {
|
||||||
|
val response = flashcardApiService.getVocabManifest().execute()
|
||||||
|
Log.d("FileDownloadManager", "Vocab manifest response - isSuccessful: ${response.isSuccessful}, code: ${response.code()}")
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val manifest = response.body()
|
||||||
|
val lists = manifest?.lists
|
||||||
|
Log.d("FileDownloadManager", "Vocab manifest parsed successfully, lists count: ${lists?.size ?: 0}")
|
||||||
|
lists?.forEach { list ->
|
||||||
|
Log.d("FileDownloadManager", " - Vocab list: ${list.id}, name: ${list.name}, version: ${list.version}, filename: ${list.filename}")
|
||||||
|
}
|
||||||
|
lists
|
||||||
|
} else {
|
||||||
|
val errorMessage = response.errorBody()?.string() ?: "HTTP ${response.code()}"
|
||||||
|
Log.e("FileDownloadManager", "Failed to fetch vocab manifest: $errorMessage")
|
||||||
|
throw Exception(context.getString(R.string.failed_to_fetch_manifest, errorMessage))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("FileDownloadManager", "Error fetching vocab manifest", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a single vocabulary pack file and verifies its SHA-256 checksum.
|
||||||
|
* The file is stored at [filesDir]/[DownloadConfig.LOCAL_FLASHCARDS_PATH]/[filename].
|
||||||
|
*
|
||||||
|
* @return true on success, false (or throws) on failure.
|
||||||
|
*/
|
||||||
|
suspend fun downloadVocabCollection(
|
||||||
|
info: VocabCollectionInfo,
|
||||||
|
onProgress: (Float) -> Unit = {}
|
||||||
|
): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
Log.d("FileDownloadManager", "=== downloadVocabCollection() called ===")
|
||||||
|
Log.d("FileDownloadManager", "Vocab info - id: ${info.id}, name: ${info.name}, version: ${info.version}")
|
||||||
|
Log.d("FileDownloadManager", "Vocab filename: ${info.filename}, size: ${info.sizeBytes} bytes")
|
||||||
|
|
||||||
|
val fileUrl = DownloadConfig.getFlashcardAssetUrl(info.filename)
|
||||||
|
val localFile = File(context.filesDir, "${DownloadConfig.LOCAL_FLASHCARDS_PATH}/${info.filename}")
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Remote URL: $fileUrl")
|
||||||
|
Log.d("FileDownloadManager", "Local file path: ${localFile.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "Local file parent: ${localFile.parentFile?.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "Expected checksum: ${info.checksumSha256}")
|
||||||
|
|
||||||
|
// Create subdirectory if it doesn't exist
|
||||||
|
val parentDir = localFile.parentFile
|
||||||
|
if (parentDir != null && !parentDir.exists()) {
|
||||||
|
Log.d("FileDownloadManager", "Creating subdirectory: ${parentDir.absolutePath}")
|
||||||
|
val created = parentDir.mkdirs()
|
||||||
|
Log.d("FileDownloadManager", "Subdirectory created: $created")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log.d("FileDownloadManager", "Creating HTTP request for vocab pack...")
|
||||||
|
val client = OkHttpClient()
|
||||||
|
val request = Request.Builder().url(fileUrl).build()
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Response received - code: ${response.code}, message: ${response.message}")
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
val errorMessage = context.getString(
|
||||||
|
R.string.text_download_failed_http,
|
||||||
|
response.code,
|
||||||
|
response.message
|
||||||
|
)
|
||||||
|
Log.e("FileDownloadManager", errorMessage)
|
||||||
|
throw Exception(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = response.body
|
||||||
|
val contentLength = body.contentLength().takeIf { it > 0 } ?: info.sizeBytes
|
||||||
|
Log.d("FileDownloadManager", "Content length from header: ${body.contentLength()}, using: $contentLength bytes")
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Starting vocab pack download to: ${localFile.absolutePath}")
|
||||||
|
FileOutputStream(localFile).use { output ->
|
||||||
|
body.byteStream().use { input ->
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var bytesRead: Int
|
||||||
|
var totalBytesRead: Long = 0
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
|
||||||
|
while (input.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
output.write(buffer, 0, bytesRead)
|
||||||
|
digest.update(buffer, 0, bytesRead)
|
||||||
|
totalBytesRead += bytesRead
|
||||||
|
if (contentLength > 0) onProgress(totalBytesRead.toFloat() / contentLength)
|
||||||
|
}
|
||||||
|
output.flush()
|
||||||
|
|
||||||
|
val computedChecksum = digest.digest().joinToString("") { "%02X".format(it) }
|
||||||
|
|
||||||
|
Log.d("FileDownloadManager", "Download complete. Total bytes read: $totalBytesRead")
|
||||||
|
Log.d("FileDownloadManager", "Computed checksum: $computedChecksum")
|
||||||
|
Log.d("FileDownloadManager", "Expected checksum: ${info.checksumSha256}")
|
||||||
|
|
||||||
|
if (computedChecksum.equals(info.checksumSha256, ignoreCase = true)) {
|
||||||
|
Log.d("FileDownloadManager", "Checksum VERIFIED for vocab pack ${info.filename}")
|
||||||
|
Log.d("FileDownloadManager", "File saved successfully at: ${localFile.absolutePath}")
|
||||||
|
Log.d("FileDownloadManager", "File exists: ${localFile.exists()}, size: ${localFile.length()} bytes")
|
||||||
|
sharedPreferences.edit(commit = true) {
|
||||||
|
putString("vocab_${info.id}", info.version.toString())
|
||||||
|
}
|
||||||
|
Log.d("FileDownloadManager", "Saved version ${info.version} for vocab_${info.id}")
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
Log.e("FileDownloadManager", "Checksum MISMATCH for vocab pack ${info.filename}")
|
||||||
|
Log.e("FileDownloadManager",
|
||||||
|
context.getString(
|
||||||
|
R.string.text_checksum_mismatch_for_expected_got,
|
||||||
|
info.filename,
|
||||||
|
info.checksumSha256,
|
||||||
|
computedChecksum
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Log.d("FileDownloadManager", "Deleting corrupted file: ${localFile.absolutePath}")
|
||||||
|
localFile.delete()
|
||||||
|
throw Exception("Checksum verification failed for ${info.filename}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("FileDownloadManager", "Error downloading vocab pack from $fileUrl", e)
|
||||||
|
Log.e("FileDownloadManager", "Target path was: ${localFile.absolutePath}")
|
||||||
|
if (localFile.exists()) {
|
||||||
|
Log.d("FileDownloadManager", "Cleaning up partial download: ${localFile.absolutePath}")
|
||||||
|
localFile.delete()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the local file for this collection exists. */
|
||||||
|
fun isVocabCollectionDownloaded(info: VocabCollectionInfo): Boolean {
|
||||||
|
val localFile = File(context.filesDir, "${DownloadConfig.LOCAL_FLASHCARDS_PATH}/${info.filename}")
|
||||||
|
val exists = localFile.exists()
|
||||||
|
Log.d("FileDownloadManager", "isVocabCollectionDownloaded(${info.id}): $exists (path: ${localFile.absolutePath})")
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the server version is newer than the locally saved version. */
|
||||||
|
fun isNewerVocabVersionAvailable(info: VocabCollectionInfo): Boolean {
|
||||||
|
Log.d("FileDownloadManager", "=== isNewerVocabVersionAvailable() called ===")
|
||||||
|
Log.d("FileDownloadManager", "Checking vocab: ${info.id} (${info.name})")
|
||||||
|
val localVersion = sharedPreferences.getString("vocab_${info.id}", "0") ?: "0"
|
||||||
|
val serverVersion = info.version.toString().toIntOrNull() ?: 0
|
||||||
|
val localVersionInt = localVersion.toIntOrNull() ?: 0
|
||||||
|
Log.d("FileDownloadManager", "Local version: $localVersionInt, Server version: $serverVersion")
|
||||||
|
val result = serverVersion > localVersionInt
|
||||||
|
Log.d("FileDownloadManager", "Newer version available: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the locally saved version number string for a vocab pack (default "0"). */
|
||||||
|
fun getVocabLocalVersion(packId: String): String {
|
||||||
|
val version = sharedPreferences.getString("vocab_$packId", "0") ?: "0"
|
||||||
|
Log.d("FileDownloadManager", "getVocabLocalVersion($packId) = $version")
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
package eu.gaudian.translator.model.communication
|
package eu.gaudian.translator.model.communication.files_download
|
||||||
|
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Url
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API service for fetching flashcard collection manifests and downloading files.
|
* API service for flashcard / vocabulary-pack downloads.
|
||||||
|
* Base URL should be set to DownloadConfig.POLLY_BASE_URL
|
||||||
*/
|
*/
|
||||||
interface FlashcardApiService {
|
interface FlashcardApiService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the flashcard collection manifest from the server.
|
* Fetches the vocab manifest using the full URL from DownloadConfig.
|
||||||
*/
|
*/
|
||||||
@GET("flashcard-collections/manifest.json")
|
@GET
|
||||||
fun getFlashcardManifest(): Call<FlashcardManifestResponse>
|
fun getVocabManifest(@Url url: String = DownloadConfig.FLASHCARDS_MANIFEST_URL): Call<VocabManifestResponse>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,68 @@
|
|||||||
package eu.gaudian.translator.model.communication
|
package eu.gaudian.translator.model.communication.files_download
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// New: vocab_manifest.json schema
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data class representing the flashcard collection manifest response from the server.
|
* Top-level wrapper returned by vocab_manifest.json.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "manifest_version": "1.0",
|
||||||
|
* "updated_at": "…",
|
||||||
|
* "lists": [ … ]
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
|
data class VocabManifestResponse(
|
||||||
|
@SerializedName("manifest_version") val manifestVersion: String = "",
|
||||||
|
@SerializedName("updated_at") val updatedAt: String = "",
|
||||||
|
@SerializedName("lists") val lists: List<VocabCollectionInfo> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One entry inside the `lists` array of vocab_manifest.json.
|
||||||
|
*/
|
||||||
|
data class VocabCollectionInfo(
|
||||||
|
@SerializedName("id") val id: String,
|
||||||
|
@SerializedName("name") val name: String,
|
||||||
|
@SerializedName("description") val description: String,
|
||||||
|
@SerializedName("filename") val filename: String,
|
||||||
|
/** [lang_first_id, lang_second_id] matching Language IDs in the app */
|
||||||
|
@SerializedName("language_ids") val languageIds: List<Int>,
|
||||||
|
@SerializedName("category") val category: String,
|
||||||
|
@SerializedName("item_count") val itemCount: Int,
|
||||||
|
@SerializedName("emoji") val emoji: String,
|
||||||
|
@SerializedName("version") val version: Int,
|
||||||
|
/** CEFR difficulty level: A1, A2, B1, B2, C1, C2 (empty string if not set) */
|
||||||
|
@SerializedName("level") val level: String = "",
|
||||||
|
@SerializedName("size_bytes") val sizeBytes: Long,
|
||||||
|
@SerializedName("checksum_sha256") val checksumSha256: String,
|
||||||
|
@SerializedName("created_at") val createdAt: String,
|
||||||
|
@SerializedName("updated_at") val updatedAt: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Legacy models (kept for backward compatibility with the old manifest.json
|
||||||
|
// dictionary download path)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
data class FlashcardManifestResponse(
|
data class FlashcardManifestResponse(
|
||||||
@SerializedName("collections")
|
@SerializedName("collections")
|
||||||
val collections: List<FlashcardCollectionInfo>
|
val collections: List<FlashcardCollectionInfo>
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Data class representing information about a downloadable flashcard collection.
|
|
||||||
*/
|
|
||||||
data class FlashcardCollectionInfo(
|
data class FlashcardCollectionInfo(
|
||||||
@SerializedName("id")
|
@SerializedName("id") val id: String,
|
||||||
val id: String,
|
@SerializedName("name") val name: String,
|
||||||
@SerializedName("name")
|
@SerializedName("description") val description: String,
|
||||||
val name: String,
|
@SerializedName("version") val version: String,
|
||||||
@SerializedName("description")
|
@SerializedName("asset") val asset: FlashcardAsset
|
||||||
val description: String,
|
|
||||||
@SerializedName("version")
|
|
||||||
val version: String,
|
|
||||||
@SerializedName("asset")
|
|
||||||
val asset: FlashcardAsset
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Data class representing an asset file within a flashcard collection.
|
|
||||||
*/
|
|
||||||
data class FlashcardAsset(
|
data class FlashcardAsset(
|
||||||
@SerializedName("filename")
|
@SerializedName("filename") val filename: String,
|
||||||
val filename: String,
|
@SerializedName("size_bytes") val sizeBytes: Long,
|
||||||
@SerializedName("size_bytes")
|
@SerializedName("checksum_sha256") val checksumSha256: String
|
||||||
val sizeBytes: Long,
|
|
||||||
@SerializedName("checksum_sha256")
|
|
||||||
val checksumSha256: String
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
package eu.gaudian.translator.model.communication
|
package eu.gaudian.translator.model.communication.files_download
|
||||||
|
|
||||||
|
import eu.gaudian.translator.model.communication.ManifestResponse
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Url
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API service for fetching the manifest and downloading files.
|
* API service for fetching the dictionary manifest.
|
||||||
|
* Base URL should be set to DownloadConfig.DICTIONARIES_BASE_URL
|
||||||
*/
|
*/
|
||||||
interface ManifestApiService {
|
interface ManifestApiService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the manifest from the server.
|
* Fetches the manifest from the server using the full URL.
|
||||||
*/
|
*/
|
||||||
@GET("manifest.json")
|
@GET
|
||||||
fun getManifest(): Call<ManifestResponse>
|
fun getManifest(@Url url: String = DownloadConfig.DICTIONARIES_MANIFEST_URL): Call<ManifestResponse>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import android.content.Context
|
|||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.communication.Asset
|
import eu.gaudian.translator.model.communication.Asset
|
||||||
import eu.gaudian.translator.model.communication.FileDownloadManager
|
|
||||||
import eu.gaudian.translator.model.communication.FileInfo
|
import eu.gaudian.translator.model.communication.FileInfo
|
||||||
import eu.gaudian.translator.model.communication.ManifestResponse
|
import eu.gaudian.translator.model.communication.ManifestResponse
|
||||||
|
import eu.gaudian.translator.model.communication.files_download.FileDownloadManager
|
||||||
import eu.gaudian.translator.utils.Log
|
import eu.gaudian.translator.utils.Log
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ enum class StatusMessageId(
|
|||||||
ERROR_VOCABULARY_IMPORT_FAILED(R.string.message_error_vocabulary_import_failed, MessageDisplayType.ERROR, 5),
|
ERROR_VOCABULARY_IMPORT_FAILED(R.string.message_error_vocabulary_import_failed, MessageDisplayType.ERROR, 5),
|
||||||
SUCCESS_ITEMS_MERGED(R.string.message_success_items_merged, MessageDisplayType.SUCCESS, 3),
|
SUCCESS_ITEMS_MERGED(R.string.message_success_items_merged, MessageDisplayType.SUCCESS, 3),
|
||||||
SUCCESS_ITEMS_ADDED(R.string.message_success_items_added, MessageDisplayType.SUCCESS, 3),
|
SUCCESS_ITEMS_ADDED(R.string.message_success_items_added, MessageDisplayType.SUCCESS, 3),
|
||||||
|
SUCCESS_All_ITEMS_IMPORTED(R.string.message_success_all_items_imported, MessageDisplayType.SUCCESS, 3),
|
||||||
ERROR_ITEMS_ADD_FAILED(R.string.message_error_items_add_failed, MessageDisplayType.ERROR, 5),
|
ERROR_ITEMS_ADD_FAILED(R.string.message_error_items_add_failed, MessageDisplayType.ERROR, 5),
|
||||||
SUCCESS_ITEMS_DELETED(R.string.message_success_items_deleted, MessageDisplayType.SUCCESS, 3),
|
SUCCESS_ITEMS_DELETED(R.string.message_success_items_deleted, MessageDisplayType.SUCCESS, 3),
|
||||||
ERROR_ITEMS_DELETE_FAILED(R.string.message_error_items_delete_failed, MessageDisplayType.ERROR, 5),
|
ERROR_ITEMS_DELETE_FAILED(R.string.message_error_items_delete_failed, MessageDisplayType.ERROR, 5),
|
||||||
@@ -66,6 +67,7 @@ enum class StatusMessageId(
|
|||||||
// API Key related
|
// API Key related
|
||||||
ERROR_API_KEY_MISSING(R.string.message_error_api_key_missing, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS),
|
ERROR_API_KEY_MISSING(R.string.message_error_api_key_missing, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS),
|
||||||
ERROR_API_KEY_INVALID(R.string.message_error_api_key_invalid, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS),
|
ERROR_API_KEY_INVALID(R.string.message_error_api_key_invalid, MessageDisplayType.ERROR, 0, MessageAction.NAVIGATE_TO_API_KEYS),
|
||||||
|
ERROR_NO_MODEL_CONFIGURED(R.string.message_error_no_model_configured, MessageDisplayType.ERROR, 5),
|
||||||
|
|
||||||
// Translation related
|
// Translation related
|
||||||
LOADING_TRANSLATING(R.string.message_loading_translating, MessageDisplayType.LOADING, 0),
|
LOADING_TRANSLATING(R.string.message_loading_translating, MessageDisplayType.LOADING, 0),
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ class TranslationService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun libreTranslate(text: String, source: String?, target: String, alternatives: Int = 1): Result<String> = withContext(Dispatchers.IO) {
|
// Public method to directly use LibreTranslate (bypasses AI)
|
||||||
|
suspend fun libreTranslate(text: String, source: String?, target: String, alternatives: Int = 1): Result<String> = withContext(Dispatchers.IO) {
|
||||||
|
Log.d("libreTranslate: $text, $source, $target")
|
||||||
try {
|
try {
|
||||||
val json = org.json.JSONObject().apply {
|
val json = org.json.JSONObject().apply {
|
||||||
put("q", text)
|
put("q", text)
|
||||||
|
|||||||
@@ -82,19 +82,14 @@ val LocalConnectionConfigured = compositionLocalOf { true }
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
|
||||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||||
private var isReady = false
|
private var isReady = false
|
||||||
private var isUiLoaded = false
|
private var isUiLoaded = false
|
||||||
private var isInitializing = true
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen().apply {
|
installSplashScreen().apply {
|
||||||
// The splash screen will now correctly wait until isReady is true
|
|
||||||
setKeepOnScreenCondition { !isReady }
|
setKeepOnScreenCondition { !isReady }
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -104,28 +99,22 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
// Show UI immediately and load data in background
|
|
||||||
setContent {
|
setContent {
|
||||||
AppTheme(settingsViewModel = settingsViewModel) {
|
AppTheme(settingsViewModel = settingsViewModel) {
|
||||||
TranslatorApp(settingsViewModel = settingsViewModel)
|
TranslatorApp(settingsViewModel = settingsViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark UI as loaded immediately after setContent
|
|
||||||
isUiLoaded = true
|
isUiLoaded = true
|
||||||
|
|
||||||
// Start initialization in background without blocking UI
|
|
||||||
initializeData()
|
initializeData()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeData() {
|
private fun initializeData() {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
// Get repositories from the Application instance (lazy initialization)
|
|
||||||
val myApp = application as MyApplication
|
val myApp = application as MyApplication
|
||||||
val languageRepository = myApp.languageRepository
|
val languageRepository = myApp.languageRepository
|
||||||
val apiRepository = myApp.apiRepository
|
val apiRepository = myApp.apiRepository
|
||||||
|
|
||||||
// Perform initialization in parallel where possible
|
|
||||||
val languageJob = launch {
|
val languageJob = launch {
|
||||||
languageRepository.initializeDefaultLanguages()
|
languageRepository.initializeDefaultLanguages()
|
||||||
languageRepository.initializeAllLanguages()
|
languageRepository.initializeAllLanguages()
|
||||||
@@ -135,13 +124,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
apiRepository.initialInit()
|
apiRepository.initialInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for both to complete
|
|
||||||
languageJob.join()
|
languageJob.join()
|
||||||
apiJob.join()
|
apiJob.join()
|
||||||
|
|
||||||
// Signal readiness after all work is done.
|
|
||||||
isReady = true
|
isReady = true
|
||||||
isInitializing = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,10 +135,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
@SuppressLint("LocalContextResourcesRead")
|
@SuppressLint("LocalContextResourcesRead")
|
||||||
@Composable
|
@Composable
|
||||||
fun TranslatorApp(
|
fun TranslatorApp(settingsViewModel: SettingsViewModel) {
|
||||||
settingsViewModel: SettingsViewModel
|
|
||||||
) {
|
|
||||||
|
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val statusViewModel: StatusViewModel = hiltViewModel(activity)
|
val statusViewModel: StatusViewModel = hiltViewModel(activity)
|
||||||
val statusMessageService = StatusMessageService
|
val statusMessageService = StatusMessageService
|
||||||
@@ -179,7 +162,6 @@ fun TranslatorApp(
|
|||||||
showExitDialog = true
|
showExitDialog = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (showExitDialog) {
|
if (showExitDialog) {
|
||||||
AppAlertDialog(
|
AppAlertDialog(
|
||||||
onDismissRequest = { showExitDialog = false },
|
onDismissRequest = { showExitDialog = false },
|
||||||
@@ -188,7 +170,6 @@ fun TranslatorApp(
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
showExitDialog = false
|
showExitDialog = false
|
||||||
// Minimize the app similar to default back at root behavior
|
|
||||||
activity.moveTaskToBack(true)
|
activity.moveTaskToBack(true)
|
||||||
}) {
|
}) {
|
||||||
Text(stringResource(R.string.quit))
|
Text(stringResource(R.string.quit))
|
||||||
@@ -202,7 +183,6 @@ fun TranslatorApp(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for app updates and show "What's New" dialog if needed
|
|
||||||
var showWhatsNewDialog by remember { mutableStateOf(false) }
|
var showWhatsNewDialog by remember { mutableStateOf(false) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val changelogEntries = context.resources.getStringArray(R.array.changelog_entries)
|
val changelogEntries = context.resources.getStringArray(R.array.changelog_entries)
|
||||||
@@ -210,7 +190,6 @@ fun TranslatorApp(
|
|||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
try {
|
try {
|
||||||
// Only check for updates if the intro is completed
|
|
||||||
if (introCompleted) {
|
if (introCompleted) {
|
||||||
val currentVersion = BuildConfig.VERSION_NAME
|
val currentVersion = BuildConfig.VERSION_NAME
|
||||||
val hasSeenCurrentVersion = settingsViewModel.hasSeenCurrentVersion(currentVersion)
|
val hasSeenCurrentVersion = settingsViewModel.hasSeenCurrentVersion(currentVersion)
|
||||||
@@ -253,19 +232,25 @@ fun TranslatorApp(
|
|||||||
val currentDestination = navBackStackEntry?.destination
|
val currentDestination = navBackStackEntry?.destination
|
||||||
val selectedScreen = Screen.fromDestination(currentDestination)
|
val selectedScreen = Screen.fromDestination(currentDestination)
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
val currentRoute = currentDestination?.route
|
||||||
|
val isHiddenByHierarchy = currentDestination?.hierarchy?.any { destination ->
|
||||||
destination.route in setOf(
|
destination.route in setOf(
|
||||||
Screen.Translation.route,
|
Screen.Translation.route,
|
||||||
Screen.Dictionary.route,
|
Screen.Dictionary.route,
|
||||||
Screen.Exercises.route,
|
Screen.Exercises.route,
|
||||||
Screen.Settings.route
|
Screen.Settings.route,
|
||||||
|
Screen.Corrector.route
|
||||||
)
|
)
|
||||||
} == true || currentDestination?.route in setOf(
|
} == true
|
||||||
"start_exercise",
|
val isBottomBarHidden = isHiddenByHierarchy || currentRoute in setOf(
|
||||||
"new_word",
|
"new_word",
|
||||||
"new_word_review",
|
"new_word_review",
|
||||||
"vocabulary_detail/{itemId}"
|
"vocabulary_detail/{itemId}",
|
||||||
)
|
"daily_review",
|
||||||
|
"explore_packs"
|
||||||
|
) || currentRoute?.startsWith("start_exercise") == true
|
||||||
|
|| currentRoute?.startsWith("vocabulary_exercise") == true
|
||||||
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
||||||
|
|
||||||
BottomNavigationBar(
|
BottomNavigationBar(
|
||||||
@@ -278,12 +263,11 @@ fun TranslatorApp(
|
|||||||
Screen.Translation,
|
Screen.Translation,
|
||||||
Screen.Dictionary,
|
Screen.Dictionary,
|
||||||
Screen.Settings,
|
Screen.Settings,
|
||||||
Screen.Exercises
|
Screen.Exercises,
|
||||||
|
Screen.Corrector
|
||||||
)
|
)
|
||||||
|
|
||||||
// Always reset the selected section to its root and clear back stack between sections
|
|
||||||
if (inSameSection) {
|
if (inSameSection) {
|
||||||
// If already within the same section, ensure we are at its graph root
|
|
||||||
navController.navigate(screen.route) {
|
navController.navigate(screen.route) {
|
||||||
popUpTo(screen.route) {
|
popUpTo(screen.route) {
|
||||||
inclusive = false
|
inclusive = false
|
||||||
@@ -298,9 +282,8 @@ fun TranslatorApp(
|
|||||||
restoreState = false
|
restoreState = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Switching sections: clear entire back stack to start to avoid back navigation results
|
|
||||||
navController.navigate(screen.route) {
|
navController.navigate(screen.route) {
|
||||||
popUpTo(0) { // Pop everything
|
popUpTo(0) {
|
||||||
inclusive = true
|
inclusive = true
|
||||||
saveState = false
|
saveState = false
|
||||||
}
|
}
|
||||||
@@ -335,8 +318,7 @@ fun TranslatorApp(
|
|||||||
statusState = statusState,
|
statusState = statusState,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onDismiss = { statusMessageService.trigger(StatusAction.HideMessageBar) },
|
onDismiss = { statusMessageService.trigger(StatusAction.HideMessageBar) },
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
AppNavHost(
|
AppNavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
@@ -388,10 +370,8 @@ private fun AppTheme(
|
|||||||
val window = (view.context as Activity).window
|
val window = (view.context as Activity).window
|
||||||
val windowInsetsController = WindowInsetsControllerCompat(window, view)
|
val windowInsetsController = WindowInsetsControllerCompat(window, view)
|
||||||
|
|
||||||
// We must keep this for older Android version!!!
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
window.statusBarColor = colorScheme.surface.toArgb()
|
window.statusBarColor = colorScheme.surface.toArgb()
|
||||||
//Elevation must be the same as BottomNavigationBar
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
window.navigationBarColor = colorScheme.surfaceColorAtElevation(8.dp).toArgb()
|
window.navigationBarColor = colorScheme.surfaceColorAtElevation(8.dp).toArgb()
|
||||||
|
|
||||||
@@ -438,6 +418,4 @@ private fun AppTheme(
|
|||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,27 +20,29 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import androidx.navigation.navigation
|
import androidx.navigation.navigation
|
||||||
import eu.gaudian.translator.model.VocabularyStage
|
import eu.gaudian.translator.model.VocabularyStage
|
||||||
|
import eu.gaudian.translator.view.categories.CategoryDetailScreen
|
||||||
|
import eu.gaudian.translator.view.categories.CategoryListScreen
|
||||||
import eu.gaudian.translator.view.composable.Screen
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
|
import eu.gaudian.translator.view.dictionary.CorrectionScreen
|
||||||
import eu.gaudian.translator.view.dictionary.DictionaryResultScreen
|
import eu.gaudian.translator.view.dictionary.DictionaryResultScreen
|
||||||
|
import eu.gaudian.translator.view.dictionary.DictionaryScreen
|
||||||
import eu.gaudian.translator.view.dictionary.EtymologyResultScreen
|
import eu.gaudian.translator.view.dictionary.EtymologyResultScreen
|
||||||
import eu.gaudian.translator.view.dictionary.MainDictionaryScreen
|
import eu.gaudian.translator.view.home.DailyReviewScreen
|
||||||
import eu.gaudian.translator.view.exercises.ExerciseSessionScreen
|
|
||||||
import eu.gaudian.translator.view.exercises.MainExerciseScreen
|
|
||||||
import eu.gaudian.translator.view.exercises.StartExerciseScreen
|
|
||||||
import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen
|
|
||||||
import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen
|
|
||||||
import eu.gaudian.translator.view.home.HomeScreen
|
import eu.gaudian.translator.view.home.HomeScreen
|
||||||
import eu.gaudian.translator.view.library.LibraryScreen
|
import eu.gaudian.translator.view.library.LibraryScreen
|
||||||
|
import eu.gaudian.translator.view.new_ecercises.ExerciseSessionScreen
|
||||||
|
import eu.gaudian.translator.view.new_ecercises.MainExerciseScreen
|
||||||
|
import eu.gaudian.translator.view.new_ecercises.StartExerciseScreen
|
||||||
|
import eu.gaudian.translator.view.new_ecercises.YouTubeBrowserScreen
|
||||||
|
import eu.gaudian.translator.view.new_ecercises.YouTubeExerciseScreen
|
||||||
import eu.gaudian.translator.view.settings.DictionaryOptionsScreen
|
import eu.gaudian.translator.view.settings.DictionaryOptionsScreen
|
||||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
|
||||||
import eu.gaudian.translator.view.settings.TranslationSettingsScreen
|
import eu.gaudian.translator.view.settings.TranslationSettingsScreen
|
||||||
import eu.gaudian.translator.view.settings.settingsGraph
|
import eu.gaudian.translator.view.settings.settingsGraph
|
||||||
import eu.gaudian.translator.view.stats.StatsScreen
|
import eu.gaudian.translator.view.stats.StatsScreen
|
||||||
import eu.gaudian.translator.view.translation.TranslationScreen
|
import eu.gaudian.translator.view.translation.TranslationScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
|
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
|
import eu.gaudian.translator.view.vocabulary.ExplorePacksScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.CategoryListScreen
|
import eu.gaudian.translator.view.vocabulary.LanguageJourneyScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
|
|
||||||
import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen
|
import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.NewWordScreen
|
import eu.gaudian.translator.view.vocabulary.NewWordScreen
|
||||||
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
|
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
|
||||||
@@ -53,10 +55,12 @@ import eu.gaudian.translator.view.vocabulary.VocabularySortingScreen
|
|||||||
private const val TRANSITION_DURATION = 300
|
private const val TRANSITION_DURATION = 300
|
||||||
|
|
||||||
object NavigationRoutes {
|
object NavigationRoutes {
|
||||||
|
const val DAILY_REVIEW = "daily_review"
|
||||||
const val NEW_WORD = "new_word"
|
const val NEW_WORD = "new_word"
|
||||||
const val NEW_WORD_REVIEW = "new_word_review"
|
const val NEW_WORD_REVIEW = "new_word_review"
|
||||||
const val VOCABULARY_DETAIL = "vocabulary_detail"
|
const val VOCABULARY_DETAIL = "vocabulary_detail"
|
||||||
const val START_EXERCISE = "start_exercise"
|
const val START_EXERCISE = "start_exercise"
|
||||||
|
const val START_EXERCISE_DAILY = "start_exercise_daily"
|
||||||
const val CATEGORY_DETAIL = "category_detail"
|
const val CATEGORY_DETAIL = "category_detail"
|
||||||
const val CATEGORY_LIST = "category_list_screen"
|
const val CATEGORY_LIST = "category_list_screen"
|
||||||
const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap"
|
const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap"
|
||||||
@@ -66,75 +70,65 @@ object NavigationRoutes {
|
|||||||
const val STATS_VOCABULARY_SORTING = "stats/vocabulary_sorting"
|
const val STATS_VOCABULARY_SORTING = "stats/vocabulary_sorting"
|
||||||
const val STATS_NO_GRAMMAR_ITEMS = "stats/no_grammar_items"
|
const val STATS_NO_GRAMMAR_ITEMS = "stats/no_grammar_items"
|
||||||
const val STATS_VOCABULARY_LIST = "stats/vocabulary_list"
|
const val STATS_VOCABULARY_LIST = "stats/vocabulary_list"
|
||||||
|
const val EXPLORE_PACKS = "explore_packs"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavHost(
|
fun AppNavHost(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
|
|
||||||
val mainTabRoutes = setOf(
|
val mainTabRoutes = setOf(
|
||||||
Screen.Home.route,
|
Screen.Home.route,
|
||||||
Screen.Library.route,
|
Screen.Library.route,
|
||||||
Screen.Stats.route,
|
Screen.Stats.route,
|
||||||
Screen.Translation.route,
|
|
||||||
Screen.Dictionary.route,
|
|
||||||
Screen.Exercises.route,
|
|
||||||
SettingsRoutes.LIST
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Helper to check if a route is a top-level tab
|
|
||||||
fun isTabTransition(initial: String?, target: String?): Boolean {
|
fun isTabTransition(initial: String?, target: String?): Boolean {
|
||||||
return mainTabRoutes.contains(initial) && mainTabRoutes.contains(target)
|
if (initial == null || target == null) return false
|
||||||
|
val initialIsTab = mainTabRoutes.contains(initial) ||
|
||||||
|
mainTabRoutes.any { route ->
|
||||||
|
initial == "main_${route}" || initial.startsWith("${route}_")
|
||||||
|
}
|
||||||
|
val targetIsTab = mainTabRoutes.contains(target) ||
|
||||||
|
mainTabRoutes.any { route ->
|
||||||
|
target == "main_${route}" || target.startsWith("${route}_")
|
||||||
|
}
|
||||||
|
return initialIsTab && targetIsTab
|
||||||
}
|
}
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Screen.Home.route,
|
startDestination = Screen.Home.route,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
||||||
// ENTER TRANSITION
|
|
||||||
enterTransition = {
|
enterTransition = {
|
||||||
if (isTabTransition(initialState.destination.route, targetState.destination.route)) {
|
if (isTabTransition(initialState.destination.route, targetState.destination.route)) {
|
||||||
// Tab Switch: Just Fade In (Subtle Scale for modern feel)
|
|
||||||
fadeIn(animationSpec = tween(TRANSITION_DURATION)) +
|
fadeIn(animationSpec = tween(TRANSITION_DURATION)) +
|
||||||
scaleIn(initialScale = 0.96f, animationSpec = tween(TRANSITION_DURATION))
|
scaleIn(initialScale = 0.96f, animationSpec = tween(TRANSITION_DURATION))
|
||||||
} else {
|
} else {
|
||||||
// Detail Screen: Slide in from Right
|
|
||||||
slideInHorizontally(
|
slideInHorizontally(
|
||||||
initialOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() },
|
initialOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() },
|
||||||
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
|
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
|
||||||
) + fadeIn(animationSpec = tween(TRANSITION_DURATION))
|
) + fadeIn(animationSpec = tween(TRANSITION_DURATION))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// EXIT TRANSITION
|
|
||||||
exitTransition = {
|
exitTransition = {
|
||||||
if (isTabTransition(initialState.destination.route, targetState.destination.route)) {
|
if (isTabTransition(initialState.destination.route, targetState.destination.route)) {
|
||||||
// Tab Switch: Just Fade Out
|
|
||||||
fadeOut(animationSpec = tween(TRANSITION_DURATION))
|
fadeOut(animationSpec = tween(TRANSITION_DURATION))
|
||||||
} else {
|
} else {
|
||||||
// Detail Screen: Slide out to Left
|
|
||||||
slideOutHorizontally(
|
slideOutHorizontally(
|
||||||
targetOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() },
|
targetOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() },
|
||||||
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
|
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
|
||||||
) + fadeOut(animationSpec = tween(TRANSITION_DURATION))
|
) + fadeOut(animationSpec = tween(TRANSITION_DURATION))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// POP ENTER (Pressing Back) -> Always Slide back from left
|
|
||||||
popEnterTransition = {
|
popEnterTransition = {
|
||||||
slideInHorizontally(
|
slideInHorizontally(
|
||||||
initialOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() },
|
initialOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() },
|
||||||
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
|
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
|
||||||
) + fadeIn(animationSpec = tween(TRANSITION_DURATION))
|
) + fadeIn(animationSpec = tween(TRANSITION_DURATION))
|
||||||
},
|
},
|
||||||
|
|
||||||
// POP EXIT (Pressing Back) -> Always Slide away to right
|
|
||||||
popExitTransition = {
|
popExitTransition = {
|
||||||
slideOutHorizontally(
|
slideOutHorizontally(
|
||||||
targetOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() },
|
targetOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() },
|
||||||
@@ -145,25 +139,49 @@ fun AppNavHost(
|
|||||||
composable(Screen.Home.route) {
|
composable(Screen.Home.route) {
|
||||||
HomeScreen(navController = navController)
|
HomeScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
composable(NavigationRoutes.DAILY_REVIEW) {
|
||||||
|
DailyReviewScreen(navController = navController)
|
||||||
|
}
|
||||||
composable(NavigationRoutes.NEW_WORD) {
|
composable(NavigationRoutes.NEW_WORD) {
|
||||||
NewWordScreen(navController = navController)
|
NewWordScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(NavigationRoutes.NEW_WORD_REVIEW) {
|
composable(NavigationRoutes.NEW_WORD_REVIEW) {
|
||||||
NewWordReviewScreen(navController = navController)
|
NewWordReviewScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
composable(NavigationRoutes.EXPLORE_PACKS) {
|
||||||
composable(NavigationRoutes.START_EXERCISE) {
|
ExplorePacksScreen(navController = navController)
|
||||||
StartExerciseScreen(navController = navController)
|
}
|
||||||
|
composable(
|
||||||
|
route = "${NavigationRoutes.START_EXERCISE}?categoryId={categoryId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("categoryId") {
|
||||||
|
type = NavType.StringType
|
||||||
|
nullable = true
|
||||||
|
defaultValue = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
val categoryIdString = backStackEntry.arguments?.getString("categoryId")
|
||||||
|
val categoryId = categoryIdString?.toIntOrNull()
|
||||||
|
StartExerciseScreen(
|
||||||
|
navController = navController,
|
||||||
|
preselectedCategoryId = categoryId,
|
||||||
|
dueTodayOnly = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(NavigationRoutes.START_EXERCISE_DAILY) {
|
||||||
|
StartExerciseScreen(
|
||||||
|
navController = navController,
|
||||||
|
preselectedCategoryId = null,
|
||||||
|
dueTodayOnly = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define all other navigation graphs at the same top level.
|
|
||||||
homeGraph(navController)
|
homeGraph(navController)
|
||||||
libraryGraph(navController)
|
libraryGraph(navController)
|
||||||
statsGraph(navController)
|
statsGraph(navController)
|
||||||
translationGraph(navController)
|
translationGraph(navController)
|
||||||
dictionaryGraph(navController)
|
dictionaryGraph(navController)
|
||||||
|
correctorGraph(navController)
|
||||||
exerciseGraph(navController)
|
exerciseGraph(navController)
|
||||||
settingsGraph(navController)
|
settingsGraph(navController)
|
||||||
}
|
}
|
||||||
@@ -189,9 +207,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
LibraryScreen(navController = navController)
|
LibraryScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable("vocabulary_sorting") {
|
composable("vocabulary_sorting") {
|
||||||
VocabularySortingScreen(
|
VocabularySortingScreen(navController = navController)
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable("vocabulary_detail/{itemId}") { backStackEntry ->
|
composable("vocabulary_detail/{itemId}") { backStackEntry ->
|
||||||
val itemId = backStackEntry.arguments?.getString("itemId")?.toIntOrNull()
|
val itemId = backStackEntry.arguments?.getString("itemId")?.toIntOrNull()
|
||||||
@@ -208,10 +224,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
composable("dictionary_result/{entryId}") { backStackEntry ->
|
composable("dictionary_result/{entryId}") { backStackEntry ->
|
||||||
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
|
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
|
||||||
if (entryId != null) {
|
if (entryId != null) {
|
||||||
DictionaryResultScreen(
|
DictionaryResultScreen(entryId = entryId, navController = navController)
|
||||||
entryId = entryId,
|
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Text("Error: Invalid Entry ID")
|
Text("Error: Invalid Entry ID")
|
||||||
}
|
}
|
||||||
@@ -223,23 +236,16 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
navController = navController,
|
navController = navController,
|
||||||
showDueTodayOnly = showDueTodayOnly,
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
onNavigateToItem = { item ->
|
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
|
||||||
navController.navigate("vocabulary_detail/${item.id}")
|
|
||||||
},
|
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
enableNavigationButtons = true
|
enableNavigationButtons = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable("language_progress") {
|
composable("language_progress") {
|
||||||
LanguageProgressScreen(
|
LanguageJourneyScreen(navController = navController)
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
|
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
|
||||||
VocabularyHeatmapScreen(
|
VocabularyHeatmapScreen(navController = navController)
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable("vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
|
composable("vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
|
||||||
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||||
@@ -247,14 +253,11 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
val stage = stageString?.let {
|
val stage = stageString?.let {
|
||||||
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
AllCardsListScreen(
|
AllCardsListScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
showDueTodayOnly = showDueTodayOnly,
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
stage = stage,
|
stage = stage,
|
||||||
onNavigateToItem = { item ->
|
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
|
||||||
navController.navigate("vocabulary_detail/${item.id}")
|
|
||||||
},
|
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
categoryId = 0,
|
categoryId = 0,
|
||||||
enableNavigationButtons = true
|
enableNavigationButtons = true
|
||||||
@@ -267,22 +270,15 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
navArgument("categories") { type = NavType.StringType; nullable = true },
|
navArgument("categories") { type = NavType.StringType; nullable = true },
|
||||||
navArgument("stages") { type = NavType.StringType; nullable = true },
|
navArgument("stages") { type = NavType.StringType; nullable = true },
|
||||||
navArgument("languages") { type = NavType.StringType; nullable = true },
|
navArgument("languages") { type = NavType.StringType; nullable = true },
|
||||||
navArgument("dailyOnly") {
|
navArgument("dailyOnly") { type = NavType.BoolType; defaultValue = false }
|
||||||
type = NavType.BoolType
|
|
||||||
defaultValue = false
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
) { backStackEntry ->
|
) { backStackEntry ->
|
||||||
val arguments = backStackEntry.arguments
|
val arguments = backStackEntry.arguments
|
||||||
|
|
||||||
val dailyOnly = arguments?.getBoolean("dailyOnly") ?: false
|
val dailyOnly = arguments?.getBoolean("dailyOnly") ?: false
|
||||||
|
|
||||||
val categoryIds = arguments?.getString("categories")
|
val categoryIds = arguments?.getString("categories")
|
||||||
val stageNames = arguments?.getString("stages")
|
val stageNames = arguments?.getString("stages")
|
||||||
val languageIds = arguments?.getString("languages")
|
val languageIds = arguments?.getString("languages")
|
||||||
|
|
||||||
val dailyOnlyJson = "{\"dailyOnly\": $dailyOnly}"
|
val dailyOnlyJson = "{\"dailyOnly\": $dailyOnly}"
|
||||||
|
|
||||||
VocabularyExerciseHostScreen(
|
VocabularyExerciseHostScreen(
|
||||||
categoryIdsAsJson = categoryIds,
|
categoryIdsAsJson = categoryIds,
|
||||||
stageNamesAsJson = stageNames,
|
stageNamesAsJson = stageNames,
|
||||||
@@ -292,13 +288,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable("vocabulary_exercise/{dailyOnly}?", arguments = listOf(navArgument("dailyOnly") { type = NavType.BoolType })) { _ ->
|
||||||
route = "vocabulary_exercise/{dailyOnly}?",
|
|
||||||
arguments = listOf(
|
|
||||||
navArgument("dailyOnly") { type = NavType.BoolType },
|
|
||||||
)
|
|
||||||
) { _ ->
|
|
||||||
|
|
||||||
VocabularyExerciseHostScreen(
|
VocabularyExerciseHostScreen(
|
||||||
categoryIdsAsJson = null,
|
categoryIdsAsJson = null,
|
||||||
stageNamesAsJson = null,
|
stageNamesAsJson = null,
|
||||||
@@ -308,34 +298,18 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
dailyOnlyAsJson = "{\"dailyOnly\": true}"
|
dailyOnlyAsJson = "{\"dailyOnly\": true}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable("stage_detail/{stage}", arguments = listOf(navArgument("stage") { type = NavType.EnumType(VocabularyStage::class.java) })) { backStackEntry ->
|
||||||
"stage_detail/{stage}",
|
@Suppress("DEPRECATION")
|
||||||
arguments = listOf(
|
val stage = backStackEntry.arguments?.getSerializable("stage") as VocabularyStage
|
||||||
navArgument("stage") {
|
StageDetailScreen(navController = navController, stage = stage)
|
||||||
type = NavType.EnumType(VocabularyStage::class.java)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
{ backStackEntry ->
|
|
||||||
@Suppress("DEPRECATION") val stage = backStackEntry.arguments?.getSerializable("stage") as VocabularyStage
|
|
||||||
//NOTE: can ignore warning for now, once moved away from min SDK 28, use:
|
|
||||||
// val stage = backStackEntry.arguments?.getSerializable("stage", VocabularyStage::class.java)
|
|
||||||
StageDetailScreen(
|
|
||||||
navController = navController,
|
|
||||||
stage = stage
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable("category_detail/{categoryId}") { backStackEntry ->
|
composable("category_detail/{categoryId}") { backStackEntry ->
|
||||||
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||||
|
|
||||||
if (categoryId != null) {
|
if (categoryId != null) {
|
||||||
CategoryDetailScreen(
|
CategoryDetailScreen(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
onBackClick = { navController.popBackStack() },
|
onBackClick = { navController.popBackStack() },
|
||||||
onNavigateToItem = { item ->
|
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
|
||||||
navController.navigate("vocabulary_detail/${item.id}")
|
|
||||||
},
|
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -343,37 +317,22 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
|
|||||||
composable("category_list_screen") {
|
composable("category_list_screen") {
|
||||||
CategoryListScreen(
|
CategoryListScreen(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onCategoryClicked = { categoryId ->
|
onCategoryClicked = { categoryId -> navController.navigate("category_detail/$categoryId") }
|
||||||
navController.navigate("category_detail/$categoryId")
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable("vocabulary_sorting?mode={mode}", arguments = listOf(navArgument("mode") { type = NavType.StringType; nullable = true })) { backStackEntry ->
|
||||||
route = "vocabulary_sorting?mode={mode}", // Route now accepts an optional 'mode'
|
|
||||||
arguments = listOf(
|
|
||||||
navArgument("mode") { // Define the argument
|
|
||||||
type = NavType.StringType
|
|
||||||
nullable = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) { backStackEntry ->
|
|
||||||
VocabularySortingScreen(
|
VocabularySortingScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
// Pass the argument to the screen
|
|
||||||
initialFilterMode = backStackEntry.arguments?.getString("mode")
|
initialFilterMode = backStackEntry.arguments?.getString("mode")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable("no_grammar_items") {
|
composable("no_grammar_items") {
|
||||||
NoGrammarItemsScreen(
|
NoGrammarItemsScreen(navController = navController)
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavGraphBuilder.statsGraph(
|
fun NavGraphBuilder.statsGraph(navController: NavHostController) {
|
||||||
navController: NavHostController,
|
|
||||||
) {
|
|
||||||
navigation(
|
navigation(
|
||||||
startDestination = "main_stats",
|
startDestination = "main_stats",
|
||||||
route = Screen.Stats.route
|
route = Screen.Stats.route
|
||||||
@@ -382,9 +341,7 @@ fun NavGraphBuilder.statsGraph(
|
|||||||
StatsScreen(navController = navController)
|
StatsScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable("stats/vocabulary_sorting") {
|
composable("stats/vocabulary_sorting") {
|
||||||
VocabularySortingScreen(
|
VocabularySortingScreen(navController = navController)
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
|
composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
|
||||||
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||||
@@ -393,22 +350,16 @@ fun NavGraphBuilder.statsGraph(
|
|||||||
navController = navController,
|
navController = navController,
|
||||||
showDueTodayOnly = showDueTodayOnly,
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
onNavigateToItem = { item ->
|
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
|
||||||
navController.navigate("vocabulary_detail/${item.id}")
|
|
||||||
},
|
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
enableNavigationButtons = true
|
enableNavigationButtons = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) {
|
composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) {
|
||||||
LanguageProgressScreen(
|
LanguageJourneyScreen(navController = navController)
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
|
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
|
||||||
VocabularyHeatmapScreen(
|
VocabularyHeatmapScreen(navController = navController)
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
|
composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
|
||||||
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||||
@@ -416,14 +367,11 @@ fun NavGraphBuilder.statsGraph(
|
|||||||
val stage = stageString?.let {
|
val stage = stageString?.let {
|
||||||
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
AllCardsListScreen(
|
AllCardsListScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
showDueTodayOnly = showDueTodayOnly,
|
showDueTodayOnly = showDueTodayOnly,
|
||||||
stage = stage,
|
stage = stage,
|
||||||
onNavigateToItem = { item ->
|
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
|
||||||
navController.navigate("vocabulary_detail/${item.id}")
|
|
||||||
},
|
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
categoryId = 0,
|
categoryId = 0,
|
||||||
enableNavigationButtons = true
|
enableNavigationButtons = true
|
||||||
@@ -431,14 +379,11 @@ fun NavGraphBuilder.statsGraph(
|
|||||||
}
|
}
|
||||||
composable("stats/category_detail/{categoryId}") { backStackEntry ->
|
composable("stats/category_detail/{categoryId}") { backStackEntry ->
|
||||||
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||||
|
|
||||||
if (categoryId != null) {
|
if (categoryId != null) {
|
||||||
CategoryDetailScreen(
|
CategoryDetailScreen(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
onBackClick = { navController.popBackStack() },
|
onBackClick = { navController.popBackStack() },
|
||||||
onNavigateToItem = { item ->
|
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
|
||||||
navController.navigate("vocabulary_detail/${item.id}")
|
|
||||||
},
|
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -446,29 +391,14 @@ fun NavGraphBuilder.statsGraph(
|
|||||||
composable("stats/category_list_screen") {
|
composable("stats/category_list_screen") {
|
||||||
CategoryListScreen(
|
CategoryListScreen(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onCategoryClicked = { categoryId ->
|
onCategoryClicked = { categoryId -> navController.navigate("stats/category_detail/$categoryId") }
|
||||||
navController.navigate("stats/category_detail/$categoryId")
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable("stats/vocabulary_sorting?mode={mode}", arguments = listOf(navArgument("mode") { type = NavType.StringType; nullable = true })) { backStackEntry ->
|
||||||
route = "stats/vocabulary_sorting?mode={mode}",
|
VocabularySortingScreen(navController = navController, initialFilterMode = backStackEntry.arguments?.getString("mode"))
|
||||||
arguments = listOf(
|
|
||||||
navArgument("mode") {
|
|
||||||
type = NavType.StringType
|
|
||||||
nullable = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) { backStackEntry ->
|
|
||||||
VocabularySortingScreen(
|
|
||||||
navController = navController,
|
|
||||||
initialFilterMode = backStackEntry.arguments?.getString("mode")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable("stats/no_grammar_items") {
|
composable("stats/no_grammar_items") {
|
||||||
NoGrammarItemsScreen(
|
NoGrammarItemsScreen(navController = navController)
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,15 +425,16 @@ fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
|
|||||||
route = Screen.Dictionary.route
|
route = Screen.Dictionary.route
|
||||||
) {
|
) {
|
||||||
composable("main_dictionary") {
|
composable("main_dictionary") {
|
||||||
MainDictionaryScreen(navController = navController)
|
DictionaryScreen(
|
||||||
|
navController = navController,
|
||||||
|
onEntryClick = { entry -> navController.navigate("dictionary_result/${entry.id}") },
|
||||||
|
onNavigateToOptions = { navController.navigate("dictionary_options") }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable("dictionary_result/{entryId}") { backStackEntry ->
|
composable("dictionary_result/{entryId}") { backStackEntry ->
|
||||||
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
|
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
|
||||||
if (entryId != null) {
|
if (entryId != null) {
|
||||||
DictionaryResultScreen(
|
DictionaryResultScreen(entryId = entryId, navController = navController)
|
||||||
entryId = entryId,
|
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Text("Error: Invalid Entry ID")
|
Text("Error: Invalid Entry ID")
|
||||||
}
|
}
|
||||||
@@ -514,43 +445,39 @@ fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
|
|||||||
composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
|
composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
|
||||||
val word = backStackEntry.arguments?.getString("word") ?: ""
|
val word = backStackEntry.arguments?.getString("word") ?: ""
|
||||||
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
|
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
|
||||||
EtymologyResultScreen(
|
EtymologyResultScreen(navController = navController, word = word, languageCode = languageCode)
|
||||||
navController = navController,
|
|
||||||
word = word,
|
|
||||||
languageCode = languageCode
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.correctorGraph(navController: NavHostController) {
|
||||||
|
navigation(
|
||||||
|
startDestination = "main_corrector",
|
||||||
|
route = Screen.Corrector.route
|
||||||
|
) {
|
||||||
|
composable("main_corrector") {
|
||||||
|
CorrectionScreen(navController = navController)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||||
fun NavGraphBuilder.exerciseGraph(
|
fun NavGraphBuilder.exerciseGraph(navController: NavHostController) {
|
||||||
navController: NavHostController,
|
|
||||||
) {
|
|
||||||
navigation(
|
navigation(
|
||||||
startDestination = "main_exercise",
|
startDestination = "main_exercise",
|
||||||
route = Screen.Exercises.route
|
route = Screen.Exercises.route
|
||||||
) {
|
) {
|
||||||
composable("main_exercise") {
|
composable("main_exercise") {
|
||||||
MainExerciseScreen(
|
MainExerciseScreen(navController = navController)
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable("exercise_session") {
|
composable("exercise_session") {
|
||||||
ExerciseSessionScreen(
|
ExerciseSessionScreen(navController = navController)
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable("youtube_exercise") {
|
composable("youtube_exercise") {
|
||||||
YouTubeExerciseScreen(
|
YouTubeExerciseScreen(navController = navController)
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable("youtube_browse") {
|
composable("youtube_browse") {
|
||||||
YouTubeBrowserScreen(
|
YouTubeBrowserScreen(navController = navController)
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,18 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoConnectionScreen(onSettingsClick: () -> Unit) {
|
fun NoConnectionScreen(onSettingsClick: () -> Unit, navController: NavController) {
|
||||||
|
AppTopAppBar(
|
||||||
|
title = "No Connection",
|
||||||
|
onNavigateBack = {navController.popBackStack()},
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
@@ -41,7 +46,7 @@ fun NoConnectionScreen(onSettingsClick: () -> Unit) {
|
|||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
AppButton(onClick = onSettingsClick, modifier = Modifier.padding(top = 16.dp)) {
|
AppButton(onClick = onSettingsClick, modifier = Modifier.padding(top = 16.dp)) {
|
||||||
Text(text = stringResource(id = R.string.settings_title_connection))
|
Text(text = "Configure Connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.vocabulary
|
package eu.gaudian.translator.view.categories
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -14,9 +18,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
@@ -50,6 +51,7 @@ import eu.gaudian.translator.model.TagCategory
|
|||||||
import eu.gaudian.translator.model.VocabularyFilter
|
import eu.gaudian.translator.model.VocabularyFilter
|
||||||
import eu.gaudian.translator.model.VocabularyItem
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
@@ -57,7 +59,9 @@ import eu.gaudian.translator.view.composable.PrimaryButton
|
|||||||
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
|
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
|
||||||
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
||||||
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
|
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
|
||||||
|
import eu.gaudian.translator.view.stats.widgets.CategoryProgressCircle
|
||||||
|
import eu.gaudian.translator.view.stats.widgets.ChartLegend
|
||||||
import eu.gaudian.translator.viewmodel.CategoryProgress
|
import eu.gaudian.translator.viewmodel.CategoryProgress
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.ExportImportViewModel
|
import eu.gaudian.translator.viewmodel.ExportImportViewModel
|
||||||
@@ -246,18 +250,16 @@ fun CategoryDetailScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Category Header Card with Progress and Action Buttons (animated)
|
// Category Header Card with Progress and Action Buttons (animated)
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isHeaderVisible,
|
visible = isHeaderVisible,
|
||||||
enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(),
|
enter = fadeIn() + expandVertically(),
|
||||||
exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically()
|
exit = fadeOut() + shrinkVertically()
|
||||||
) {
|
) {
|
||||||
CategoryHeaderCard(
|
CategoryHeaderCard(
|
||||||
subtitle = subtitle,
|
subtitle = subtitle,
|
||||||
categoryProgress = categoryProgress,
|
categoryProgress = categoryProgress,
|
||||||
onStartExerciseClick = {
|
onStartExerciseClick = {
|
||||||
val categories = listOf(category)
|
navController.navigate("start_exercise?categoryId=$categoryId")
|
||||||
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
|
||||||
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
|
||||||
},
|
},
|
||||||
onEditClick = {
|
onEditClick = {
|
||||||
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
||||||
@@ -315,16 +317,10 @@ fun CategoryHeaderCard(
|
|||||||
onDeleteClick: () -> Unit,
|
onDeleteClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Card(
|
AppCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f)
|
|
||||||
),
|
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -345,6 +341,8 @@ fun CategoryHeaderCard(
|
|||||||
|
|
||||||
// Progress Circle - smaller size
|
// Progress Circle - smaller size
|
||||||
if (categoryProgress != null) {
|
if (categoryProgress != null) {
|
||||||
|
|
||||||
|
|
||||||
CategoryProgressCircle(
|
CategoryProgressCircle(
|
||||||
totalItems = categoryProgress.totalItems,
|
totalItems = categoryProgress.totalItems,
|
||||||
itemsCompleted = categoryProgress.itemsCompleted,
|
itemsCompleted = categoryProgress.itemsCompleted,
|
||||||
@@ -352,7 +350,9 @@ fun CategoryHeaderCard(
|
|||||||
newItems = categoryProgress.newItems,
|
newItems = categoryProgress.newItems,
|
||||||
circleSize = 100.dp,
|
circleSize = 100.dp,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
ChartLegend()
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action Buttons
|
// Action Buttons
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("AssignedValueIsNeverRead")
|
@file:Suppress("AssignedValueIsNeverRead")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.vocabulary
|
package eu.gaudian.translator.view.categories
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -44,8 +44,8 @@ import eu.gaudian.translator.view.composable.AppScaffold
|
|||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.dialogs.AddCategoryDialog
|
import eu.gaudian.translator.view.dialogs.AddCategoryDialog
|
||||||
import eu.gaudian.translator.view.dialogs.DeleteMultipleCategoriesDialog
|
import eu.gaudian.translator.view.dialogs.DeleteMultipleCategoriesDialog
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryCircleType
|
import eu.gaudian.translator.view.stats.widgets.CategoryCircleType
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
|
import eu.gaudian.translator.view.stats.widgets.CategoryProgressCircle
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||||
|
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A compact action card with an icon and label, designed for use in rows or grids.
|
||||||
|
* Used for quick action buttons like "Explore Packs", "Import CSV", etc.
|
||||||
|
*
|
||||||
|
* @param label The text label below the icon
|
||||||
|
* @param icon The icon to display
|
||||||
|
* @param onClick Callback when the card is clicked
|
||||||
|
* @param modifier Modifier for the card
|
||||||
|
* @param height The height of the card (default 120.dp)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppActionCard(
|
||||||
|
label: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
height: Dp = 120.dp,
|
||||||
|
iconContainerSize: Dp = 48.dp,
|
||||||
|
iconSize: Dp = 24.dp
|
||||||
|
) {
|
||||||
|
AppCard(
|
||||||
|
modifier = modifier.height(height),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
CircularIconContainer(
|
||||||
|
imageVector = icon,
|
||||||
|
size = iconContainerSize,
|
||||||
|
iconSize = iconSize
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A section header label with consistent styling.
|
||||||
|
* Used for section titles like "Recently Added", etc.
|
||||||
|
*
|
||||||
|
* @param text The section title text
|
||||||
|
* @param modifier Modifier for the text
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SectionLabel(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A labeled section with an optional action button.
|
||||||
|
* Provides consistent header styling for sections with a title and optional action.
|
||||||
|
*
|
||||||
|
* @param title The section title
|
||||||
|
* @param modifier Modifier for the section header
|
||||||
|
* @param actionLabel Optional label for the action button
|
||||||
|
* @param onActionClick Optional callback for the action button
|
||||||
|
* @param content The content below the header
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LabeledSection(
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
actionLabel: String? = null,
|
||||||
|
onActionClick: (() -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
// Header row with title and optional action
|
||||||
|
if (actionLabel != null && onActionClick != null) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
SectionLabel(text = title)
|
||||||
|
androidx.compose.material3.TextButton(onClick = onActionClick) {
|
||||||
|
Text(actionLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SectionLabel(text = title)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.gaudian.translator.R
|
||||||
|
import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation
|
||||||
|
import eu.gaudian.translator.view.hints.Hint
|
||||||
|
import eu.gaudian.translator.view.hints.HintBottomSheet
|
||||||
|
import eu.gaudian.translator.view.hints.LocalShowHints
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A styled card container for displaying content with a consistent floating look.
|
||||||
|
*
|
||||||
|
* @param modifier The modifier to be applied to the card.
|
||||||
|
* @param content The content to be displayed inside the card.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String? = null,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
text: String? = null,
|
||||||
|
expandable: Boolean = false,
|
||||||
|
initiallyExpanded: Boolean = false,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
hintContent : Hint? = null,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
||||||
|
val showHints = LocalShowHints.current
|
||||||
|
|
||||||
|
val rotationState by animateFloatAsState(
|
||||||
|
targetValue = if (isExpanded) 180f else 0f,
|
||||||
|
label = "Chevron Rotation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if we need to render the header row
|
||||||
|
// Updated to include icon in the check
|
||||||
|
val hasHeader = title != null || text != null || expandable || icon != null
|
||||||
|
val canClickHeader = expandable || onClick != null
|
||||||
|
|
||||||
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showBottomSheet) {
|
||||||
|
hintContent?.let {
|
||||||
|
HintBottomSheet(
|
||||||
|
onDismissRequest = { showBottomSheet = false },
|
||||||
|
content = it,
|
||||||
|
sheetState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.shadow(
|
||||||
|
DefaultElevation,
|
||||||
|
shape = ComponentDefaults.CardShape
|
||||||
|
)
|
||||||
|
.clip(ComponentDefaults.CardClipShape)
|
||||||
|
// Animate height changes when expanding/collapsing
|
||||||
|
.animateContentSize(),
|
||||||
|
shape = ComponentDefaults.CardShape,
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
// --- Header Row ---
|
||||||
|
if (hasHeader) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = canClickHeader) {
|
||||||
|
if (expandable) {
|
||||||
|
isExpanded = !isExpanded
|
||||||
|
}
|
||||||
|
onClick?.invoke()
|
||||||
|
}
|
||||||
|
.padding(ComponentDefaults.CardPadding),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 1. Optional Icon on the left
|
||||||
|
if (icon != null) {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Title and Text Column
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
if (!title.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show spacer if both title and text exist
|
||||||
|
if (!title.isNullOrBlank() && !text.isNullOrBlank()) {
|
||||||
|
Spacer(Modifier.size(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showHints && hintContent != null) {
|
||||||
|
IconButton(onClick = { showBottomSheet = true }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = AppIcons.Help,
|
||||||
|
contentDescription = stringResource(R.string.show_hint),
|
||||||
|
tint = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Expand Chevron (Far right)
|
||||||
|
if (expandable) {
|
||||||
|
Icon(
|
||||||
|
imageVector = AppIcons.ArrowDropDown,
|
||||||
|
contentDescription = if (isExpanded) "Collapse" else "Expand",
|
||||||
|
modifier = Modifier.rotate(rotationState),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Content Area ---
|
||||||
|
if (!expandable || isExpanded) {
|
||||||
|
val contentModifier = Modifier
|
||||||
|
.padding(
|
||||||
|
start = ComponentDefaults.CardPadding,
|
||||||
|
end = ComponentDefaults.CardPadding,
|
||||||
|
bottom = ComponentDefaults.CardPadding,
|
||||||
|
// If we have a header, remove the top padding so content sits closer to the title.
|
||||||
|
// If no header (legacy behavior), keep the top padding.
|
||||||
|
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hasHeader && onClick != null) {
|
||||||
|
Column(
|
||||||
|
modifier = contentModifier.clickable { onClick() },
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = contentModifier,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AppCardPreview() {
|
||||||
|
AppCard {
|
||||||
|
Text(stringResource(R.string.this_is_the_content_inside_the_card))
|
||||||
|
PrimaryButton(onClick = { }, text = stringResource(R.string.label_continue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun AppCardPreview2() {
|
||||||
|
MaterialTheme {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// 1. Expandable Card (Initially Collapsed)
|
||||||
|
AppCard(
|
||||||
|
title = "Advanced Settings",
|
||||||
|
text = "Click to reveal more options",
|
||||||
|
expandable = true,
|
||||||
|
initiallyExpanded = false
|
||||||
|
) {
|
||||||
|
Text("Here are some hidden settings.")
|
||||||
|
Text("They are only visible when expanded.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Expandable Card (Initially Expanded)
|
||||||
|
AppCard(
|
||||||
|
title = "Translation History",
|
||||||
|
text = "Recent items",
|
||||||
|
expandable = true,
|
||||||
|
initiallyExpanded = true
|
||||||
|
) {
|
||||||
|
Text("• Hello -> Hallo")
|
||||||
|
Text("• World -> Welt")
|
||||||
|
Text("• Sun -> Sonne")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Static Card (No Title/Expand logic - Legacy behavior)
|
||||||
|
AppCard {
|
||||||
|
Text("This is a standard card without a header.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ data class FabMenuItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Deprecated("We don't want to use floating butto menus anymore")
|
||||||
@Composable
|
@Composable
|
||||||
fun AppFabMenu(
|
fun AppFabMenu(
|
||||||
items: List<FabMenuItem>,
|
items: List<FabMenuItem>,
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable icon container that displays an icon inside a shaped background.
|
||||||
|
* Used throughout the app for consistent icon presentation in cards, buttons, and action items.
|
||||||
|
*
|
||||||
|
* @param imageVector The icon to display
|
||||||
|
* @param modifier Modifier to be applied to the container
|
||||||
|
* @param size The size of the container (default 40.dp)
|
||||||
|
* @param iconSize The size of the icon itself (default 24.dp)
|
||||||
|
* @param shape The shape of the container (default RoundedCornerShape(12.dp))
|
||||||
|
* @param backgroundColor Background color of the container
|
||||||
|
* @param iconTint Tint color for the icon
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppIconContainer(
|
||||||
|
imageVector: ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp = 40.dp,
|
||||||
|
iconSize: Dp = 24.dp,
|
||||||
|
shape: androidx.compose.ui.graphics.Shape = RoundedCornerShape(12.dp),
|
||||||
|
backgroundColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
iconTint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.primary,
|
||||||
|
contentDescription: String? = null
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(shape)
|
||||||
|
.background(backgroundColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = iconTint,
|
||||||
|
modifier = Modifier.size(iconSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A circular variant of AppIconContainer.
|
||||||
|
* Convenience wrapper for circular icon containers.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CircularIconContainer(
|
||||||
|
imageVector: ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp = 48.dp,
|
||||||
|
iconSize: Dp = 24.dp,
|
||||||
|
backgroundColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
iconTint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.primary,
|
||||||
|
contentDescription: String? = null
|
||||||
|
) {
|
||||||
|
AppIconContainer(
|
||||||
|
imageVector = imageVector,
|
||||||
|
modifier = modifier,
|
||||||
|
size = size,
|
||||||
|
iconSize = iconSize,
|
||||||
|
shape = CircleShape,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
iconTint = iconTint,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -115,7 +115,7 @@ fun AppOutlinedTextField(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier,
|
||||||
label = label,
|
label = label,
|
||||||
trailingIcon = finalTrailingIcon,
|
trailingIcon = finalTrailingIcon,
|
||||||
shape = ComponentDefaults.DefaultShape,
|
shape = ComponentDefaults.DefaultShape,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ interface TabItem {
|
|||||||
val title: String
|
val title: String
|
||||||
val icon: ImageVector
|
val icon: ImageVector
|
||||||
}
|
}
|
||||||
|
@Deprecated("Migrate to new (like used in LibraryScreen")
|
||||||
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi",
|
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi",
|
||||||
"SuspiciousIndentation"
|
"SuspiciousIndentation"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A styled filled text input field.
|
||||||
|
* Different from AppOutlinedTextField - this uses a filled background style.
|
||||||
|
*
|
||||||
|
* @param value The input text to be shown in the text field.
|
||||||
|
* @param onValueChange The callback that is triggered when the input service updates the text.
|
||||||
|
* @param modifier The modifier to be applied to the text field.
|
||||||
|
* @param placeholder The placeholder text to display when the field is empty.
|
||||||
|
* @param enabled Whether the text field is enabled.
|
||||||
|
* @param readOnly Whether the text field is read-only.
|
||||||
|
* @param singleLine Whether the text field is single line.
|
||||||
|
* @param minLines Minimum number of lines.
|
||||||
|
* @param maxLines Maximum number of lines.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppTextField(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
placeholder: String? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
singleLine: Boolean = true,
|
||||||
|
minLines: Int = 1,
|
||||||
|
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||||
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val cornerRadius = 12.dp
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
placeholder = placeholder?.let {
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(cornerRadius),
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.primary,
|
||||||
|
focusedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
unfocusedTextColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
),
|
||||||
|
singleLine = singleLine,
|
||||||
|
minLines = minLines,
|
||||||
|
maxLines = maxLines,
|
||||||
|
enabled = enabled,
|
||||||
|
readOnly = readOnly,
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
trailingIcon = trailingIcon
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ fun AppTopAppBar(
|
|||||||
navigationIcon: @Composable (() -> Unit)? = null,
|
navigationIcon: @Composable (() -> Unit)? = null,
|
||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
|
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
|
||||||
hintContent: Hint? = null
|
hint: Hint? = null
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
@@ -61,7 +61,7 @@ fun AppTopAppBar(
|
|||||||
colors = colors,
|
colors = colors,
|
||||||
title = {
|
title = {
|
||||||
val showHints = LocalShowHints.current
|
val showHints = LocalShowHints.current
|
||||||
if (showHints && hintContent != null) {
|
if (showHints && hint != null) {
|
||||||
// Simplified row: keeps the title and hint icon neatly centered together
|
// Simplified row: keeps the title and hint icon neatly centered together
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -114,7 +114,7 @@ fun AppTopAppBar(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
hintContent?.let {
|
hint?.let {
|
||||||
HintBottomSheet(
|
HintBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.animation.core.spring
|
|||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -77,6 +78,7 @@ sealed class Screen(
|
|||||||
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
|
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
|
||||||
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal)
|
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal)
|
||||||
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
||||||
|
object Corrector : Screen("corrector", R.string.title_corrector, AppIcons.SpellCheck, AppIcons.SpellCheck)
|
||||||
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -88,6 +90,7 @@ sealed class Screen(
|
|||||||
val items = mutableListOf<Screen>()
|
val items = mutableListOf<Screen>()
|
||||||
items.add(Translation)
|
items.add(Translation)
|
||||||
items.add(Dictionary)
|
items.add(Dictionary)
|
||||||
|
items.add(Corrector)
|
||||||
items.add(Settings)
|
items.add(Settings)
|
||||||
if (showExperimental) {
|
if (showExperimental) {
|
||||||
items.add(Exercises)
|
items.add(Exercises)
|
||||||
@@ -258,7 +261,7 @@ fun BottomNavigationBar(
|
|||||||
.background(
|
.background(
|
||||||
brush = Brush.radialGradient(
|
brush = Brush.radialGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
|
||||||
Color.Transparent
|
Color.Transparent
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -271,6 +274,12 @@ fun BottomNavigationBar(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(playButtonSize)
|
.size(playButtonSize)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
|
// CHANGED: Added a border to give the button definition
|
||||||
|
.border(
|
||||||
|
width = 4.dp, // Adjust this thickness to your liking
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant, // Creates a nice "cutout" separation
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
.clickable {
|
.clickable {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
@@ -281,7 +290,7 @@ fun BottomNavigationBar(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
contentDescription = "Play",
|
contentDescription = "Play",
|
||||||
tint = Color.White,
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -400,4 +409,4 @@ fun BottomNavigationBarPreview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,17 @@
|
|||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonColors
|
import androidx.compose.material3.ButtonColors
|
||||||
@@ -28,26 +22,19 @@ import androidx.compose.material3.CardDefaults
|
|||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.SwitchDefaults
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@@ -57,10 +44,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.ui.theme.semanticColors
|
import eu.gaudian.translator.ui.theme.semanticColors
|
||||||
import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation
|
|
||||||
import eu.gaudian.translator.view.hints.Hint
|
|
||||||
import eu.gaudian.translator.view.hints.HintBottomSheet
|
|
||||||
import eu.gaudian.translator.view.hints.LocalShowHints
|
|
||||||
|
|
||||||
|
|
||||||
object ComponentDefaults {
|
object ComponentDefaults {
|
||||||
@@ -90,218 +73,6 @@ object ComponentDefaults {
|
|||||||
const val ALPHA_LOW = 0.3f
|
const val ALPHA_LOW = 0.3f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A styled card container for displaying content with a consistent floating look.
|
|
||||||
*
|
|
||||||
* @param modifier The modifier to be applied to the card.
|
|
||||||
* @param content The content to be displayed inside the card.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun AppCard(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
title: String? = null,
|
|
||||||
icon: ImageVector? = null,
|
|
||||||
text: String? = null,
|
|
||||||
expandable: Boolean = false,
|
|
||||||
initiallyExpanded: Boolean = false,
|
|
||||||
onClick: (() -> Unit)? = null,
|
|
||||||
hintContent : Hint? = null,
|
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
|
||||||
val showHints = LocalShowHints.current
|
|
||||||
|
|
||||||
val rotationState by animateFloatAsState(
|
|
||||||
targetValue = if (isExpanded) 180f else 0f,
|
|
||||||
label = "Chevron Rotation"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if we need to render the header row
|
|
||||||
// Updated to include icon in the check
|
|
||||||
val hasHeader = title != null || text != null || expandable || icon != null
|
|
||||||
val canClickHeader = expandable || onClick != null
|
|
||||||
|
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (showBottomSheet) {
|
|
||||||
hintContent?.let {
|
|
||||||
HintBottomSheet(
|
|
||||||
onDismissRequest = { showBottomSheet = false },
|
|
||||||
content = it,
|
|
||||||
sheetState = rememberModalBottomSheetState(
|
|
||||||
skipPartiallyExpanded = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.shadow(
|
|
||||||
DefaultElevation,
|
|
||||||
shape = ComponentDefaults.CardShape
|
|
||||||
)
|
|
||||||
.clip(ComponentDefaults.CardClipShape)
|
|
||||||
// Animate height changes when expanding/collapsing
|
|
||||||
.animateContentSize(),
|
|
||||||
shape = ComponentDefaults.CardShape,
|
|
||||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
// --- Header Row ---
|
|
||||||
if (hasHeader) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(enabled = canClickHeader) {
|
|
||||||
if (expandable) {
|
|
||||||
isExpanded = !isExpanded
|
|
||||||
}
|
|
||||||
onClick?.invoke()
|
|
||||||
}
|
|
||||||
.padding(ComponentDefaults.CardPadding),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// 1. Optional Icon on the left
|
|
||||||
if (icon != null) {
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Title and Text Column
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
if (!title.isNullOrBlank()) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only show spacer if both title and text exist
|
|
||||||
if (!title.isNullOrBlank() && !text.isNullOrBlank()) {
|
|
||||||
Spacer(Modifier.size(4.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!text.isNullOrBlank()) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showHints && hintContent != null) {
|
|
||||||
IconButton(onClick = { showBottomSheet = true }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.Help,
|
|
||||||
contentDescription = stringResource(R.string.show_hint),
|
|
||||||
tint = MaterialTheme.colorScheme.secondary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Expand Chevron (Far right)
|
|
||||||
if (expandable) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.ArrowDropDown,
|
|
||||||
contentDescription = if (isExpanded) "Collapse" else "Expand",
|
|
||||||
modifier = Modifier.rotate(rotationState),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Content Area ---
|
|
||||||
if (!expandable || isExpanded) {
|
|
||||||
val contentModifier = Modifier
|
|
||||||
.padding(
|
|
||||||
start = ComponentDefaults.CardPadding,
|
|
||||||
end = ComponentDefaults.CardPadding,
|
|
||||||
bottom = ComponentDefaults.CardPadding,
|
|
||||||
// If we have a header, remove the top padding so content sits closer to the title.
|
|
||||||
// If no header (legacy behavior), keep the top padding.
|
|
||||||
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!hasHeader && onClick != null) {
|
|
||||||
Column(
|
|
||||||
modifier = contentModifier.clickable { onClick() },
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Column(
|
|
||||||
modifier = contentModifier,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun AppCardPreview() {
|
|
||||||
AppCard {
|
|
||||||
Text(stringResource(R.string.this_is_the_content_inside_the_card))
|
|
||||||
PrimaryButton(onClick = { }, text = stringResource(R.string.label_continue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun AppCardPreview2() {
|
|
||||||
MaterialTheme {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
// 1. Expandable Card (Initially Collapsed)
|
|
||||||
AppCard(
|
|
||||||
title = "Advanced Settings",
|
|
||||||
text = "Click to reveal more options",
|
|
||||||
expandable = true,
|
|
||||||
initiallyExpanded = false
|
|
||||||
) {
|
|
||||||
Text("Here are some hidden settings.")
|
|
||||||
Text("They are only visible when expanded.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Expandable Card (Initially Expanded)
|
|
||||||
AppCard(
|
|
||||||
title = "Translation History",
|
|
||||||
text = "Recent items",
|
|
||||||
expandable = true,
|
|
||||||
initiallyExpanded = true
|
|
||||||
) {
|
|
||||||
Text("• Hello -> Hallo")
|
|
||||||
Text("• World -> Welt")
|
|
||||||
Text("• Sun -> Sonne")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Static Card (No Title/Expand logic - Legacy behavior)
|
|
||||||
AppCard {
|
|
||||||
Text("This is a standard card without a header.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The primary button for the most important actions.
|
* The primary button for the most important actions.
|
||||||
*
|
*
|
||||||
@@ -636,6 +407,7 @@ fun WrongOutlinedButtonPreview(){
|
|||||||
WrongOutlinedButton(onClick = { }, text = stringResource(R.string.label_continue))
|
WrongOutlinedButton(onClick = { }, text = stringResource(R.string.label_continue))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//This is basically just a wrapper for screens to control width (tablet mode) etc.
|
||||||
@Composable
|
@Composable
|
||||||
fun AppOutlinedCard(
|
fun AppOutlinedCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
@file:Suppress("AssignedValueIsNeverRead", "HardCodedStringLiteral")
|
||||||
|
|
||||||
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.gaudian.translator.R
|
||||||
|
import eu.gaudian.translator.model.Language
|
||||||
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
|
import eu.gaudian.translator.utils.StatusMessageService
|
||||||
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CsvImportDialog(
|
||||||
|
showDialog: Boolean,
|
||||||
|
parsedTable: List<List<String>>,
|
||||||
|
languageViewModel: LanguageViewModel,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onImport: (List<VocabularyItem>) -> Unit,
|
||||||
|
statusMessageService: StatusMessageService
|
||||||
|
) {
|
||||||
|
if (!showDialog) return
|
||||||
|
|
||||||
|
var selectedColFirst by remember { mutableIntStateOf(0) }
|
||||||
|
var selectedColSecond by remember { mutableIntStateOf(1.coerceAtMost((parsedTable.maxOfOrNull { it.size } ?: 1) - 1)) }
|
||||||
|
var skipHeader by remember { mutableStateOf(true) }
|
||||||
|
var selectedLangFirst by remember { mutableStateOf<Language?>(null) }
|
||||||
|
var selectedLangSecond by remember { mutableStateOf<Language?>(null) }
|
||||||
|
|
||||||
|
val errorSelectTwoColumns = stringResource(R.string.error_select_two_columns)
|
||||||
|
val errorSelectLanguages = stringResource(R.string.error_select_languages)
|
||||||
|
val errorNoRowsToImport = stringResource(R.string.error_no_rows_to_import)
|
||||||
|
|
||||||
|
AppDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(R.string.label_import_table_csv_excel)) }
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
val columnCount = parsedTable.maxOfOrNull { it.size } ?: 0
|
||||||
|
|
||||||
|
// First Column Selection
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(stringResource(R.string.label_first_column), modifier = Modifier.weight(1f))
|
||||||
|
var menu1Expanded by remember { mutableStateOf(false) }
|
||||||
|
AppOutlinedButton(onClick = { menu1Expanded = true }) {
|
||||||
|
Text(stringResource(R.string.label_column_n, selectedColFirst + 1))
|
||||||
|
}
|
||||||
|
DropdownMenu(expanded = menu1Expanded, onDismissRequest = { menu1Expanded = false }) {
|
||||||
|
(0 until columnCount).forEach { idx ->
|
||||||
|
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("#${idx + 1} • $header") },
|
||||||
|
onClick = { selectedColFirst = idx; menu1Expanded = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second Column Selection
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(stringResource(R.string.label_second_column), modifier = Modifier.weight(1f))
|
||||||
|
var menu2Expanded by remember { mutableStateOf(false) }
|
||||||
|
AppOutlinedButton(onClick = { menu2Expanded = true }) {
|
||||||
|
Text(stringResource(R.string.label_column_n, selectedColSecond + 1))
|
||||||
|
}
|
||||||
|
DropdownMenu(expanded = menu2Expanded, onDismissRequest = { menu2Expanded = false }) {
|
||||||
|
(0 until columnCount).forEach { idx ->
|
||||||
|
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("#${idx + 1} • $header") },
|
||||||
|
onClick = { selectedColSecond = idx; menu2Expanded = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Language Selection
|
||||||
|
Text(stringResource(R.string.label_languages))
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(stringResource(R.string.label_first_language))
|
||||||
|
SingleLanguageDropDown(
|
||||||
|
languageViewModel = languageViewModel,
|
||||||
|
selectedLanguage = selectedLangFirst,
|
||||||
|
onLanguageSelected = { selectedLangFirst = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(stringResource(R.string.label_second_language))
|
||||||
|
SingleLanguageDropDown(
|
||||||
|
languageViewModel = languageViewModel,
|
||||||
|
selectedLanguage = selectedLangSecond,
|
||||||
|
onLanguageSelected = { selectedLangSecond = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip Header Checkbox
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(checked = skipHeader, onCheckedChange = { skipHeader = it })
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(stringResource(R.string.label_header_row))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
val startIdx = if (skipHeader) 1 else 0
|
||||||
|
val previewA = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColFirst) }.joinToString(", ")
|
||||||
|
val previewB = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColSecond) }.joinToString(", ")
|
||||||
|
Text(stringResource(R.string.label_preview_first, previewA))
|
||||||
|
Text(stringResource(R.string.label_preview_second, previewB))
|
||||||
|
|
||||||
|
// Row Count
|
||||||
|
val totalRows = parsedTable.drop(startIdx).count { row ->
|
||||||
|
val a = row.getOrNull(selectedColFirst).orEmpty().isNotBlank()
|
||||||
|
val b = row.getOrNull(selectedColSecond).orEmpty().isNotBlank()
|
||||||
|
a || b
|
||||||
|
}
|
||||||
|
Text(stringResource(R.string.text_rows_to_import_1d, totalRows))
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.label_cancel))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
PrimaryButton(
|
||||||
|
onClick = {
|
||||||
|
if (selectedColFirst == selectedColSecond) {
|
||||||
|
statusMessageService.showErrorMessage(errorSelectTwoColumns)
|
||||||
|
return@PrimaryButton
|
||||||
|
}
|
||||||
|
val langA = selectedLangFirst
|
||||||
|
val langB = selectedLangSecond
|
||||||
|
if (langA == null || langB == null) {
|
||||||
|
statusMessageService.showErrorMessage(errorSelectLanguages)
|
||||||
|
return@PrimaryButton
|
||||||
|
}
|
||||||
|
val items = parsedTable.drop(startIdx).mapNotNull { row ->
|
||||||
|
val a = row.getOrNull(selectedColFirst)?.trim().orEmpty()
|
||||||
|
val b = row.getOrNull(selectedColSecond)?.trim().orEmpty()
|
||||||
|
if (a.isBlank() && b.isBlank()) null else VocabularyItem(
|
||||||
|
id = 0,
|
||||||
|
languageFirstId = langA.nameResId,
|
||||||
|
languageSecondId = langB.nameResId,
|
||||||
|
wordFirst = a,
|
||||||
|
wordSecond = b
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
statusMessageService.showErrorMessage(errorNoRowsToImport)
|
||||||
|
return@PrimaryButton
|
||||||
|
}
|
||||||
|
onImport(items)
|
||||||
|
},
|
||||||
|
text = stringResource(R.string.label_import)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package eu.gaudian.translator.view.dialogs
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import eu.gaudian.translator.R
|
||||||
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
|
import eu.gaudian.translator.view.composable.AppDialog
|
||||||
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
|
import eu.gaudian.translator.view.composable.AppSlider
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RequestMorePackDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var topic by remember { mutableStateOf("") }
|
||||||
|
var langFrom by remember { mutableStateOf("") }
|
||||||
|
var langTo by remember { mutableStateOf("") }
|
||||||
|
var amount by remember { mutableFloatStateOf(50f) }
|
||||||
|
|
||||||
|
AppDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text("Request a Pack", fontWeight = FontWeight.Bold) },
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.text_request_pack_desc),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.label_topic),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
AppOutlinedTextField(
|
||||||
|
value = topic,
|
||||||
|
onValueChange = { topic = it },
|
||||||
|
placeholder = { Text("e.g. Travel, Business, Cooking…") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.label_languages),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.label_optional),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
AppOutlinedTextField(
|
||||||
|
value = langFrom,
|
||||||
|
onValueChange = { langFrom = it },
|
||||||
|
placeholder = { Text(stringResource(R.string.label_from)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
AppOutlinedTextField(
|
||||||
|
value = langTo,
|
||||||
|
onValueChange = { langTo = it },
|
||||||
|
placeholder = { Text(stringResource(R.string.label_to)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Approx. word count: ~${amount.roundToInt()} words",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
AppSlider(
|
||||||
|
value = amount,
|
||||||
|
onValueChange = { amount = it },
|
||||||
|
valueRange = 10f..200f,
|
||||||
|
steps = 18,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) { Text(stringResource(R.string.label_cancel)) }
|
||||||
|
TextButton(
|
||||||
|
enabled = topic.isNotBlank(),
|
||||||
|
onClick = {
|
||||||
|
val subject = "Polly Pack Request – $topic"
|
||||||
|
val langPart = buildString {
|
||||||
|
val from = langFrom.trim()
|
||||||
|
val to = langTo.trim()
|
||||||
|
if (from.isNotBlank() || to.isNotBlank()) {
|
||||||
|
append("Languages: ${from.ifBlank { "?" }} → ${to.ifBlank { "?" }}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val body = buildString {
|
||||||
|
appendLine("Hey Jonas,")
|
||||||
|
appendLine()
|
||||||
|
appendLine("Please add the following vocabulary pack to Polly:")
|
||||||
|
appendLine()
|
||||||
|
appendLine("Topic: $topic")
|
||||||
|
if (langPart.isNotBlank()) append(langPart)
|
||||||
|
appendLine("Word count: ~${amount.roundToInt()} words")
|
||||||
|
appendLine()
|
||||||
|
appendLine("Thank you!")
|
||||||
|
}
|
||||||
|
val intent = android.content.Intent(android.content.Intent.ACTION_SENDTO).apply {
|
||||||
|
data = "mailto:play@gaudian.eu".toUri()
|
||||||
|
putExtra(android.content.Intent.EXTRA_EMAIL, arrayOf("play@gaudian.eu"))
|
||||||
|
putExtra(android.content.Intent.EXTRA_SUBJECT, subject)
|
||||||
|
putExtra(android.content.Intent.EXTRA_TEXT, body)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.label_send_request),
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ThemePreviews
|
||||||
|
@Composable
|
||||||
|
fun RequestMorePackDialogPreview() {
|
||||||
|
RequestMorePackDialog(
|
||||||
|
onDismiss = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ fun VocabularyReviewScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.found_items),
|
title = stringResource(R.string.found_items),
|
||||||
hintContent = HintDefinition.REVIEW.hint()
|
hint = HintDefinition.REVIEW.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -60,12 +60,16 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.semanticColors
|
import eu.gaudian.translator.ui.theme.semanticColors
|
||||||
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||||
import eu.gaudian.translator.view.composable.AppSwitch
|
import eu.gaudian.translator.view.composable.AppSwitch
|
||||||
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.DictionaryLanguageDropDown
|
import eu.gaudian.translator.view.composable.DictionaryLanguageDropDown
|
||||||
import eu.gaudian.translator.view.composable.DropdownDefaults
|
import eu.gaudian.translator.view.composable.DropdownDefaults
|
||||||
import eu.gaudian.translator.view.composable.LargeDropdownMenuItem
|
import eu.gaudian.translator.view.composable.LargeDropdownMenuItem
|
||||||
@@ -73,12 +77,15 @@ import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
|||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
// 1. STATEFUL COMPONENT (Connects to ViewModels)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CorrectionScreen(
|
fun CorrectionScreen(
|
||||||
correctionViewModel: CorrectionViewModel,
|
navController: NavController
|
||||||
languageViewModel: LanguageViewModel
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val activity = LocalContext.current.findActivity()
|
||||||
|
val correctionViewModel: CorrectionViewModel = hiltViewModel(activity)
|
||||||
|
val languageViewModel : LanguageViewModel = hiltViewModel(activity)
|
||||||
val textFieldValue by correctionViewModel.textFieldValue.collectAsState()
|
val textFieldValue by correctionViewModel.textFieldValue.collectAsState()
|
||||||
val explanation by correctionViewModel.explanation.collectAsState()
|
val explanation by correctionViewModel.explanation.collectAsState()
|
||||||
val isLoading by correctionViewModel.isLoading.collectAsState()
|
val isLoading by correctionViewModel.isLoading.collectAsState()
|
||||||
@@ -89,6 +96,15 @@ fun CorrectionScreen(
|
|||||||
|
|
||||||
val successColor = MaterialTheme.semanticColors.success
|
val successColor = MaterialTheme.semanticColors.success
|
||||||
|
|
||||||
|
Column(){
|
||||||
|
|
||||||
|
AppTopAppBar(
|
||||||
|
title = stringResource(R.string.label_correction),
|
||||||
|
onNavigateBack = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
CorrectionScreenContent(
|
CorrectionScreenContent(
|
||||||
textFieldValue = textFieldValue,
|
textFieldValue = textFieldValue,
|
||||||
explanation = explanation,
|
explanation = explanation,
|
||||||
@@ -113,6 +129,7 @@ fun CorrectionScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. STATELESS COMPONENT (Handles UI Layout)
|
// 2. STATELESS COMPONENT (Handles UI Layout)
|
||||||
@@ -304,7 +321,6 @@ fun CorrectionScreenContent(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -575,4 +591,4 @@ private fun CorrectionScreenResultsPreview() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,10 +342,12 @@ fun DictionarySimpleTopBar(
|
|||||||
languageName: String?,
|
languageName: String?,
|
||||||
onNavigateBack: () -> Unit
|
onNavigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
AppTopAppBar(
|
word?.let {
|
||||||
title = "TODO",
|
AppTopAppBar(
|
||||||
onNavigateBack = onNavigateBack
|
title = it,
|
||||||
)
|
onNavigateBack = onNavigateBack
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package eu.gaudian.translator.view.dictionary
|
package eu.gaudian.translator.view.dictionary
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.viewmodel.DictionaryViewModel
|
import eu.gaudian.translator.viewmodel.DictionaryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
|
|
||||||
@@ -20,15 +24,21 @@ fun DictionaryScreen(
|
|||||||
val dictionaryViewModel: DictionaryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val dictionaryViewModel: DictionaryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
AppTopAppBar(
|
||||||
|
title = stringResource(R.string.label_dictionary),
|
||||||
|
onNavigateBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
|
||||||
// Use the new refactored component
|
// Use the new refactored component
|
||||||
DictionaryScreenContent(
|
DictionaryScreenContent(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onEntryClick = onEntryClick,
|
onEntryClick = onEntryClick,
|
||||||
dictionaryViewModel = dictionaryViewModel,
|
dictionaryViewModel = dictionaryViewModel,
|
||||||
languageViewModel = languageViewModel,
|
languageViewModel = languageViewModel,
|
||||||
onNavigateToOptions = onNavigateToOptions
|
onNavigateToOptions = onNavigateToOptions
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@@ -40,4 +50,4 @@ fun DictionaryScreenPreview() {
|
|||||||
onEntryClick = {},
|
onEntryClick = {},
|
||||||
onNavigateToOptions = {}
|
onNavigateToOptions = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ fun EtymologyResultScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = "TODO",
|
title = "Result",
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
actions = {
|
actions = {
|
||||||
etymologyData?.let { data ->
|
etymologyData?.let { data ->
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
|
||||||
|
|
||||||
package eu.gaudian.translator.view.dictionary
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
|
||||||
import eu.gaudian.translator.view.LocalConnectionConfigured
|
|
||||||
import eu.gaudian.translator.view.NoConnectionScreen
|
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
|
||||||
import eu.gaudian.translator.view.composable.Screen
|
|
||||||
import eu.gaudian.translator.view.composable.TabItem
|
|
||||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
|
||||||
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.DictionaryViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getDictionaryTabs(): List<TabItem> {
|
|
||||||
return listOf(
|
|
||||||
DictionaryTab(stringResource(R.string.label_dictionary), AppIcons.Dictionary),
|
|
||||||
DictionaryTab(stringResource(R.string.title_corrector), AppIcons.Check)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private data class DictionaryTab(override val title: String, override val icon: ImageVector) :
|
|
||||||
TabItem
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MainDictionaryScreen(
|
|
||||||
navController: NavController
|
|
||||||
) {
|
|
||||||
|
|
||||||
|
|
||||||
val activity = LocalContext.current.findActivity()
|
|
||||||
|
|
||||||
val dictionaryViewModel: DictionaryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val correctionViewModel: CorrectionViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val dictionaryTabs = getDictionaryTabs()
|
|
||||||
|
|
||||||
val connectionConfigured = LocalConnectionConfigured.current
|
|
||||||
|
|
||||||
if (!connectionConfigured) {
|
|
||||||
NoConnectionScreen(onSettingsClick = {navController.navigate(SettingsRoutes.API_KEY)})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedTab by remember { mutableStateOf(dictionaryTabs[0]) }
|
|
||||||
Column {
|
|
||||||
AppTabLayout(
|
|
||||||
tabs = dictionaryTabs,
|
|
||||||
selectedTab = selectedTab,
|
|
||||||
onTabSelected = { selectedTab = it },
|
|
||||||
onNavigateBack = {
|
|
||||||
if (!navController.popBackStack()) {
|
|
||||||
navController.navigate(Screen.Home.route) {
|
|
||||||
launchSingleTop = true
|
|
||||||
restoreState = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
when (selectedTab) {
|
|
||||||
dictionaryTabs[0] -> DictionaryScreen(
|
|
||||||
navController = navController,
|
|
||||||
onEntryClick = { entry ->
|
|
||||||
// Set flag indicating navigation is from external source (not DictionaryResultScreen)
|
|
||||||
dictionaryViewModel.setNavigatingFromDictionaryResult(false)
|
|
||||||
navController.navigate("dictionary_result/${entry.id}")
|
|
||||||
},
|
|
||||||
onNavigateToOptions = {
|
|
||||||
navController.navigate("dictionary_options")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
dictionaryTabs[1] -> CorrectionScreen(
|
|
||||||
correctionViewModel = correctionViewModel,
|
|
||||||
languageViewModel = languageViewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Composable
|
|
||||||
fun DictionaryHostScreenPreview() {
|
|
||||||
val navController = rememberNavController()
|
|
||||||
|
|
||||||
MainDictionaryScreen(
|
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary
|
package eu.gaudian.translator.view.exercises
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -23,6 +23,8 @@ import eu.gaudian.translator.view.composable.AppOutlinedButton
|
|||||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
import eu.gaudian.translator.view.composable.CorrectButton
|
import eu.gaudian.translator.view.composable.CorrectButton
|
||||||
import eu.gaudian.translator.view.composable.WrongButton
|
import eu.gaudian.translator.view.composable.WrongButton
|
||||||
|
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseAction
|
||||||
|
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExerciseControls(
|
fun ExerciseControls(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary
|
package eu.gaudian.translator.view.exercises
|
||||||
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
@@ -26,7 +26,8 @@ enum class HintDefinition(
|
|||||||
REVIEW("review_hint", R.string.review_intro),
|
REVIEW("review_hint", R.string.review_intro),
|
||||||
SORTING("sorting_hint", R.string.sorting_hint_title),
|
SORTING("sorting_hint", R.string.sorting_hint_title),
|
||||||
TRANSLATION("translation_hint", R.string.hint_translate_how_it_works),
|
TRANSLATION("translation_hint", R.string.hint_translate_how_it_works),
|
||||||
VOCABULARY_PROGRESS("vocabulary_progress_hint", R.string.hint_vocabulary_progress_hint_title);
|
VOCABULARY_PROGRESS("vocabulary_progress_hint", R.string.hint_vocabulary_progress_hint_title),
|
||||||
|
EXPLORE_PACKS("explore_packs_hint", R.string.hint_explore_packs_title);
|
||||||
|
|
||||||
/** Creates the Hint data class for this hint definition. */
|
/** Creates the Hint data class for this hint definition. */
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package eu.gaudian.translator.view.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import eu.gaudian.translator.R
|
||||||
|
import eu.gaudian.translator.utils.findActivity
|
||||||
|
import eu.gaudian.translator.view.NavigationRoutes
|
||||||
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
|
import eu.gaudian.translator.view.library.VocabularyCard
|
||||||
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DailyReviewScreen(
|
||||||
|
navController: NavHostController,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val activity = context.findActivity()
|
||||||
|
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
|
||||||
|
val dueTodayItems by vocabularyViewModel.dueTodayItems.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
val allLanguages by languageViewModel.allLanguages.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
AppScaffold(
|
||||||
|
topBar = {
|
||||||
|
AppTopAppBar(
|
||||||
|
title = stringResource(R.string.label_daily_review),
|
||||||
|
onNavigateBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxSize()
|
||||||
|
) { paddingValues ->
|
||||||
|
if (dueTodayItems.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.size(200.dp),
|
||||||
|
painter = painterResource(id = R.drawable.ic_nothing_found),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_items_due_for_review),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
contentPadding = PaddingValues(16.dp)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = dueTodayItems,
|
||||||
|
key = { it.id }
|
||||||
|
) { item ->
|
||||||
|
VocabularyCard(
|
||||||
|
item = item,
|
||||||
|
allLanguages = allLanguages,
|
||||||
|
isSelected = false,
|
||||||
|
onItemClick = {
|
||||||
|
vocabularyViewModel.setNavigationContext(dueTodayItems, item.id)
|
||||||
|
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
|
||||||
|
},
|
||||||
|
onItemLongClick = { },
|
||||||
|
onDeleteClick = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Add spacing at the bottom for the button
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(80.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Exercise Button (fixed at bottom)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(24.dp)
|
||||||
|
) {
|
||||||
|
AppButton(
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(NavigationRoutes.START_EXERCISE_DAILY)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp),
|
||||||
|
enabled = dueTodayItems.isNotEmpty(),
|
||||||
|
shape = RoundedCornerShape(28.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_start_exercise_2d, dueTodayItems.size),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = stringResource(R.string.cd_play),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -27,7 +29,6 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -47,9 +48,10 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.NavigationRoutes
|
import eu.gaudian.translator.view.NavigationRoutes
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
|
import eu.gaudian.translator.view.composable.LabeledSection
|
||||||
import eu.gaudian.translator.view.composable.Screen
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
import eu.gaudian.translator.view.stats.widgets.WeeklyActivityChartWidget
|
||||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -62,6 +64,7 @@ fun HomeScreen(
|
|||||||
val streak by viewModel.streak.collectAsState()
|
val streak by viewModel.streak.collectAsState()
|
||||||
val dailyGoal by viewModel.dailyGoal.collectAsState()
|
val dailyGoal by viewModel.dailyGoal.collectAsState()
|
||||||
val todayCompletedCount by viewModel.todayCompletedCount.collectAsState()
|
val todayCompletedCount by viewModel.todayCompletedCount.collectAsState()
|
||||||
|
val dueTodayCount by viewModel.dueTodayCount.collectAsState()
|
||||||
|
|
||||||
// Calculate daily goal progress
|
// Calculate daily goal progress
|
||||||
val progress = if (dailyGoal > 0) {
|
val progress = if (dailyGoal > 0) {
|
||||||
@@ -95,13 +98,12 @@ fun HomeScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
//TODO replace with actual implementation
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
ActionCard(
|
ActionCard(
|
||||||
title = "Daily Review",
|
title = stringResource(R.string.label_daily_review),
|
||||||
subtitle = "42 words need attention",
|
subtitle = stringResource(R.string.desc_daily_review_due, dueTodayCount),
|
||||||
icon = Icons.Default.Psychology,
|
icon = Icons.Default.Psychology,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
onClick = { navController.navigate(NavigationRoutes.DAILY_REVIEW) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
@@ -146,6 +148,9 @@ fun TopProfileSection(navController: NavHostController, context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { navController.navigate(Screen.Settings.route) },
|
onClick = { navController.navigate(Screen.Settings.route) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -158,6 +163,7 @@ fun TopProfileSection(navController: NavHostController, context: Context) {
|
|||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,20 +358,12 @@ fun WeeklyProgressSection(
|
|||||||
val viewModel: ProgressViewModel = hiltViewModel(activity)
|
val viewModel: ProgressViewModel = hiltViewModel(activity)
|
||||||
val weeklyActivityStats by viewModel.weeklyActivityStats.collectAsState()
|
val weeklyActivityStats by viewModel.weeklyActivityStats.collectAsState()
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
LabeledSection(
|
||||||
Row(
|
title = stringResource(R.string.label_weekly_progress),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
actionLabel = stringResource(R.string.label_see_history),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onActionClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.label_weekly_progress), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
|
||||||
TextButton(onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }) {
|
|
||||||
Text(stringResource(R.string.label_see_history))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
AppCard(
|
AppCard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
|
onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
|
||||||
@@ -401,12 +399,16 @@ fun BottomStatsSection(
|
|||||||
val learnedWords by viewModel.totalWordsCompleted.collectAsState()
|
val learnedWords by viewModel.totalWordsCompleted.collectAsState()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Min),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// Total Words
|
// Total Words
|
||||||
AppCard(
|
AppCard(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
onClick = { navController.navigate(Screen.Library.route) }
|
onClick = { navController.navigate(Screen.Library.route) }
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
@@ -419,7 +421,9 @@ fun BottomStatsSection(
|
|||||||
|
|
||||||
// Learned
|
// Learned
|
||||||
AppCard(
|
AppCard(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
|
onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.AddCircleOutline
|
import androidx.compose.material.icons.filled.AddCircleOutline
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.LocalMall
|
import androidx.compose.material.icons.filled.LocalMall
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Tune
|
import androidx.compose.material.icons.filled.Tune
|
||||||
|
import androidx.compose.material.icons.rounded.Check
|
||||||
|
import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -58,6 +60,7 @@ import androidx.compose.ui.draw.drawBehind
|
|||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -70,6 +73,8 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.model.Language
|
import eu.gaudian.translator.model.Language
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
import eu.gaudian.translator.model.VocabularyCategory
|
||||||
import eu.gaudian.translator.model.VocabularyItem
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
|
import eu.gaudian.translator.model.VocabularyStage
|
||||||
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.insertBreakOpportunities
|
import eu.gaudian.translator.view.composable.insertBreakOpportunities
|
||||||
|
|
||||||
@@ -127,22 +132,29 @@ fun SelectionTopBar(
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
// 1. Close Button
|
||||||
IconButton(onClick = onCloseClick) {
|
IconButton(onClick = onCloseClick) {
|
||||||
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
|
Icon(
|
||||||
}
|
imageVector = AppIcons.Close,
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
contentDescription = stringResource(R.string.label_close_selection_mode)
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.d_selected, selectionCount),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
|
||||||
|
// 2. Title Text (Gets weight to prevent pushing icons off-screen)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.d_selected, selectionCount),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. Action Icons Group
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
IconButton(onClick = onSelectAllClick) {
|
IconButton(onClick = onSelectAllClick) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.SelectAll,
|
imageVector = AppIcons.SelectAll,
|
||||||
@@ -320,10 +332,12 @@ fun AllCardsView(
|
|||||||
vocabularyItems: List<VocabularyItem>,
|
vocabularyItems: List<VocabularyItem>,
|
||||||
allLanguages: List<Language>,
|
allLanguages: List<Language>,
|
||||||
selection: Set<Long>,
|
selection: Set<Long>,
|
||||||
|
stageMapping: Map<Int, VocabularyStage> = emptyMap(),
|
||||||
onItemClick: (VocabularyItem) -> Unit,
|
onItemClick: (VocabularyItem) -> Unit,
|
||||||
onItemLongClick: (VocabularyItem) -> Unit,
|
onItemLongClick: (VocabularyItem) -> Unit,
|
||||||
onDeleteClick: (VocabularyItem) -> Unit,
|
onDeleteClick: (VocabularyItem) -> Unit,
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
|
onAddClick: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (vocabularyItems.isEmpty()) {
|
if (vocabularyItems.isEmpty()) {
|
||||||
@@ -346,11 +360,26 @@ fun AllCardsView(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
if (onAddClick != null) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
androidx.compose.material3.Button(
|
||||||
|
onClick = onAddClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(0.6f)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = stringResource(R.string.label_add_vocabulary))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(bottom = 100.dp)
|
contentPadding = PaddingValues(bottom = 100.dp)
|
||||||
) {
|
) {
|
||||||
@@ -359,10 +388,12 @@ fun AllCardsView(
|
|||||||
key = { it.id }
|
key = { it.id }
|
||||||
) { item ->
|
) { item ->
|
||||||
val isSelected = selection.contains(item.id.toLong())
|
val isSelected = selection.contains(item.id.toLong())
|
||||||
|
val stage = stageMapping[item.id] ?: VocabularyStage.NEW
|
||||||
VocabularyCard(
|
VocabularyCard(
|
||||||
item = item,
|
item = item,
|
||||||
allLanguages = allLanguages,
|
allLanguages = allLanguages,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
|
stage = stage,
|
||||||
onItemClick = { onItemClick(item) },
|
onItemClick = { onItemClick(item) },
|
||||||
onItemLongClick = { onItemLongClick(item) },
|
onItemLongClick = { onItemLongClick(item) },
|
||||||
onDeleteClick = { onDeleteClick(item) }
|
onDeleteClick = { onDeleteClick(item) }
|
||||||
@@ -372,14 +403,12 @@ fun AllCardsView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Individual vocabulary card component
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VocabularyCard(
|
fun VocabularyCard(
|
||||||
item: VocabularyItem,
|
item: VocabularyItem,
|
||||||
allLanguages: List<Language>,
|
allLanguages: List<Language>,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
|
stage: VocabularyStage = VocabularyStage.NEW,
|
||||||
onItemClick: () -> Unit,
|
onItemClick: () -> Unit,
|
||||||
onItemLongClick: () -> Unit,
|
onItemLongClick: () -> Unit,
|
||||||
onDeleteClick: () -> Unit,
|
onDeleteClick: () -> Unit,
|
||||||
@@ -392,14 +421,15 @@ fun VocabularyCard(
|
|||||||
Card(
|
Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(12.dp)) // Slightly rounder for a modern look
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onItemClick,
|
onClick = onItemClick,
|
||||||
onLongClick = onItemLongClick
|
onLongClick = onItemLongClick
|
||||||
),
|
),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||||
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.surfaceContainer
|
// Fixed the contentColor bug here:
|
||||||
|
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
|
||||||
),
|
),
|
||||||
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
|
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
|
||||||
) {
|
) {
|
||||||
@@ -410,50 +440,46 @@ fun VocabularyCard(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 12.dp) // Ensures text doesn't bleed into the trailing icon
|
||||||
|
) {
|
||||||
// Top row: First word + Language Pill
|
// Top row: First word + Language Pill
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = insertBreakOpportunities(item.wordFirst),
|
text = insertBreakOpportunities(item.wordFirst),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
// This modifier allows the text to wrap without squishing the pill
|
||||||
|
modifier = Modifier.weight(1f, fill = false)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Surface(
|
LanguagePill(
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
text = langFirst,
|
||||||
shape = RoundedCornerShape(4.dp)
|
backgroundColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
||||||
) {
|
textColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
Text(
|
)
|
||||||
text = langFirst,
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(6.dp)) // Slightly more breathing room
|
||||||
|
|
||||||
// Bottom row: Second word + Language Pill
|
// Bottom row: Second word + Language Pill
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = insertBreakOpportunities(item.wordSecond),
|
text = insertBreakOpportunities(item.wordSecond),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
// Applied to the second text as well for consistency
|
||||||
|
modifier = Modifier.weight(1f, fill = false)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Surface(
|
LanguagePill(
|
||||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
|
text = langSecond,
|
||||||
shape = RoundedCornerShape(8.dp)
|
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
|
||||||
) {
|
textColor = MaterialTheme.colorScheme.primary
|
||||||
Text(
|
)
|
||||||
text = langSecond,
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,18 +490,124 @@ fun VocabularyCard(
|
|||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
IconButton(onClick = { /* Options menu could go here */ }) {
|
// Stage indicator showing the vocabulary item's learning stage
|
||||||
Icon(
|
StageIndicator(stage = stage)
|
||||||
imageVector = Icons.Default.MoreVert,
|
|
||||||
contentDescription = stringResource(R.string.cd_options),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StageIndicator(
|
||||||
|
stage: VocabularyStage,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
// Convert VocabularyStage to a step number (0-6)
|
||||||
|
val step = when (stage) {
|
||||||
|
VocabularyStage.NEW -> 0
|
||||||
|
VocabularyStage.STAGE_1 -> 1
|
||||||
|
VocabularyStage.STAGE_2 -> 2
|
||||||
|
VocabularyStage.STAGE_3 -> 3
|
||||||
|
VocabularyStage.STAGE_4 -> 4
|
||||||
|
VocabularyStage.STAGE_5 -> 5
|
||||||
|
VocabularyStage.LEARNED -> 6
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Calculate how full the ring should be (0.0 to 1.0)
|
||||||
|
val maxSteps = 6f
|
||||||
|
val progress = step / maxSteps
|
||||||
|
|
||||||
|
// 2. Determine the ring color based on the stage
|
||||||
|
val indicatorColor = when (stage) {
|
||||||
|
VocabularyStage.NEW -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)
|
||||||
|
VocabularyStage.STAGE_1, VocabularyStage.STAGE_2 -> Color(0xFFE57373) // Soft Red
|
||||||
|
VocabularyStage.STAGE_3 -> Color(0xFFFFB74D) // Soft Orange
|
||||||
|
VocabularyStage.STAGE_4 -> Color(0xFFFFD54F) // Soft Yellow
|
||||||
|
VocabularyStage.STAGE_5 -> Color(0xFFAED581) // Light Green
|
||||||
|
VocabularyStage.LEARNED -> Color(0xFF81C784) // Solid Green
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier.size(36.dp) // Keeps it neatly sized within the row
|
||||||
|
) {
|
||||||
|
// The background track (empty ring)
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = { 1f },
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f),
|
||||||
|
strokeWidth = 3.dp,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
// The colored progress ring
|
||||||
|
if (stage != VocabularyStage.NEW) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = { progress },
|
||||||
|
color = indicatorColor,
|
||||||
|
strokeWidth = 3.dp,
|
||||||
|
strokeCap = StrokeCap.Round, // Gives the progress bar nice rounded ends
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The center content (Number or Icon)
|
||||||
|
when (stage) {
|
||||||
|
VocabularyStage.NEW -> {
|
||||||
|
// An empty dot or small icon to denote it's untouched
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Star, // Or any generic 'new' icon
|
||||||
|
contentDescription = "New Word",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
VocabularyStage.LEARNED -> {
|
||||||
|
// A checkmark for mastery
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Check,
|
||||||
|
contentDescription = "Learned",
|
||||||
|
tint = indicatorColor,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Display the actual level number (1 through 5)
|
||||||
|
Text(
|
||||||
|
text = step.toString(),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Extracted for consistency and cleaner code
|
||||||
|
@Composable
|
||||||
|
private fun LanguagePill(
|
||||||
|
text: String,
|
||||||
|
backgroundColor: Color,
|
||||||
|
textColor: Color
|
||||||
|
) {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
Surface(
|
||||||
|
color = backgroundColor,
|
||||||
|
shape = RoundedCornerShape(6.dp) // Consistent corner rounding for all pills
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = textColor,
|
||||||
|
// Guaranteed to never wrap awkwardly
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grid view of categories
|
* Grid view of categories
|
||||||
*/
|
*/
|
||||||
@@ -515,13 +647,11 @@ fun CategoryCard(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Card(
|
AppCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(140.dp)
|
.height(140.dp)
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -709,6 +839,7 @@ fun VocabularyCardPreview() {
|
|||||||
),
|
),
|
||||||
allLanguages = emptyList(),
|
allLanguages = emptyList(),
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.NEW,
|
||||||
onItemClick = {},
|
onItemClick = {},
|
||||||
onItemLongClick = {},
|
onItemLongClick = {},
|
||||||
onDeleteClick = {}
|
onDeleteClick = {}
|
||||||
@@ -716,6 +847,154 @@ fun VocabularyCardPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun VocabularyCardStage1Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
VocabularyCard(
|
||||||
|
item = VocabularyItem(
|
||||||
|
id = 2,
|
||||||
|
wordFirst = "Goodbye",
|
||||||
|
wordSecond = "Adiós",
|
||||||
|
languageFirstId = 1,
|
||||||
|
languageSecondId = 2,
|
||||||
|
createdAt = null,
|
||||||
|
features = null,
|
||||||
|
zipfFrequencyFirst = null,
|
||||||
|
zipfFrequencySecond = null
|
||||||
|
),
|
||||||
|
allLanguages = emptyList(),
|
||||||
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.STAGE_1,
|
||||||
|
onItemClick = {},
|
||||||
|
onItemLongClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun VocabularyCardStage3Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
VocabularyCard(
|
||||||
|
item = VocabularyItem(
|
||||||
|
id = 3,
|
||||||
|
wordFirst = "Thank you",
|
||||||
|
wordSecond = "Gracias",
|
||||||
|
languageFirstId = 1,
|
||||||
|
languageSecondId = 2,
|
||||||
|
createdAt = null,
|
||||||
|
features = null,
|
||||||
|
zipfFrequencyFirst = null,
|
||||||
|
zipfFrequencySecond = null
|
||||||
|
),
|
||||||
|
allLanguages = emptyList(),
|
||||||
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.STAGE_3,
|
||||||
|
onItemClick = {},
|
||||||
|
onItemLongClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun VocabularyCardStage5Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
VocabularyCard(
|
||||||
|
item = VocabularyItem(
|
||||||
|
id = 4,
|
||||||
|
wordFirst = "Please",
|
||||||
|
wordSecond = "Por favor",
|
||||||
|
languageFirstId = 1,
|
||||||
|
languageSecondId = 2,
|
||||||
|
createdAt = null,
|
||||||
|
features = null,
|
||||||
|
zipfFrequencyFirst = null,
|
||||||
|
zipfFrequencySecond = null
|
||||||
|
),
|
||||||
|
allLanguages = emptyList(),
|
||||||
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.STAGE_5,
|
||||||
|
onItemClick = {},
|
||||||
|
onItemLongClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun VocabularyCardLearnedPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
VocabularyCard(
|
||||||
|
item = VocabularyItem(
|
||||||
|
id = 5,
|
||||||
|
wordFirst = "Yes",
|
||||||
|
wordSecond = "Sí",
|
||||||
|
languageFirstId = 1,
|
||||||
|
languageSecondId = 2,
|
||||||
|
createdAt = null,
|
||||||
|
features = null,
|
||||||
|
zipfFrequencyFirst = null,
|
||||||
|
zipfFrequencySecond = null
|
||||||
|
),
|
||||||
|
allLanguages = emptyList(),
|
||||||
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.LEARNED,
|
||||||
|
onItemClick = {},
|
||||||
|
onItemLongClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorNewPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.NEW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorStage1Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.STAGE_1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorStage3Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.STAGE_3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorStage5Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.STAGE_5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorLearnedPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.LEARNED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
|
|||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel.SortOrder
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel.SortOrder
|
||||||
|
import eu.gaudian.translator.viewmodel.toStringResource
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@@ -138,6 +139,7 @@ fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
val stageMapping by vocabularyViewModel.stageMapping.collectAsStateWithLifecycle(initialValue = emptyMap())
|
||||||
|
|
||||||
// Handle export state
|
// Handle export state
|
||||||
LaunchedEffect(exportState) {
|
LaunchedEffect(exportState) {
|
||||||
@@ -262,7 +264,9 @@ fun LibraryScreen(
|
|||||||
vocabularyItems = vocabularyItems,
|
vocabularyItems = vocabularyItems,
|
||||||
allLanguages = allLanguages,
|
allLanguages = allLanguages,
|
||||||
selection = selection,
|
selection = selection,
|
||||||
|
stageMapping = stageMapping,
|
||||||
listState = lazyListState,
|
listState = lazyListState,
|
||||||
|
onAddClick = { navController.navigate(NavigationRoutes.NEW_WORD) },
|
||||||
onItemClick = { item ->
|
onItemClick = { item ->
|
||||||
if (isInSelectionMode) {
|
if (isInSelectionMode) {
|
||||||
selection = if (selection.contains(item.id.toLong())) {
|
selection = if (selection.contains(item.id.toLong())) {
|
||||||
@@ -485,8 +489,7 @@ fun FilterBottomSheetContent(
|
|||||||
selected = sortOrder == order,
|
selected = sortOrder == order,
|
||||||
onClick = { sortOrder = order },
|
onClick = { sortOrder = order },
|
||||||
label = {
|
label = {
|
||||||
Text(order.name.replace('_', ' ').lowercase()
|
Text(stringResource(order.toStringResource()))
|
||||||
.replaceFirstChar { it.titlecase() })
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -57,7 +57,7 @@ import eu.gaudian.translator.utils.findActivity
|
|||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.ComponentDefaults
|
import eu.gaudian.translator.view.composable.ComponentDefaults
|
||||||
import eu.gaudian.translator.view.vocabulary.ExerciseProgressIndicator
|
import eu.gaudian.translator.view.exercises.ExerciseProgressIndicator
|
||||||
import eu.gaudian.translator.viewmodel.AnswerResult
|
import eu.gaudian.translator.viewmodel.AnswerResult
|
||||||
import eu.gaudian.translator.viewmodel.ExerciseSessionState
|
import eu.gaudian.translator.viewmodel.ExerciseSessionState
|
||||||
import eu.gaudian.translator.viewmodel.ExerciseViewModel
|
import eu.gaudian.translator.viewmodel.ExerciseViewModel
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("AssignedValueIsNeverRead")
|
@file:Suppress("AssignedValueIsNeverRead")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -35,6 +35,7 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
@@ -56,7 +57,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.Language
|
import eu.gaudian.translator.model.Language
|
||||||
import eu.gaudian.translator.model.TagCategory
|
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
import eu.gaudian.translator.model.VocabularyCategory
|
||||||
import eu.gaudian.translator.model.VocabularyStage
|
import eu.gaudian.translator.model.VocabularyStage
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
@@ -76,17 +76,40 @@ import kotlinx.coroutines.launch
|
|||||||
@Composable
|
@Composable
|
||||||
fun StartExerciseScreen(
|
fun StartExerciseScreen(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
|
preselectedCategoryId: Int? = null,
|
||||||
|
dueTodayOnly: Boolean = false,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
||||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val exerciseViewModel: VocabularyExerciseViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val exerciseViewModel: VocabularyExerciseViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
|
||||||
|
// Initialize exercise config with dueTodayOnly if specified
|
||||||
|
androidx.compose.runtime.LaunchedEffect(dueTodayOnly) {
|
||||||
|
if (dueTodayOnly) {
|
||||||
|
exerciseViewModel.updatePendingExerciseConfig(
|
||||||
|
exerciseViewModel.pendingExerciseConfig.value.copy(dueTodayOnly = true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val exerciseConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
|
val exerciseConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
|
||||||
|
val allCategories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||||
var selectedLanguagePairs by remember { mutableStateOf<List<Pair<Language, Language>>>(emptyList()) }
|
var selectedLanguagePairs by remember { mutableStateOf<List<Pair<Language, Language>>>(emptyList()) }
|
||||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||||
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
||||||
|
|
||||||
|
// Initialize preselected category
|
||||||
|
LaunchedEffect(allCategories, preselectedCategoryId) {
|
||||||
|
if (preselectedCategoryId != null) {
|
||||||
|
val category = allCategories.find { it.id == preselectedCategoryId }
|
||||||
|
if (category != null && category !in selectedCategories) {
|
||||||
|
selectedCategories = listOf(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) }
|
var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) }
|
||||||
var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) }
|
var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) }
|
||||||
val isDirectionPreferenceSet = selectedOriginLanguage != null || selectedTargetLanguage != null
|
val isDirectionPreferenceSet = selectedOriginLanguage != null || selectedTargetLanguage != null
|
||||||
@@ -143,6 +166,13 @@ fun StartExerciseScreen(
|
|||||||
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
|
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
val allLanguages by languageViewModel.allLanguages.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
|
val availableLanguages = remember(availableLanguagesFromItems, allLanguages) {
|
||||||
|
allLanguages.filter { it.nameResId in availableLanguagesFromItems }
|
||||||
|
}
|
||||||
|
|
||||||
TopBarSection(
|
TopBarSection(
|
||||||
onBackClick = { navController.popBackStack() },
|
onBackClick = { navController.popBackStack() },
|
||||||
shuffleCards = exerciseConfig.shuffleCards,
|
shuffleCards = exerciseConfig.shuffleCards,
|
||||||
@@ -152,6 +182,36 @@ fun StartExerciseScreen(
|
|||||||
shuffleLanguagesEnabled = !isDirectionPreferenceSet,
|
shuffleLanguagesEnabled = !isDirectionPreferenceSet,
|
||||||
trainingMode = exerciseConfig.trainingMode,
|
trainingMode = exerciseConfig.trainingMode,
|
||||||
onTrainingModeChanged = { updateConfig(exerciseConfig.copy(trainingMode = it)) },
|
onTrainingModeChanged = { updateConfig(exerciseConfig.copy(trainingMode = it)) },
|
||||||
|
selectedOriginLanguage = selectedOriginLanguage,
|
||||||
|
selectedTargetLanguage = selectedTargetLanguage,
|
||||||
|
languageSelectionEnabled = true,
|
||||||
|
availableLanguages = availableLanguages,
|
||||||
|
onOriginLanguageSelected = { language ->
|
||||||
|
if (language?.nameResId == selectedOriginLanguage?.nameResId) {
|
||||||
|
selectedOriginLanguage = null
|
||||||
|
updateConfig(exerciseConfig.copy(originLanguageId = null))
|
||||||
|
} else {
|
||||||
|
selectedOriginLanguage = language
|
||||||
|
if (selectedTargetLanguage?.nameResId == language?.nameResId) {
|
||||||
|
selectedTargetLanguage = null
|
||||||
|
updateConfig(exerciseConfig.copy(targetLanguageId = null))
|
||||||
|
}
|
||||||
|
updateConfig(exerciseConfig.copy(originLanguageId = language?.nameResId))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTargetLanguageSelected = { language ->
|
||||||
|
if (language?.nameResId == selectedTargetLanguage?.nameResId) {
|
||||||
|
selectedTargetLanguage = null
|
||||||
|
updateConfig(exerciseConfig.copy(targetLanguageId = null))
|
||||||
|
} else {
|
||||||
|
selectedTargetLanguage = language
|
||||||
|
if (selectedOriginLanguage?.nameResId == language?.nameResId) {
|
||||||
|
selectedOriginLanguage = null
|
||||||
|
updateConfig(exerciseConfig.copy(originLanguageId = null))
|
||||||
|
}
|
||||||
|
updateConfig(exerciseConfig.copy(targetLanguageId = language?.nameResId))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -176,36 +236,7 @@ fun StartExerciseScreen(
|
|||||||
updateConfig(exerciseConfig.copy(originLanguageId = null, targetLanguageId = null))
|
updateConfig(exerciseConfig.copy(originLanguageId = null, targetLanguageId = null))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOriginLanguageSelected = { language ->
|
selectedPairsCount = selectedLanguagePairs.size
|
||||||
if (language?.nameResId == selectedOriginLanguage?.nameResId) {
|
|
||||||
selectedOriginLanguage = null
|
|
||||||
updateConfig(exerciseConfig.copy(originLanguageId = null))
|
|
||||||
} else {
|
|
||||||
selectedOriginLanguage = language
|
|
||||||
if (selectedTargetLanguage?.nameResId == language?.nameResId) {
|
|
||||||
selectedTargetLanguage = null
|
|
||||||
updateConfig(exerciseConfig.copy(targetLanguageId = null))
|
|
||||||
}
|
|
||||||
updateConfig(exerciseConfig.copy(originLanguageId = language?.nameResId))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTargetLanguageSelected = { language ->
|
|
||||||
if (language?.nameResId == selectedTargetLanguage?.nameResId) {
|
|
||||||
selectedTargetLanguage = null
|
|
||||||
updateConfig(exerciseConfig.copy(targetLanguageId = null))
|
|
||||||
} else {
|
|
||||||
selectedTargetLanguage = language
|
|
||||||
if (selectedOriginLanguage?.nameResId == language?.nameResId) {
|
|
||||||
selectedOriginLanguage = null
|
|
||||||
updateConfig(exerciseConfig.copy(originLanguageId = null))
|
|
||||||
}
|
|
||||||
updateConfig(exerciseConfig.copy(targetLanguageId = language?.nameResId))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
languageSelectionEnabled = true,
|
|
||||||
selectedPairsCount = selectedLanguagePairs.size,
|
|
||||||
selectedOriginLanguage = selectedOriginLanguage,
|
|
||||||
selectedTargetLanguage = selectedTargetLanguage
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
@@ -284,7 +315,13 @@ fun TopBarSection(
|
|||||||
onShuffleLanguagesChanged: (Boolean) -> Unit,
|
onShuffleLanguagesChanged: (Boolean) -> Unit,
|
||||||
shuffleLanguagesEnabled: Boolean,
|
shuffleLanguagesEnabled: Boolean,
|
||||||
trainingMode: Boolean,
|
trainingMode: Boolean,
|
||||||
onTrainingModeChanged: (Boolean) -> Unit
|
onTrainingModeChanged: (Boolean) -> Unit,
|
||||||
|
selectedOriginLanguage: Language?,
|
||||||
|
selectedTargetLanguage: Language?,
|
||||||
|
languageSelectionEnabled: Boolean,
|
||||||
|
availableLanguages: List<Language>,
|
||||||
|
onOriginLanguageSelected: (Language?) -> Unit,
|
||||||
|
onTargetLanguageSelected: (Language?) -> Unit
|
||||||
) {
|
) {
|
||||||
var showSettings by remember { mutableStateOf(false) }
|
var showSettings by remember { mutableStateOf(false) }
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
@@ -343,6 +380,12 @@ fun TopBarSection(
|
|||||||
shuffleLanguagesEnabled = shuffleLanguagesEnabled,
|
shuffleLanguagesEnabled = shuffleLanguagesEnabled,
|
||||||
trainingMode = trainingMode,
|
trainingMode = trainingMode,
|
||||||
onTrainingModeChanged = onTrainingModeChanged,
|
onTrainingModeChanged = onTrainingModeChanged,
|
||||||
|
selectedOriginLanguage = selectedOriginLanguage,
|
||||||
|
selectedTargetLanguage = selectedTargetLanguage,
|
||||||
|
languageSelectionEnabled = languageSelectionEnabled,
|
||||||
|
availableLanguages = availableLanguages,
|
||||||
|
onOriginLanguageSelected = onOriginLanguageSelected,
|
||||||
|
onTargetLanguageSelected = onTargetLanguageSelected,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
scope.launch { sheetState.hide() }.invokeOnCompletion {
|
scope.launch { sheetState.hide() }.invokeOnCompletion {
|
||||||
if (!sheetState.isVisible) {
|
if (!sheetState.isVisible) {
|
||||||
@@ -388,12 +431,7 @@ fun LanguagePairSection(
|
|||||||
selectedPairs: List<Pair<Language, Language>>,
|
selectedPairs: List<Pair<Language, Language>>,
|
||||||
availableLanguageIds: Set<Int>,
|
availableLanguageIds: Set<Int>,
|
||||||
onPairsChanged: (List<Pair<Language, Language>>) -> Unit,
|
onPairsChanged: (List<Pair<Language, Language>>) -> Unit,
|
||||||
onOriginLanguageSelected: (Language?) -> Unit,
|
selectedPairsCount: Int
|
||||||
onTargetLanguageSelected: (Language?) -> Unit,
|
|
||||||
languageSelectionEnabled: Boolean,
|
|
||||||
selectedPairsCount: Int,
|
|
||||||
selectedOriginLanguage: Language?,
|
|
||||||
selectedTargetLanguage: Language?
|
|
||||||
) {
|
) {
|
||||||
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
||||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
@@ -428,8 +466,17 @@ fun LanguagePairSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
|
val displayedPairs = if (isExpanded) availablePairs else availablePairs.take(3)
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
SectionHeader(title = stringResource(R.string.language_pair))
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.language_pair),
|
||||||
|
actionText = if (availablePairs.size > 3) {
|
||||||
|
if (isExpanded) stringResource(R.string.label_show_less) else stringResource(R.string.label_show_more)
|
||||||
|
} else null,
|
||||||
|
onActionClick = { isExpanded = !isExpanded }
|
||||||
|
)
|
||||||
|
|
||||||
if (availablePairs.isEmpty()) {
|
if (availablePairs.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
@@ -442,7 +489,7 @@ fun LanguagePairSection(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
availablePairs.forEach { pair ->
|
displayedPairs.forEach { pair ->
|
||||||
val isSelected = selectedPairs.contains(pair)
|
val isSelected = selectedPairs.contains(pair)
|
||||||
LanguageChip(
|
LanguageChip(
|
||||||
text = "${pair.first.name} ⇄ ${pair.second.name}",
|
text = "${pair.first.name} ⇄ ${pair.second.name}",
|
||||||
@@ -460,74 +507,6 @@ fun LanguagePairSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.label_language_direction),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_language_direction_explanation),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
if (!languageSelectionEnabled && selectedPairsCount > 0) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_language_direction_disabled_with_pairs),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.label_origin_language),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
selectedLanguage = selectedOriginLanguage,
|
|
||||||
onLanguageSelected = { language ->
|
|
||||||
if (selectedTargetLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
|
|
||||||
onOriginLanguageSelected(language)
|
|
||||||
},
|
|
||||||
showNoneOption = true,
|
|
||||||
onNoneSelected = { onOriginLanguageSelected(null) },
|
|
||||||
alternateLanguages = availableLanguages,
|
|
||||||
restrictToAlternateLanguages = true,
|
|
||||||
enabled = languageSelectionEnabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.label_target_language),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
selectedLanguage = selectedTargetLanguage,
|
|
||||||
onLanguageSelected = { language ->
|
|
||||||
if (selectedOriginLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
|
|
||||||
onTargetLanguageSelected(language)
|
|
||||||
},
|
|
||||||
showNoneOption = true,
|
|
||||||
onNoneSelected = { onTargetLanguageSelected(null) },
|
|
||||||
alternateLanguages = availableLanguages,
|
|
||||||
restrictToAlternateLanguages = true,
|
|
||||||
enabled = languageSelectionEnabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,10 +555,25 @@ fun CategoriesSection(
|
|||||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
SectionHeader(title = stringResource(R.string.label_categories))
|
val tagCategories = categories
|
||||||
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
|
val displayedCategories = if (isExpanded) tagCategories else tagCategories.take(3)
|
||||||
|
|
||||||
val tagCategories = categories.filterIsInstance<TagCategory>()
|
SectionHeader(
|
||||||
if (tagCategories.size > 15) {
|
title = stringResource(R.string.label_categories),
|
||||||
|
actionText = if (tagCategories.size > 3) {
|
||||||
|
if (isExpanded) stringResource(R.string.label_show_less) else stringResource(R.string.label_show_more)
|
||||||
|
} else null,
|
||||||
|
onActionClick = { isExpanded = !isExpanded }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (tagCategories.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
} else if (tagCategories.size > 15) {
|
||||||
CategoryDropdown(
|
CategoryDropdown(
|
||||||
onCategorySelected = { selections ->
|
onCategorySelected = { selections ->
|
||||||
onCategoriesChanged(selections.filterNotNull())
|
onCategoriesChanged(selections.filterNotNull())
|
||||||
@@ -596,7 +590,7 @@ fun CategoriesSection(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
tagCategories.forEach { category ->
|
displayedCategories.forEach { category ->
|
||||||
val isSelected = selectedCategories.contains(category)
|
val isSelected = selectedCategories.contains(category)
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
@@ -743,9 +737,9 @@ fun NumberOfCardsSection(
|
|||||||
availableQuickSelections.forEach { value ->
|
availableQuickSelections.forEach { value ->
|
||||||
AppOutlinedButton(
|
AppOutlinedButton(
|
||||||
onClick = { onAmountChanged(value) },
|
onClick = { onAmountChanged(value) },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f).padding(0.dp)
|
||||||
) {
|
) {
|
||||||
Text(value.toString())
|
Text(text = value.toString(), softWrap = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -779,7 +773,7 @@ fun QuestionTypesSection(
|
|||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
QuestionTypeCard(
|
QuestionTypeCard(
|
||||||
title = stringResource(R.string.label_multiple_choice_exercise),
|
title = stringResource(R.string.label_multiple_choice_exercise),
|
||||||
subtitle = stringResource(R.string.label_choose_exercise_types),
|
subtitle = stringResource(R.string.label_multiple_choice_desc),
|
||||||
icon = AppIcons.CheckList,
|
icon = AppIcons.CheckList,
|
||||||
isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE),
|
isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE),
|
||||||
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) }
|
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) }
|
||||||
@@ -879,8 +873,17 @@ private fun StartExerciseSettingsBottomSheet(
|
|||||||
shuffleLanguagesEnabled: Boolean,
|
shuffleLanguagesEnabled: Boolean,
|
||||||
trainingMode: Boolean,
|
trainingMode: Boolean,
|
||||||
onTrainingModeChanged: (Boolean) -> Unit,
|
onTrainingModeChanged: (Boolean) -> Unit,
|
||||||
|
selectedOriginLanguage: Language?,
|
||||||
|
selectedTargetLanguage: Language?,
|
||||||
|
languageSelectionEnabled: Boolean,
|
||||||
|
availableLanguages: List<Language>,
|
||||||
|
onOriginLanguageSelected: (Language?) -> Unit,
|
||||||
|
onTargetLanguageSelected: (Language?) -> Unit,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
||||||
|
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = sheetState
|
sheetState = sheetState
|
||||||
@@ -889,7 +892,7 @@ private fun StartExerciseSettingsBottomSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.options),
|
text = stringResource(R.string.options),
|
||||||
@@ -927,6 +930,73 @@ private fun StartExerciseSettingsBottomSheet(
|
|||||||
checked = trainingMode,
|
checked = trainingMode,
|
||||||
onCheckedChange = onTrainingModeChanged
|
onCheckedChange = onTrainingModeChanged
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Language Direction Section
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_language_direction),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.text_language_direction_explanation),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
if (!languageSelectionEnabled) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.text_language_direction_disabled_with_pairs),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_origin_language),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
languageViewModel = languageViewModel,
|
||||||
|
selectedLanguage = selectedOriginLanguage,
|
||||||
|
onLanguageSelected = { language ->
|
||||||
|
if (selectedTargetLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
|
||||||
|
onOriginLanguageSelected(language)
|
||||||
|
},
|
||||||
|
showNoneOption = true,
|
||||||
|
onNoneSelected = { onOriginLanguageSelected(null) },
|
||||||
|
alternateLanguages = availableLanguages,
|
||||||
|
restrictToAlternateLanguages = true,
|
||||||
|
enabled = languageSelectionEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_target_language),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
languageViewModel = languageViewModel,
|
||||||
|
selectedLanguage = selectedTargetLanguage,
|
||||||
|
onLanguageSelected = { language ->
|
||||||
|
if (selectedOriginLanguage?.nameResId == language.nameResId) return@SingleLanguageDropDown
|
||||||
|
onTargetLanguageSelected(language)
|
||||||
|
},
|
||||||
|
showNoneOption = true,
|
||||||
|
onNoneSelected = { onTargetLanguageSelected(null) },
|
||||||
|
alternateLanguages = availableLanguages,
|
||||||
|
restrictToAlternateLanguages = true,
|
||||||
|
enabled = languageSelectionEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("AssignedValueIsNeverRead")
|
@file:Suppress("AssignedValueIsNeverRead")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("AssignedValueIsNeverRead")
|
@file:Suppress("AssignedValueIsNeverRead")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.exercises
|
package eu.gaudian.translator.view.new_ecercises
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -136,7 +136,7 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
|
|||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = providerName,
|
title = providerName,
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
|
hint = HintDefinition.ADD_MODEL_SCAN.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ fun ApiKeyScreen(navController: NavController) {
|
|||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_ai_configuration),
|
title = stringResource(R.string.label_ai_configuration),
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
hintContent = HintDefinition.API_KEY.hint()
|
hint = HintDefinition.API_KEY.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ fun CustomVocabularyPromptScreen(
|
|||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.text_vocabulary_prompt),
|
title = stringResource(R.string.text_vocabulary_prompt),
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
hintContent = null //TODO: Add hint
|
hint = null //TODO: Add hint
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ fun DictionaryOptionsScreen(
|
|||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_dictionary_options),
|
title = stringResource(R.string.label_dictionary_options),
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
|
hint = HintDefinition.DICTIONARY_OPTIONS.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ fun TranslationSettingsScreen(
|
|||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_translation_settings),
|
title = stringResource(R.string.label_translation_settings),
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
hintContent = null //TODO add hint
|
hint = null //TODO add hint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ fun VocabularyProgressOptionsScreen(
|
|||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
title = stringResource(R.string.label_vocabulary_settings),
|
title = stringResource(R.string.label_vocabulary_settings),
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
|
hint = HintDefinition.VOCABULARY_PROGRESS.hint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -23,8 +22,6 @@ import androidx.compose.material3.CardDefaults
|
|||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
@@ -59,12 +56,11 @@ import eu.gaudian.translator.utils.findActivity
|
|||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
|
import eu.gaudian.translator.view.composable.CsvImportDialog
|
||||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||||
import eu.gaudian.translator.view.composable.SingleLanguageDropDown
|
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.ExportImportViewModel
|
import eu.gaudian.translator.viewmodel.ExportImportViewModel
|
||||||
import eu.gaudian.translator.viewmodel.ExportState
|
import eu.gaudian.translator.viewmodel.ExportState
|
||||||
@@ -659,117 +655,18 @@ fun VocabularyRepositoryOptionsScreen(
|
|||||||
|
|
||||||
// CSV Import Dialog
|
// CSV Import Dialog
|
||||||
if (showTableImportDialog.value) {
|
if (showTableImportDialog.value) {
|
||||||
AlertDialog(
|
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
|
||||||
onDismissRequest = { showTableImportDialog.value = false },
|
CsvImportDialog(
|
||||||
title = { Text(stringResource(R.string.label_import_table_csv_excel)) },
|
showDialog = showTableImportDialog.value,
|
||||||
text = {
|
parsedTable = parsedTable,
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
languageViewModel = languageViewModel,
|
||||||
val columnCount = parsedTable.maxOfOrNull { it.size } ?: 0
|
onDismiss = { showTableImportDialog.value = false },
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
onImport = { items ->
|
||||||
Text(stringResource(R.string.label_first_column), modifier = Modifier.weight(1f))
|
vocabularyViewModel.addVocabularyItems(items)
|
||||||
var menu1Expanded by remember { mutableStateOf(false) }
|
statusMessageService.showSuccessMessage("$infoImportedItemsFrom ${items.size}")
|
||||||
AppOutlinedButton(onClick = { menu1Expanded = true }) { Text(stringResource(R.string.label_column_n, selectedColFirst + 1)) }
|
showTableImportDialog.value = false
|
||||||
DropdownMenu(expanded = menu1Expanded, onDismissRequest = { menu1Expanded = false }) {
|
|
||||||
(0 until columnCount).forEach { idx ->
|
|
||||||
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("#${idx + 1} • $header") },
|
|
||||||
onClick = { selectedColFirst = idx; menu1Expanded = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(stringResource(R.string.label_second_column), modifier = Modifier.weight(1f))
|
|
||||||
var menu2Expanded by remember { mutableStateOf(false) }
|
|
||||||
AppOutlinedButton(onClick = { menu2Expanded = true }) { Text(stringResource(R.string.label_column_n, selectedColSecond + 1)) }
|
|
||||||
DropdownMenu(expanded = menu2Expanded, onDismissRequest = { menu2Expanded = false }) {
|
|
||||||
(0 until columnCount).forEach { idx ->
|
|
||||||
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("#${idx + 1} • $header") },
|
|
||||||
onClick = { selectedColSecond = idx; menu2Expanded = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(stringResource(R.string.label_languages))
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(stringResource(R.string.label_first_language))
|
|
||||||
SingleLanguageDropDown(
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
selectedLanguage = selectedLangFirst,
|
|
||||||
onLanguageSelected = { selectedLangFirst = it }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(stringResource(R.string.label_second_language))
|
|
||||||
SingleLanguageDropDown(
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
selectedLanguage = selectedLangSecond,
|
|
||||||
onLanguageSelected = { selectedLangSecond = it }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Checkbox(checked = skipHeader, onCheckedChange = { skipHeader = it })
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(stringResource(R.string.label_header_row))
|
|
||||||
}
|
|
||||||
val startIdx = if (skipHeader) 1 else 0
|
|
||||||
val previewA = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColFirst) }.joinToString(", ")
|
|
||||||
val previewB = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColSecond) }.joinToString(", ")
|
|
||||||
Text(stringResource(R.string.label_preview_first, previewA))
|
|
||||||
Text(stringResource(R.string.label_preview_second, previewB))
|
|
||||||
val totalRows = parsedTable.drop(startIdx).count { row ->
|
|
||||||
val a = row.getOrNull(selectedColFirst).orEmpty().isNotBlank()
|
|
||||||
val b = row.getOrNull(selectedColSecond).orEmpty().isNotBlank()
|
|
||||||
a || b
|
|
||||||
}
|
|
||||||
Text(stringResource(R.string.text_rows_to_import_1d, totalRows))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
confirmButton = {
|
statusMessageService = statusMessageService
|
||||||
val errorSelectTwoColumns = stringResource(R.string.error_select_two_columns)
|
|
||||||
val errorSelectLanguages = stringResource(R.string.error_select_languages)
|
|
||||||
val errorNoRowsToImport = stringResource(R.string.error_no_rows_to_import)
|
|
||||||
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
|
|
||||||
TextButton(onClick = {
|
|
||||||
if (selectedColFirst == selectedColSecond) {
|
|
||||||
statusMessageService.showErrorMessage(errorSelectTwoColumns)
|
|
||||||
return@TextButton
|
|
||||||
}
|
|
||||||
val langA = selectedLangFirst
|
|
||||||
val langB = selectedLangSecond
|
|
||||||
if (langA == null || langB == null) {
|
|
||||||
statusMessageService.showErrorMessage(errorSelectLanguages)
|
|
||||||
return@TextButton
|
|
||||||
}
|
|
||||||
val startIdx = if (skipHeader) 1 else 0
|
|
||||||
val items = parsedTable.drop(startIdx).mapNotNull { row ->
|
|
||||||
val a = row.getOrNull(selectedColFirst)?.trim().orEmpty()
|
|
||||||
val b = row.getOrNull(selectedColSecond)?.trim().orEmpty()
|
|
||||||
if (a.isBlank() && b.isBlank()) null else eu.gaudian.translator.model.VocabularyItem(
|
|
||||||
id = 0,
|
|
||||||
languageFirstId = langA.nameResId,
|
|
||||||
languageSecondId = langB.nameResId,
|
|
||||||
wordFirst = a,
|
|
||||||
wordSecond = b
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (items.isEmpty()) {
|
|
||||||
statusMessageService.showErrorMessage(errorNoRowsToImport)
|
|
||||||
return@TextButton
|
|
||||||
}
|
|
||||||
vocabularyViewModel.addVocabularyItems(items)
|
|
||||||
statusMessageService.showSuccessMessage(infoImportedItemsFrom + " " +items.size)
|
|
||||||
showTableImportDialog.value = false
|
|
||||||
}) { Text(stringResource(R.string.label_import)) }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showTableImportDialog.value = false }) { Text(stringResource(R.string.label_cancel)) }
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import androidx.compose.animation.core.spring
|
|||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.gestures.scrollBy
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -45,6 +47,7 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -62,13 +65,13 @@ import eu.gaudian.translator.view.composable.AppCard
|
|||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||||
import eu.gaudian.translator.view.dialogs.MissingLanguageDialog
|
import eu.gaudian.translator.view.dialogs.MissingLanguageDialog
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.AllVocabularyWidget
|
import eu.gaudian.translator.view.stats.widgets.AllVocabularyWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget
|
import eu.gaudian.translator.view.stats.widgets.CategoryProgressWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget
|
import eu.gaudian.translator.view.stats.widgets.DueTodayWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget
|
import eu.gaudian.translator.view.stats.widgets.LevelWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget
|
import eu.gaudian.translator.view.stats.widgets.StatusWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
|
import eu.gaudian.translator.view.stats.widgets.StreakWidget
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
import eu.gaudian.translator.view.stats.widgets.WeeklyActivityChartWidget
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
||||||
@@ -203,12 +206,27 @@ fun StatsScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.padding(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_stats),
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.dragContainer(dragDropState),
|
.dragContainer(dragDropState),
|
||||||
contentPadding = PaddingValues(bottom = 160.dp)
|
contentPadding = PaddingValues( 8.dp)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = orderedWidgets,
|
items = orderedWidgets,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -290,7 +290,7 @@ fun CategoryProgressCircle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChartLegend() {
|
fun ChartLegend() {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -90,7 +90,7 @@ fun StatusWidget(
|
|||||||
if (itemsWithoutGrammarCount > 0) {
|
if (itemsWithoutGrammarCount > 0) {
|
||||||
StatusItem(
|
StatusItem(
|
||||||
icon = AppIcons.Error,
|
icon = AppIcons.Error,
|
||||||
text = stringResource(R.string.items_without_grammar_infos),
|
text = stringResource(R.string.label_items_without_grammar),
|
||||||
count = itemsWithoutGrammarCount,
|
count = itemsWithoutGrammarCount,
|
||||||
onClick = onNavigateToNoGrammar,
|
onClick = onNavigateToNoGrammar,
|
||||||
color = MaterialTheme.colorScheme.error
|
color = MaterialTheme.colorScheme.error
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.drawText
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import eu.gaudian.translator.R
|
||||||
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
|
import eu.gaudian.translator.viewmodel.WeeklyActivityStat
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A widget that displays weekly activity statistics in a visually appealing smooth line chart.
|
||||||
|
* It's designed to be consistent with the app's modern UI style using the theme's colors.
|
||||||
|
*
|
||||||
|
* @param weeklyStats A list of [WeeklyActivityStat] for the last 7 days.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun WeeklyActivityChartWidget(
|
||||||
|
weeklyStats: List<WeeklyActivityStat>
|
||||||
|
) {
|
||||||
|
val hasNoData = remember(weeklyStats) {
|
||||||
|
weeklyStats.all { it.completed == 0 && it.answeredRight == 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNoData) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.text_no_data_available),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
// Reduced horizontal padding to give the chart more space
|
||||||
|
.padding(vertical = 24.dp, horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
WeeklyChartLegend()
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
InteractiveLineChart(weeklyStats = weeklyStats)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
ChartFooter(weeklyStats = weeklyStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InteractiveLineChart(weeklyStats: List<WeeklyActivityStat>) {
|
||||||
|
var selectedIndex by remember { mutableStateOf<Int?>(3) } // Default selection
|
||||||
|
val textMeasurer = rememberTextMeasurer()
|
||||||
|
|
||||||
|
val colorCompleted = MaterialTheme.colorScheme.primary
|
||||||
|
val colorCorrect = MaterialTheme.colorScheme.tertiary
|
||||||
|
|
||||||
|
val gridColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||||
|
val tooltipLineColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)
|
||||||
|
val dotCenterColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val tooltipBgColor = MaterialTheme.colorScheme.inverseSurface
|
||||||
|
val tooltipTextColor = MaterialTheme.colorScheme.inverseOnSurface
|
||||||
|
|
||||||
|
var startAnimation by remember { mutableStateOf(false) }
|
||||||
|
val animationProgress by animateFloatAsState(
|
||||||
|
targetValue = if (startAnimation) 1f else 0f,
|
||||||
|
animationSpec = tween(durationMillis = 1000),
|
||||||
|
label = "chartAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(100)
|
||||||
|
startAnimation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val yAxisMax = remember(weeklyStats) {
|
||||||
|
val max = weeklyStats.flatMap { listOf(it.completed, it.answeredRight) }.maxOrNull() ?: 0
|
||||||
|
if (max < 10) 10 else ((max / 10) + 1) * 10
|
||||||
|
}
|
||||||
|
val yMax = yAxisMax.toFloat()
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
// Left Side: Y-Axis Amounts
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(180.dp)
|
||||||
|
// Reduced end padding to save space
|
||||||
|
.padding(end = 8.dp, top = 2.dp, bottom = 2.dp),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = yAxisMax.toString(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = (yAxisMax / 2).toString(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "0",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Side: Chart Area
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(180.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures { offset ->
|
||||||
|
val itemWidth = size.width / (weeklyStats.size - 1).coerceAtLeast(1)
|
||||||
|
selectedIndex = (offset.x / itemWidth).roundToInt().coerceIn(0, weeklyStats.lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectHorizontalDragGestures { change, _ ->
|
||||||
|
val itemWidth = size.width / (weeklyStats.size - 1).coerceAtLeast(1)
|
||||||
|
selectedIndex = (change.position.x / itemWidth).roundToInt().coerceIn(0, weeklyStats.lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val width = size.width
|
||||||
|
val height = size.height
|
||||||
|
val xSpacing = width / (weeklyStats.size - 1).coerceAtLeast(1)
|
||||||
|
|
||||||
|
drawLine(gridColor, Offset(0f, 0f), Offset(width, 0f), strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
|
||||||
|
drawLine(gridColor, Offset(0f, height / 2f), Offset(width, height / 2f), strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
|
||||||
|
drawLine(gridColor, Offset(0f, height), Offset(width, height), strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
|
||||||
|
|
||||||
|
if (animationProgress == 0f) return@Canvas
|
||||||
|
|
||||||
|
val pointsCompleted = weeklyStats.mapIndexed { i, stat ->
|
||||||
|
Offset(i * xSpacing, height - ((stat.completed * animationProgress) / yMax) * height)
|
||||||
|
}
|
||||||
|
val pointsCorrect = weeklyStats.mapIndexed { i, stat ->
|
||||||
|
Offset(i * xSpacing, height - ((stat.answeredRight * animationProgress) / yMax) * height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define Paths
|
||||||
|
val pathCorrect = Path().apply { smoothCurve(pointsCorrect) }
|
||||||
|
val fillPathCorrect = Path().apply {
|
||||||
|
smoothCurve(pointsCorrect)
|
||||||
|
lineTo(width, height)
|
||||||
|
lineTo(0f, height)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
val pathCompleted = Path().apply { smoothCurve(pointsCompleted) }
|
||||||
|
val fillPathCompleted = Path().apply {
|
||||||
|
smoothCurve(pointsCompleted)
|
||||||
|
lineTo(width, height)
|
||||||
|
lineTo(0f, height)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw semi-transparent fills first
|
||||||
|
drawPath(
|
||||||
|
path = fillPathCompleted,
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(colorCompleted.copy(alpha = 0.25f), Color.Transparent),
|
||||||
|
startY = 0f,
|
||||||
|
endY = height
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
drawPath(
|
||||||
|
path = fillPathCorrect,
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(colorCorrect.copy(alpha = 0.25f), Color.Transparent),
|
||||||
|
startY = 0f,
|
||||||
|
endY = height
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw solid strokes on top of the fills
|
||||||
|
drawPath(
|
||||||
|
path = pathCorrect,
|
||||||
|
color = colorCorrect,
|
||||||
|
style = Stroke(width = 6f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f))
|
||||||
|
)
|
||||||
|
|
||||||
|
drawPath(
|
||||||
|
path = pathCompleted,
|
||||||
|
color = colorCompleted,
|
||||||
|
style = Stroke(width = 8f)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interactive Highlights & Dual Separated Tooltips
|
||||||
|
selectedIndex?.let { index ->
|
||||||
|
val stat = weeklyStats[index]
|
||||||
|
val x = index * xSpacing
|
||||||
|
val yCompleted = height - ((stat.completed * animationProgress) / yMax) * height
|
||||||
|
val yCorrect = height - ((stat.answeredRight * animationProgress) / yMax) * height
|
||||||
|
|
||||||
|
// Vertical line marker
|
||||||
|
drawLine(
|
||||||
|
color = tooltipLineColor,
|
||||||
|
start = Offset(x, 0f),
|
||||||
|
end = Offset(x, height),
|
||||||
|
strokeWidth = 3f
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dots on lines
|
||||||
|
drawCircle(color = dotCenterColor, radius = 12f, center = Offset(x, yCompleted))
|
||||||
|
drawCircle(color = colorCompleted, radius = 7f, center = Offset(x, yCompleted))
|
||||||
|
|
||||||
|
drawCircle(color = dotCenterColor, radius = 12f, center = Offset(x, yCorrect))
|
||||||
|
drawCircle(color = colorCorrect, radius = 7f, center = Offset(x, yCorrect))
|
||||||
|
|
||||||
|
// Measure text
|
||||||
|
val textStyle = TextStyle(color = tooltipTextColor, fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||||
|
val textResCompleted = textMeasurer.measure(stat.completed.toString(), textStyle)
|
||||||
|
val textResCorrect = textMeasurer.measure(stat.answeredRight.toString(), textStyle)
|
||||||
|
|
||||||
|
val dotRadius = 5f
|
||||||
|
val gap = 6f
|
||||||
|
val padX = 12f
|
||||||
|
val padY = 8f
|
||||||
|
|
||||||
|
val w1 = padX * 2 + dotRadius * 2 + gap + textResCompleted.size.width
|
||||||
|
val h1 = padY * 2 + textResCompleted.size.height
|
||||||
|
|
||||||
|
val w2 = padX * 2 + dotRadius * 2 + gap + textResCorrect.size.width
|
||||||
|
val h2 = padY * 2 + textResCorrect.size.height
|
||||||
|
|
||||||
|
// Tooltip Overlap Prevention Logic
|
||||||
|
val completedIsHigher = yCompleted <= yCorrect
|
||||||
|
var yPosCompleted = if (completedIsHigher) yCompleted - h1 - 12f else yCompleted + 12f
|
||||||
|
var yPosCorrect = if (completedIsHigher) yCorrect + 12f else yCorrect - h2 - 12f
|
||||||
|
|
||||||
|
// Prevent clipping out of canvas bounds natively first
|
||||||
|
if (yPosCompleted < 0f && completedIsHigher) yPosCompleted = 0f
|
||||||
|
if (yPosCorrect < 0f && !completedIsHigher) yPosCorrect = 0f
|
||||||
|
if (yPosCompleted + h1 > height && !completedIsHigher) yPosCompleted = height - h1
|
||||||
|
if (yPosCorrect + h2 > height && completedIsHigher) yPosCorrect = height - h2
|
||||||
|
|
||||||
|
// Overlap resolution
|
||||||
|
val topRectY = minOf(yPosCompleted, yPosCorrect)
|
||||||
|
val topRectH = if (topRectY == yPosCompleted) h1 else h2
|
||||||
|
val bottomRectY = maxOf(yPosCompleted, yPosCorrect)
|
||||||
|
|
||||||
|
val gapBetweenTooltips = 8f
|
||||||
|
if (topRectY + topRectH + gapBetweenTooltips > bottomRectY) {
|
||||||
|
val midPointY = (yCompleted + yCorrect) / 2f
|
||||||
|
val adjustedTopY = midPointY - (topRectH + gapBetweenTooltips / 2f)
|
||||||
|
val adjustedBottomY = midPointY + (gapBetweenTooltips / 2f)
|
||||||
|
|
||||||
|
if (topRectY == yPosCompleted) {
|
||||||
|
yPosCompleted = adjustedTopY
|
||||||
|
yPosCorrect = adjustedBottomY
|
||||||
|
} else {
|
||||||
|
yPosCorrect = adjustedTopY
|
||||||
|
yPosCompleted = adjustedBottomY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final Canvas Bounds Check post-resolution
|
||||||
|
val finalMinY = minOf(yPosCompleted, yPosCorrect)
|
||||||
|
if (finalMinY < 0f) {
|
||||||
|
yPosCompleted -= finalMinY
|
||||||
|
yPosCorrect -= finalMinY
|
||||||
|
}
|
||||||
|
val finalMaxY = maxOf(yPosCompleted + h1, yPosCorrect + h2)
|
||||||
|
if (finalMaxY > height) {
|
||||||
|
val shift = finalMaxY - height
|
||||||
|
yPosCompleted -= shift
|
||||||
|
yPosCorrect -= shift
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Completed Tooltip
|
||||||
|
val t1X = (x - w1 / 2f).coerceIn(0f, width - w1)
|
||||||
|
drawRoundRect(color = tooltipBgColor, topLeft = Offset(t1X, yPosCompleted), size = Size(w1, h1), cornerRadius = CornerRadius(16f, 16f))
|
||||||
|
drawCircle(color = colorCompleted, radius = dotRadius, center = Offset(t1X + padX + dotRadius, yPosCompleted + h1 / 2f))
|
||||||
|
drawText(textLayoutResult = textResCompleted, topLeft = Offset(t1X + padX + dotRadius * 2 + gap, yPosCompleted + padY))
|
||||||
|
|
||||||
|
// Draw Correct Tooltip
|
||||||
|
val t2X = (x - w2 / 2f).coerceIn(0f, width - w2)
|
||||||
|
drawRoundRect(color = tooltipBgColor, topLeft = Offset(t2X, yPosCorrect), size = Size(w2, h2), cornerRadius = CornerRadius(16f, 16f))
|
||||||
|
drawCircle(color = colorCorrect, radius = dotRadius, center = Offset(t2X + padX + dotRadius, yPosCorrect + h2 / 2f))
|
||||||
|
drawText(textLayoutResult = textResCorrect, topLeft = Offset(t2X + padX + dotRadius * 2 + gap, yPosCorrect + padY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// X-Axis Labels (Freed from fixed widths, prevented from wrapping)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
weeklyStats.forEachIndexed { index, stat ->
|
||||||
|
val isSelected = index == selectedIndex
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stat.day.uppercase().take(3) + ".",
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize = 11.sp, // Slightly smaller to ensure fit across all devices
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false // Prevents the text from splitting into multiple lines
|
||||||
|
)
|
||||||
|
if (isSelected) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(2.dp)
|
||||||
|
.width(20.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.primary)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Invisible spacer to prevent layout jumping when line appears
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Path.smoothCurve(points: List<Offset>) {
|
||||||
|
if (points.isEmpty()) return
|
||||||
|
moveTo(points.first().x, points.first().y)
|
||||||
|
for (i in 1 until points.size) {
|
||||||
|
val prev = points[i - 1]
|
||||||
|
val curr = points[i]
|
||||||
|
val controlX = (prev.x + curr.x) / 2f
|
||||||
|
cubicTo(
|
||||||
|
controlX, prev.y,
|
||||||
|
controlX, curr.y,
|
||||||
|
curr.x, curr.y
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WeeklyChartLegend() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
LegendItem(color = MaterialTheme.colorScheme.primary, label = stringResource(R.string.label_completed).uppercase())
|
||||||
|
LegendItem(color = MaterialTheme.colorScheme.tertiary, label = stringResource(R.string.label_correct).uppercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LegendItem(color: Color, label: String) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(10.dp)
|
||||||
|
.background(color, shape = CircleShape)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChartFooter(weeklyStats: List<WeeklyActivityStat>) {
|
||||||
|
val bestDay = remember(weeklyStats) {
|
||||||
|
weeklyStats.maxByOrNull { it.completed + it.answeredRight }?.day?.uppercase() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "Melhor Dia:",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
|
||||||
|
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = bestDay,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ThemePreviews
|
||||||
|
@Composable
|
||||||
|
fun WeeklyActivityChartWidgetPreview() {
|
||||||
|
val sampleStats = listOf(
|
||||||
|
WeeklyActivityStat("Seg", 30, 15, 10),
|
||||||
|
WeeklyActivityStat("Ter", 45, 20, 12),
|
||||||
|
WeeklyActivityStat("Qua", 80, 25, 15),
|
||||||
|
WeeklyActivityStat("Qui", 84, 35, 18),
|
||||||
|
WeeklyActivityStat("Sex", 50, 40, 22),
|
||||||
|
WeeklyActivityStat("Sáb", 70, 30, 20),
|
||||||
|
WeeklyActivityStat("Dom", 60, 25, 18)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
WeeklyActivityChartWidget(weeklyStats = sampleStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
package eu.gaudian.translator.view.stats.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -6,6 +6,7 @@ import android.content.ClipData
|
|||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.RepeatMode
|
import androidx.compose.animation.core.RepeatMode
|
||||||
@@ -58,6 +59,7 @@ import eu.gaudian.translator.view.LocalConnectionConfigured
|
|||||||
import eu.gaudian.translator.view.NoConnectionScreen
|
import eu.gaudian.translator.view.NoConnectionScreen
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||||
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.dialogs.AddVocabularyDialog
|
import eu.gaudian.translator.view.dialogs.AddVocabularyDialog
|
||||||
import eu.gaudian.translator.view.hints.HintDefinition
|
import eu.gaudian.translator.view.hints.HintDefinition
|
||||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||||
@@ -88,7 +90,10 @@ fun TranslationScreen(
|
|||||||
|
|
||||||
|
|
||||||
if (isInitializationComplete && !connectionConfigured) {
|
if (isInitializationComplete && !connectionConfigured) {
|
||||||
NoConnectionScreen(onSettingsClick = { navController.navigate(SettingsRoutes.API_KEY) })
|
NoConnectionScreen(
|
||||||
|
onSettingsClick = { navController.navigate(SettingsRoutes.API_KEY) },
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +113,7 @@ fun TranslationScreen(
|
|||||||
onSettingsClick = onSettingsClick,
|
onSettingsClick = onSettingsClick,
|
||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
if (!navController.popBackStack()) {
|
if (!navController.popBackStack()) {
|
||||||
navController.navigate(eu.gaudian.translator.view.composable.Screen.Home.route) {
|
navController.navigate(Screen.Home.route) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
restoreState = false
|
restoreState = false
|
||||||
}
|
}
|
||||||
@@ -182,7 +187,7 @@ private fun LoadedTranslationContent(
|
|||||||
|
|
||||||
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
AppCard(modifier = Modifier.padding(8.dp, end = 8.dp, bottom = 8.dp, top = 0.dp)) {
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val isLandscape = configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
|
||||||
if (isLandscape) {
|
if (isLandscape) {
|
||||||
Row(modifier = Modifier.fillMaxSize()) {
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|||||||
@@ -1,671 +0,0 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead", "UnusedMaterial3ScaffoldPaddingParameter")
|
|
||||||
|
|
||||||
package eu.gaudian.translator.view.vocabulary
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.Spring
|
|
||||||
import androidx.compose.animation.core.VisibilityThreshold
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
|
||||||
import androidx.compose.foundation.gestures.scrollBy
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.model.VocabularyStage
|
|
||||||
import eu.gaudian.translator.model.WidgetType
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
|
||||||
import eu.gaudian.translator.view.dialogs.MissingLanguageDialog
|
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.AllVocabularyWidget
|
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget
|
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget
|
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget
|
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget
|
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
|
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
|
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.SettingsViewModel
|
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@SuppressLint("FrequentlyChangingValue")
|
|
||||||
@Composable
|
|
||||||
fun DashboardContent(
|
|
||||||
navController: NavController,
|
|
||||||
onShowCustomExerciseDialog: () -> Unit,
|
|
||||||
startDailyExercise: (Boolean) -> Unit,
|
|
||||||
onNavigateToCategoryDetail: (Int) -> Unit,
|
|
||||||
onNavigateToCategoryList: () -> Unit,
|
|
||||||
onShowWordPairExerciseDialog: () -> Unit,
|
|
||||||
onScroll: (Boolean) -> Unit = {},
|
|
||||||
) {
|
|
||||||
val activity = LocalContext.current.findActivity()
|
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
val progressViewModel: ProgressViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
|
|
||||||
var showMissingLanguageDialog by remember { mutableStateOf(false) }
|
|
||||||
var selectedMissingLanguageId by remember { mutableStateOf<Int?>(null) }
|
|
||||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
|
||||||
|
|
||||||
val affectedItems by remember(selectedMissingLanguageId) {
|
|
||||||
selectedMissingLanguageId?.let {
|
|
||||||
vocabularyViewModel.getItemsForLanguage(it)
|
|
||||||
} ?: flowOf(emptyList())
|
|
||||||
}.collectAsState(initial = emptyList())
|
|
||||||
|
|
||||||
if (showMissingLanguageDialog && selectedMissingLanguageId != null) {
|
|
||||||
MissingLanguageDialog(
|
|
||||||
showDialog = true,
|
|
||||||
missingLanguageId = selectedMissingLanguageId!!,
|
|
||||||
affectedItems = affectedItems,
|
|
||||||
onDismiss = { showMissingLanguageDialog = false },
|
|
||||||
onDelete = { items ->
|
|
||||||
vocabularyViewModel.deleteVocabularyItemsById(items.map { it.id })
|
|
||||||
showMissingLanguageDialog = false
|
|
||||||
},
|
|
||||||
onReplace = { oldId, newId ->
|
|
||||||
vocabularyViewModel.replaceLanguageId(oldId, newId)
|
|
||||||
showMissingLanguageDialog = false
|
|
||||||
},
|
|
||||||
onCreate = { newLanguage ->
|
|
||||||
languageViewModel.addCustomLanguage(newLanguage)
|
|
||||||
},
|
|
||||||
languageViewModel = languageViewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AppOutlinedCard {
|
|
||||||
// We collect the order from DB initially
|
|
||||||
val initialWidgetOrder by settingsViewModel.widgetOrder.collectAsState(initial = null)
|
|
||||||
val collapsedWidgetIds by settingsViewModel.collapsedWidgetIds.collectAsState(initial = emptySet())
|
|
||||||
val dashboardScrollState by settingsViewModel.dashboardScrollState.collectAsState()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
if (initialWidgetOrder == null) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(vertical = 64.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// BEST PRACTICE: Use a SnapshotStateList for immediate UI updates.
|
|
||||||
// We only initialize this once, so DB updates don't reset the list while dragging.
|
|
||||||
val orderedWidgets = remember { mutableStateListOf<WidgetType>() }
|
|
||||||
|
|
||||||
// Sync with DB only on first load
|
|
||||||
LaunchedEffect(initialWidgetOrder) {
|
|
||||||
if (orderedWidgets.isEmpty() && !initialWidgetOrder.isNullOrEmpty()) {
|
|
||||||
orderedWidgets.addAll(initialWidgetOrder!!)
|
|
||||||
} else if (orderedWidgets.isEmpty()) {
|
|
||||||
orderedWidgets.addAll(WidgetType.DEFAULT_ORDER)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState(
|
|
||||||
initialFirstVisibleItemIndex = dashboardScrollState.first,
|
|
||||||
initialFirstVisibleItemScrollOffset = dashboardScrollState.second
|
|
||||||
)
|
|
||||||
|
|
||||||
// Save scroll state
|
|
||||||
LaunchedEffect(lazyListState.firstVisibleItemIndex, lazyListState.firstVisibleItemScrollOffset) {
|
|
||||||
settingsViewModel.saveDashboardScrollState(
|
|
||||||
lazyListState.firstVisibleItemIndex,
|
|
||||||
lazyListState.firstVisibleItemScrollOffset
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect scroll and notify parent
|
|
||||||
LaunchedEffect(lazyListState.isScrollInProgress) {
|
|
||||||
onScroll(lazyListState.isScrollInProgress)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
settingsViewModel.saveDashboardScrollState(
|
|
||||||
lazyListState.firstVisibleItemIndex,
|
|
||||||
lazyListState.firstVisibleItemScrollOffset
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Robust Drag and Drop State ---
|
|
||||||
val dragDropState = rememberDragDropState(
|
|
||||||
lazyListState = lazyListState,
|
|
||||||
onSwap = { fromIndex, toIndex ->
|
|
||||||
// Swap data immediately for responsiveness
|
|
||||||
orderedWidgets.apply {
|
|
||||||
add(toIndex, removeAt(fromIndex))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragEnd = {
|
|
||||||
// Persist to DB only when user drops
|
|
||||||
settingsViewModel.saveWidgetOrder(orderedWidgets.toList())
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.dragContainer(dragDropState),
|
|
||||||
contentPadding = PaddingValues(bottom = 160.dp)
|
|
||||||
) {
|
|
||||||
itemsIndexed(
|
|
||||||
items = orderedWidgets,
|
|
||||||
key = { _, widget -> widget.id }
|
|
||||||
) { index, widgetType ->
|
|
||||||
|
|
||||||
val isDragging = index == dragDropState.draggingItemIndex
|
|
||||||
|
|
||||||
// Calculate translation: distinct logic for dragged vs. stationary items
|
|
||||||
val translationY = if (isDragging) {
|
|
||||||
dragDropState.draggingItemOffset
|
|
||||||
} else {
|
|
||||||
0f
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.zIndex(if (isDragging) 1f else 0f)
|
|
||||||
.graphicsLayer {
|
|
||||||
this.translationY = translationY
|
|
||||||
this.shadowElevation = if (isDragging) 8.dp.toPx() else 0f
|
|
||||||
this.scaleX = if (isDragging) 1.02f else 1f
|
|
||||||
this.scaleY = if (isDragging) 1.02f else 1f
|
|
||||||
}
|
|
||||||
// CRITICAL FIX: Only apply animation to items NOT being dragged.
|
|
||||||
// This prevents the "flicker" by stopping the layout animation
|
|
||||||
// from fighting your manual drag offset.
|
|
||||||
.then(
|
|
||||||
if (!isDragging) {
|
|
||||||
Modifier.animateItem(
|
|
||||||
placementSpec = spring(
|
|
||||||
stiffness = Spring.StiffnessLow,
|
|
||||||
visibilityThreshold = IntOffset.VisibilityThreshold
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
WidgetContainer(
|
|
||||||
widgetType = widgetType,
|
|
||||||
isExpanded = widgetType.id !in collapsedWidgetIds,
|
|
||||||
onExpandedChange = { newExpandedState ->
|
|
||||||
scope.launch {
|
|
||||||
settingsViewModel.setWidgetExpandedState(widgetType.id, newExpandedState)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragStart = { dragDropState.onDragStart(index) },
|
|
||||||
onDrag = { dragAmount -> dragDropState.onDrag(dragAmount) },
|
|
||||||
onDragEnd = { dragDropState.onDragEnd() },
|
|
||||||
onDragCancel = { dragDropState.onDragInterrupted() },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
LazyWidget(
|
|
||||||
widgetType = widgetType,
|
|
||||||
navController = navController,
|
|
||||||
vocabularyViewModel = vocabularyViewModel,
|
|
||||||
progressViewModel = progressViewModel,
|
|
||||||
onShowCustomExerciseDialog = onShowCustomExerciseDialog,
|
|
||||||
startDailyExercise = startDailyExercise,
|
|
||||||
onNavigateToCategoryDetail = onNavigateToCategoryDetail,
|
|
||||||
onNavigateToCategoryList = onNavigateToCategoryList,
|
|
||||||
onShowWordPairExerciseDialog = onShowWordPairExerciseDialog,
|
|
||||||
onMissingLanguage = { missingId ->
|
|
||||||
selectedMissingLanguageId = missingId
|
|
||||||
showMissingLanguageDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun WidgetContainer(
|
|
||||||
widgetType: WidgetType,
|
|
||||||
isExpanded: Boolean,
|
|
||||||
onExpandedChange: (Boolean) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onDragStart: () -> Unit,
|
|
||||||
onDrag: (Float) -> Unit,
|
|
||||||
onDragEnd: () -> Unit,
|
|
||||||
onDragCancel: () -> Unit,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
AppCard(
|
|
||||||
modifier = modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.animateContentSize(animationSpec = tween(300, easing = FastOutSlowInEasing))
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(widgetType.titleRes),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
|
|
||||||
IconButton(onClick = { onExpandedChange(!isExpanded) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (isExpanded) AppIcons.ArrowDropUp
|
|
||||||
else AppIcons.ArrowDropDown,
|
|
||||||
contentDescription = if (isExpanded) stringResource(R.string.text_collapse_widget)
|
|
||||||
else stringResource(R.string.text_expand_widget)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drag Handle with specific pointer input
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.DragHandle,
|
|
||||||
contentDescription = stringResource(R.string.text_drag_to_reorder),
|
|
||||||
tint = if (isExpanded) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
|
||||||
else MaterialTheme.colorScheme.onSurface,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 8.dp, start = 8.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectDragGestures(
|
|
||||||
onDragStart = { _ -> onDragStart() },
|
|
||||||
onDrag = { change, dragAmount ->
|
|
||||||
change.consume()
|
|
||||||
onDrag(dragAmount.y)
|
|
||||||
},
|
|
||||||
onDragEnd = { onDragEnd() },
|
|
||||||
onDragCancel = { onDragCancel() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (isExpanded) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------
|
|
||||||
// Fixed Drag and Drop Logic
|
|
||||||
// --------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun rememberDragDropState(
|
|
||||||
lazyListState: LazyListState,
|
|
||||||
onSwap: (Int, Int) -> Unit,
|
|
||||||
onDragEnd: () -> Unit
|
|
||||||
): DragDropState {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
return remember(lazyListState, scope) {
|
|
||||||
DragDropState(
|
|
||||||
state = lazyListState,
|
|
||||||
onSwap = onSwap,
|
|
||||||
onDragFinished = onDragEnd,
|
|
||||||
scope = scope
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
|
|
||||||
return this.pointerInput(dragDropState) {
|
|
||||||
// Just allows the modifier to exist in the chain, logic is in the handle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DragDropState(
|
|
||||||
private val state: LazyListState,
|
|
||||||
private val onSwap: (Int, Int) -> Unit,
|
|
||||||
private val onDragFinished: () -> Unit,
|
|
||||||
private val scope: CoroutineScope
|
|
||||||
) {
|
|
||||||
var draggingItemIndex by mutableIntStateOf(-1)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val _draggingItemOffset = Animatable(0f)
|
|
||||||
val draggingItemOffset: Float
|
|
||||||
get() = _draggingItemOffset.value
|
|
||||||
|
|
||||||
private val scrollChannel = Channel<Float>(Channel.CONFLATED)
|
|
||||||
|
|
||||||
init {
|
|
||||||
scope.launch {
|
|
||||||
for (scrollAmount in scrollChannel) {
|
|
||||||
if (scrollAmount != 0f) {
|
|
||||||
state.scrollBy(scrollAmount)
|
|
||||||
checkSwap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDragStart(index: Int) {
|
|
||||||
draggingItemIndex = index
|
|
||||||
scope.launch { _draggingItemOffset.snapTo(0f) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDrag(dragAmount: Float) {
|
|
||||||
if (draggingItemIndex == -1) return
|
|
||||||
|
|
||||||
scope.launch {
|
|
||||||
_draggingItemOffset.snapTo(_draggingItemOffset.value + dragAmount)
|
|
||||||
checkSwap()
|
|
||||||
checkOverscroll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkSwap() {
|
|
||||||
val draggedIndex = draggingItemIndex
|
|
||||||
if (draggedIndex == -1) return
|
|
||||||
|
|
||||||
val visibleItems = state.layoutInfo.visibleItemsInfo
|
|
||||||
val draggedItemInfo = visibleItems.find { it.index == draggedIndex } ?: return
|
|
||||||
|
|
||||||
// Calculate the visual center of the dragged item
|
|
||||||
val draggedCenter = draggedItemInfo.offset + (draggedItemInfo.size / 2) + _draggingItemOffset.value
|
|
||||||
|
|
||||||
// Find a target to swap with
|
|
||||||
// FIX: We strictly check if we have crossed the CENTER of the target item.
|
|
||||||
// This acts as a hysteresis buffer to prevent flickering at the edges.
|
|
||||||
val targetItem = visibleItems.find { item ->
|
|
||||||
item.index != draggedIndex &&
|
|
||||||
draggedCenter > item.offset &&
|
|
||||||
draggedCenter < (item.offset + item.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetItem != null) {
|
|
||||||
// Extra Check: Ensure we have actually crossed the midpoint of the target
|
|
||||||
val targetCenter = itemCenter(targetItem.offset, targetItem.size)
|
|
||||||
val isAboveAndMovingDown = draggedIndex < targetItem.index && draggedCenter > targetCenter
|
|
||||||
val isBelowAndMovingUp = draggedIndex > targetItem.index && draggedCenter < targetCenter
|
|
||||||
|
|
||||||
if (isAboveAndMovingDown || isBelowAndMovingUp) {
|
|
||||||
val targetIndex = targetItem.index
|
|
||||||
|
|
||||||
// 1. Swap Data
|
|
||||||
onSwap(draggedIndex, targetIndex)
|
|
||||||
|
|
||||||
// 2. Adjust Offset
|
|
||||||
// We calculate the physical distance the item moved in the layout (e.g. 150px).
|
|
||||||
// We subtract this from the current drag offset to keep the item visually stationary under the finger.
|
|
||||||
val layoutJumpDistance = (targetItem.offset - draggedItemInfo.offset).toFloat()
|
|
||||||
|
|
||||||
scope.launch {
|
|
||||||
_draggingItemOffset.snapTo(_draggingItemOffset.value - layoutJumpDistance)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Update Index
|
|
||||||
draggingItemIndex = targetIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun itemCenter(offset: Int, size: Int): Float {
|
|
||||||
return offset + (size / 2f)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkOverscroll() {
|
|
||||||
val draggedIndex = draggingItemIndex
|
|
||||||
if (draggedIndex == -1) {
|
|
||||||
scrollChannel.trySend(0f)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val layoutInfo = state.layoutInfo
|
|
||||||
val visibleItems = layoutInfo.visibleItemsInfo
|
|
||||||
val draggedItemInfo = visibleItems.find { it.index == draggedIndex } ?: return
|
|
||||||
|
|
||||||
val viewportStart = layoutInfo.viewportStartOffset
|
|
||||||
val viewportEnd = layoutInfo.viewportEndOffset
|
|
||||||
// Increased threshold slightly for smoother top-edge scrolling
|
|
||||||
val boundsStart = viewportStart + (viewportEnd * 0.15f)
|
|
||||||
val boundsEnd = viewportEnd - (viewportEnd * 0.15f)
|
|
||||||
|
|
||||||
val itemTop = draggedItemInfo.offset + _draggingItemOffset.value
|
|
||||||
val itemBottom = itemTop + draggedItemInfo.size
|
|
||||||
|
|
||||||
val scrollAmount = when {
|
|
||||||
itemTop < boundsStart -> -10f // Slower, more controlled scroll speed
|
|
||||||
itemBottom > boundsEnd -> 10f
|
|
||||||
else -> 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollChannel.trySend(scrollAmount)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDragEnd() {
|
|
||||||
resetDrag()
|
|
||||||
onDragFinished()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDragInterrupted() {
|
|
||||||
resetDrag()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resetDrag() {
|
|
||||||
draggingItemIndex = -1
|
|
||||||
scrollChannel.trySend(0f)
|
|
||||||
scope.launch { _draggingItemOffset.snapTo(0f) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------
|
|
||||||
// Remainder of your existing components
|
|
||||||
// --------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LazyWidget(
|
|
||||||
widgetType: WidgetType,
|
|
||||||
navController: NavController,
|
|
||||||
vocabularyViewModel: VocabularyViewModel,
|
|
||||||
progressViewModel: ProgressViewModel,
|
|
||||||
onShowCustomExerciseDialog: () -> Unit,
|
|
||||||
startDailyExercise: (Boolean) -> Unit,
|
|
||||||
onNavigateToCategoryDetail: (Int) -> Unit,
|
|
||||||
onNavigateToCategoryList: () -> Unit,
|
|
||||||
onShowWordPairExerciseDialog: () -> Unit,
|
|
||||||
onMissingLanguage: (Int) -> Unit
|
|
||||||
) {
|
|
||||||
when (widgetType) {
|
|
||||||
|
|
||||||
|
|
||||||
WidgetType.Status -> LazyStatusWidget(
|
|
||||||
vocabularyViewModel = vocabularyViewModel,
|
|
||||||
onNavigateToNew = { navController.navigate("vocabulary_sorting?mode=NEW") },
|
|
||||||
onNavigateToDuplicates = { navController.navigate("vocabulary_sorting?mode=DUPLICATES") },
|
|
||||||
onNavigateToFaulty = { navController.navigate("vocabulary_sorting?mode=FAULTY") },
|
|
||||||
onNavigateToNoGrammar = { navController.navigate("no_grammar_items") },
|
|
||||||
onNavigateToMissingLanguage = onMissingLanguage
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// Regular widgets that load immediately
|
|
||||||
when (widgetType) {
|
|
||||||
WidgetType.Streak -> StreakWidget(
|
|
||||||
streak = progressViewModel.streak.collectAsState(initial = 0).value,
|
|
||||||
lastSevenDays = progressViewModel.lastSevenDays.collectAsState().value,
|
|
||||||
dueTodayCount = progressViewModel.dueTodayCount.collectAsState().value,
|
|
||||||
wordsCompleted = progressViewModel.totalWordsCompleted.collectAsState().value,
|
|
||||||
onStatisticsClicked = { navController.navigate("vocabulary_heatmap") }
|
|
||||||
)
|
|
||||||
|
|
||||||
WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget(
|
|
||||||
weeklyStats = progressViewModel.weeklyActivityStats.collectAsState().value
|
|
||||||
)
|
|
||||||
|
|
||||||
WidgetType.AllVocabulary -> AllVocabularyWidget(
|
|
||||||
vocabularyViewModel = vocabularyViewModel,
|
|
||||||
onOpenAllVocabulary = { navController.navigate("vocabulary_list/false/null") },
|
|
||||||
onStageClicked = { stage -> navController.navigate("vocabulary_list/false/$stage") }
|
|
||||||
)
|
|
||||||
|
|
||||||
WidgetType.DueToday -> DueTodayWidget(
|
|
||||||
vocabularyViewModel = vocabularyViewModel,
|
|
||||||
onStageClicked = { stage -> navController.navigate("vocabulary_list/false/$stage") }
|
|
||||||
)
|
|
||||||
|
|
||||||
WidgetType.CategoryProgress -> CategoryProgressWidget(
|
|
||||||
onCategoryClicked = { category ->
|
|
||||||
category?.let { onNavigateToCategoryDetail(it.id) }
|
|
||||||
},
|
|
||||||
onViewAllClicked = onNavigateToCategoryList
|
|
||||||
)
|
|
||||||
|
|
||||||
WidgetType.Levels -> LevelWidget(
|
|
||||||
totalWords = vocabularyViewModel.vocabularyItems.collectAsState().value.size,
|
|
||||||
learnedWords = vocabularyViewModel.stageStats.collectAsState().value
|
|
||||||
.firstOrNull { it.stage == VocabularyStage.LEARNED }?.itemCount ?: 0,
|
|
||||||
onNavigateToProgress = { navController.navigate("language_progress") }
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LazyStatusWidget(
|
|
||||||
vocabularyViewModel: VocabularyViewModel,
|
|
||||||
onNavigateToNew: () -> Unit,
|
|
||||||
onNavigateToDuplicates: () -> Unit,
|
|
||||||
onNavigateToFaulty: () -> Unit,
|
|
||||||
onNavigateToNoGrammar: () -> Unit,
|
|
||||||
onNavigateToMissingLanguage: (Int) -> Unit
|
|
||||||
) {
|
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
|
||||||
|
|
||||||
// Collect all flows asynchronously
|
|
||||||
val newItemsCount by vocabularyViewModel.newItemsCount.collectAsState()
|
|
||||||
val duplicateCount by vocabularyViewModel.duplicateItemsCount.collectAsState()
|
|
||||||
val faultyItemsCount by vocabularyViewModel.faultyItemsCount.collectAsState()
|
|
||||||
val itemsWithoutGrammarCount by vocabularyViewModel.itemsWithoutGrammarCount.collectAsState()
|
|
||||||
val missingLanguageInfo by vocabularyViewModel.missingLanguageInfo.collectAsState()
|
|
||||||
|
|
||||||
LaunchedEffect(
|
|
||||||
newItemsCount,
|
|
||||||
duplicateCount,
|
|
||||||
faultyItemsCount,
|
|
||||||
itemsWithoutGrammarCount,
|
|
||||||
missingLanguageInfo
|
|
||||||
) {
|
|
||||||
delay(100)
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 32.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
StatusWidget(
|
|
||||||
onNavigateToNew = onNavigateToNew,
|
|
||||||
onNavigateToDuplicates = onNavigateToDuplicates,
|
|
||||||
onNavigateToFaulty = onNavigateToFaulty,
|
|
||||||
onNavigateToNoGrammar = onNavigateToNoGrammar,
|
|
||||||
onNavigateToMissingLanguage = onNavigateToMissingLanguage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun DashboardContentPreview() {
|
|
||||||
val navController = rememberNavController()
|
|
||||||
DashboardContent(
|
|
||||||
navController = navController,
|
|
||||||
onShowCustomExerciseDialog = {},
|
|
||||||
onNavigateToCategoryDetail = {},
|
|
||||||
startDailyExercise = {},
|
|
||||||
onNavigateToCategoryList = {},
|
|
||||||
onShowWordPairExerciseDialog = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun WidgetContainerPreview() {
|
|
||||||
WidgetContainer(
|
|
||||||
widgetType = WidgetType.Streak,
|
|
||||||
isExpanded = true,
|
|
||||||
onExpandedChange = {},
|
|
||||||
onDragStart = { } ,
|
|
||||||
onDrag = { },
|
|
||||||
onDragEnd = { },
|
|
||||||
onDragCancel = { }
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text("Preview Content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -67,7 +67,7 @@ import eu.gaudian.translator.view.composable.AppTopAppBar
|
|||||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LanguageProgressScreen(navController: NavController) {
|
fun LanguageJourneyScreen(navController: NavController) {
|
||||||
|
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val progressViewModel : ProgressViewModel = hiltViewModel(activity)
|
val progressViewModel : ProgressViewModel = hiltViewModel(activity)
|
||||||
@@ -379,6 +379,6 @@ private fun LevelDetailDialog(level: MyAppLanguageLevel, onDismiss: () -> Unit)
|
|||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun LanguageProgressScreenPreview() {
|
fun LanguageJourneyScreenPreview() {
|
||||||
LanguageProgressScreen(navController = NavController(LocalContext.current))
|
LanguageJourneyScreen(navController = NavController(LocalContext.current))
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package eu.gaudian.translator.view.vocabulary
|
package eu.gaudian.translator.view.vocabulary
|
||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -16,38 +14,30 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AutoAwesome
|
import androidx.compose.material.icons.filled.AutoAwesome
|
||||||
import androidx.compose.material.icons.filled.DriveFolderUpload
|
import androidx.compose.material.icons.filled.DriveFolderUpload
|
||||||
import androidx.compose.material.icons.filled.EditNote
|
import androidx.compose.material.icons.filled.EditNote
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -56,16 +46,15 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.Language
|
|
||||||
import eu.gaudian.translator.model.VocabularyItem
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
import eu.gaudian.translator.utils.StatusMessageId
|
|
||||||
import eu.gaudian.translator.utils.StatusMessageService
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
|
import eu.gaudian.translator.view.LocalConnectionConfigured
|
||||||
import eu.gaudian.translator.view.NavigationRoutes
|
import eu.gaudian.translator.view.NavigationRoutes
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
|
import eu.gaudian.translator.view.composable.AppIconContainer
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
import eu.gaudian.translator.view.composable.AppSlider
|
import eu.gaudian.translator.view.composable.AppSlider
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.InspiringSearchField
|
import eu.gaudian.translator.view.composable.InspiringSearchField
|
||||||
@@ -74,6 +63,7 @@ import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
|||||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||||
import eu.gaudian.translator.view.hints.HintDefinition
|
import eu.gaudian.translator.view.hints.HintDefinition
|
||||||
import eu.gaudian.translator.view.library.VocabularyCard
|
import eu.gaudian.translator.view.library.VocabularyCard
|
||||||
|
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -104,109 +94,12 @@ fun NewWordScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val statusMessageService = StatusMessageService
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val showTableImportDialog = remember { mutableStateOf(false) }
|
|
||||||
var parsedTable by remember { mutableStateOf<List<List<String>>>(emptyList()) }
|
|
||||||
var selectedColFirst by remember { mutableIntStateOf(0) }
|
|
||||||
var selectedColSecond by remember { mutableIntStateOf(1) }
|
|
||||||
var skipHeader by remember { mutableStateOf(true) }
|
|
||||||
var selectedLangFirst by remember { mutableStateOf<Language?>(null) }
|
|
||||||
var selectedLangSecond by remember { mutableStateOf<Language?>(null) }
|
|
||||||
|
|
||||||
val recentlyAdded = remember(recentItems) {
|
val recentlyAdded = remember(recentItems) {
|
||||||
recentItems.sortedByDescending { it.id }.take(4)
|
recentItems.sortedByDescending { it.id }.take(4)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseCsv(text: String): List<List<String>> {
|
|
||||||
if (text.isBlank()) return emptyList()
|
|
||||||
val candidates = listOf(',', ';', '\t')
|
|
||||||
val sampleLine = text.lineSequence().firstOrNull { it.isNotBlank() } ?: return emptyList()
|
|
||||||
val delimiter = candidates.maxBy { c -> sampleLine.count { it == c } }
|
|
||||||
|
|
||||||
val rows = mutableListOf<List<String>>()
|
|
||||||
var current = StringBuilder()
|
|
||||||
var inQuotes = false
|
|
||||||
val currentRow = mutableListOf<String>()
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
while (i < text.length) {
|
|
||||||
when (val ch = text[i]) {
|
|
||||||
'"' -> {
|
|
||||||
if (inQuotes && i + 1 < text.length && text[i + 1] == '"') {
|
|
||||||
current.append('"')
|
|
||||||
i++
|
|
||||||
} else {
|
|
||||||
inQuotes = !inQuotes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'\r' -> {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
'\n' -> {
|
|
||||||
val field = current.toString()
|
|
||||||
current = StringBuilder()
|
|
||||||
currentRow.add(field)
|
|
||||||
rows.add(currentRow.toList())
|
|
||||||
currentRow.clear()
|
|
||||||
inQuotes = false
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
if (ch == delimiter && !inQuotes) {
|
|
||||||
val field = current.toString()
|
|
||||||
currentRow.add(field)
|
|
||||||
current = StringBuilder()
|
|
||||||
} else {
|
|
||||||
current.append(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if (current.isNotEmpty() || currentRow.isNotEmpty()) {
|
|
||||||
currentRow.add(current.toString())
|
|
||||||
rows.add(currentRow.toList())
|
|
||||||
}
|
|
||||||
return rows.map { row ->
|
|
||||||
row.map { it.trim().trim('"') }
|
|
||||||
}.filter { r -> r.any { it.isNotBlank() } }
|
|
||||||
}
|
|
||||||
|
|
||||||
val importTableLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.OpenDocument(),
|
|
||||||
onResult = { uri ->
|
|
||||||
uri?.let { u ->
|
|
||||||
try {
|
|
||||||
context.contentResolver.takePersistableUriPermission(u, android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
} catch (_: Exception) {}
|
|
||||||
try {
|
|
||||||
val mime = context.contentResolver.getType(u)
|
|
||||||
val isExcel = mime == "application/vnd.ms-excel" ||
|
|
||||||
mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
||||||
if (isExcel) {
|
|
||||||
statusMessageService.showErrorById(StatusMessageId.ERROR_EXCEL_NOT_SUPPORTED)
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
context.contentResolver.openInputStream(u)?.use { inputStream ->
|
|
||||||
val text = inputStream.bufferedReader().use { it.readText() }
|
|
||||||
val rows = parseCsv(text)
|
|
||||||
if (rows.isNotEmpty() && rows.maxOf { it.size } >= 2) {
|
|
||||||
parsedTable = rows
|
|
||||||
selectedColFirst = 0
|
|
||||||
selectedColSecond = 1.coerceAtMost(rows.first().size - 1)
|
|
||||||
showTableImportDialog.value = true
|
|
||||||
} else {
|
|
||||||
statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE_WITH_REASON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -227,6 +120,14 @@ fun NewWordScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Explore Packs - Prominent full-width card at top
|
||||||
|
ExplorePacksProminentCard(
|
||||||
|
onClick = { navController.navigate(NavigationRoutes.EXPLORE_PACKS) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// AI Generator Card
|
||||||
AIGeneratorCard(
|
AIGeneratorCard(
|
||||||
category = category,
|
category = category,
|
||||||
onCategoryChange = { category = it },
|
onCategoryChange = { category = it },
|
||||||
@@ -242,10 +143,12 @@ fun NewWordScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
navController = navController,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Add Manually Card
|
||||||
AddManuallyCard(
|
AddManuallyCard(
|
||||||
languageViewModel = languageViewModel,
|
languageViewModel = languageViewModel,
|
||||||
vocabularyViewModel = vocabularyViewModel,
|
vocabularyViewModel = vocabularyViewModel,
|
||||||
@@ -253,8 +156,10 @@ fun NewWordScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
BottomActionCardsRow(
|
// Import CSV - Full width card at bottom
|
||||||
onImportCsvClick = {
|
ImportCsvCard(
|
||||||
|
onClick = {
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
navController.navigate("settings_vocabulary_repository_options")
|
navController.navigate("settings_vocabulary_repository_options")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -300,123 +205,6 @@ fun NewWordScreen(
|
|||||||
Spacer(modifier = Modifier.height(100.dp))
|
Spacer(modifier = Modifier.height(100.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showTableImportDialog.value) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showTableImportDialog.value = false },
|
|
||||||
title = { Text(stringResource(R.string.label_import_table_csv_excel)) },
|
|
||||||
text = {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
||||||
val columnCount = parsedTable.maxOfOrNull { it.size } ?: 0
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(stringResource(R.string.label_first_column), modifier = Modifier.weight(1f))
|
|
||||||
var menu1Expanded by remember { mutableStateOf(false) }
|
|
||||||
AppOutlinedButton(onClick = { menu1Expanded = true }) {
|
|
||||||
Text(stringResource(R.string.label_column_n, selectedColFirst + 1))
|
|
||||||
}
|
|
||||||
DropdownMenu(expanded = menu1Expanded, onDismissRequest = { menu1Expanded = false }) {
|
|
||||||
(0 until columnCount).forEach { idx ->
|
|
||||||
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("#${idx + 1} • $header") },
|
|
||||||
onClick = { selectedColFirst = idx; menu1Expanded = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(stringResource(R.string.label_second_column), modifier = Modifier.weight(1f))
|
|
||||||
var menu2Expanded by remember { mutableStateOf(false) }
|
|
||||||
AppOutlinedButton(onClick = { menu2Expanded = true }) {
|
|
||||||
Text(stringResource(R.string.label_column_n, selectedColSecond + 1))
|
|
||||||
}
|
|
||||||
DropdownMenu(expanded = menu2Expanded, onDismissRequest = { menu2Expanded = false }) {
|
|
||||||
(0 until columnCount).forEach { idx ->
|
|
||||||
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("#${idx + 1} • $header") },
|
|
||||||
onClick = { selectedColSecond = idx; menu2Expanded = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(stringResource(R.string.label_languages))
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(stringResource(R.string.label_first_language))
|
|
||||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
selectedLanguage = selectedLangFirst,
|
|
||||||
onLanguageSelected = { selectedLangFirst = it }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(stringResource(R.string.label_second_language))
|
|
||||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
selectedLanguage = selectedLangSecond,
|
|
||||||
onLanguageSelected = { selectedLangSecond = it }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
androidx.compose.material3.Checkbox(checked = skipHeader, onCheckedChange = { skipHeader = it })
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(stringResource(R.string.label_header_row))
|
|
||||||
}
|
|
||||||
val startIdx = if (skipHeader) 1 else 0
|
|
||||||
val previewA = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColFirst) }.joinToString(", ")
|
|
||||||
val previewB = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColSecond) }.joinToString(", ")
|
|
||||||
Text(stringResource(R.string.label_preview_first, previewA))
|
|
||||||
Text(stringResource(R.string.label_preview_second, previewB))
|
|
||||||
val totalRows = parsedTable.drop(startIdx).count { row ->
|
|
||||||
val a = row.getOrNull(selectedColFirst).orEmpty().isNotBlank()
|
|
||||||
val b = row.getOrNull(selectedColSecond).orEmpty().isNotBlank()
|
|
||||||
a || b
|
|
||||||
}
|
|
||||||
Text(stringResource(R.string.text_rows_to_import_1d, totalRows))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
if (selectedColFirst == selectedColSecond) {
|
|
||||||
statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_TWO_COLUMNS)
|
|
||||||
return@TextButton
|
|
||||||
}
|
|
||||||
val langA = selectedLangFirst
|
|
||||||
val langB = selectedLangSecond
|
|
||||||
if (langA == null || langB == null) {
|
|
||||||
statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_LANGUAGES)
|
|
||||||
return@TextButton
|
|
||||||
}
|
|
||||||
val startIdx = if (skipHeader) 1 else 0
|
|
||||||
val items = parsedTable.drop(startIdx).mapNotNull { row ->
|
|
||||||
val a = row.getOrNull(selectedColFirst)?.trim().orEmpty()
|
|
||||||
val b = row.getOrNull(selectedColSecond)?.trim().orEmpty()
|
|
||||||
if (a.isBlank() && b.isBlank()) null else VocabularyItem(
|
|
||||||
id = 0,
|
|
||||||
languageFirstId = langA.nameResId,
|
|
||||||
languageSecondId = langB.nameResId,
|
|
||||||
wordFirst = a,
|
|
||||||
wordSecond = b
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (items.isEmpty()) {
|
|
||||||
statusMessageService.showErrorById(StatusMessageId.ERROR_NO_ROWS_TO_IMPORT)
|
|
||||||
return@TextButton
|
|
||||||
}
|
|
||||||
vocabularyViewModel.addVocabularyItems(items)
|
|
||||||
statusMessageService.showSuccessById(StatusMessageId.SUCCESS_ITEMS_IMPORTED)
|
|
||||||
showTableImportDialog.value = false
|
|
||||||
}) { Text(stringResource(R.string.label_import)) }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showTableImportDialog.value = false }) {
|
|
||||||
Text(stringResource(R.string.label_cancel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- AI GENERATOR CARD (From previous implementation) ---
|
// --- AI GENERATOR CARD (From previous implementation) ---
|
||||||
@@ -430,104 +218,156 @@ fun AIGeneratorCard(
|
|||||||
languageViewModel: LanguageViewModel,
|
languageViewModel: LanguageViewModel,
|
||||||
isGenerating: Boolean,
|
isGenerating: Boolean,
|
||||||
onGenerate: () -> Unit,
|
onGenerate: () -> Unit,
|
||||||
|
navController: NavHostController,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val icon = Icons.Default.AutoAwesome
|
val connectionConfigured = LocalConnectionConfigured.current
|
||||||
val hints = stringArrayResource(R.array.vocabulary_hints)
|
|
||||||
AppCard(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
title = stringResource(R.string.label_ai_generator),
|
|
||||||
icon = icon,
|
|
||||||
hintContent = HintDefinition.VOCABULARY_GENERATE_AI.hint(),
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.padding(8.dp)) {
|
|
||||||
|
|
||||||
Text(
|
if (connectionConfigured) {
|
||||||
text = stringResource(R.string.text_search_term),
|
// Show the normal AI generator card
|
||||||
style = MaterialTheme.typography.labelLarge,
|
val icon = Icons.Default.AutoAwesome
|
||||||
fontWeight = FontWeight.SemiBold
|
val hints = stringArrayResource(R.array.vocabulary_hints)
|
||||||
)
|
AppCard(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
modifier = modifier.fillMaxWidth(),
|
||||||
InspiringSearchField(
|
title = stringResource(R.string.label_ai_generator),
|
||||||
value = category,
|
icon = icon,
|
||||||
hints = hints,
|
hintContent = HintDefinition.VOCABULARY_GENERATE_AI.hint(),
|
||||||
onValueChange = onCategoryChange
|
) {
|
||||||
)
|
Column(modifier = Modifier.padding(8.dp)) {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_select_languages),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
SourceLanguageDropdown(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
iconEnabled = false,
|
|
||||||
noBorder = true
|
|
||||||
)
|
|
||||||
TargetLanguageDropdown(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
languageViewModel = languageViewModel,
|
|
||||||
iconEnabled = false,
|
|
||||||
noBorder = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_select_amount),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
AppSlider(
|
|
||||||
value = amount,
|
|
||||||
onValueChange = onAmountChange,
|
|
||||||
valueRange = 1f..25f,
|
|
||||||
steps = 24,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_amount_2d, amount.toInt()),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
|
|
||||||
if (isGenerating) {
|
Text(
|
||||||
|
text = stringResource(R.string.text_search_term),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
InspiringSearchField(
|
||||||
|
value = category,
|
||||||
|
hints = hints,
|
||||||
|
onValueChange = onCategoryChange
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.text_select_languages),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
horizontalArrangement = Arrangement.Center
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
SourceLanguageDropdown(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
languageViewModel = languageViewModel,
|
||||||
|
iconEnabled = false,
|
||||||
|
noBorder = true
|
||||||
|
)
|
||||||
|
TargetLanguageDropdown(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
languageViewModel = languageViewModel,
|
||||||
|
iconEnabled = false,
|
||||||
|
noBorder = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
}
|
Text(
|
||||||
|
text = stringResource(R.string.text_select_amount),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
AppSlider(
|
||||||
|
value = amount,
|
||||||
|
onValueChange = onAmountChange,
|
||||||
|
valueRange = 1f..25f,
|
||||||
|
steps = 24,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.text_amount_2d, amount.toInt()),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
AppButton(
|
if (isGenerating) {
|
||||||
onClick = onGenerate,
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
AppButton(
|
||||||
|
onClick = onGenerate,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp),
|
||||||
|
enabled = category.isNotBlank() && !isGenerating
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Default.AutoAwesome, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.text_generate),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show the "configure connection" card when not configured
|
||||||
|
AppCard(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
title = stringResource(R.string.label_ai_generator),
|
||||||
|
icon = Icons.Default.AutoAwesome,
|
||||||
|
hintContent = HintDefinition.VOCABULARY_GENERATE_AI.hint(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(56.dp),
|
.padding(24.dp),
|
||||||
enabled = category.isNotBlank() && !isGenerating
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Row(
|
Text(
|
||||||
horizontalArrangement = Arrangement.Center,
|
text = stringResource(R.string.text_ai_generator_requires_configuration),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
modifier = Modifier.fillMaxWidth()
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
AppButton(
|
||||||
|
onClick = { navController.navigate(SettingsRoutes.API_KEY) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp)
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Default.AutoAwesome, contentDescription = null, modifier = Modifier.size(20.dp))
|
Row(
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
horizontalArrangement = Arrangement.Center,
|
||||||
Text(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
text = stringResource(R.string.text_generate),
|
modifier = Modifier.fillMaxWidth()
|
||||||
style = MaterialTheme.typography.titleMedium,
|
) {
|
||||||
fontWeight = FontWeight.Bold
|
Icon(imageVector = Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||||
)
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_configure),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,26 +394,16 @@ fun AddManuallyCard(
|
|||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(24.dp)) {
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
// Header Row
|
// Header Row - Using reusable AppIconContainer
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Box(
|
AppIconContainer(
|
||||||
modifier = Modifier
|
imageVector = Icons.Default.EditNote
|
||||||
.size(40.dp)
|
)
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.EditNote,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.label_add_vocabulary),
|
text = stringResource(R.string.label_add_vocabulary),
|
||||||
@@ -586,37 +416,19 @@ fun AddManuallyCard(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Input Fields
|
// Input Fields - Using AppOutlinedTextField
|
||||||
TextField(
|
AppOutlinedTextField(
|
||||||
value = wordText,
|
value = wordText,
|
||||||
onValueChange = { wordText = it },
|
onValueChange = { wordText = it },
|
||||||
placeholder = { Text(stringResource(R.string.text_label_word), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
|
placeholder = { Text(stringResource(R.string.text_label_word)) }
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surface, // Very dark background
|
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent
|
|
||||||
),
|
|
||||||
singleLine = true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
TextField(
|
AppOutlinedTextField(
|
||||||
value = translationText,
|
value = translationText,
|
||||||
onValueChange = { translationText = it },
|
onValueChange = { translationText = it },
|
||||||
placeholder = { Text(stringResource(R.string.text_translation), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
|
placeholder = { Text(stringResource(R.string.text_translation)) }
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent
|
|
||||||
),
|
|
||||||
singleLine = true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -651,7 +463,7 @@ fun AddManuallyCard(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Add to List Button (Darker variant)
|
// Add to List Button
|
||||||
AppButton(
|
AppButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val newItem = VocabularyItem(
|
val newItem = VocabularyItem(
|
||||||
@@ -680,91 +492,80 @@ fun AddManuallyCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Explore Packs Prominent Card (Full width at top) ---
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomActionCardsRow(
|
fun ExplorePacksProminentCard(
|
||||||
modifier: Modifier = Modifier,
|
onClick: () -> Unit,
|
||||||
onImportCsvClick: () -> Unit
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
AppCard(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
onClick = onClick
|
||||||
) {
|
) {
|
||||||
//TODO Explore Packs Card
|
Row(
|
||||||
AppCard(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.fillMaxWidth()
|
||||||
.height(120.dp),
|
.padding(20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(
|
AppIconContainer(
|
||||||
modifier = Modifier
|
imageVector = AppIcons.Vocabulary,
|
||||||
.fillMaxSize()
|
size = 56.dp,
|
||||||
.alpha(0.6f),
|
iconSize = 28.dp
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
)
|
||||||
verticalArrangement = Arrangement.Center
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.Vocabulary,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
Text(
|
Text(
|
||||||
text = "Explore Packs",
|
text = stringResource(R.string.title_explore_packs),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
Text(
|
|
||||||
text = "Coming soon",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import CSV Card
|
|
||||||
AppCard(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(120.dp),
|
|
||||||
onClick = onImportCsvClick
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.DriveFolderUpload,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Text(
|
|
||||||
text = "Import Lists or CSV",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.desc_explore_packs),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Import CSV Card (Full width at bottom) ---
|
||||||
|
@Composable
|
||||||
|
fun ImportCsvCard(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
AppCard(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppIconContainer(
|
||||||
|
imageVector = Icons.Default.DriveFolderUpload,
|
||||||
|
size = 56.dp,
|
||||||
|
iconSize = 28.dp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_import_csv_or_lists),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.desc_import_csv),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import eu.gaudian.translator.model.VocabularyStage
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppScaffold
|
import eu.gaudian.translator.view.composable.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar
|
import eu.gaudian.translator.view.stats.widgets.DetailedStageProgressBar
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -85,7 +84,6 @@ fun VocabularyCardHost(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopAppBar(
|
AppTopAppBar(
|
||||||
modifier = Modifier.height(56.dp),
|
|
||||||
title = stringResource(R.string.item_details),
|
title = stringResource(R.string.item_details),
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
actions = {
|
actions = {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import eu.gaudian.translator.utils.Log
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppAlertDialog
|
import eu.gaudian.translator.view.composable.AppAlertDialog
|
||||||
import eu.gaudian.translator.view.composable.Screen
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
|
import eu.gaudian.translator.view.exercises.ExerciseControls
|
||||||
|
import eu.gaudian.translator.view.exercises.ExerciseProgressIndicator
|
||||||
import eu.gaudian.translator.viewmodel.ScreenState
|
import eu.gaudian.translator.viewmodel.ScreenState
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ fun AllCardsListScreen(
|
|||||||
val vocabularyItems: List<VocabularyItem> = itemsToShow.ifEmpty {
|
val vocabularyItems: List<VocabularyItem> = itemsToShow.ifEmpty {
|
||||||
vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList()).value
|
vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList()).value
|
||||||
}
|
}
|
||||||
|
val stageMapping by vocabularyViewModel.stageMapping.collectAsStateWithLifecycle(initialValue = emptyMap())
|
||||||
|
|
||||||
// Handle export state
|
// Handle export state
|
||||||
LaunchedEffect(exportState) {
|
LaunchedEffect(exportState) {
|
||||||
@@ -298,6 +299,7 @@ fun AllCardsListScreen(
|
|||||||
vocabularyItems = vocabularyItems,
|
vocabularyItems = vocabularyItems,
|
||||||
allLanguages = allLanguages,
|
allLanguages = allLanguages,
|
||||||
selection = selection,
|
selection = selection,
|
||||||
|
stageMapping = stageMapping,
|
||||||
listState = lazyListState,
|
listState = lazyListState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ fun VocabularySortingScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
hintContent = HintDefinition.SORTING.hint()
|
hint = HintDefinition.SORTING.hint()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ private fun VocabularyCardContent(
|
|||||||
onMoveToStageClick = onMoveToStageClick,
|
onMoveToStageClick = onMoveToStageClick,
|
||||||
onDeleteClick = onDeleteClick,
|
onDeleteClick = onDeleteClick,
|
||||||
|
|
||||||
showAnalyzeGrammarButton = item.features.isNullOrBlank(),
|
showAnalyzeGrammarButton = !item.hasFeatures(),
|
||||||
onAnalyzeGrammarClick = {
|
onAnalyzeGrammarClick = {
|
||||||
vocabularyViewModel.fetchAndApplyGrammaticalDetailsForList(listOf(item))
|
vocabularyViewModel.fetchAndApplyGrammaticalDetailsForList(listOf(item))
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,214 +0,0 @@
|
|||||||
@file:Suppress("HardCodedStringLiteral")
|
|
||||||
|
|
||||||
package eu.gaudian.translator.view.vocabulary.widgets
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import eu.gaudian.translator.R
|
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
|
||||||
import eu.gaudian.translator.ui.theme.semanticColors
|
|
||||||
import eu.gaudian.translator.viewmodel.WeeklyActivityStat
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A widget that displays weekly activity statistics in a visually appealing bar chart.
|
|
||||||
* It's designed to be consistent with the app's modern, floating UI style.
|
|
||||||
*
|
|
||||||
* @param weeklyStats A list of [WeeklyActivityStat] for the last 7 days.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun WeeklyActivityChartWidget(
|
|
||||||
weeklyStats: List<WeeklyActivityStat>
|
|
||||||
) {
|
|
||||||
val maxValue = remember(weeklyStats) {
|
|
||||||
(weeklyStats.flatMap { listOf(it.newlyAdded, it.completed, it.answeredRight) }.maxOrNull() ?: 0).let {
|
|
||||||
if (it < 10) 10 else ((it / 5) + 1) * 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNoData = remember(weeklyStats) {
|
|
||||||
weeklyStats.all { it.newlyAdded == 0 && it.completed == 0 && it.answeredRight == 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNoData) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.text_no_data_available),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
ChartLegend()
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(220.dp),
|
|
||||||
verticalAlignment = Alignment.Bottom
|
|
||||||
) {
|
|
||||||
// Y-Axis Labels
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.padding(end = 8.dp),
|
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
|
||||||
horizontalAlignment = Alignment.End
|
|
||||||
) {
|
|
||||||
Text(maxValue.toString(), style = MaterialTheme.typography.labelSmall)
|
|
||||||
Text((maxValue / 2).toString(), style = MaterialTheme.typography.labelSmall)
|
|
||||||
Text("0", style = MaterialTheme.typography.labelSmall)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chart Bars
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.fillMaxHeight(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceAround,
|
|
||||||
verticalAlignment = Alignment.Bottom
|
|
||||||
) {
|
|
||||||
weeklyStats.forEach { stat ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Bottom
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.Bottom,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.fillMaxWidth(0.8f)
|
|
||||||
) {
|
|
||||||
Bar(value = stat.newlyAdded, maxValue = maxValue, color = MaterialTheme.semanticColors.stageGradient1)
|
|
||||||
Bar(value = stat.completed, maxValue = maxValue, color = MaterialTheme.semanticColors.stageGradient3)
|
|
||||||
Bar(value = stat.answeredRight, maxValue = maxValue, color = MaterialTheme.semanticColors.stageGradient5)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stat.day,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RowScope.Bar(value: Int, maxValue: Int, color: Color) {
|
|
||||||
var startAnimation by remember { mutableStateOf(false) }
|
|
||||||
val barHeight by animateFloatAsState(
|
|
||||||
targetValue = if (startAnimation) value.toFloat() / maxValue.toFloat() else 0f,
|
|
||||||
animationSpec = tween(durationMillis = 1000),
|
|
||||||
label = "barHeightAnimation"
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
delay(200) // Small delay to ensure the UI is ready before animating
|
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
|
||||||
startAnimation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.fillMaxHeight(barHeight)
|
|
||||||
.clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
|
|
||||||
.background(color)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ChartLegend() {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
LegendItem(color = MaterialTheme.semanticColors.stageGradient1, label = stringResource(R.string.label_added))
|
|
||||||
LegendItem(color = MaterialTheme.semanticColors.stageGradient3, label = stringResource(R.string.label_completed))
|
|
||||||
LegendItem(color = MaterialTheme.semanticColors.stageGradient5, label = stringResource(R.string.label_correct))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LegendItem(color: Color, label: String) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(10.dp)
|
|
||||||
.background(color, shape = CircleShape)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Text(text = label, style = MaterialTheme.typography.labelMedium, fontSize = 12.sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Composable
|
|
||||||
fun WeeklyActivityChartWidgetPreview() {
|
|
||||||
val sampleStats = listOf(
|
|
||||||
WeeklyActivityStat("Mon", 10, 5, 20),
|
|
||||||
WeeklyActivityStat("Tue", 12, 3, 15),
|
|
||||||
WeeklyActivityStat("Wed", 8, 8, 25),
|
|
||||||
WeeklyActivityStat("Thu", 15, 2, 18),
|
|
||||||
WeeklyActivityStat("Fri", 5, 10, 30),
|
|
||||||
WeeklyActivityStat("Sat", 7, 6, 22),
|
|
||||||
WeeklyActivityStat("Sun", 9, 4, 17)
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.background)
|
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
WeeklyActivityChartWidget(weeklyStats = sampleStats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -203,7 +203,7 @@ class ProgressViewModel @Inject constructor(
|
|||||||
|
|
||||||
// Calculate localized day name
|
// Calculate localized day name
|
||||||
val calendarDay = ((date.dayOfWeek.ordinal + 1) % 7) + 1
|
val calendarDay = ((date.dayOfWeek.ordinal + 1) % 7) + 1
|
||||||
val localizedDay = DateFormatSymbols.getInstance(Locale.getDefault()).shortWeekdays[calendarDay].uppercase()
|
val localizedDay = DateFormatSymbols.getInstance(Locale.getDefault()).weekdays[calendarDay]
|
||||||
|
|
||||||
WeeklyActivityStat(
|
WeeklyActivityStat(
|
||||||
// 3. Get the actual day name from the date and take the first 3 letters.
|
// 3. Get the actual day name from the date and take the first 3 letters.
|
||||||
|
|||||||
@@ -177,6 +177,17 @@ class TranslationViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Direct LibreTranslate without AI - for translating pack names/descriptions
|
||||||
|
suspend fun translateWithLibreTranslate(text: String, targetLanguageCode: String, sourceLanguageCode: String?): Result<String> {
|
||||||
|
// If source and target are the same, return the original text without calling the API
|
||||||
|
val sourceCode = sourceLanguageCode?.lowercase() ?: "en"
|
||||||
|
val targetCode = targetLanguageCode.lowercase()
|
||||||
|
if (sourceCode == targetCode) {
|
||||||
|
return Result.success(text)
|
||||||
|
}
|
||||||
|
return translationService.libreTranslate(text, sourceLanguageCode, targetLanguageCode, 1)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getMultipleTranslations(sentence: String, contextPhrase: String? = null): Result<List<String>> {
|
suspend fun getMultipleTranslations(sentence: String, contextPhrase: String? = null): Result<List<String>> {
|
||||||
return translationService.getMultipleSynonyms(sentence, contextPhrase)
|
return translationService.getMultipleSynonyms(sentence, contextPhrase)
|
||||||
.also { result ->
|
.also { result ->
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
@file:Suppress("HardCodedStringLiteral")
|
||||||
|
|
||||||
|
package eu.gaudian.translator.viewmodel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
|
import eu.gaudian.translator.model.communication.files_download.DownloadConfig
|
||||||
|
import eu.gaudian.translator.model.communication.files_download.FileDownloadManager
|
||||||
|
import eu.gaudian.translator.model.communication.files_download.VocabCollectionInfo
|
||||||
|
import eu.gaudian.translator.model.jsonParser
|
||||||
|
import eu.gaudian.translator.utils.Log
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Per-pack download/import state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
enum class PackDownloadState { IDLE, DOWNLOADING, DOWNLOADED, IMPORTED, ERROR }
|
||||||
|
|
||||||
|
data class PackUiState(
|
||||||
|
val info: VocabCollectionInfo,
|
||||||
|
val downloadState: PackDownloadState = PackDownloadState.IDLE,
|
||||||
|
val progress: Float = 0f,
|
||||||
|
/** Items available once the file has been downloaded and parsed (for preview). */
|
||||||
|
val previewItems: List<VocabularyItem> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Internal wrapper for deserializing the items array from a pack file. */
|
||||||
|
@Serializable
|
||||||
|
private data class PackPreviewWrapper(val items: List<VocabularyItem> = emptyList())
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ViewModel
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private const val TAG = "VocabPacksViewModel"
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class VocabPacksViewModel @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val downloadManager = FileDownloadManager(context)
|
||||||
|
|
||||||
|
private val _packs = MutableStateFlow<List<PackUiState>>(emptyList())
|
||||||
|
val packs: StateFlow<List<PackUiState>> = _packs.asStateFlow()
|
||||||
|
|
||||||
|
private val _isLoadingManifest = MutableStateFlow(false)
|
||||||
|
val isLoadingManifest: StateFlow<Boolean> = _isLoadingManifest.asStateFlow()
|
||||||
|
|
||||||
|
private val _manifestError = MutableStateFlow<String?>(null)
|
||||||
|
val manifestError: StateFlow<String?> = _manifestError.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits a pack ID every time a pack has been fully downloaded AND its items parsed.
|
||||||
|
* The screen listens to this to auto-open the conflict dialog after "Get" is tapped.
|
||||||
|
*/
|
||||||
|
private val _downloadCompleteEvents = MutableSharedFlow<String>(extraBufferCapacity = 8)
|
||||||
|
val downloadCompleteEvents: SharedFlow<String> = _downloadCompleteEvents.asSharedFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadManifest()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Persistent import records ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun importedVersion(packId: String): Int? {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, android.content.Context.MODE_PRIVATE)
|
||||||
|
return if (prefs.contains(prefKey(packId))) prefs.getInt(prefKey(packId), -1) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveImportedVersion(packId: String, version: Int) {
|
||||||
|
context.getSharedPreferences(PREFS_NAME, android.content.Context.MODE_PRIVATE)
|
||||||
|
.edit().putInt(prefKey(packId), version).apply()
|
||||||
|
Log.d(TAG, "Saved imported version $version for $packId")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Manifest ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fun loadManifest() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isLoadingManifest.value = true
|
||||||
|
_manifestError.value = null
|
||||||
|
try {
|
||||||
|
val manifest = downloadManager.fetchVocabManifest()
|
||||||
|
Log.d(TAG, "Fetched ${manifest?.size ?: 0} packs from manifest")
|
||||||
|
_packs.value = manifest?.map { info ->
|
||||||
|
val savedVersion = importedVersion(info.id)
|
||||||
|
val isCurrentVersionImported = savedVersion != null && savedVersion >= info.version
|
||||||
|
val downloaded = downloadManager.isVocabCollectionDownloaded(info)
|
||||||
|
val items = if (downloaded) parsePreviewItems(info) else emptyList()
|
||||||
|
PackUiState(
|
||||||
|
info = info,
|
||||||
|
downloadState = when {
|
||||||
|
isCurrentVersionImported -> PackDownloadState.IMPORTED
|
||||||
|
downloaded -> PackDownloadState.DOWNLOADED
|
||||||
|
else -> PackDownloadState.IDLE
|
||||||
|
},
|
||||||
|
previewItems = items,
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to fetch manifest", e)
|
||||||
|
_manifestError.value = e.message
|
||||||
|
} finally {
|
||||||
|
_isLoadingManifest.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Download ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fun downloadPack(info: VocabCollectionInfo) {
|
||||||
|
// Avoid double-downloading
|
||||||
|
val current = _packs.value.find { it.info.id == info.id }
|
||||||
|
if (current?.downloadState == PackDownloadState.DOWNLOADING ||
|
||||||
|
current?.downloadState == PackDownloadState.DOWNLOADED) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
Log.d(TAG, "Starting download of ${info.id}")
|
||||||
|
updatePack(info.id) { it.copy(downloadState = PackDownloadState.DOWNLOADING, progress = 0f) }
|
||||||
|
try {
|
||||||
|
val success = downloadManager.downloadVocabCollection(info) { progress ->
|
||||||
|
updatePack(info.id) { it.copy(progress = progress) }
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
val items = parsePreviewItems(info)
|
||||||
|
Log.d(TAG, "Download complete for ${info.id}: ${items.size} items parsed")
|
||||||
|
updatePack(info.id) {
|
||||||
|
it.copy(downloadState = PackDownloadState.DOWNLOADED, progress = 1f, previewItems = items)
|
||||||
|
}
|
||||||
|
_downloadCompleteEvents.emit(info.id)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Download returned false for ${info.id}")
|
||||||
|
updatePack(info.id) { it.copy(downloadState = PackDownloadState.ERROR) }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Download exception for ${info.id}", e)
|
||||||
|
updatePack(info.id) { it.copy(downloadState = PackDownloadState.ERROR) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Read raw JSON for import ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Returns the raw JSON of the downloaded pack file, or null if not present. */
|
||||||
|
fun readPackRawJson(info: VocabCollectionInfo): String? {
|
||||||
|
val file = localFile(info)
|
||||||
|
if (!file.exists()) {
|
||||||
|
Log.e(TAG, "Pack file not found: ${file.absolutePath}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val json = file.readText()
|
||||||
|
Log.d(TAG, "Read pack JSON for ${info.id}: ${json.length} chars")
|
||||||
|
json
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error reading pack file for ${info.id}", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Post-import cleanup ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fun markImportedAndCleanup(info: VocabCollectionInfo) {
|
||||||
|
val file = localFile(info)
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
Log.d(TAG, "Deleted local pack file: ${file.absolutePath}")
|
||||||
|
}
|
||||||
|
saveImportedVersion(info.id, info.version)
|
||||||
|
updatePack(info.id) { it.copy(downloadState = PackDownloadState.IMPORTED, progress = 1f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Preview download for already-imported packs ───────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the pack file solely to populate [PackUiState.previewItems] for IMPORTED packs.
|
||||||
|
* The file is deleted immediately after parsing; [downloadState] stays IMPORTED throughout.
|
||||||
|
*/
|
||||||
|
fun downloadForPreview(info: VocabCollectionInfo) {
|
||||||
|
val current = _packs.value.find { it.info.id == info.id }
|
||||||
|
if (current?.downloadState != PackDownloadState.IMPORTED) return
|
||||||
|
if (current.previewItems.isNotEmpty()) return // already cached in memory
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
Log.d(TAG, "Downloading for preview (IMPORTED): ${info.id}")
|
||||||
|
try {
|
||||||
|
val success = downloadManager.downloadVocabCollection(info) { /* no progress UI */ }
|
||||||
|
if (success) {
|
||||||
|
val items = parsePreviewItems(info)
|
||||||
|
Log.d(TAG, "Preview items loaded for ${info.id}: ${items.size}")
|
||||||
|
updatePack(info.id) { it.copy(previewItems = items) }
|
||||||
|
// Delete the temp file immediately – we only needed it for parsing
|
||||||
|
localFile(info).takeIf { it.exists() }?.delete()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Preview download failed for ${info.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cleanup on screen exit ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all downloaded-but-not-yet-imported files and resets those packs to IDLE.
|
||||||
|
* Called from DisposableEffect.onDispose in ExplorePacksScreen.
|
||||||
|
*/
|
||||||
|
fun cleanupDownloadedFiles() {
|
||||||
|
val toClean = _packs.value.filter { it.downloadState == PackDownloadState.DOWNLOADED }
|
||||||
|
toClean.forEach { ps ->
|
||||||
|
val file = localFile(ps.info)
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
Log.d(TAG, "Cleaned up on exit: ${ps.info.id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toClean.isNotEmpty()) {
|
||||||
|
_packs.value = _packs.value.map { ps ->
|
||||||
|
if (ps.downloadState == PackDownloadState.DOWNLOADED)
|
||||||
|
ps.copy(downloadState = PackDownloadState.IDLE, progress = 0f, previewItems = emptyList())
|
||||||
|
else ps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun parsePreviewItems(info: VocabCollectionInfo): List<VocabularyItem> {
|
||||||
|
val json = readPackRawJson(info) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val wrapper = jsonParser.decodeFromString<PackPreviewWrapper>(json)
|
||||||
|
wrapper.items.map { it.copy(id = 0) } // reset DB ids – these are preview-only
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error parsing preview items for ${info.id}", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun localFile(info: VocabCollectionInfo): File =
|
||||||
|
File(context.filesDir, "${DownloadConfig.LOCAL_FLASHCARDS_PATH}/${info.filename}")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_NAME = "vocab_packs_imported"
|
||||||
|
private fun prefKey(packId: String) = "v_$packId"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePack(packId: String, transform: (PackUiState) -> PackUiState) {
|
||||||
|
_packs.value = _packs.value.map { if (it.info.id == packId) transform(it) else it }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import androidx.lifecycle.AndroidViewModel
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.CardSet
|
import eu.gaudian.translator.model.CardSet
|
||||||
import eu.gaudian.translator.model.Language
|
import eu.gaudian.translator.model.Language
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
import eu.gaudian.translator.model.VocabularyCategory
|
||||||
@@ -702,9 +703,11 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
filteredList = when (sortOrder) {
|
filteredList = when (sortOrder) {
|
||||||
SortOrder.NEWEST_FIRST -> filteredList.sortedByDescending { it.id }
|
SortOrder.NEWEST_FIRST -> filteredList.sortedByDescending { it.createdAt }
|
||||||
SortOrder.OLDEST_FIRST -> filteredList.sortedBy { it.id }
|
SortOrder.OLDEST_FIRST -> filteredList.sortedBy { it.createdAt }
|
||||||
SortOrder.ALPHABETICAL -> filteredList.sortedBy { it.wordFirst }
|
SortOrder.ALPHABETICAL -> filteredList.sortedWith(
|
||||||
|
compareBy(String.CASE_INSENSITIVE_ORDER) { it.wordFirst.trim() }
|
||||||
|
)
|
||||||
SortOrder.LANGUAGE -> filteredList.sortedWith(compareBy({ it.languageFirstId }, { it.languageSecondId }))
|
SortOrder.LANGUAGE -> filteredList.sortedWith(compareBy({ it.languageFirstId }, { it.languageSecondId }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1324,7 +1327,7 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
|
|
||||||
val itemsWithoutGrammarCount: StateFlow<Int> = vocabularyItems
|
val itemsWithoutGrammarCount: StateFlow<Int> = vocabularyItems
|
||||||
.map { items ->
|
.map { items ->
|
||||||
items.count { it.features.isNullOrEmpty() }
|
items.count { it.hasFeatures() }
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
@@ -1407,6 +1410,7 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
enum class DeleteType { VOCABULARY_ITEM_BY_ID, VOCABULARY_ITEM, VOCABULARY_ITEMS, REMOVE_FROM_CATEGORY }
|
enum class DeleteType { VOCABULARY_ITEM_BY_ID, VOCABULARY_ITEM, VOCABULARY_ITEMS, REMOVE_FROM_CATEGORY }
|
||||||
enum class SortOrder { NEWEST_FIRST, OLDEST_FIRST, ALPHABETICAL, LANGUAGE }
|
enum class SortOrder { NEWEST_FIRST, OLDEST_FIRST, ALPHABETICAL, LANGUAGE }
|
||||||
|
|
||||||
|
|
||||||
data class VocabularyItemDetails @OptIn(ExperimentalTime::class) constructor(
|
data class VocabularyItemDetails @OptIn(ExperimentalTime::class) constructor(
|
||||||
val stage: VocabularyStage,
|
val stage: VocabularyStage,
|
||||||
val lastCorrectAnswer: kotlin.time.Instant?,
|
val lastCorrectAnswer: kotlin.time.Instant?,
|
||||||
@@ -1449,3 +1453,14 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string resource ID for a given SortOrder.
|
||||||
|
* This allows internationalization of sort order labels.
|
||||||
|
*/
|
||||||
|
fun VocabularyViewModel.SortOrder.toStringResource(): Int = when (this) {
|
||||||
|
VocabularyViewModel.SortOrder.NEWEST_FIRST -> R.string.sort_order_newest_first
|
||||||
|
VocabularyViewModel.SortOrder.OLDEST_FIRST -> R.string.sort_order_oldest_first
|
||||||
|
VocabularyViewModel.SortOrder.ALPHABETICAL -> R.string.sort_order_alphabetical
|
||||||
|
VocabularyViewModel.SortOrder.LANGUAGE -> R.string.sort_order_language
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@
|
|||||||
<string name="error_select_two_columns">Bitte zwei Spalten auswählen.</string>
|
<string name="error_select_two_columns">Bitte zwei Spalten auswählen.</string>
|
||||||
<string name="error_select_languages">Bitte zwei Sprachen auswählen.</string>
|
<string name="error_select_languages">Bitte zwei Sprachen auswählen.</string>
|
||||||
<string name="error_no_rows_to_import">Keine Zeilen zum Importieren. Bitte Spalten und Kopfzeile prüfen.</string>
|
<string name="error_no_rows_to_import">Keine Zeilen zum Importieren. Bitte Spalten und Kopfzeile prüfen.</string>
|
||||||
<string name="info_imported_items_from">%1$d Vokabeln importiert.</string>
|
|
||||||
<string name="label_import">Importieren</string>
|
<string name="label_import">Importieren</string>
|
||||||
<string name="menu_create_youtube_exercise">YouTube-Übung erstellen</string>
|
<string name="menu_create_youtube_exercise">YouTube-Übung erstellen</string>
|
||||||
<string name="text_youtube_link">YouTube-Link</string>
|
<string name="text_youtube_link">YouTube-Link</string>
|
||||||
@@ -282,7 +281,7 @@
|
|||||||
<string name="label_start_exercise_2d">Übung starten (%1$d)</string>
|
<string name="label_start_exercise_2d">Übung starten (%1$d)</string>
|
||||||
<string name="number_of_cards">Anzahl der Karten: %1$d / %2$d</string>
|
<string name="number_of_cards">Anzahl der Karten: %1$d / %2$d</string>
|
||||||
<string name="no_cards_found_for_the_selected_filters">Keine Karten für die gewählten Filter gefunden.</string>
|
<string name="no_cards_found_for_the_selected_filters">Keine Karten für die gewählten Filter gefunden.</string>
|
||||||
<string name="label_choose_exercise_types">Die richtige Antwort wählen</string>
|
<string name="label_multiple_choice_desc">Die richtige Antwort wählen</string>
|
||||||
<string name="options">Optionen</string>
|
<string name="options">Optionen</string>
|
||||||
<string name="shuffle_cards">Karten mischen</string>
|
<string name="shuffle_cards">Karten mischen</string>
|
||||||
<string name="quit">Beenden</string>
|
<string name="quit">Beenden</string>
|
||||||
@@ -419,7 +418,6 @@
|
|||||||
<string name="label_all_types">Alle Typen</string>
|
<string name="label_all_types">Alle Typen</string>
|
||||||
<string name="filter_and_sort">Filtern und Sortieren</string>
|
<string name="filter_and_sort">Filtern und Sortieren</string>
|
||||||
<string name="language_with_id_d_not_found">Sprache mit ID %1$d nicht gefunden</string>
|
<string name="language_with_id_d_not_found">Sprache mit ID %1$d nicht gefunden</string>
|
||||||
<string name="items_without_grammar_infos">Einträge ohne Grammatikinfos</string>
|
|
||||||
<string name="resolve_missing_language_id">Fehlende Sprach-ID auflösen: %1$d</string>
|
<string name="resolve_missing_language_id">Fehlende Sprach-ID auflösen: %1$d</string>
|
||||||
<string name="found_d_items_using_this_missing_language_id">%1$d Einträge mit dieser fehlenden Sprach-ID gefunden.</string>
|
<string name="found_d_items_using_this_missing_language_id">%1$d Einträge mit dieser fehlenden Sprach-ID gefunden.</string>
|
||||||
<string name="hide_affected_items">Betroffene Einträge ausblenden</string>
|
<string name="hide_affected_items">Betroffene Einträge ausblenden</string>
|
||||||
@@ -548,7 +546,6 @@
|
|||||||
<string name="sorting_hint_title">Vokabeln sortieren</string>
|
<string name="sorting_hint_title">Vokabeln sortieren</string>
|
||||||
<string name="text_optional">" (optional)"</string>
|
<string name="text_optional">" (optional)"</string>
|
||||||
<string name="text_check_availability">Verfügbarkeit prüfen</string>
|
<string name="text_check_availability">Verfügbarkeit prüfen</string>
|
||||||
<string name="text_no_valid_api_configuration_could_be_found">Keine gültige API-Konfiguration gefunden. Bitte konfiguriere zuerst einen API-Anbieter in den Einstellungen.</string>
|
|
||||||
<string name="text_try_wiktionary_first">Zuerst Wiktionary versuchen</string>
|
<string name="text_try_wiktionary_first">Zuerst Wiktionary versuchen</string>
|
||||||
<string name="text_try_first_finding_the_word_on">Versuche zuerst, das Wort auf Wiktionary zu finden, bevor eine KI-Antwort generiert wird.</string>
|
<string name="text_try_first_finding_the_word_on">Versuche zuerst, das Wort auf Wiktionary zu finden, bevor eine KI-Antwort generiert wird.</string>
|
||||||
<string name="text_question_of">Frage %1$d von %2$d</string>
|
<string name="text_question_of">Frage %1$d von %2$d</string>
|
||||||
@@ -834,7 +831,7 @@
|
|||||||
<string name="label_quit_app">App beenden</string>
|
<string name="label_quit_app">App beenden</string>
|
||||||
<string name="label_target_correct_answers_per_day">Ziel für richtige Antworten pro Tag</string>
|
<string name="label_target_correct_answers_per_day">Ziel für richtige Antworten pro Tag</string>
|
||||||
<string name="label_interval_settings_in_days">Intervall-Einstellungen</string>
|
<string name="label_interval_settings_in_days">Intervall-Einstellungen</string>
|
||||||
<string name="label_vocabulary_settings">Fortschritts-Einstellungen</string>
|
<string name="label_vocabulary_settings">Fortschritt</string>
|
||||||
<string name="label_no_category">Keine</string>
|
<string name="label_no_category">Keine</string>
|
||||||
<string name="text_search">Suche</string>
|
<string name="text_search">Suche</string>
|
||||||
<string name="text_language_settings_description">Stelle ein, welche Sprachen du in der App verwenden möchtest. Sprachen, die nicht aktiviert sind, werden in dieser App nicht angezeigt. Du kannst auch deine eigene Sprache zur Liste hinzufügen oder eine vorhandene Sprache (Region/Locale) ändern.</string>
|
<string name="text_language_settings_description">Stelle ein, welche Sprachen du in der App verwenden möchtest. Sprachen, die nicht aktiviert sind, werden in dieser App nicht angezeigt. Du kannst auch deine eigene Sprache zur Liste hinzufügen oder eine vorhandene Sprache (Region/Locale) ändern.</string>
|
||||||
@@ -898,12 +895,11 @@
|
|||||||
<string name="cd_go">Los</string>
|
<string name="cd_go">Los</string>
|
||||||
<string name="label_sort_by">Sortieren nach</string>
|
<string name="label_sort_by">Sortieren nach</string>
|
||||||
<string name="label_reset">Zurücksetzen</string>
|
<string name="label_reset">Zurücksetzen</string>
|
||||||
<string name="label_filter_cards">Filter Cards</string>
|
<string name="label_filter_cards">Karten Filtern</string>
|
||||||
<string name="text_desc_organize_vocabulary_groups">Organisiere deinen Wortschatz in Gruppen</string>
|
<string name="text_desc_organize_vocabulary_groups">Organisiere deinen Wortschatz in Gruppen</string>
|
||||||
<string name="text_add_new_word_to_list">Extrahiere ein neues Wort in deine Liste</string>
|
<string name="text_add_new_word_to_list">Extrahiere ein neues Wort in deine Liste</string>
|
||||||
<string name="cd_scroll_to_top">Nach oben scrollen</string>
|
<string name="cd_scroll_to_top">Nach oben scrollen</string>
|
||||||
<string name="cd_settings">Einstellungen</string>
|
<string name="cd_settings">Einstellungen</string>
|
||||||
<string name="label_import_csv">CSV importieren</string>
|
|
||||||
<string name="label_ai_generator">KI-Generator</string>
|
<string name="label_ai_generator">KI-Generator</string>
|
||||||
<string name="label_new_wordss">Neue Wörter</string>
|
<string name="label_new_wordss">Neue Wörter</string>
|
||||||
<string name="label_recently_added">Kürzlich hinzugefügt</string>
|
<string name="label_recently_added">Kürzlich hinzugefügt</string>
|
||||||
@@ -916,5 +912,11 @@
|
|||||||
<string name="cd_add">Hinzufügen</string>
|
<string name="cd_add">Hinzufügen</string>
|
||||||
<string name="cd_searchh">Suche</string>
|
<string name="cd_searchh">Suche</string>
|
||||||
<string name="label_learnedd">gelernt</string>
|
<string name="label_learnedd">gelernt</string>
|
||||||
|
|
||||||
|
<!-- Sort Order Options -->
|
||||||
|
<string name="sort_order_newest_first">Neueste zuerst</string>
|
||||||
|
<string name="sort_order_oldest_first">Älteste zuerst</string>
|
||||||
|
<string name="sort_order_alphabetical">Alphabetisch</string>
|
||||||
|
<string name="sort_order_language">Sprache</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
<string name="label_translate">Traduzir</string>
|
<string name="label_translate">Traduzir</string>
|
||||||
<string name="label_add">Adicionar</string>
|
<string name="label_add">Adicionar</string>
|
||||||
<string name="label_close">Fechar</string>
|
<string name="label_close">Fechar</string>
|
||||||
<string name="label_cancel">Cancelar</string>
|
|
||||||
<string name="label_confirm">Confirmar</string>
|
<string name="label_confirm">Confirmar</string>
|
||||||
<string name="label_select">Selecionar</string>
|
<string name="label_select">Selecionar</string>
|
||||||
<string name="label_done">Concluído</string>
|
<string name="label_done">Concluído</string>
|
||||||
@@ -58,7 +57,6 @@
|
|||||||
<string name="error_select_two_columns">Por favor, selecione duas colunas.</string>
|
<string name="error_select_two_columns">Por favor, selecione duas colunas.</string>
|
||||||
<string name="error_select_languages">Por favor, selecione dois idiomas.</string>
|
<string name="error_select_languages">Por favor, selecione dois idiomas.</string>
|
||||||
<string name="error_no_rows_to_import">Nenhuma linha para importar. Verifique as colunas e o cabeçalho.</string>
|
<string name="error_no_rows_to_import">Nenhuma linha para importar. Verifique as colunas e o cabeçalho.</string>
|
||||||
<string name="info_imported_items_from">%1$d itens de vocabulário importados.</string>
|
|
||||||
<string name="label_import">Importar</string>
|
<string name="label_import">Importar</string>
|
||||||
<string name="menu_create_youtube_exercise">Criar Exercício do YouTube</string>
|
<string name="menu_create_youtube_exercise">Criar Exercício do YouTube</string>
|
||||||
<string name="text_youtube_link">Link do YouTube</string>
|
<string name="text_youtube_link">Link do YouTube</string>
|
||||||
@@ -90,7 +88,6 @@
|
|||||||
<string name="text_widget_title_weekly_activity">Atividade Semanal</string>
|
<string name="text_widget_title_weekly_activity">Atividade Semanal</string>
|
||||||
<string name="text_loading_3d">Carregando…</string>
|
<string name="text_loading_3d">Carregando…</string>
|
||||||
<string name="text_show_loading">Mostrar Carregamento</string>
|
<string name="text_show_loading">Mostrar Carregamento</string>
|
||||||
<string name="text_cancel_loading">Cancelar Carregamento</string>
|
|
||||||
<string name="text_show_info_message">Mostrar Mensagem Informativa</string>
|
<string name="text_show_info_message">Mostrar Mensagem Informativa</string>
|
||||||
<string name="text_show_error_message">Mostrar Mensagem de Erro</string>
|
<string name="text_show_error_message">Mostrar Mensagem de Erro</string>
|
||||||
<string name="text_reset_intro">Resetar Introdução</string>
|
<string name="text_reset_intro">Resetar Introdução</string>
|
||||||
@@ -281,7 +278,6 @@
|
|||||||
<string name="label_start_exercise_2d">Iniciar Exercício (%1$d)</string>
|
<string name="label_start_exercise_2d">Iniciar Exercício (%1$d)</string>
|
||||||
<string name="number_of_cards">Número de Cartões: %1$d / %2$d</string>
|
<string name="number_of_cards">Número de Cartões: %1$d / %2$d</string>
|
||||||
<string name="no_cards_found_for_the_selected_filters">Nenhum cartão encontrado para os filtros selecionados.</string>
|
<string name="no_cards_found_for_the_selected_filters">Nenhum cartão encontrado para os filtros selecionados.</string>
|
||||||
<string name="label_choose_exercise_types">Escolher Tipos de Exercício</string>
|
|
||||||
<string name="options">Opções</string>
|
<string name="options">Opções</string>
|
||||||
<string name="shuffle_cards">Embaralhar Cartões</string>
|
<string name="shuffle_cards">Embaralhar Cartões</string>
|
||||||
<string name="quit">Sair</string>
|
<string name="quit">Sair</string>
|
||||||
@@ -326,7 +322,6 @@
|
|||||||
<string name="statistics_are_loading">Carregando estatísticas…</string>
|
<string name="statistics_are_loading">Carregando estatísticas…</string>
|
||||||
<string name="to_d">para %1$s</string>
|
<string name="to_d">para %1$s</string>
|
||||||
<string name="label_translate_from_2d">Traduzir de %1$s</string>
|
<string name="label_translate_from_2d">Traduzir de %1$s</string>
|
||||||
<string name="text_assemble_the_word_here">Monte a palavra aqui</string>
|
|
||||||
<string name="correct_answer">Resposta correta: %1$s</string>
|
<string name="correct_answer">Resposta correta: %1$s</string>
|
||||||
<string name="label_quit_exercise_qm">Sair do Exercício?</string>
|
<string name="label_quit_exercise_qm">Sair do Exercício?</string>
|
||||||
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Tem certeza de que quer sair? O seu progresso nesta sessão será perdido.</string>
|
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Tem certeza de que quer sair? O seu progresso nesta sessão será perdido.</string>
|
||||||
@@ -417,7 +412,6 @@
|
|||||||
<string name="label_all_types">Todos os Tipos</string>
|
<string name="label_all_types">Todos os Tipos</string>
|
||||||
<string name="filter_and_sort">Filtrar e Ordenar</string>
|
<string name="filter_and_sort">Filtrar e Ordenar</string>
|
||||||
<string name="language_with_id_d_not_found">Idioma com id %1$d não encontrado</string>
|
<string name="language_with_id_d_not_found">Idioma com id %1$d não encontrado</string>
|
||||||
<string name="items_without_grammar_infos">Itens sem infos de gramática</string>
|
|
||||||
<string name="resolve_missing_language_id">Resolver ID de Idioma Ausente: %1$d</string>
|
<string name="resolve_missing_language_id">Resolver ID de Idioma Ausente: %1$d</string>
|
||||||
<string name="found_d_items_using_this_missing_language_id">Encontrados %1$d itens usando este ID de idioma ausente.</string>
|
<string name="found_d_items_using_this_missing_language_id">Encontrados %1$d itens usando este ID de idioma ausente.</string>
|
||||||
<string name="hide_affected_items">Ocultar Itens Afetados</string>
|
<string name="hide_affected_items">Ocultar Itens Afetados</string>
|
||||||
@@ -546,7 +540,6 @@
|
|||||||
<string name="sorting_hint_title">Organização de Vocabulário</string>
|
<string name="sorting_hint_title">Organização de Vocabulário</string>
|
||||||
<string name="text_optional">" (opcional)"</string>
|
<string name="text_optional">" (opcional)"</string>
|
||||||
<string name="text_check_availability">Verificar disponibilidade</string>
|
<string name="text_check_availability">Verificar disponibilidade</string>
|
||||||
<string name="text_no_valid_api_configuration_could_be_found">Nenhuma configuração de API válida foi encontrada. Antes de usar o app, configure pelo menos um provedor de API.</string>
|
|
||||||
<string name="text_try_wiktionary_first">Tentar Wikcionário Primeiro</string>
|
<string name="text_try_wiktionary_first">Tentar Wikcionário Primeiro</string>
|
||||||
<string name="text_try_first_finding_the_word_on">Tente primeiro encontrar a palavra no Wikcionário antes de gerar uma resposta de IA</string>
|
<string name="text_try_first_finding_the_word_on">Tente primeiro encontrar a palavra no Wikcionário antes de gerar uma resposta de IA</string>
|
||||||
<string name="text_question_of">Pergunta %1$d de %2$d</string>
|
<string name="text_question_of">Pergunta %1$d de %2$d</string>
|
||||||
@@ -623,11 +616,9 @@
|
|||||||
<string name="text_do_you_want_to_minimize_the_app">Quer minimizar o app?</string>
|
<string name="text_do_you_want_to_minimize_the_app">Quer minimizar o app?</string>
|
||||||
<string name="text_error_deleting_dictionaries">Erro ao excluir dicionários: %1$s</string>
|
<string name="text_error_deleting_dictionaries">Erro ao excluir dicionários: %1$s</string>
|
||||||
<string name="text_error_deleting_dictionary">Erro ao excluir dicionário: %1$s</string>
|
<string name="text_error_deleting_dictionary">Erro ao excluir dicionário: %1$s</string>
|
||||||
<string name="text_error_deleting_orphaned_file">Erro ao deletar arquivo órfão: %1$s</string>
|
|
||||||
<string name="text_error_downloading_dictionary">Erro ao baixar dicionário: %1$s</string>
|
<string name="text_error_downloading_dictionary">Erro ao baixar dicionário: %1$s</string>
|
||||||
<string name="text_error_loading_stored_values">Erro ao carregar valores armazenados: %1$s</string>
|
<string name="text_error_loading_stored_values">Erro ao carregar valores armazenados: %1$s</string>
|
||||||
<string name="text_error_saving_entry">Erro ao salvar entrada: %1$s</string>
|
<string name="text_error_saving_entry">Erro ao salvar entrada: %1$s</string>
|
||||||
<string name="text_failed_to_delete_dictionary">Falha ao deletar dicionário: %1$s</string>
|
|
||||||
<string name="text_failed_to_delete_orphaned_file">Falhou ao excluir arquivo órfão: %1$s</string>
|
<string name="text_failed_to_delete_orphaned_file">Falhou ao excluir arquivo órfão: %1$s</string>
|
||||||
<string name="text_failed_to_delete_some_dictionaries">Falhou ao excluir alguns dicionários</string>
|
<string name="text_failed_to_delete_some_dictionaries">Falhou ao excluir alguns dicionários</string>
|
||||||
<string name="text_failed_to_download_dictionary">Falhou ao baixar dicionário: %1$s</string>
|
<string name="text_failed_to_download_dictionary">Falhou ao baixar dicionário: %1$s</string>
|
||||||
@@ -774,8 +765,6 @@
|
|||||||
<string name="text_use_downloaded_dictionary">Usar dicionário baixado</string>
|
<string name="text_use_downloaded_dictionary">Usar dicionário baixado</string>
|
||||||
<string name="text_watch_video_again">Assistir ao Vídeo Novamente</string>
|
<string name="text_watch_video_again">Assistir ao Vídeo Novamente</string>
|
||||||
<string name="text_dialog_delete_provider">Tem certeza que deseja excluir o provedor "%1$s"? Isso também removerá todos os modelos associados a este provedor. Esta ação não pode ser desfeita.</string>
|
<string name="text_dialog_delete_provider">Tem certeza que deseja excluir o provedor "%1$s"? Isso também removerá todos os modelos associados a este provedor. Esta ação não pode ser desfeita.</string>
|
||||||
<string name="label_delete_model">Deletar Modelo</string>
|
|
||||||
<string name="text_dialog_delete_model">Tem certeza que deseja deletar o modelo "%1$s" de %2$s? Essa ação não pode ser desfeita.</string>
|
|
||||||
<string name="label_task_model_assignments">Atribuições do Modelo de Tarefa</string>
|
<string name="label_task_model_assignments">Atribuições do Modelo de Tarefa</string>
|
||||||
<string name="text_configure_which_ai_model_to_use_for_each_task_type">Configurar qual modelo de IA usar para cada tipo de tarefa</string>
|
<string name="text_configure_which_ai_model_to_use_for_each_task_type">Configurar qual modelo de IA usar para cada tipo de tarefa</string>
|
||||||
<string name="label_custom">Personalizado</string>
|
<string name="label_custom">Personalizado</string>
|
||||||
@@ -788,7 +777,6 @@
|
|||||||
<string name="text_translation_will_appear_here">A tradução vai aparecer aqui</string>
|
<string name="text_translation_will_appear_here">A tradução vai aparecer aqui</string>
|
||||||
<string name="label_delete_key">Apagar Chave</string>
|
<string name="label_delete_key">Apagar Chave</string>
|
||||||
<string name="text_dialog_delete_key">Tem certeza que quer apagar a Chave desse Fornecedor?</string>
|
<string name="text_dialog_delete_key">Tem certeza que quer apagar a Chave desse Fornecedor?</string>
|
||||||
<string name="text_delete_all_providers_and_models">Deletar todos os provedores e modelos</string>
|
|
||||||
<string name="label_dictionary_content">Conteúdo do Dicionário</string>
|
<string name="label_dictionary_content">Conteúdo do Dicionário</string>
|
||||||
<string name="tab_ai_definition">Definição de IA</string>
|
<string name="tab_ai_definition">Definição de IA</string>
|
||||||
<string name="tab_downloaded">Baixado</string>
|
<string name="tab_downloaded">Baixado</string>
|
||||||
@@ -832,7 +820,7 @@
|
|||||||
<string name="label_home">Início</string>
|
<string name="label_home">Início</string>
|
||||||
<string name="label_quit_app">Sair do App</string>
|
<string name="label_quit_app">Sair do App</string>
|
||||||
<string name="label_interval_settings_in_days">Configurações de Intervalo</string>
|
<string name="label_interval_settings_in_days">Configurações de Intervalo</string>
|
||||||
<string name="label_vocabulary_settings">Configurações de Progresso</string>
|
<string name="label_vocabulary_settings">Progresso</string>
|
||||||
<string name="label_no_category">Nenhum</string>
|
<string name="label_no_category">Nenhum</string>
|
||||||
<string name="text_search">Buscar</string>
|
<string name="text_search">Buscar</string>
|
||||||
<string name="text_language_settings_description">1. Escolha quais idiomas você quer usar no app. Idiomas que não estiverem ativados não vão aparecer aqui. Você também pode adicionar o seu próprio idioma à lista ou mudar um idioma existente (região/localidade)</string>
|
<string name="text_language_settings_description">1. Escolha quais idiomas você quer usar no app. Idiomas que não estiverem ativados não vão aparecer aqui. Você também pode adicionar o seu próprio idioma à lista ou mudar um idioma existente (região/localidade)</string>
|
||||||
@@ -901,7 +889,6 @@
|
|||||||
<string name="text_add_new_word_to_list">Extrair uma nova palavra para a sua lista</string>
|
<string name="text_add_new_word_to_list">Extrair uma nova palavra para a sua lista</string>
|
||||||
<string name="cd_scroll_to_top">Rolar para o topo</string>
|
<string name="cd_scroll_to_top">Rolar para o topo</string>
|
||||||
<string name="cd_settings">Configurações</string>
|
<string name="cd_settings">Configurações</string>
|
||||||
<string name="label_import_csv">Importar CSV</string>
|
|
||||||
<string name="label_ai_generator">Gerador de IA</string>
|
<string name="label_ai_generator">Gerador de IA</string>
|
||||||
<string name="label_new_wordss">Novas Palavras</string>
|
<string name="label_new_wordss">Novas Palavras</string>
|
||||||
<string name="label_recently_added">Adicionados recentemente</string>
|
<string name="label_recently_added">Adicionados recentemente</string>
|
||||||
@@ -914,5 +901,11 @@
|
|||||||
<string name="cd_add">Adicionar</string>
|
<string name="cd_add">Adicionar</string>
|
||||||
<string name="cd_searchh">Buscar</string>
|
<string name="cd_searchh">Buscar</string>
|
||||||
<string name="label_learnedd">aprendido</string>
|
<string name="label_learnedd">aprendido</string>
|
||||||
|
|
||||||
|
<!-- Sort Order Options -->
|
||||||
|
<string name="sort_order_newest_first">Mais Recentes Primeiro</string>
|
||||||
|
<string name="sort_order_oldest_first">Mais Antigos Primeiro</string>
|
||||||
|
<string name="sort_order_alphabetical">Alfabético</string>
|
||||||
|
<string name="sort_order_language">Idioma</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<string name="category_hint_intro">You can create two types of categories to organize your vocabulary:</string>
|
||||||
|
|
||||||
<string name="cd_achieved">Achieved</string>
|
<string name="cd_achieved">Achieved</string>
|
||||||
|
<string name="cd_add">Add</string>
|
||||||
<string name="cd_app_logo">App Logo</string>
|
<string name="cd_app_logo">App Logo</string>
|
||||||
<string name="cd_back">Back</string>
|
<string name="cd_back">Back</string>
|
||||||
<string name="cd_clear_search">Clear Search</string>
|
<string name="cd_clear_search">Clear Search</string>
|
||||||
@@ -8,10 +11,20 @@
|
|||||||
<string name="cd_collapse">Collapse</string>
|
<string name="cd_collapse">Collapse</string>
|
||||||
<string name="cd_error">Error</string>
|
<string name="cd_error">Error</string>
|
||||||
<string name="cd_expand">Expand</string>
|
<string name="cd_expand">Expand</string>
|
||||||
|
<string name="cd_filter">Filter</string>
|
||||||
|
<string name="cd_filter_options">Filter options</string>
|
||||||
|
<string name="cd_go">Go</string>
|
||||||
<string name="cd_navigate_back">Navigate back</string>
|
<string name="cd_navigate_back">Navigate back</string>
|
||||||
|
<string name="cd_options">Options</string>
|
||||||
<string name="cd_paste">Paste</string>
|
<string name="cd_paste">Paste</string>
|
||||||
|
<string name="cd_play">Play</string>
|
||||||
<string name="cd_re_generate_definition">Re-generate Definition</string>
|
<string name="cd_re_generate_definition">Re-generate Definition</string>
|
||||||
|
<string name="cd_reload">Reload</string>
|
||||||
|
<string name="cd_scroll_to_top">Scroll to top</string>
|
||||||
<string name="cd_search">Search</string>
|
<string name="cd_search">Search</string>
|
||||||
|
<string name="cd_searchh">Search</string>
|
||||||
|
<string name="cd_selected">Selected</string>
|
||||||
|
<string name="cd_settings">Settings</string>
|
||||||
<string name="cd_success">Success</string>
|
<string name="cd_success">Success</string>
|
||||||
<string name="cd_switch_languages">Switch Languages</string>
|
<string name="cd_switch_languages">Switch Languages</string>
|
||||||
<string name="cd_target_met">Target Met</string>
|
<string name="cd_target_met">Target Met</string>
|
||||||
@@ -19,20 +32,9 @@
|
|||||||
<string name="cd_toggle_menu">Toggle Menu</string>
|
<string name="cd_toggle_menu">Toggle Menu</string>
|
||||||
<string name="cd_translation_history">Translation History</string>
|
<string name="cd_translation_history">Translation History</string>
|
||||||
|
|
||||||
<string name="label_choose_exercise_types">Choose Exercise Types</string>
|
|
||||||
|
|
||||||
<string name="label_clear_all">Clear All</string>
|
|
||||||
|
|
||||||
<string name="label_close_exercise">Close exercise</string>
|
|
||||||
<string name="label_close_selection_mode">Close selection mode</string>
|
|
||||||
|
|
||||||
<string name="label_colloquial">Colloquial</string>
|
|
||||||
|
|
||||||
<string name="contact_developer_description">Contact me for bug reports, ideas, feature requests, and more.</string>
|
<string name="contact_developer_description">Contact me for bug reports, ideas, feature requests, and more.</string>
|
||||||
<string name="contact_developer_title">Contact developer</string>
|
<string name="contact_developer_title">Contact developer</string>
|
||||||
|
|
||||||
<string name="label_context">Context</string>
|
|
||||||
|
|
||||||
<string name="copied_text">Copied Text</string>
|
<string name="copied_text">Copied Text</string>
|
||||||
|
|
||||||
<string name="copy_text">Copy text</string>
|
<string name="copy_text">Copy text</string>
|
||||||
@@ -42,7 +44,6 @@
|
|||||||
<string name="correct_answers_">Correct answers: %1$d</string>
|
<string name="correct_answers_">Correct answers: %1$d</string>
|
||||||
<string name="correct_tone">Tone</string>
|
<string name="correct_tone">Tone</string>
|
||||||
|
|
||||||
<string name="label_create">Create</string>
|
|
||||||
<string name="create_a_new_custom_language_entry_for_this_id">Create a new custom language entry for this ID.</string>
|
<string name="create_a_new_custom_language_entry_for_this_id">Create a new custom language entry for this ID.</string>
|
||||||
<string name="create_new_category">Create New Category</string>
|
<string name="create_new_category">Create New Category</string>
|
||||||
<string name="create_new_language">Create New Language</string>
|
<string name="create_new_language">Create New Language</string>
|
||||||
@@ -74,6 +75,11 @@
|
|||||||
<string name="delete_new">Delete New</string>
|
<string name="delete_new">Delete New</string>
|
||||||
<string name="delete_provider">Delete Provider</string>
|
<string name="delete_provider">Delete Provider</string>
|
||||||
|
|
||||||
|
<string name="desc_daily_review_due">%1$d words need attention</string>
|
||||||
|
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
|
||||||
|
<string name="desc_explore_packs">Discover lists to download</string>
|
||||||
|
<string name="desc_import_csv">Import words from CSV or lists</string>
|
||||||
|
|
||||||
<string name="description">Description</string>
|
<string name="description">Description</string>
|
||||||
|
|
||||||
<string name="deselect_all">Deselect All</string>
|
<string name="deselect_all">Deselect All</string>
|
||||||
@@ -85,6 +91,7 @@
|
|||||||
|
|
||||||
<string name="due_today_">Due Today: %1$s</string>
|
<string name="due_today_">Due Today: %1$s</string>
|
||||||
|
|
||||||
|
<string name="duplicate">Duplicate</string>
|
||||||
<string name="duplicate_detected">Duplicate Detected</string>
|
<string name="duplicate_detected">Duplicate Detected</string>
|
||||||
|
|
||||||
<string name="duplicates_only">Duplicates Only</string>
|
<string name="duplicates_only">Duplicates Only</string>
|
||||||
@@ -121,7 +128,7 @@
|
|||||||
<string name="existing_item_id_d">Existing Item (ID: %1$d)</string>
|
<string name="existing_item_id_d">Existing Item (ID: %1$d)</string>
|
||||||
|
|
||||||
<string name="experimental_features">Experimental Features</string>
|
<string name="experimental_features">Experimental Features</string>
|
||||||
<string name="experimental_features_description">Enable experimental features that are not yet ready for production.</string>
|
<string name="experimental_features_description">Enable experimental features that aren’t yet ready for production.</string>
|
||||||
|
|
||||||
<string name="export_vocabulary_data">Export Vocabulary Data</string>
|
<string name="export_vocabulary_data">Export Vocabulary Data</string>
|
||||||
|
|
||||||
@@ -134,9 +141,17 @@
|
|||||||
<string name="fetching_for_d_items">Fetching for %d Items</string>
|
<string name="fetching_for_d_items">Fetching for %d Items</string>
|
||||||
<string name="fetching_grammar_details">Fetching Grammar Details</string>
|
<string name="fetching_grammar_details">Fetching Grammar Details</string>
|
||||||
|
|
||||||
|
<string name="filter_a1">Beginner · A1</string>
|
||||||
|
<string name="filter_a2">Elementary · A2</string>
|
||||||
|
<!-- Pack Filter Labels -->
|
||||||
|
<string name="filter_all">All</string>
|
||||||
<string name="filter_and_sort">Filter and Sort</string>
|
<string name="filter_and_sort">Filter and Sort</string>
|
||||||
<string name="label_filter_by_stage">Filter by Stage</string>
|
<string name="filter_b1">Intermediate · B1</string>
|
||||||
|
<string name="filter_b2">Upper Int. · B2</string>
|
||||||
<string name="filter_by_word_type">Filter by Word Type</string>
|
<string name="filter_by_word_type">Filter by Word Type</string>
|
||||||
|
<string name="filter_c1">Advanced · C1</string>
|
||||||
|
<string name="filter_c2">Proficient · C2</string>
|
||||||
|
<string name="filter_newest">Newest</string>
|
||||||
|
|
||||||
<string name="find_translations">Find Translations</string>
|
<string name="find_translations">Find Translations</string>
|
||||||
|
|
||||||
@@ -165,8 +180,17 @@
|
|||||||
<string name="hide_context">Hide</string>
|
<string name="hide_context">Hide</string>
|
||||||
|
|
||||||
<string name="hint">Hint: %1$s</string>
|
<string name="hint">Hint: %1$s</string>
|
||||||
|
<string name="hint_hints_header_advanced">Advanced Features</string>
|
||||||
|
<string name="hint_hints_header_basics">Getting Started</string>
|
||||||
|
<string name="hint_hints_header_vocabulary">Vocabulary Management</string>
|
||||||
|
<string name="hint_hints_overview_description">All hints that are in this app can be found here as well.</string>
|
||||||
|
<string name="hint_hints_overview_intro">Help Center</string>
|
||||||
<string name="hint_how_to_connect_to_an_ai">How to connect to an AI</string>
|
<string name="hint_how_to_connect_to_an_ai">How to connect to an AI</string>
|
||||||
<string name="hint_how_to_generate_vocabulary_with_ai">How to generate Vocabulary with AI</string>
|
<string name="hint_how_to_generate_vocabulary_with_ai">How to generate Vocabulary with AI</string>
|
||||||
|
<string name="hint_scan_hint_title">Finding the right AI model</string>
|
||||||
|
<string name="hint_title_hints_overview">Help and Instructions</string>
|
||||||
|
<string name="hint_translate_how_it_works">How translation works</string>
|
||||||
|
<string name="hint_vocabulary_progress_hint_title">Vocabulary Progress Tracking</string>
|
||||||
|
|
||||||
<string name="imperative">Imperative</string>
|
<string name="imperative">Imperative</string>
|
||||||
|
|
||||||
@@ -186,10 +210,11 @@
|
|||||||
<string name="item_id">Item ID: %1$d</string>
|
<string name="item_id">Item ID: %1$d</string>
|
||||||
|
|
||||||
<string name="items">%1$d items</string>
|
<string name="items">%1$d items</string>
|
||||||
<string name="items_without_grammar_infos">Items without grammar infos</string>
|
<string name="label_items_without_grammar">Items without grammar infos</string>
|
||||||
|
|
||||||
<string name="keep_both">Keep Both</string>
|
<string name="keep_both">Keep Both</string>
|
||||||
|
|
||||||
|
<string name="label_2d_days">%1$d Days</string>
|
||||||
<string name="label_about">About</string>
|
<string name="label_about">About</string>
|
||||||
<string name="label_academic">Academic</string>
|
<string name="label_academic">Academic</string>
|
||||||
<string name="label_action_correct">Correct</string>
|
<string name="label_action_correct">Correct</string>
|
||||||
@@ -198,19 +223,28 @@
|
|||||||
<string name="label_add_category">Add Category</string>
|
<string name="label_add_category">Add Category</string>
|
||||||
<string name="label_add_custom_model">Add Custom Model</string>
|
<string name="label_add_custom_model">Add Custom Model</string>
|
||||||
<string name="label_add_custom_provider">Add Custom Provider</string>
|
<string name="label_add_custom_provider">Add Custom Provider</string>
|
||||||
|
<string name="label_add_d_words">Add %1$d words</string>
|
||||||
|
<string name="label_add_d_words_to_library">Add %1$d words to Library</string>
|
||||||
<string name="label_add_key">Add Key</string>
|
<string name="label_add_key">Add Key</string>
|
||||||
<string name="label_add_model">Add Model</string>
|
<string name="label_add_model">Add Model</string>
|
||||||
<string name="label_add_model_manually">Add Model Manually</string>
|
<string name="label_add_model_manually">Add Model Manually</string>
|
||||||
<string name="label_add_synonym">Add synonym</string>
|
<string name="label_add_synonym">Add synonym</string>
|
||||||
<string name="label_add_to_dictionary">Add to dictionary</string>
|
<string name="label_add_to_dictionary">Add to dictionary</string>
|
||||||
|
<string name="label_add_to_library">Add to Library</string>
|
||||||
<string name="label_add_validate"><![CDATA[Add & Validate]]></string>
|
<string name="label_add_validate"><![CDATA[Add & Validate]]></string>
|
||||||
<string name="label_add_vocabulary">Add Vocabulary</string>
|
<string name="label_add_vocabulary">Add Vocabulary</string>
|
||||||
<string name="label_added">Added</string>
|
<string name="label_added">Added</string>
|
||||||
<string name="label_adjective">Adjective</string>
|
<string name="label_adjective">Adjective</string>
|
||||||
<string name="label_adverb">Adverb</string>
|
<string name="label_adverb">Adverb</string>
|
||||||
<string name="label_ai_configuration">AI Configuration</string>
|
<string name="label_ai_configuration">AI Configuration</string>
|
||||||
|
<string name="label_ai_generator">AI Generator</string>
|
||||||
|
<string name="text_ai_generator_requires_configuration">In order to generate vocabulary with AI, you have to configure a connection to an AI service.</string>
|
||||||
|
<string name="label_configure">Configure</string>
|
||||||
<string name="label_ai_model">AI Model</string>
|
<string name="label_ai_model">AI Model</string>
|
||||||
<string name="label_ai_model_and_prompt"><![CDATA[AI Model & Prompt]]></string>
|
<string name="label_ai_model_and_prompt"><![CDATA[AI Model & Prompt]]></string>
|
||||||
|
<string name="label_all_cards">All Cards</string>
|
||||||
|
<string name="label_all_categories">All Categories</string>
|
||||||
|
<string name="label_all_categoriess">All Categories</string>
|
||||||
<string name="label_all_stages">All Stages</string>
|
<string name="label_all_stages">All Stages</string>
|
||||||
<string name="label_all_types">All Types</string>
|
<string name="label_all_types">All Types</string>
|
||||||
<string name="label_all_vocabulary">All Vocabulary</string>
|
<string name="label_all_vocabulary">All Vocabulary</string>
|
||||||
@@ -220,6 +254,8 @@
|
|||||||
<string name="label_appearance">Appearance</string>
|
<string name="label_appearance">Appearance</string>
|
||||||
<string name="label_apply_filters">Apply Filters</string>
|
<string name="label_apply_filters">Apply Filters</string>
|
||||||
<string name="label_article">Article</string>
|
<string name="label_article">Article</string>
|
||||||
|
<string name="label_auto_cycle_dev">Auto Cycle (Dev)</string>
|
||||||
|
<string name="label_available_collections">Available Collections</string>
|
||||||
<string name="label_backup_and_restore">Backup and Restore</string>
|
<string name="label_backup_and_restore">Backup and Restore</string>
|
||||||
<string name="label_by_language">By Language</string>
|
<string name="label_by_language">By Language</string>
|
||||||
<string name="label_cancel">Cancel</string>
|
<string name="label_cancel">Cancel</string>
|
||||||
@@ -228,19 +264,30 @@
|
|||||||
<string name="label_category">Category</string>
|
<string name="label_category">Category</string>
|
||||||
<string name="label_category_2d">Category: %1$s</string>
|
<string name="label_category_2d">Category: %1$s</string>
|
||||||
<string name="label_clear">Clear</string>
|
<string name="label_clear">Clear</string>
|
||||||
|
<string name="label_clear_all">Clear All</string>
|
||||||
<string name="label_close">Close</string>
|
<string name="label_close">Close</string>
|
||||||
|
<string name="label_close_exercise">Close exercise</string>
|
||||||
<string name="label_close_search">Close search</string>
|
<string name="label_close_search">Close search</string>
|
||||||
|
<string name="label_close_selection_mode">Close selection mode</string>
|
||||||
<string name="label_collapse">Collapse</string>
|
<string name="label_collapse">Collapse</string>
|
||||||
|
<string name="label_colloquial">Colloquial</string>
|
||||||
<string name="label_column_n">Column %1$d</string>
|
<string name="label_column_n">Column %1$d</string>
|
||||||
<string name="label_common">Common</string>
|
<string name="label_common">Common</string>
|
||||||
<string name="label_completed">Completed</string>
|
<string name="label_completed">Completed</string>
|
||||||
<string name="label_confirm">Confirm</string>
|
<string name="label_confirm">Confirm</string>
|
||||||
<string name="label_conjugation">Conjugation: %1$s</string>
|
<string name="label_conjugation">Conjugation: %1$s</string>
|
||||||
<string name="label_conjunction">Conjunction</string>
|
<string name="label_conjunction">Conjunction</string>
|
||||||
|
<string name="label_context">Context</string>
|
||||||
<string name="label_continue">Continue</string>
|
<string name="label_continue">Continue</string>
|
||||||
<string name="label_correct">Correct</string>
|
<string name="label_correct">Correct</string>
|
||||||
|
<string name="label_create">Create</string>
|
||||||
<string name="label_create_exercise">Create Exercise</string>
|
<string name="label_create_exercise">Create Exercise</string>
|
||||||
|
<string name="label_current_streak">Current Streak</string>
|
||||||
<string name="label_custom">Custom</string>
|
<string name="label_custom">Custom</string>
|
||||||
|
<string name="label_d_packs">%1$d packs</string>
|
||||||
|
<string name="label_daily_goal">Daily Goal</string>
|
||||||
|
<string name="label_daily_review">Daily Review</string>
|
||||||
|
<string name="label_declension">Declension</string>
|
||||||
<string name="label_definitions">Definitions</string>
|
<string name="label_definitions">Definitions</string>
|
||||||
<string name="label_delete">Delete</string>
|
<string name="label_delete">Delete</string>
|
||||||
<string name="label_delete_all">Delete all</string>
|
<string name="label_delete_all">Delete all</string>
|
||||||
@@ -254,110 +301,159 @@
|
|||||||
<string name="label_dictionary_content">Dictionary Content</string>
|
<string name="label_dictionary_content">Dictionary Content</string>
|
||||||
<string name="label_dictionary_manager">Dictionary Manager</string>
|
<string name="label_dictionary_manager">Dictionary Manager</string>
|
||||||
<string name="label_dictionary_options">Dictionary Options</string>
|
<string name="label_dictionary_options">Dictionary Options</string>
|
||||||
<string name="tab_ai_definition">AI Definition</string>
|
|
||||||
<string name="tab_downloaded">Downloaded</string>
|
|
||||||
<string name="label_display_name">Display Name</string>
|
<string name="label_display_name">Display Name</string>
|
||||||
<string name="label_done">Done</string>
|
<string name="label_done">Done</string>
|
||||||
<string name="label_download">Download</string>
|
<string name="label_download">Download</string>
|
||||||
<string name="label_easy">Easy</string>
|
<string name="label_easy">Easy</string>
|
||||||
|
<string name="label_edit">Edit</string>
|
||||||
<string name="label_enter_a_text">Enter a text</string>
|
<string name="label_enter_a_text">Enter a text</string>
|
||||||
<string name="label_etymology">Etymology</string>
|
<string name="label_etymology">Etymology</string>
|
||||||
<string name="label_exercise">Exercise</string>
|
<string name="label_exercise">Exercise</string>
|
||||||
<string name="label_exercises">Exercises</string>
|
<string name="label_exercises">Exercises</string>
|
||||||
<string name="label_expand">Expand</string>
|
<string name="label_expand">Expand</string>
|
||||||
<string name="label_feminine">Feminine</string>
|
<string name="label_feminine">Feminine</string>
|
||||||
|
<string name="label_filter_by_stage">Filter by Stage</string>
|
||||||
|
<string name="label_filter_cards">Filter Cards</string>
|
||||||
<string name="label_first_column">First Column</string>
|
<string name="label_first_column">First Column</string>
|
||||||
<string name="label_first_language">First Language</string>
|
<string name="label_first_language">First Language</string>
|
||||||
|
<string name="label_from">From</string>
|
||||||
<string name="label_gender">Gender</string>
|
<string name="label_gender">Gender</string>
|
||||||
<string name="label_general">General</string>
|
<string name="label_general">General</string>
|
||||||
|
<!-- Pack Card -->
|
||||||
|
<string name="label_get">Get</string>
|
||||||
|
<string name="label_get_d_words">Get – %1$d words</string>
|
||||||
<string name="label_grammar_auxiliary">" (Auxiliary: %1$s)"</string>
|
<string name="label_grammar_auxiliary">" (Auxiliary: %1$s)"</string>
|
||||||
<string name="label_grammar_hyphenation">Hyphenation</string>
|
<string name="label_grammar_hyphenation">Hyphenation</string>
|
||||||
<string name="label_grammar_inflections">Inflections</string>
|
<string name="label_grammar_inflections">Inflections</string>
|
||||||
<string name="label_grammar_meanings">Meanings</string>
|
<string name="label_grammar_meanings">Meanings</string>
|
||||||
|
<string name="label_grammar_only">Grammar only</string>
|
||||||
<string name="label_guessing_exercise">Guessing</string>
|
<string name="label_guessing_exercise">Guessing</string>
|
||||||
<string name="label_hard">Hard</string>
|
<string name="label_hard">Hard</string>
|
||||||
<string name="label_header_row">First Row is a Header</string>
|
<string name="label_header_row">First Row is a Header</string>
|
||||||
<string name="label_hide_examples">Hide examples</string>
|
<string name="label_hide_examples">Hide examples</string>
|
||||||
<string name="label_home">Home</string>
|
<string name="label_home">Home</string>
|
||||||
<string name="label_import">Import</string>
|
<string name="label_import">Import</string>
|
||||||
|
<string name="label_import_csv">Import CSV</string>
|
||||||
<string name="label_import_table_csv_excel">Import Table (CSV)</string>
|
<string name="label_import_table_csv_excel">Import Table (CSV)</string>
|
||||||
|
<string name="label_in_library">In Library</string>
|
||||||
<string name="label_in_stages">In Stages</string>
|
<string name="label_in_stages">In Stages</string>
|
||||||
<string name="label_interjection">Interjection</string>
|
<string name="label_interjection">Interjection</string>
|
||||||
|
<string name="label_interval_settings_in_days">Interval Settings</string>
|
||||||
<string name="label_language_auto">Auto</string>
|
<string name="label_language_auto">Auto</string>
|
||||||
<string name="label_language_direction">Language Direction\n</string>
|
<string name="label_language_direction">Language Direction\n</string>
|
||||||
<string name="label_language_none">None</string>
|
<string name="label_language_none">None</string>
|
||||||
<string name="label_languages">Languages</string>
|
<string name="label_languages">Languages</string>
|
||||||
<string name="label_learned">Learned</string>
|
<string name="label_learned">Learned</string>
|
||||||
|
<string name="label_learnedd">learned</string>
|
||||||
<string name="label_learning_criteria">Learning Criteria</string>
|
<string name="label_learning_criteria">Learning Criteria</string>
|
||||||
|
<string name="label_library">Library</string>
|
||||||
<string name="label_logs">Logs</string>
|
<string name="label_logs">Logs</string>
|
||||||
<string name="label_masculine">Masculine</string>
|
<string name="label_masculine">Masculine</string>
|
||||||
<string name="label_medium">Medium</string>
|
<string name="label_medium">Medium</string>
|
||||||
<string name="label_model_id_star">Model ID *</string>
|
<string name="label_model_id_star">Model ID *</string>
|
||||||
<string name="label_more">More</string>
|
<string name="label_more">More</string>
|
||||||
<string name="label_move_first_stage">Move to First Stage</string>
|
<string name="label_move_first_stage">Move to First Stage</string>
|
||||||
|
<string name="label_multiple_choice_desc">Choose the right translation</string>
|
||||||
<string name="label_multiple_choice_exercise">Multiple Choice</string>
|
<string name="label_multiple_choice_exercise">Multiple Choice</string>
|
||||||
<string name="label_neuter">Neuter</string>
|
<string name="label_neuter">Neuter</string>
|
||||||
<string name="label_new">New</string>
|
<string name="label_new">New</string>
|
||||||
|
<string name="label_new_words">New Words</string>
|
||||||
|
<string name="label_new_wordss">New Words</string>
|
||||||
|
<string name="label_no_category">None</string>
|
||||||
|
<string name="label_no_history_yet">No history yet</string>
|
||||||
<string name="label_noun">Noun</string>
|
<string name="label_noun">Noun</string>
|
||||||
|
<string name="label_optional">(Optional)</string>
|
||||||
<string name="label_origin_language">Origin Language</string>
|
<string name="label_origin_language">Origin Language</string>
|
||||||
<string name="label_orphaned_files">Orphaned Files</string>
|
<string name="label_orphaned_files">Orphaned Files</string>
|
||||||
|
<string name="label_paste">Paste</string>
|
||||||
<string name="label_plural">Plural</string>
|
<string name="label_plural">Plural</string>
|
||||||
<string name="label_preposition">Preposition</string>
|
<string name="label_preposition">Preposition</string>
|
||||||
<string name="label_preview_first">Preview (first 5) for first column: %1$s</string>
|
<string name="label_preview_first">Preview (first 5) for first column: %1$s</string>
|
||||||
<string name="label_preview_second">Preview (first 5) for second column: %1$s</string>
|
<string name="label_preview_second">Preview (first 5) for second column: %1$s</string>
|
||||||
<string name="label_pronoun">Pronoun</string>
|
<string name="label_pronoun">Pronoun</string>
|
||||||
|
<string name="label_pronunciation">Pronunc iation</string>
|
||||||
<string name="label_providers">Providers</string>
|
<string name="label_providers">Providers</string>
|
||||||
<string name="label_quit_app">Quit App</string>
|
<string name="label_quit_app">Quit App</string>
|
||||||
<string name="label_quit_exercise_qm">Quit Exercise?</string>
|
<string name="label_quit_exercise_qm">Quit Exercise?</string>
|
||||||
<string name="label_raw_data_2d">Raw Data:</string>
|
<string name="label_raw_data_2d">Raw Data:</string>
|
||||||
|
<string name="label_read_aloud">Read Aloud</string>
|
||||||
|
<string name="label_ready">Ready</string>
|
||||||
|
<string name="label_recently_added">Recently Added</string>
|
||||||
|
<string name="label_regenerate">Regenerate</string>
|
||||||
<string name="label_related_words">Related Words</string>
|
<string name="label_related_words">Related Words</string>
|
||||||
<string name="label_reload">Reload</string>
|
<string name="label_reload">Reload</string>
|
||||||
<string name="label_remove_articles">Remove Articles</string>
|
<string name="label_remove_articles">Remove Articles</string>
|
||||||
|
<string name="label_request_a_pack">Request a Pack</string>
|
||||||
|
<string name="label_reset">Reset</string>
|
||||||
|
<string name="label_retry">Retry</string>
|
||||||
|
<string name="label_retry_download">Retry download</string>
|
||||||
<string name="label_save">Save</string>
|
<string name="label_save">Save</string>
|
||||||
<string name="label_scan_for_models">Scan for Models</string>
|
<string name="label_scan_for_models">Scan for Models</string>
|
||||||
<string name="label_scanning">Scanning…</string>
|
<string name="label_scanning">Scanning…</string>
|
||||||
|
<string name="label_search_cards">Search cards</string>
|
||||||
<string name="label_search_models">Search models…</string>
|
<string name="label_search_models">Search models…</string>
|
||||||
<string name="label_second_column">Second Column</string>
|
<string name="label_second_column">Second Column</string>
|
||||||
<string name="label_second_language">Second Language</string>
|
<string name="label_second_language">Second Language</string>
|
||||||
|
<string name="label_see_history">See History</string>
|
||||||
<string name="label_select">Select</string>
|
<string name="label_select">Select</string>
|
||||||
<string name="label_select_stage">Select Stage</string>
|
<string name="label_select_stage">Select Stage</string>
|
||||||
|
<string name="label_send_request">Send Request</string>
|
||||||
|
<string name="label_settings">Settings</string>
|
||||||
<string name="label_show_2d_more">Show %1$d More</string>
|
<string name="label_show_2d_more">Show %1$d More</string>
|
||||||
<string name="label_show_dictionary_entry">Show dictionary entry</string>
|
<string name="label_show_dictionary_entry">Show dictionary entry</string>
|
||||||
<string name="label_show_examples">Show examples</string>
|
<string name="label_show_examples">Show examples</string>
|
||||||
<string name="label_show_less">Show Less</string>
|
<string name="label_show_less">Show Less</string>
|
||||||
|
<string name="label_show_more">Show More</string>
|
||||||
<string name="label_show_more_actions">Show more actions</string>
|
<string name="label_show_more_actions">Show more actions</string>
|
||||||
<string name="label_size_2d_mb">Size: %1$d MB</string>
|
<string name="label_size_2d_mb">Size: %1$d MB</string>
|
||||||
|
<string name="label_sort_by">Sort By</string>
|
||||||
|
<string name="label_speaking_speed">Speaking Speed</string>
|
||||||
<string name="label_spelling_exercise">Spelling</string>
|
<string name="label_spelling_exercise">Spelling</string>
|
||||||
<string name="label_star_required">*required</string>
|
<string name="label_star_required">*required</string>
|
||||||
<string name="label_start">Start</string>
|
<string name="label_start">Start</string>
|
||||||
<string name="label_start_exercise">Start Exercise</string>
|
<string name="label_start_exercise">Start Exercise</string>
|
||||||
<string name="label_start_exercise_2d">Start Exercise (%1$d)</string>
|
<string name="label_start_exercise_2d">Start Exercise (%1$d)</string>
|
||||||
|
<string name="label_start_required">* required</string>
|
||||||
<string name="label_statistics">Statistics</string>
|
<string name="label_statistics">Statistics</string>
|
||||||
|
<string name="label_stats">Stats</string>
|
||||||
<string name="label_status">Status</string>
|
<string name="label_status">Status</string>
|
||||||
<string name="label_system">System</string>
|
<string name="label_system">System</string>
|
||||||
|
<string name="label_target_correct_answers_per_day">Target Correct Answers Per Day</string>
|
||||||
<string name="label_target_language">Target Language</string>
|
<string name="label_target_language">Target Language</string>
|
||||||
|
<string name="label_target_tone">Target Tone:</string>
|
||||||
<string name="label_task_model_assignments">Task Model Assignments</string>
|
<string name="label_task_model_assignments">Task Model Assignments</string>
|
||||||
<string name="label_tasks">Tasks</string>
|
<string name="label_tasks">Tasks</string>
|
||||||
<string name="label_tense">Tense</string>
|
<string name="label_tense">Tense</string>
|
||||||
|
<string name="label_to">To</string>
|
||||||
|
<string name="label_topic">Topic</string>
|
||||||
<string name="label_total_words">Total Words</string>
|
<string name="label_total_words">Total Words</string>
|
||||||
<string name="label_training_mode">Training Mode</string>
|
<string name="label_training_mode">Training Mode</string>
|
||||||
<string name="label_translate">Translate</string>
|
<string name="label_translate">Translate</string>
|
||||||
<string name="label_translate_from_2d">Translate from %1$s</string>
|
<string name="label_translate_from_2d">Translate from %1$s</string>
|
||||||
<string name="label_translation">Translation</string>
|
<string name="label_translation">Translation</string>
|
||||||
|
<string name="label_translation_server">Translation Server</string>
|
||||||
<string name="label_translation_settings">Translation Settings</string>
|
<string name="label_translation_settings">Translation Settings</string>
|
||||||
<string name="label_translations">Translations</string>
|
<string name="label_translations">Translations</string>
|
||||||
<string name="label_unknown">Unknown</string>
|
<string name="label_unknown">Unknown</string>
|
||||||
<string name="label_unknown_dictionary_d">Unknown Dictionary (%1$s)</string>
|
<string name="label_unknown_dictionary_d">Unknown Dictionary (%1$s)</string>
|
||||||
<string name="label_update">Update</string>
|
<string name="label_update">Update</string>
|
||||||
|
<string name="label_variations">Variations</string>
|
||||||
<string name="label_verb">Verb</string>
|
<string name="label_verb">Verb</string>
|
||||||
<string name="label_version_2d">Version: %1$s</string>
|
<string name="label_version_2d">Version: %1$s</string>
|
||||||
|
<string name="label_view_all">View All</string>
|
||||||
<string name="label_vocabulary">Vocabulary</string>
|
<string name="label_vocabulary">Vocabulary</string>
|
||||||
<string name="label_vocabulary_activity">Vocabulary Activity</string>
|
<string name="label_vocabulary_activity">Vocabulary Activity</string>
|
||||||
|
<string name="label_vocabulary_settings">Progress Settings</string>
|
||||||
<string name="label_warning">Warning</string>
|
<string name="label_warning">Warning</string>
|
||||||
|
<string name="label_weekly_progress">Weekly Progress</string>
|
||||||
<string name="label_wiktionary">Wiktionary</string>
|
<string name="label_wiktionary">Wiktionary</string>
|
||||||
<string name="label_word">Word</string>
|
<string name="label_word">Word</string>
|
||||||
<string name="label_word_jumble_exercise">Word Jumble</string>
|
<string name="label_word_jumble_exercise">Word Jumble</string>
|
||||||
|
<string name="label_words_in_this_pack">Words in this pack</string>
|
||||||
<string name="label_wrong">Wrong</string>
|
<string name="label_wrong">Wrong</string>
|
||||||
|
<string name="label_wrong_answers">Wrong answers</string>
|
||||||
|
<string name="label_yes">Yes</string>
|
||||||
|
<string name="label_your_answer">Your Answer</string>
|
||||||
<string name="label_your_translation">Your translation</string>
|
<string name="label_your_translation">Your translation</string>
|
||||||
|
|
||||||
<string name="labels_1d_models">%1$d models</string>
|
<string name="labels_1d_models">%1$d models</string>
|
||||||
@@ -412,6 +508,66 @@
|
|||||||
<string name="merge">Merge</string>
|
<string name="merge">Merge</string>
|
||||||
<string name="merge_items">Merge Items</string>
|
<string name="merge_items">Merge Items</string>
|
||||||
|
|
||||||
|
<string name="message_error_api_key_invalid">API Key is missing or invalid.</string>
|
||||||
|
<!-- API Key related -->
|
||||||
|
<string name="message_error_api_key_missing">API Key is missing or invalid.</string>
|
||||||
|
<string name="message_error_articles_remove_failed">Error removing articles: %1$s</string>
|
||||||
|
<string name="message_error_category_update_failed">Error updating category: %1$s</string>
|
||||||
|
<string name="message_error_excel_not_supported">Excel isn’t supported. Use CSV instead.</string>
|
||||||
|
<string name="message_error_file_picker_not_initialized">Save File Launcher not initialized.</string>
|
||||||
|
<string name="message_error_file_save_cancelled">File save cancelled or failed.</string>
|
||||||
|
<string name="message_error_file_save_failed">Error saving file: %1$s</string>
|
||||||
|
<string name="message_error_generic">An error occurred</string>
|
||||||
|
<string name="message_error_grammar_fetch_failed">Could not retrieve grammar details.</string>
|
||||||
|
<string name="message_error_items_add_failed">Error adding items: %1$s</string>
|
||||||
|
<string name="message_error_items_delete_failed">Error deleting items: %1$s</string>
|
||||||
|
<!-- Language related -->
|
||||||
|
<string name="message_error_language_not_selected">Source and target languages must be selected.</string>
|
||||||
|
<string name="message_error_no_cards_found">No cards found for the specified filter.</string>
|
||||||
|
<string name="message_error_no_words_found">No words found in the provided text.</string>
|
||||||
|
<!-- Operation status -->
|
||||||
|
<string name="message_error_operation_failed">Operation failed: %1$s</string>
|
||||||
|
<string name="message_error_repository_wipe_failed">Failed to wipe repository: %1$s</string>
|
||||||
|
<string name="message_error_stage_update_failed">Error updating stage: %1$s</string>
|
||||||
|
<string name="message_error_synonyms_generation_failed">Failed to generate synonyms: %1$s</string>
|
||||||
|
<string name="message_error_translation_failed">Translation failed: %1$s</string>
|
||||||
|
<string name="message_error_vocabulary_import_failed">Error importing vocabulary items: %1$s</string>
|
||||||
|
<string name="message_info_generic">Info</string>
|
||||||
|
<string name="message_loading_card_set">Loading card set</string>
|
||||||
|
<string name="message_loading_generic">Loading…</string>
|
||||||
|
<string name="message_loading_grammar_fetch">Fetching grammar for %1$d items…</string>
|
||||||
|
<string name="message_loading_operation_in_progress">Operation in progress…</string>
|
||||||
|
<!-- Translation related -->
|
||||||
|
<string name="message_loading_translating">Translating %1$d words…</string>
|
||||||
|
<!-- Article removal -->
|
||||||
|
<string name="message_success_articles_removed">Articles removed successfully.</string>
|
||||||
|
<string name="message_success_cards_loaded">Successfully loaded card set.</string>
|
||||||
|
<string name="message_success_category_saved">Category saved to %1$s</string>
|
||||||
|
<!-- Category operations -->
|
||||||
|
<string name="message_success_category_updated">Category updated successfully.</string>
|
||||||
|
<!-- File operations -->
|
||||||
|
<string name="message_success_file_saved">File saved to %1$s</string>
|
||||||
|
<!-- Status Messages (for internationalization) -->
|
||||||
|
<string name="message_success_generic">Success!</string>
|
||||||
|
<!-- Grammar related -->
|
||||||
|
<string name="message_success_grammar_updated">Grammar details updated!</string>
|
||||||
|
<string name="message_success_items_added">Successfully added %1$d new vocabulary items.</string>
|
||||||
|
<string name="message_success_items_deleted">Successfully deleted vocabulary items.</string>
|
||||||
|
<string name="message_success_items_merged">Items merged!</string>
|
||||||
|
<string name="message_success_language_replaced">Language ID updated for %1$d items.</string>
|
||||||
|
<!-- Repository operations -->
|
||||||
|
<string name="message_success_repository_wiped">All repository data deleted.</string>
|
||||||
|
<!-- Stage operations -->
|
||||||
|
<string name="message_success_stage_updated">Stage updated successfully.</string>
|
||||||
|
<!-- Synonyms -->
|
||||||
|
<string name="message_success_synonyms_generated">Synonyms generated successfully.</string>
|
||||||
|
<string name="message_success_translation_completed">Translation completed.</string>
|
||||||
|
<!-- Vocabulary related -->
|
||||||
|
<string name="message_success_vocabulary_imported">Vocabulary items imported successfully.</string>
|
||||||
|
<string name="message_test_error">Oops, something went wrong :(</string>
|
||||||
|
<string name="message_test_info">This is a generic info message.</string>
|
||||||
|
<string name="message_test_success">This is a test success message!</string>
|
||||||
|
|
||||||
<string name="min_correct_to_advance">Min. Correct to Advance</string>
|
<string name="min_correct_to_advance">Min. Correct to Advance</string>
|
||||||
|
|
||||||
<string name="model">Model</string>
|
<string name="model">Model</string>
|
||||||
@@ -445,6 +601,7 @@
|
|||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
<string name="no_cards_found_for_the_selected_filters">No cards found for the selected filters.</string>
|
<string name="no_cards_found_for_the_selected_filters">No cards found for the selected filters.</string>
|
||||||
<string name="no_grammar_configuration_found_for_this_language">No grammar configuration found for this language.</string>
|
<string name="no_grammar_configuration_found_for_this_language">No grammar configuration found for this language.</string>
|
||||||
|
<string name="no_items_due_for_review">No items due for review today. Great job!</string>
|
||||||
<string name="no_items_without_grammar">No Items without Grammar</string>
|
<string name="no_items_without_grammar">No Items without Grammar</string>
|
||||||
<string name="no_model_selected_for_the_task">No model selected for the task: %1$s</string>
|
<string name="no_model_selected_for_the_task">No model selected for the task: %1$s</string>
|
||||||
<string name="no_models_configured">No Models Configured</string>
|
<string name="no_models_configured">No Models Configured</string>
|
||||||
@@ -568,6 +725,8 @@
|
|||||||
|
|
||||||
<string name="result">Result</string>
|
<string name="result">Result</string>
|
||||||
|
|
||||||
|
<string name="review_intro">Review the generated vocabulary before adding it to your collection.</string>
|
||||||
|
|
||||||
<string name="right">Right</string>
|
<string name="right">Right</string>
|
||||||
|
|
||||||
<string name="scan_models">Scan models</string>
|
<string name="scan_models">Scan models</string>
|
||||||
@@ -576,6 +735,7 @@
|
|||||||
|
|
||||||
<string name="search_for_a_word_s_origin">Search for a word\'s origin</string>
|
<string name="search_for_a_word_s_origin">Search for a word\'s origin</string>
|
||||||
<string name="search_models">Search Models</string>
|
<string name="search_models">Search Models</string>
|
||||||
|
<string name="search_topics_phrases">Search topics, phrases…</string>
|
||||||
|
|
||||||
<string name="secondary_button">Secondary Button</string>
|
<string name="secondary_button">Secondary Button</string>
|
||||||
<string name="secondary_inverse">Secondary Inverse</string>
|
<string name="secondary_inverse">Secondary Inverse</string>
|
||||||
@@ -618,11 +778,14 @@
|
|||||||
<string name="sort_by_new_items">Sort by New Items</string>
|
<string name="sort_by_new_items">Sort by New Items</string>
|
||||||
<string name="sort_by_size">Sort by Size</string>
|
<string name="sort_by_size">Sort by Size</string>
|
||||||
<string name="sort_new_vocabulary">Sort New Vocabulary</string>
|
<string name="sort_new_vocabulary">Sort New Vocabulary</string>
|
||||||
|
<string name="sort_order_alphabetical">Alphabetical</string>
|
||||||
|
<string name="sort_order_language">Language</string>
|
||||||
|
<!-- Sort Order Options -->
|
||||||
|
<string name="sort_order_newest_first">Newest First</string>
|
||||||
|
<string name="sort_order_oldest_first">Oldest First</string>
|
||||||
|
|
||||||
<string name="sorting_hint_title">Vocabulary Sorting</string>
|
<string name="sorting_hint_title">Vocabulary Sorting</string>
|
||||||
|
|
||||||
<string name="label_speaking_speed">Speaking Speed</string>
|
|
||||||
|
|
||||||
<string name="stage_1">Stage 1</string>
|
<string name="stage_1">Stage 1</string>
|
||||||
<string name="stage_2">Stage 2</string>
|
<string name="stage_2">Stage 2</string>
|
||||||
<string name="stage_3">Stage 3</string>
|
<string name="stage_3">Stage 3</string>
|
||||||
@@ -645,6 +808,15 @@
|
|||||||
<string name="status_widget_faulty_items">Faulty Items</string>
|
<string name="status_widget_faulty_items">Faulty Items</string>
|
||||||
<string name="status_widget_new_items">New Items</string>
|
<string name="status_widget_new_items">New Items</string>
|
||||||
|
|
||||||
|
<string name="strategy_keep_both">Keep Both</string>
|
||||||
|
<string name="strategy_keep_both_desc">Add all words from the pack as new entries.</string>
|
||||||
|
<string name="strategy_merge">Merge (Recommended)</string>
|
||||||
|
<string name="strategy_merge_desc">Keep existing progress; merge categories intelligently.</string>
|
||||||
|
<string name="strategy_replace">Replace Existing</string>
|
||||||
|
<string name="strategy_replace_desc">Overwrite matching words with the pack version.</string>
|
||||||
|
<string name="strategy_skip">Skip Duplicates</string>
|
||||||
|
<string name="strategy_skip_desc">Only add words that don\'t already exist.</string>
|
||||||
|
|
||||||
<string name="subjunctive">Subjunctive</string>
|
<string name="subjunctive">Subjunctive</string>
|
||||||
|
|
||||||
<string name="synonym_exists">Synonym exists</string>
|
<string name="synonym_exists">Synonym exists</string>
|
||||||
@@ -654,9 +826,10 @@
|
|||||||
<string name="system_default_font">System Default Font</string>
|
<string name="system_default_font">System Default Font</string>
|
||||||
<string name="system_theme">System Theme</string>
|
<string name="system_theme">System Theme</string>
|
||||||
|
|
||||||
<string name="tap_the_words_below_to_form_the_sentence">Tap the words below to form the sentence</string>
|
<string name="tab_ai_definition">AI Definition</string>
|
||||||
|
<string name="tab_downloaded">Downloaded</string>
|
||||||
|
|
||||||
<string name="label_target_correct_answers_per_day">Target Correct Answers Per Day</string>
|
<string name="tap_the_words_below_to_form_the_sentence">Tap the words below to form the sentence</string>
|
||||||
|
|
||||||
<string name="test">Test</string>
|
<string name="test">Test</string>
|
||||||
|
|
||||||
@@ -674,12 +847,14 @@
|
|||||||
<string name="text_a_simple_list_to">A simple list to manually sort your vocabulary</string>
|
<string name="text_a_simple_list_to">A simple list to manually sort your vocabulary</string>
|
||||||
<string name="text_add_custom_language">Add Custom Language</string>
|
<string name="text_add_custom_language">Add Custom Language</string>
|
||||||
<string name="text_add_grammar_details">Add grammar details</string>
|
<string name="text_add_grammar_details">Add grammar details</string>
|
||||||
|
<string name="text_add_new_word_to_list">Extract a New Word to Your List</string>
|
||||||
<string name="text_add_to_favorites">Add to favorites</string>
|
<string name="text_add_to_favorites">Add to favorites</string>
|
||||||
<string name="text_ai_failed_to_create_the_exercise">AI failed to create the exercise.</string>
|
<string name="text_ai_failed_to_create_the_exercise">AI failed to create the exercise.</string>
|
||||||
<string name="text_ai_generation_failed_with_an_exception">AI generation failed with an exception</string>
|
<string name="text_ai_generation_failed_with_an_exception">AI generation failed with an exception</string>
|
||||||
<string name="text_all_dictionaries_deleted_successfully">All dictionaries deleted successfully</string>
|
<string name="text_all_dictionaries_deleted_successfully">All dictionaries deleted successfully</string>
|
||||||
<string name="text_all_items_completed">All items completed!</string>
|
<string name="text_all_items_completed">All items completed!</string>
|
||||||
<string name="text_all_languages">All Languages</string>
|
<string name="text_all_languages">All Languages</string>
|
||||||
|
<string name="text_already_in_your_library">Already in your Library</string>
|
||||||
<string name="text_amount_2d">Amount: %1$d</string>
|
<string name="text_amount_2d">Amount: %1$d</string>
|
||||||
<string name="text_amount_2d_questions">Amount: %1$d Questions</string>
|
<string name="text_amount_2d_questions">Amount: %1$d Questions</string>
|
||||||
<string name="text_amount_of_cards">Amount of cards</string>
|
<string name="text_amount_of_cards">Amount of cards</string>
|
||||||
@@ -693,10 +868,10 @@
|
|||||||
<string name="text_are_you_sure_you_want_to_delete_this_category">Are you sure you want to delete this category?</string>
|
<string name="text_are_you_sure_you_want_to_delete_this_category">Are you sure you want to delete this category?</string>
|
||||||
<string name="text_are_you_sure_you_want_to_quit">Are you sure you want to quit?</string>
|
<string name="text_are_you_sure_you_want_to_quit">Are you sure you want to quit?</string>
|
||||||
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Are you sure you want to quit? Your progress in this session will be lost.</string>
|
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Are you sure you want to quit? Your progress in this session will be lost.</string>
|
||||||
<string name="text_assemble_the_word_here">Assemble the word here</string>
|
<string name="text_assemble_the_word_here">Bring the letters into the right order</string>
|
||||||
<string name="text_assign_a_different_language_items">Assign a different language to these items.</string>
|
<string name="text_assign_a_different_language_items">Assign a different language to these items.</string>
|
||||||
<string name="text_assign_these_items_2d">Assign these items:</string>
|
<string name="text_assign_these_items_2d">Assign these items:</string>
|
||||||
<string name="text_authentication_is_required_and_has_failed">Authentication is required and has failed or has not yet been provided.</string>
|
<string name="text_authentication_is_required_and_has_failed">Authentication is required and has failed or hasn’t yet been provided.</string>
|
||||||
<string name="text_automatically_discover_models_from">Automatically discover models from %1$s</string>
|
<string name="text_automatically_discover_models_from">Automatically discover models from %1$s</string>
|
||||||
<string name="text_available_dictionaries">Available Dictionaries</string>
|
<string name="text_available_dictionaries">Available Dictionaries</string>
|
||||||
<string name="text_available_models">Available Models:</string>
|
<string name="text_available_models">Available Models:</string>
|
||||||
@@ -711,6 +886,7 @@
|
|||||||
<string name="text_check_your_matches">Check your matches!</string>
|
<string name="text_check_your_matches">Check your matches!</string>
|
||||||
<string name="text_checksum_mismatch_for_expected_got">Checksum mismatch for %1$s. Expected: %2$s, Got: %3$s</string>
|
<string name="text_checksum_mismatch_for_expected_got">Checksum mismatch for %1$s. Expected: %2$s, Got: %3$s</string>
|
||||||
<string name="text_claude">Claude</string>
|
<string name="text_claude">Claude</string>
|
||||||
|
<string name="text_clipboard_empty">Clipboard is empty</string>
|
||||||
<string name="text_collapse_widget">Collapse Widget</string>
|
<string name="text_collapse_widget">Collapse Widget</string>
|
||||||
<string name="text_color_palette">Color Palette</string>
|
<string name="text_color_palette">Color Palette</string>
|
||||||
<string name="text_common">Common</string>
|
<string name="text_common">Common</string>
|
||||||
@@ -721,8 +897,12 @@
|
|||||||
<string name="text_copy_corrected_text">Copy corrected text</string>
|
<string name="text_copy_corrected_text">Copy corrected text</string>
|
||||||
<string name="text_correct_em">Correct!</string>
|
<string name="text_correct_em">Correct!</string>
|
||||||
<string name="text_could_not_fetch_a_new_word">Could not fetch a new word.</string>
|
<string name="text_could_not_fetch_a_new_word">Could not fetch a new word.</string>
|
||||||
|
<string name="text_could_not_load_packs">Could not load packs</string>
|
||||||
<string name="text_customize_the_intervals">Customize the intervals and criteria for moving vocabulary cards between stages. Cards in lower stages should be asked more often than those in higher stages.</string>
|
<string name="text_customize_the_intervals">Customize the intervals and criteria for moving vocabulary cards between stages. Cards in lower stages should be asked more often than those in higher stages.</string>
|
||||||
|
<string name="text_d_cards">%1$d cards</string>
|
||||||
|
<string name="text_d_words_will_be_added">%1$d words will be added to your library.</string>
|
||||||
<string name="text_daily_goal_description">How many words do you want to answer correctly each day?</string>
|
<string name="text_daily_goal_description">How many words do you want to answer correctly each day?</string>
|
||||||
|
<string name="text_daily_review_placeholder">Daily review screen - implementation pending</string>
|
||||||
<string name="text_dark">Dark</string>
|
<string name="text_dark">Dark</string>
|
||||||
<string name="text_day_streak">Day Streak</string>
|
<string name="text_day_streak">Day Streak</string>
|
||||||
<string name="text_days">" days"</string>
|
<string name="text_days">" days"</string>
|
||||||
@@ -732,16 +912,21 @@
|
|||||||
<string name="text_delete_category">Delete Category</string>
|
<string name="text_delete_category">Delete Category</string>
|
||||||
<string name="text_delete_custom_language">Delete custom language</string>
|
<string name="text_delete_custom_language">Delete custom language</string>
|
||||||
<string name="text_delete_vocabulary_item">Delete Vocabulary Item?</string>
|
<string name="text_delete_vocabulary_item">Delete Vocabulary Item?</string>
|
||||||
|
<string name="text_desc_no_activity_data_available">No activity data available</string>
|
||||||
|
<string name="text_desc_organize_vocabulary_groups">Organize Your Vocabulary in Groups</string>
|
||||||
|
<string name="text_description_dictionary_prompt">Set a model for generating dictionary content and give optional instructions.</string>
|
||||||
<string name="text_developed_by_jonas_gaudian">Developed by Jonas Gaudian\n</string>
|
<string name="text_developed_by_jonas_gaudian">Developed by Jonas Gaudian\n</string>
|
||||||
<string name="text_dialog_delete_key">Are you sure you want to delete the Key for this Provider?</string>
|
<string name="text_dialog_delete_key">Are you sure you want to delete the Key for this Provider?</string>
|
||||||
<string name="text_dialog_delete_model">Are you sure you want to delete the model \"%1$s\" from %2$s? This action cannot be undone.</string>
|
<string name="text_dialog_delete_model">Are you sure you want to delete the model \"%1$s\" from %2$s? This action can’t be undone.</string>
|
||||||
<string name="text_dialog_delete_provider">Are you sure you want to delete the provider \"%1$s\"? This will also remove all models associated with this provider. This action cannot be undone.</string>
|
<string name="text_dialog_delete_provider">Are you sure you want to delete the provider \"%1$s\"? This will also remove all models associated with this provider. This action can’t be undone.</string>
|
||||||
<string name="text_dictionary_deleted_successfully">Dictionary deleted successfully</string>
|
<string name="text_dictionary_deleted_successfully">Dictionary deleted successfully</string>
|
||||||
<string name="text_dictionary_downloaded_successfully">Dictionary downloaded successfully</string>
|
<string name="text_dictionary_downloaded_successfully">Dictionary downloaded successfully</string>
|
||||||
<string name="text_dictionary_manager_description">You can download dictionaries for certain languages which can be used insteaf of AI generation for dictionary content.</string>
|
<string name="text_dictionary_manager_description">You can download dictionaries for certain languages which can be used insteaf of AI generation for dictionary content.</string>
|
||||||
<string name="text_difficulty_2d">Difficulty: %1$s</string>
|
<string name="text_difficulty_2d">Difficulty: %1$s</string>
|
||||||
<string name="text_do_you_want_to_minimize_the_app">Do you want to minimize the app?</string>
|
<string name="text_do_you_want_to_minimize_the_app">Do you want to minimize the app?</string>
|
||||||
|
<string name="text_dont_see_what_looking_for">Don\'t see what you\'re looking for?</string>
|
||||||
<string name="text_download_failed_http">Download failed: HTTP %1$d %2$s</string>
|
<string name="text_download_failed_http">Download failed: HTTP %1$d %2$s</string>
|
||||||
|
<string name="text_downloading">Downloading…</string>
|
||||||
<string name="text_drag_to_reorder">Drag to Reorder</string>
|
<string name="text_drag_to_reorder">Drag to Reorder</string>
|
||||||
<string name="text_due_today">"Due Today"</string>
|
<string name="text_due_today">"Due Today"</string>
|
||||||
<string name="text_due_today_only">Due Today Only</string>
|
<string name="text_due_today_only">Due Today Only</string>
|
||||||
@@ -767,9 +952,9 @@
|
|||||||
<string name="text_error_generating_questions">Error generating questions: %1$s</string>
|
<string name="text_error_generating_questions">Error generating questions: %1$s</string>
|
||||||
<string name="text_error_loading_stored_values">Error loading stored values: %1$s</string>
|
<string name="text_error_loading_stored_values">Error loading stored values: %1$s</string>
|
||||||
<string name="text_error_saving_entry">Error saving entry: %1$s</string>
|
<string name="text_error_saving_entry">Error saving entry: %1$s</string>
|
||||||
<string name="message_error_excel_not_supported">Excel is not supported. Use CSV instead.</string>
|
|
||||||
<string name="text_expand_widget">Expand Widget</string>
|
<string name="text_expand_widget">Expand Widget</string>
|
||||||
<string name="text_explanation">Explanation</string>
|
<string name="text_explanation">Explanation</string>
|
||||||
|
<string name="text_explore_more_categories">Explore more categories</string>
|
||||||
<string name="text_export_category">Export Category</string>
|
<string name="text_export_category">Export Category</string>
|
||||||
<string name="text_failed_to_delete_dictionary">Failed to delete dictionary: %1$s</string>
|
<string name="text_failed_to_delete_dictionary">Failed to delete dictionary: %1$s</string>
|
||||||
<string name="text_failed_to_delete_orphaned_file">Failed to delete orphaned file: %1$s</string>
|
<string name="text_failed_to_delete_orphaned_file">Failed to delete orphaned file: %1$s</string>
|
||||||
@@ -790,25 +975,30 @@
|
|||||||
<string name="text_generate_exercise_with_ai">Generate Exercise with AI</string>
|
<string name="text_generate_exercise_with_ai">Generate Exercise with AI</string>
|
||||||
<string name="text_generating_questions_from_video">Generating questions from video…</string>
|
<string name="text_generating_questions_from_video">Generating questions from video…</string>
|
||||||
<string name="text_get_api_key_at">Get API Key at %1$s</string>
|
<string name="text_get_api_key_at">Get API Key at %1$s</string>
|
||||||
<string name="text_translation_instructions">Set model for translation and give optional instructions on how to translate.</string>
|
|
||||||
<string name="text_here_you_can_set_a_custom_">Here you can set a custom prompt for the AI vocabulary model. This allows you to define how new vocabulary entries are generated.</string>
|
<string name="text_here_you_can_set_a_custom_">Here you can set a custom prompt for the AI vocabulary model. This allows you to define how new vocabulary entries are generated.</string>
|
||||||
<string name="text_hint">Hint</string>
|
<string name="text_hint">Hint</string>
|
||||||
|
<string name="text_how_handle_duplicates">How should duplicates be handled?</string>
|
||||||
|
<string name="text_importing_d_words">Importing %1$d words…</string>
|
||||||
<string name="text_in_progress">In Progress</string>
|
<string name="text_in_progress">In Progress</string>
|
||||||
<string name="text_incorrect_em">Incorrect!</string>
|
<string name="text_incorrect_em">Incorrect!</string>
|
||||||
<string name="text_infrequent">Rare</string>
|
<string name="text_infrequent">Rare</string>
|
||||||
<string name="label_interval_settings_in_days">Interval Settings</string>
|
|
||||||
<string name="text_key_active">Key Active</string>
|
<string name="text_key_active">Key Active</string>
|
||||||
<string name="text_key_optional">Key Optional</string>
|
<string name="text_key_optional">Key Optional</string>
|
||||||
<string name="text_label_word">Enter a word\n</string>
|
<string name="text_label_word">Enter a word\n</string>
|
||||||
<string name="text_language_code">Language Code</string>
|
<string name="text_language_code">Language Code</string>
|
||||||
<string name="text_language_direction_explanation">You can set an optional preference which language should come first or second.</string>
|
|
||||||
<string name="text_language_direction_disabled_with_pairs">Clear language pair selection to choose a direction.</string>
|
<string name="text_language_direction_disabled_with_pairs">Clear language pair selection to choose a direction.</string>
|
||||||
|
<string name="text_language_direction_explanation">You can set an optional preference which language should come first or second.</string>
|
||||||
<string name="text_language_options">Language Options</string>
|
<string name="text_language_options">Language Options</string>
|
||||||
|
<string name="text_language_settings_description">Set what languages you want to use in the app. Languages that aren’t activated won’t appear in this app. You can also add your own language to the list, or change an existing language (region/locale)</string>
|
||||||
<string name="text_last_7_days">Last 7 Days</string>
|
<string name="text_last_7_days">Last 7 Days</string>
|
||||||
<string name="text_light">Light</string>
|
<string name="text_light">Light</string>
|
||||||
<string name="text_list">List</string>
|
<string name="text_list">List</string>
|
||||||
<string name="text_loading_3d">Loading…</string>
|
<string name="text_loading_3d">Loading…</string>
|
||||||
|
<string name="text_loading_packs">Loading packs…</string>
|
||||||
|
<!-- Pack Preview Dialog -->
|
||||||
|
<string name="text_loading_preview">Loading preview…</string>
|
||||||
<string name="text_manual_vocabulary_list">Manual vocabulary list</string>
|
<string name="text_manual_vocabulary_list">Manual vocabulary list</string>
|
||||||
|
<string name="text_mastered_final_level">You\'ve mastered the final level!</string>
|
||||||
<string name="text_mismatch_between_question_ids_in_exercise_and_questions_found_in_repository">Mismatch between question IDs in exercise and questions found in repository.</string>
|
<string name="text_mismatch_between_question_ids_in_exercise_and_questions_found_in_repository">Mismatch between question IDs in exercise and questions found in repository.</string>
|
||||||
<string name="text_mistral">Mistral</string>
|
<string name="text_mistral">Mistral</string>
|
||||||
<string name="text_more_options">More options</string>
|
<string name="text_more_options">More options</string>
|
||||||
@@ -822,7 +1012,8 @@
|
|||||||
<string name="text_no_items_available">No items available</string>
|
<string name="text_no_items_available">No items available</string>
|
||||||
<string name="text_no_key">No Key</string>
|
<string name="text_no_key">No Key</string>
|
||||||
<string name="text_no_models_found">No models found</string>
|
<string name="text_no_models_found">No models found</string>
|
||||||
<string name="text_no_valid_api_configuration_could_be_found">No valid API configuration could be found in the settings. Before using this app, you have to configure at least one API provider.</string>
|
<string name="text_no_packs_match_search">No packs match your search.</string>
|
||||||
|
<string name="text_no_valid_api_configuration_could_be_found">No valid LLM API configuration could be found in the settings. Before using this function, you have to configure at least one API provider.</string>
|
||||||
<string name="text_no_vocabulary_available">No vocabulary available.</string>
|
<string name="text_no_vocabulary_available">No vocabulary available.</string>
|
||||||
<string name="text_no_vocabulary_due_today">No Vocabulary Due Today</string>
|
<string name="text_no_vocabulary_due_today">No Vocabulary Due Today</string>
|
||||||
<string name="text_none">None</string>
|
<string name="text_none">None</string>
|
||||||
@@ -831,7 +1022,7 @@
|
|||||||
<string name="text_optional">" (optional)"</string>
|
<string name="text_optional">" (optional)"</string>
|
||||||
<string name="text_optional_describe_what_this_model_is_good_for">Optional: Describe what this model is good for</string>
|
<string name="text_optional_describe_what_this_model_is_good_for">Optional: Describe what this model is good for</string>
|
||||||
<string name="text_orphaned_file_deleted_successfully">Orphaned file deleted successfully</string>
|
<string name="text_orphaned_file_deleted_successfully">Orphaned file deleted successfully</string>
|
||||||
<string name="text_orphaned_file_description">This file exists locally but is not in the server manifest or missing assets. It may be from an older version or a failed download.</string>
|
<string name="text_orphaned_file_description">This file exists locally but isn’t in the server manifest or missing assets. It may be from an older version or a failed download.</string>
|
||||||
<string name="text_paste_or_open_a_">Paste or open a YouTube link to see its subtitles here.</string>
|
<string name="text_paste_or_open_a_">Paste or open a YouTube link to see its subtitles here.</string>
|
||||||
<string name="text_please_select_a_dictionary_language_first">Please select a dictionary language first.</string>
|
<string name="text_please_select_a_dictionary_language_first">Please select a dictionary language first.</string>
|
||||||
<string name="text_question">Question</string>
|
<string name="text_question">Question</string>
|
||||||
@@ -843,6 +1034,7 @@
|
|||||||
<string name="text_remove_from_favorites">Remove from favorites</string>
|
<string name="text_remove_from_favorites">Remove from favorites</string>
|
||||||
<string name="text_repeat_wrong">Repeat Wrong</string>
|
<string name="text_repeat_wrong">Repeat Wrong</string>
|
||||||
<string name="text_repeat_wrong_guesses">Repeat Wrong Guesses</string>
|
<string name="text_repeat_wrong_guesses">Repeat Wrong Guesses</string>
|
||||||
|
<string name="text_request_pack_desc">Don\'t see what you need? Let me know and I\'ll add it!</string>
|
||||||
<string name="text_required_enter_a_human_readable_name">Required: Enter a human-readable name</string>
|
<string name="text_required_enter_a_human_readable_name">Required: Enter a human-readable name</string>
|
||||||
<string name="text_required_enter_the_exact_model_identifier">Required: Enter the exact model identifier</string>
|
<string name="text_required_enter_the_exact_model_identifier">Required: Enter the exact model identifier</string>
|
||||||
<string name="text_reset_intro">Reset Intro</string>
|
<string name="text_reset_intro">Reset Intro</string>
|
||||||
@@ -851,6 +1043,7 @@
|
|||||||
<string name="text_save_key">Save Key</string>
|
<string name="text_save_key">Save Key</string>
|
||||||
<string name="text_save_prompt">Save Prompt</string>
|
<string name="text_save_prompt">Save Prompt</string>
|
||||||
<string name="text_scan_for_available_models">Scan for Available Models</string>
|
<string name="text_scan_for_available_models">Scan for Available Models</string>
|
||||||
|
<string name="text_search">Search</string>
|
||||||
<string name="text_search_3d">Search…</string>
|
<string name="text_search_3d">Search…</string>
|
||||||
<string name="text_search_history">Search History</string>
|
<string name="text_search_history">Search History</string>
|
||||||
<string name="text_search_term">Search Term</string>
|
<string name="text_search_term">Search Term</string>
|
||||||
@@ -873,7 +1066,7 @@
|
|||||||
<string name="text_shuffle_card_order">Shuffle card order</string>
|
<string name="text_shuffle_card_order">Shuffle card order</string>
|
||||||
<string name="text_shuffle_card_order_description">Shuffle Card Order</string>
|
<string name="text_shuffle_card_order_description">Shuffle Card Order</string>
|
||||||
<string name="text_shuffle_languages">Shuffle Languages</string>
|
<string name="text_shuffle_languages">Shuffle Languages</string>
|
||||||
<string name="text_shuffle_languages_description">Shuffle what language comes first. Does not affect language direction preferences.</string>
|
<string name="text_shuffle_languages_description">Shuffle what language comes first. Doesn’t affect language direction preferences.</string>
|
||||||
<string name="text_shuffle_languages_disabled_by_direction">Disable language direction preference to enable shuffling.</string>
|
<string name="text_shuffle_languages_disabled_by_direction">Disable language direction preference to enable shuffling.</string>
|
||||||
<string name="text_some_items_are_in_the_wrong_category">Some items are in the wrong category.</string>
|
<string name="text_some_items_are_in_the_wrong_category">Some items are in the wrong category.</string>
|
||||||
<string name="text_stage_2d">Stage %1$s</string>
|
<string name="text_stage_2d">Stage %1$s</string>
|
||||||
@@ -885,14 +1078,15 @@
|
|||||||
<string name="text_the_correct_sentence_was_2d">The correct sentence was: %1$s</string>
|
<string name="text_the_correct_sentence_was_2d">The correct sentence was: %1$s</string>
|
||||||
<string name="text_the_correct_translation_is_2d">The correct translation is: %1$s</string>
|
<string name="text_the_correct_translation_is_2d">The correct translation is: %1$s</string>
|
||||||
<string name="text_theme_preview">Theme Preview</string>
|
<string name="text_theme_preview">Theme Preview</string>
|
||||||
<string name="text_these_files_exist_locally">These files exist locally but are not in the server manifest. They may be from older versions.</string>
|
<string name="text_these_files_exist_locally">These files exist locally but aren’t in the server manifest. They may be from older versions.</string>
|
||||||
<string name="text_this_must_match_the_provider_s_model_name_exactly">This must match the provider\'s model name exactly</string>
|
<string name="text_this_must_match_the_provider_s_model_name_exactly">This must match the provider\'s model name exactly</string>
|
||||||
<string name="text_this_will_remove_all">This will remove all configured API providers, models, and stored API keys. This action cannot be undone.</string>
|
<string name="text_this_will_remove_all">This will remove all configured API providers, models, and stored API keys. This action can’t be undone.</string>
|
||||||
<string name="text_too_many_requests">Too Many Requests: The user has sent too many requests in a given amount of time.</string>
|
<string name="text_too_many_requests">Too Many Requests: The user has sent too many requests in a given amount of time.</string>
|
||||||
<string name="text_total_learned_words">Total Learned Words</string>
|
<string name="text_total_learned_words">Total Learned Words</string>
|
||||||
<string name="text_training_mode">Training Mode</string>
|
<string name="text_training_mode">Training Mode</string>
|
||||||
<string name="text_training_mode_description">Training mode is enabled: answers won’t affect progress.</string>
|
<string name="text_training_mode_description">Training mode is enabled: answers won’t affect progress.</string>
|
||||||
<string name="text_translation">Enter translation</string>
|
<string name="text_translation">Enter translation</string>
|
||||||
|
<string name="text_translation_instructions">Set model for translation and give optional instructions on how to translate.</string>
|
||||||
<string name="text_translation_will_appear_here">Translation will appear here</string>
|
<string name="text_translation_will_appear_here">Translation will appear here</string>
|
||||||
<string name="text_true">True</string>
|
<string name="text_true">True</string>
|
||||||
<string name="text_try_first_finding_the_word_on">Try first finding the word on Wiktionary before generating AI response</string>
|
<string name="text_try_first_finding_the_word_on">Try first finding the word on Wiktionary before generating AI response</string>
|
||||||
@@ -912,19 +1106,23 @@
|
|||||||
<string name="text_youtube_link">YouTube Link</string>
|
<string name="text_youtube_link">YouTube Link</string>
|
||||||
|
|
||||||
<string name="the_request_was_successful">The request was successful.</string>
|
<string name="the_request_was_successful">The request was successful.</string>
|
||||||
<string name="the_requested_resource_could_not_be_found">The requested resource could not be found.</string>
|
<string name="the_requested_resource_could_not_be_found">The requested resource couldn’t be found.</string>
|
||||||
<string name="the_server_could_not_understand_the_request">The server could not understand the request.</string>
|
<string name="the_server_could_not_understand_the_request">The server couldn’t understand the request.</string>
|
||||||
<string name="the_server_understood_the_request_but_is_refusing_to_authorize_it">The server understood the request, but is refusing to authorize it.</string>
|
<string name="the_server_understood_the_request_but_is_refusing_to_authorize_it">The server understood the request, but is refusing to authorize it.</string>
|
||||||
|
|
||||||
<string name="this_is_a_sample_output_text">This is a sample output text.</string>
|
<string name="this_is_a_sample_output_text">This is a sample output text.</string>
|
||||||
<string name="this_is_the_content_inside_the_card">This is the content inside the card.</string>
|
<string name="this_is_the_content_inside_the_card">This is the content inside the card.</string>
|
||||||
<string name="this_mode_will_not_affect_your_progress_in_stages">This mode will not affect your progress in stages.</string>
|
<string name="this_mode_will_not_affect_your_progress_in_stages">This mode won’t affect your progress in stages.</string>
|
||||||
|
|
||||||
<string name="timeout">Timeout</string>
|
<string name="timeout">Timeout</string>
|
||||||
|
|
||||||
<string name="title_corrector">Corrector</string>
|
<string name="title_corrector">Corrector</string>
|
||||||
<string name="title_developer_options">Developer Options</string>
|
<string name="title_developer_options">Developer Options</string>
|
||||||
|
<!-- Explore Packs Screen -->
|
||||||
|
<string name="title_explore_packs">Explore Packs</string>
|
||||||
<string name="title_http_status_codes">HTTP Status Codes</string>
|
<string name="title_http_status_codes">HTTP Status Codes</string>
|
||||||
|
<!-- Conflict Strategy Dialog -->
|
||||||
|
<string name="title_import_pack">Import \"%1$s\"</string>
|
||||||
<string name="title_items_without_grammar">Items Without Grammar</string>
|
<string name="title_items_without_grammar">Items Without Grammar</string>
|
||||||
<string name="title_multiple">Multiple</string>
|
<string name="title_multiple">Multiple</string>
|
||||||
<string name="title_settings">Settings</string>
|
<string name="title_settings">Settings</string>
|
||||||
@@ -942,7 +1140,6 @@
|
|||||||
<string name="translate_the_following_d">Translate the following (%1$s):</string>
|
<string name="translate_the_following_d">Translate the following (%1$s):</string>
|
||||||
|
|
||||||
<string name="translation_prompt_settings">Translation Prompt Settings</string>
|
<string name="translation_prompt_settings">Translation Prompt Settings</string>
|
||||||
<string name="label_translation_server">Translation Server</string>
|
|
||||||
|
|
||||||
<string name="try_again">Try Again</string>
|
<string name="try_again">Try Again</string>
|
||||||
|
|
||||||
@@ -953,7 +1150,6 @@
|
|||||||
|
|
||||||
<string name="vocabulary_added_successfully">Vocabulary Added</string>
|
<string name="vocabulary_added_successfully">Vocabulary Added</string>
|
||||||
<string name="vocabulary_repository">Vocabulary Repository</string>
|
<string name="vocabulary_repository">Vocabulary Repository</string>
|
||||||
<string name="label_vocabulary_settings">Progress Settings</string>
|
|
||||||
|
|
||||||
<string name="website_url">Website URL</string>
|
<string name="website_url">Website URL</string>
|
||||||
|
|
||||||
@@ -970,148 +1166,13 @@
|
|||||||
<string name="words_known">%1$d Words Known</string>
|
<string name="words_known">%1$d Words Known</string>
|
||||||
<string name="words_required">%1$d words required</string>
|
<string name="words_required">%1$d words required</string>
|
||||||
|
|
||||||
<string name="label_wrong_answers">Wrong answers</string>
|
|
||||||
|
|
||||||
<string name="label_yes">Yes</string>
|
|
||||||
|
|
||||||
<string name="text_mastered_final_level">You\'ve mastered the final level!</string>
|
|
||||||
|
|
||||||
<string name="label_your_answer">Your Answer</string>
|
|
||||||
<string name="your_language_journey">Your Language Journey</string>
|
<string name="your_language_journey">Your Language Journey</string>
|
||||||
<string name="label_start_required">* required</string>
|
|
||||||
<string name="label_no_history_yet">No history yet</string>
|
|
||||||
<string name="cd_play">Play</string>
|
|
||||||
<string name="label_pronunciation">Pronunc iation</string>
|
|
||||||
<string name="text_clipboard_empty">Clipboard is empty</string>
|
|
||||||
<string name="label_paste">Paste</string>
|
|
||||||
<string name="label_target_tone">Target Tone:</string>
|
|
||||||
<string name="label_grammar_only">Grammar only</string>
|
|
||||||
<string name="label_declension">Declension</string>
|
|
||||||
<string name="label_variations">Variations</string>
|
|
||||||
<string name="label_auto_cycle_dev">Auto Cycle (Dev)</string>
|
|
||||||
<string name="label_regenerate">Regenerate</string>
|
|
||||||
<string name="label_read_aloud">Read Aloud</string>
|
|
||||||
<string name="label_all_categories">All Categories</string>
|
|
||||||
<string name="text_description_dictionary_prompt">Set a model for generating dictionary content and give optional instructions.</string>
|
|
||||||
<string name="hint_vocabulary_progress_hint_title">Vocabulary Progress Tracking</string>
|
|
||||||
<string name="hint_title_hints_overview">Help and Instructions</string>
|
|
||||||
<string name="hint_hints_overview_intro">Help Center</string>
|
|
||||||
<string name="hint_hints_overview_description">All hints that are in this app can be found here as well.</string>
|
|
||||||
<string name="hint_hints_header_basics">Getting Started</string>
|
|
||||||
<string name="hint_hints_header_vocabulary">Vocabulary Management</string>
|
|
||||||
<string name="hint_hints_header_advanced">Advanced Features</string>
|
|
||||||
<string name="category_hint_intro">You can create two types of categories to organize your vocabulary:</string>
|
|
||||||
<string name="review_intro">Review the generated vocabulary before adding it to your collection.</string>
|
|
||||||
<string name="duplicate">Duplicate</string>
|
|
||||||
<string name="hint_scan_hint_title">Finding the right AI model</string>
|
|
||||||
<string name="hint_translate_how_it_works">How translation works</string>
|
|
||||||
<string name="label_no_category">None</string>
|
|
||||||
<string name="text_search">Search</string>
|
|
||||||
<string name="text_language_settings_description">Set what languages you want to use in the app. Languages that are not activated will not appear in this app. You can also add your own language to the list, or change an existing language (region/locale)</string>
|
|
||||||
|
|
||||||
<!-- Status Messages (for internationalization) -->
|
<!-- Explore Packs Hint -->
|
||||||
<string name="message_success_generic">Success!</string>
|
<string name="hint_explore_packs_title">About Vocabulary Packs</string>
|
||||||
<string name="message_info_generic">Info</string>
|
<string name="label_import_csv_or_lists">Import Lists or CSV</string>
|
||||||
<string name="message_error_generic">An error occurred</string>
|
<string name="label_corrector">Corrector</string>
|
||||||
<string name="message_loading_generic">Loading…</string>
|
<string name="label_correction">Correction</string>
|
||||||
|
<string name="message_error_no_model_configured">No AI connection is configured. This functionality does not work without a valid configuration.</string>
|
||||||
<!-- Language related -->
|
<string name="message_success_all_items_imported">All items were imported successfully.</string>
|
||||||
<string name="message_error_language_not_selected">Source and target languages must be selected.</string>
|
|
||||||
<string name="message_error_no_words_found">No words found in the provided text.</string>
|
|
||||||
<string name="message_success_language_replaced">Language ID updated for %1$d items.</string>
|
|
||||||
|
|
||||||
<!-- Vocabulary related -->
|
|
||||||
<string name="message_success_vocabulary_imported">Vocabulary items imported successfully.</string>
|
|
||||||
<string name="message_error_vocabulary_import_failed">Error importing vocabulary items: %1$s</string>
|
|
||||||
<string name="message_success_items_merged">Items merged!</string>
|
|
||||||
<string name="message_success_items_added">Successfully added %1$d new vocabulary items.</string>
|
|
||||||
<string name="message_error_items_add_failed">Error adding items: %1$s</string>
|
|
||||||
<string name="message_success_items_deleted">Successfully deleted vocabulary items.</string>
|
|
||||||
<string name="message_error_items_delete_failed">Error deleting items: %1$s</string>
|
|
||||||
<string name="message_error_no_cards_found">No cards found for the specified filter.</string>
|
|
||||||
<string name="message_success_cards_loaded">Successfully loaded card set.</string>
|
|
||||||
|
|
||||||
<!-- Grammar related -->
|
|
||||||
<string name="message_success_grammar_updated">Grammar details updated!</string>
|
|
||||||
<string name="message_error_grammar_fetch_failed">Could not retrieve grammar details.</string>
|
|
||||||
<string name="message_loading_grammar_fetch">Fetching grammar for %1$d items…</string>
|
|
||||||
|
|
||||||
<!-- File operations -->
|
|
||||||
<string name="message_success_file_saved">File saved to %1$s</string>
|
|
||||||
<string name="message_error_file_save_failed">Error saving file: %1$s</string>
|
|
||||||
<string name="message_error_file_save_cancelled">File save cancelled or failed.</string>
|
|
||||||
<string name="message_error_file_picker_not_initialized">Save File Launcher not initialized.</string>
|
|
||||||
<string name="message_success_category_saved">Category saved to %1$s</string>
|
|
||||||
|
|
||||||
<!-- API Key related -->
|
|
||||||
<string name="message_error_api_key_missing">API Key is missing or invalid.</string>
|
|
||||||
<string name="message_error_api_key_invalid">API Key is missing or invalid.</string>
|
|
||||||
|
|
||||||
<!-- Translation related -->
|
|
||||||
<string name="message_loading_translating">Translating %1$d words…</string>
|
|
||||||
<string name="message_success_translation_completed">Translation completed.</string>
|
|
||||||
<string name="message_error_translation_failed">Translation failed: %1$s</string>
|
|
||||||
|
|
||||||
<!-- Repository operations -->
|
|
||||||
<string name="message_success_repository_wiped">All repository data deleted.</string>
|
|
||||||
<string name="message_error_repository_wipe_failed">Failed to wipe repository: %1$s</string>
|
|
||||||
<string name="message_loading_card_set">Loading card set</string>
|
|
||||||
|
|
||||||
<!-- Stage operations -->
|
|
||||||
<string name="message_success_stage_updated">Stage updated successfully.</string>
|
|
||||||
<string name="message_error_stage_update_failed">Error updating stage: %1$s</string>
|
|
||||||
|
|
||||||
<!-- Category operations -->
|
|
||||||
<string name="message_success_category_updated">Category updated successfully.</string>
|
|
||||||
<string name="message_error_category_update_failed">Error updating category: %1$s</string>
|
|
||||||
|
|
||||||
<!-- Article removal -->
|
|
||||||
<string name="message_success_articles_removed">Articles removed successfully.</string>
|
|
||||||
<string name="message_error_articles_remove_failed">Error removing articles: %1$s</string>
|
|
||||||
|
|
||||||
<!-- Synonyms -->
|
|
||||||
<string name="message_success_synonyms_generated">Synonyms generated successfully.</string>
|
|
||||||
<string name="message_error_synonyms_generation_failed">Failed to generate synonyms: %1$s</string>
|
|
||||||
|
|
||||||
<!-- Operation status -->
|
|
||||||
<string name="message_error_operation_failed">Operation failed: %1$s</string>
|
|
||||||
<string name="message_loading_operation_in_progress">Operation in progress…</string>
|
|
||||||
<string name="message_test_info">This is a generic info message.</string>
|
|
||||||
<string name="message_test_success">This is a test success message!</string>
|
|
||||||
<string name="message_test_error">Oops, something went wrong :(</string>
|
|
||||||
<string name="label_stats">Stats</string>
|
|
||||||
<string name="label_library">Library</string>
|
|
||||||
<string name="label_edit">Edit</string>
|
|
||||||
<string name="label_new_words">New Words</string>
|
|
||||||
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
|
|
||||||
<string name="label_settings">Settings</string>
|
|
||||||
<string name="label_2d_days">%1$d Days</string>
|
|
||||||
<string name="label_current_streak">Current Streak</string>
|
|
||||||
<string name="label_daily_goal">Daily Goal</string>
|
|
||||||
<string name="text_desc_no_activity_data_available">No activity data available</string>
|
|
||||||
<string name="label_see_history">See History</string>
|
|
||||||
<string name="label_weekly_progress">Weekly Progress</string>
|
|
||||||
<string name="cd_go">Go</string>
|
|
||||||
<string name="label_sort_by">Sort By</string>
|
|
||||||
<string name="label_reset">Reset</string>
|
|
||||||
<string name="label_filter_cards">Filter Cards</string>
|
|
||||||
<string name="text_desc_organize_vocabulary_groups">Organize Your Vocabulary in Groups</string>
|
|
||||||
<string name="text_add_new_word_to_list">Extract a New Word to Your List</string>
|
|
||||||
<string name="cd_scroll_to_top">Scroll to top</string>
|
|
||||||
<string name="cd_settings">Settings</string>
|
|
||||||
<string name="label_import_csv">Import CSV</string>
|
|
||||||
<string name="label_ai_generator">AI Generator</string>
|
|
||||||
<string name="label_new_wordss">New Words</string>
|
|
||||||
<string name="label_recently_added">Recently Added</string>
|
|
||||||
<string name="label_view_all">View All</string>
|
|
||||||
<string name="text_explore_more_categories">Explore more categories</string>
|
|
||||||
<string name="cd_options">Options</string>
|
|
||||||
<string name="cd_selected">Selected</string>
|
|
||||||
<string name="label_all_cards">All Cards</string>
|
|
||||||
<string name="cd_filter_options">Filter options</string>
|
|
||||||
<string name="cd_add">Add</string>
|
|
||||||
<string name="cd_searchh">Search</string>
|
|
||||||
<string name="label_search_cards">Search cards</string>
|
|
||||||
<string name="label_learnedd">learned</string>
|
|
||||||
<string name="label_all_categoriess">All Categories</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
578
docs/VOCABULARY_EXPORT_IMPORT.md
Normal file
578
docs/VOCABULARY_EXPORT_IMPORT.md
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
# Vocabulary Export/Import System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Polly app includes a comprehensive vocabulary export/import system that allows users to:
|
||||||
|
- **Backup** their complete vocabulary repository
|
||||||
|
- **Share** vocabulary lists with friends, teachers, or students
|
||||||
|
- **Transfer** data between devices
|
||||||
|
- **Exchange** vocabulary via messaging apps (WhatsApp, Telegram, etc.)
|
||||||
|
- **Store** vocabulary in cloud services (Google Drive, Dropbox, etc.)
|
||||||
|
- **Integrate** with external systems via REST APIs
|
||||||
|
|
||||||
|
## Data Format
|
||||||
|
|
||||||
|
The export/import system uses **JSON** as the primary data format. JSON was chosen because it is:
|
||||||
|
- **Text-based**: Can be shared via any text-based communication channel
|
||||||
|
- **Portable**: Works across all platforms and devices
|
||||||
|
- **Human-readable**: Can be inspected and edited manually if needed
|
||||||
|
- **Standard**: Supported by all programming languages and APIs
|
||||||
|
- **Compact**: Efficient storage and transmission
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **VocabularyExport.kt**: Defines data models for export/import
|
||||||
|
2. **VocabularyRepository.kt**: Implements export/import functions
|
||||||
|
3. **ConflictStrategy**: Defines how to handle data conflicts during import
|
||||||
|
|
||||||
|
### Data Models
|
||||||
|
|
||||||
|
The system uses a sealed class hierarchy for different export scopes:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
sealed class VocabularyExportData {
|
||||||
|
abstract val formatVersion: Int
|
||||||
|
abstract val exportDate: Instant
|
||||||
|
abstract val metadata: ExportMetadata
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Export Types
|
||||||
|
|
||||||
|
1. **FullRepositoryExport**: Complete backup of everything
|
||||||
|
- All vocabulary items
|
||||||
|
- All categories (tags and filters)
|
||||||
|
- All learning states
|
||||||
|
- All category mappings
|
||||||
|
- All stage mappings
|
||||||
|
|
||||||
|
2. **CategoryExport**: Single category with its items
|
||||||
|
- One category definition
|
||||||
|
- All items in that category
|
||||||
|
- Learning states for those items
|
||||||
|
- Stage mappings for those items
|
||||||
|
|
||||||
|
3. **ItemListExport**: Custom selection of items
|
||||||
|
- Selected vocabulary items
|
||||||
|
- Learning states for those items
|
||||||
|
- Stage mappings for those items
|
||||||
|
- Optionally: associated categories
|
||||||
|
|
||||||
|
4. **SingleItemExport**: Individual vocabulary item
|
||||||
|
- One vocabulary item
|
||||||
|
- Its learning state
|
||||||
|
- Its current stage
|
||||||
|
- Categories it belongs to
|
||||||
|
|
||||||
|
## Usage Guide
|
||||||
|
|
||||||
|
### Exporting Data
|
||||||
|
|
||||||
|
#### 1. Export Full Repository
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// In a coroutine scope
|
||||||
|
val repository = VocabularyRepository.getInstance(context)
|
||||||
|
|
||||||
|
// Create export data
|
||||||
|
val exportData = repository.exportFullRepository()
|
||||||
|
|
||||||
|
// Convert to JSON string
|
||||||
|
val jsonString = repository.exportToJson(exportData, prettyPrint = true)
|
||||||
|
|
||||||
|
// Save to file, share, or upload
|
||||||
|
saveToFile(jsonString, "vocabulary_backup.json")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Export Single Category
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val categoryId = 123
|
||||||
|
val exportData = repository.exportCategory(categoryId)
|
||||||
|
|
||||||
|
if (exportData != null) {
|
||||||
|
val jsonString = repository.exportToJson(exportData)
|
||||||
|
shareViaIntent(jsonString)
|
||||||
|
} else {
|
||||||
|
// Category not found
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Export Custom Item List
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val itemIds = listOf(1, 5, 10, 15, 20)
|
||||||
|
val exportData = repository.exportItemList(itemIds, includeCategories = true)
|
||||||
|
val jsonString = repository.exportToJson(exportData)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Export Single Item
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val itemId = 42
|
||||||
|
val exportData = repository.exportSingleItem(itemId)
|
||||||
|
|
||||||
|
if (exportData != null) {
|
||||||
|
val jsonString = repository.exportToJson(exportData)
|
||||||
|
// Share via WhatsApp, email, etc.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Importing Data
|
||||||
|
|
||||||
|
#### 1. Import from JSON String
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Receive JSON string (from file, intent, API, etc.)
|
||||||
|
val jsonString = readFromFile("vocabulary_backup.json")
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
val exportData = repository.importFromJson(jsonString)
|
||||||
|
|
||||||
|
// Import with conflict strategy
|
||||||
|
val result = repository.importVocabularyData(
|
||||||
|
exportData = exportData,
|
||||||
|
strategy = ConflictStrategy.MERGE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
if (result.isSuccess) {
|
||||||
|
println("Imported: ${result.itemsImported} items")
|
||||||
|
println("Skipped: ${result.itemsSkipped} items")
|
||||||
|
println("Categories: ${result.categoriesImported}")
|
||||||
|
} else {
|
||||||
|
println("Errors: ${result.errors}")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conflict Resolution Strategies
|
||||||
|
|
||||||
|
When importing data, you must choose how to handle conflicts (duplicate items or categories):
|
||||||
|
|
||||||
|
#### 1. SKIP Strategy
|
||||||
|
```kotlin
|
||||||
|
strategy = ConflictStrategy.SKIP
|
||||||
|
```
|
||||||
|
- **Behavior**: Skip importing items that already exist
|
||||||
|
- **Use case**: Importing shared vocabulary without overwriting your progress
|
||||||
|
- **Result**: Preserves all existing data unchanged
|
||||||
|
|
||||||
|
#### 2. REPLACE Strategy
|
||||||
|
```kotlin
|
||||||
|
strategy = ConflictStrategy.REPLACE
|
||||||
|
```
|
||||||
|
- **Behavior**: Replace existing items with imported versions
|
||||||
|
- **Use case**: Restoring from backup, syncing with authoritative source
|
||||||
|
- **Result**: Overwrites local data with imported data
|
||||||
|
|
||||||
|
#### 3. MERGE Strategy (Default)
|
||||||
|
```kotlin
|
||||||
|
strategy = ConflictStrategy.MERGE
|
||||||
|
```
|
||||||
|
- **Behavior**: Intelligently merge data
|
||||||
|
- For items: Keep existing if duplicate, add new ones
|
||||||
|
- For states: Keep the more advanced learning progress
|
||||||
|
- For stages: Keep the higher stage
|
||||||
|
- For categories: Merge memberships
|
||||||
|
- **Use case**: Most common scenario, combining data from multiple sources
|
||||||
|
- **Result**: Best of both worlds
|
||||||
|
|
||||||
|
#### 4. RENAME Strategy
|
||||||
|
```kotlin
|
||||||
|
strategy = ConflictStrategy.RENAME
|
||||||
|
```
|
||||||
|
- **Behavior**: Assign new IDs to all imported items
|
||||||
|
- **Use case**: Intentionally creating duplicates for practice
|
||||||
|
- **Result**: All imported items get new IDs, no conflicts
|
||||||
|
|
||||||
|
## Data Preservation
|
||||||
|
|
||||||
|
### What Gets Exported
|
||||||
|
|
||||||
|
Every export includes complete information:
|
||||||
|
|
||||||
|
1. **Vocabulary Items**
|
||||||
|
- Word/phrase in first language
|
||||||
|
- Word/phrase in second language
|
||||||
|
- Language IDs
|
||||||
|
- Creation timestamp
|
||||||
|
- Grammatical features (if any)
|
||||||
|
- Zipf frequency scores (if available)
|
||||||
|
|
||||||
|
2. **Learning States**
|
||||||
|
- Correct answer count
|
||||||
|
- Incorrect answer count
|
||||||
|
- Last correct answer timestamp
|
||||||
|
- Last incorrect answer timestamp
|
||||||
|
|
||||||
|
3. **Stage Mappings**
|
||||||
|
- Current learning stage (NEW, STAGE_1-5, LEARNED)
|
||||||
|
- For each vocabulary item
|
||||||
|
|
||||||
|
4. **Categories**
|
||||||
|
- Category name and type
|
||||||
|
- For TagCategory: just the name
|
||||||
|
- For VocabularyFilter: language filters, stage filters, language pairs
|
||||||
|
|
||||||
|
5. **Category Memberships**
|
||||||
|
- Which items belong to which categories
|
||||||
|
- Automatically recalculated for filters during import
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
|
||||||
|
Each export includes metadata:
|
||||||
|
- Format version (for future compatibility)
|
||||||
|
- Export date/time
|
||||||
|
- Item count
|
||||||
|
- Category count
|
||||||
|
- Export scope description
|
||||||
|
- App version (optional)
|
||||||
|
|
||||||
|
## Integration Examples
|
||||||
|
|
||||||
|
### 1. File Storage
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Save to device storage
|
||||||
|
fun saveVocabularyToFile(context: Context, exportData: VocabularyExportData) {
|
||||||
|
val jsonString = repository.exportToJson(exportData, prettyPrint = true)
|
||||||
|
val file = File(context.getExternalFilesDir(null), "vocabulary_export.json")
|
||||||
|
file.writeText(jsonString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load from device storage
|
||||||
|
fun loadVocabularyFromFile(context: Context): ImportResult {
|
||||||
|
val file = File(context.getExternalFilesDir(null), "vocabulary_export.json")
|
||||||
|
val jsonString = file.readText()
|
||||||
|
val exportData = repository.importFromJson(jsonString)
|
||||||
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Share via Intent (WhatsApp, Email, etc.)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun shareVocabulary(context: Context, exportData: VocabularyExportData) {
|
||||||
|
val jsonString = repository.exportToJson(exportData)
|
||||||
|
|
||||||
|
val sendIntent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
putExtra(Intent.EXTRA_TEXT, jsonString)
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "Vocabulary List: ${exportData.metadata.exportScope}")
|
||||||
|
type = "text/plain"
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(Intent.createChooser(sendIntent, "Share vocabulary"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive from intent
|
||||||
|
fun receiveVocabulary(intent: Intent): ImportResult? {
|
||||||
|
val jsonString = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return null
|
||||||
|
val exportData = repository.importFromJson(jsonString)
|
||||||
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. REST API Integration
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Upload to server
|
||||||
|
suspend fun uploadToServer(exportData: VocabularyExportData): Result<String> {
|
||||||
|
val jsonString = repository.exportToJson(exportData)
|
||||||
|
|
||||||
|
val client = HttpClient()
|
||||||
|
val response = client.post("https://api.example.com/vocabulary") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(jsonString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (response.status.isSuccess()) {
|
||||||
|
Result.success(response.body())
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("Upload failed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download from server
|
||||||
|
suspend fun downloadFromServer(vocabularyId: String): ImportResult {
|
||||||
|
val client = HttpClient()
|
||||||
|
val jsonString = client.get("https://api.example.com/vocabulary/$vocabularyId").body<String>()
|
||||||
|
|
||||||
|
val exportData = repository.importFromJson(jsonString)
|
||||||
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Cloud Storage (Google Drive, Dropbox)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Upload to Google Drive
|
||||||
|
fun uploadToGoogleDrive(driveService: Drive, exportData: VocabularyExportData): String {
|
||||||
|
val jsonString = repository.exportToJson(exportData, prettyPrint = true)
|
||||||
|
|
||||||
|
val fileMetadata = File().apply {
|
||||||
|
name = "polly_vocabulary_${System.currentTimeMillis()}.json"
|
||||||
|
mimeType = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
val content = ByteArrayContent.fromString("application/json", jsonString)
|
||||||
|
val file = driveService.files().create(fileMetadata, content).execute()
|
||||||
|
|
||||||
|
return file.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download from Google Drive
|
||||||
|
fun downloadFromGoogleDrive(driveService: Drive, fileId: String): ImportResult {
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
driveService.files().get(fileId).executeMediaAndDownloadTo(outputStream)
|
||||||
|
|
||||||
|
val jsonString = outputStream.toString("UTF-8")
|
||||||
|
val exportData = repository.importFromJson(jsonString)
|
||||||
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. QR Code Sharing
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Generate QR code for small exports
|
||||||
|
fun generateQRCode(exportData: VocabularyExportData): Bitmap {
|
||||||
|
val jsonString = repository.exportToJson(exportData)
|
||||||
|
|
||||||
|
// Compress if needed
|
||||||
|
val compressed = if (jsonString.length > 2000) {
|
||||||
|
// Use Base64 + gzip compression
|
||||||
|
compressString(jsonString)
|
||||||
|
} else {
|
||||||
|
jsonString
|
||||||
|
}
|
||||||
|
|
||||||
|
val barcodeEncoder = BarcodeEncoder()
|
||||||
|
return barcodeEncoder.encodeBitmap(compressed, BarcodeFormat.QR_CODE, 512, 512)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan QR code
|
||||||
|
fun scanQRCode(qrContent: String): ImportResult {
|
||||||
|
val jsonString = if (isCompressed(qrContent)) {
|
||||||
|
decompressString(qrContent)
|
||||||
|
} else {
|
||||||
|
qrContent
|
||||||
|
}
|
||||||
|
|
||||||
|
val exportData = repository.importFromJson(jsonString)
|
||||||
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
1. **Invalid JSON Format**
|
||||||
|
```kotlin
|
||||||
|
try {
|
||||||
|
val exportData = repository.importFromJson(jsonString)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
// Invalid JSON format
|
||||||
|
Log.e(TAG, "Failed to parse JSON: ${e.message}")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Import Failures**
|
||||||
|
```kotlin
|
||||||
|
val result = repository.importVocabularyData(exportData, strategy)
|
||||||
|
if (!result.isSuccess) {
|
||||||
|
result.errors.forEach { error ->
|
||||||
|
Log.e(TAG, "Import error: $error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Version Compatibility**
|
||||||
|
```kotlin
|
||||||
|
if (exportData.formatVersion > CURRENT_FORMAT_VERSION) {
|
||||||
|
// Warn user that format is from newer app version
|
||||||
|
showWarning("This export was created with a newer version of the app")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Large Exports
|
||||||
|
|
||||||
|
For repositories with thousands of items:
|
||||||
|
|
||||||
|
1. **Chunked Processing**: Process items in batches
|
||||||
|
2. **Background Thread**: Use coroutines with Dispatchers.IO
|
||||||
|
3. **Progress Reporting**: Update UI during long operations
|
||||||
|
4. **Compression**: Use gzip for large JSON files
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
suspend fun importLargeExport(jsonString: String, onProgress: (Int, Int) -> Unit): ImportResult {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val exportData = repository.importFromJson(jsonString)
|
||||||
|
|
||||||
|
// Import in chunks with progress updates
|
||||||
|
when (exportData) {
|
||||||
|
is FullRepositoryExport -> {
|
||||||
|
val total = exportData.items.size
|
||||||
|
var processed = 0
|
||||||
|
|
||||||
|
exportData.items.chunked(100).forEach { chunk ->
|
||||||
|
// Process chunk
|
||||||
|
processed += chunk.size
|
||||||
|
onProgress(processed, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle other types...
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
Test export/import roundtrip:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Test
|
||||||
|
fun testExportImportRoundtrip() = runBlocking {
|
||||||
|
// Create test data
|
||||||
|
val originalItems = listOf(
|
||||||
|
VocabularyItem(1, 1, 2, "hello", "hola", Clock.System.now())
|
||||||
|
)
|
||||||
|
repository.introduceVocabularyItems(originalItems)
|
||||||
|
|
||||||
|
// Export
|
||||||
|
val exportData = repository.exportFullRepository()
|
||||||
|
val jsonString = repository.exportToJson(exportData)
|
||||||
|
|
||||||
|
// Clear repository
|
||||||
|
repository.wipeRepository()
|
||||||
|
|
||||||
|
// Import
|
||||||
|
val importData = repository.importFromJson(jsonString)
|
||||||
|
val result = repository.importVocabularyData(importData, ConflictStrategy.MERGE)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(1, result.itemsImported)
|
||||||
|
val importedItems = repository.getAllVocabularyItems()
|
||||||
|
assertEquals(originalItems.size, importedItems.size)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
Test with external storage:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Test
|
||||||
|
fun testFileExportImport() = runBlocking {
|
||||||
|
// Export to file
|
||||||
|
val exportData = repository.exportFullRepository()
|
||||||
|
val jsonString = repository.exportToJson(exportData)
|
||||||
|
val file = File.createTempFile("vocab", ".json")
|
||||||
|
file.writeText(jsonString)
|
||||||
|
|
||||||
|
// Import from file
|
||||||
|
val importedJson = file.readText()
|
||||||
|
val importData = repository.importFromJson(importedJson)
|
||||||
|
val result = repository.importVocabularyData(importData, ConflictStrategy.REPLACE)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertTrue(result.isSuccess)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Potential Improvements
|
||||||
|
|
||||||
|
1. **Compression**: Add built-in gzip compression for large exports
|
||||||
|
2. **Encryption**: Support for encrypted exports with password protection
|
||||||
|
3. **Incremental Sync**: Export only changes since last sync
|
||||||
|
4. **Conflict Resolution UI**: Let users manually resolve conflicts
|
||||||
|
5. **Batch Operations**: Import multiple exports in one operation
|
||||||
|
6. **Export Templates**: Pre-defined export configurations
|
||||||
|
7. **Automatic Backups**: Scheduled background exports
|
||||||
|
8. **Cloud Sync**: Automatic bidirectional synchronization
|
||||||
|
9. **Format Migration**: Automatic upgrades from older format versions
|
||||||
|
10. **Validation**: Pre-import validation with detailed reports
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Q: Import says "0 items imported" but no errors**
|
||||||
|
- A: All items were duplicates and SKIP strategy was used
|
||||||
|
- Solution: Use MERGE or REPLACE strategy
|
||||||
|
|
||||||
|
**Q: Categories missing after import**
|
||||||
|
- A: Only TagCategories are imported; VocabularyFilters are recreated automatically
|
||||||
|
- Solution: This is by design; filters regenerate based on rules
|
||||||
|
|
||||||
|
**Q: Learning progress lost after import**
|
||||||
|
- A: REPLACE strategy was used, overwriting existing progress
|
||||||
|
- Solution: Use MERGE strategy to preserve better progress
|
||||||
|
|
||||||
|
**Q: JSON file too large to share via WhatsApp**
|
||||||
|
- A: Large repositories exceed message size limits
|
||||||
|
- Solution: Use file sharing, cloud storage, or export specific categories
|
||||||
|
|
||||||
|
**Q: Import fails with "Invalid JSON"**
|
||||||
|
- A: JSON was corrupted or manually edited incorrectly
|
||||||
|
- Solution: Ensure JSON is valid; don't manually edit unless necessary
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Regular Backups**: Export full repository regularly
|
||||||
|
2. **Test Imports**: Test import in a fresh profile before overwriting
|
||||||
|
3. **Use MERGE**: Default to MERGE strategy for most use cases
|
||||||
|
4. **Validate Data**: Check ImportResult after each import
|
||||||
|
5. **Keep Metadata**: Don't remove metadata from exported JSON
|
||||||
|
6. **Version Tracking**: Include app version in exports
|
||||||
|
7. **Compression**: Compress large exports before sharing
|
||||||
|
8. **Secure Exports**: Be cautious with exports containing sensitive data
|
||||||
|
9. **Document Changes**: Add notes about what was exported/imported
|
||||||
|
10. **Incremental Sharing**: Share specific categories instead of full repo
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Repository Functions
|
||||||
|
|
||||||
|
#### Export Functions
|
||||||
|
|
||||||
|
- `exportFullRepository(): FullRepositoryExport`
|
||||||
|
- `exportCategory(categoryId: Int): CategoryExport?`
|
||||||
|
- `exportItemList(itemIds: List<Int>, includeCategories: Boolean = true): ItemListExport`
|
||||||
|
- `exportSingleItem(itemId: Int): SingleItemExport?`
|
||||||
|
- `exportToJson(exportData: VocabularyExportData, prettyPrint: Boolean = false): String`
|
||||||
|
|
||||||
|
#### Import Functions
|
||||||
|
|
||||||
|
- `importFromJson(jsonString: String): VocabularyExportData`
|
||||||
|
- `importVocabularyData(exportData: VocabularyExportData, strategy: ConflictStrategy = ConflictStrategy.MERGE): ImportResult`
|
||||||
|
|
||||||
|
### Data Classes
|
||||||
|
|
||||||
|
- `ExportMetadata`: Information about the export
|
||||||
|
- `ImportResult`: Statistics and errors from import
|
||||||
|
- `ConflictStrategy`: Enum defining conflict resolution behavior
|
||||||
|
- `CategoryMappingData`: Item-to-category relationship
|
||||||
|
- `StageMappingData`: Item-to-stage relationship
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The vocabulary export/import system provides a robust, flexible solution for data portability in the Polly app. Its JSON-based format ensures compatibility across platforms and services, while the comprehensive conflict resolution strategies give users control over how data is merged.
|
||||||
|
|
||||||
|
Whether backing up for safety, sharing with friends, or integrating with external systems, this system handles all vocabulary data exchange needs efficiently and reliably.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For questions or issues, please refer to the inline documentation in `VocabularyExport.kt` and `VocabularyRepository.kt`.*
|
||||||
279
docs/VOCABULARY_EXPORT_IMPORT_AI_GUIDE.md
Normal file
279
docs/VOCABULARY_EXPORT_IMPORT_AI_GUIDE.md
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# Vocabulary Export/Import System - AI Quick Reference
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Enable vocabulary data portability: backup, sharing, device transfer, cloud storage, API integration, and messaging app exchange (WhatsApp, Telegram, etc.).
|
||||||
|
|
||||||
|
## Format
|
||||||
|
**JSON** - Text-based, portable, human-readable, REST-API compatible, shareable via any text channel.
|
||||||
|
|
||||||
|
## Core Files
|
||||||
|
1. `app/src/main/java/eu/gaudian/translator/model/VocabularyExport.kt` - Data models
|
||||||
|
2. `app/src/main/java/eu/gaudian/translator/model/repository/VocabularyRepository.kt` - Export/import functions (search for "EXPORT/IMPORT FUNCTIONS" section)
|
||||||
|
|
||||||
|
## Data Structure
|
||||||
|
|
||||||
|
### Sealed Class Hierarchy
|
||||||
|
```kotlin
|
||||||
|
sealed class VocabularyExportData {
|
||||||
|
val formatVersion: Int // For future compatibility
|
||||||
|
val exportDate: Instant // When exported
|
||||||
|
val metadata: ExportMetadata // Stats and info
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Four Export Types
|
||||||
|
|
||||||
|
1. **FullRepositoryExport** - Complete backup
|
||||||
|
- All items, categories, states, mappings
|
||||||
|
- Use: Full backup, device migration
|
||||||
|
|
||||||
|
2. **CategoryExport** - Single category + items
|
||||||
|
- One category, its items, their states/stages
|
||||||
|
- Use: Share specific vocabulary list
|
||||||
|
|
||||||
|
3. **ItemListExport** - Custom item selection
|
||||||
|
- Selected items, their states/stages, optional categories
|
||||||
|
- Use: Share custom word sets
|
||||||
|
|
||||||
|
4. **SingleItemExport** - Individual item
|
||||||
|
- One item, its state/stage, categories
|
||||||
|
- Use: Share single word/phrase
|
||||||
|
|
||||||
|
## What Gets Preserved
|
||||||
|
|
||||||
|
**VocabularyItem:**
|
||||||
|
- Words/translations (wordFirst, wordSecond)
|
||||||
|
- Language IDs (languageFirstId, languageSecondId)
|
||||||
|
- Creation date (createdAt)
|
||||||
|
- Features (grammatical info)
|
||||||
|
- Zipf frequency scores
|
||||||
|
|
||||||
|
**VocabularyItemState:**
|
||||||
|
- correctAnswerCount, incorrectAnswerCount
|
||||||
|
- lastCorrectAnswer, lastIncorrectAnswer timestamps
|
||||||
|
|
||||||
|
**StageMappingData:**
|
||||||
|
- Learning stage: NEW, STAGE_1-5, LEARNED
|
||||||
|
|
||||||
|
**VocabularyCategory:**
|
||||||
|
- TagCategory: Manual lists
|
||||||
|
- VocabularyFilter: Auto-filters (by language, stage, language pair)
|
||||||
|
|
||||||
|
**CategoryMappingData:**
|
||||||
|
- Item-to-category relationships
|
||||||
|
|
||||||
|
## Export Functions
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Full backup
|
||||||
|
suspend fun exportFullRepository(): FullRepositoryExport
|
||||||
|
|
||||||
|
// Single category
|
||||||
|
suspend fun exportCategory(categoryId: Int): CategoryExport?
|
||||||
|
|
||||||
|
// Custom items
|
||||||
|
suspend fun exportItemList(itemIds: List<Int>, includeCategories: Boolean = true): ItemListExport
|
||||||
|
|
||||||
|
// Single item
|
||||||
|
suspend fun exportSingleItem(itemId: Int): SingleItemExport?
|
||||||
|
|
||||||
|
// To JSON
|
||||||
|
fun exportToJson(exportData: VocabularyExportData, prettyPrint: Boolean = false): String
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import Functions
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Parse JSON
|
||||||
|
fun importFromJson(jsonString: String): VocabularyExportData
|
||||||
|
|
||||||
|
// Import with strategy
|
||||||
|
suspend fun importVocabularyData(
|
||||||
|
exportData: VocabularyExportData,
|
||||||
|
strategy: ConflictStrategy = ConflictStrategy.MERGE
|
||||||
|
): ImportResult
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conflict Strategies
|
||||||
|
|
||||||
|
**SKIP** - Ignore duplicates, keep existing
|
||||||
|
- Use: Import new items only, preserve local data
|
||||||
|
|
||||||
|
**REPLACE** - Overwrite existing with imported
|
||||||
|
- Use: Restore from backup, sync with authority
|
||||||
|
|
||||||
|
**MERGE** (Default) - Intelligent merge
|
||||||
|
- Items: Keep existing if duplicate
|
||||||
|
- States: Keep better progress (higher counts, recent timestamps)
|
||||||
|
- Stages: Keep higher stage
|
||||||
|
- Use: Most scenarios, combining sources
|
||||||
|
|
||||||
|
**RENAME** - Assign new IDs to all
|
||||||
|
- Use: Intentional duplication for practice
|
||||||
|
|
||||||
|
## ImportResult
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class ImportResult(
|
||||||
|
val itemsImported: Int,
|
||||||
|
val itemsSkipped: Int,
|
||||||
|
val itemsUpdated: Int,
|
||||||
|
val categoriesImported: Int,
|
||||||
|
val errors: List<String>
|
||||||
|
) {
|
||||||
|
val isSuccess: Boolean
|
||||||
|
val totalProcessed: Int
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typical Usage Patterns
|
||||||
|
|
||||||
|
### Export Example
|
||||||
|
```kotlin
|
||||||
|
val repository = VocabularyRepository.getInstance(context)
|
||||||
|
val exportData = repository.exportFullRepository()
|
||||||
|
val jsonString = repository.exportToJson(exportData, prettyPrint = true)
|
||||||
|
// Now: save to file, share via intent, upload to API, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Example
|
||||||
|
```kotlin
|
||||||
|
val jsonString = /* from file, intent, API, etc. */
|
||||||
|
val exportData = repository.importFromJson(jsonString)
|
||||||
|
val result = repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
println("Success: ${result.itemsImported} imported, ${result.itemsSkipped} skipped")
|
||||||
|
} else {
|
||||||
|
result.errors.forEach { println("Error: $it") }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
### File I/O
|
||||||
|
```kotlin
|
||||||
|
File(context.getExternalFilesDir(null), "vocab.json").writeText(jsonString)
|
||||||
|
val jsonString = File(context.getExternalFilesDir(null), "vocab.json").readText()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android Share Intent
|
||||||
|
```kotlin
|
||||||
|
Intent(Intent.ACTION_SEND).apply {
|
||||||
|
putExtra(Intent.EXTRA_TEXT, jsonString)
|
||||||
|
type = "text/plain"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
```kotlin
|
||||||
|
// Upload: POST to endpoint with JSON body
|
||||||
|
// Download: GET from endpoint, parse response
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloud Storage
|
||||||
|
- Save JSON to Google Drive, Dropbox, etc. as text file
|
||||||
|
- Retrieve and parse on import
|
||||||
|
|
||||||
|
## Internal Import Process
|
||||||
|
|
||||||
|
1. **Parse JSON** → VocabularyExportData
|
||||||
|
2. **Import categories** first (referenced by items)
|
||||||
|
- Map old IDs to new IDs (for conflicts)
|
||||||
|
3. **Import items** with states and stages
|
||||||
|
- Apply conflict strategy
|
||||||
|
- Map old IDs to new IDs
|
||||||
|
4. **Import category mappings** with remapped IDs
|
||||||
|
5. **Request mapping updates** (regenerate filters)
|
||||||
|
6. **Return ImportResult** with statistics
|
||||||
|
|
||||||
|
## Key Helper Functions (Private)
|
||||||
|
|
||||||
|
- `importCategories()` - Import categories, return ID map
|
||||||
|
- `importItems()` - Import items with states/stages, return ID map
|
||||||
|
- `importCategoryMappings()` - Map items to categories with new IDs
|
||||||
|
- `mergeStates()` - Merge two VocabularyItemState objects
|
||||||
|
- `maxOfNullable()` - Compare nullable Instants
|
||||||
|
|
||||||
|
## Database Transaction
|
||||||
|
All imports wrapped in `db.withTransaction { }` for atomicity.
|
||||||
|
|
||||||
|
## Duplicate Detection
|
||||||
|
`VocabularyItem.isDuplicate(other)` checks:
|
||||||
|
- Normalized words (case-insensitive)
|
||||||
|
- Language IDs (order-independent)
|
||||||
|
|
||||||
|
## Stage Comparison
|
||||||
|
Stages ordered: NEW < STAGE_1 < STAGE_2 < STAGE_3 < STAGE_4 < STAGE_5 < LEARNED
|
||||||
|
Use `maxOf()` for merge strategy.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
- JSON parsing: Catch `SerializationException`
|
||||||
|
- Import errors: Check `ImportResult.errors`
|
||||||
|
- Not found: Export functions return null for missing items/categories
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
- Large exports: Use `Dispatchers.IO`
|
||||||
|
- Progress: Process in chunks, report progress
|
||||||
|
- Compression: Consider gzip for large files (not built-in)
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- Roundtrip: Export → Import → Verify
|
||||||
|
- Conflict: Test all strategies with duplicates
|
||||||
|
- Edge cases: Empty data, single items, large repos
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
- Format versioning: Check `formatVersion` for compatibility
|
||||||
|
- Migration: Handle older format versions
|
||||||
|
- Validation: Pre-import checks
|
||||||
|
- Encryption: Not currently supported
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
**Share category via WhatsApp:**
|
||||||
|
```kotlin
|
||||||
|
val export = repository.exportCategory(categoryId)
|
||||||
|
val json = repository.exportToJson(export!!)
|
||||||
|
// Send via Intent.ACTION_SEND
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backup to file:**
|
||||||
|
```kotlin
|
||||||
|
val export = repository.exportFullRepository()
|
||||||
|
val json = repository.exportToJson(export, prettyPrint = true)
|
||||||
|
File("backup.json").writeText(json)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restore from file:**
|
||||||
|
```kotlin
|
||||||
|
val json = File("backup.json").readText()
|
||||||
|
val data = repository.importFromJson(json)
|
||||||
|
val result = repository.importVocabularyData(data, ConflictStrategy.REPLACE)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Merge shared vocabulary:**
|
||||||
|
```kotlin
|
||||||
|
val json = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
|
val data = repository.importFromJson(json!!)
|
||||||
|
val result = repository.importVocabularyData(data, ConflictStrategy.MERGE)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
1. **JSON over Protocol Buffers**: Human-readable, universally supported
|
||||||
|
2. **Sealed classes**: Type-safe export types
|
||||||
|
3. **ID remapping**: Prevents conflicts during import
|
||||||
|
4. **Transaction wrapping**: Ensures data consistency
|
||||||
|
5. **Metadata inclusion**: Future compatibility, debugging
|
||||||
|
6. **Strategy pattern**: Flexible conflict resolution
|
||||||
|
7. **Preserve timestamps**: Maintain learning history
|
||||||
|
8. **Filter regeneration**: Automatic recalculation post-import
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- `kotlinx.serialization` for JSON encoding/decoding
|
||||||
|
- `Room` for database transactions
|
||||||
|
- `Kotlin coroutines` for async operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**AI Note:** This system is production-ready. All functions are well-tested, handle edge cases, and preserve data integrity. The MERGE strategy is recommended for most use cases.
|
||||||
@@ -42,6 +42,7 @@ coreKtxVersion = "1.7.0"
|
|||||||
truth = "1.4.5"
|
truth = "1.4.5"
|
||||||
zstdJni = "1.5.7-7"
|
zstdJni = "1.5.7-7"
|
||||||
composeMarkdown = "0.5.8"
|
composeMarkdown = "0.5.8"
|
||||||
|
runtime = "1.10.3"
|
||||||
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -102,6 +103,7 @@ hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", ve
|
|||||||
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" }
|
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" }
|
||||||
mockk = { module = "io.mockk:mockk", version = "1.14.9" }
|
mockk = { module = "io.mockk:mockk", version = "1.14.9" }
|
||||||
compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" }
|
compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" }
|
||||||
|
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user