Remove old settings
This commit is contained in:
parent
bbf7c541bb
commit
b0886979e5
|
@ -397,7 +397,6 @@
|
|||
DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */; };
|
||||
DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6427B216500082E365 /* ReportResultViewModel.swift */; };
|
||||
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */; };
|
||||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
|
||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
|
||||
|
@ -730,11 +729,6 @@
|
|||
46DAB0EBDDFB678347CD96FF /* Pods-MastodonTests.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk - release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk - release.xcconfig"; sourceTree = "<group>"; };
|
||||
5B24BBD7262DB14800A9381B /* ReportViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = "<group>"; };
|
||||
5B24BBD8262DB14800A9381B /* ReportStatusViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
5B90C456262599800002E742 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsToggleTableViewCell.swift; sourceTree = "<group>"; };
|
||||
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppearanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||
5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsLinkTableViewCell.swift; sourceTree = "<group>"; };
|
||||
5B90C45C262599800002E742 /* SettingsSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeader.swift; sourceTree = "<group>"; };
|
||||
5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewController.swift; sourceTree = "<group>"; };
|
||||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSection.swift; sourceTree = "<group>"; };
|
||||
5CE45680252519F42FEA2D13 /* Pods-ShareActionExtension.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - release.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - release.xcconfig"; sourceTree = "<group>"; };
|
||||
|
@ -938,7 +932,6 @@
|
|||
DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastodonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
|
||||
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
DB443CD32694627B00159B29 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = "<group>"; };
|
||||
DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = "<group>"; };
|
||||
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = "<group>"; };
|
||||
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
|
@ -1062,9 +1055,6 @@
|
|||
DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFooterTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = "<group>"; };
|
||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
|
||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = "<group>"; };
|
||||
DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
|
||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
||||
DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBatchFetchViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1124,7 +1114,6 @@
|
|||
DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewController.swift; sourceTree = "<group>"; };
|
||||
DB98EB6427B216500082E365 /* ReportResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewModel.swift; sourceTree = "<group>"; };
|
||||
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsAppearanceTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
||||
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1768,32 +1757,11 @@
|
|||
D8F917042A4B0657008A5370 /* General Settings */,
|
||||
D81D12432A4E181C005009D4 /* Notification Settings */,
|
||||
D8F917092A4B2AFF008A5370 /* About Mastodon */,
|
||||
D8318A7E2A4466C900C0FB73 /* Legacy */,
|
||||
D8318A7F2A4466D300C0FB73 /* SettingsCoordinator.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5B90C457262599800002E742 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5B90C45C262599800002E742 /* SettingsSectionHeader.swift */,
|
||||
DB443CD32694627B00159B29 /* AppearanceView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5B90C458262599800002E742 /* Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */,
|
||||
DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */,
|
||||
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */,
|
||||
5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */,
|
||||
);
|
||||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5D03938E2612D200007FE196 /* Webview */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1865,19 +1833,6 @@
|
|||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8318A7E2A4466C900C0FB73 /* Legacy */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5B90C458262599800002E742 /* Cell */,
|
||||
5B90C457262599800002E742 /* View */,
|
||||
DB6D9F9626367249008423CD /* SettingsViewController.swift */,
|
||||
5B90C456262599800002E742 /* SettingsViewModel.swift */,
|
||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
||||
);
|
||||
path = Legacy;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8A6AB68291C50F3003AB663 /* Login */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
//
|
||||
// SettingsAppearanceTableViewCell+ViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
||||
extension SettingsAppearanceTableViewCell {
|
||||
final class ViewModel: ObservableObject {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
// input
|
||||
@Published public var customUserInterfaceStyle: UIUserInterfaceStyle = .unspecified
|
||||
|
||||
// output
|
||||
@Published public var appearanceMode: SettingsItem.AppearanceMode = .system
|
||||
|
||||
init() {
|
||||
UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak self] defaults, _ in
|
||||
guard let self = self else { return }
|
||||
self.customUserInterfaceStyle = defaults.customUserInterfaceStyle
|
||||
}
|
||||
.store(in: &observations)
|
||||
}
|
||||
|
||||
public func prepareForReuse() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsAppearanceTableViewCell.ViewModel {
|
||||
func bind(cell: SettingsAppearanceTableViewCell) {
|
||||
$customUserInterfaceStyle.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { customUserInterfaceStyle in
|
||||
cell.appearanceViews.forEach { view in
|
||||
view.selected = false
|
||||
}
|
||||
|
||||
switch customUserInterfaceStyle {
|
||||
case .unspecified:
|
||||
cell.systemAppearanceView.selected = true
|
||||
case .dark:
|
||||
cell.darkAppearanceView.selected = true
|
||||
case .light:
|
||||
cell.lightAppearanceView.selected = true
|
||||
@unknown default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
//
|
||||
// SettingsAppearanceTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
protocol SettingsAppearanceTableViewCellDelegate: AnyObject {
|
||||
func settingsAppearanceTableViewCell(_ cell: SettingsAppearanceTableViewCell, didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode)
|
||||
}
|
||||
|
||||
class SettingsAppearanceTableViewCell: UITableViewCell {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
static let spacing: CGFloat = 28
|
||||
|
||||
weak var delegate: SettingsAppearanceTableViewCellDelegate?
|
||||
|
||||
public private(set) var viewModel = ViewModel()
|
||||
|
||||
lazy var stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .horizontal
|
||||
view.distribution = .fillEqually
|
||||
view.spacing = SettingsAppearanceTableViewCell.spacing
|
||||
return view
|
||||
}()
|
||||
|
||||
let systemAppearanceView = AppearanceView(
|
||||
image: Asset.Settings.automatic.image,
|
||||
title: L10n.Scene.Settings.Section.Appearance.automatic
|
||||
)
|
||||
let darkAppearanceView = AppearanceView(
|
||||
image: Asset.Settings.dark.image,
|
||||
title: L10n.Scene.Settings.Section.Appearance.dark
|
||||
)
|
||||
let lightAppearanceView = AppearanceView(
|
||||
image: Asset.Settings.light.image,
|
||||
title: L10n.Scene.Settings.Section.Appearance.light
|
||||
)
|
||||
|
||||
var appearanceViews: [AppearanceView] {
|
||||
return [
|
||||
systemAppearanceView,
|
||||
darkAppearanceView,
|
||||
lightAppearanceView,
|
||||
]
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
observations.removeAll()
|
||||
viewModel.prepareForReuse()
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
viewModel.bind(cell: self)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
// remove separator line in section of group tableview
|
||||
for subview in self.subviews {
|
||||
if subview != self.contentView && subview.frame.width == self.frame.width {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
// remove grouped style table corner radius
|
||||
layer.cornerRadius = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SettingsAppearanceTableViewCell {
|
||||
|
||||
// MARK: Private methods
|
||||
private func setupUI() {
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pinToParent()
|
||||
|
||||
stackView.addArrangedSubview(systemAppearanceView)
|
||||
stackView.addArrangedSubview(darkAppearanceView)
|
||||
stackView.addArrangedSubview(lightAppearanceView)
|
||||
|
||||
appearanceViews.forEach { view in
|
||||
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
tapGestureRecognizer.addTarget(self, action: #selector(SettingsAppearanceTableViewCell.appearanceViewDidPressed(_:)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
extension SettingsAppearanceTableViewCell {
|
||||
@objc func appearanceViewDidPressed(_ sender: UITapGestureRecognizer) {
|
||||
let mode: SettingsItem.AppearanceMode
|
||||
|
||||
switch sender.view {
|
||||
case systemAppearanceView:
|
||||
mode = .system
|
||||
case darkAppearanceView:
|
||||
mode = .dark
|
||||
case lightAppearanceView:
|
||||
mode = .light
|
||||
default:
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
delegate?.settingsAppearanceTableViewCell(self, didSelectAppearanceMode: mode)
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
//
|
||||
// SettingsLinkTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SettingsLinkTableViewCell: UITableViewCell {
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
selectionStyle = .none
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
super.setHighlighted(highlighted, animated: animated)
|
||||
textLabel?.alpha = highlighted ? 0.6 : 1.0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
extension SettingsLinkTableViewCell {
|
||||
func update(with link: SettingsItem.Link) {
|
||||
textLabel?.text = link.title
|
||||
textLabel?.textColor = link.textColor
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
//
|
||||
// SettingsToggleTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
protocol SettingsToggleCellDelegate: AnyObject {
|
||||
func settingsToggleCell(_ cell: SettingsToggleTableViewCell, switchValueDidChange switch: UISwitch)
|
||||
}
|
||||
|
||||
class SettingsToggleTableViewCell: UITableViewCell {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
private(set) lazy var switchButton: UISwitch = {
|
||||
let view = UISwitch(frame:.zero)
|
||||
view.onTintColor = Asset.Colors.Brand.blurple.color
|
||||
return view
|
||||
}()
|
||||
|
||||
weak var delegate: SettingsToggleCellDelegate?
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
// MARK: Private methods
|
||||
private func setupUI() {
|
||||
selectionStyle = .none
|
||||
accessoryView = switchButton
|
||||
textLabel?.numberOfLines = 0
|
||||
|
||||
switchButton.addTarget(self, action: #selector(switchValueDidChange(sender:)), for: .valueChanged)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
extension SettingsToggleTableViewCell {
|
||||
|
||||
@objc private func switchValueDidChange(sender: UISwitch) {
|
||||
guard let delegate = delegate else { return }
|
||||
delegate.settingsToggleCell(self, switchValueDidChange: sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SettingsToggleTableViewCell {
|
||||
|
||||
func update(enabled: Bool?) {
|
||||
switchButton.isEnabled = enabled != nil
|
||||
textLabel?.textColor = enabled != nil ? Asset.Colors.Label.primary.color : Asset.Colors.Label.secondary.color
|
||||
switchButton.isOn = enabled ?? false
|
||||
}
|
||||
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
//
|
||||
// SettingsItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum SettingsItem {
|
||||
case appearance(record: ManagedObjectRecord<Setting>)
|
||||
case preference(settingRecord: ManagedObjectRecord<Setting>, preferenceType: PreferenceType)
|
||||
case notification(settingRecord: ManagedObjectRecord<Setting>, switchMode: NotificationSwitchMode)
|
||||
case boringZone(item: Link)
|
||||
case spicyZone(item: Link)
|
||||
|
||||
enum AppearanceMode: String {
|
||||
case system
|
||||
case dark
|
||||
case light
|
||||
}
|
||||
|
||||
enum NotificationSwitchMode: CaseIterable, Hashable {
|
||||
case favorite
|
||||
case follow
|
||||
case reblog
|
||||
case mention
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .favorite: return L10n.Scene.Settings.Section.Notifications.favorites
|
||||
case .follow: return L10n.Scene.Settings.Section.Notifications.follows
|
||||
case .reblog: return L10n.Scene.Settings.Section.Notifications.boosts
|
||||
case .mention: return L10n.Scene.Settings.Section.Notifications.mentions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PreferenceType: CaseIterable {
|
||||
case disableAvatarAnimation
|
||||
case disableEmojiAnimation
|
||||
case useDefaultBrowser
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .disableAvatarAnimation: return L10n.Scene.Settings.Section.Preference.disableAvatarAnimation
|
||||
case .disableEmojiAnimation: return L10n.Scene.Settings.Section.Preference.disableEmojiAnimation
|
||||
case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Link: CaseIterable, Hashable {
|
||||
case accountSettings
|
||||
case github
|
||||
case termsOfService
|
||||
case privacyPolicy
|
||||
case clearMediaCache
|
||||
case signOut
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .accountSettings: return L10n.Scene.Settings.Section.BoringZone.accountSettings
|
||||
case .github: return "GitHub"
|
||||
case .termsOfService: return L10n.Scene.Settings.Section.BoringZone.terms
|
||||
case .privacyPolicy: return L10n.Scene.Settings.Section.BoringZone.privacy
|
||||
case .clearMediaCache: return L10n.Scene.Settings.Section.SpicyZone.clear
|
||||
case .signOut: return L10n.Scene.Settings.Section.SpicyZone.signout
|
||||
}
|
||||
}
|
||||
|
||||
var textColor: UIColor? {
|
||||
switch self {
|
||||
case .accountSettings: return nil // tintColor
|
||||
case .github: return nil
|
||||
case .termsOfService: return nil
|
||||
case .privacyPolicy: return nil
|
||||
case .clearMediaCache: return .systemRed
|
||||
case .signOut: return .systemRed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SettingsItem: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .appearance(let record):
|
||||
hasher.combine(String(describing: SettingsItem.AppearanceMode.self))
|
||||
hasher.combine(record)
|
||||
case .notification(let settingObjectID, let switchMode):
|
||||
hasher.combine(String(describing: SettingsItem.notification.self))
|
||||
hasher.combine(settingObjectID)
|
||||
hasher.combine(switchMode)
|
||||
case .preference(let settingObjectID, let preferenceType):
|
||||
hasher.combine(String(describing: SettingsItem.preference.self))
|
||||
hasher.combine(settingObjectID)
|
||||
hasher.combine(preferenceType)
|
||||
case .boringZone(let link):
|
||||
hasher.combine(String(describing: SettingsItem.boringZone.self))
|
||||
hasher.combine(link)
|
||||
case .spicyZone(let link):
|
||||
hasher.combine(String(describing: SettingsItem.spicyZone.self))
|
||||
hasher.combine(link)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
//
|
||||
// SettingsSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
||||
enum SettingsSection: Hashable {
|
||||
case appearance
|
||||
case preference
|
||||
case notifications
|
||||
case boringZone
|
||||
case spicyZone
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .appearance: return L10n.Scene.Settings.Section.LookAndFeel.title
|
||||
case .preference: return ""
|
||||
case .notifications: return L10n.Scene.Settings.Section.Notifications.title
|
||||
case .boringZone: return L10n.Scene.Settings.Section.BoringZone.title
|
||||
case .spicyZone: return L10n.Scene.Settings.Section.SpicyZone.title
|
||||
}
|
||||
}
|
||||
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
settingsAppearanceTableViewCellDelegate: SettingsAppearanceTableViewCellDelegate,
|
||||
settingsToggleCellDelegate: SettingsToggleCellDelegate
|
||||
) -> UITableViewDiffableDataSource<SettingsSection, SettingsItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
weak settingsAppearanceTableViewCellDelegate,
|
||||
weak settingsToggleCellDelegate
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .appearance:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
|
||||
cell.delegate = settingsAppearanceTableViewCellDelegate
|
||||
return cell
|
||||
case .preference(let record, _):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
|
||||
cell.delegate = settingsToggleCellDelegate
|
||||
managedObjectContext.performAndWait {
|
||||
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||
SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
|
||||
|
||||
ManagedObjectObserver.observe(object: setting)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { _ in
|
||||
// do nothing
|
||||
}, receiveValue: { [weak cell] change in
|
||||
guard let cell = cell else { return }
|
||||
guard case .update(let object) = change.changeType,
|
||||
let setting = object as? Setting else { return }
|
||||
SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
|
||||
})
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
return cell
|
||||
case .notification(let record, let switchMode):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
|
||||
managedObjectContext.performAndWait {
|
||||
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||
if let subscription = setting.activeSubscription {
|
||||
SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
|
||||
}
|
||||
ManagedObjectObserver.observe(object: setting)
|
||||
.sink(receiveCompletion: { _ in
|
||||
// do nothing
|
||||
}, receiveValue: { [weak cell] change in
|
||||
guard let cell = cell else { return }
|
||||
guard case .update(let object) = change.changeType,
|
||||
let setting = object as? Setting else { return }
|
||||
guard let subscription = setting.activeSubscription else { return }
|
||||
SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
|
||||
})
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
cell.delegate = settingsToggleCellDelegate
|
||||
return cell
|
||||
case .boringZone(let item),
|
||||
.spicyZone(let item):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
|
||||
cell.update(with: item)
|
||||
return cell
|
||||
} // end switch
|
||||
}
|
||||
}
|
||||
|
||||
public static func configureSettingToggle(
|
||||
cell: SettingsToggleTableViewCell,
|
||||
item: SettingsItem,
|
||||
setting: Setting
|
||||
) {
|
||||
switch item {
|
||||
case .preference(_, let preferenceType):
|
||||
cell.textLabel?.text = preferenceType.title
|
||||
|
||||
switch preferenceType {
|
||||
case .disableAvatarAnimation:
|
||||
cell.switchButton.isOn = setting.preferredStaticAvatar
|
||||
case .disableEmojiAnimation:
|
||||
cell.switchButton.isOn = setting.preferredStaticEmoji
|
||||
case .useDefaultBrowser:
|
||||
cell.switchButton.isOn = setting.preferredUsingDefaultBrowser
|
||||
}
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
public static func configureSettingToggle(
|
||||
cell: SettingsToggleTableViewCell,
|
||||
switchMode: SettingsItem.NotificationSwitchMode,
|
||||
subscription: NotificationSubscription
|
||||
) {
|
||||
cell.textLabel?.text = switchMode.title
|
||||
|
||||
let enabled: Bool?
|
||||
switch switchMode {
|
||||
case .favorite: enabled = subscription.alert.favourite
|
||||
case .follow: enabled = subscription.alert.follow
|
||||
case .reblog: enabled = subscription.alert.reblog
|
||||
case .mention: enabled = subscription.alert.mention
|
||||
}
|
||||
cell.update(enabled: enabled)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,515 +0,0 @@
|
|||
//
|
||||
// SettingsViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import AuthenticationServices
|
||||
import MetaTextKit
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
class SettingsViewController: UIViewController, NeedsDependency {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: SettingsViewModel! { willSet { precondition(!isViewLoaded) } }
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var notificationPolicySubscription: AnyCancellable?
|
||||
|
||||
var triggerMenu: UIMenu {
|
||||
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
|
||||
let follower = L10n.Scene.Settings.Section.Notifications.Trigger.follower
|
||||
let follow = L10n.Scene.Settings.Section.Notifications.Trigger.follow
|
||||
let noOne = L10n.Scene.Settings.Section.Notifications.Trigger.noone
|
||||
let menu = UIMenu(
|
||||
image: nil,
|
||||
identifier: nil,
|
||||
options: .displayInline,
|
||||
children: [
|
||||
UIAction(title: anyone, image: UIImage(systemName: "person.3"), attributes: []) { [weak self] action in
|
||||
self?.updateTrigger(policy: .all)
|
||||
},
|
||||
UIAction(title: follower, image: UIImage(systemName: "person.crop.circle.badge.plus"), attributes: []) { [weak self] action in
|
||||
self?.updateTrigger(policy: .follower)
|
||||
},
|
||||
UIAction(title: follow, image: UIImage(systemName: "person.crop.circle.badge.checkmark"), attributes: []) { [weak self] action in
|
||||
self?.updateTrigger(policy: .followed)
|
||||
},
|
||||
UIAction(title: noOne, image: UIImage(systemName: "nosign"), attributes: []) { [weak self] action in
|
||||
self?.updateTrigger(policy: .none)
|
||||
},
|
||||
]
|
||||
)
|
||||
return menu
|
||||
}
|
||||
|
||||
private let notifySectionHeaderStackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isLayoutMarginsRelativeArrangement = true
|
||||
view.axis = .horizontal
|
||||
view.spacing = 4
|
||||
return view
|
||||
}()
|
||||
|
||||
let notifyLabel = UILabel()
|
||||
private(set) lazy var notifySectionHeader: UIView = {
|
||||
let view = notifySectionHeaderStackView
|
||||
|
||||
notifyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
notifyLabel.adjustsFontForContentSizeCategory = true
|
||||
notifyLabel.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold))
|
||||
notifyLabel.textColor = Asset.Colors.Label.primary.color
|
||||
notifyLabel.text = L10n.Scene.Settings.Section.Notifications.Trigger.title
|
||||
notifyLabel.adjustsFontSizeToFitWidth = true
|
||||
notifyLabel.minimumScaleFactor = 0.5
|
||||
|
||||
view.addArrangedSubview(notifyLabel)
|
||||
view.addArrangedSubview(whoButton)
|
||||
whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
||||
whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .vertical)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
private(set) lazy var whoButton: UIButton = {
|
||||
let whoButton = UIButton(type: .roundedRect)
|
||||
whoButton.menu = triggerMenu
|
||||
whoButton.showsMenuAsPrimaryAction = true
|
||||
whoButton.setBackgroundColor(Asset.Colors.battleshipGrey.color, for: .normal)
|
||||
whoButton.setTitleColor(Asset.Colors.Label.primary.color, for: .normal)
|
||||
whoButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
whoButton.titleLabel?.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold))
|
||||
whoButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
|
||||
whoButton.layer.cornerRadius = 10
|
||||
whoButton.clipsToBounds = true
|
||||
whoButton.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
whoButton.titleLabel?.minimumScaleFactor = 0.5
|
||||
return whoButton
|
||||
}()
|
||||
|
||||
private(set) lazy var tableView: UITableView = {
|
||||
// init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Width' UIStackView:0x7f8c2b6c0590.width == 0)
|
||||
let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 320, height: 320), style: .insetGrouped)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.delegate = self
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorColor = SystemTheme.separator
|
||||
|
||||
tableView.register(SettingsAppearanceTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsAppearanceTableViewCell.self))
|
||||
tableView.register(SettingsToggleTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsToggleTableViewCell.self))
|
||||
tableView.register(SettingsLinkTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsLinkTableViewCell.self))
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let tableFooterLabel = MetaLabel(style: .settingTableFooter)
|
||||
lazy var tableFooterView: UIView = {
|
||||
// init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Height' UIStackView:0x7ffe41e47da0.height == 0)
|
||||
let view = UIStackView(frame: CGRect(x: 0, y: 0, width: 320, height: 320))
|
||||
view.isLayoutMarginsRelativeArrangement = true
|
||||
view.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
|
||||
view.axis = .vertical
|
||||
view.alignment = .center
|
||||
|
||||
// tableFooterLabel.linkDelegate = self
|
||||
view.addArrangedSubview(tableFooterLabel)
|
||||
return view
|
||||
}()
|
||||
}
|
||||
|
||||
extension SettingsViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupView()
|
||||
bindViewModel()
|
||||
|
||||
viewModel.viewDidLoad.send()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// make large title not collapsed
|
||||
navigationController?.navigationBar.sizeToFit()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
guard let footerView = self.tableView.tableFooterView else {
|
||||
return
|
||||
}
|
||||
|
||||
let width = self.tableView.bounds.size.width
|
||||
let size = footerView.systemLayoutSizeFitting(CGSize(width: width, height: UIView.layoutFittingCompressedSize.height))
|
||||
if footerView.frame.size.height != size.height {
|
||||
footerView.frame.size.height = size.height
|
||||
self.tableView.tableFooterView = footerView
|
||||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
updateSectionHeaderStackViewLayout()
|
||||
}
|
||||
|
||||
|
||||
// MAKR: - Private methods
|
||||
private func updateSectionHeaderStackViewLayout() {
|
||||
// accessibility
|
||||
if traitCollection.preferredContentSizeCategory < .accessibilityMedium {
|
||||
notifySectionHeaderStackView.axis = .horizontal
|
||||
notifyLabel.numberOfLines = 1
|
||||
} else {
|
||||
notifySectionHeaderStackView.axis = .vertical
|
||||
notifyLabel.numberOfLines = 0
|
||||
}
|
||||
}
|
||||
|
||||
private func bindViewModel() {
|
||||
self.whoButton.setTitle(viewModel.setting.value.activeSubscription?.policy.title, for: .normal)
|
||||
viewModel.setting
|
||||
.sink { [weak self] setting in
|
||||
guard let self = self else { return }
|
||||
self.notificationPolicySubscription = ManagedObjectObserver.observe(object: setting)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { [weak self] change in
|
||||
guard let self = self else { return }
|
||||
guard case let .update(object) = change.changeType,
|
||||
let setting = object as? Setting else { return }
|
||||
if let activeSubscription = setting.activeSubscription {
|
||||
self.whoButton.setTitle(activeSubscription.policy.title, for: .normal)
|
||||
} else {
|
||||
// assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
let footer = "Mastodon for iOS v\(UIApplication.appVersion()) (\(UIApplication.appBuild()))"
|
||||
let metaContent = PlaintextMetaContent(string: footer)
|
||||
tableFooterLabel.configure(content: metaContent)
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
setupBackgroundColor()
|
||||
|
||||
setupNavigation()
|
||||
view.addSubview(tableView)
|
||||
tableView.pinToParent()
|
||||
setupTableView()
|
||||
|
||||
updateSectionHeaderStackViewLayout()
|
||||
}
|
||||
|
||||
private func setupBackgroundColor() {
|
||||
view.backgroundColor = UIColor(dynamicProvider: { traitCollection in
|
||||
switch traitCollection.userInterfaceLevel {
|
||||
case .elevated where traitCollection.userInterfaceStyle == .dark:
|
||||
return SystemTheme.systemElevatedBackgroundColor
|
||||
default:
|
||||
return .secondarySystemBackground
|
||||
}
|
||||
})
|
||||
|
||||
tableView.separatorColor = SystemTheme.separator
|
||||
}
|
||||
|
||||
private func setupNavigation() {
|
||||
navigationController?.navigationBar.prefersLargeTitles = true
|
||||
navigationItem.rightBarButtonItem
|
||||
= UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done,
|
||||
target: self,
|
||||
action: #selector(doneButtonDidClick))
|
||||
navigationItem.title = L10n.Scene.Settings.title
|
||||
}
|
||||
|
||||
private func setupTableView() {
|
||||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
settingsAppearanceTableViewCellDelegate: self,
|
||||
settingsToggleCellDelegate: self
|
||||
)
|
||||
tableView.tableFooterView = tableFooterView
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Mark: - Actions
|
||||
extension SettingsViewController {
|
||||
@objc private func doneButtonDidClick() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension SettingsViewController: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
let sections = viewModel.dataSource.snapshot().sectionIdentifiers
|
||||
guard section < sections.count else { return nil }
|
||||
|
||||
let sectionIdentifier = sections[section]
|
||||
|
||||
let header: SettingsSectionHeader
|
||||
switch sectionIdentifier {
|
||||
case .preference:
|
||||
return UIView()
|
||||
case .notifications:
|
||||
header = SettingsSectionHeader(
|
||||
frame: CGRect(x: 0, y: 0, width: 375, height: 66),
|
||||
customView: notifySectionHeader)
|
||||
header.update(title: sectionIdentifier.title)
|
||||
default:
|
||||
header = SettingsSectionHeader(frame: CGRect(x: 0, y: 0, width: 375, height: 66))
|
||||
header.update(title: sectionIdentifier.title)
|
||||
}
|
||||
header.preservesSuperviewLayoutMargins = true
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
// remove the gap of table's footer
|
||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
// remove the gap of table's footer
|
||||
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||
return CGFloat.leastNonzeroMagnitude
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let dataSource = viewModel.dataSource else { return }
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
switch item {
|
||||
case .appearance:
|
||||
// do nothing
|
||||
break
|
||||
case .notification:
|
||||
// do nothing
|
||||
break
|
||||
case .preference:
|
||||
// do nothing
|
||||
break
|
||||
case .boringZone(let link), .spicyZone(let link):
|
||||
let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
feedbackGenerator.impactOccurred()
|
||||
switch link {
|
||||
case .accountSettings:
|
||||
case .github:
|
||||
case .termsOfService, .privacyPolicy:
|
||||
// same URL
|
||||
case .clearMediaCache:
|
||||
context.purgeCache()
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] byteCount in
|
||||
guard let self = self else { return }
|
||||
let byteCountFormatted = AppContext.byteCountFormatter.string(fromByteCount: Int64(byteCount))
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Common.Alerts.CleanCache.title,
|
||||
message: L10n.Common.Alerts.CleanCache.message(byteCountFormatted),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
||||
alertController.addAction(okAction)
|
||||
_ = self.coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
case .signOut:
|
||||
feedbackGenerator.impactOccurred()
|
||||
alertToSignOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update setting into core data
|
||||
extension SettingsViewController {
|
||||
func updateTrigger(policy: Mastodon.API.Subscriptions.Policy) {
|
||||
let objectID = self.viewModel.setting.value.objectID
|
||||
let managedObjectContext = context.backgroundManagedObjectContext
|
||||
|
||||
managedObjectContext.performChanges {
|
||||
let setting = managedObjectContext.object(with: objectID) as! Setting
|
||||
let (subscription, _) = APIService.CoreData.createOrFetchSubscription(
|
||||
into: managedObjectContext,
|
||||
setting: setting,
|
||||
policy: policy
|
||||
)
|
||||
let now = Date()
|
||||
subscription.update(activedAt: now)
|
||||
setting.didUpdate(at: now)
|
||||
}
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SettingsAppearanceTableViewCellDelegate
|
||||
extension SettingsViewController: SettingsAppearanceTableViewCellDelegate {
|
||||
func settingsAppearanceTableViewCell(
|
||||
_ cell: SettingsAppearanceTableViewCell,
|
||||
didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode
|
||||
) {
|
||||
guard let dataSource = viewModel.dataSource else { return }
|
||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||
let item = dataSource.itemIdentifier(for: indexPath)
|
||||
guard case .appearance = item else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
switch appearanceMode {
|
||||
case .system:
|
||||
UserDefaults.shared.customUserInterfaceStyle = .unspecified
|
||||
case .dark:
|
||||
UserDefaults.shared.customUserInterfaceStyle = .dark
|
||||
case .light:
|
||||
UserDefaults.shared.customUserInterfaceStyle = .light
|
||||
}
|
||||
|
||||
let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
feedbackGenerator.impactOccurred()
|
||||
} // end Task
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SettingsViewController: SettingsToggleCellDelegate {
|
||||
func settingsToggleCell(_ cell: SettingsToggleTableViewCell, switchValueDidChange switch: UISwitch) {
|
||||
guard let dataSource = viewModel.dataSource else { return }
|
||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||
|
||||
let isOn = `switch`.isOn
|
||||
let item = dataSource.itemIdentifier(for: indexPath)
|
||||
|
||||
switch item {
|
||||
case .notification(let record, let switchMode):
|
||||
let managedObjectContext = context.backgroundManagedObjectContext
|
||||
managedObjectContext.performChanges {
|
||||
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||
guard let subscription = setting.activeSubscription else { return }
|
||||
let alert = subscription.alert
|
||||
switch switchMode {
|
||||
case .favorite: alert.update(favourite: isOn)
|
||||
case .follow: alert.update(follow: isOn)
|
||||
case .reblog: alert.update(reblog: isOn)
|
||||
case .mention: alert.update(mention: isOn)
|
||||
}
|
||||
// trigger setting update
|
||||
alert.subscription.setting?.didUpdate(at: Date())
|
||||
}
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
case .preference(let record, let preferenceType):
|
||||
let managedObjectContext = context.backgroundManagedObjectContext
|
||||
managedObjectContext.performChanges {
|
||||
guard let setting = record.object(in: managedObjectContext) else { return }
|
||||
switch preferenceType {
|
||||
case .disableAvatarAnimation:
|
||||
setting.update(preferredStaticAvatar: isOn)
|
||||
case .disableEmojiAnimation:
|
||||
setting.update(preferredStaticEmoji: isOn)
|
||||
case .useDefaultBrowser:
|
||||
setting.update(preferredUsingDefaultBrowser: isOn)
|
||||
}
|
||||
}
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .success:
|
||||
switch preferenceType {
|
||||
case .disableAvatarAnimation:
|
||||
UserDefaults.shared.preferredStaticAvatar = isOn
|
||||
case .disableEmojiAnimation:
|
||||
UserDefaults.shared.preferredStaticEmoji = isOn
|
||||
case .useDefaultBrowser:
|
||||
UserDefaults.shared.preferredUsingDefaultBrowser = isOn
|
||||
}
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
default:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MetaLabelDelegate
|
||||
extension SettingsViewController: MetaLabelDelegate {
|
||||
func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) {
|
||||
switch meta {
|
||||
case .url(_, _, let url, _):
|
||||
guard let url = URL(string: url) else { return }
|
||||
_ = coordinator.present(scene: .safari(url: url), from: self, transition: .safariPresent(animated: true, completion: nil))
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ASAuthorizationControllerPresentationContextProviding
|
||||
extension SettingsViewController: ASWebAuthenticationPresentationContextProviding {
|
||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||
return view.window!
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
extension SettingsViewController: UIAdaptivePresentationControllerDelegate {
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
return .pageSheet
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController {
|
||||
|
||||
var closeKeyCommand: UIKeyCommand {
|
||||
UIKeyCommand(
|
||||
title: L10n.Scene.Settings.Keyboard.closeSettingsWindow,
|
||||
image: nil,
|
||||
action: #selector(SettingsViewController.closeSettingsWindowKeyCommandHandler(_:)),
|
||||
input: "w",
|
||||
modifierFlags: .command,
|
||||
propertyList: nil,
|
||||
alternates: [],
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
)
|
||||
}
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return [closeKeyCommand]
|
||||
}
|
||||
|
||||
@objc private func closeSettingsWindowKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
//
|
||||
// SettingsViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/7.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import AuthenticationServices
|
||||
import MastodonCore
|
||||
|
||||
class SettingsViewModel {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
var mastodonAuthenticationController: MastodonAuthenticationController?
|
||||
|
||||
let setting: CurrentValueSubject<Setting, Never>
|
||||
var updateDisposeBag = Set<AnyCancellable>()
|
||||
var createDisposeBag = Set<AnyCancellable>()
|
||||
|
||||
let viewDidLoad = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
var dataSource: UITableViewDiffableDataSource<SettingsSection, SettingsItem>!
|
||||
/// create a subscription when:
|
||||
/// - does not has one
|
||||
/// - does not find subscription for selected trigger when change trigger
|
||||
let createSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
|
||||
let currentInstance = CurrentValueSubject<Mastodon.Entity.Instance?, Never>(nil)
|
||||
|
||||
/// update a subscription when:
|
||||
/// - change switch for specified alerts
|
||||
let updateSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
|
||||
|
||||
init(context: AppContext, authContext: AuthContext, setting: Setting) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.setting = CurrentValueSubject(setting)
|
||||
|
||||
self.setting
|
||||
.sink(receiveValue: { [weak self] setting in
|
||||
guard let self = self else { return }
|
||||
self.processDataSource(setting)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
context.apiService.instance(domain: authContext.mastodonAuthenticationBox.domain)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(_):
|
||||
self.currentInstance.value = nil
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
self.currentInstance.value = response.value
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewModel {
|
||||
|
||||
func openAuthenticationPage(
|
||||
authenticateURL: URL,
|
||||
presentationContextProvider: ASWebAuthenticationPresentationContextProviding
|
||||
) {
|
||||
let authenticationController = MastodonAuthenticationController(
|
||||
context: self.context,
|
||||
authenticateURL: authenticateURL
|
||||
)
|
||||
|
||||
self.mastodonAuthenticationController = authenticationController
|
||||
authenticationController.authenticationSession?.presentationContextProvider = presentationContextProvider
|
||||
authenticationController.authenticationSession?.start()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func processDataSource(_ setting: Setting) {
|
||||
guard let dataSource = self.dataSource else { return }
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SettingsSection, SettingsItem>()
|
||||
|
||||
// appearance
|
||||
let appearanceItems = [
|
||||
SettingsItem.appearance(record: .init(objectID: setting.objectID))
|
||||
]
|
||||
snapshot.appendSections([.appearance])
|
||||
snapshot.appendItems(appearanceItems, toSection: .appearance)
|
||||
|
||||
// preference
|
||||
snapshot.appendSections([.preference])
|
||||
let preferenceItems: [SettingsItem] = SettingsItem.PreferenceType.allCases.map { preferenceType in
|
||||
SettingsItem.preference(settingRecord: .init(objectID: setting.objectID), preferenceType: preferenceType)
|
||||
}
|
||||
snapshot.appendItems(preferenceItems,toSection: .preference)
|
||||
|
||||
// notification
|
||||
let notificationItems = SettingsItem.NotificationSwitchMode.allCases.map { mode in
|
||||
SettingsItem.notification(settingRecord: .init(objectID: setting.objectID), switchMode: mode)
|
||||
}
|
||||
snapshot.appendSections([.notifications])
|
||||
snapshot.appendItems(notificationItems, toSection: .notifications)
|
||||
|
||||
// boring zone
|
||||
let boringZoneSettingsItems: [SettingsItem] = {
|
||||
let links: [SettingsItem.Link] = [
|
||||
.accountSettings,
|
||||
.github,
|
||||
.termsOfService,
|
||||
.privacyPolicy
|
||||
]
|
||||
let items = links.map { SettingsItem.boringZone(item: $0) }
|
||||
return items
|
||||
}()
|
||||
snapshot.appendSections([.boringZone])
|
||||
snapshot.appendItems(boringZoneSettingsItems, toSection: .boringZone)
|
||||
|
||||
let spicyZoneSettingsItems: [SettingsItem] = {
|
||||
let links: [SettingsItem.Link] = [
|
||||
.clearMediaCache,
|
||||
.signOut
|
||||
]
|
||||
let items = links.map { SettingsItem.spicyZone(item: $0) }
|
||||
return items
|
||||
}()
|
||||
snapshot.appendSections([.spicyZone])
|
||||
snapshot.appendItems(spicyZoneSettingsItems, toSection: .spicyZone)
|
||||
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SettingsViewModel {
|
||||
func setupDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
settingsAppearanceTableViewCellDelegate: SettingsAppearanceTableViewCellDelegate,
|
||||
settingsToggleCellDelegate: SettingsToggleCellDelegate
|
||||
) {
|
||||
dataSource = SettingsSection.tableViewDiffableDataSource(
|
||||
for: tableView,
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
settingsAppearanceTableViewCellDelegate: settingsAppearanceTableViewCellDelegate,
|
||||
settingsToggleCellDelegate: settingsToggleCellDelegate
|
||||
)
|
||||
processDataSource(self.setting.value)
|
||||
}
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
//
|
||||
// AppearanceView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-6.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
class AppearanceView: UIView {
|
||||
|
||||
let imageViewShadowBackgroundContainer = ShadowBackgroundContainer()
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = 4
|
||||
view.layer.cornerCurve = .continuous
|
||||
// accessibility
|
||||
view.accessibilityIgnoresInvertColors = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var checkmarkButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.isUserInteractionEnabled = false
|
||||
button.setImage(UIImage(systemName: "circle"), for: .normal)
|
||||
button.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .selected)
|
||||
button.imageView?.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body)
|
||||
button.imageView?.tintColor = Asset.Colors.Label.primary.color
|
||||
button.imageView?.contentMode = .scaleAspectFill
|
||||
return button
|
||||
}()
|
||||
|
||||
lazy var stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.spacing = 8
|
||||
view.distribution = .equalSpacing
|
||||
return view
|
||||
}()
|
||||
|
||||
var selected: Bool = false {
|
||||
didSet { setNeedsLayout() }
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
init(image: UIImage?, title: String) {
|
||||
super.init(frame: .zero)
|
||||
setupUI()
|
||||
|
||||
imageView.image = image
|
||||
titleLabel.text = title
|
||||
}
|
||||
|
||||
override var isAccessibilityElement: Bool {
|
||||
get { return true }
|
||||
set { }
|
||||
|
||||
}
|
||||
override var accessibilityLabel: String? {
|
||||
get { titleLabel.text }
|
||||
set { }
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AppearanceView {
|
||||
|
||||
private func setupUI() {
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageViewShadowBackgroundContainer.addSubview(imageView)
|
||||
imageView.pinToParent()
|
||||
imageViewShadowBackgroundContainer.cornerRadius = 4
|
||||
|
||||
stackView.addArrangedSubview(imageViewShadowBackgroundContainer)
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
stackView.addArrangedSubview(checkmarkButton)
|
||||
|
||||
addSubview(stackView)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.pinToParent()
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 121.0 / 100.0), // height / width
|
||||
])
|
||||
}
|
||||
|
||||
private func configureForSelection() {
|
||||
if selected {
|
||||
accessibilityTraits.insert(.selected)
|
||||
} else {
|
||||
accessibilityTraits.remove(.selected)
|
||||
}
|
||||
|
||||
checkmarkButton.isSelected = selected
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
configureForSelection()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AppearanceView {
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
self.alpha = 0.5
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
UIView.animate(withDuration: 0.33) {
|
||||
self.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
UIView.animate(withDuration: 0.33) {
|
||||
self.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// SettingsSectionHeader.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
struct GroupedTableViewConstraints {
|
||||
static let topMargin: CGFloat = 40
|
||||
static let bottomMargin: CGFloat = 10
|
||||
}
|
||||
|
||||
/// section header which supports add a custom view blelow the title
|
||||
class SettingsSectionHeader: UIView {
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isLayoutMarginsRelativeArrangement = true
|
||||
view.layoutMargins = UIEdgeInsets(
|
||||
top: GroupedTableViewConstraints.topMargin,
|
||||
left: 0,
|
||||
bottom: GroupedTableViewConstraints.bottomMargin,
|
||||
right: 0
|
||||
)
|
||||
view.axis = .vertical
|
||||
return view
|
||||
}()
|
||||
|
||||
init(frame: CGRect, customView: UIView? = nil) {
|
||||
super.init(frame: frame)
|
||||
|
||||
backgroundColor = .clear
|
||||
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
if let view = customView {
|
||||
stackView.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
addSubview(stackView)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.leadingAnchor.constraint(equalTo: self.readableContentGuide.leadingAnchor),
|
||||
stackView.trailingAnchor.constraint(lessThanOrEqualTo: self.readableContentGuide.trailingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
stackView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(title: String?) {
|
||||
titleLabel.text = title?.uppercased()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue