diff --git a/README.md b/README.md index 39be7e9..f46a0db 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Regélő – Android alkalmazás (v1.3.3) +# Regélő – Android alkalmazás (v1.3.4) Magyar írói és olvasói platform (Wattpad / Inkitt jellegű), Kotlin + Jetpack Compose. @@ -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 - `133`; ez azonos vagy kisebb értékkel nem tölthető fel frissítésként. + `134`; 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: @@ -87,6 +87,9 @@ Hivatalos e-mail: regeloapp@gmail.com · Hivatalos profil az appban: @regelohiva titkosítva tárolódnak, nem kerülnek biztonsági mentésbe, és kijelentkezéskor/fióktörléskor törlődnek; a korábbi egyszerű JPEG-fájlok az első feltöltéskor atomi cserével titkosítottra migrálódnak. A GDPR-export már a saját közösségi jelentéseket és értesítéseket is tartalmazza, más felhasználók ilyen rekordjai nélkül. +- **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. - **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, diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 70bde4b..f7eaa25 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "hu.regelo.app" minSdk = 26 targetSdk = 37 - versionCode = 133 - versionName = "1.3.3" + versionCode = 134 + versionName = "1.3.4" buildConfigField("String", "SUPABASE_URL", "\"https://pqmfezkvaslqedalwwnn.supabase.co\"") // Nyilvános (publishable) kulcs – a sorszintű jogosultságok (RLS) védik az adatokat. diff --git a/app/src/main/java/hu/regelo/app/ui/screens/ProfileScreens.kt b/app/src/main/java/hu/regelo/app/ui/screens/ProfileScreens.kt index 7873861..dbb95e1 100644 --- a/app/src/main/java/hu/regelo/app/ui/screens/ProfileScreens.kt +++ b/app/src/main/java/hu/regelo/app/ui/screens/ProfileScreens.kt @@ -67,7 +67,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -166,11 +165,7 @@ private fun ProfileContent(nav: Navigator, userId: String, isMe: Boolean) { var tip by remember { mutableStateOf(false) } var postNews by remember { mutableStateOf(false) } var deleteNews by remember { mutableStateOf(null) } - var newBook by remember { mutableStateOf(false) } - var deleteBook by remember { mutableStateOf(null) } - var exportBook by remember { mutableStateOf(null) } val myProfile by AppGraph.profiles.me.collectAsState() - val myBooks by AppGraph.writing.myBooks().collectAsState(initial = emptyList()) val loader = rememberLoader(userId, if (isMe) myProfile else null) { loadProfile(userId) } Column(Modifier.fillMaxSize()) { @@ -270,48 +265,6 @@ private fun ProfileContent(nav: Navigator, userId: String, isMe: Boolean) { AnnouncementCard(hu.regelo.app.data.AnnouncementItem(a, p), nav, onDelete = if (isMe) ({ deleteNews = a.id }) else null) } } - if (isMe) { - item(key = "my-writing-tools") { - Column(Modifier.fillMaxWidth().padding(top = 12.dp)) { - SectionTitle("Saját történetek és piszkozatok (${myBooks.size})") - Row( - Modifier.fillMaxWidth().padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button(onClick = { newBook = true }, modifier = Modifier.weight(1f)) { - Icon(Icons.Default.Add, null) - Spacer(Modifier.width(6.dp)) - Text("Új történet", maxLines = 1, overflow = TextOverflow.Ellipsis) - } - OutlinedButton(onClick = { nav.push(Route.Import(null)) }, modifier = Modifier.weight(1f)) { - Text("Importálás", maxLines = 1, overflow = TextOverflow.Ellipsis) - } - } - OutlinedButton( - onClick = { nav.push(Route.SeriesManager) }, - modifier = Modifier.padding(start = 16.dp, top = 4.dp), - ) { Text("Sorozataim kezelése") } - if (myBooks.isEmpty()) { - Text( - "Itt kezelheted a piszkozataidat és publikált műveidet.", - modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } - items(myBooks, key = { "my-work-${it.id}" }) { book -> - MyBookCard( - book, - onOpen = { nav.push(Route.BookEditor(book.id)) }, - onPreview = { nav.push(Route.Reader(book.id)) }, - onStats = { nav.push(Route.Stats(book.id)) }, - onWorld = { nav.push(Route.World(book.id)) }, - onExport = { exportBook = book }, - onDelete = { deleteBook = book }, - ) - } - } // A hivatalos kiadványok külön sorban, a „Művei” előtt (a Regélő hivatalos profilján). val officialCards = d.books.filter { it.book.id in officialBooks } val ownCards = d.books.filter { it.book.id !in officialBooks } @@ -352,33 +305,6 @@ private fun ProfileContent(nav: Navigator, userId: String, isMe: Boolean) { } } if (tip) TipDialog(p.displayName, p.paypalMe) { tip = false } - if (newBook) NewBookDialog(onDismiss = { newBook = false }) { title, genre, description -> - scope.launch { - try { - val book = AppGraph.writing.createBook(title, genre, description) - val firstChapter = AppGraph.writing.chapterList(book.id).firstOrNull() - if (firstChapter != null) nav.push(Route.ChapterEditor(book.id, firstChapter.id)) - else nav.push(Route.BookEditor(book.id)) - } catch (e: Exception) { - snackbar.showSnackbar(e.friendlyMessage().also { - hu.regelo.app.util.ErrorReporter.report(e, "ProfileScreens.createBook") - }) - } - } - } - exportBook?.let { book -> ExportDialog(book.id, book.title, onDismiss = { exportBook = null }) } - deleteBook?.let { book -> - ConfirmDialog( - title = "Történet törlése", - text = "„${book.title}” és minden fejezete véglegesen törlődik a telefonról és a felhőből. Ez nem vonható vissza.", - confirm = "Végleges törlés", - destructive = true, - onConfirm = { - scope.runAction(snackbar, "Történet törölve.") { AppGraph.writing.deleteBook(book.id) } - }, - onDismiss = { deleteBook = null }, - ) - } if (postNews) PostNewsDialog(null, "", onDismiss = { postNews = false }, onPosted = { loader.reload() }) deleteNews?.let { id -> ConfirmDialog("Hír törlése", "Törlöd ezt a hírt? Az Újdonságok közül is eltűnik.", "Törlés", destructive = true, diff --git a/app/src/main/java/hu/regelo/app/ui/screens/WriteScreen.kt b/app/src/main/java/hu/regelo/app/ui/screens/WriteScreen.kt index 7589619..8e3b8b3 100644 --- a/app/src/main/java/hu/regelo/app/ui/screens/WriteScreen.kt +++ b/app/src/main/java/hu/regelo/app/ui/screens/WriteScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -66,10 +67,13 @@ import hu.regelo.app.data.Genres import hu.regelo.app.data.LocalBook import hu.regelo.app.data.PublishStatus import hu.regelo.app.ui.components.BookCover +import hu.regelo.app.ui.components.ConfirmDialog import hu.regelo.app.ui.components.LocalSnackbar import hu.regelo.app.ui.components.Pill +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.util.friendlyMessage import hu.regelo.app.util.formatIsoDay import hu.regelo.app.util.relativeTime import kotlinx.coroutines.launch @@ -81,8 +85,13 @@ fun WriteScreen(nav: Navigator) { val scope = rememberCoroutineScope() val sync by AppGraph.writing.syncState.collectAsState() val todayWords by AppGraph.writing.todayWords().collectAsState(initial = 0) + val myBooks by AppGraph.writing.myBooks().collectAsState(initial = emptyList()) val settings by AppGraph.settings.settings.collectAsState(initial = AppSettings()) var goalDialog by remember { mutableStateOf(false) } + var newBook by remember { mutableStateOf(false) } + var deleteBook by remember { mutableStateOf(null) } + var exportBook by remember { mutableStateOf(null) } + val snackbar = LocalSnackbar.current Column(Modifier.fillMaxSize()) { TopAppBar( @@ -94,21 +103,46 @@ fun WriteScreen(nav: Navigator) { ) LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(bottom = 96.dp)) { item { - Card( - onClick = { nav.select(hu.regelo.app.ui.nav.Tab.ME) }, - modifier = Modifier.fillMaxWidth().padding(16.dp, 8.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer), - ) { - Column(Modifier.padding(16.dp)) { - Text("Saját történetek és piszkozatok", style = MaterialTheme.typography.titleMedium) - Spacer(Modifier.height(4.dp)) + Column(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 4.dp)) { + Text("Saját történetek és piszkozatok (${myBooks.size})", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) + Row( + Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button(onClick = { newBook = true }, modifier = Modifier.weight(1f)) { + Icon(Icons.Default.Edit, null) + Spacer(Modifier.width(6.dp)) + Text("Új történet", maxLines = 1, overflow = TextOverflow.Ellipsis) + } + OutlinedButton( + onClick = { nav.push(Route.Import(null)) }, + modifier = Modifier.weight(1f), + ) { Text("Importálás", maxLines = 1, overflow = TextOverflow.Ellipsis) } + } + OutlinedButton( + onClick = { nav.push(Route.SeriesManager) }, + modifier = Modifier.padding(start = 16.dp, top = 4.dp), + ) { Text("Sorozataim kezelése") } + if (myBooks.isEmpty()) { Text( - "A műveid, importálás, sorozatok és szerkesztési eszközök a Profil oldalon találhatók.", - style = MaterialTheme.typography.bodyMedium, + "Itt kezelheted a piszkozataidat és publikált műveidet.", + modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } } + items(myBooks, key = { "my-work-${it.id}" }) { book -> + MyBookCard( + book, + onOpen = { nav.push(Route.BookEditor(book.id)) }, + onPreview = { nav.push(Route.Reader(book.id)) }, + onStats = { nav.push(Route.Stats(book.id)) }, + onWorld = { nav.push(Route.World(book.id)) }, + onExport = { exportBook = book }, + onDelete = { deleteBook = book }, + ) + } openChallenges.firstOrNull()?.let { ch -> item { Card( @@ -160,6 +194,33 @@ fun WriteScreen(nav: Navigator) { } if (goalDialog) GoalDialog(settings.dailyGoal, onDismiss = { goalDialog = false }) { g -> scope.launch { AppGraph.settings.setDailyGoal(g) } } + if (newBook) NewBookDialog(onDismiss = { newBook = false }) { title, genre, description -> + scope.launch { + try { + val book = AppGraph.writing.createBook(title, genre, description) + val firstChapter = AppGraph.writing.chapterList(book.id).firstOrNull() + if (firstChapter != null) nav.push(Route.ChapterEditor(book.id, firstChapter.id)) + else nav.push(Route.BookEditor(book.id)) + } catch (e: Exception) { + snackbar.showSnackbar(e.friendlyMessage().also { + hu.regelo.app.util.ErrorReporter.report(e, "WriteScreen.createBook") + }) + } + } + } + exportBook?.let { book -> ExportDialog(book.id, book.title, onDismiss = { exportBook = null }) } + deleteBook?.let { book -> + ConfirmDialog( + title = "Történet törlése", + text = "„${book.title}” és minden fejezete véglegesen törlődik a telefonról és a felhőből. Ez nem vonható vissza.", + confirm = "Végleges törlés", + destructive = true, + onConfirm = { + scope.runAction(snackbar, "Történet törölve.") { AppGraph.writing.deleteBook(book.id) } + }, + onDismiss = { deleteBook = null }, + ) + } } @Composable diff --git a/app/src/test/java/hu/regelo/app/NavigatorTest.kt b/app/src/test/java/hu/regelo/app/NavigatorTest.kt new file mode 100644 index 0000000..50be108 --- /dev/null +++ b/app/src/test/java/hu/regelo/app/NavigatorTest.kt @@ -0,0 +1,24 @@ +package hu.regelo.app + +import hu.regelo.app.ui.nav.Navigator +import hu.regelo.app.ui.nav.Route +import hu.regelo.app.ui.nav.Tab +import org.junit.Assert.assertEquals +import org.junit.Test + +class NavigatorTest { + @Test + fun authoringDestinationsStayOnWriteTabWhenSwitchingTabs() { + val nav = Navigator() + + nav.select(Tab.WRITE) + nav.push(Route.BookEditor("book-1")) + nav.select(Tab.ME) + nav.select(Tab.WRITE) + + assertEquals(Tab.WRITE, nav.tab) + assertEquals(Route.BookEditor("book-1"), nav.current) + assertEquals(true, nav.back()) + assertEquals(Route.Root(Tab.WRITE), nav.current) + } +}