@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
66import androidx.compose.foundation.layout.Box
77import androidx.compose.foundation.layout.Column
88import androidx.compose.foundation.layout.PaddingValues
9+ import androidx.compose.foundation.layout.Row
910import androidx.compose.foundation.layout.aspectRatio
1011import androidx.compose.foundation.layout.fillMaxSize
1112import androidx.compose.foundation.layout.fillMaxWidth
@@ -15,9 +16,15 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
1516import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
1617import androidx.compose.foundation.lazy.grid.itemsIndexed
1718import androidx.compose.material3.AlertDialog
19+ import androidx.compose.material3.Checkbox
20+ import androidx.compose.material3.ExperimentalMaterial3Api
1821import androidx.compose.material3.MaterialTheme
22+ import androidx.compose.material3.ModalBottomSheet
1923import androidx.compose.material3.Text
2024import androidx.compose.material3.TextButton
25+ import androidx.compose.material3.SnackbarHost
26+ import androidx.compose.material3.SnackbarHostState
27+ import androidx.compose.material3.rememberModalBottomSheetState
2128import androidx.compose.runtime.Composable
2229import androidx.compose.runtime.LaunchedEffect
2330import androidx.compose.runtime.collectAsState
@@ -29,14 +36,14 @@ import androidx.compose.ui.Alignment
2936import androidx.compose.ui.Modifier
3037import androidx.compose.ui.platform.LocalContext
3138import androidx.compose.ui.text.style.TextOverflow
32- import io.theficos.ereader.data.sync.SyncEnqueuer
3339import androidx.compose.ui.unit.dp
3440import io.theficos.ereader.core.model.Document
41+ import io.theficos.ereader.data.sync.SyncEnqueuer
3542import io.theficos.ereader.ui.components.CoverImage
3643import io.theficos.ereader.ui.components.SectionLabel
3744import io.theficos.ereader.ui.theme.Lora
3845
39- @OptIn(ExperimentalFoundationApi ::class )
46+ @OptIn(ExperimentalFoundationApi ::class , ExperimentalMaterial3Api :: class )
4047@Composable
4148fun LibraryScreen (
4249 viewModel : LibraryViewModel ,
@@ -48,63 +55,142 @@ fun LibraryScreen(
4855
4956 val items by viewModel.items.collectAsState()
5057 val cont by viewModel.continueReading.collectAsState()
58+ var menuFor by remember { mutableStateOf<Document ?>(null ) }
5159 var pendingDelete by remember { mutableStateOf<Document ?>(null ) }
60+ var pendingRestart by remember { mutableStateOf<Document ?>(null ) }
61+ val snackbarHostState = remember { SnackbarHostState () }
62+
63+ LaunchedEffect (Unit ) {
64+ viewModel.events.collect { event ->
65+ when (event) {
66+ LibraryEvent .RestartFailed ->
67+ snackbarHostState.showSnackbar(" Couldn't sync restart — will retry." )
68+ }
69+ }
70+ }
5271
5372 if (items.isEmpty()) {
5473 EmptyState (modifier = Modifier .padding(contentPadding))
5574 return
5675 }
5776
58- LazyVerticalGrid (
59- columns = GridCells .Fixed (3 ),
60- modifier = Modifier
61- .fillMaxSize()
62- .padding(contentPadding),
63- contentPadding = PaddingValues (16 .dp),
64- verticalArrangement = Arrangement .spacedBy(16 .dp),
65- horizontalArrangement = Arrangement .spacedBy(12 .dp),
66- ) {
67- item(span = { GridItemSpan (maxLineSpan) }) {
68- Text (
69- text = " Quire" ,
70- style = MaterialTheme .typography.displaySmall,
71- color = MaterialTheme .colorScheme.onSurface,
72- )
73- }
74- cont?.let { row ->
77+ Box (modifier = Modifier .fillMaxSize().padding(contentPadding)) {
78+ LazyVerticalGrid (
79+ columns = GridCells .Fixed (3 ),
80+ modifier = Modifier .fillMaxSize(),
81+ contentPadding = PaddingValues (16 .dp),
82+ verticalArrangement = Arrangement .spacedBy(16 .dp),
83+ horizontalArrangement = Arrangement .spacedBy(12 .dp),
84+ ) {
7585 item(span = { GridItemSpan (maxLineSpan) }) {
76- ContinueReadingCard (row = row, onClick = { onOpenBook(row.document.id) })
86+ Text (
87+ text = " Quire" ,
88+ style = MaterialTheme .typography.displaySmall,
89+ color = MaterialTheme .colorScheme.onSurface,
90+ )
91+ }
92+ cont?.let { row ->
93+ item(span = { GridItemSpan (maxLineSpan) }) {
94+ ContinueReadingCard (row = row, onClick = { onOpenBook(row.document.id) })
95+ }
96+ }
97+ item(span = { GridItemSpan (maxLineSpan) }) {
98+ SectionLabel (" Library · ${items.size} " )
99+ }
100+ itemsIndexed(items, key = { _, r -> r.document.id }) { _, row ->
101+ Column (
102+ modifier = Modifier .combinedClickable(
103+ onClick = { onOpenBook(row.document.id) },
104+ onLongClick = { menuFor = row.document },
105+ ),
106+ ) {
107+ CoverImage (
108+ source = row.document.coverPath,
109+ title = row.document.title,
110+ author = row.document.author,
111+ modifier = Modifier
112+ .fillMaxWidth()
113+ .aspectRatio(2f / 3f ),
114+ )
115+ Text (
116+ text = row.document.title,
117+ style = MaterialTheme .typography.titleMedium,
118+ maxLines = 2 ,
119+ overflow = TextOverflow .Ellipsis ,
120+ modifier = Modifier .padding(top = 6 .dp),
121+ )
122+ }
77123 }
78124 }
79- item(span = { GridItemSpan (maxLineSpan) }) {
80- SectionLabel (" Library · ${items.size} " )
81- }
82- itemsIndexed(items, key = { _, r -> r.document.id }) { _, row ->
83- Column (
84- modifier = Modifier .combinedClickable(
85- onClick = { onOpenBook(row.document.id) },
86- onLongClick = { pendingDelete = row.document },
87- ),
88- ) {
89- CoverImage (
90- source = row.document.coverPath,
91- title = row.document.title,
92- author = row.document.author,
93- modifier = Modifier
94- .fillMaxWidth()
95- .aspectRatio(2f / 3f ),
96- )
125+ SnackbarHost (hostState = snackbarHostState, modifier = Modifier .align(Alignment .BottomCenter ))
126+ }
127+
128+ menuFor?.let { doc ->
129+ val sheetState = rememberModalBottomSheetState()
130+ ModalBottomSheet (
131+ onDismissRequest = { menuFor = null },
132+ sheetState = sheetState,
133+ ) {
134+ Column (modifier = Modifier .fillMaxWidth().padding(bottom = 24 .dp)) {
97135 Text (
98- text = row.document .title,
136+ text = doc .title,
99137 style = MaterialTheme .typography.titleMedium,
100138 maxLines = 2 ,
101139 overflow = TextOverflow .Ellipsis ,
102- modifier = Modifier .padding(top = 6 .dp),
140+ modifier = Modifier .padding(horizontal = 24 .dp, vertical = 12 .dp),
103141 )
142+ TextButton (
143+ onClick = {
144+ pendingRestart = doc
145+ menuFor = null
146+ },
147+ modifier = Modifier .fillMaxWidth().padding(horizontal = 16 .dp),
148+ ) {
149+ Text (" Restart book" , modifier = Modifier .fillMaxWidth())
150+ }
151+ TextButton (
152+ onClick = {
153+ pendingDelete = doc
154+ menuFor = null
155+ },
156+ modifier = Modifier .fillMaxWidth().padding(horizontal = 16 .dp),
157+ ) {
158+ Text (
159+ " Delete from library" ,
160+ modifier = Modifier .fillMaxWidth(),
161+ color = MaterialTheme .colorScheme.error,
162+ )
163+ }
104164 }
105165 }
106166 }
107167
168+ pendingRestart?.let { doc ->
169+ var alsoDelete by remember { mutableStateOf(false ) }
170+ AlertDialog (
171+ onDismissRequest = { pendingRestart = null },
172+ title = { Text (" Restart book?" ) },
173+ text = {
174+ Column (verticalArrangement = Arrangement .spacedBy(12 .dp)) {
175+ Text (" \" ${doc.title} \" will be marked as unread and synced to your other devices." )
176+ Row (verticalAlignment = Alignment .CenterVertically ) {
177+ Checkbox (checked = alsoDelete, onCheckedChange = { alsoDelete = it })
178+ Text (" Also delete the downloaded copy" , modifier = Modifier .padding(start = 8 .dp))
179+ }
180+ }
181+ },
182+ confirmButton = {
183+ TextButton (onClick = {
184+ viewModel.restartFromUi(doc, alsoDelete, context)
185+ pendingRestart = null
186+ }) { Text (" Restart" ) }
187+ },
188+ dismissButton = {
189+ TextButton (onClick = { pendingRestart = null }) { Text (" Cancel" ) }
190+ },
191+ )
192+ }
193+
108194 pendingDelete?.let { doc ->
109195 AlertDialog (
110196 onDismissRequest = { pendingDelete = null },
0 commit comments