diff --git a/ImageViewer/ViewController.swift b/ImageViewer/ViewController.swift index 981445f..9a99c88 100644 --- a/ImageViewer/ViewController.swift +++ b/ImageViewer/ViewController.swift @@ -47,7 +47,13 @@ struct GlobalState: Codable { struct LocalState: Codable { var progress: ReadProgress - var backgroundColor: String = "black" + var backgroundColor: String? +} + +struct Settings: Codable { + var homeBackgroundColor: String = "White" + var defaultReadingBackgroundColor: String = "Black" + var defaultPageTurnMode: PageTurnMode = .leftToRight } final class ViewController: UIViewController, UIGestureRecognizerDelegate { @@ -92,9 +98,11 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { let backgroundColorDropdownButton = UIButton() let homeButton = UIButton() + let settingsButton = UIButton(type: .system) let fileManager = FileManager.default var globalState = getGlobalState() + var settings = getSettings() let documentsURL = getDocumentsURL().unsafelyUnwrapped @@ -111,8 +119,8 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { setup() } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) + override func viewDidAppear(_: Bool) { + super.viewDidAppear(false) if let alert = pendingAlert { present(alert, animated: false, completion: nil) } @@ -132,6 +140,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { readerView.isHidden = true readerView.backgroundColor = .black homeView.isHidden = false + homeView.backgroundColor = convertStringToColor(settings.homeBackgroundColor) view.addSubview(homeView) view.addSubview(readerView) NSLayoutConstraint.activate([ @@ -145,6 +154,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { readerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), readerView.widthAnchor.constraint(equalTo: view.widthAnchor), ]) + mode = settings.defaultPageTurnMode setupScrollingCollectionView() loadComics() setupImageView() @@ -162,18 +172,47 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { layout.minimumInteritemSpacing = spacing layout.minimumLineSpacing = spacing - comicCollectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) + let toolbarView = UIView() + toolbarView.backgroundColor = .clear + toolbarView.translatesAutoresizingMaskIntoConstraints = false + + settingsButton.setTitle("Settings", for: .normal) + settingsButton.backgroundColor = .clear + settingsButton.translatesAutoresizingMaskIntoConstraints = false + settingsButton.addTarget( + self, action: #selector(settingsTapped), for: .touchDown + ) + + comicCollectionView = UICollectionView(frame: homeView.bounds, collectionViewLayout: layout) comicCollectionView.translatesAutoresizingMaskIntoConstraints = false comicCollectionView.dataSource = self comicCollectionView.delegate = self - comicCollectionView.backgroundColor = .white + comicCollectionView.backgroundColor = .clear comicCollectionView.register( ComicImageCell.self, forCellWithReuseIdentifier: "ComicImageCell" ) let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) longPress.minimumPressDuration = 0.5 comicCollectionView.addGestureRecognizer(longPress) + + toolbarView.addSubview(settingsButton) + homeView.addSubview(toolbarView) homeView.addSubview(comicCollectionView) + + NSLayoutConstraint.activate([ + toolbarView.topAnchor.constraint(equalTo: homeView.safeAreaLayoutGuide.topAnchor), + toolbarView.leadingAnchor.constraint(equalTo: homeView.leadingAnchor), + toolbarView.trailingAnchor.constraint(equalTo: homeView.trailingAnchor), + toolbarView.heightAnchor.constraint(equalToConstant: 44), + + settingsButton.trailingAnchor.constraint(equalTo: toolbarView.trailingAnchor, constant: -16), + settingsButton.centerYAnchor.constraint(equalTo: toolbarView.centerYAnchor), + + comicCollectionView.topAnchor.constraint(equalTo: toolbarView.bottomAnchor), + comicCollectionView.leadingAnchor.constraint(equalTo: homeView.leadingAnchor), + comicCollectionView.trailingAnchor.constraint(equalTo: homeView.trailingAnchor), + comicCollectionView.bottomAnchor.constraint(equalTo: homeView.bottomAnchor), + ]) } func loadComics() { @@ -281,27 +320,39 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { } } - func convertColorToString(color: UIColor) -> String { + func saveSettings() { + queue.async { + do { + try JSONEncoder().encode(self.settings).write( + to: self.documentsURL.appendingPathComponent("settings.json") + ) + } catch { + print("failed to save settings") + } + } + } + + func convertColorToString(_ color: UIColor) -> String { let r: String! switch color { - case .white: r = "white" - case .gray: r = "gray" - case .black: r = "black" - case .red: r = "red" - case .blue: r = "blue" - default: r = "black" + case .white: r = "White" + case .gray: r = "Gray" + case .black: r = "Black" + case .red: r = "Red" + case .blue: r = "Blue" + default: r = "Black" } return r } - func convertStringToColor(str: String) -> UIColor { + func convertStringToColor(_ str: String) -> UIColor { let r: UIColor! switch str { - case "white": r = .white - case "gray": r = .gray - case "black": r = .black - case "red": r = .red - case "blue": r = .blue + case "White": r = .white + case "Gray": r = .gray + case "Black": r = .black + case "Red": r = .red + case "Blue": r = .blue default: r = .black } return r @@ -737,7 +788,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { do { try JSONEncoder().encode( LocalState( - progress: newProgress, backgroundColor: self.convertColorToString(color: color) + progress: newProgress, backgroundColor: self.convertColorToString(color) ) ).write( to: self.currentPath.appendingPathComponent("state.json") @@ -789,7 +840,11 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { } mode = .scroll } - readerView.backgroundColor = convertStringToColor(str: local.backgroundColor) + if let color = local.backgroundColor { + readerView.backgroundColor = convertStringToColor(color) + } else { + readerView.backgroundColor = convertStringToColor(settings.defaultReadingBackgroundColor) + } } catch let decodingError as DecodingError { print(decodingError.errorDescription!) } catch { @@ -899,7 +954,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { layout.minimumInteritemSpacing = 0 layout.minimumLineSpacing = 0 scrollingCollectionView = UICollectionView( - frame: view.bounds, collectionViewLayout: layout + frame: readerView.bounds, collectionViewLayout: layout ) scrollingCollectionView.translatesAutoresizingMaskIntoConstraints = false scrollingCollectionView.dataSource = self @@ -1062,7 +1117,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { stackView.layoutMargins = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) backgroundColorDropdownView.addSubview(stackView) - let colorOptions = ["White", "Gray", "Black", "Red", "Blue"] + let colorOptions = ["White", "Gray", "Black", "Red", "Blue", "Default"] for title in colorOptions { let button = UIButton(type: .system) button.setTitle(title, for: .normal) @@ -1136,7 +1191,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { stackView.layoutMargins = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) pageTurnDropdownView.addSubview(stackView) - let pageTurnOptions = ["Left to right", "Right to left", "Scroll"] + let pageTurnOptions = ["Left to right", "Right to left", "Scroll", "Default"] for title in pageTurnOptions { let button = UIButton(type: .system) button.setTitle(title, for: .normal) @@ -1193,6 +1248,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { case "left to right": mode = .leftToRight case "right to left": mode = .rightToLeft case "scroll": mode = .scroll + case "default": mode = settings.defaultPageTurnMode default: break } if prev == mode { return } @@ -1229,6 +1285,7 @@ final class ViewController: UIViewController, UIGestureRecognizerDelegate { case "black": readerView.backgroundColor = .black case "red": readerView.backgroundColor = .red case "blue": readerView.backgroundColor = .blue + case "default": readerView.backgroundColor = convertStringToColor(settings.defaultReadingBackgroundColor) default: break } } @@ -1602,6 +1659,27 @@ func getGlobalState() -> GlobalState { return GlobalState(comicName: nil) } +func getSettings() -> Settings { + let fileManager = FileManager.default + if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { + let path = documentsURL.appendingPathComponent("settings.json").path + if !fileManager.fileExists(atPath: path) { + return Settings() + } + do { + let json = try Data( + String( + contentsOfFile: path + ).utf8 + ) + return try JSONDecoder().decode(Settings.self, from: json) + } catch { + print("Error reading directory contents: \(error)") + } + } + return Settings() +} + func getDocumentsURL() -> URL? { if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) .first @@ -2128,3 +2206,135 @@ private func monthIntToString(_ month: UInt8) -> String { default: return "" } } + +struct SettingsMenuItem { + let title: String + let children: [SettingsMenuItem]? // non-nil = pushes another menu + let action: (() -> Void)? // non-nil = leaf, performs an action +} + +class SettingsMenuViewController: UITableViewController { + private let items: [SettingsMenuItem] + + init(title: String, items: [SettingsMenuItem]) { + self.items = items + super.init(style: .plain) + self.title = title + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) not implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + preferredContentSize = CGSize(width: 440, height: items.count * 44) + } + + override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + return items.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + let item = items[indexPath.row] + cell.textLabel?.text = item.title + cell.accessoryType = item.children != nil ? .disclosureIndicator : .none + + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let item = items[indexPath.row] + if let children = item.children { + let next = SettingsMenuViewController(title: item.title, items: children) + navigationController?.pushViewController(next, animated: false) + } else { + tableView.deselectRow(at: indexPath, animated: false) + item.action?() + } + } +} + +extension ViewController { + private func colorLeafItems(applyTo colorHandler: @escaping (UIColor) -> Void) -> [SettingsMenuItem] { + let palette: [(String, UIColor)] = [ + ("Black", .black), ("White", .white), ("Red", .red), ("Blue", .blue), + ] + return palette.map { name, color in + SettingsMenuItem(title: name, children: nil, action: { [weak self] in + colorHandler(color) + self?.dismiss(animated: false) + }) + } + } + + private func pageTurnLeafItems(applyTo pageTurnHandler: @escaping (PageTurnMode) -> Void) -> [SettingsMenuItem] { + let palette: [(String, PageTurnMode)] = [ + ("Left to right", .leftToRight), ("Right to left", .rightToLeft), ("Scroll", .scroll), + ] + return palette.map { name, pageTurnMode in + SettingsMenuItem(title: name, children: nil, action: { [weak self] in + pageTurnHandler(pageTurnMode) + self?.dismiss(animated: false) + }) + } + } + + private func makeSettingsTree() -> [SettingsMenuItem] { + let backgroundColors = colorLeafItems { [weak self] color in + self?.homeView.backgroundColor = color + self?.settings.homeBackgroundColor = self?.convertColorToString(color) ?? "White" + self?.saveSettings() + } + let defaultReadingBackgroundColor = colorLeafItems { [weak self] color in + self?.settings.defaultReadingBackgroundColor = self?.convertColorToString(color) ?? "Black" + self?.saveSettings() + } + let defaultPageTurnMode = pageTurnLeafItems { [weak self] pageTurnMode in + self?.settings.defaultPageTurnMode = pageTurnMode + self?.saveSettings() + } + + let colorsMenu = SettingsMenuItem(title: "Colors", children: [ + SettingsMenuItem( + title: "Background Reading", + children: defaultReadingBackgroundColor, + action: nil + ), + SettingsMenuItem(title: "Background Home", children: backgroundColors, action: nil), + ], action: nil) + + let pageTurnMenu = SettingsMenuItem(title: "Page Turn Mode", children: defaultPageTurnMode, action: nil) + + return [colorsMenu, pageTurnMenu] + } +} + +extension ViewController: UIPopoverPresentationControllerDelegate { + @objc private func settingsTapped() { + let root = SettingsMenuViewController(title: "Settings", items: makeSettingsTree()) + let nav = UINavigationController(rootViewController: root) + nav.modalPresentationStyle = .popover + + if let popover = nav.popoverPresentationController { + popover.sourceView = settingsButton + popover.sourceRect = settingsButton.bounds + popover.permittedArrowDirections = .up + popover.delegate = self + } + + present(nav, animated: false) + } + + func adaptivePresentationStyle(for _: UIPresentationController) -> UIModalPresentationStyle { + return .none // keeps it a real popover on iPhone instead of a full-screen sheet + } + + func popoverPresentationControllerShouldDismissPopover(_: UIPopoverPresentationController) -> Bool { + UIView.setAnimationsEnabled(false) + return true + } +}