use subviews for home and reader

This commit is contained in:
2025-07-13 23:00:03 +02:00
parent 7d4cbecbf2
commit 375467e830

View File

@@ -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")
}
}