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.3)
# Regélő – Android alkalmazás (v1.3.4)

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
`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:
Expand Down Expand Up @@ -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,
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 = 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.
Expand Down
74 changes: 0 additions & 74 deletions app/src/main/java/hu/regelo/app/ui/screens/ProfileScreens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String?>(null) }
var newBook by remember { mutableStateOf(false) }
var deleteBook by remember { mutableStateOf<hu.regelo.app.data.LocalBook?>(null) }
var exportBook by remember { mutableStateOf<hu.regelo.app.data.LocalBook?>(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()) {
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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,
Expand Down
81 changes: 71 additions & 10 deletions app/src/main/java/hu/regelo/app/ui/screens/WriteScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<LocalBook?>(null) }
var exportBook by remember { mutableStateOf<LocalBook?>(null) }
val snackbar = LocalSnackbar.current

Column(Modifier.fillMaxSize()) {
TopAppBar(
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions app/src/test/java/hu/regelo/app/NavigatorTest.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading