Implement settings menu #28
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user