From 17f440b7b2eecd6c47d6f3de558e594c1ffd9028 Mon Sep 17 00:00:00 2001 From: "Darryl L. Pierce" Date: Wed, 6 Aug 2025 20:02:08 -0400 Subject: [PATCH] Added showing a page overlay while reading a comic [#149] --- .../view/reading/PageNavigationView.kt | 227 ++++++++++++++++++ .../android/view/reading/ReadingPageView.kt | 175 -------------- .../android/view/reading/ReadingView.kt | 27 +-- iosVariant/Variant.xcodeproj/project.pbxproj | 8 +- iosVariant/iosVariant/Data/ImageLoader.swift | 5 + .../Views/Reading/PageNavigationView.swift | 168 +++++++++++++ .../Views/Reading/ReadingPageView.swift | 138 ----------- .../Views/Reading/ReadingView.swift | 22 +- 8 files changed, 414 insertions(+), 356 deletions(-) create mode 100644 androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/PageNavigationView.kt delete mode 100644 androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/ReadingPageView.kt create mode 100644 iosVariant/iosVariant/Views/Reading/PageNavigationView.swift delete mode 100644 iosVariant/iosVariant/Views/Reading/ReadingPageView.swift diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/PageNavigationView.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/PageNavigationView.kt new file mode 100644 index 0000000..b69498f --- /dev/null +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/PageNavigationView.kt @@ -0,0 +1,227 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2025, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.android.view.reading + +import android.graphics.BitmapFactory +import androidx.compose.foundation.Image +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import org.comixedproject.variant.adaptor.ArchiveAPI +import org.comixedproject.variant.android.COMIC_BOOK_LIST +import org.comixedproject.variant.android.R +import org.comixedproject.variant.android.VariantTheme +import org.comixedproject.variant.model.library.ComicBook +import org.comixedproject.variant.platform.Log + +private const val TAG = "PageNavigationView" + +@Composable +fun PageNavigationView( + comicBook: ComicBook, + onStopReading: () -> Unit, + modifier: Modifier = Modifier +) { + var pageFilename by remember { mutableStateOf("") } + var currentPage by remember { mutableIntStateOf(0) } + var currentPageContent by remember { mutableStateOf(null) } + var showPageOverlay by remember { mutableStateOf(false) } + val context = LocalContext.current + + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .pointerInput(Unit) { + detectTapGestures( + onPress = { location -> + var handled = false + val screenHeightMiddle = context.resources.displayMetrics.heightPixels / 2 + if (location.y > (screenHeightMiddle * 1.5)) { + val screenWidthMiddle = context.resources.displayMetrics.widthPixels / 2 + if (location.x <= (screenWidthMiddle / 2)) { + if (currentPage > 0) { + Log.info(TAG, "Navigating back to page ${currentPage - 1}") + currentPageContent = null + currentPage = currentPage - 1 + } + handled = true + } else if (location.x >= (screenWidthMiddle * 3) / 2) { + if (currentPage < comicBook.pages.size - 1) { + Log.info( + TAG, + "Navigating forward to page ${currentPage + 1}" + ) + currentPageContent = null + currentPage = currentPage + 1 + } + handled = true + } + } + if (!handled) { + showPageOverlay = (showPageOverlay == false) + Log.info(TAG, "Setting show overlay: ${showPageOverlay}") + } + } + ) + } + .fillMaxSize()) { + if (showPageOverlay) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + IconButton( + onClick = { + Log.debug( + TAG, + "Closing comic book" + ) + onStopReading() + } + ) { + Icon( + painterResource(R.drawable.ic_close), + contentDescription = stringResource(R.string.stopReadingLabel) + ) + } + + Text( + text = comicBook.filename, + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + } + + Text( + text = pageFilename, + style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Center, + maxLines = 1, overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + Row(modifier = Modifier.fillMaxWidth()) { + IconButton(onClick = { + currentPageContent = null + currentPage = currentPage - 1 + }, enabled = (currentPage > 0)) { + Icon( + painterResource(R.drawable.ic_previous_page), + contentDescription = stringResource(R.string.previousPageLabel) + ) + } + + Slider( + value = currentPage.toFloat(), + valueRange = 0f..(comicBook.pages.size - 1).toFloat(), + steps = comicBook.pages.size, + onValueChange = { + currentPageContent = null + currentPage = it.toInt() + }, + modifier = Modifier.weight(0.9f) + ) + + IconButton(onClick = { + currentPageContent = null + currentPage = currentPage + 1 + }, enabled = (currentPage < (comicBook.pages.size - 1))) { + Icon( + painterResource(R.drawable.ic_next_page), + contentDescription = stringResource(R.string.nextPageLabel) + ) + } + } + } + + + if (currentPageContent == null) { + LaunchedEffect(currentPageContent) { + pageFilename = comicBook.pages.get(currentPage).filename + currentPageContent = + ArchiveAPI.loadPage(comicBook.path, pageFilename) + + } + } else { + currentPageContent?.let { content -> + Image( + bitmap = BitmapFactory.decodeByteArray(content, 0, content.size) + .asImageBitmap(), + contentDescription = comicBook.pages.get(currentPage).filename, + modifier = modifier + .fillMaxHeight() + ) + } + } + } +} + + +@Composable +@Preview +fun PageNavigationPreview() { + val comic = COMIC_BOOK_LIST.get(0) + + VariantTheme { + PageNavigationView( + comic, + onStopReading = {}) + } +} + +@Composable +@Preview +fun PageNavigationPreviewWithOverlay() { + val comic = COMIC_BOOK_LIST.get(0) + + VariantTheme { + PageNavigationView( + comic, + onStopReading = {}) + } +} diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/ReadingPageView.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/ReadingPageView.kt deleted file mode 100644 index c7e2e4d..0000000 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/ReadingPageView.kt +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Variant - A digital comic book reading application for the iPad and Android tablets. - * Copyright (C) 2025, The ComiXed Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - */ - -package org.comixedproject.variant.android.view.reading - -import android.graphics.BitmapFactory -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Slider -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import org.comixedproject.variant.adaptor.ArchiveAPI -import org.comixedproject.variant.android.COMIC_BOOK_LIST -import org.comixedproject.variant.android.R -import org.comixedproject.variant.android.VariantTheme -import org.comixedproject.variant.platform.Log - -private const val TAG = "ReadingPageView" - -@Composable -fun ReadingPageView( - comicFilename: String, - pageFilename: String, - title: String, - currentPage: Int, - totalPages: Int, - onChangePage: (Int) -> Unit, - onStopReading: () -> Unit, - modifier: Modifier = Modifier -) { - var currentPageContent by remember { mutableStateOf(null) } - - Scaffold( - content = { padding -> - if (currentPageContent == null) { - LaunchedEffect(currentPageContent) { - currentPageContent = - ArchiveAPI.loadPage(comicFilename, pageFilename) - } - } else { - currentPageContent?.let { content -> - Image( - bitmap = BitmapFactory.decodeByteArray(content, 0, content.size) - .asImageBitmap(), - contentDescription = title, - modifier = Modifier - .padding(padding) - .fillMaxHeight() - ) - } - } - }, - topBar = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - IconButton( - onClick = { - Log.debug( - TAG, - "Closing comic book" - ) - onStopReading() - } - ) { - Icon( - painterResource(R.drawable.ic_close), - contentDescription = stringResource(R.string.stopReadingLabel) - ) - } - - Text( - title, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth() - ) - } - }, - bottomBar = { - BottomAppBar { - Row(modifier = Modifier.fillMaxWidth()) { - IconButton(onClick = { - currentPageContent = null - onChangePage(currentPage - 1) - }, enabled = (currentPage > 0)) { - Icon( - painterResource(R.drawable.ic_previous_page), - contentDescription = stringResource(R.string.previousPageLabel) - ) - } - - Slider( - value = currentPage.toFloat(), - valueRange = 0f..(totalPages - 1).toFloat(), - steps = totalPages, - onValueChange = { - currentPageContent = null - onChangePage(it.toInt()) - }, - modifier = Modifier.weight(0.9f) - ) - - IconButton(onClick = { - currentPageContent = null - onChangePage(currentPage + 1) - }, enabled = (currentPage < (totalPages - 1))) { - Icon( - painterResource(R.drawable.ic_next_page), - contentDescription = stringResource(R.string.nextPageLabel) - ) - } - } - } - }, - modifier = modifier - .fillMaxSize() - ) -} - - -@Composable -@Preview -fun ReadingPageViewPreview() { - val comic = COMIC_BOOK_LIST.get(0) - VariantTheme { - ReadingPageView( - comic.filename, - comic.pages.get(0).filename, - "Page Title", - 5, - 10, - onChangePage = {}, - onStopReading = {} - ) - } -} \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/ReadingView.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/ReadingView.kt index 70e4fb6..e241261 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/ReadingView.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/view/reading/ReadingView.kt @@ -19,33 +19,22 @@ package org.comixedproject.variant.android.view.reading import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import org.comixedproject.variant.android.COMIC_BOOK_LIST import org.comixedproject.variant.android.VariantTheme import org.comixedproject.variant.model.library.ComicBook -import org.comixedproject.variant.platform.Log private const val TAG = "ReadingView" @Composable -fun ReadingView(comicBook: ComicBook, onStopReading: () -> Unit, modifier: Modifier = Modifier) { - var currentPage by remember { mutableIntStateOf(0) } - - ReadingPageView( - comicBook.path, - comicBook.pages.get(currentPage).filename, - comicBook.pages.get(currentPage).filename, - currentPage, - comicBook.pages.size, - onChangePage = { - Log.debug(TAG, "Going to page ${it}") - currentPage = it - }, +fun ReadingView( + comicBook: ComicBook, + onStopReading: () -> Unit, + modifier: Modifier = Modifier +) { + PageNavigationView( + comicBook, onStopReading = onStopReading, modifier = modifier ) @@ -55,4 +44,4 @@ fun ReadingView(comicBook: ComicBook, onStopReading: () -> Unit, modifier: Modif @Preview fun ReadingViewPreview() { VariantTheme { ReadingView(COMIC_BOOK_LIST.get(0), onStopReading = {}) } -} \ No newline at end of file +} diff --git a/iosVariant/Variant.xcodeproj/project.pbxproj b/iosVariant/Variant.xcodeproj/project.pbxproj index 97125f0..7089c80 100644 --- a/iosVariant/Variant.xcodeproj/project.pbxproj +++ b/iosVariant/Variant.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 6AA5984C2DE72EF40078CC9F /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AA5984B2DE72EF40078CC9F /* Fixtures.swift */; }; 6AA598502DE73A8F0078CC9F /* EditServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AA5984F2DE73A8F0078CC9F /* EditServerView.swift */; }; 6AF012932E1DDF67001A6A19 /* ReadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AF012922E1DDF67001A6A19 /* ReadingView.swift */; }; - 6AF012952E1E89A3001A6A19 /* ReadingPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AF012942E1E89A3001A6A19 /* ReadingPageView.swift */; }; 7555FF83242A565900829871 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* HomeView.swift */; }; B62AADC32DFDDF01000BCC0C /* DirectoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62AADC22DFDDF01000BCC0C /* DirectoryEntry.swift */; }; B67F711E2E1848A1005BFB6B /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67F711D2E1848A1005BFB6B /* ImageLoader.swift */; }; @@ -26,6 +25,7 @@ B68FF2E92E0834980010853B /* ComicBooksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68FF2E82E0834980010853B /* ComicBooksView.swift */; }; B68FF2EB2E0834EF0010853B /* ComicBookListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68FF2EA2E0834EF0010853B /* ComicBookListView.swift */; }; B68FF2ED2E0837610010853B /* ComicBookListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68FF2EC2E0837610010853B /* ComicBookListItemView.swift */; }; + B6A363ED2E456E5600E9D270 /* PageNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A363EC2E456E5600E9D270 /* PageNavigationView.swift */; }; B6AEB7952E1024FC003FB705 /* AppDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AEB7942E1024FC003FB705 /* AppDestination.swift */; }; B6AEB7972E102901003FB705 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AEB7962E102901003FB705 /* SettingsView.swift */; }; B6B20B942DE5048E008598A7 /* KMPObservableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B20B932DE5048E008598A7 /* KMPObservableViewModel.swift */; }; @@ -57,7 +57,6 @@ 6AA5984B2DE72EF40078CC9F /* Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fixtures.swift; sourceTree = ""; }; 6AA5984F2DE73A8F0078CC9F /* EditServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServerView.swift; sourceTree = ""; }; 6AF012922E1DDF67001A6A19 /* ReadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadingView.swift; sourceTree = ""; }; - 6AF012942E1E89A3001A6A19 /* ReadingPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadingPageView.swift; sourceTree = ""; }; 7555FF7B242A565900829871 /* Variant.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Variant.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF82242A565900829871 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -69,6 +68,7 @@ B68FF2E82E0834980010853B /* ComicBooksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComicBooksView.swift; sourceTree = ""; }; B68FF2EA2E0834EF0010853B /* ComicBookListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComicBookListView.swift; sourceTree = ""; }; B68FF2EC2E0837610010853B /* ComicBookListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComicBookListItemView.swift; sourceTree = ""; }; + B6A363EC2E456E5600E9D270 /* PageNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageNavigationView.swift; sourceTree = ""; }; B6AEB7942E1024FC003FB705 /* AppDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDestination.swift; sourceTree = ""; }; B6AEB7962E102901003FB705 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; B6B20B932DE5048E008598A7 /* KMPObservableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMPObservableViewModel.swift; sourceTree = ""; }; @@ -108,7 +108,7 @@ isa = PBXGroup; children = ( 6AF012922E1DDF67001A6A19 /* ReadingView.swift */, - 6AF012942E1E89A3001A6A19 /* ReadingPageView.swift */, + B6A363EC2E456E5600E9D270 /* PageNavigationView.swift */, ); path = Reading; sourceTree = ""; @@ -322,6 +322,7 @@ B6B20B9E2DE50C0E008598A7 /* Koin.swift in Sources */, B68FF2E92E0834980010853B /* ComicBooksView.swift in Sources */, 6A4D18062E14191D00FF9EA2 /* Constants.swift in Sources */, + B6A363ED2E456E5600E9D270 /* PageNavigationView.swift in Sources */, B68922532DFDC29400BDC032 /* DirectoryItemView.swift in Sources */, 6AA598492DE72E910078CC9F /* ServerView.swift in Sources */, B6AEB7972E102901003FB705 /* SettingsView.swift in Sources */, @@ -330,7 +331,6 @@ B6B20B942DE5048E008598A7 /* KMPObservableViewModel.swift in Sources */, B689224F2DFDA77C00BDC032 /* BrowseServerView.swift in Sources */, B6AEB7952E1024FC003FB705 /* AppDestination.swift in Sources */, - 6AF012952E1E89A3001A6A19 /* ReadingPageView.swift in Sources */, B68FF2EB2E0834EF0010853B /* ComicBookListView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/iosVariant/iosVariant/Data/ImageLoader.swift b/iosVariant/iosVariant/Data/ImageLoader.swift index dcf53ae..282b59f 100644 --- a/iosVariant/iosVariant/Data/ImageLoader.swift +++ b/iosVariant/iosVariant/Data/ImageLoader.swift @@ -44,6 +44,11 @@ class ImageLoader: ObservableObject { doLoadPage() } + func loadPage(filename: String) { + self.pageFilename = filename + doLoadPage() + } + private func doLoadCover() { Task { Log().debug( diff --git a/iosVariant/iosVariant/Views/Reading/PageNavigationView.swift b/iosVariant/iosVariant/Views/Reading/PageNavigationView.swift new file mode 100644 index 0000000..6bfb013 --- /dev/null +++ b/iosVariant/iosVariant/Views/Reading/PageNavigationView.swift @@ -0,0 +1,168 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2025, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import KMPObservableViewModelSwiftUI +import SwiftUI +import shared + +private let TAG = "PageNavigationView" + +struct PageNavigationView: View { + @ObservedObject var imageLoader: ImageLoader + @State var paageFilename = "" + @State var currentPage = 0.0 + @State var showPageOverlay = false + + let comicBook: ComicBook + var totalPages = 0 + var onStopReading: () -> Void + + var pageFilename: String { + return (self.comicBook.pages[Int(currentPage)] as! ComicPage).filename + } + + init( + comicBook: ComicBook, + onStopReading: @escaping () -> Void + ) { + self.comicBook = comicBook + self.totalPages = comicBook.pages.count + self.onStopReading = onStopReading + + self.imageLoader = ImageLoader( + comicFilename: comicBook.path, + pageFileanme: (comicBook.pages[Int(0)] as! ComicPage).filename + ) + } + + var maxPage: Int { + return self.comicBook.pages.count - 1 + } + + func loadPage() { + let filename = + (self.comicBook.pages[Int(self.currentPage)] as! ComicPage).filename + self.imageLoader.loadPage(filename: filename) + } + + var body: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading) { + if showPageOverlay { + VStack(alignment: .center) { + Text(self.comicBook.filename).font(.headline).frame( + maxWidth: .infinity + ) + Text(pageFilename).font(.subheadline).frame( + maxWidth: .infinity + ) + } + HStack(alignment: .center) { + Button { + self.currentPage = self.currentPage - 1 + Log().info( + tag: TAG, + message: + "Going back to page \(self.currentPage) of \(self.totalPages)" + ) + loadPage() + } label: { + Image("previous_page") + } + .disabled(self.currentPage == 0) + + Slider( + value: $currentPage, + in: 0...Double(maxPage), + step: 1, + onEditingChanged: { editing in + if !editing { + Log().debug( + tag: TAG, + message: + "Going to page \(self.currentPage)" + ) + loadPage() + } + } + ) + + Button { + self.currentPage = self.currentPage + 1 + Log().info( + tag: TAG, + message: + "Going forward to page \(self.currentPage) of \(self.totalPages)" + ) + loadPage() + } label: { + Image("next_page") + } + .disabled(Int(currentPage) == (self.totalPages - 1)) + } + } + + if imageLoader.image != nil { + Image(uiImage: imageLoader.image!) + .resizable() + .aspectRatio(contentMode: .fill) + .onTapGesture { + Log().debug( + tag: TAG, + message: "Comic page tapped" + ) + self.showPageOverlay = !self.showPageOverlay + } + } else { + Image(systemName: "placeholdertext.fill") + .resizable() + .aspectRatio(contentMode: .fill) + } + } + .toolbar { + if showPageOverlay { + ToolbarItem(placement: .topBarLeading) { + Button("Close") { + Log().info( + tag: TAG, + message: "Closing comic book" + ) + onStopReading() + } + } + } + } + } + } + } +} + +#Preview("normal") { + PageNavigationView( + comicBook: COMIC_BOOK_LIST[0], + onStopReading: {} + ) +} + +#Preview("withOverlay") { + PageNavigationView( + comicBook: COMIC_BOOK_LIST[0], + onStopReading: {} + ) +} diff --git a/iosVariant/iosVariant/Views/Reading/ReadingPageView.swift b/iosVariant/iosVariant/Views/Reading/ReadingPageView.swift deleted file mode 100644 index 04fe980..0000000 --- a/iosVariant/iosVariant/Views/Reading/ReadingPageView.swift +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Variant - A digital comic book reading application for the iPad and Android tablets. - * Copyright (C) 2025, The ComiXed Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - */ - -import SwiftUI -import shared - -private let TAG = "ReadingPageView" - -struct ReadingPageView: View { - @ObservedObject var imageLoader: ImageLoader - @Binding var pageNumber: Double - - let comicFilename: String - let pageFilename: String - let title: String - let totalPages: Double - - var onStopReading: () -> Void - - init( - comicFilename: String, - pageFilename: String, - title: String, - pageNumber: Binding, - totalPages: Int, - onStopReading: @escaping () -> Void - ) { - self.comicFilename = comicFilename - self.pageFilename = pageFilename - self.title = title - self._pageNumber = pageNumber - self.totalPages = Double(totalPages) - self.onStopReading = onStopReading - - self.imageLoader = ImageLoader( - comicFilename: self.comicFilename, - pageFileanme: self.pageFilename - ) - } - - var maxPage: Double { - return self.totalPages - 1 - } - - var body: some View { - NavigationStack { - ScrollView { - VStack(alignment: .leading) { - if imageLoader.image != nil { - Image(uiImage: imageLoader.image!) - .resizable() - .aspectRatio(contentMode: .fill) - } else { - Image(systemName: "placeholdertext.fill") - .resizable() - .aspectRatio(contentMode: .fill) - } - - HStack(alignment: .center) { - Button { - Log().info( - tag: TAG, - message: "Going to previous page" - ) - self.pageNumber = self.pageNumber - 1 - } label: { - Image("previous_page") - } - .disabled(pageNumber == 0) - - Slider( - value: $pageNumber, - in: 0...maxPage, - step: 1, - onEditingChanged: { editing in - if !editing { - Log().debug( - tag: TAG, - message: "Going to page \(pageNumber)" - ) - } - } - ) - - Button { - Log().info(tag: TAG, message: "Going to next page") - self.pageNumber = self.pageNumber + 1 - } label: { - Image("next_page") - } - .disabled(pageNumber == (totalPages - 1)) - } - } - .onTapGesture { - Log().debug(tag: TAG, message: "Comic page tapped") - } - .navigationTitle(title) - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button("Close") { - Log().info( - tag: TAG, - message: "Closing comic book" - ) - onStopReading() - } - } - } - } - } - } -} - -#Preview { - ReadingPageView( - comicFilename: COMIC_BOOK_LIST[0].filename, - pageFilename: (COMIC_BOOK_LIST[0].pages[0] as! ComicPage).filename, - title: (COMIC_BOOK_LIST[0].pages[0] as! ComicPage).filename, - pageNumber: .constant(5.0), - totalPages: 10, - onStopReading: {} - ) -} diff --git a/iosVariant/iosVariant/Views/Reading/ReadingView.swift b/iosVariant/iosVariant/Views/Reading/ReadingView.swift index 762875d..7a4b9ec 100644 --- a/iosVariant/iosVariant/Views/Reading/ReadingView.swift +++ b/iosVariant/iosVariant/Views/Reading/ReadingView.swift @@ -23,31 +23,13 @@ import shared private let TAG = "ReadingView" struct ReadingView: View { - @State private var currentPage = 0.0 - let comicBook: ComicBook var onStopReading: () -> Void - var comicFilename: String { - return comicBook.path - } - - var pageFilename: String { - return (comicBook.pages[Int(currentPage)] as! ComicPage).filename - } - - var title: String { - return (comicBook.pages[Int(currentPage)] as! ComicPage).filename - } - var body: some View { - ReadingPageView( - comicFilename: comicFilename, - pageFilename: pageFilename, - title: title, - pageNumber: $currentPage, - totalPages: comicBook.pages.count, + PageNavigationView( + comicBook: self.comicBook, onStopReading: { onStopReading() } ) }