mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
More settings work
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// AccountsManagementView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 13/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import Combine
|
||||
|
||||
struct AddAccountWrapper: UIViewControllerRepresentable {
|
||||
func makeUIViewController(context: Context) -> AddAccountViewController {
|
||||
let controller = UIStoryboard.settings.instantiateViewController(withIdentifier: "AddAccountViewController") as! AddAccountViewController
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: AddAccountViewController, context: Context) {
|
||||
//
|
||||
}
|
||||
|
||||
typealias UIViewControllerType = AddAccountViewController
|
||||
|
||||
}
|
||||
|
||||
struct AccountsManagementView: View {
|
||||
|
||||
@State private var showAddAccountSheet: Bool = false
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
@State private var sortedAccounts = [Account]()
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(sortedAccounts, id: \.self) { account in
|
||||
accountRow(account)
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("Accounts"))
|
||||
.tint(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showAddAccountSheet = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .AccountStateDidChange)) { _ in
|
||||
refreshAccounts()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in
|
||||
refreshAccounts()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .UserDidDeleteAccount)) { _ in
|
||||
refreshAccounts()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .DisplayNameDidChange)) { _ in
|
||||
refreshAccounts()
|
||||
}
|
||||
.task(priority: .userInitiated) {
|
||||
refreshAccounts()
|
||||
}
|
||||
.sheet(isPresented: $showAddAccountSheet) {
|
||||
NavigationView {
|
||||
AddAccountWrapper()
|
||||
.navigationTitle("Add Account")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAccounts() {
|
||||
sortedAccounts = []
|
||||
sortedAccounts = AccountManager.shared.sortedAccounts
|
||||
}
|
||||
|
||||
func accountRow(_ account: Account) -> some View {
|
||||
NavigationLink {
|
||||
AccountInspectorWrapper(account: account)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
} label: {
|
||||
Image(uiImage: account.smallIcon!.image)
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
Text(account.nameForDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
var inactiveFooterText: some View {
|
||||
if AccountManager.shared.sortedAccounts.filter({ $0.isActive == false }).count == 0 {
|
||||
return Text("There are no inactive accounts.")
|
||||
} else {
|
||||
return Text("")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct AddAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AccountsManagementView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// ExtensionsManagementView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 30/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
|
||||
struct ExtensionsManagementView: View {
|
||||
|
||||
@State private var availableExtensionPointTypes = ExtensionPointManager.shared.availableExtensionPointTypes.sorted(by: { $0.title < $1.title })
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Add Extension"), footer: Text("Extensions allow you to subscribe to some pages as if they were RSS feeds.")) {
|
||||
ForEach(0..<availableExtensionPointTypes.count, id: \.self) { i in
|
||||
NavigationLink {
|
||||
EnableExtensionPointViewWrapper(extensionPoint: availableExtensionPointTypes[i])
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
} label: {
|
||||
Image(uiImage: availableExtensionPointTypes[i].image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25)
|
||||
|
||||
Text("\(availableExtensionPointTypes[i].title)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Active Extensions")) {
|
||||
ForEach(0..<ExtensionPointManager.shared.activeExtensionPoints.count, id: \.self) { i in
|
||||
let point = Array(ExtensionPointManager.shared.activeExtensionPoints)[i]
|
||||
NavigationLink {
|
||||
ExtensionPointInspectorWrapper(extensionPoint: point.value)
|
||||
.navigationBarTitle(Text(point.value.title))
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
} label: {
|
||||
Image(uiImage: point.value.image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25)
|
||||
Text(point.value.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("Manage Extensions"))
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtensionsManagementView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ExtensionsManagementView()
|
||||
}
|
||||
}
|
||||
87
iOS/Settings/Views/Appearance/ColorPaletteSelectorView.swift
Normal file
87
iOS/Settings/Views/Appearance/ColorPaletteSelectorView.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// ColorPaletteSelectorView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 13/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ColorPaletteSelectorView: View {
|
||||
|
||||
@StateObject private var appDefaults = AppDefaults.shared
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
appLightButton()
|
||||
Spacer()
|
||||
appDarkButton()
|
||||
Spacer()
|
||||
appAutomaticButton()
|
||||
}
|
||||
}
|
||||
|
||||
func appLightButton() -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Image("app.appearance.light")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40.0, height: 40.0)
|
||||
Text("Always Light")
|
||||
.font(.subheadline)
|
||||
if AppDefaults.userInterfaceColorPalette == .light {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
}
|
||||
}.onTapGesture {
|
||||
AppDefaults.userInterfaceColorPalette = .light
|
||||
}
|
||||
}
|
||||
|
||||
func appDarkButton() -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Image("app.appearance.dark")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40.0, height: 40.0)
|
||||
Text("Always Dark")
|
||||
.font(.subheadline)
|
||||
if AppDefaults.userInterfaceColorPalette == .dark {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
}
|
||||
}.onTapGesture {
|
||||
AppDefaults.userInterfaceColorPalette = .dark
|
||||
}
|
||||
}
|
||||
|
||||
func appAutomaticButton() -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Image("app.appearance.automatic")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40.0, height: 40.0)
|
||||
Text("Use System")
|
||||
.font(.subheadline)
|
||||
if AppDefaults.userInterfaceColorPalette == .automatic {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
}
|
||||
}.onTapGesture {
|
||||
AppDefaults.userInterfaceColorPalette = .automatic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayModeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ColorPaletteSelectorView()
|
||||
}
|
||||
}
|
||||
50
iOS/Settings/Views/Appearance/DisplayAndBehaviorsView.swift
Normal file
50
iOS/Settings/Views/Appearance/DisplayAndBehaviorsView.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// DisplayAndBehaviorsView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DisplayAndBehaviorsView: View {
|
||||
|
||||
@StateObject private var appDefaults = AppDefaults.shared
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Application") {
|
||||
ColorPaletteSelectorView()
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
Section("Timeline") {
|
||||
SettingsViewRows.SortOldestToNewest($appDefaults.timelineSortDirectionBool)
|
||||
SettingsViewRows.GroupByFeed($appDefaults.timelineGroupByFeed)
|
||||
SettingsViewRows.RefreshToClearReadArticles($appDefaults.refreshClearsReadArticles)
|
||||
}
|
||||
|
||||
Section("Article") {
|
||||
SettingsViewRows.ThemeSelection
|
||||
SettingsViewRows.ConfirmMarkAllAsRead($appDefaults.confirmMarkAllAsRead)
|
||||
SettingsViewRows.OpenLinksInNetNewsWire(Binding<Bool>(
|
||||
get: { !appDefaults.useSystemBrowser },
|
||||
set: { appDefaults.useSystemBrowser = !$0 }
|
||||
))
|
||||
// TODO: Add Reader Mode Defaults here. See #3684.
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("Display & Behaviors"))
|
||||
.tint(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
struct AppearanceManagementView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DisplayAndBehaviorsView()
|
||||
}
|
||||
}
|
||||
254
iOS/Settings/Views/General/SettingsRows.swift
Normal file
254
iOS/Settings/Views/General/SettingsRows.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
121
iOS/Settings/Views/General/SettingsView.swift
Normal file
121
iOS/Settings/Views/General/SettingsView.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
iOS/Settings/Views/General/SettingsViewModel.swift
Normal file
32
iOS/Settings/Views/General/SettingsViewModel.swift
Normal 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?
|
||||
|
||||
}
|
||||
108
iOS/Settings/Views/Help/AboutView.swift
Normal file
108
iOS/Settings/Views/Help/AboutView.swift
Normal file
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// AboutView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 02/10/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AboutView: View, LoadableAboutData {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: aboutHeaderView) {}
|
||||
Section(header: Text("Primary Contributors")) {
|
||||
ForEach(0..<about.PrimaryContributors.count, id: \.self) { i in
|
||||
contributorView(about.PrimaryContributors[i])
|
||||
}
|
||||
}
|
||||
Section(header: Text("Additional Contributors")) {
|
||||
ForEach(0..<about.AdditionalContributors.count, id: \.self) { i in
|
||||
contributorView(about.AdditionalContributors[i])
|
||||
}
|
||||
}
|
||||
Section(header: Text("Thanks"), footer: thanks, content: {})
|
||||
Section(footer: copyright, content: {})
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle(Text("About"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
var aboutHeaderView: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack(alignment: .center, spacing: 8) {
|
||||
Image("About")
|
||||
.resizable()
|
||||
.frame(width: 75, height: 75)
|
||||
|
||||
Text(Bundle.main.appName)
|
||||
.font(.headline)
|
||||
|
||||
Text("\(Bundle.main.versionNumber) (\(Bundle.main.buildNumber))")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.callout)
|
||||
|
||||
Text("By Brent Simmons and the Ranchero Software team.")
|
||||
.font(.subheadline)
|
||||
|
||||
Text("[netnewswire.com](https://netnewswire.com)")
|
||||
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.textCase(.none)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
func contributorView(_ appCredit: AboutData.Contributor) -> some View {
|
||||
HStack {
|
||||
Text(appCredit.name)
|
||||
Spacer()
|
||||
if let role = appCredit.role {
|
||||
Text(role)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
if let _ = appCredit.url {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
guard let url = appCredit.url else { return }
|
||||
if let creditURL = URL(string: url) {
|
||||
UIApplication.shared.open(creditURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var thanks: some View {
|
||||
Text(about.ThanksMarkdown)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
var copyright: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(verbatim: "Copyright © Brent Simmons 2002 - \(Calendar.current.component(.year, from: .now))")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct AboutView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
AboutView()
|
||||
}
|
||||
}
|
||||
}
|
||||
42
iOS/Settings/Views/Help/SettingsHelpSheets.swift
Normal file
42
iOS/Settings/Views/Help/SettingsHelpSheets.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// SettingsHelpSheets.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public enum HelpSheet: CustomStringConvertible, CaseIterable {
|
||||
|
||||
case help, website
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .help:
|
||||
return NSLocalizedString("NetNewsWire Help", comment: "NetNewsWire Help")
|
||||
case .website:
|
||||
return NSLocalizedString("NetNewsWire Website", comment: "NetNewsWire Website")
|
||||
}
|
||||
}
|
||||
|
||||
public var url: URL {
|
||||
switch self {
|
||||
case .help:
|
||||
return URL(string: "https://netnewswire.com/help/ios/6.1/en/")!
|
||||
case .website:
|
||||
return URL(string: "https://netnewswire.com/")!
|
||||
}
|
||||
}
|
||||
|
||||
public var systemImage: String {
|
||||
switch self {
|
||||
case .help:
|
||||
return "questionmark.app"
|
||||
case .website:
|
||||
return "globe"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// NewArticleNotificationsView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 29/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
struct NewArticleNotificationsView: View {
|
||||
|
||||
@State private var activeAccounts = AccountManager.shared.sortedActiveAccounts
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(activeAccounts, id: \.accountID) { account in
|
||||
Section(header: Text(account.nameForDisplay)) {
|
||||
ForEach(sortedWebFeedsForAccount(account), id: \.webFeedID) { feed in
|
||||
notificationToggle(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("New Article Notifications"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .WebFeedIconDidBecomeAvailable)) { _ in
|
||||
activeAccounts = AccountManager.shared.sortedActiveAccounts
|
||||
}
|
||||
}
|
||||
.tint(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
}
|
||||
|
||||
|
||||
private func sortedWebFeedsForAccount(_ account: Account) -> [WebFeed] {
|
||||
return Array(account.flattenedWebFeeds()).sorted(by: { $0.nameForDisplay.caseInsensitiveCompare($1.nameForDisplay) == .orderedAscending })
|
||||
}
|
||||
|
||||
private func notificationToggle(_ webfeed: WebFeed) -> some View {
|
||||
HStack {
|
||||
Image(uiImage: IconImageCache.shared.imageFor(webfeed.feedID!)!.image)
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.cornerRadius(4)
|
||||
|
||||
Text(webfeed.nameForDisplay)
|
||||
Spacer()
|
||||
Toggle("", isOn: Binding(
|
||||
get: { webfeed.isNotifyAboutNewArticles ?? false },
|
||||
set: { webfeed.isNotifyAboutNewArticles = $0 }))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct NewArticleNotificationsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NewArticleNotificationsView()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user