More settings work

This commit is contained in:
Stuart Breckenridge
2022-11-30 21:37:39 +08:00
parent 36fa87b8c4
commit f2cda7fcad
17 changed files with 275 additions and 191 deletions

View File

@@ -0,0 +1,254 @@
//
// SettingsRows.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 12/11/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import UniformTypeIdentifiers
// MARK: - Rows
struct SettingsViewRows {
/// This row, when tapped, will open iOS System Settings.
static var OpenSystemSettings: some View {
Label {
Text("Open System Settings")
} icon: {
Image("system.settings")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 25.0, height: 25.0)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
.onTapGesture {
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
}
}
/// This row, when tapped, will push the New Article Notifications
/// screen in to view.
static var ConfigureNewArticleNotifications: some View {
NavigationLink(destination: NewArticleNotificationsView()) {
Label {
Text("New Article Notifications")
} icon: {
Image("notifications.sounds")
.resizable()
.frame(width: 25.0, height: 25.0)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
}
/// This row, when tapped, will push the the Add Account screen
/// in to view.
static var AddAccount: some View {
NavigationLink(destination: AccountsManagementView()) {
Label {
Text("Manage Accounts")
} icon: {
Image("app.account")
.resizable()
.frame(width: 25.0, height: 25.0)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
}
/// This row, when tapped, will push the the Manage Extension screen
/// in to view.
static var ManageExtensions: some View {
NavigationLink(destination: ExtensionsManagementView()) {
Label {
Text("Manage Extensions")
} icon: {
Image("app.extension")
.resizable()
.frame(width: 25.0, height: 25.0)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
}
/// This row, when tapped, will present an Import/Export
/// menu.
static func ImportExportOPML(showImportView: Binding<Bool>, showExportView: Binding<Bool>, importAccount: Binding<Account?>, exportDocument: Binding<OPMLDocument?>) -> some View {
Menu {
Menu {
ForEach(AccountManager.shared.sortedActiveAccounts, id: \.self) { account in
Button(account.nameForDisplay) {
importAccount.wrappedValue = account
showImportView.wrappedValue = true
}
}
} label: {
Label("Import Subscriptions To...", systemImage: "arrow.down.doc")
}
Divider()
Menu {
ForEach(AccountManager.shared.sortedAccounts, id: \.self) { account in
Button(account.nameForDisplay) {
do {
let document = try OPMLDocument(account)
exportDocument.wrappedValue = document
showExportView.wrappedValue = true
} catch {
print(error.localizedDescription)
}
}
}
} label: {
Label("Export Subscriptions From...", systemImage: "arrow.up.doc")
}
} label: {
Label {
Text("Import/Export Subscriptions")
.foregroundColor(.primary)
} icon: {
Image("app.opml")
.resizable()
.frame(width: 25.0, height: 25.0)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
}
/// Returns a `Toggle` which triggers changes to the user's sort order preference.
/// - Parameter preference: `Binding<Bool>`
/// - Returns: `Toggle`
static func SortOldestToNewest(_ preference: Binding<Bool>) -> some View {
Toggle("Sort Oldest to Newest", isOn: preference)
}
/// Returns a `Toggle` which triggers changes to the user's grouping preference.
/// - Parameter preference: `Binding<Bool>`
/// - Returns: `Toggle`
static func GroupByFeed(_ preference: Binding<Bool>) -> some View {
Toggle("Group by Feed", isOn: preference)
}
/// Returns a `Toggle` which triggers changes to the user's refresh to clear preferences.
/// - Parameter preference: `Binding<Bool>`
/// - Returns: `Toggle`
static func RefreshToClearReadArticles(_ preference: Binding<Bool>) -> some View {
Toggle("Refresh To Clear Read Articles", isOn: preference)
}
/// This row, when tapped, will push the the Timeline Layout screen
/// in to view.
static var TimelineLayout: some View {
NavigationLink(destination: NotificationsViewControllerRepresentable()) {
Label {
Text("Timeline Layout")
} icon: {
Image(systemName: "slider.vertical.3")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 25.0, height: 25.0)
}
}
}
/// This row, when tapped, will push the the Theme Selector screen
/// in to view.
static var ThemeSelection: some View {
NavigationLink(destination: ArticleThemesViewControllerRepresentable().edgesIgnoringSafeArea(.all)) {
HStack {
Text("Article Theme")
Spacer()
Text(ArticleThemesManager.shared.currentTheme.name)
.font(.callout)
.foregroundColor(.secondary)
}
}
}
static func ConfirmMarkAllAsRead(_ preference: Binding<Bool>) -> some View {
Toggle("Confirm Mark All as Read", isOn: preference)
}
static func OpenLinksInNetNewsWire(_ preference: Binding<Bool>) -> some View {
Toggle("Open Links in NetNewsWire", isOn: preference)
}
// TODO: Add Reader Mode Defaults here. See #3684.
static func EnableFullScreenArticles(_ preference: Binding<Bool>) -> some View {
Toggle(isOn: preference) {
VStack(alignment: .leading, spacing: 4) {
Text("Enable Full Screen Articles")
Text("Tap the article top bar to enter Full Screen. Tap the top or bottom to exit.")
.font(.caption)
.foregroundColor(.gray)
}
}
}
/// This row, when tapped, will push the New Article Notifications
/// screen in to view.
static var ConfigureAppearance: some View {
NavigationLink(destination: DisplayAndBehaviorsView()) {
Label {
Text("Display & Behaviors")
} icon: {
Image("app.appearance")
.resizable()
.frame(width: 25.0, height: 25.0)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
}
/// Sets the help sheet the user wishes to see.
/// - Parameters:
/// - sheet: The sheet provided to create the view.
/// - selectedSheet: A `Binding` to the currently selected sheet. This is set, followed by `show`.
/// - show: A `Binding` to `Bool` which triggers the sheet to display.
/// - Returns: `View`
static func ShowHelpSheet(sheet: HelpSheet, selectedSheet: Binding<HelpSheet>, _ show: Binding<Bool>) -> some View {
Label {
Text(sheet.description)
} icon: {
Image(systemName: sheet.systemImage)
.resizable()
.renderingMode(.template)
.foregroundColor(Color(uiColor: .tertiaryLabel))
.aspectRatio(contentMode: .fit)
.frame(width: 25.0, height: 25.0)
}
.onTapGesture {
selectedSheet.wrappedValue = sheet
show.wrappedValue.toggle()
}
}
static var AboutNetNewsWire: some View {
NavigationLink {
AboutView()
} label: {
Label {
Text("About NetNewsWire")
} icon: {
Image(systemName: "info.circle")
.resizable()
.renderingMode(.template)
.foregroundColor(Color(uiColor: .tertiaryLabel))
.aspectRatio(contentMode: .fit)
.frame(width: 25.0, height: 25.0)
}
}
}
}
extension Binding where Value == Bool {
func negate() -> Bool {
return !(self.wrappedValue)
}
}

View File

@@ -0,0 +1,121 @@
//
// SettingsView.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 12/11/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import UniformTypeIdentifiers
import UserNotifications
struct SettingsView: View {
@StateObject private var appDefaults = AppDefaults.shared
@StateObject private var viewModel = SettingsViewModel()
var body: some View {
NavigationView {
List {
// Device Permissions
Section(header: Text("Device Permissions"), footer: Text("Configure NetNewsWire's access to Siri, background app refresh, mobile data, and more.")) {
SettingsViewRows.OpenSystemSettings
}
// Account/Extensions/OPML Management
Section(header: Text("Accounts & Extensions"), footer: Text("Add, delete, enable, or disable accounts and extensions.")) {
SettingsViewRows.AddAccount
SettingsViewRows.ManageExtensions
SettingsViewRows.ImportExportOPML(showImportView: $viewModel.showImportView, showExportView: $viewModel.showExportView, importAccount: $viewModel.importAccount, exportDocument: $viewModel.exportDocument)
}
// Appearance
Section(header: Text("Appearance"), footer: Text("Manage the look, feel, and behavior of NetNewsWire.")) {
SettingsViewRows.ConfigureAppearance
if viewModel.notificationPermissions == .authorized {
SettingsViewRows.ConfigureNewArticleNotifications
}
}
// Help
Section {
ForEach(0..<HelpSheet.allCases.count, id: \.self) { i in
SettingsViewRows.ShowHelpSheet(sheet: HelpSheet.allCases[i], selectedSheet: $viewModel.helpSheet, $viewModel.showHelpSheet)
}
SettingsViewRows.AboutNetNewsWire
}
}
.tint(Color(uiColor: AppAssets.primaryAccentColor))
.listStyle(.insetGrouped)
.navigationTitle(Text("Settings"))
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $viewModel.showAddAccountView) {
AddAccountViewControllerRepresentable().edgesIgnoringSafeArea(.all)
}
.sheet(isPresented: $viewModel.showHelpSheet) {
SafariView(url: viewModel.helpSheet.url)
}
.sheet(isPresented: $viewModel.showAbout) {
AboutView()
}
.task {
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
self.viewModel.notificationPermissions = settings.authorizationStatus
}
}
}
.onReceive(NotificationCenter.default.publisher(for: UIScene.willEnterForegroundNotification)) { _ in
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
self.viewModel.notificationPermissions = settings.authorizationStatus
}
}
}
.fileImporter(isPresented: $viewModel.showImportView, allowedContentTypes: OPMLDocument.readableContentTypes) { result in
switch result {
case .success(let url):
viewModel.importAccount!.importOPML(url) { importResult in
switch importResult {
case .success(_):
viewModel.showImportSuccess = true
case .failure(let error):
viewModel.importExportError = error
viewModel.showImportExportError = true
}
}
case .failure(let error):
viewModel.importExportError = error
viewModel.showImportExportError = true
}
}
.fileExporter(isPresented: $viewModel.showExportView, document: viewModel.exportDocument, contentType: OPMLDocument.writableContentTypes.first!, onCompletion: { result in
switch result {
case .success(_):
viewModel.showExportSuccess = true
case .failure(let error):
viewModel.importExportError = error
viewModel.showImportExportError = true
}
})
.alert("Imported Successfully", isPresented: $viewModel.showImportSuccess) {
Button("Dismiss") {}
} message: {
Text("Import to your \(viewModel.importAccount?.nameForDisplay ?? "") account has completed.")
}
.alert("Exported Successfully", isPresented: $viewModel.showExportSuccess) {
Button("Dismiss") {}
} message: {
Text("Your OPML file has been successfully exported.")
}
.alert("Error", isPresented: $viewModel.showImportExportError) {
Button("Dismiss") {}
} message: {
Text(viewModel.importExportError?.localizedDescription ?? "Import/Export Error")
}
}
}
}

View File

@@ -0,0 +1,32 @@
//
// SettingsViewModel.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 29/11/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import UniformTypeIdentifiers
import UserNotifications
public final class SettingsViewModel: ObservableObject {
@Published public var showAddAccountView: Bool = false
@Published public var helpSheet: HelpSheet = .help
@Published public var showHelpSheet: Bool = false
@Published public var showAbout: Bool = false
@Published public var notificationPermissions: UNAuthorizationStatus = .notDetermined
@Published public var importAccount: Account? = nil
@Published public var exportAccount: Account? = nil
@Published public var showImportView: Bool = false
@Published public var showExportView: Bool = false
@Published public var showImportExportError: Bool = false
@Published public var importExportError: Error?
@Published public var showImportSuccess: Bool = false
@Published public var showExportSuccess: Bool = false
@Published public var exportDocument: OPMLDocument?
}