diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index ac0592c71..c110a7842 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -860,6 +860,10 @@ DFCE4F9228EF26F100405869 /* About.plist in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9028EF26F000405869 /* About.plist */; }; DFCE4F9428EF278300405869 /* Thanks.md in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9328EF278300405869 /* Thanks.md */; }; DFCE4F9528EF278300405869 /* Thanks.md in Resources */ = {isa = PBXBuildFile; fileRef = DFCE4F9328EF278300405869 /* Thanks.md */; }; + DFD406F5291F79C900C02962 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD406F4291F79C900C02962 /* SettingsView.swift */; }; + DFD406F7291FB1A600C02962 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD406F6291FB1A600C02962 /* SafariView.swift */; }; + DFD406FA291FB5E400C02962 /* SettingsRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD406F9291FB5E400C02962 /* SettingsRows.swift */; }; + DFD406FC291FB63B00C02962 /* SettingsHelpSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD406FB291FB63B00C02962 /* SettingsHelpSheets.swift */; }; DFFB8FC2279B75E300AC21D7 /* Account in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4A24D343A500E90810 /* Account */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; DFFC199827A0D0D7004B7AEF /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */; }; DFFC199A27A0D32A004B7AEF /* NotificationsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */; }; @@ -1601,6 +1605,10 @@ DFC14F1628EB17A800F6EE86 /* CreditsNetNewsWireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsNetNewsWireView.swift; sourceTree = ""; }; DFCE4F9028EF26F000405869 /* About.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = About.plist; sourceTree = ""; }; DFCE4F9328EF278300405869 /* Thanks.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Thanks.md; sourceTree = ""; }; + DFD406F4291F79C900C02962 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + DFD406F6291FB1A600C02962 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; + DFD406F9291FB5E400C02962 /* SettingsRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRows.swift; sourceTree = ""; }; + DFD406FB291FB63B00C02962 /* SettingsHelpSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHelpSheets.swift; sourceTree = ""; }; DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NewsFax.nnwtheme; sourceTree = ""; }; DFFC199727A0D0D7004B7AEF /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = ""; }; DFFC199927A0D32A004B7AEF /* NotificationsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewCell.swift; sourceTree = ""; }; @@ -2000,6 +2008,7 @@ 516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */, 519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */, 51A16990235E10D600EB091F /* Settings.storyboard */, + DFD406F8291FB5D500C02962 /* Settings View */, 516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */, 516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */, 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */, @@ -2059,6 +2068,7 @@ 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */, 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */, 51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */, + DFD406F6291FB1A600C02962 /* SafariView.swift */, 51C45250226506F400C03939 /* String-Extensions.swift */, 5108F6D723763094001ABC45 /* TickMarkSlider.swift */, C5A6ED6C23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift */, @@ -2882,6 +2892,16 @@ path = About; sourceTree = ""; }; + DFD406F8291FB5D500C02962 /* Settings View */ = { + isa = PBXGroup; + children = ( + DFD406F4291F79C900C02962 /* SettingsView.swift */, + DFD406F9291FB5E400C02962 /* SettingsRows.swift */, + DFD406FB291FB63B00C02962 /* SettingsHelpSheets.swift */, + ); + path = "Settings View"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -4062,6 +4082,7 @@ 51C452772265091600C03939 /* MultilineUILabelSizer.swift in Sources */, 51C452A522650A2D00C03939 /* SmallIconProvider.swift in Sources */, 51AB8AB323B7F4C6008F147D /* WebViewController.swift in Sources */, + DFD406F7291FB1A600C02962 /* SafariView.swift in Sources */, 516A09392360A2AE00EAE89B /* SettingsComboTableViewCell.swift in Sources */, 176813D22564BA5900D98635 /* WidgetDataEncoder.swift in Sources */, 51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */, @@ -4081,6 +4102,7 @@ 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, 5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */, 84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */, + DFD406FA291FB5E400C02962 /* SettingsRows.swift in Sources */, 5193CD5A245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */, 51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */, 51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */, @@ -4151,6 +4173,7 @@ 51C452A922650DC600C03939 /* ArticleRenderer.swift in Sources */, 51C45297226509E300C03939 /* DefaultFeedsImporter.swift in Sources */, 512E094D2268B8AB00BDCFDD /* DeleteCommand.swift in Sources */, + DFD406FC291FB63B00C02962 /* SettingsHelpSheets.swift in Sources */, 5110C37D2373A8D100A9C04F /* InspectorIconHeaderView.swift in Sources */, 51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */, 515A5180243E90260089E588 /* TwitterFeedProvider-Extensions.swift in Sources */, @@ -4176,6 +4199,7 @@ 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */, D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */, 8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */, + DFD406F5291F79C900C02962 /* SettingsView.swift in Sources */, B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */, C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, 17071EF126F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */, diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift index 254cccbd1..8e7d53dbc 100644 --- a/Shared/ArticleStyles/ArticleThemesManager.swift +++ b/Shared/ArticleStyles/ArticleThemesManager.swift @@ -8,13 +8,14 @@ import Foundation import RSCore +import Combine public extension Notification.Name { static let ArticleThemeNamesDidChangeNotification = Notification.Name("ArticleThemeNamesDidChangeNotification") static let CurrentArticleThemeDidChangeNotification = Notification.Name("CurrentArticleThemeDidChangeNotification") } -final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { +final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, ObservableObject { static var shared: ArticleThemesManager! public let folderPath: String diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index 4c6ec9d75..91de47635 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -7,6 +7,8 @@ // import UIKit +import Combine +import SwiftUI enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable { case automatic = 0 @@ -26,7 +28,7 @@ enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable { } -final class AppDefaults { +final class AppDefaults: ObservableObject { static let defaultThemeName = "Default" @@ -85,6 +87,7 @@ final class AppDefaults { } set { setInt(for: Key.userInterfaceColorPalette, newValue.rawValue) + AppDefaults.shared.objectWillChange.send() } } @@ -94,6 +97,7 @@ final class AppDefaults { } set { AppDefaults.setString(for: Key.addWebFeedAccountID, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -103,6 +107,7 @@ final class AppDefaults { } set { AppDefaults.setString(for: Key.addWebFeedFolderName, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -112,6 +117,7 @@ final class AppDefaults { } set { AppDefaults.setString(for: Key.addFolderAccountID, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -121,6 +127,7 @@ final class AppDefaults { } set { UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs) + AppDefaults.shared.objectWillChange.send() } } @@ -130,6 +137,7 @@ final class AppDefaults { } set { UserDefaults.standard.set(newValue, forKey: Key.hasUsedFullScreenPreviously) + AppDefaults.shared.objectWillChange.send() } } @@ -139,6 +147,7 @@ final class AppDefaults { } set { UserDefaults.standard.setValue(newValue, forKey: Key.useSystemBrowser) + AppDefaults.shared.objectWillChange.send() } } @@ -148,6 +157,7 @@ final class AppDefaults { } set { AppDefaults.setDate(for: Key.lastImageCacheFlushDate, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -157,6 +167,7 @@ final class AppDefaults { } set { AppDefaults.setBool(for: Key.timelineGroupByFeed, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -166,6 +177,7 @@ final class AppDefaults { } set { AppDefaults.setBool(for: Key.refreshClearsReadArticles, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -175,15 +187,33 @@ final class AppDefaults { } set { AppDefaults.setSortDirection(for: Key.timelineSortDirection, newValue) + AppDefaults.shared.objectWillChange.send() } } - + + /// This is a `Bool` wrapper for `timelineSortDirection`'s + /// `ComparisonResult` + var timelineSortDirectionBool: Bool { + get { + if AppDefaults.shared.timelineSortDirection == .orderedAscending { + return true + } + return false + } + set { + if newValue == true { timelineSortDirection = .orderedAscending } else { + timelineSortDirection = .orderedDescending + } + } + } + var articleFullscreenEnabled: Bool { get { return AppDefaults.bool(for: Key.articleFullscreenEnabled) } set { AppDefaults.setBool(for: Key.articleFullscreenEnabled, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -193,6 +223,7 @@ final class AppDefaults { } set { AppDefaults.setBool(for: Key.confirmMarkAllAsRead, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -202,6 +233,7 @@ final class AppDefaults { } set { AppDefaults.setDate(for: Key.lastRefresh, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -211,6 +243,7 @@ final class AppDefaults { } set { AppDefaults.setInt(for: Key.timelineNumberOfLines, newValue) + AppDefaults.shared.objectWillChange.send() } } @@ -221,6 +254,7 @@ final class AppDefaults { } set { AppDefaults.store.set(newValue.rawValue, forKey: Key.timelineIconDimension) + AppDefaults.shared.objectWillChange.send() } } @@ -230,6 +264,7 @@ final class AppDefaults { } set { AppDefaults.setString(for: Key.currentThemeName, newValue) + AppDefaults.shared.objectWillChange.send() } } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index ba3c3a737..ce3ceed7d 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -13,6 +13,7 @@ import Articles import RSCore import RSTree import SafariServices +import SwiftUI protocol MainControllerIdentifiable { var mainControllerIdentifer: MainControllerIdentifier { get } @@ -1129,12 +1130,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } func showSettings(scrollToArticlesSection: Bool = false) { - let settingsNavController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController - let settingsViewController = settingsNavController.topViewController as! SettingsViewController - settingsViewController.scrollToArticlesSection = scrollToArticlesSection - settingsNavController.modalPresentationStyle = .formSheet - settingsViewController.presentingParentController = rootSplitViewController - rootSplitViewController.present(settingsNavController, animated: true) +// let settingsNavController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController +// let settingsViewController = settingsNavController.topViewController as! SettingsViewController +// settingsViewController.scrollToArticlesSection = scrollToArticlesSection +// settingsNavController.modalPresentationStyle = .formSheet +// settingsViewController.presentingParentController = rootSplitViewController +// rootSplitViewController.present(settingsNavController, animated: true) + let hostedSettings = UIHostingController(rootView: SettingsView()) + rootSplitViewController.present(hostedSettings, animated: true) } func showAccountInspector(for account: Account) { diff --git a/iOS/Settings/AddAccountViewController.swift b/iOS/Settings/AddAccountViewController.swift index 8f0e92e44..d2d76de9c 100644 --- a/iOS/Settings/AddAccountViewController.swift +++ b/iOS/Settings/AddAccountViewController.swift @@ -8,8 +8,37 @@ import Account import UIKit +import SwiftUI import RSCore +struct AddAccountViewControllerRepresentable: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> AddAccountViewController { + let storyboard = UIStoryboard(name: "Settings", bundle: .main) + let controller = storyboard.instantiateViewController(withIdentifier: "AddAccountViewController") as! AddAccountViewController + + context.coordinator.parentObserver = controller.observe(\.parent, changeHandler: { vc, _ in + vc.parent?.title = vc.title + vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems + }) + + return controller + } + + func updateUIViewController(_ uiViewController: AddAccountViewController, context: Context) { + // + } + + typealias UIViewControllerType = AddAccountViewController + + class Coordinator { + var parentObserver: NSKeyValueObservation? + } + + func makeCoordinator() -> Self.Coordinator { Coordinator() } + +} + + protocol AddAccountDismissDelegate: UIViewController { func dismiss() } diff --git a/iOS/Settings/ArticleThemesTableViewController.swift b/iOS/Settings/ArticleThemesTableViewController.swift index deefb41e9..1fcf94b9e 100644 --- a/iOS/Settings/ArticleThemesTableViewController.swift +++ b/iOS/Settings/ArticleThemesTableViewController.swift @@ -10,6 +10,36 @@ import Foundation import UniformTypeIdentifiers import RSCore import UIKit +import SwiftUI + +struct ArticleThemesViewControllerRepresentable: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> ArticleThemesTableViewController { + let storyboard = UIStoryboard(name: "Settings", bundle: .main) + let controller = storyboard.instantiateViewController(withIdentifier: "ArticleThemesTableViewController") as! ArticleThemesTableViewController + + context.coordinator.parentObserver = controller.observe(\.parent, changeHandler: { vc, _ in + vc.parent?.title = vc.title + vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems + }) + + return controller + } + + func updateUIViewController(_ uiViewController: ArticleThemesTableViewController, context: Context) { + // + } + + typealias UIViewControllerType = ArticleThemesTableViewController + + class Coordinator { + var parentObserver: NSKeyValueObservation? + } + + func makeCoordinator() -> Self.Coordinator { Coordinator() } + +} + + class ArticleThemesTableViewController: UITableViewController, Logging { diff --git a/iOS/Settings/NotificationsViewController.swift b/iOS/Settings/NotificationsViewController.swift index 1c4d78524..64387f718 100644 --- a/iOS/Settings/NotificationsViewController.swift +++ b/iOS/Settings/NotificationsViewController.swift @@ -7,9 +7,38 @@ // import UIKit +import SwiftUI import Account import UserNotifications +struct NotificationsViewControllerRepresentable: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> NotificationsViewController { + let storyboard = UIStoryboard(name: "Settings", bundle: .main) + let controller = storyboard.instantiateViewController(withIdentifier: "NotificationsViewController") as! NotificationsViewController + + context.coordinator.parentObserver = controller.observe(\.parent, changeHandler: { vc, _ in + vc.parent?.title = vc.title + vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems + }) + + return controller + } + + func updateUIViewController(_ uiViewController: NotificationsViewController, context: Context) { + // + } + + typealias UIViewControllerType = NotificationsViewController + + class Coordinator { + var parentObserver: NSKeyValueObservation? + } + + func makeCoordinator() -> Self.Coordinator { Coordinator() } + +} + + class NotificationsViewController: UIViewController { @IBOutlet weak var notificationsTableView: UITableView! @@ -89,9 +118,9 @@ class NotificationsViewController: UIViewController { private func reloadVisibleCells(_ notification: Notification) { guard let faviconURLString = notification.userInfo?["faviconURL"] as? String, let faviconHost = URL(string: faviconURLString)?.host else { - return - } - + return + } + for cell in notificationsTableView.visibleCells { if let notificationCell = cell as? NotificationsTableViewCell { if let feedURLHost = URL(string: notificationCell.feed!.url)?.host { @@ -117,21 +146,21 @@ class NotificationsViewController: UIViewController { let filterMenu = UIMenu(title: "", - image: nil, - identifier: nil, - options: [.displayInline], - children: [ - UIAction( - title: NSLocalizedString("Show Feeds with Notifications Enabled", comment: "Feeds with Notifications"), image: nil, identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: newArticleNotificationFilter ? .on : .off, - handler: { [weak self] _ in - self?.newArticleNotificationFilter.toggle() - self?.notificationsTableView.reloadData() - })]) + options: [.displayInline], + children: [ + UIAction( + title: NSLocalizedString("Show Feeds with Notifications Enabled", comment: "Feeds with Notifications"), + image: nil, + identifier: nil, + discoverabilityTitle: nil, + attributes: [], + state: newArticleNotificationFilter ? .on : .off, + handler: { [weak self] _ in + self?.newArticleNotificationFilter.toggle() + self?.notificationsTableView.reloadData() + })]) var menus = [UIMenuElement]() diff --git a/iOS/Settings/Settings View/SettingsHelpSheets.swift b/iOS/Settings/Settings View/SettingsHelpSheets.swift new file mode 100644 index 000000000..8fbda4166 --- /dev/null +++ b/iOS/Settings/Settings View/SettingsHelpSheets.swift @@ -0,0 +1,77 @@ +// +// SettingsHelpSheets.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 12/11/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import Foundation + +enum HelpSheet: CustomStringConvertible, CaseIterable { + + case help, website, releaseNotes, howToSupport, gitHubRepository, bugTracker, technotes, slack + + var description: String { + switch self { + case .help: + return NSLocalizedString("NetNewsWire Help", comment: "NetNewsWire Help") + case .website: + return NSLocalizedString("Website", comment: "Website") + case .releaseNotes: + return NSLocalizedString("Release Notes", comment: "Release Notes") + case .howToSupport: + return NSLocalizedString("How to Support NetNewsWire", comment: "How to Support") + case .gitHubRepository: + return NSLocalizedString("GitHub Respository", comment: "Github") + case .bugTracker: + return NSLocalizedString("Bug Tracker", comment: "Bug Tracker") + case .technotes: + return NSLocalizedString("Technotes", comment: "Technotes") + case .slack: + return NSLocalizedString("Slack", comment: "Slack") + } + } + + 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/")! + case .releaseNotes: + return URL(string: URL.releaseNotes.absoluteString)! + case .howToSupport: + return URL(string: "https://github.com/brentsimmons/NetNewsWire/blob/main/Technotes/HowToSupportNetNewsWire.markdown")! + case .gitHubRepository: + return URL(string: "https://github.com/brentsimmons/NetNewsWire")! + case .bugTracker: + return URL(string: "https://github.com/brentsimmons/NetNewsWire/issues")! + case .technotes: + return URL(string: "https://github.com/brentsimmons/NetNewsWire/tree/main/Technotes")! + case .slack: + return URL(string: "https://netnewswire.com/slack")! + } + } + + var systemImage: String { + switch self { + case .help: + return "questionmark.app" + case .website: + return "globe" + case .releaseNotes: + return "quote.opening" + case .howToSupport: + return "person.3.fill" + case .gitHubRepository: + return "archivebox" + case .bugTracker: + return "ladybug" + case .technotes: + return "chevron.left.slash.chevron.right" + case .slack: + return "quote.bubble.fill" + } + } +} diff --git a/iOS/Settings/Settings View/SettingsRows.swift b/iOS/Settings/Settings View/SettingsRows.swift new file mode 100644 index 000000000..cea3f0448 --- /dev/null +++ b/iOS/Settings/Settings View/SettingsRows.swift @@ -0,0 +1,256 @@ +// +// SettingsRows.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 12/11/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Account + +// MARK: - Headers + +struct SettingsViewHeaders { + + static func AddAccountHeader(_ showAddAccount: Binding) -> some View { + HStack { + Text("Accounts") + Spacer() + Button { + showAddAccount.wrappedValue.toggle() + } label: { + Text("Add") + .font(.caption) + .bold() + Image(systemName: "plus") + .font(.caption) + } + .buttonBorderShape(.capsule) + .buttonStyle(.bordered) + .padding(.trailing, -15) // moves to trailing edge + } + } + +} + +// MARK: - Rows + +struct SettingsViewRows { + + /// This row, when tapped, will open system settings. + static var OpenSystemSettings: some View { + Label { + Text("Open System Settings") + } icon: { + Image(systemName: "gear.circle.fill") + .resizable() + .renderingMode(.template) + .foregroundColor(.gray) + .aspectRatio(contentMode: .fit) + .frame(width: 25.0, height: 25.0) + } + .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: NotificationsViewControllerRepresentable().edgesIgnoringSafeArea(.all)) { + Label { + Text("New Article Notifications") + } icon: { + Image(systemName: "bell.square.fill") + .resizable() + .renderingMode(.template) + .foregroundColor(.red) + .aspectRatio(contentMode: .fit) + .frame(width: 25.0, height: 25.0) + } + } + } + + /// This row, when tapped, will push the the Add Account screen + /// in to view. + static var AddAccount: some View { + NavigationLink(destination: AddAccountViewControllerRepresentable().edgesIgnoringSafeArea(.all)) { + Label { + Text("Add Account") + } icon: { + Image(systemName: "plus.app.fill") + .resizable() + .renderingMode(.template) + .foregroundColor(Color(uiColor: AppAssets.primaryAccentColor)) + .aspectRatio(contentMode: .fit) + .frame(width: 25.0, height: 25.0) + } + } + } + + /// This `view` creates a `Label` for each active `Account`. + /// Each `Label`, when tapped, will present the configurator for + /// the `Account`. + static var ActiveAccounts: some View { + ForEach(AccountManager.shared.sortedActiveAccounts, id: \.self) { account in + Label { + Text(account.nameForDisplay) + } icon: { + Image(uiImage: AppAssets.image(for: account.type)!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 25.0, height: 25.0) + } + } + } + + /// This row, when tapped, will push the the Add Extension screen + /// in to view. + static var AddExtension: some View { + NavigationLink(destination: NotificationsViewControllerRepresentable()) { + Label { + Text("Add Extension") + } icon: { + Image(systemName: "puzzlepiece.extension") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 25.0, height: 25.0) + } + } + } + + /// This row, when tapped, will push the the Import subscriptions screen + /// in to view. + static var ImportSubscription: some View { + Label { + Text("Import Subscriptions") + } icon: { + Image(systemName: "square.and.arrow.down") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 25.0, height: 25.0) + } + } + + /// This row, when tapped, will push the the Export subscriptions screen + /// in to view. + static var ExportSubscription: some View { + Label { + Text("Import Subscriptions") + } icon: { + Image(systemName: "square.and.arrow.up") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 25.0, height: 25.0) + } + } + + /// Returns a `Toggle` which triggers changes to the user's sort order preference. + /// - Parameter preference: `Binding` + /// - Returns: `Toggle` + static func SortOldestToNewest(_ preference: Binding) -> some View { + Toggle("Sort Oldest to Newest", isOn: preference) + } + + /// Returns a `Toggle` which triggers changes to the user's grouping preference. + /// - Parameter preference: `Binding` + /// - Returns: `Toggle` + static func GroupByFeed(_ preference: Binding) -> 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` + /// - Returns: `Toggle` + static func RefreshToClearReadArticles(_ preference: Binding) -> 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) + } + } + } + + static var ThemeSelection: some View { + NavigationLink(destination: ArticleThemesViewControllerRepresentable().edgesIgnoringSafeArea(.all)) { + Label { + Text("Article Themes") + } icon: { + Image(systemName: "textformat") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 25.0, height: 25.0) + } + } + } + + static func ConfirmMarkAllAsRead(_ preference: Binding) -> some View { + Toggle("Confirm Mark All as Read", isOn: preference) + } + + static func OpenLinksInNetNewsWire(_ preference: Binding) -> some View { + Toggle("Open Links in NetNewsWire", isOn: preference) + } + + static func EnableFullScreenArticles(_ preference: Binding) -> 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: NotificationsViewControllerRepresentable().edgesIgnoringSafeArea(.all)) { + Label { + Text("Configure Appearance") + } icon: { + Image(systemName: "rectangle.trailinghalf.filled") + .resizable() + .renderingMode(.template) + .foregroundColor(.black) + .aspectRatio(contentMode: .fit) + .frame(width: 25.0, height: 25.0) + } + } + } + + /// 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, _ show: Binding) -> 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() + } + } +} diff --git a/iOS/Settings/Settings View/SettingsView.swift b/iOS/Settings/Settings View/SettingsView.swift new file mode 100644 index 000000000..82f05315d --- /dev/null +++ b/iOS/Settings/Settings View/SettingsView.swift @@ -0,0 +1,76 @@ +// +// SettingsView.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 12/11/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Account + +struct SettingsView: View { + + @StateObject private var appDefaults = AppDefaults.shared + @State private var showAddAccountView: Bool = false + @State private var helpSheet: HelpSheet = .help + @State private var showHelpSheet: Bool = false + + var body: some View { + NavigationView { + List { + Section("Notifications, Badge, Data, and More") { + SettingsViewRows.OpenSystemSettings + SettingsViewRows.ConfigureNewArticleNotifications + } + + Section(header: SettingsViewHeaders.AddAccountHeader($showAddAccountView)) { + SettingsViewRows.ActiveAccounts + } + + Section("Extensions") { + SettingsViewRows.AddExtension + } + + Section("Subscriptions") { + SettingsViewRows.ImportSubscription + SettingsViewRows.ExportSubscription + } + + Section("Timeline") { + SettingsViewRows.SortOldestToNewest($appDefaults.timelineSortDirectionBool) + SettingsViewRows.GroupByFeed($appDefaults.timelineGroupByFeed) + SettingsViewRows.RefreshToClearReadArticles($appDefaults.refreshClearsReadArticles) + SettingsViewRows.TimelineLayout + } + + Section("Articles") { + SettingsViewRows.ThemeSelection + SettingsViewRows.ConfirmMarkAllAsRead($appDefaults.confirmMarkAllAsRead) + SettingsViewRows.OpenLinksInNetNewsWire($appDefaults.useSystemBrowser) + SettingsViewRows.EnableFullScreenArticles($appDefaults.articleFullscreenEnabled) + } + + Section("Appearance") { + SettingsViewRows.ConfigureAppearance + } + + Section("Help") { + ForEach(0.. - + - + - + - + - - + - + - - + diff --git a/iOS/UIKit Extensions/SafariView.swift b/iOS/UIKit Extensions/SafariView.swift new file mode 100644 index 000000000..0b2e2179a --- /dev/null +++ b/iOS/UIKit Extensions/SafariView.swift @@ -0,0 +1,24 @@ +// +// SafariView.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 12/11/2022. +// Copyright © 2022 Ranchero Software. All rights reserved. +// + +import SwiftUI +import SafariServices + +struct SafariView: UIViewControllerRepresentable { + + let url: URL + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { + return SFSafariViewController(url: url) + } + + func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) { + + } + +}