Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
folium-app
GitHub Repository: folium-app/Folium
Path: blob/a-new-beginning/Folium-iOS/Controllers/SettingsControllers/MandarineSettingsController.swift
2 views
//
//  MandarineSettingsController.swift
//  Folium-iOS
//
//  Created by Jarrod Norwell on 27/2/2026.
//

import Foundation
import SettingsKit
import Mandarine
import UIKit

enum MandarineSettingsHeaders : String, CaseIterable {
    case core = "Core"
    case graphics = "Graphics"
    case resolution = "Resolution"
    
    var header: SettingHeader {
        switch self {
        case .core,
                .graphics,
                .resolution:
            SettingHeader(text: rawValue)
        }
    }
    
    static var allHeaders: [SettingHeader] { allCases.map { $0.header } }
}

enum MandarineSettingsItems : String, CaseIterable {
    // Core
    case forceNTSC = "mandarine.v1.38.forceNTSC"
    
    // Graphics
    case widescreen = "mandarine.v1.38.widescreen"
    
    // Resolution
    case height = "mandarine.v1.38.height"
    case width = "mandarine.v1.38.width"
    
    var title: String {
        switch self {
        case .forceNTSC:
            "Force NTSC"
        case .height:
            "Height"
        case .widescreen:
            "Widescreen"
        case .width:
            "Width"
        }
    }
    
    var secondaryTitle: String? {
        switch self {
        case .height,
                .width:
            "Unavailable with software rendering"
        default:
            nil
        }
    }
    
    var details: String? {
        switch self {
        case .widescreen:
            "Enables widescreen patches so that 3D games render in a 16:9 aspect ratio"
        default:
            nil
        }
    }
    
    var isEnabled: Bool {
        switch self {
        case .height,
                .width:
            false
        default:
            true
        }
    }
    
    func setting(_ delegate: SettingDelegate? = nil) -> BaseSetting {
        switch self {
        case .height,
                .width:
            InputNumberSetting(key: rawValue,
                               title: title,
                               details: details,
                               secondaryTitle: secondaryTitle,
                               isEnabled: isEnabled,
                               min: self == .height ? 480 : 640,
                               max: self == .height ? 1920 : 2560,
                               value: UserDefaults.standard.double(forKey: rawValue),
                               delegate: delegate)
        case .forceNTSC,
                .widescreen:
            BoolSetting(key: rawValue,
                        title: title,
                        details: details,
                        isEnabled: isEnabled,
                        value: UserDefaults.standard.bool(forKey: rawValue),
                        delegate: delegate)
        }
    }
    
    static func settings(_ header: MandarineSettingsHeaders) -> [MandarineSettingsItems] {
        switch header {
        case .core:
            [.forceNTSC]
        case .graphics:
            [.widescreen]
        case .resolution:
            [.height, .width]
        }
    }
}

class MandarineSettingsController : UIViewController, UICollectionViewDelegate {
    var dataSource: UICollectionViewDiffableDataSource<MandarineSettingsHeaders, AnyHashableSendable>? = nil
    var snapshot: NSDiffableDataSourceSnapshot<MandarineSettingsHeaders, AnyHashableSendable>? = nil
    
