fix: better local state handling

This commit is contained in:
2026-08-03 21:31:35 +02:00
parent 5dd596f0fb
commit 9a34d4e952
+81 -103
View File
@@ -21,10 +21,10 @@ enum ReadProgress: Codable {
case scroll(CGPoint)
}
struct ProgressIndices {
var v: Int
var c: Int
var i: Int
struct ProgressIndices: Codable {
var v: Int = 0
var c: Int = 0
var i: Int = 0
}
enum PageTurnMode: Codable, CaseIterable {
@@ -43,11 +43,13 @@ struct GlobalState: Codable {
var comicName: String?
}
/// nil indicates a default should be used, aka what is in settings.
struct LocalState: Codable {
var progress: ReadProgress
var progress: ProgressIndices = ProgressIndices()
var mode: PageTurnMode?
var backgroundColor: String?
var useDefaultPageTurnMode: Bool
var aniListTracking: Bool
var aniListTracking: Bool = false
var scrollPos: CGPoint = CGPoint()
}
struct Settings: Codable {
@@ -63,7 +65,6 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
var readerView = UIView()
@IBOutlet var scrollingCollectionView: UICollectionView!
var scrollPos: CGPoint!
var hasSetContentOffset = false
var pendingAlert: UIAlertController?
@@ -72,16 +73,13 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
let dragRangeForFullBrightness: CGFloat = 300
var imageView = UIImageView()
var mode = PageTurnMode.leftToRight
var useDefaultPageTurnMode = true
var useDefaultBackgroundColor = true
var metadataList: [URL: Metadata] = [:]
// directory name is key, indexPath.item can change.
var state: [String: LocalState] = [:]
var metadata: Metadata!
var progress = ProgressIndices(v: 0, c: 0, i: 0)
var currentPath: URL!
var lastLayoutSize: CGSize = .zero
var aniListToken: String? = getAniListToken()
var aniListTracking: [Int: Bool] = [:]
var currentItemIndex = 0
var leftTap: UITapGestureRecognizer!
@@ -126,9 +124,9 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
if let alert = pendingAlert {
present(alert, animated: false, completion: nil)
}
if !hasSetContentOffset, scrollPos != nil {
if !hasSetContentOffset, let scrollPos = state[currentPath.lastPathComponent]?.scrollPos {
let screenSize = UIScreen.main.bounds.size
var offset = scrollPos!
var offset = scrollPos
if screenSize.width > screenSize.height {
offset.y *= (screenSize.width / screenSize.height)
}
@@ -142,6 +140,20 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
}
func getMode() -> PageTurnMode {
if let state = state[currentPath.lastPathComponent] {
return state.mode ?? settings.defaultPageTurnMode
}
return settings.defaultPageTurnMode
}
func getProgress() -> ProgressIndices {
if let state = state[currentPath.lastPathComponent] {
return state.progress
}
return ProgressIndices()
}
func setup() {
do {
if try fileManager.contentsOfDirectory(atPath: documentsURL.path).isEmpty {
@@ -170,7 +182,6 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
readerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
readerView.widthAnchor.constraint(equalTo: view.widthAnchor),
])
mode = settings.defaultPageTurnMode
setupScrollingCollectionView()
loadComics()
setupImageView()
@@ -285,7 +296,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
metadataList[dir] = metadata
loadLocalState()
let volCovP = ProgressIndices(v: progress.v, c: 0, i: 0)
let volCovP = ProgressIndices(v: state[currentPath.lastPathComponent]!.progress.v, c: 0, i: 0)
let archivePath = getArchiveURL(volCovP.v)
let archive = try Archive(url: archivePath, accessMode: .read)
let filename = metadata.volumes[volCovP.v].chapters[volCovP.c].images[volCovP.i].filename!
@@ -772,37 +783,30 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
func saveLocalState() {
guard let currentDir = currentPath?.lastPathComponent else { print("did not save currentPath nil"); return }
guard let currentState = state[currentDir] else { print("did not save currentState nil"); return }
let progress = currentState.progress
let mode = currentState.mode
let color = readerView.backgroundColor ?? .black
let aniListTracking = currentState.aniListTracking
let screenSize = UIScreen.main.bounds.size
var scrollOffset = scrollingCollectionView.contentOffset
if screenSize.width > screenSize.height {
scrollOffset.y *= (screenSize.height / screenSize.width)
}
var newProgress =
ReadProgress.leftToRight(
volumeIndex: progress.v, chapterIndex: progress.c,
imageIndex: progress.i
)
// TODO: Calucate the offset based on progress, to allow seamless switching.
if mode != .scroll {}
switch mode {
case .leftToRight:
newProgress = ReadProgress.leftToRight(
volumeIndex: progress.v, chapterIndex: progress.c,
imageIndex: progress.i
)
case .rightToLeft: newProgress = ReadProgress.rightToLeft(
volumeIndex: progress.v, chapterIndex: progress.c,
imageIndex: progress.i
)
case .scroll: newProgress = ReadProgress.scroll(scrollOffset)
}
queue.async {
do {
try JSONEncoder().encode(
LocalState(
progress: newProgress, backgroundColor: self.convertColorToString(color),
useDefaultPageTurnMode: self.useDefaultPageTurnMode,
aniListTracking: self.aniListTracking[self.currentItemIndex] ?? false
progress: progress,
mode: mode,
backgroundColor: self.convertColorToString(color),
aniListTracking: aniListTracking,
scrollPos: scrollOffset
)
).write(
to: self.currentPath.appendingPathComponent("state.json")
@@ -814,59 +818,22 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
func loadLocalState() {
if state[currentPath.lastPathComponent] != nil { return }
do {
let path = currentPath.appendingPathComponent("state.json").path
if !fileManager.fileExists(atPath: path) {
progress = ProgressIndices(v: 0, c: 0, i: 0)
mode = settings.defaultPageTurnMode
state[currentPath.lastPathComponent] = LocalState()
return
}
let json = try Data(String(contentsOfFile: path).utf8)
let local = try JSONDecoder().decode(LocalState.self, from: json)
useDefaultPageTurnMode = local.useDefaultPageTurnMode
aniListTracking[currentItemIndex] = local.aniListTracking
switch local.progress {
case let .leftToRight(volumeIndex, chapterIndex, imageIndex):
progress.v = volumeIndex
progress.c = chapterIndex
progress.i = imageIndex
mode = useDefaultPageTurnMode ? settings.defaultPageTurnMode : .leftToRight
case let .rightToLeft(volumeIndex, chapterIndex, imageIndex):
progress.v = volumeIndex
progress.c = chapterIndex
progress.i = imageIndex
mode = useDefaultPageTurnMode ? settings.defaultPageTurnMode : .rightToLeft
case let .scroll(point):
if scrollPos == nil {
scrollPos = point
let screenSize = UIScreen.main.bounds.size
var scrollOffset = point
if screenSize.width > screenSize.height {
scrollOffset.y *= (screenSize.width / screenSize.height)
}
if let indexPath = scrollingCollectionView.indexPathForItem(at: scrollOffset) {
var theProgress = ProgressIndices(v: 0, c: 0, i: 0)
for _ in 0 ..< indexPath.item {
theProgress = getProgressIndicesFromTurn(
turn: .next, progress: theProgress
)
}
progress = theProgress
}
}
mode = useDefaultPageTurnMode ? settings.defaultPageTurnMode : .scroll
}
if let color = local.backgroundColor {
readerView.backgroundColor = convertStringToColor(color)
useDefaultBackgroundColor = false
} else {
readerView.backgroundColor = convertStringToColor(settings.defaultReadingBackgroundColor)
useDefaultBackgroundColor = true
}
state[currentPath.lastPathComponent] = local
} catch let decodingError as DecodingError {
print(decodingError.errorDescription!)
print("Failed to decode state.json, setting default: \(decodingError.errorDescription ?? "")")
state[currentPath.lastPathComponent] = LocalState()
} catch {
print("Unexpected error: \(error)")
print("Failed to decode state.json, setting default: \(error)")
state[currentPath.lastPathComponent] = LocalState()
}
}
@@ -899,8 +866,9 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
for _ in 0 ..< indexPath.item {
newProgress = getProgressIndicesFromTurn(turn: .next, progress: newProgress)
}
guard let progress = state[currentPath.lastPathComponent]?.progress else { return }
aniListProgressUpdate(progress: progress, newProgress: newProgress)
progress = newProgress
state[currentPath.lastPathComponent]!.progress = newProgress
}
if scrollingCollectionView.isHidden == false {
saveLocalState()
@@ -912,10 +880,11 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
guard view.bounds.size != lastLayoutSize else { return }
lastLayoutSize = UIScreen.main.bounds.size
if scrollingCollectionView.isHidden
|| mode != .scroll
|| getMode() != .scroll
|| scrollingCollectionView.contentSize == .zero
{
if metadata == nil { return }
let progress = getProgress()
imageLoader.loadImage(
archiveURL: getArchiveURL(progress.v),
filename: getImagePath(progress),
@@ -936,6 +905,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
if let path = getPathFromComicName(name: name) {
currentPath = path
metadata = metadataList[path]
let mode = getMode()
globalState.comicName = metadata.title
saveGlobalState()
@@ -1187,7 +1157,9 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
let point = gesture.location(in: comicCollectionView)
guard let indexPath = comicCollectionView.indexPathForItem(at: point) else { return }
currentPath = comics[indexPath.item].path
loadLocalState()
comicOptionLongPressed(indexPath)
}
@@ -1196,7 +1168,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
@objc func handleLeftTap() {
switch mode {
switch getMode() {
case .rightToLeft: changeImage(turn: .next)
case .leftToRight: changeImage(turn: .previous)
case .scroll: break
@@ -1204,7 +1176,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
@objc func handleRightTap() {
switch mode {
switch getMode() {
case .rightToLeft: changeImage(turn: .previous)
case .leftToRight: changeImage(turn: .next)
case .scroll: break
@@ -1214,13 +1186,13 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
@objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
switch gesture.direction {
case .left:
switch mode {
switch getMode() {
case .rightToLeft: changeImage(turn: .previous)
case .leftToRight: changeImage(turn: .next)
case .scroll: break
}
case .right:
switch mode {
switch getMode() {
case .rightToLeft: changeImage(turn: .next)
case .leftToRight: changeImage(turn: .previous)
case .scroll: break
@@ -1280,13 +1252,14 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
func changeImage(turn: PageTurn) {
let scaling = UIView.ContentMode.scaleAspectFit
let progress = getProgress()
var newProgress = progress
newProgress = getProgressIndicesFromTurn(turn: turn, progress: newProgress)
if newProgress.v == progress.v, newProgress.c == progress.c, newProgress.i == progress.i {
return
}
aniListProgressUpdate(progress: progress, newProgress: newProgress)
progress = newProgress
state[currentPath.lastPathComponent]!.progress = newProgress
updateInfo()
let path = getImagePath(progress)
@@ -1326,6 +1299,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
func updateInfo() {
if metadata == nil { return }
let progress = getProgress()
var text = "\(metadata.title ?? "")\n"
@@ -1427,6 +1401,8 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
func setImages() {
let mode = getMode()
let progress = getProgress()
let scaling: UIView.ContentMode = mode == .scroll ? .scaleAspectFill : .scaleAspectFit
imageLoader.loadImage(
archiveURL: getArchiveURL(progress.v),
@@ -1560,7 +1536,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
else {
return ScrollingImageCell()
}
if mode != .scroll { return cell }
if getMode() != .scroll { return cell }
if metadata == nil {
print("metadata is nil, should probably not be the case")
return cell
@@ -1571,6 +1547,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
newProgress = getProgressIndicesFromTurn(turn: .next, progress: newProgress)
}
}
let progress = getProgress()
imageLoader.loadImage(
archiveURL: getArchiveURL(progress.v),
filename: getImagePath(newProgress),
@@ -2244,11 +2221,12 @@ extension ViewController {
return SettingsMenuItem(
title: title,
children: nil, action: { [self] in
self.useDefaultBackgroundColor = color == nil
self.state[currentPath.lastPathComponent]!.backgroundColor = color == nil
? nil : convertColorToString(color!)
colorHandler(color ?? self.convertStringToColor(settings.defaultReadingBackgroundColor))
self.dismiss(animated: false)
}, selected: { [self] in
if self.useDefaultBackgroundColor { return color == nil }
if self.state[currentPath.lastPathComponent]!.backgroundColor == nil { return color == nil }
return color == self.readerView.backgroundColor && color != nil
}
)
@@ -2266,7 +2244,7 @@ extension ViewController {
self.dismiss(animated: false)
},
selected: { [self] in
return self.mode == pageTurnMode
return self.getMode() == pageTurnMode
}
)
}
@@ -2284,14 +2262,12 @@ extension ViewController {
return SettingsMenuItem(
title: title,
children: nil, action: { [self] in
self.useDefaultPageTurnMode = pageTurnMode == nil
let mode = pageTurnMode ?? self.settings.defaultPageTurnMode
let mode = getMode()
pageTurnHandler(mode)
self.dismiss(animated: false)
},
selected: { [self] in
if self.useDefaultPageTurnMode { return pageTurnMode == nil }
return self.mode == pageTurnMode && pageTurnMode != nil
return self.getMode() == pageTurnMode
}
)
}
@@ -2334,9 +2310,11 @@ extension ViewController {
private func makePageTurnTree() -> [SettingsMenuItem] {
return pageTurnDefaultItems { [self] pageTurnMode in
let mode = getMode()
let progress = getProgress()
if mode == pageTurnMode { return }
let scrollSwitch = mode != .scroll && pageTurnMode != .scroll
mode = pageTurnMode
state[currentPath.lastPathComponent]!.mode = pageTurnMode
saveLocalState()
if scrollSwitch { return }
let archiveURL = getArchiveURL(progress.v)
@@ -2426,7 +2404,7 @@ extension ViewController {
case .updateStatus: children = aniListUpdateStatusItems(indexPath)
case .fetchMetadata: _ = 0
case .track:
if aniListTracking[indexPath.item] ?? false {
if state[currentPath.lastPathComponent]!.aniListTracking {
title += " (Enabled)"
} else {
title += " (Disabled)"
@@ -2435,14 +2413,14 @@ extension ViewController {
return ComicOptionItem(
title: title, children: children,
destructive: false, available: { true },
action: { [weak self] in
action: { [self] in
if action == .track {
var v = self?.aniListTracking[indexPath.item] ?? false
var v = self.state[currentPath.lastPathComponent]!.aniListTracking
v.toggle()
self?.aniListTracking[indexPath.item] = v
self?.saveLocalState()
self.state[self.currentPath.lastPathComponent]!.aniListTracking = v
self.saveLocalState()
}
self?.dismiss(animated: false)
self.dismiss(animated: false)
}
)
}