From 375467e83088cafc9edfdbb3628676493e399490 Mon Sep 17 00:00:00 2001 From: Vegard Matthey Date: Sun, 13 Jul 2025 23:00:03 +0200 Subject: [PATCH] use subviews for home and reader --- ImageViewer/ViewController.swift | 454 +++++++++++++++++++++++++------ 1 file changed, 368 insertions(+), 86 deletions(-) diff --git a/ImageViewer/ViewController.swift b/ImageViewer/ViewController.swift index 5829f8a..4b2510e 100644 --- a/ImageViewer/ViewController.swift +++ b/ImageViewer/ViewController.swift @@ -11,11 +11,51 @@ enum PageTurnMode { case scroll } +struct Comic { + var cover: UIImage + var metadata: Metadata + var path: URL +} + +struct GlobalState: Codable { + var comicName: String? = nil +} + +struct Metadata: Decodable { + var title: String + var original_language: String + var last_volume: Float + var last_chapter: Float + var publication_demographic: String + var status: String + var year: Int? + var content_rating: String + var state: String + var created_at: String + var updated_at: String + var chapters: [ChapterMetadata] +} + +struct ChapterMetadata: Decodable { + var chapter: Float + var volume: Float + var title: String + var pages: Int +} + class ViewController: UIViewController, UIGestureRecognizerDelegate { + var homeView = UIView() + var readerView = UIView() + var imageView = UIImageView() - var images: [UIImage] = [] - var page = 0 + var previousImage: UIImage? + var currentImage: UIImage? + var nextImage: UIImage? + var currentPage = 1 var mode = PageTurnMode.leftToRight + var metadata: Metadata! + var currentChapter: Int! + var currentPath: URL! var leftTap: UITapGestureRecognizer! var rightTap: UITapGestureRecognizer! @@ -29,33 +69,142 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { let backgroundColorDropdownView = UIView() let backgroundColorDropdownButton = UIButton() + let fileManager = FileManager.default + var globalState = getGlobalState() + + let documentsURL = getDocumentsURL().unsafelyUnwrapped + + var comics: [Comic] = [] + var collectionView: UICollectionView! + override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .white + view.backgroundColor = .clear setup() } + func setupHomeView() { + loadComics() + + let layout = UICollectionViewFlowLayout() + let spacing: CGFloat = 8 + layout.minimumInteritemSpacing = spacing + layout.minimumLineSpacing = spacing + + collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.dataSource = self + collectionView.delegate = self + collectionView.backgroundColor = .white + collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "ImageCell") + homeView.addSubview(collectionView) + } + func setup() { - createTestFile() - setupImageView() - setupGestures() - setupTopBar() + homeView.translatesAutoresizingMaskIntoConstraints = false + readerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(homeView) + view.addSubview(readerView) + NSLayoutConstraint.activate([ + homeView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + homeView.topAnchor.constraint(equalTo: view.topAnchor), + homeView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + homeView.widthAnchor.constraint(equalTo: view.widthAnchor), + + readerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + readerView.topAnchor.constraint(equalTo: view.topAnchor), + readerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + readerView.widthAnchor.constraint(equalTo: view.widthAnchor), + ]) + if let name = globalState.comicName { + globalState.comicName = name + saveGlobalState() + readComic(name: name) + } else { + readerView.isHidden = true + homeView.isHidden = false + setupHomeView() + } + } + + func loadComics() { + do { + var directories: [URL] = [] + + let contents = try fileManager.contentsOfDirectory( + at: documentsURL, + includingPropertiesForKeys: [.isDirectoryKey], + options: [.skipsHiddenFiles] + ) + + directories = contents.filter { url in + var isDirectory: ObjCBool = false + return fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) + && isDirectory.boolValue + }.sorted { $0.path < $1.path } + + for dir in directories { + comics.append( + Comic( + cover: UIImage( + contentsOfFile: dir.appendingPathComponent("cover.jpg").path)!, + metadata: try JSONDecoder().decode( + Metadata.self, + from: + Data( + try String( + contentsOfFile: dir.appendingPathComponent( + "metadata.json" + ) + .path + ).utf8), + ), + path: dir, + )) + } + } catch { + print("Failed to read directories") + } + } + + func saveGlobalState() { + do { + try JSONEncoder().encode(self.globalState).write( + to: documentsURL.appendingPathComponent("state.json")) + } catch { + print("failed to save global state") + } } func setupGestures() { let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(_:))) swipeLeft.direction = .left - view.addGestureRecognizer(swipeLeft) + readerView.addGestureRecognizer(swipeLeft) let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(_:))) swipeRight.direction = .right - view.addGestureRecognizer(swipeRight) + readerView.addGestureRecognizer(swipeRight) setupTapZones() } + func readComic(name: String) { + readerView.isHidden = false + homeView.isHidden = true + let new_path = getPathFromComicName(name: name) + if let path = new_path { + currentPath = path + metadata = getMetadata(path: path)! + setImages(path: path, metadata: metadata) + setupImageView() + setupGestures() + setupTopBar() + imageView.image = currentImage + } + } + func setupTapZones() { let leftView = UIView() let rightView = UIView() @@ -65,25 +214,25 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { rightView.translatesAutoresizingMaskIntoConstraints = false topView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(leftView) - view.addSubview(rightView) - view.addSubview(topView) + readerView.addSubview(leftView) + readerView.addSubview(rightView) + readerView.addSubview(topView) NSLayoutConstraint.activate([ - leftView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - leftView.topAnchor.constraint(equalTo: view.topAnchor), - leftView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - leftView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5), + leftView.leadingAnchor.constraint(equalTo: readerView.leadingAnchor), + leftView.topAnchor.constraint(equalTo: readerView.topAnchor), + leftView.bottomAnchor.constraint(equalTo: readerView.bottomAnchor), + leftView.widthAnchor.constraint(equalTo: readerView.widthAnchor, multiplier: 0.5), - rightView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - rightView.topAnchor.constraint(equalTo: view.topAnchor), - rightView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - rightView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5), + rightView.trailingAnchor.constraint(equalTo: readerView.trailingAnchor), + rightView.topAnchor.constraint(equalTo: readerView.topAnchor), + rightView.bottomAnchor.constraint(equalTo: readerView.bottomAnchor), + rightView.widthAnchor.constraint(equalTo: readerView.widthAnchor, multiplier: 0.5), - topView.topAnchor.constraint(equalTo: view.topAnchor), - topView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - topView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - topView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2), + topView.topAnchor.constraint(equalTo: readerView.topAnchor), + topView.leadingAnchor.constraint(equalTo: readerView.leadingAnchor), + topView.trailingAnchor.constraint(equalTo: readerView.trailingAnchor), + topView.heightAnchor.constraint(equalTo: readerView.heightAnchor, multiplier: 0.2), ]) leftView.backgroundColor = .clear @@ -107,12 +256,12 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { topBarView.translatesAutoresizingMaskIntoConstraints = false topBarView.backgroundColor = UIColor.black.withAlphaComponent(0.8) // Or any style topBarView.isHidden = true - view.addSubview(topBarView) + readerView.addSubview(topBarView) NSLayoutConstraint.activate([ - topBarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - topBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - topBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + topBarView.topAnchor.constraint(equalTo: readerView.safeAreaLayoutGuide.topAnchor), + topBarView.leadingAnchor.constraint(equalTo: readerView.leadingAnchor), + topBarView.trailingAnchor.constraint(equalTo: readerView.trailingAnchor), topBarView.heightAnchor.constraint(equalToConstant: 64), ]) @@ -134,7 +283,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { backgroundColorDropdownView.backgroundColor = UIColor.darkGray backgroundColorDropdownView.translatesAutoresizingMaskIntoConstraints = false backgroundColorDropdownView.isHidden = true - view.addSubview(backgroundColorDropdownView) + readerView.addSubview(backgroundColorDropdownView) let stackView = UIStackView() stackView.axis = .vertical @@ -201,7 +350,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { pageTurnDropdownView.backgroundColor = UIColor.darkGray pageTurnDropdownView.translatesAutoresizingMaskIntoConstraints = false pageTurnDropdownView.isHidden = true - view.addSubview(pageTurnDropdownView) + readerView.addSubview(pageTurnDropdownView) let stackView = UIStackView() stackView.axis = .vertical @@ -269,11 +418,11 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { @objc func handleBackgroundColorOption(_ sender: UIButton) { if let title = sender.currentTitle { switch title.lowercased() { - case "white": view.backgroundColor = .white - case "gray": view.backgroundColor = .gray - case "black": view.backgroundColor = .black - case "red": view.backgroundColor = .red - case "blue": view.backgroundColor = .blue + case "white": readerView.backgroundColor = .white + case "gray": readerView.backgroundColor = .gray + case "black": readerView.backgroundColor = .black + case "red": readerView.backgroundColor = .red + case "blue": readerView.backgroundColor = .blue default: break } } @@ -355,81 +504,214 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate { imageView.contentMode = UIView.ContentMode.scaleAspectFit imageView.clipsToBounds = true - view.addSubview(imageView) + readerView.addSubview(imageView) NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: view.topAnchor), - imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + imageView.topAnchor.constraint(equalTo: readerView.topAnchor), + imageView.bottomAnchor.constraint(equalTo: readerView.bottomAnchor), + imageView.leadingAnchor.constraint(equalTo: readerView.leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: readerView.trailingAnchor), ]) - images = getImages() - imageView.image = images[0] } func changeImage(turn: PageTurn) { - if images.count == 0 { - return - } + let (chapter, page) = getChapterAndPageFromTurn( + chapter: currentChapter, page: currentPage, turn: turn) + if (chapter, page) == (currentChapter, currentPage) { return } + let vol = Int(metadata.chapters[currentChapter - 1].volume) switch turn { - case .next: page = min(images.count - 1, page + 1) - case .previous: page = max(0, page - 1) + case .next: + previousImage = currentImage + currentImage = nextImage + imageView.image = currentImage + let (c, p) = getChapterAndPageFromTurn(chapter: chapter, page: page, turn: turn) + nextImage = getImage(chapter: c, volume: vol, page: p, path: currentPath) + case .previous: + nextImage = currentImage + currentImage = previousImage + imageView.image = currentImage + let (c, p) = getChapterAndPageFromTurn(chapter: chapter, page: page, turn: turn) + previousImage = getImage(chapter: c, volume: vol, page: p, path: currentPath) } - imageView.image = images[page] - } -} - -func getImages() -> [UIImage] { - let fileManager = FileManager.default - let supportedExtensions = ["png", "jpg", "jpeg"] - - guard - let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first - else { - print("Documents directory not found.") - return [] + currentPage = page + currentChapter = chapter } - do { - let contents = try fileManager.contentsOfDirectory( - at: documentsURL, includingPropertiesForKeys: nil - ) + func getImage(chapter: Int, volume: Int, page: Int, path: URL) -> UIImage! { + // let supportedExtensions = ["png", "jpg", "jpeg"] - var images: [UIImage] = [] + var testPath = path + testPath.appendPathComponent(String(format: "volume_%04d", volume)) + testPath.appendPathComponent(String(format: "chapter_%04d_image_%04d.png", chapter, page - 1)) + if !fileManager.fileExists(atPath: testPath.path) { + print("Did not find image at path") + return nil + } + return UIImage(contentsOfFile: testPath.path)! + } - for file in contents { - if supportedExtensions.contains(file.pathExtension.lowercased()) { - print("Loading image: \(file.lastPathComponent)") - if let image = UIImage(contentsOfFile: file.path) { - images.append(image) + func getChapterAndPageFromTurn(chapter: Int, page: Int, turn: PageTurn) -> (Int, Int) { + switch turn { + case .next: + if metadata.chapters.count >= chapter + 1 { + if page + 1 > metadata.chapters[chapter - 1].pages { + return (chapter + 1, 1) } else { - print("Failed to load image.") + return (chapter, page + 1) } } else { - print("No image files found in Documents directory.") + return (chapter, min(metadata.chapters[chapter - 1].pages, page + 1)) + } + case .previous: + if page < 2 { + if chapter > 1 { + return ( + chapter - 1, + metadata.chapters[chapter - 1].pages + ) + } else { + return (chapter, page) + } + } else { + return (chapter, max(1, page - 1)) } } - return images - } catch { - print("Error reading contents of Documents directory: \(error)") } - return [] + + func getMetadata(path: URL) -> Metadata? { + do { + let json = Data( + try String(contentsOfFile: path.appendingPathComponent("metadata.json").path) + .utf8) + let metadata = try JSONDecoder().decode(Metadata.self, from: json) + return metadata + + } catch { + print("Error reading stuff") + } + return nil + } + + func setImages(path: URL, metadata: Metadata) { + var directories: [URL] = [] + + do { + let contents = try fileManager.contentsOfDirectory( + at: path, + includingPropertiesForKeys: [.isRegularFileKey], + options: [.skipsHiddenFiles] + ) + + directories = contents.filter { url in + var isDirectory: ObjCBool = false + return fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) + && isDirectory.boolValue + }.sorted { $0.path < $1.path } + let dir: URL = directories[0] + let ps: [String] = try fileManager.contentsOfDirectory(atPath: dir.path).sorted() + let current = dir.appendingPathComponent(ps[0]) + let next = dir.appendingPathComponent(ps[1]) + currentImage = UIImage(contentsOfFile: current.path) + nextImage = UIImage(contentsOfFile: next.path) + let path_meta = current.lastPathComponent.components(separatedBy: "_") + assert(path_meta[0] == "chapter") + currentChapter = Int(path_meta[1])! + } catch { + print("failed to set images") + } + } + + func getPathFromComicName(name: String) -> URL? { + for comic in comics { + if comic.metadata.title == name { + return comic.path + } + } + return nil + } } -func createTestFile() { +func getGlobalState() -> GlobalState { let fileManager = FileManager.default - if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask) - .first - { + if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { do { - let contents = try fileManager.contentsOfDirectory(atPath: documentsURL.path) - if contents.isEmpty { - let fileURL = documentsURL.appendingPathComponent("dummy.txt") - let data = Data(".".utf8) - try? data.write(to: fileURL) - } + let json = Data( + try String( + contentsOfFile: documentsURL.appendingPathComponent("state.json").path + ).utf8) + return try JSONDecoder().decode(GlobalState.self, from: json) } catch { print("Error reading directory contents: \(error)") } } + return GlobalState(comicName: nil) +} + +func getDocumentsURL() -> URL? { + if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + .first + { + return documentsURL + } + print("failed to get documents dir") + return nil +} + +extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) + -> Int + { + return comics.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) + -> UICollectionViewCell + { + let cell = + collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) + as! ImageCell + cell.imageView.image = comics[indexPath.item].cover + return cell + } + + // Size for cells in a grid layout + func collectionView( + _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath + ) -> CGSize { + let spacing: CGFloat = 8 + let itemsPerRow: CGFloat = 3 + let totalSpacing = (itemsPerRow - 1) * spacing + let width = (collectionView.bounds.width - totalSpacing) / itemsPerRow + return CGSize(width: width, height: width) + } + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let selectedComic = comics[indexPath.item] + + readComic(name: selectedComic.metadata.title) + } +} + +class ImageCell: UICollectionViewCell { + let imageView = UIImageView() + + override init(frame: CGRect) { + super.init(frame: frame) + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(imageView) + + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: contentView.topAnchor), + imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } }