This commit is contained in:
2026-07-17 20:44:34 +02:00
parent b2ff1dc624
commit 73d7309b76
4 changed files with 165 additions and 108 deletions
+4
View File
@@ -0,0 +1,4 @@
[*.swift]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
-1
View File
@@ -2,7 +2,6 @@
/.nvim
.DS_Store
buildServer.json
.swiftlint.yml
/ImageViewer.xcodeproj/xcuserdata
/ImageViewer.xcodeproj/project.xcworkspace/xcuserdata
/ImageViewer/flatc
+19
View File
@@ -0,0 +1,19 @@
function_body_length: 1000
type_body_length:
warning: 10000
trailing_comma:
mandatory_comma: true
line_length: 120
opening_brace:
ignore_multiline_statement_conditions: true
identifier_name:
min_length: 1
allowed_symbols: ["_"]
file_length:
error: 10000
warning: 3000
cyclomatic_complexity:
warning: 50
error: 100
disabled_rules:
- todo
+142 -107
View File
@@ -8,7 +8,8 @@ import UIKit
import ZIPFoundation
let preloadCount = 2
/// With a whopping 2 cores, and 2 threads because no hyperthreading, it makes sense to only have a queue as an extra thread to do work with to allow the main thread for ui.
/// With a whopping 2 cores, and 2 threads because no hyperthreading,
/// it makes sense to only have a queue as an extra thread to do work with to allow the main thread for ui.
let queue = DispatchQueue(label: "queue", qos: .utility)
enum PageTurn {
@@ -41,7 +42,7 @@ struct Comic {
}
struct GlobalState: Codable {
var comicName: String? = nil
var comicName: String?
}
struct LocalState: Codable {
@@ -170,7 +171,11 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
func loadComics() {
let alert = UIAlertController(title: "Failed to load comics.", message: "This is an alert.", preferredStyle: .alert)
let alert = UIAlertController(
title: "Failed to load comics.",
message: "This is an alert.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
do {
var directories: [URL] = []
@@ -202,16 +207,18 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
currentPath = dir
guard let data = try? Data(contentsOf: dir.appendingPathComponent(metadataFilename))
let metadataPath = dir.appendingPathComponent(metadataFilename)
guard let data = try? Data(contentsOf: metadataPath)
else {
alert.message = String(format: "Failed to read contents of metadata: \(dir.appendingPathComponent(metadataFilename))")
alert.message = String(format: "Failed to read contents of metadata: \(metadataPath))")
pendingAlert = alert
return
}
var byteBuffer = ByteBuffer(data: data)
metadata = try? getCheckedRoot(byteBuffer: &byteBuffer)
if metadata == nil {
alert.message = String(format: "Failed to read contents of metadata flatbuffer file: \(dir.appendingPathComponent(metadataFilename))")
alert.message =
String(format: "Failed to read contents of metadata flatbuffer file: \(metadataPath))")
pendingAlert = alert
return
}
@@ -307,21 +314,60 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
return CGSize(width: width, height: height)
}
struct Pattern {
var pattern: NSRegularExpression
var title: NSRegularExpression
var volumePosition: Int
var chapterPosition: Int
var pagePosition: Int
}
func getMetadataFromFilename(path: URL) {
let alert = UIAlertController(
title: "Failed to get metadata from filename.",
message: "This is an alert.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
// Beautiful, is it not?
let patterns: [(NSRegularExpression?, NSRegularExpression?, Int, Int, Int)] = [
(try? NSRegularExpression(pattern: #"c([0-9]+(?:x[0-9]+)?) \(v([0-9]+)\) - p([0-9]+(?:-p?[0-9]+)?)"#), try? NSRegularExpression(pattern: #"^(.*) - c([0-9]+(?:x[0-9]+)?) \(v([0-9]+)\) - p([0-9]+(?:-p?[0-9]+)?)"#), 3, 1, 2),
(try? NSRegularExpression(pattern: "v([0-9]+) p([0-9]+(?:-[0-9]+)?) - c([0-9]+(?:x[0-9]+)?)"), try? NSRegularExpression(pattern: "^(.*) - v([0-9]+) p([0-9]+(?:-[0-9]+)?) - c([0-9]+(?:x[0-9]+)?)"), 2, 3, 1),
]
var pattern: NSRegularExpression! = nil
var titlePattern: NSRegularExpression! = nil
var pageRegexPosition: Int! = nil
var chapterRegexPosition: Int! = nil
var volumeRegexPosition: Int! = nil
var currentVolume: UInt32! = nil
var currentChapter: (UInt32, UInt32?)!
var nPatterns: [Pattern]?
do {
let fPatterns: [Pattern] = try [
Pattern(
pattern: NSRegularExpression(
pattern: #"c([0-9]+(?:x[0-9]+)?) \(v([0-9]+)\) - p([0-9]+(?:-p?[0-9]+)?)"#
),
title: NSRegularExpression(
pattern: #"^(.*) - c([0-9]+(?:x[0-9]+)?) \(v([0-9]+)\) - p([0-9]+(?:-p?[0-9]+)?)"#
),
volumePosition: 2,
chapterPosition: 1,
pagePosition: 3
),
Pattern(
pattern: NSRegularExpression(
pattern: "v([0-9]+) p([0-9]+(?:-[0-9]+)?) - c([0-9]+(?:x[0-9]+)?)"
),
title: NSRegularExpression(
pattern: "^(.*) - v([0-9]+) p([0-9]+(?:-[0-9]+)?) - c([0-9]+(?:x[0-9]+)?)"
),
volumePosition: 1,
chapterPosition: 3,
pagePosition: 2
),
]
nPatterns = fPatterns
} catch {
alert.message = String(format: "Regular expression is invalid")
pendingAlert = alert
return
}
guard let patterns = nPatterns else {
alert.message = String(format: "Regular expression is invalid")
pendingAlert = alert
return
}
if fileManager.fileExists(
atPath: path.appendingPathComponent(metadataFilename).absoluteString
@@ -339,8 +385,8 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
var titleValue = ""
let alert = UIAlertController(title: "Failed to get metadata from filename.", message: "This is an alert.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
var currentVolume: UInt32!
var currentChapter: (UInt32, UInt32?)!
let volumeDir = path.appendingPathComponent("volumes")
@@ -370,18 +416,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
for volumeURL in volumeURLs! {
do {
let volumeArchive = try Archive(url: volumeURL, accessMode: .read)
var volumeArchiveFiles: [String] = []
for entry in volumeArchive {
// guard let url = URL(string: entry.path) else {
// alert.message = String(format: "failed to create URL from string: \(entry.path)")
// pendingAlert = alert
// return
// }
volumeArchiveFiles.append(entry.path)
}
for filename in volumeArchiveFiles.sorted() {
filenames.append((volumeURL.lastPathComponent, filename))
}
volumeArchive.map { $0.path }.sorted().forEach { filenames.append((volumeURL.lastPathComponent, $0)) }
} catch {
alert.message = String(format: "failed to read archive: \(volumeURL)")
pendingAlert = alert
@@ -394,77 +429,61 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
return
}
var matchingPattern: Pattern?
do {
for (currentPattern, currentTitlePattern, currentPageRegexPosition, currentChapterRegexPosition, currentVolumeRegexPosition) in patterns {
guard let currentPattern = currentPattern else {
alert.message = String(format: "failed to get currentPattern")
pendingAlert = alert
continue
}
guard let currentTitlePattern = currentTitlePattern else {
alert.message = String(format: "failed to get currentTitlePattern")
pendingAlert = alert
continue
}
for testPattern in patterns {
let filename = filenames[0].1
let range = NSRange(filename.startIndex..., in: filename)
if let match = currentTitlePattern.firstMatch(in: filename, range: range) {
if let match = testPattern.title.firstMatch(in: filename, range: range) {
if (filename as NSString).substring(with: match.range(at: 1)) == "" {
continue
}
} else {
continue
}
if let match = currentPattern.firstMatch(in: filename, range: range) {
if (filename as NSString).substring(with: match.range(at: currentPageRegexPosition)) == "" {
if let match = testPattern.pattern.firstMatch(in: filename, range: range) {
if (filename as NSString).substring(with: match.range(at: testPattern.pagePosition)) == "" {
continue
}
} else {
continue
}
if let match = currentPattern.firstMatch(in: filename, range: range) {
if (filename as NSString).substring(with: match.range(at: currentChapterRegexPosition)) == "" {
if let match = testPattern.pattern.firstMatch(in: filename, range: range) {
if (filename as NSString).substring(with: match.range(at: testPattern.chapterPosition)) == "" {
continue
}
} else {
continue
}
if let match = currentPattern.firstMatch(in: filename, range: range) {
if (filename as NSString).substring(with: match.range(at: currentVolumeRegexPosition)) == "" {
if let match = testPattern.pattern.firstMatch(in: filename, range: range) {
if (filename as NSString).substring(with: match.range(at: testPattern.volumePosition)) == "" {
continue
}
} else {
continue
}
pattern = currentPattern
titlePattern = currentTitlePattern
pageRegexPosition = currentPageRegexPosition
chapterRegexPosition = currentChapterRegexPosition
volumeRegexPosition = currentVolumeRegexPosition
matchingPattern = testPattern
break
}
if pattern == nil || titlePattern == nil || pageRegexPosition == nil || chapterRegexPosition == nil || volumeRegexPosition == nil {
print("Could not find pattern, returning")
guard let pattern = matchingPattern else {
alert.message = String(format: "Could not find matching pattern, likely missing regex.")
pendingAlert = alert
return
}
let regex = pattern!
let titleRegex = titlePattern!
for (i, (volumeURL, filename)) in filenames.enumerated() {
let range = NSRange(filename.startIndex..., in: filename)
if i == 0 {
if let match = titleRegex.firstMatch(in: filename, range: range) {
if let match = pattern.title.firstMatch(in: filename, range: range) {
titleValue = (filename as NSString).substring(with: match.range(at: 1))
} else {
print("Did not find match for title, regex did not handle this case.")
return
}
}
if let match = regex.firstMatch(in: filename, range: range) {
let chapterStr = (filename as NSString).substring(with: match.range(at: chapterRegexPosition))
if let match = pattern.pattern.firstMatch(in: filename, range: range) {
let chapterStr = (filename as NSString).substring(with: match.range(at: pattern.chapterPosition))
let chapterParts = chapterStr.split(separator: "x", maxSplits: 1)
if chapterParts.count < 1 {
alert.message = String(format: "Did not find chapter number in filename: \(filename)")
@@ -480,13 +499,16 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
nChapter0, chapterParts.count > 1 ? UInt32(chapterParts[1]) : nil
)
guard let volume = UInt32((filename as NSString).substring(with: match.range(at: volumeRegexPosition))) else {
guard let volume =
UInt32((filename as NSString)
.substring(with: match.range(at: pattern.volumePosition)))
else {
alert.message = String(format: "failed to get match for volume in filename: \(filename)")
pendingAlert = alert
return
}
let pageStr = (filename as NSString).substring(with: match.range(at: pageRegexPosition))
let pageStr = (filename as NSString).substring(with: match.range(at: pattern.pagePosition))
let pageParts = pageStr.split(separator: "-", maxSplits: 1)
guard let nPage = UInt32(pageParts[0]) else {
alert.message = String(format: "failed to read archive: \(filename)")
@@ -798,7 +820,8 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
super.viewDidLayoutSubviews()
guard view.bounds.size != lastLayoutSize else { return }
lastLayoutSize = UIScreen.main.bounds.size
if scrollingCollectionView.isHidden == true || mode != .scroll
if scrollingCollectionView.isHidden == true
|| mode != .scroll
|| scrollingCollectionView.contentSize == .zero
{
if metadata == nil { return }
@@ -1172,7 +1195,8 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
leftView.isHidden = mode == .scroll
rightView.isHidden = mode == .scroll
imageView.isHidden = mode == .scroll
// This does not optmize for the case where you switch between scrolling and non-scrolling in the same comic. However this badly supported because of the lac of state conversion.
// This does not optmize for the case where you switch between scrolling and non-scrolling in the same comic.
// However this is badly supported because of the lack of state conversion.
if mode == .scroll {
scrollingCollectionView.reloadData()
}
@@ -1352,7 +1376,10 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
let chapterValue = metadata.volumes[progress.v].chapters[progress.c].chapter!
let chapterString = metaValueToString(m: chapterValue)
let lastChapterValue = metadata.volumes[metadata.volumes.count - 1].chapters[metadata.volumes[metadata.volumes.count - 1].chapters.count - 1].chapter!
let lastChapterValue = metadata
.volumes[metadata.volumes.count - 1]
.chapters[metadata.volumes[metadata.volumes.count - 1]
.chapters.count - 1].chapter!
let lastChapterString = metaValueToString(m: lastChapterValue)
let volumeValue = metadata.volumes[progress.v].volume!
@@ -1389,12 +1416,15 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
Chapter \(chapterString) of \(lastChapterString)
Page \(pageString) of \(lastPageString)
"""
text += "\nImage size: \(metadata.volumes[progress.v].chapters[progress.c].images[progress.i].size!.width)x\(metadata.volumes[progress.v].chapters[progress.c].images[progress.i].size!.height)"
let imageSize = metadata.volumes[progress.v].chapters[progress.c].images[progress.i].size!
text += "\nImage size: \(imageSize.width)x\(imageSize.height)"
info.text = text
}
func getArchiveURL(_ volumeProgress: Int) -> URL {
return currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[volumeProgress].archive)
return currentPath
.appendingPathComponent("volumes")
.appendingPathComponent(metadata.volumes[volumeProgress].archive)
}
func getImagePath(_ progress: ProgressIndices) -> String {
@@ -1440,9 +1470,9 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
func getMetadata(path: URL) -> Metadata? {
let data = try! Data(contentsOf: path.appendingPathComponent(metadataFilename))
guard let data = try? Data(contentsOf: path.appendingPathComponent(metadataFilename)) else { return nil }
var byteBuffer = ByteBuffer(data: data)
let metadata: Metadata = try! getCheckedRoot(byteBuffer: &byteBuffer)
guard let metadata: Metadata = try? getCheckedRoot(byteBuffer: &byteBuffer) else { return nil }
return metadata
}
@@ -1471,10 +1501,8 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
func getPathFromComicName(name: String) -> URL? {
for comic in comics {
if comic.metadata.title == name {
return comic.path
}
for comic in comics where comic.metadata.title == name {
return comic.path
}
return nil
}
@@ -1512,11 +1540,7 @@ func getDocumentsURL() -> URL? {
}
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(
_ collectionView: UICollectionView, numberOfItemsInSection _: Int
)
-> Int
{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection _: Int) -> Int {
if collectionView == comicCollectionView {
return comics.count
} else if collectionView == scrollingCollectionView {
@@ -1534,24 +1558,30 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
return 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath)
-> UICollectionViewCell
{
func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
if collectionView == comicCollectionView {
let cell =
guard let cell =
collectionView.dequeueReusableCell(
withReuseIdentifier: "ComicImageCell", for: indexPath
)
as! ComicImageCell
) as? ComicImageCell
else {
return ComicImageCell()
}
cell.imageView.image = comics[indexPath.item].cover
return cell
} else if collectionView == scrollingCollectionView {
let scaling = UIView.ContentMode.scaleAspectFill
let cell =
guard let cell =
collectionView.dequeueReusableCell(
withReuseIdentifier: "ScrollingImageCell", for: indexPath
)
as! ScrollingImageCell
as? ScrollingImageCell
else {
return ScrollingImageCell()
}
if mode != .scroll { return cell }
if metadata == nil {
print("metadata is nil, should probably not be the case")
@@ -1576,11 +1606,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
}
// Xcode profiling sucks:
return
collectionView.dequeueReusableCell(
withReuseIdentifier: "ScrollingImageCell", for: indexPath
)
as! ScrollingImageCell
return ComicImageCell()
}
func collectionView(
@@ -1605,7 +1631,9 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
let imageMetadata = metadata.volumes[newProgress.v].chapters[newProgress.c].images[newProgress.i]
return CGSize(
width: readerView.bounds.width,
height: CGFloat(imageMetadata.size!.height) * readerView.bounds.width / CGFloat(imageMetadata.size!.width)
height: CGFloat(imageMetadata.size!.height)
* readerView.bounds.width
/ CGFloat(imageMetadata.size!.width)
)
} else {
assertionFailure()
@@ -1696,7 +1724,8 @@ class ImageLoader {
landscapeFitCache.totalCostLimit = 128 * 1024 * 1024
portraitFillCache.totalCostLimit = 128 * 1024 * 1024
landscapeFillCache.totalCostLimit = 128 * 1024 * 1024
// The reason archives are cached and have a strict limit of four, is because Archive supposedly opens a posix file pointer and keeps it until the object is destroyed.
// The reason archives are cached and have a strict limit of four,
// is because Archive supposedly opens a posix file pointer and keeps it until the object is destroyed.
archives.countLimit = 4
}
@@ -1759,14 +1788,16 @@ class ImageLoader {
if token.isCancelled { finish(); return }
func extractData() -> Data {
func extractData() -> Data? {
var extractedData = Data()
if self.archives.object(forKey: archiveURL.lastPathComponent as NSString) == nil {
let archive = try! Archive(url: archiveURL, accessMode: .read)
self.archives.setObject(archive, forKey: archiveURL.lastPathComponent as NSString)
do {
let archive = try Archive(url: archiveURL, accessMode: .read)
self.archives.setObject(archive, forKey: archiveURL.lastPathComponent as NSString)
} catch { return nil }
}
let archive = self.archives.object(forKey: archiveURL.lastPathComponent as NSString)!
_ = try! archive.extract(archive[filename]!) { data in
_ = try? archive.extract(archive[filename]!) { data in
extractedData.append(data)
}
return extractedData
@@ -1774,7 +1805,7 @@ class ImageLoader {
if token.isCancelled { finish(); return }
guard let image = cache.object(forKey: key as NSString) ?? UIImage(data: extractData()) else {
guard let image = cache.object(forKey: key as NSString) ?? UIImage(data: extractData()!) else {
finish()
return
}
@@ -1808,7 +1839,13 @@ class ImageLoader {
}
}
func scaleAndCacheImage(image: UIImage, orientation: Rotation, screenSize: CGSize, scaling: UIView.ContentMode, filename: String) -> UIImage {
func scaleAndCacheImage(
image: UIImage,
orientation: Rotation,
screenSize: CGSize,
scaling: UIView.ContentMode,
filename: String
) -> UIImage {
let resizeTo = if orientation == .vertical {
CGSize(
width: min(screenSize.width, screenSize.height),
@@ -1860,9 +1897,7 @@ func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage {
return scaledImage!
}
func aspectSize(for imageSize: CGSize, in boundingSize: CGSize, scaling: UIView.ContentMode)
-> CGSize
{
func aspectSize(for imageSize: CGSize, in boundingSize: CGSize, scaling: UIView.ContentMode) -> CGSize {
let widthRatio = boundingSize.width / imageSize.width
let heightRatio = boundingSize.height / imageSize.height
var scale = CGFloat()