    var mandarine: Mandarine
    init(_ mandarine: Mandarine) {
        self.mandarine = mandarine
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        if let navigationController {
            navigationController.navigationBar.prefersLargeTitles = true
        }
        navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "xmark"), primaryAction: UIAction { _ in
            self.dismiss(animated: true)
        })
        if #available(iOS 26, *) {
            navigationItem.title = "Mandarine"
            navigationItem.largeTitle = navigationItem.title
            navigationItem.subtitle = "PlayStation 1 (Avocado)"
            navigationItem.largeSubtitle = navigationItem.subtitle
        } else {
            title = "Mandarine"
        }
        navigationItem.style = .browser
        
        var configuration: UICollectionLayoutListConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        configuration.backgroundColor = .clear
        configuration.headerMode = .supplementary
        configuration.trailingSwipeActionsConfigurationProvider = { indexPath in
            guard let dataSource = self.dataSource, let item: BaseSetting = dataSource.itemIdentifier(for: indexPath) as? BaseSetting else {
                return UISwipeActionsConfiguration()
            }
            
            let informationContextualAction: UIContextualAction = UIContextualAction(style: .normal, title: nil, handler: { action, view, performed in
                let alertController: UIAlertController = UIAlertController(title: item.title, message: item.details, preferredStyle: .alert)
                alertController.addAction(UIAlertAction(title: "Dismiss", style: .default) { _ in
                    performed(true)
                })
                // alertController.preferredAction = alertController.actions.first
                self.present(alertController, animated: true)
            })
            informationContextualAction.image = UIImage(systemName: "info")
            
            return UISwipeActionsConfiguration(actions: [
                informationContextualAction
            ])
        }
        
        let collectionView: UICollectionView = UICollectionView(frame: .zero,
                                                                collectionViewLayout: UICollectionViewCompositionalLayout.list(using: configuration))
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.delegate = self
        view.addSubview(collectionView)
        
        collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        
        if #available(iOS 26, *) {
            let backgroundView: UIVisualEffectView = UIVisualEffectView(effect: UIGlassEffect(style: .regular))
            backgroundView.cornerConfiguration = .corners(radius: .containerConcentric())
            
            collectionView.backgroundColor = .clear
            collectionView.backgroundView = backgroundView
            view.backgroundColor = .clear
        } else {
            view.backgroundColor = .systemBackground
        }
        
        let headerCellRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
            var contentConfiguration = UIListContentConfiguration.extraProminentInsetGroupedHeader()
            if let dataSource = self.dataSource, let section: MandarineSettingsHeaders = dataSource.sectionIdentifier(for: indexPath.section) {
                contentConfiguration.text = section.header.text
                contentConfiguration.secondaryText = section.header.secondaryText
                contentConfiguration.secondaryTextProperties.color = .secondaryLabel
            }
            supplementaryView.contentConfiguration = contentConfiguration
        }
        
        let boolCell: UICollectionView.CellRegistration<UICollectionViewListCell, BoolSetting> = CellManager.Settings.boolCell
        let inputNumberCell: UICollectionView.CellRegistration<UICollectionViewListCell, InputNumberSetting> = CellManager.Settings.inputNumberCell
        
        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            switch itemIdentifier {
            case let boolSetting as BoolSetting:
                collectionView.dequeueConfiguredReusableCell(using: boolCell, for: indexPath, item: boolSetting)
            case let inputNumberSetting as InputNumberSetting:
                collectionView.dequeueConfiguredReusableCell(using: inputNumberCell, for: indexPath, item: inputNumberSetting)
            default:
                nil
            }
        }
        
        guard let dataSource else {
            return
        }
        
        dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
            collectionView.dequeueConfiguredReusableSupplementary(using: headerCellRegistration, for: indexPath)
        }
        
        snapshot = NSDiffableDataSourceSnapshot<MandarineSettingsHeaders, AnyHashableSendable>()
        guard var snapshot else {
            return
        }
        
        snapshot.appendSections(MandarineSettingsHeaders.allCases)
        snapshot.sectionIdentifiers.forEach { header in
            snapshot.appendItems(MandarineSettingsItems.settings(header).map { setting in
                setting.setting(self)
            }, toSection: header)
        }
        
        Task {
            await dataSource.apply(snapshot)
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        collectionView.deselectItem(at: indexPath, animated: true)
        guard let dataSource else {
            return
        }
        
        switch dataSource.itemIdentifier(for: indexPath) {
        case let inputSetting as InputNumberSetting:
            guard inputSetting.isEnabled else {
                return
            }
            
            let alertController = UIAlertController(title: inputSetting.title,
                                                    message: "Min: \(Int(inputSetting.min)), Max: \(Int(inputSetting.max))",
                                                    preferredStyle: .alert)
            alertController.addTextField { textField in
                textField.keyboardType = .numberPad
            }
            alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel))
            alertController.addAction(UIAlertAction(title: "Save", style: .default) { action in
                guard let textFields = alertController.textFields, let textField = textFields.first,
                      let value: NSString = textField.text as? NSString else {
                    return
                }
                
                UserDefaults.standard.set(value.doubleValue, forKey: inputSetting.key)
                if let delegate = inputSetting.delegate {
                    delegate.didChangeSetting(at: indexPath)
                }
            })
            present(alertController, animated: true)
        default:
            break
        }
    }
}

extension MandarineSettingsController : SettingDelegate {
    func didChangeSetting(at indexPath: IndexPath) {
        guard let dataSource, let sectionIdentifier = dataSource.sectionIdentifier(for: indexPath.section) else {
            return
        }
        
        var snapshot = dataSource.snapshot()
        let item = snapshot.itemIdentifiers(inSection: sectionIdentifier)[indexPath.item]
        
        switch item {
        case let boolSetting as BoolSetting:
            boolSetting.value = UserDefaults.standard.bool(forKey: boolSetting.key)
        case let inputNumberSetting as InputNumberSetting:
            inputNumberSetting.value = UserDefaults.standard.double(forKey: inputNumberSetting.key)
        default:
            break
        }
        
        snapshot.reloadItems([item])
        
        Task {
            await dataSource.apply(snapshot, animatingDifferences: false)
        }
    }
}