Implement settings menu #28

Merged
vegardbm merged 1 commits from settings into master 2026-07-29 23:43:38 +02:00
+233 -23
View File
@@ -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
}
}