Skip to content

Commit 264ce25

Browse files
committed
Added showing a page overlay while reading a comic [#149]
1 parent 0a819d0 commit 264ce25

8 files changed

Lines changed: 414 additions & 356 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Variant - A digital comic book reading application for the iPad and Android tablets.
3+
* Copyright (C) 2025, The ComiXed Project
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses>
17+
*/
18+
19+
package org.comixedproject.variant.android.view.reading
20+
21+
import android.graphics.BitmapFactory
22+
import androidx.compose.foundation.Image
23+
import androidx.compose.foundation.gestures.detectTapGestures
24+
import androidx.compose.foundation.layout.Column
25+
import androidx.compose.foundation.layout.Row
26+
import androidx.compose.foundation.layout.fillMaxHeight
27+
import androidx.compose.foundation.layout.fillMaxSize
28+
import androidx.compose.foundation.layout.fillMaxWidth
29+
import androidx.compose.foundation.rememberScrollState
30+
import androidx.compose.foundation.verticalScroll
31+
import androidx.compose.material3.Icon
32+
import androidx.compose.material3.IconButton
33+
import androidx.compose.material3.MaterialTheme
34+
import androidx.compose.material3.Slider
35+
import androidx.compose.material3.Text
36+
import androidx.compose.runtime.Composable
37+
import androidx.compose.runtime.LaunchedEffect
38+
import androidx.compose.runtime.getValue
39+
import androidx.compose.runtime.mutableIntStateOf
40+
import androidx.compose.runtime.mutableStateOf
41+
import androidx.compose.runtime.remember
42+
import androidx.compose.runtime.setValue
43+
import androidx.compose.ui.Alignment
44+
import androidx.compose.ui.Modifier
45+
import androidx.compose.ui.graphics.asImageBitmap
46+
import androidx.compose.ui.input.pointer.pointerInput
47+
import androidx.compose.ui.platform.LocalContext
48+
import androidx.compose.ui.res.painterResource
49+
import androidx.compose.ui.res.stringResource
50+
import androidx.compose.ui.text.style.TextAlign
51+
import androidx.compose.ui.text.style.TextOverflow
52+
import androidx.compose.ui.tooling.preview.Preview
53+
import org.comixedproject.variant.adaptor.ArchiveAPI
54+
import org.comixedproject.variant.android.COMIC_BOOK_LIST
55+
import org.comixedproject.variant.android.R
56+
import org.comixedproject.variant.android.VariantTheme
57+
import org.comixedproject.variant.model.library.ComicBook
58+
import org.comixedproject.variant.platform.Log
59+
60+
private const val TAG = "PageNavigationView"
61+
62+
@Composable
63+
fun PageNavigationView(
64+
comicBook: ComicBook,
65+
onStopReading: () -> Unit,
66+
modifier: Modifier = Modifier
67+
) {
68+
var pageFilename by remember { mutableStateOf("") }
69+
var currentPage by remember { mutableIntStateOf(0) }
70+
var currentPageContent by remember { mutableStateOf<ByteArray?>(null) }
71+
var showPageOverlay by remember { mutableStateOf(false) }
72+
val context = LocalContext.current
73+
74+
Column(
75+
modifier = modifier
76+
.verticalScroll(rememberScrollState())
77+
.pointerInput(Unit) {
78+
detectTapGestures(
79+
onPress = { location ->
80+
var handled = false
81+
val screenHeightMiddle = context.resources.displayMetrics.heightPixels / 2
82+
if (location.y > (screenHeightMiddle * 1.5)) {
83+
val screenWidthMiddle = context.resources.displayMetrics.widthPixels / 2
84+
if (location.x <= (screenWidthMiddle / 2)) {
85+
if (currentPage > 0) {
86+
Log.info(TAG, "Navigating back to page ${currentPage - 1}")
87+
currentPageContent = null
88+
currentPage = currentPage - 1
89+
}
90+
handled = true
91+
} else if (location.x >= (screenWidthMiddle * 3) / 2) {
92+
if (currentPage < comicBook.pages.size - 1) {
93+
Log.info(
94+
TAG,
95+
"Navigating forward to page ${currentPage + 1}"
96+
)
97+
currentPageContent = null
98+
currentPage = currentPage + 1
99+
}
100+
handled = true
101+
}
102+
}
103+
if (!handled) {
104+
showPageOverlay = (showPageOverlay == false)
105+
Log.info(TAG, "Setting show overlay: ${showPageOverlay}")
106+
}
107+
}
108+
)
109+
}
110+
.fillMaxSize()) {
111+
if (showPageOverlay) {
112+
Row(
113+
verticalAlignment = Alignment.CenterVertically,
114+
modifier = Modifier.fillMaxWidth()
115+
) {
116+
IconButton(
117+
onClick = {
118+
Log.debug(
119+
TAG,
120+
"Closing comic book"
121+
)
122+
onStopReading()
123+
}
124+
) {
125+
Icon(
126+
painterResource(R.drawable.ic_close),
127+
contentDescription = stringResource(R.string.stopReadingLabel)
128+
)
129+
}
130+
131+
Text(
132+
text = comicBook.filename,
133+
style = MaterialTheme.typography.titleMedium,
134+
textAlign = TextAlign.Center,
135+
maxLines = 1,
136+
overflow = TextOverflow.Ellipsis,
137+
modifier = Modifier.fillMaxWidth()
138+
)
139+
}
140+
141+
Text(
142+
text = pageFilename,
143+
style = MaterialTheme.typography.titleSmall,
144+
textAlign = TextAlign.Center,
145+
maxLines = 1, overflow = TextOverflow.Ellipsis,
146+
modifier = Modifier.fillMaxWidth()
147+
)
148+
Row(modifier = Modifier.fillMaxWidth()) {
149+
IconButton(onClick = {
150+
currentPageContent = null
151+
currentPage = currentPage - 1
152+
}, enabled = (currentPage > 0)) {
153+
Icon(
154+
painterResource(R.drawable.ic_previous_page),
155+
contentDescription = stringResource(R.string.previousPageLabel)
156+
)
157+
}
158+
159+
Slider(
160+
value = currentPage.toFloat(),
161+
valueRange = 0f..(comicBook.pages.size - 1).toFloat(),
162+
steps = comicBook.pages.size,
163+
onValueChange = {
164+
currentPageContent = null
165+
currentPage = it.toInt()
166+
},
167+
modifier = Modifier.weight(0.9f)
168+
)
169+
170+
IconButton(onClick = {
171+
currentPageContent = null
172+
currentPage = currentPage + 1
173+
}, enabled = (currentPage < (comicBook.pages.size - 1))) {
174+
Icon(
175+
painterResource(R.drawable.ic_next_page),
176+
contentDescription = stringResource(R.string.nextPageLabel)
177+
)
178+
}
179+
}
180+
}
181+
182+
183+
if (currentPageContent == null) {
184+
LaunchedEffect(currentPageContent) {
185+
pageFilename = comicBook.pages.get(currentPage).filename
186+
currentPageContent =
187+
ArchiveAPI.loadPage(comicBook.path, pageFilename)
188+
189+
}
190+
} else {
191+
currentPageContent?.let { content ->
192+
Image(
193+
bitmap = BitmapFactory.decodeByteArray(content, 0, content.size)
194+
.asImageBitmap(),
195+
contentDescription = comicBook.pages.get(currentPage).filename,
196+
modifier = modifier
197+
.fillMaxHeight()
198+
)
199+
}
200+
}
201+
}
202+
}
203+
204+
205+
@Composable
206+
@Preview
207+
fun PageNavigationPreview() {
208+
val comic = COMIC_BOOK_LIST.get(0)
209+
210+
VariantTheme {
211+
PageNavigationView(
212+
comic,
213+
onStopReading = {})
214+
}
215+
}
216+
217+
@Composable
218+
@Preview
219+
fun PageNavigationPreviewWithOverlay() {
220+
val comic = COMIC_BOOK_LIST.get(0)
221+
222+
VariantTheme {
223+
PageNavigationView(
224+
comic,
225+
onStopReading = {})
226+
}
227+
}

0 commit comments

Comments
 (0)