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
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/*
* 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 <http://www.gnu.org/licenses>
*/

package org.comixedproject.variant.android.view.comics

import android.graphics.BitmapFactory
Expand Down Expand Up @@ -72,12 +90,12 @@ fun ComicBookListItemView(
contentDescription = title
)

coroutineScope.launch(Dispatchers.IO) {
coroutineScope.launch(Dispatchers.Main) {
Log.debug(
TAG,
"Loading content for ${comicBook.filename}:${cover.filename}"
)
coverContent = ArchiveAPI.loadPage(
coverContent = ArchiveAPI.loadCover(
comicBook.path,
cover.filename
)
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ kmp-io = { group = "io.github.skolson", name = "kmp-io", version = "0.2.1" }
kfs-watch = { group = "io.github.irgaly.kfswatch", name = "kfswatch", version = "1.3.0" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version = "0.6.2" }
multiplatform-settings = { group = "com.russhwolf", name = "multiplatform-settings", version = "1.3.0" }
stately-concurrent-collections = { group = "co.touchlab", name = "stately-concurrent-collections", version = "2.0.0" }

# koin
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin-version" }
Expand Down
23 changes: 21 additions & 2 deletions iosVariant/iosVariant/Data/ImageLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ImageLoader: ObservableObject {

if comicBook.pages.count > 0 {
self.pageFilename = (comicBook.pages[0] as! ComicPage).filename
doLoadPage()
doLoadCover()
}
}

Expand All @@ -43,13 +43,32 @@ class ImageLoader: ObservableObject {
doLoadPage()
}

private func doLoadPage() {
private func doLoadCover() {
Task {
Log().debug(
tag: TAG,
message:
"Loading cover image: \(self.comicFilename):\(self.pageFilename)"
)
let imageData = try await ArchiveAPI().loadCover(
comicFilename: self.comicFilename,
pageFilename: self.pageFilename
)

if imageData != nil {
self.image = imageData!.toUIImage()
}
}

}

private func doLoadPage() {
Task {
Log().debug(
tag: TAG,
message:
"Loading page image: \(self.comicFilename):\(self.pageFilename)"
)
let imageData = try await ArchiveAPI().loadPage(
comicFilename: self.comicFilename,
pageFilename: self.pageFilename
Expand Down
13 changes: 9 additions & 4 deletions iosVariant/iosVariant/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct HomeView: View {
var body: some View {
TabView(selection: $currentDestination) {
ComicBooksView(
comicBook: variantViewModel.comicBook,
comicBookList: variantViewModel.comicBookList,
selectionMode: variantViewModel.selectionMode,
selectionList: variantViewModel.selectionList,
Expand All @@ -44,22 +45,26 @@ struct HomeView: View {
Log().info(
tag: TAG,
message:
"Toggling comic book select: \(comicBook.path)"
"Toggling comic book select: \(comicBook!.path)"
)
variantViewModel.updateSelectionList(
filename: comicBook.path
filename: comicBook!.path
)
} else {
Log().info(
tag: TAG,
message: "Reading comic book: \(comicBook.filename)"
message: "Reading comic book: \(comicBook?.filename)"
)
variantViewModel.readComicBook(comicBook: comicBook)
currentDestination = .comics
}
},
onDeleteComics: {
Log().info(tag: TAG, message: "Deleting \(variantViewModel.selectionList.count) comic book(s)")
Log().info(
tag: TAG,
message:
"Deleting \(variantViewModel.selectionList.count) comic book(s)"
)
Task {
try await variantViewModel.deleteSelections()
}
Expand Down
71 changes: 41 additions & 30 deletions iosVariant/iosVariant/Views/Comics/ComicBooksView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,45 +23,53 @@ import shared
private let TAG = "ComicBooksView"

struct ComicBooksView: View {
let comicBook: ComicBook?
let comicBookList: [ComicBook]
let selectionMode: Bool
let selectionList: [String]

var onSetSelectionMode: (Bool) -> Void
var onComicClicked: (ComicBook) -> Void
var onComicClicked: (ComicBook?) -> Void
var onDeleteComics: () -> Void

var body: some View {
NavigationStack {
ComicBookListView(
comicBookList: comicBookList,
selectionList: selectionList,
onClick: { comicBook in onComicClicked(comicBook) }
if comicBook != nil {
ReadingView(
comicBook: comicBook!,
onStopReading: { onComicClicked(nil) }
)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
if selectionMode {
Button {
onSetSelectionMode(false)
} label: {
Image("selection_mode_on")
}
} else {
NavigationStack {
ComicBookListView(
comicBookList: comicBookList,
selectionList: selectionList,
onClick: { comicBook in onComicClicked(comicBook) }
)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
if selectionMode {
Button {
onSetSelectionMode(false)
} label: {
Image("selection_mode_on")
}

Button {
onDeleteComics()
} label: {
Image(systemName: "trash.fill")
}
.disabled(selectionList.isEmpty)
} else {
Button {
onSetSelectionMode(true)
} label: {
Image("selection_mode_off")
Button {
onDeleteComics()
} label: {
Image(systemName: "trash.fill")
}
.disabled(selectionList.isEmpty)
} else {
Button {
onSetSelectionMode(true)
} label: {
Image("selection_mode_off")
}
}
}

Spacer()
Spacer()
}
}
}
}
Expand All @@ -70,33 +78,36 @@ struct ComicBooksView: View {

#Preview {
ComicBooksView(
comicBook: nil,
comicBookList: COMIC_BOOK_LIST,
selectionMode: false,
selectionList: [],
onSetSelectionMode: { _ in },
onComicClicked: { _ in },
onDeleteComics: { }
onDeleteComics: {}
)
}

#Preview("selection mode on") {
ComicBooksView(
comicBook: nil,
comicBookList: COMIC_BOOK_LIST,
selectionMode: true,
selectionList: [COMIC_BOOK_LIST[0].path],
onSetSelectionMode: { _ in },
onComicClicked: { _ in },
onDeleteComics: { }
onDeleteComics: {}
)
}

#Preview("selection mode on no selections") {
ComicBooksView(
comicBook: nil,
comicBookList: COMIC_BOOK_LIST,
selectionMode: true,
selectionList: [],
onSetSelectionMode: { _ in },
onComicClicked: { _ in },
onDeleteComics: { }
onDeleteComics: {}
)
}
1 change: 1 addition & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ kotlin {
implementation(libs.kotlinx.datetime)
implementation(libs.multiplatform.settings)
implementation(libs.bundles.metadata)
implementation(libs.stately.concurrent.collections)

api(libs.kmp.viewmodel.core)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

package org.comixedproject.variant.adaptor

import co.touchlab.stately.collections.ConcurrentMutableMap
import com.oldguy.common.io.File
import com.oldguy.common.io.ZipFile
import org.comixedproject.variant.model.library.ComicBook
Expand All @@ -29,6 +30,10 @@ private const val TAG = "ArchiveAPI"
private const val UNKNOWN_METADATA = "Unknown"

public object ArchiveAPI {
val coverCache = ConcurrentMutableMap<String, ByteArray?>()
var cachedComic = ""
val pageCache = ConcurrentMutableMap<String, ByteArray?>()

suspend fun loadComicBook(archive: File): ComicBook {
val pages = mutableListOf<ComicPage>()
var metadata = ComicBookMetadata()
Expand Down Expand Up @@ -71,8 +76,39 @@ public object ArchiveAPI {
return result
}

suspend fun loadCover(comicFilename: String, pageFilename: String): ByteArray? {
val key = "${comicFilename}:${pageFilename}"

if (coverCache.contains(key)) {
Log.debug(TAG, "Retrieving cached cover for comic: ${comicFilename}")
return coverCache.get(key)
}

val image = loadImageFromFile(comicFilename, pageFilename)
Log.debug(TAG, "Caching cover image: ${key}")
coverCache.put(key, image)
Log.debug(TAG, "Returning ${image?.size} bytes for ${key}")
return image
}

suspend fun loadPage(comicFilename: String, pageFilename: String): ByteArray? {
Log.debug(TAG, "Loading page entry: ${comicFilename}:${pageFilename}")
if (!cachedComic.equals(comicFilename)) {
Log.debug(TAG, "Resetting page caching for comic: ${comicFilename}")
pageCache.clear()
cachedComic = comicFilename
} else if (pageCache.contains(pageFilename)) {
Log.debug(TAG, "Retrieving cached page for comic: ${comicFilename}:${pageFilename}")
return pageCache.get(pageFilename)
}

val image = loadImageFromFile(comicFilename, pageFilename)
Log.debug(TAG, "Caching page for comic: ${pageFilename}")
pageCache.put(pageFilename, image)
return image
}

private suspend fun loadImageFromFile(comicFilename: String, pageFilename: String): ByteArray? {
Log.debug(TAG, "Loading image from file: ${comicFilename}:${pageFilename}")
var result: ByteArray? = null
try {
ZipFile(File(comicFilename)).use { zip ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 <http://www.gnu.org/licenses>
*/

package org.comixedproject.variant.model.cache

import kotlinx.serialization.Serializable

@Serializable
data class ImageCache(
val entries: MutableMap<String, ByteArray?> = HashMap<String, ByteArray?>()
)
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ open class VariantViewModel(
Log.debug(TAG, "_libraryDirectory=${directory}")
_libraryDirectory = directory
viewModelScope.coroutineScope.launch {

if (!File(_libraryDirectory).exists) {
Log.info(TAG, "Creating library directory: ${_libraryDirectory}")
File(_libraryDirectory).makeDirectory()
Expand Down