Skip to content

Commit 15227fa

Browse files
committed
feat: add reading a comic [#66]
1 parent dff13ba commit 15227fa

17 files changed

Lines changed: 560 additions & 32 deletions

File tree

androidVariant/src/main/java/org/comixedproject/variant/android/view/HomeView.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package org.comixedproject.variant.android.view
2121
import androidx.compose.foundation.layout.padding
2222
import androidx.compose.material3.Scaffold
2323
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.collectAsState
2425
import androidx.compose.runtime.getValue
2526
import androidx.compose.runtime.mutableStateOf
2627
import androidx.compose.runtime.remember
@@ -32,6 +33,7 @@ import kotlinx.coroutines.Dispatchers
3233
import kotlinx.coroutines.launch
3334
import org.comixedproject.variant.android.VariantTheme
3435
import org.comixedproject.variant.android.view.comics.ComicBookView
36+
import org.comixedproject.variant.android.view.reading.ReadingView
3537
import org.comixedproject.variant.android.view.server.ServerView
3638
import org.comixedproject.variant.android.view.settings.SettingsView
3739
import org.comixedproject.variant.viewmodel.VariantViewModel
@@ -42,6 +44,7 @@ fun HomeView() {
4244
val variantViewModel: VariantViewModel = koinViewModel()
4345
var currentDestination by remember { mutableStateOf(AppDestination.COMICS) }
4446
val coroutineScope = rememberCoroutineScope()
47+
val comicBook by variantViewModel.comicBook.collectAsState()
4548

4649
Scaffold(
4750
topBar = {
@@ -60,7 +63,22 @@ fun HomeView() {
6063
},
6164
content = { padding ->
6265
when (currentDestination) {
63-
AppDestination.COMICS -> ComicBookView(modifier = Modifier.padding(padding))
66+
AppDestination.COMICS ->
67+
if (comicBook != null) {
68+
ReadingView(
69+
comicBook!!,
70+
modifier = Modifier.padding(padding),
71+
onStopReading = { variantViewModel.readComicBook(null) }
72+
)
73+
} else {
74+
ComicBookView(
75+
onReadComicBook = { comicBook ->
76+
variantViewModel.readComicBook(comicBook)
77+
},
78+
modifier = Modifier.padding(padding)
79+
)
80+
}
81+
6482
AppDestination.BROWSE -> ServerView(modifier = Modifier.padding(padding))
6583
AppDestination.SETTINGS -> SettingsView(onCloseSettings = {
6684
currentDestination = AppDestination.COMICS

androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListItemView.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.comixedproject.variant.android.view.comics
22

33
import android.graphics.BitmapFactory
44
import androidx.compose.foundation.Image
5+
import androidx.compose.foundation.clickable
56
import androidx.compose.foundation.layout.Column
67
import androidx.compose.foundation.layout.fillMaxWidth
78
import androidx.compose.material3.CardDefaults
@@ -33,7 +34,11 @@ import org.comixedproject.variant.platform.Log
3334
private val TAG = "ComicBookListItemView"
3435

3536
@Composable
36-
fun ComicBookListItemView(comicBook: ComicBook, modifier: Modifier = Modifier) {
37+
fun ComicBookListItemView(
38+
comicBook: ComicBook,
39+
onClick: (ComicBook) -> Unit,
40+
modifier: Modifier = Modifier
41+
) {
3742
var coverContent by remember { mutableStateOf<ByteArray?>(null) }
3843
val coroutineScope = rememberCoroutineScope()
3944

@@ -44,7 +49,7 @@ fun ComicBookListItemView(comicBook: ComicBook, modifier: Modifier = Modifier) {
4449
) {
4550
val title = MetadataAPI.displayableTitle(comicBook)
4651

47-
Column {
52+
Column(modifier = Modifier.clickable(onClick = { onClick(comicBook) })) {
4853
comicBook.pages.firstOrNull()?.let { cover ->
4954
if (coverContent == null) {
5055
Image(
@@ -86,5 +91,5 @@ fun ComicBookListItemView(comicBook: ComicBook, modifier: Modifier = Modifier) {
8691
@Composable
8792
@Preview
8893
fun ComicBookListItemViewPreview() {
89-
VariantTheme { ComicBookListItemView(comicBook = COMIC_BOOK_LIST.get(0)) }
94+
VariantTheme { ComicBookListItemView(comicBook = COMIC_BOOK_LIST.get(0), onClick = {}) }
9095
}

androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListView.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ import org.comixedproject.variant.platform.Log
3939
private val TAG = "ComicBookListView"
4040

4141
@Composable
42-
fun ComicBookListView(comicBookList: List<ComicBook>, modifier: Modifier = Modifier) {
42+
fun ComicBookListView(
43+
comicBookList: List<ComicBook>,
44+
onClick: (ComicBook) -> Unit,
45+
modifier: Modifier = Modifier
46+
) {
4347
Scaffold(
4448
content = { padding ->
4549
if (comicBookList.isEmpty()) {
@@ -55,7 +59,11 @@ fun ComicBookListView(comicBookList: List<ComicBook>, modifier: Modifier = Modif
5559
horizontalArrangement = Arrangement.spacedBy(4.dp),
5660
content = {
5761
items(comicBookList) { comicBook ->
58-
ComicBookListItemView(comicBook, modifier = Modifier.padding(padding))
62+
ComicBookListItemView(
63+
comicBook,
64+
onClick = { onClick(it) },
65+
modifier = Modifier.padding(padding)
66+
)
5967
}
6068
})
6169
}
@@ -67,6 +75,6 @@ fun ComicBookListView(comicBookList: List<ComicBook>, modifier: Modifier = Modif
6775
@Preview
6876
fun ComicBookListViewPreview() {
6977
VariantTheme {
70-
ComicBookListView(COMIC_BOOK_LIST)
78+
ComicBookListView(COMIC_BOOK_LIST, onClick = {})
7179
}
7280
}

androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookView.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,22 @@ import androidx.compose.runtime.getValue
2424
import androidx.compose.ui.Modifier
2525
import androidx.compose.ui.tooling.preview.Preview
2626
import org.comixedproject.variant.android.VariantTheme
27+
import org.comixedproject.variant.model.library.ComicBook
2728
import org.comixedproject.variant.viewmodel.VariantViewModel
2829
import org.koin.androidx.compose.koinViewModel
2930

3031
private val TAG = "ComicBookView"
3132

3233
@Composable
33-
fun ComicBookView(modifier: Modifier = Modifier) {
34+
fun ComicBookView(onReadComicBook: (ComicBook) -> Unit, modifier: Modifier = Modifier) {
3435
val variantViewModel: VariantViewModel = koinViewModel()
3536
val comicBookList by variantViewModel.comicBookList.collectAsState()
3637

37-
ComicBookListView(comicBookList, modifier = modifier)
38+
ComicBookListView(comicBookList, onClick = { onReadComicBook(it) }, modifier = modifier)
3839
}
3940

4041
@Composable
4142
@Preview
4243
fun ComicBookViewPreview() {
43-
VariantTheme { ComicBookView() }
44+
VariantTheme { ComicBookView(onReadComicBook = { }) }
4445
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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.layout.Row
24+
import androidx.compose.foundation.layout.fillMaxHeight
25+
import androidx.compose.foundation.layout.fillMaxSize
26+
import androidx.compose.foundation.layout.fillMaxWidth
27+
import androidx.compose.foundation.layout.padding
28+
import androidx.compose.material.icons.Icons
29+
import androidx.compose.material.icons.automirrored.filled.ArrowBack
30+
import androidx.compose.material.icons.automirrored.filled.ArrowForward
31+
import androidx.compose.material3.BottomAppBar
32+
import androidx.compose.material3.Icon
33+
import androidx.compose.material3.IconButton
34+
import androidx.compose.material3.MaterialTheme
35+
import androidx.compose.material3.Scaffold
36+
import androidx.compose.material3.Slider
37+
import androidx.compose.material3.Text
38+
import androidx.compose.runtime.Composable
39+
import androidx.compose.runtime.LaunchedEffect
40+
import androidx.compose.runtime.getValue
41+
import androidx.compose.runtime.mutableStateOf
42+
import androidx.compose.runtime.remember
43+
import androidx.compose.runtime.setValue
44+
import androidx.compose.ui.Alignment
45+
import androidx.compose.ui.Modifier
46+
import androidx.compose.ui.graphics.asImageBitmap
47+
import androidx.compose.ui.res.stringResource
48+
import androidx.compose.ui.text.style.TextOverflow
49+
import androidx.compose.ui.tooling.preview.Preview
50+
import org.comixedproject.variant.adaptor.ArchiveAPI
51+
import org.comixedproject.variant.android.COMIC_BOOK_LIST
52+
import org.comixedproject.variant.android.R
53+
import org.comixedproject.variant.android.VariantTheme
54+
import org.comixedproject.variant.platform.Log
55+
56+
private const val TAG = "ReadingPageView"
57+
58+
@Composable
59+
fun ReadingPageView(
60+
comicFilename: String,
61+
pageFilename: String,
62+
title: String,
63+
currentPage: Int,
64+
totalPages: Int,
65+
onChangePage: (Int) -> Unit,
66+
onStopReading: () -> Unit,
67+
modifier: Modifier = Modifier
68+
) {
69+
var currentPageContent by remember { mutableStateOf<ByteArray?>(null) }
70+
71+
Scaffold(
72+
content = { padding ->
73+
if (currentPageContent == null) {
74+
LaunchedEffect(currentPageContent) {
75+
currentPageContent =
76+
ArchiveAPI.loadPage(comicFilename, pageFilename)
77+
}
78+
} else {
79+
currentPageContent?.let { content ->
80+
Image(
81+
bitmap = BitmapFactory.decodeByteArray(content, 0, content.size)
82+
.asImageBitmap(),
83+
contentDescription = title,
84+
modifier = Modifier
85+
.padding(padding)
86+
.fillMaxHeight()
87+
)
88+
}
89+
}
90+
},
91+
topBar = {
92+
Row(
93+
verticalAlignment = Alignment.CenterVertically,
94+
modifier = Modifier.fillMaxWidth()
95+
) {
96+
IconButton(
97+
onClick = {
98+
Log.debug(
99+
TAG,
100+
"Closing comic book"
101+
)
102+
onStopReading()
103+
}
104+
) {
105+
Icon(
106+
Icons.AutoMirrored.Filled.ArrowBack,
107+
contentDescription = stringResource(R.string.stopReadingLabel)
108+
)
109+
}
110+
111+
Text(
112+
title,
113+
style = MaterialTheme.typography.titleMedium,
114+
maxLines = 1,
115+
overflow = TextOverflow.Ellipsis,
116+
modifier = Modifier.fillMaxWidth()
117+
)
118+
}
119+
},
120+
bottomBar = {
121+
BottomAppBar {
122+
Row(modifier = Modifier.fillMaxWidth()) {
123+
IconButton(onClick = {
124+
currentPageContent = null
125+
onChangePage(currentPage - 1)
126+
}, enabled = (currentPage > 0)) {
127+
Icon(
128+
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
129+
contentDescription = stringResource(R.string.previousPageLabel)
130+
)
131+
}
132+
133+
Slider(
134+
value = currentPage.toFloat(),
135+
valueRange = 0f..(totalPages - 1).toFloat(),
136+
steps = totalPages,
137+
onValueChange = {
138+
currentPageContent = null
139+
onChangePage(it.toInt())
140+
},
141+
modifier = Modifier.weight(0.9f)
142+
)
143+
144+
IconButton(onClick = {
145+
currentPageContent = null
146+
onChangePage(currentPage + 1)
147+
}, enabled = (currentPage < (totalPages - 1))) {
148+
Icon(
149+
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
150+
contentDescription = stringResource(R.string.nextPageLabel)
151+
)
152+
}
153+
}
154+
}
155+
},
156+
modifier = modifier
157+
.fillMaxSize()
158+
)
159+
}
160+
161+
162+
@Composable
163+
@Preview
164+
fun ReadingPageViewPreview() {
165+
val comic = COMIC_BOOK_LIST.get(0)
166+
VariantTheme {
167+
ReadingPageView(
168+
comic.filename,
169+
comic.pages.get(0).filename,
170+
"Page Title",
171+
5,
172+
10,
173+
onChangePage = {},
174+
onStopReading = {}
175+
)
176+
}
177+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.getValue
23+
import androidx.compose.runtime.mutableIntStateOf
24+
import androidx.compose.runtime.remember
25+
import androidx.compose.runtime.setValue
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.tooling.preview.Preview
28+
import org.comixedproject.variant.android.COMIC_BOOK_LIST
29+
import org.comixedproject.variant.android.VariantTheme
30+
import org.comixedproject.variant.model.library.ComicBook
31+
import org.comixedproject.variant.platform.Log
32+
33+
private const val TAG = "ReadingView"
34+
35+
@Composable
36+
fun ReadingView(comicBook: ComicBook, onStopReading: () -> Unit, modifier: Modifier = Modifier) {
37+
var currentPage by remember { mutableIntStateOf(0) }
38+
39+
ReadingPageView(
40+
comicBook.path,
41+
comicBook.pages.get(currentPage).filename,
42+
comicBook.pages.get(currentPage).filename,
43+
currentPage,
44+
comicBook.pages.size,
45+
onChangePage = {
46+
Log.debug(TAG, "Going to page ${it}")
47+
currentPage = it
48+
},
49+
onStopReading = onStopReading,
50+
modifier = modifier
51+
)
52+
}
53+
54+
@Composable
55+
@Preview
56+
fun ReadingViewPreview() {
57+
VariantTheme { ReadingView(COMIC_BOOK_LIST.get(0), onStopReading = {}) }
58+
}

androidVariant/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@
1313
<string name="emptyComicListText">No comic books to show...</string>
1414
<string name="comicFileSizeText">%1$s MB</string>
1515
<string name="cancelButton">Cancel</string>
16+
<string name="previousPageLabel">Go to the previous page.</string>
17+
<string name="nextPageLabel">Go to the next page.</string>
18+
<string name="stopReadingLabel">Stop reading comic book.</string>
1619
</resources>

0 commit comments

Comments
 (0)