Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Regélő – Android alkalmazás (v1.3.4)
# Regélő – Android alkalmazás (v1.3.5)

Magyar írói és olvasói platform (Wattpad / Inkitt jellegű), Kotlin + Jetpack Compose.

Expand All @@ -23,7 +23,7 @@ Google Playre aláírt Android App Bundle-t (AAB) tölts fel, ne fejlesztői deb
4. Készítsd el az AAB-t, ellenőrizd a Play Console belső tesztcsatornáján, majd csak sikeres ellenőrzés után
indítsd a szélesebb körű kiadást.
5. Minden új feltöltés előtt növeld az `app/build.gradle.kts` fájlban a `versionCode` értékét. Jelenlegi értéke
`134`; ez azonos vagy kisebb értékkel nem tölthető fel frissítésként.
`135`; ez azonos vagy kisebb értékkel nem tölthető fel frissítésként.

### Parancssoros/CI aláírás
Biztonságos build/CI titokként add meg mind a négy környezeti változót:
Expand Down Expand Up @@ -90,6 +90,9 @@ Hivatalos e-mail: regeloapp@gmail.com · Hivatalos profil az appban: @regelohiva
- **1.3.4 – Írói eszközök az Írás fülön** – A saját könyvek és piszkozatok, új könyv létrehozása, fájlimport,
sorozatok, szerkesztés, előnézet, világépítés, statisztika, exportálás és törlés egy helyre, az Írás fülre került.
A Profil fülön maradnak a profil- és fiókbeállítások, a közösségi funkciók és a személyes adatok kezelése.
- **1.3.5 – Saját művek a Profilon** – A saját profil új „Műveim” részén külön választhatók a publikált művek,
piszkozatok és sorozatok. A saját történetek megnyithatók vagy szerkeszthetők, a sorozatok adatlapja és kezelése
közvetlenül elérhető; mások profilján privát írói tartalom nem jelenik meg.
- **v1.4 backend-kiegészítés** – A könyv befejezésének mentése szerveroldali, idempotens művelet,
amely a budapesti naptári évet és az adatbázisban szereplő publikált/befejezett állapotot használja.
A rate limit párhuzamos kéréseknél is atomikusan érvényesül. A kliens nem ment tokent titkosítatlanul,
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ android {
applicationId = "hu.regelo.app"
minSdk = 26
targetSdk = 37
versionCode = 134
versionName = "1.3.4"
versionCode = 135
versionName = "1.3.5"

buildConfigField("String", "SUPABASE_URL", "\"https://pqmfezkvaslqedalwwnn.supabase.co\"")
// Nyilvános (publishable) kulcs – a sorszintű jogosultságok (RLS) védik az adatokat.
Expand Down
148 changes: 148 additions & 0 deletions app/src/main/java/hu/regelo/app/ui/screens/ProfileScreens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
Expand All @@ -76,15 +77,19 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import hu.regelo.app.AppGraph
import hu.regelo.app.data.BookCard
import hu.regelo.app.data.LocalBook
import hu.regelo.app.data.ProfileDto
import hu.regelo.app.data.ProfileLink
import hu.regelo.app.data.ProfileRepository
import hu.regelo.app.data.ProfileStats
import hu.regelo.app.data.ReadingList
import hu.regelo.app.data.SeriesDto
import hu.regelo.app.data.PublishStatus
import hu.regelo.app.data.StorageRepository
import hu.regelo.app.ui.components.Avatar
import hu.regelo.app.ui.components.BackTopBar
Expand All @@ -105,12 +110,14 @@ import hu.regelo.app.ui.components.rememberLoader
import hu.regelo.app.ui.components.runAction
import hu.regelo.app.ui.nav.Navigator
import hu.regelo.app.ui.nav.Route
import hu.regelo.app.ui.nav.Tab
import hu.regelo.app.ui.theme.Ink
import hu.regelo.app.ui.theme.Rust
import hu.regelo.app.util.Images
import hu.regelo.app.util.friendlyMessage
import hu.regelo.app.util.nowMs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import hu.regelo.app.util.openSafe
Expand Down Expand Up @@ -166,8 +173,29 @@ private fun ProfileContent(nav: Navigator, userId: String, isMe: Boolean) {
var postNews by remember { mutableStateOf(false) }
var deleteNews by remember { mutableStateOf<String?>(null) }
val myProfile by AppGraph.profiles.me.collectAsState()
val myBooks by AppGraph.writing.myBooks().collectAsState(initial = emptyList())
var mySeries by remember { mutableStateOf<List<SeriesDto>?>(null) }
var mySeriesError by remember { mutableStateOf(false) }
var mySeriesLoadAttempt by remember { mutableStateOf(0) }
var myWorksTab by remember { mutableStateOf(0) }
val loader = rememberLoader(userId, if (isMe) myProfile else null) { loadProfile(userId) }

LaunchedEffect(isMe, userId, mySeriesLoadAttempt) {
if (isMe) {
mySeries = null
mySeriesError = false
try {
mySeries = AppGraph.writing.mySeries()
} catch (e: Exception) {
if (e is CancellationException) throw e
hu.regelo.app.util.ErrorReporter.report(e, "ProfileScreens.mySeries")
mySeriesError = true
}
} else {
mySeries = null
}
}

Column(Modifier.fillMaxSize()) {
if (!isMe || nav.stack.size > 1) {
BackTopBar("", onBack = { nav.back() }) {
Expand Down Expand Up @@ -259,6 +287,99 @@ private fun ProfileContent(nav: Navigator, userId: String, isMe: Boolean) {
}
}
item(key = "badges-$userId") { hu.regelo.app.ui.components.BadgesSection(userId, isMe) }
if (isMe) {
item(key = "my-works-title") { SectionTitle("Műveim") }
item(key = "my-works-tabs") {
FlowRow(
Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
FilterChip(
selected = myWorksTab == 0,
onClick = { myWorksTab = 0 },
label = { Text("Publikált (${myBooks.count { it.publishStatus == PublishStatus.PUBLISHED && !it.deleted }})") },
)
FilterChip(
selected = myWorksTab == 1,
onClick = { myWorksTab = 1 },
label = { Text("Piszkozatok (${myBooks.count { it.publishStatus == PublishStatus.DRAFT && !it.deleted }})") },
)
FilterChip(
selected = myWorksTab == 2,
onClick = { myWorksTab = 2 },
label = { Text("Sorozataim (${mySeries.orEmpty().size})") },
)
}
}
when (myWorksTab) {
0, 1 -> {
val wantedStatus = if (myWorksTab == 0) PublishStatus.PUBLISHED else PublishStatus.DRAFT
val books = myBooks.filter { !it.deleted && it.publishStatus == wantedStatus }
if (books.isEmpty()) {
item(key = "my-works-empty-$myWorksTab") {
Text(
if (myWorksTab == 0) "Még nincs publikált műved." else "Még nincs piszkozatod.",
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} else {
items(books, key = { "profile-work-${it.id}" }) { book ->
ProfileWorkCard(book, onOpen = {
if (book.publishStatus == PublishStatus.PUBLISHED) {
nav.push(Route.BookDetail(book.id))
} else {
nav.open(Tab.WRITE, Route.BookEditor(book.id))
}
}, onEdit = { nav.open(Tab.WRITE, Route.BookEditor(book.id)) })
}
}
}
else -> {
val series = mySeries
when {
mySeriesError -> item(key = "my-series-error") {
Column(Modifier.padding(horizontal = 20.dp, vertical = 8.dp)) {
Text("A sorozataid betöltése nem sikerült.", color = MaterialTheme.colorScheme.error)
TextButton(onClick = { mySeriesLoadAttempt++ }) { Text("Újrapróbálás") }
}
}
series == null -> item(key = "my-series-loading") {
Text("Sorozatok betöltése…", modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp))
}
series.isEmpty() -> item(key = "my-series-empty") {
Text("Még nincs sorozatod.", modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp), color = MaterialTheme.colorScheme.onSurfaceVariant)
}
else -> items(series, key = { "profile-series-${it.id}" }) { s ->
Card(
onClick = { nav.open(Tab.WRITE, Route.SeriesDetail(s.id)) },
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
) {
Column(Modifier.padding(16.dp)) {
Text(s.name, style = MaterialTheme.typography.titleMedium)
Text(
"${myBooks.count { it.seriesId == s.id && !it.deleted }} kötet",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (s.description.isNotBlank()) {
Text(s.description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
}
}
item(key = "manage-my-series") {
OutlinedButton(
onClick = { nav.open(Tab.WRITE, Route.SeriesManager) },
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
) { Text("Sorozatok kezelése") }
}
}
}
}
if (d.news.isNotEmpty()) {
item { SectionTitle("Hírek") }
items(d.news, key = { "n-" + it.id }) { a ->
Expand Down Expand Up @@ -323,6 +444,33 @@ private fun ProfileContent(nav: Navigator, userId: String, isMe: Boolean) {
}
}

@Composable
private fun ProfileWorkCard(book: LocalBook, onOpen: () -> Unit, onEdit: () -> Unit) {
Card(
onClick = onOpen,
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
) {
Row(
Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = 12.dp, bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
Text(book.title, style = MaterialTheme.typography.titleMedium, maxLines = 2, overflow = TextOverflow.Ellipsis)
Spacer(Modifier.height(4.dp))
Text(
PublishStatus.label(book.publishStatus),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
IconButton(onClick = onEdit) {
Icon(Icons.Default.Edit, contentDescription = "Szerkesztés")
}
}
}
}

@Composable
private fun StatItem(value: String, label: String, onClick: (() -> Unit)? = null) {
Column(Modifier.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)) {
Expand Down
13 changes: 13 additions & 0 deletions app/src/test/java/hu/regelo/app/NavigatorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,17 @@ class NavigatorTest {
assertEquals(true, nav.back())
assertEquals(Route.Root(Tab.WRITE), nav.current)
}

@Test
fun openingOwnWorkFromProfileUsesWriteTabAndKeepsEditorBackStack() {
val nav = Navigator()
nav.select(Tab.ME)

nav.open(Tab.WRITE, Route.BookEditor("book-1"))

assertEquals(Tab.WRITE, nav.tab)
assertEquals(Route.BookEditor("book-1"), nav.current)
assertEquals(true, nav.back())
assertEquals(Route.Root(Tab.WRITE), nav.current)
}
}
Loading