diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index eab9a6c36..e0bdd2f85 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -65,6 +65,22 @@ 848E84D82DB749720023F3BA /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 848E84D72DB749720023F3BA /* RSCore */; }; 848E84DB2DB749860023F3BA /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 848E84DA2DB749860023F3BA /* RSCore */; }; 848E84DF2DB749A40023F3BA /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 848E84DE2DB749A40023F3BA /* RSCore */; }; + 849182CC2FD32B1B005C3020 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3010 /* Appanoose.nnwtheme */; }; + 849182CC2FD32B1B005C3021 /* Biblioteca.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3011 /* Biblioteca.nnwtheme */; }; + 849182CC2FD32B1B005C3022 /* Hyperlegible.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3012 /* Hyperlegible.nnwtheme */; }; + 849182CC2FD32B1B005C3023 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3013 /* NewsFax.nnwtheme */; }; + 849182CC2FD32B1B005C3024 /* Promenade.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3014 /* Promenade.nnwtheme */; }; + 849182CC2FD32B1B005C3025 /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3015 /* Sepia.nnwtheme */; }; + 849182CC2FD32B1B005C3026 /* Tiqoe Dark.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3016 /* Tiqoe Dark.nnwtheme */; }; + 849182CC2FD32B1B005C3027 /* Verdana Revival.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3017 /* Verdana Revival.nnwtheme */; }; + 849182DD2FD32B1B005C3030 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3010 /* Appanoose.nnwtheme */; }; + 849182DD2FD32B1B005C3031 /* Biblioteca.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3011 /* Biblioteca.nnwtheme */; }; + 849182DD2FD32B1B005C3032 /* Hyperlegible.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3012 /* Hyperlegible.nnwtheme */; }; + 849182DD2FD32B1B005C3033 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3013 /* NewsFax.nnwtheme */; }; + 849182DD2FD32B1B005C3034 /* Promenade.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3014 /* Promenade.nnwtheme */; }; + 849182DD2FD32B1B005C3035 /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3015 /* Sepia.nnwtheme */; }; + 849182DD2FD32B1B005C3036 /* Tiqoe Dark.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3016 /* Tiqoe Dark.nnwtheme */; }; + 849182DD2FD32B1B005C3037 /* Verdana Revival.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3017 /* Verdana Revival.nnwtheme */; }; 84EE3F122DB8A088009D3A8D /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 84EE3F112DB8A088009D3A8D /* RSTree */; }; 84EE3F132DB8A088009D3A8D /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 84EE3F112DB8A088009D3A8D /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 84EE3F152DB8A0A0009D3A8D /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 84EE3F142DB8A0A0009D3A8D /* RSTree */; }; @@ -91,22 +107,6 @@ D1A7E100000000000000C005 /* Images in Frameworks */ = {isa = PBXBuildFile; productRef = D1A7E100000000000000C004 /* Images */; }; D1A7E100000000000000C006 /* Images in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D1A7E100000000000000C004 /* Images */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; D1A7E100000000000000C008 /* Images in Frameworks */ = {isa = PBXBuildFile; productRef = D1A7E100000000000000C007 /* Images */; }; - 849182CC2FD32B1B005C3020 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3010 /* Appanoose.nnwtheme */; }; - 849182CC2FD32B1B005C3021 /* Biblioteca.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3011 /* Biblioteca.nnwtheme */; }; - 849182CC2FD32B1B005C3022 /* Hyperlegible.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3012 /* Hyperlegible.nnwtheme */; }; - 849182CC2FD32B1B005C3023 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3013 /* NewsFax.nnwtheme */; }; - 849182CC2FD32B1B005C3024 /* Promenade.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3014 /* Promenade.nnwtheme */; }; - 849182CC2FD32B1B005C3025 /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3015 /* Sepia.nnwtheme */; }; - 849182CC2FD32B1B005C3026 /* Tiqoe Dark.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3016 /* Tiqoe Dark.nnwtheme */; }; - 849182CC2FD32B1B005C3027 /* Verdana Revival.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3017 /* Verdana Revival.nnwtheme */; }; - 849182DD2FD32B1B005C3030 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3010 /* Appanoose.nnwtheme */; }; - 849182DD2FD32B1B005C3031 /* Biblioteca.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3011 /* Biblioteca.nnwtheme */; }; - 849182DD2FD32B1B005C3032 /* Hyperlegible.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3012 /* Hyperlegible.nnwtheme */; }; - 849182DD2FD32B1B005C3033 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3013 /* NewsFax.nnwtheme */; }; - 849182DD2FD32B1B005C3034 /* Promenade.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3014 /* Promenade.nnwtheme */; }; - 849182DD2FD32B1B005C3035 /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3015 /* Sepia.nnwtheme */; }; - 849182DD2FD32B1B005C3036 /* Tiqoe Dark.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3016 /* Tiqoe Dark.nnwtheme */; }; - 849182DD2FD32B1B005C3037 /* Verdana Revival.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 849182BB2FD32B1B005C3017 /* Verdana Revival.nnwtheme */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -326,9 +326,6 @@ 848CEF1E2DB74B56002B35B5 /* NetNewsWire-iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "NetNewsWire-iOS.xctestplan"; sourceTree = ""; }; 848E84C92DB743FF0023F3BA /* NetNewsWire-CI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "NetNewsWire-CI.xctestplan"; sourceTree = ""; }; 848E84CA2DB743FF0023F3BA /* NetNewsWire.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = NetNewsWire.xctestplan; sourceTree = ""; }; - 849C64601ED37A5D003D8FC0 /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 849C64711ED37A5D003D8FC0 /* NetNewsWireTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetNewsWireTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 84D2200922B0BC4B0019E085 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; 849182BB2FD32B1B005C3010 /* Appanoose.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Appanoose.nnwtheme; sourceTree = ""; }; 849182BB2FD32B1B005C3011 /* Biblioteca.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Biblioteca.nnwtheme; sourceTree = ""; }; 849182BB2FD32B1B005C3012 /* Hyperlegible.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Hyperlegible.nnwtheme; sourceTree = ""; }; @@ -337,6 +334,9 @@ 849182BB2FD32B1B005C3015 /* Sepia.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Sepia.nnwtheme; sourceTree = ""; }; 849182BB2FD32B1B005C3016 /* Tiqoe Dark.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Tiqoe Dark.nnwtheme"; sourceTree = ""; }; 849182BB2FD32B1B005C3017 /* Verdana Revival.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Verdana Revival.nnwtheme"; sourceTree = ""; }; + 849C64601ED37A5D003D8FC0 /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 849C64711ED37A5D003D8FC0 /* NetNewsWireTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetNewsWireTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 84D2200922B0BC4B0019E085 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -685,6 +685,21 @@ name = "Recovered References"; sourceTree = ""; }; + 849182072FD32B1B005C3150 /* Themes */ = { + isa = PBXGroup; + children = ( + 849182BB2FD32B1B005C3010 /* Appanoose.nnwtheme */, + 849182BB2FD32B1B005C3011 /* Biblioteca.nnwtheme */, + 849182BB2FD32B1B005C3012 /* Hyperlegible.nnwtheme */, + 849182BB2FD32B1B005C3013 /* NewsFax.nnwtheme */, + 849182BB2FD32B1B005C3014 /* Promenade.nnwtheme */, + 849182BB2FD32B1B005C3015 /* Sepia.nnwtheme */, + 849182BB2FD32B1B005C3016 /* Tiqoe Dark.nnwtheme */, + 849182BB2FD32B1B005C3017 /* Verdana Revival.nnwtheme */, + ); + path = Themes; + sourceTree = ""; + }; 849C64571ED37A5D003D8FC0 = { isa = PBXGroup; children = ( @@ -727,21 +742,6 @@ name = Products; sourceTree = ""; }; - 849182072FD32B1B005C3150 /* Themes */ = { - isa = PBXGroup; - children = ( - 849182BB2FD32B1B005C3010 /* Appanoose.nnwtheme */, - 849182BB2FD32B1B005C3011 /* Biblioteca.nnwtheme */, - 849182BB2FD32B1B005C3012 /* Hyperlegible.nnwtheme */, - 849182BB2FD32B1B005C3013 /* NewsFax.nnwtheme */, - 849182BB2FD32B1B005C3014 /* Promenade.nnwtheme */, - 849182BB2FD32B1B005C3015 /* Sepia.nnwtheme */, - 849182BB2FD32B1B005C3016 /* Tiqoe Dark.nnwtheme */, - 849182BB2FD32B1B005C3017 /* Verdana Revival.nnwtheme */, - ); - path = Themes; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ diff --git a/Shared/Localizable.xcstrings b/Shared/Localizable.xcstrings index da730e9cd..1e4256dbe 100644 --- a/Shared/Localizable.xcstrings +++ b/Shared/Localizable.xcstrings @@ -112,7 +112,7 @@ "comment" : "Article" }, "Article Content Records" : { - "comment" : "Article content records section header" + "comment" : "Article content records header" }, "Article Search" : { "comment" : "Article Search" @@ -130,7 +130,7 @@ "comment" : "Automatic" }, "BazQux" : { - "comment" : "Account name" + "comment" : "Account name\nBazQux" }, "Can’t add a feed because no feed was found." : { "comment" : "Feed finder" @@ -145,10 +145,10 @@ "comment" : "Feed finder" }, "Cancel" : { - "comment" : "Cancel\nCancel Delete Account\nCancel Install Theme\nCancel button" + "comment" : "Cancel\nCancel button" }, "Canceled." : { - "comment" : "Scan status text when canceled" + "comment" : "Scan canceled text" }, "Choose a location for the exported OPML file." : { "comment" : "Export OPML" @@ -166,7 +166,7 @@ "comment" : "Export Account" }, "Clean Up" : { - "comment" : "Clean Up\nClean Up button title\nClean up alert button" + "comment" : "Clean Up\nClean up alert button" }, "Clean Up iCloud Records" : { "comment" : "Clean up alert title" @@ -202,19 +202,19 @@ }, "Copy Article URL" : { - "comment" : "Command" + "comment" : "Copy Article URL" }, "Copy Contents" : { "comment" : "Copy Contents button" }, "Copy External URL" : { - "comment" : "Command" + "comment" : "Copy External URL" }, "Copy Feed URL" : { - "comment" : "Command" + "comment" : "Copy Feed URL" }, "Copy Home Page URL" : { - "comment" : "Command" + "comment" : "Copy Home Page URL" }, "Couldn’t complete the iCloud scan because of an error:" : { "comment" : "Scan error alert title" @@ -238,19 +238,19 @@ "comment" : "Default" }, "Delete" : { - "comment" : "Command\nDelete\nDelete Account\nDelete button" + "comment" : "Delete" }, "Delete Feed" : { - "comment" : "Delete Feed\nDelete feed alert title\nUndo action name\ncommand" + "comment" : "Delete feed\ncommand" }, "Delete Feeds" : { - "comment" : "Delete feeds alert title\nUndo action name\ncommand" + "comment" : "command" }, "Delete Feeds and Folders" : { "comment" : "command" }, "Delete Folder" : { - "comment" : "Delete Folder\ncommand" + "comment" : "Delete folder\ncommand" }, "Delete Folders" : { "comment" : "command" @@ -372,6 +372,10 @@ "Feed not found" : { "comment" : "Feed finder" }, + "Feed Title" : { + "comment" : "A label for the feed title.", + "isCommentAutoGenerated" : true + }, "Feedbin" : { "comment" : "Account name" }, @@ -415,7 +419,7 @@ "comment" : "Folder Inspector window title" }, "FreshRSS" : { - "comment" : "Account name" + "comment" : "Account name\nFreshRSS" }, "General" : { "comment" : "Preferences" @@ -469,7 +473,7 @@ "comment" : "Cleanup phase text when completed" }, "iCloud Storage Stats" : { - "comment" : "iCloud Storage Stats window title" + "comment" : "Navigation title for iCloud stats view" }, "Icon Preview" : { "comment" : "Icon Preview" @@ -484,7 +488,7 @@ "comment" : "Import Theme" }, "Inoreader" : { - "comment" : "Account name" + "comment" : "Account name\nInoreader" }, "Inspector" : { "comment" : "Inspector window title" @@ -543,37 +547,37 @@ "comment" : "Manually" }, "Mark Above as Read" : { - "comment" : "Command" + "comment" : "Mark Above as Read" }, "Mark All as Read" : { - "comment" : "Command\nMark All as Read" + "comment" : "Mark All as Read" }, "Mark All as Read in “%@”" : { - "comment" : "Command" + "comment" : "Command\nMark All as Read in Feed" }, "Mark Article Unread" : { "comment" : "Mark Article Unread" }, "Mark as Read" : { - "comment" : "Command\nMark as Read" + "comment" : "Mark as Read" }, "Mark As Read" : { "comment" : "Mark As Read" }, "Mark as Starred" : { - "comment" : "Command\nMark as Starred" + "comment" : "Mark as Starred" }, "Mark as Unread" : { - "comment" : "Command" + "comment" : "Mark as Unread" }, "Mark as Unstarred" : { - "comment" : "Command" + "comment" : "Mark as Unstarred" }, "Mark Below as Read" : { - "comment" : "Command" + "comment" : "Mark Below as Read" }, "Mark Read" : { - "comment" : "Mark Read\ncommand" + "comment" : "command" }, "Mark Starred" : { "comment" : "command" @@ -622,13 +626,13 @@ "comment" : "New Article Notifications" }, "New Feed" : { - "comment" : "Command" + "comment" : "New Feed" }, "New Feed…" : { "comment" : "New Feed" }, "New Folder" : { - "comment" : "Command" + "comment" : "New Folder" }, "New Folder…" : { "comment" : "New Folder" @@ -658,6 +662,10 @@ "Notifications" : { "comment" : "Notifications" }, + "Notifications are currently disabled. Enable them in Settings." : { + "comment" : "A message displayed when the user has denied notifications for the app.", + "isCommentAutoGenerated" : true + }, "Notifications are not enabled" : { "comment" : "Notifications are not enabled." }, @@ -665,23 +673,20 @@ "comment" : "A title for a view that displays a message that notifications are disabled.", "isCommentAutoGenerated" : true }, - "Notifications need to be enabled in the Settings app." : { - "comment" : "Notifications need to be enabled in the Settings app." - }, "Number of Lines" : { "comment" : "Number of Lines" }, "OK" : { - "comment" : "OK\nOK button" + "comment" : "OK" }, "Open" : { "comment" : "Open" }, "Open Home Page" : { - "comment" : "Command" + "comment" : "Open Home Page" }, "Open in Browser" : { - "comment" : "Command\nOpen in Browser" + "comment" : "Open in Browser" }, "Open In Browser" : { "comment" : "Open In Browser" @@ -693,7 +698,7 @@ "comment" : "Open in Browser in Foreground menu item title" }, "Open Settings" : { - "comment" : "Open Settings button" + "comment" : "Open Settings\nOpen Settings button" }, "Open Settings to configure iCloud and enable iCloud Drive." : { "comment" : "CloudKit account setup recovery suggestion" @@ -720,13 +725,13 @@ "comment" : "Processing - Reader View" }, "Read" : { - "comment" : "Read row label" + "comment" : "Read label" }, "Read Articles Filter" : { "comment" : "Read Articles Filter" }, "Read Content Deleted" : { - "comment" : "Cleanup stat row label" + "comment" : "Read content deleted label" }, "read content record" : { "comment" : "Singular label for read content records" @@ -753,7 +758,7 @@ "comment" : "Remove Account" }, "Rename" : { - "comment" : "Command" + "comment" : "Rename" }, "Rename “%@”" : { "comment" : "Rename feed" @@ -762,10 +767,10 @@ "comment" : "Rename sheet" }, "Return to Previous Scan Results" : { - "comment" : "Return to previous scan results button" + "comment" : "Return to scan results button" }, "Scan completed." : { - "comment" : "Scan status text when completed" + "comment" : "Scan completed text" }, "Scan failed." : { "comment" : "Scan failed text" @@ -777,7 +782,7 @@ "comment" : "Spinner accessibility label" }, "Scanning iCloud storage" : { - "comment" : "Scan status text while fetching" + "comment" : "Scan status text" }, "Search" : { "comment" : "Search" @@ -828,7 +833,7 @@ "comment" : "Self hosted Account" }, "Share" : { - "comment" : "Share\nShare button accessibility description" + "comment" : "Share" }, "Share Stats" : { "comment" : "Share stats menu item" @@ -909,10 +914,10 @@ "comment" : "Star Article" }, "Starred" : { - "comment" : "Starred pseudo-feed title\nStarred row label" + "comment" : "Accessibility announcement\nStarred article for accessibility\nStarred label\nStarred pseudo-feed title" }, "Status Records" : { - "comment" : "Status records section header" + "comment" : "Status records header" }, "Sync content of unread articles" : { @@ -931,7 +936,7 @@ }, "The Old Reader" : { - "comment" : "Account name" + "comment" : "Account name\nThe Old Reader" }, "The theme “%@” already exists. Overwrite it?" : { "comment" : "Overwrite theme" @@ -1036,10 +1041,10 @@ "comment" : "Unread label for accessibility" }, "Unread" : { - "comment" : "Unread row label" + "comment" : "Unread\nUnread label" }, "Unread Content Deleted" : { - "comment" : "Cleanup stat row label" + "comment" : "Unread content deleted label" }, "unread content record" : { "comment" : "Singular label for unread content records" diff --git a/iOS/Inspector/FeedInspectorView.swift b/iOS/Inspector/FeedInspectorView.swift new file mode 100644 index 000000000..be12105ce --- /dev/null +++ b/iOS/Inspector/FeedInspectorView.swift @@ -0,0 +1,182 @@ +// +// FeedInspectorView.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 06/06/2026. +// Copyright © 2026 Ranchero Software. All rights reserved. +// + +import SwiftUI +import UserNotifications +import RSCore +import Account +import Images + +struct FeedInspectorView: View { + + // MARK: Environment + @Environment(\.dismiss) var dismiss + @Environment(\.openURL) var openURL + + // MARK: State + @State private var authorizationStatus: UNAuthorizationStatus? + @State private var navigationTitle: String? + @State private var newArticleNotificationEnabled: Bool = false + + // MARK: Bindings + + // MARK: Constants + + // MARK: Variables + var feed: Feed! + + init(feed: Feed) { + self.feed = feed + } + + // MARK: Views + + var body: some View { + NavigationStack { + Form { + Section { + feedTitle + newArticleNotificationToggle + alwaysUseReaderViewToggle + } header: { + headerView + } footer: { + if authorizationStatus == .denied { + Text("Notifications are currently disabled. Enable them in Settings.") + .onTapGesture { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + } + } + } + + if feed.homePageURL != nil { + Section { + homePageURL + } + } + + Section { + feedURL + } + } + .navigationTitle(navigationTitle ?? "") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + } + .help("Dismiss") + } + } + .onAppear { + navigationTitle = feed.nameForDisplay + updateNotificationSettings() + newArticleNotificationEnabled = feed.newArticleNotificationsEnabled + } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in + updateNotificationSettings() + } + } + } + + private var headerView: some View { + HStack { + Spacer() + if let feedImage = IconImageCache.shared.imageForFeed(feed) { + IconImageView(icon: feedImage, size: .large) + } + Spacer() + } + } + + private var feedTitle: some View { + TextField("Feed Title", + text: Binding(get: { feed.nameForDisplay }, + set: { newTitle in + let newName = newTitle.isEmpty ? (feed.name ?? NSLocalizedString("Untitled", comment: "Feed name")) : newTitle + feed.rename(to: newName) { _ in + self.navigationTitle = newName + } + })) + } + + private var newArticleNotificationToggle: some View { + Toggle(isOn: Binding(get: { feed.newArticleNotificationsEnabled }, set: { newValue in + feed.newArticleNotificationsEnabled = newValue + })) { + Text(feed.notificationDisplayName.capitalized) + } + .tint(Color.accentColor) + .disabled(authorizationStatus != .authorized) + } + + private var alwaysUseReaderViewToggle: some View { + Toggle(isOn: Binding(get: { feed.readerViewAlwaysEnabled }, set: { newValue in + feed.readerViewAlwaysEnabled = newValue + })) { + Text("Always Use Reader View") + } + .tint(Color.accentColor) + .disabled(AppDefaults.shared.isDeveloperBuild) + } + + private var homePageURL: some View { + HStack { + Text(feed.homePageURL ?? "") + Spacer() + Image(systemName: "safari") + .foregroundStyle(Color.accentColor) + } + .onTapGesture { + guard let homePage = feed.homePageURL, + let url = URL(string: homePage) else { + return + } + openURL(url) + } + .contextMenu { + Button { + UIPasteboard.general.string = feed.homePageURL + } label: { + Text("Copy Home Page URL") + Image(systemName: "document.on.document") + } + } + } + + private var feedURL: some View { + HStack { + Text(feed.url) + } + .contextMenu { + Button { + UIPasteboard.general.string = feed.url + } label: { + Text("Copy Feed URL") + Image(systemName: "document.on.document") + } + } + } + + // MARK: Functions + + private func updateNotificationSettings() { + UNUserNotificationCenter.current().getNotificationSettings { (settings) in + let updatedAuthorizationStatus = settings.authorizationStatus + Task { @MainActor in + self.authorizationStatus = updatedAuthorizationStatus + if self.authorizationStatus == .authorized { + UIApplication.shared.registerForRemoteNotifications() + } + } + } + } +} diff --git a/iOS/Inspector/FeedInspectorViewController.swift b/iOS/Inspector/FeedInspectorViewController.swift deleted file mode 100644 index c294861be..000000000 --- a/iOS/Inspector/FeedInspectorViewController.swift +++ /dev/null @@ -1,226 +0,0 @@ -// -// FeedInspectorViewController.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 11/6/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import UIKit -import SafariServices -import UserNotifications -import RSCore -import Account -import Images - -final class FeedInspectorViewController: UITableViewController { - - static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0) - - var feed: Feed! - - @IBOutlet var nameTextField: UITextField! - @IBOutlet var newArticleNotificationsEnabledSwitch: UISwitch! - @IBOutlet var readerViewAlwaysEnabledSwitch: UISwitch! - @IBOutlet var homePageLabel: InteractiveLabel! - @IBOutlet var feedURLLabel: InteractiveLabel! - - private var headerView: InspectorIconHeaderView? - private var iconImage: IconImage? { - return IconImageCache.shared.imageForFeed(feed) - } - - private let homePageIndexPath = IndexPath(row: 0, section: 1) - - private var shouldHideHomePageSection: Bool { - return feed.homePageURL == nil - } - - private var authorizationStatus: UNAuthorizationStatus? - - override func viewDidLoad() { - tableView.register(InspectorIconHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") - - navigationItem.title = feed.nameForDisplay - nameTextField.text = feed.nameForDisplay - - newArticleNotificationsEnabledSwitch.setOn(feed.newArticleNotificationsEnabled, animated: false) - - readerViewAlwaysEnabledSwitch.setOn(feed.readerViewAlwaysEnabled, animated: false) - - homePageLabel.text = feed.homePageURL - feedURLLabel.text = feed.url - - NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(updateNotificationSettings), name: UIApplication.willEnterForegroundNotification, object: nil) - - } - - override func viewDidAppear(_ animated: Bool) { - updateNotificationSettings() - } - - override func viewDidDisappear(_ animated: Bool) { - if nameTextField.text != feed.nameForDisplay { - let nameText = nameTextField.text ?? "" - let newName = nameText.isEmpty ? (feed.name ?? NSLocalizedString("Untitled", comment: "Feed name")) : nameText - feed.rename(to: newName) { _ in } - } - } - - // MARK: Notifications - @objc func feedIconDidBecomeAvailable(_ notification: Notification) { - headerView?.iconView.iconImage = iconImage - } - - @IBAction func newArticleNotificationsEnabledChanged(_ sender: Any) { - guard let authorizationStatus else { - newArticleNotificationsEnabledSwitch.isOn = !newArticleNotificationsEnabledSwitch.isOn - return - } - if authorizationStatus == .denied { - newArticleNotificationsEnabledSwitch.isOn = !newArticleNotificationsEnabledSwitch.isOn - present(notificationUpdateErrorAlert(), animated: true, completion: nil) - } else if authorizationStatus == .authorized { - feed.newArticleNotificationsEnabled = newArticleNotificationsEnabledSwitch.isOn - } else { - UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { granted, _ in - Task { @MainActor in - self.updateNotificationSettings() - if granted { - self.feed.newArticleNotificationsEnabled = self.newArticleNotificationsEnabledSwitch.isOn - UIApplication.shared.registerForRemoteNotifications() - } else { - self.newArticleNotificationsEnabledSwitch.isOn = !self.newArticleNotificationsEnabledSwitch.isOn - } - } - } - } - } - - @IBAction func readerViewAlwaysEnabledChanged(_ sender: Any) { - feed.readerViewAlwaysEnabled = readerViewAlwaysEnabledSwitch.isOn - } - - @IBAction func done(_ sender: Any) { - dismiss(animated: true) - } - - /// Returns a new indexPath, taking into consideration any - /// conditions that may require the tableView to be - /// displayed differently than what is setup in the storyboard. - private func shift(_ indexPath: IndexPath) -> IndexPath { - return IndexPath(row: indexPath.row, section: shift(indexPath.section)) - } - - /// Returns a new section, taking into consideration any - /// conditions that may require the tableView to be - /// displayed differently than what is setup in the storyboard. - private func shift(_ section: Int) -> Int { - if section >= homePageIndexPath.section && shouldHideHomePageSection { - return section + 1 - } - return section - } -} - -// MARK: Table View - -extension FeedInspectorViewController { - - override func numberOfSections(in tableView: UITableView) -> Int { - let numberOfSections = super.numberOfSections(in: tableView) - return shouldHideHomePageSection ? numberOfSections - 1 : numberOfSections - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return super.tableView(tableView, numberOfRowsInSection: shift(section)) - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: shift(section)) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = super.tableView(tableView, cellForRowAt: shift(indexPath)) - if indexPath.section == 0 && indexPath.row == 1 { - guard let label = cell.contentView.subviews.filter({ $0.isKind(of: UILabel.self) })[0] as? UILabel else { - return cell - } - label.numberOfLines = 2 - label.text = feed.notificationDisplayName.capitalized - } - return cell - } - - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - super.tableView(tableView, titleForHeaderInSection: shift(section)) - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - if shift(section) == 0 { - headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as? InspectorIconHeaderView - headerView?.iconView.iconImage = iconImage - return headerView - } else { - return super.tableView(tableView, viewForHeaderInSection: shift(section)) - } - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if shift(indexPath) == homePageIndexPath, - let homePageUrlString = feed.homePageURL, - let homePageUrl = URL(string: homePageUrlString) { - - let safari = SFSafariViewController(url: homePageUrl) - safari.modalPresentationStyle = .pageSheet - present(safari, animated: true) { - tableView.deselectRow(at: indexPath, animated: true) - } - } - } - -} - -// MARK: UITextFieldDelegate - -extension FeedInspectorViewController: UITextFieldDelegate { - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() - return true - } - -} - -// MARK: UNUserNotificationCenter - -extension FeedInspectorViewController { - - @objc func updateNotificationSettings() { - UNUserNotificationCenter.current().getNotificationSettings { (settings) in - let updatedAuthorizationStatus = settings.authorizationStatus - DispatchQueue.main.async { - self.authorizationStatus = updatedAuthorizationStatus - if self.authorizationStatus == .authorized { - UIApplication.shared.registerForRemoteNotifications() - } - } - } - } - - func notificationUpdateErrorAlert() -> UIAlertController { - let alert = UIAlertController(title: NSLocalizedString("Enable Notifications", comment: "Notifications"), - message: NSLocalizedString("Notifications need to be enabled in the Settings app.", comment: "Notifications need to be enabled in the Settings app."), preferredStyle: .alert) - let openSettings = UIAlertAction(title: NSLocalizedString("Open Settings", comment: "Open Settings"), style: .default) { _ in - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: false], completionHandler: nil) - } - let dismiss = UIAlertAction(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil) - alert.addAction(openSettings) - alert.addAction(dismiss) - alert.preferredAction = openSettings - return alert - } - -} diff --git a/iOS/Inspector/Inspector.storyboard b/iOS/Inspector/Inspector.storyboard index 06468ddce..7b94c7b38 100644 --- a/iOS/Inspector/Inspector.storyboard +++ b/iOS/Inspector/Inspector.storyboard @@ -1,8 +1,8 @@ - + - + @@ -16,11 +16,11 @@ - +