Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -6,8 +6,10 @@ import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Multipart
|
||||
import retrofit2.http.PATCH
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Part
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.Url
|
||||
@@ -17,7 +19,7 @@ import okhttp3.ResponseBody
|
||||
import okhttp3.RequestBody
|
||||
|
||||
data class ContactRequest(val name: String, val email: String, val message: String)
|
||||
data class ContactResponse(val ok: Boolean, val id: String? = null)
|
||||
data class ContactResponse(val ok: Boolean, val id: String? = null, val message: String? = null)
|
||||
data class TermineResponse(val success: Boolean = true, val termine: List<TerminDto> = emptyList())
|
||||
data class TerminDto(
|
||||
val datum: String = "",
|
||||
@@ -557,9 +559,29 @@ interface ApiService {
|
||||
@GET("/api/cms/users/list")
|
||||
suspend fun cmsUsers(): Response<CmsUsersResponse>
|
||||
|
||||
data class UpdateUserRolesRequest(val id: String, val roles: List<String>)
|
||||
data class UpdateUserActiveRequest(val id: String, val active: Boolean)
|
||||
|
||||
@PUT("/api/cms/users/update-roles")
|
||||
suspend fun updateUserRoles(@Body request: UpdateUserRolesRequest): Response<AuthMessageResponse>
|
||||
|
||||
@PUT("/api/cms/users/update-active")
|
||||
suspend fun updateUserActive(@Body request: UpdateUserActiveRequest): Response<AuthMessageResponse>
|
||||
|
||||
@POST("/api/cms/users/resend-invite")
|
||||
suspend fun resendInvite(@Query("id") id: String): Response<AuthMessageResponse>
|
||||
|
||||
@GET("/api/cms/contact-requests")
|
||||
suspend fun contactRequests(): Response<List<ContactRequestDto>>
|
||||
|
||||
data class ContactReplyRequest(val message: String)
|
||||
|
||||
@POST("/api/cms/contact-requests/{id}/reply")
|
||||
suspend fun replyToContactRequest(@Path("id") id: String, @Body request: ContactReplyRequest): Response<de.harheimertc.data.ContactResponse>
|
||||
|
||||
@PATCH("/api/cms/contact-requests/{id}/toggle-status")
|
||||
suspend fun toggleContactRequestStatus(@Path("id") id: String): Response<de.harheimertc.data.ContactResponse>
|
||||
|
||||
@GET("/api/newsletter/list")
|
||||
suspend fun newsletters(): Response<NewsletterListResponse>
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ object NetworkModule {
|
||||
cache: Cache,
|
||||
): OkHttpClient {
|
||||
val logging = HttpLoggingInterceptor()
|
||||
logging.level = HttpLoggingInterceptor.Level.BASIC
|
||||
logging.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC
|
||||
val cookies = CookieManager().apply {
|
||||
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
}
|
||||
@@ -84,8 +84,10 @@ object NetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(moshi: Moshi, client: OkHttpClient): Retrofit {
|
||||
val runtimeBase = BuildConfig.API_BASE_URL
|
||||
android.util.Log.i("NetworkModule", "Retrofit baseUrl runtime=$runtimeBase")
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.baseUrl(runtimeBase)
|
||||
.client(client)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.build()
|
||||
|
||||
@@ -44,6 +44,26 @@ class CmsRepository @Inject constructor(
|
||||
fallbackMessage = "Benutzer konnten nicht geladen werden.",
|
||||
)
|
||||
|
||||
suspend fun updateUserRoles(id: String, roles: List<String>): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val req = de.harheimertc.data.ApiService.UpdateUserRolesRequest(id, roles)
|
||||
val response = api.updateUserRoles(req)
|
||||
if (!response.isSuccessful) error("Benutzerrollen konnten nicht aktualisiert werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun updateUserActive(id: String, active: Boolean): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val req = de.harheimertc.data.ApiService.UpdateUserActiveRequest(id, active)
|
||||
val response = api.updateUserActive(req)
|
||||
if (!response.isSuccessful) error("Benutzerstatus konnte nicht aktualisiert werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun resendInvite(id: String): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.resendInvite(id)
|
||||
if (!response.isSuccessful) error("Einladung konnte nicht erneut gesendet werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun contactRequests(): Result<List<ContactRequestDto>> =
|
||||
fetchEncryptedFallback(
|
||||
load = {
|
||||
@@ -56,6 +76,19 @@ class CmsRepository @Inject constructor(
|
||||
fallbackMessage = "Kontaktanfragen konnten nicht geladen werden.",
|
||||
)
|
||||
|
||||
suspend fun replyToContactRequest(id: String, message: String): Result<de.harheimertc.data.ContactResponse> = runCatching {
|
||||
val req = ApiService.ContactReplyRequest(message)
|
||||
val response = api.replyToContactRequest(id, req)
|
||||
if (!response.isSuccessful) error("Antwort konnte nicht gesendet werden.")
|
||||
response.body() ?: de.harheimertc.data.ContactResponse(ok = false)
|
||||
}
|
||||
|
||||
suspend fun toggleContactRequestStatus(id: String): Result<de.harheimertc.data.ContactResponse> = runCatching {
|
||||
val response = api.toggleContactRequestStatus(id)
|
||||
if (!response.isSuccessful) error("Status konnte nicht geändert werden.")
|
||||
response.body() ?: de.harheimertc.data.ContactResponse(ok = false)
|
||||
}
|
||||
|
||||
suspend fun newsletters(): Result<NewsletterListResponse> =
|
||||
fetchEncryptedFallback(
|
||||
load = {
|
||||
@@ -92,6 +125,30 @@ class CmsRepository @Inject constructor(
|
||||
fallbackMessage = "Passwort-Reset-Diagnose konnte nicht geladen werden.",
|
||||
)
|
||||
|
||||
suspend fun news(): Result<de.harheimertc.data.NewsResponse> =
|
||||
fetchEncryptedFallback(
|
||||
load = {
|
||||
val response = api.memberNews()
|
||||
if (!response.isSuccessful) error("News konnten nicht geladen werden.")
|
||||
response.body() ?: de.harheimertc.data.NewsResponse()
|
||||
},
|
||||
save = cache::putNews,
|
||||
cached = cache::getNews,
|
||||
fallbackMessage = "News konnten nicht geladen werden.",
|
||||
)
|
||||
|
||||
suspend fun saveNews(request: de.harheimertc.data.NewsSaveRequest): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.saveNews(request)
|
||||
if (!response.isSuccessful) error("News konnten nicht gespeichert werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun deleteNews(id: Int): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.deleteNews(id)
|
||||
if (!response.isSuccessful) error("News konnten nicht gelöscht werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
private suspend fun <T> fetchEncryptedFallback(
|
||||
load: suspend () -> T,
|
||||
save: (T) -> Unit,
|
||||
|
||||
@@ -40,8 +40,19 @@ class MemberAreaRepository @Inject constructor(
|
||||
fetchEncryptedFallback(
|
||||
load = {
|
||||
val response = api.memberNews()
|
||||
if (!response.isSuccessful) error("News konnten nicht geladen werden.")
|
||||
response.body() ?: error("Leere Antwort vom Server.")
|
||||
if (!response.isSuccessful) {
|
||||
try {
|
||||
val body = response.errorBody()?.string()
|
||||
android.util.Log.w("MemberAreaRepository", "memberNews failed: code=${response.code()} body=${body?.take(500)}")
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
error("News konnten nicht geladen werden.")
|
||||
}
|
||||
response.body() ?: run {
|
||||
android.util.Log.w("MemberAreaRepository", "memberNews: successful but empty body (null)")
|
||||
NewsResponse(success = false, news = emptyList())
|
||||
}
|
||||
},
|
||||
save = cache::putNews,
|
||||
cached = cache::getNews,
|
||||
|
||||
@@ -202,16 +202,4 @@ private fun selectedText(value: TextFieldValue): String {
|
||||
return value.text.substring(start, end)
|
||||
}
|
||||
|
||||
private fun normalizeEmptyHtml(value: String): String =
|
||||
if (stripHtml(value).isBlank() && !value.contains("<img", ignoreCase = true)) "" else value
|
||||
|
||||
private fun stripHtml(value: String): String = value
|
||||
.replace(Regex("<[^>]+>"), "")
|
||||
.replace(" ", " ")
|
||||
.trim()
|
||||
|
||||
private fun escapeHtml(value: String): String = value
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
// HTML helper functions moved to RichTextUtils.kt for reuse and testing
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.harheimertc.ui.components
|
||||
|
||||
fun normalizeEmptyHtml(value: String): String =
|
||||
if (stripHtml(value).isBlank() && !value.contains("<img", ignoreCase = true)) "" else value
|
||||
|
||||
fun stripHtml(value: String): String = value
|
||||
.replace(Regex("<[^>]+>"), "")
|
||||
.replace(" ", " ")
|
||||
.trim()
|
||||
|
||||
fun escapeHtml(value: String): String = value
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.harheimertc.ui.screens.cms
|
||||
|
||||
// Placeholder: functionality moved to CmsScreens.kt (CmsUserListPage / UserCard)
|
||||
@@ -0,0 +1,306 @@
|
||||
package de.harheimertc.ui.screens.cms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import de.harheimertc.data.NewsDto
|
||||
import de.harheimertc.data.NewsSaveRequest
|
||||
import de.harheimertc.ui.components.FormMessages
|
||||
import de.harheimertc.ui.components.NativeRichTextEditor
|
||||
import de.harheimertc.ui.navigation.Destinations
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun CmsNewsScreen(navController: NavController, showBackNavigation: Boolean, viewModel: CmsViewModel = hiltViewModel()) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
var selection by remember { mutableStateOf(setOf<Int>()) }
|
||||
val loginVm: de.harheimertc.ui.screens.login.LoginViewModel = hiltViewModel()
|
||||
val loginState by loginVm.state.collectAsState()
|
||||
val canWrite = loginState.roles.any { it == "admin" || it == "vorstand" }
|
||||
val context = LocalContext.current
|
||||
var showSuccessDialog by remember { mutableStateOf(false) }
|
||||
|
||||
androidx.compose.runtime.LaunchedEffect(state.message) {
|
||||
if (!state.message.isNullOrBlank()) showSuccessDialog = true
|
||||
}
|
||||
|
||||
// Local dialog state for create/edit + delete confirmation (hoisted)
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
var deletingIds by remember { mutableStateOf<List<Int>?>(null) }
|
||||
var editing by remember { mutableStateOf<NewsDto?>(null) }
|
||||
var title by remember { mutableStateOf("") }
|
||||
var content by remember { mutableStateOf("") }
|
||||
var isPublic by remember { mutableStateOf(false) }
|
||||
var isHidden by remember { mutableStateOf(false) }
|
||||
var expiresAt by remember { mutableStateOf("") } // format: yyyy-MM-dd'T'HH:mm
|
||||
|
||||
val dtFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
|
||||
val displayFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy, HH:mm", Locale.GERMANY)
|
||||
|
||||
fun convertUTCToLocal(utc: String?): String {
|
||||
if (utc.isNullOrBlank()) return ""
|
||||
return try {
|
||||
val instant = Instant.parse(utc)
|
||||
LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).format(dtFormatter)
|
||||
} catch (e: Exception) { "" }
|
||||
}
|
||||
|
||||
fun convertLocalToUTC(local: String?): String? {
|
||||
if (local.isNullOrBlank()) return null
|
||||
return try {
|
||||
val ldt = LocalDateTime.parse(local, dtFormatter)
|
||||
ldt.atZone(ZoneId.systemDefault()).toInstant().toString()
|
||||
} catch (e: Exception) { null }
|
||||
}
|
||||
|
||||
// open create
|
||||
fun openAdd() {
|
||||
editing = null
|
||||
title = ""
|
||||
content = ""
|
||||
isPublic = false
|
||||
isHidden = false
|
||||
expiresAt = ""
|
||||
dialogOpen = true
|
||||
}
|
||||
|
||||
// open edit
|
||||
fun openEdit(item: NewsDto) {
|
||||
editing = item
|
||||
title = item.title
|
||||
content = item.content
|
||||
isPublic = item.isPublic
|
||||
isHidden = item.isHidden
|
||||
expiresAt = convertUTCToLocal(item.expiresAt)
|
||||
dialogOpen = true
|
||||
}
|
||||
|
||||
CmsPage(navController, showBackNavigation, "News", "Interne und öffentliche News") {
|
||||
if (state.loading) item { CircularProgressIndicator() }
|
||||
|
||||
item {
|
||||
Button(onClick = { viewModel.load(); /* ensure latest */ }, modifier = Modifier.fillMaxWidth()) { Text("Neu laden") }
|
||||
}
|
||||
|
||||
item {
|
||||
if (canWrite) Button(onClick = { openAdd() }, modifier = Modifier.fillMaxWidth()) { Text("News erstellen") }
|
||||
}
|
||||
|
||||
item {
|
||||
FormMessages(state.error, state.message)
|
||||
}
|
||||
|
||||
if (!state.loading && state.news.isEmpty()) item { Text("Noch keine News vorhanden.", modifier = Modifier.padding(12.dp)) }
|
||||
|
||||
// selection state for bulk actions (moved to outer scope)
|
||||
|
||||
items(state.news) { news ->
|
||||
val selected = news.id?.let { selection.contains(it) } ?: false
|
||||
NewsListItem(news = news, selected = selected, onSelect = { id, sel ->
|
||||
id?.let {
|
||||
selection = if (sel) selection + it else selection - it
|
||||
}
|
||||
}, onEdit = { openEdit(news) }, onDelete = { news.id?.let { id -> deletingIds = listOf(id) } })
|
||||
}
|
||||
|
||||
// bulk action bar
|
||||
if (selection.isNotEmpty()) {
|
||||
item {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewModel.bulkSetPublic(selection.toList(), true) }) { Text("Als öffentlich markieren") }
|
||||
Button(onClick = { viewModel.bulkSetPublic(selection.toList(), false) }) { Text("Als nicht-öffentlich markieren") }
|
||||
Button(onClick = { viewModel.bulkSetHidden(selection.toList(), true) }) { Text("Ausblenden") }
|
||||
Button(onClick = { viewModel.bulkSetHidden(selection.toList(), false) }) { Text("Einblenden") }
|
||||
Button(onClick = { /* confirm then delete */ deletingIds = selection.toList() }) { Text("Löschen") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (moved earlier)
|
||||
|
||||
// delete confirmation dialog
|
||||
if (deletingIds != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { deletingIds = null },
|
||||
title = { Text("News löschen") },
|
||||
text = { Text("Möchten Sie die ausgewählten News wirklich löschen?") },
|
||||
confirmButton = { Button(onClick = {
|
||||
deletingIds?.let { viewModel.bulkDelete(it) }
|
||||
deletingIds = null
|
||||
selection = emptySet()
|
||||
}) { Text("Löschen") } },
|
||||
dismissButton = { TextButton(onClick = { deletingIds = null }) { Text("Abbrechen") } },
|
||||
)
|
||||
}
|
||||
|
||||
// dialog for create/edit
|
||||
if (dialogOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { dialogOpen = false },
|
||||
title = { Text(if (editing == null) "News erstellen" else "News bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(value = title, onValueChange = { title = it }, label = { Text("Titel *") }, modifier = Modifier.fillMaxWidth())
|
||||
NativeRichTextEditor(content, { content = it }, "Inhalt *")
|
||||
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Checkbox(checked = isPublic, onCheckedChange = { isPublic = it })
|
||||
Text("Öffentliche News (auf Startseite anzeigen)", modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
|
||||
if (isPublic) {
|
||||
// read-only datetime field that opens native pickers
|
||||
OutlinedTextField(
|
||||
value = expiresAt,
|
||||
onValueChange = { /* no-op: controlled by pickers */ },
|
||||
label = { Text("Ablaufdatum (optional)") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
// open date then time picker
|
||||
val now = java.util.Calendar.getInstance()
|
||||
val year = now.get(java.util.Calendar.YEAR)
|
||||
val month = now.get(java.util.Calendar.MONTH)
|
||||
val day = now.get(java.util.Calendar.DAY_OF_MONTH)
|
||||
android.app.DatePickerDialog(context, { _, y, m, d ->
|
||||
val hour = now.get(java.util.Calendar.HOUR_OF_DAY)
|
||||
val minute = now.get(java.util.Calendar.MINUTE)
|
||||
android.app.TimePickerDialog(context, { _, h, min ->
|
||||
val ldt = LocalDateTime.of(y, m + 1, d, h, min)
|
||||
expiresAt = ldt.format(dtFormatter)
|
||||
}, hour, minute, true).show()
|
||||
}, year, month, day).show()
|
||||
},
|
||||
readOnly = true,
|
||||
)
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Checkbox(checked = isHidden, onCheckedChange = { isHidden = it })
|
||||
Text("News ausblenden", modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
val err = state.error
|
||||
if (err != null) {
|
||||
Text(err, color = Color(0xFF842029))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
val req = NewsSaveRequest(
|
||||
id = editing?.id,
|
||||
title = title,
|
||||
content = content,
|
||||
isPublic = isPublic,
|
||||
isHidden = isHidden,
|
||||
expiresAt = convertLocalToUTC(expiresAt),
|
||||
)
|
||||
viewModel.saveNews(req)
|
||||
dialogOpen = false
|
||||
}, enabled = !state.saving) { Text(if (state.saving) "Speichert..." else "Speichern") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { dialogOpen = false }) { Text("Abbrechen") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showSuccessDialog && !state.message.isNullOrBlank()) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showSuccessDialog = false },
|
||||
title = { Text("Erfolg") },
|
||||
text = { Text(state.message ?: "") },
|
||||
confirmButton = { Button(onClick = { showSuccessDialog = false }) { Text("OK") } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsListItem(
|
||||
news: NewsDto,
|
||||
selected: Boolean = false,
|
||||
onSelect: (Int?, Boolean) -> Unit = { _, _ -> },
|
||||
onEdit: (NewsDto) -> Unit,
|
||||
onDelete: (Int) -> Unit,
|
||||
) {
|
||||
androidx.compose.material3.Surface(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Checkbox(checked = selected, onCheckedChange = { onSelect(news.id, it) })
|
||||
Text(news.title.ifBlank { "(Ohne Titel)" }, modifier = Modifier.padding(start = 8.dp))
|
||||
if (news.isPublic) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, modifier = Modifier.padding(start = 8.dp)) {
|
||||
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(Color(0xFF0EA5A6)))
|
||||
Text("Öffentlich", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
if (news.isHidden) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, modifier = Modifier.padding(start = 8.dp)) {
|
||||
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(Color.Gray))
|
||||
Text("Ausgeblendet", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
val expired = news.expiresAt?.let {
|
||||
try { Instant.parse(it).isBefore(Instant.now()) || Instant.parse(it).equals(Instant.now()) } catch (e: Exception) { false }
|
||||
} ?: false
|
||||
if (expired) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, modifier = Modifier.padding(start = 8.dp)) {
|
||||
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(Color(0xFFB91C1C)))
|
||||
Text("Abgelaufen", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(top = 4.dp)) {
|
||||
Text(news.author ?: "-", modifier = Modifier.padding(end = 12.dp))
|
||||
Text(news.created ?: "-")
|
||||
}
|
||||
if (news.updated != null && news.updated != news.created) {
|
||||
Text("Aktualisiert: ${news.updated}")
|
||||
}
|
||||
}
|
||||
Row {
|
||||
TextButton(onClick = { onEdit(news) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { news.id?.let { onDelete(it) } }) { Text("Löschen") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import de.harheimertc.ui.navigation.NavigationViewModel
|
||||
import androidx.navigation.NavController
|
||||
import coil.compose.AsyncImage
|
||||
import de.harheimertc.BuildConfig
|
||||
@@ -72,6 +73,8 @@ fun HomeScreen(
|
||||
showNavigationHeader: Boolean = true,
|
||||
viewModel: HomeViewModel = hiltViewModel(),
|
||||
) {
|
||||
val navigationViewModel: NavigationViewModel = hiltViewModel()
|
||||
val navigationState by navigationViewModel.state.collectAsState()
|
||||
val state by viewModel.state.collectAsState()
|
||||
var selectedNews by remember { mutableStateOf<NewsDto?>(null) }
|
||||
|
||||
@@ -97,6 +100,7 @@ fun HomeScreen(
|
||||
AppNavigationHeader(
|
||||
selectedRoute = Destinations.Home.route,
|
||||
onNavigate = navController::navigate,
|
||||
navigationState = navigationState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.harheimertc.ui.util
|
||||
|
||||
import java.net.UnknownHostException
|
||||
|
||||
object ErrorMapper {
|
||||
fun mapError(t: Throwable?): String? {
|
||||
if (t == null) return null
|
||||
return when (t) {
|
||||
is UnknownHostException -> "Server nicht erreichbar. Prüfe Netzwerkverbindung."
|
||||
else -> {
|
||||
val msg = t.message
|
||||
when {
|
||||
msg == null -> "Unbekannter Fehler"
|
||||
msg.contains("401") || msg.contains("Unauthorized", ignoreCase = true) -> "Nicht autorisiert"
|
||||
msg.contains("timeout", ignoreCase = true) -> "Zeitüberschreitung beim Server"
|
||||
else -> msg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
<resources>
|
||||
<string name="app_name">Harheimer TC</string>
|
||||
<string name="gallery_title">Photo gallery</string>
|
||||
<string name="gallery_empty">There are no images in the gallery yet.</string>
|
||||
<string name="gallery_upload_title">Upload image</string>
|
||||
<string name="gallery_upload_show">Open</string>
|
||||
<string name="gallery_upload_hide">Close</string>
|
||||
<string name="gallery_upload_choose_file">Select image file</string>
|
||||
<string name="gallery_upload_image_title">Title</string>
|
||||
<string name="gallery_upload_description">Description (optional)</string>
|
||||
<string name="gallery_upload_public">Publicly visible</string>
|
||||
<string name="gallery_upload_submit">Upload image</string>
|
||||
<string name="gallery_uploading">Uploading...</string>
|
||||
<string name="gallery_close_image">Close image</string>
|
||||
<string name="gallery_image_description">Gallery image: %1$s</string>
|
||||
<string name="gallery_title">Bildergalerie</string>
|
||||
<string name="gallery_empty">Noch keine Bilder in der Galerie.</string>
|
||||
<string name="gallery_upload_title">Bild hochladen</string>
|
||||
<string name="gallery_upload_show">Öffnen</string>
|
||||
<string name="gallery_upload_hide">Schließen</string>
|
||||
<string name="gallery_upload_choose_file">Bilddatei auswählen</string>
|
||||
<string name="gallery_upload_image_title">Titel</string>
|
||||
<string name="gallery_upload_description">Beschreibung (optional)</string>
|
||||
<string name="gallery_upload_public">Öffentlich sichtbar</string>
|
||||
<string name="gallery_upload_submit">Bild hochladen</string>
|
||||
<string name="gallery_uploading">Wird hochgeladen...</string>
|
||||
<string name="gallery_close_image">Bild schließen</string>
|
||||
<string name="gallery_image_description">Galeriebild: %1$s</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user