Merge pull request #2330 from stuartbreckenridge/ios-multiplat-updates

Multiplatform updates
This commit is contained in:
Maurice Parker
2020-08-14 15:59:42 -05:00
committed by GitHub
26 changed files with 255 additions and 140 deletions

View File

@@ -12,31 +12,29 @@ import RSCore
struct AddFolderView: View {
@Environment(\.presentationMode) private var presentationMode
@ObservedObject private var viewModel = AddFolderModel()
@Binding var isPresented: Bool
@ViewBuilder var body: some View {
var body: some View {
#if os(iOS)
iosForm
.onReceive(viewModel.$shouldDismiss, perform: {
dismiss in
if dismiss == true {
presentationMode
.wrappedValue
.dismiss()
isPresented = false
}
})
#else
macForm
.onReceive(viewModel.$shouldDismiss, perform: { dismiss in
if dismiss == true {
presentationMode.wrappedValue.dismiss()
isPresented = false
}
})
#endif
}
#if os(iOS)
@ViewBuilder var iosForm: some View {
var iosForm: some View {
NavigationView {
Form {
Section {
@@ -50,7 +48,7 @@ struct AddFolderView: View {
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
leading:Button("Cancel", action: {
presentationMode.wrappedValue.dismiss()
isPresented = false
}
)
.help("Cancel Adding Folder"),
@@ -67,7 +65,7 @@ struct AddFolderView: View {
#endif
#if os(macOS)
@ViewBuilder var macForm: some View {
var macForm: some View {
Form {
HStack {
Spacer()
@@ -113,7 +111,7 @@ struct AddFolderView: View {
}
Spacer()
Button("Cancel", action: {
presentationMode.wrappedValue.dismiss()
isPresented = false
})
.help("Cancel Adding Folder")
@@ -125,9 +123,3 @@ struct AddFolderView: View {
}
}
}
struct AddFolderView_Previews: PreviewProvider {
static var previews: some View {
AddFolderView()
}
}

View File

@@ -13,10 +13,10 @@ import RSCore
struct AddWebFeedView: View {
@Environment(\.presentationMode) private var presentationMode
@StateObject private var viewModel = AddWebFeedModel()
@Binding var isPresented: Bool
@ViewBuilder var body: some View {
var body: some View {
#if os(iOS)
iosForm
.onAppear {
@@ -24,7 +24,7 @@ struct AddWebFeedView: View {
}
.onReceive(viewModel.$shouldDismiss, perform: { dismiss in
if dismiss == true {
presentationMode.wrappedValue.dismiss()
isPresented = false
}
})
#else
@@ -37,9 +37,10 @@ struct AddWebFeedView: View {
dismissButton: Alert.Button.cancel({
viewModel.addFeedError = AddWebFeedError.none
}))
}.onReceive(viewModel.$shouldDismiss, perform: { dismiss in
}
.onChange(of: viewModel.shouldDismiss, perform: { dismiss in
if dismiss == true {
presentationMode.wrappedValue.dismiss()
isPresented = false
}
})
#endif
@@ -80,7 +81,7 @@ struct AddWebFeedView: View {
#endif
#if os(iOS)
@ViewBuilder var iosForm: some View {
var iosForm: some View {
NavigationView {
List {
urlTextField
@@ -92,7 +93,7 @@ struct AddWebFeedView: View {
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading:
Button("Cancel", action: {
presentationMode.wrappedValue.dismiss()
isPresented = false
})
.help("Cancel Add Feed")
, trailing:
@@ -188,7 +189,7 @@ struct AddWebFeedView: View {
}
Spacer()
Button("Cancel", action: {
presentationMode.wrappedValue.dismiss()
isPresented = false
})
.help("Cancel Add Feed")
@@ -205,8 +206,4 @@ struct AddWebFeedView: View {
}
struct AddFeedView_Previews: PreviewProvider {
static var previews: some View {
AddWebFeedView()
}
}

View File

@@ -11,7 +11,7 @@ import Articles
struct ArticleContainerView: View {
@ViewBuilder var body: some View {
var body: some View {
ArticleView()
.modifier(ArticleToolbarModifier())
}

View File

@@ -13,7 +13,7 @@ struct IconImageView: View {
@Environment(\.colorScheme) var colorScheme
var iconImage: IconImage
@ViewBuilder var body: some View {
var body: some View {
GeometryReader { proxy in
let newSize = newImageSize(viewSize: proxy.size)

View File

@@ -16,17 +16,13 @@ struct InspectorPlatformModifier: ViewModifier {
@ViewBuilder func body(content: Content) -> some View {
#if os(macOS)
Form {
content
}
content
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 300)
.padding()
#else
NavigationView {
List {
content
}
content
.listStyle(InsetGroupedListStyle())
.navigationBarTitle("Inspector", displayMode: .inline)
.navigationBarItems(

View File

@@ -17,7 +17,6 @@ struct InspectorView: View {
@StateObject private var inspectorModel = InspectorModel()
var sidebarItem: SidebarItem
@ViewBuilder
var body: some View {
switch sidebarItem.representedType {
case .webFeed:
@@ -36,9 +35,9 @@ struct InspectorView: View {
// MARK: WebFeed Inspector
@ViewBuilder
var WebFeedInspectorView: some View {
Group {
Form {
Section(header: webFeedHeader) {
TextField("", text: $inspectorModel.editedName)
}
@@ -85,13 +84,13 @@ struct InspectorView: View {
Text("Copy Home Page URL")
})
}))
}
.sheet(isPresented: $inspectorModel.showHomePage, onDismiss: { inspectorModel.showHomePage = false }) {
#if os(macOS)
EmptyView()
#else
SafariView(url: URL(string: (sidebarItem.feed as! WebFeed).homePageURL!)!)
#endif
.sheet(isPresented: $inspectorModel.showHomePage, onDismiss: { inspectorModel.showHomePage = false }) {
#if os(macOS)
EmptyView()
#else
SafariView(url: URL(string: (sidebarItem.feed as! WebFeed).homePageURL!)!)
#endif
}
}
}
@@ -101,9 +100,9 @@ struct InspectorView: View {
Section(header: Text("Feed URL")) {
VStack {
#if os(macOS)
Spacer() // This shouldn't be necessary, but for some reason macOS doesn't put the space in itself
#endif
// #if os(macOS)
// Spacer() // This shouldn't be necessary, but for some reason macOS doesn't put the space in itself
// #endif
Text(verbatim: (sidebarItem.feed as? WebFeed)?.url ?? "")
.fixedSize(horizontal: false, vertical: true)
.contextMenu(ContextMenu(menuItems: {
@@ -127,11 +126,11 @@ struct InspectorView: View {
Spacer()
Button("Cancel", action: {
presentationMode.wrappedValue.dismiss()
}).keyboardShortcut(.cancelAction)
})
Button("Done", action: {
inspectorModel.shouldUpdate = true
}).keyboardShortcut(.defaultAction)
}.padding(.top)
})
}.padding([.top, .bottom], 20)
#endif
}
.onAppear {
@@ -155,15 +154,14 @@ struct InspectorView: View {
.frame(width: 50, height: 50)
}
Spacer()
}.padding()
}.padding(.top, 20)
}
// MARK: Folder Inspector
@ViewBuilder
var FolderInspectorView: some View {
Group {
Form {
Section(header: folderHeader) {
TextField("", text: $inspectorModel.editedName)
}
@@ -173,13 +171,12 @@ struct InspectorView: View {
Spacer()
Button("Cancel", action: {
presentationMode.wrappedValue.dismiss()
}).keyboardShortcut(.cancelAction)
})
Button("Done", action: {
inspectorModel.shouldUpdate = true
}).keyboardShortcut(.defaultAction)
}.padding(.top)
})
}.padding([.top, .bottom])
#endif
}
.onAppear {
inspectorModel.configure(with: sidebarItem.represented as! Folder)
@@ -203,15 +200,14 @@ struct InspectorView: View {
.frame(width: 50, height: 50)
}
Spacer()
}.padding()
}.padding(.top, 20)
}
// MARK: Account Inspector
@ViewBuilder
var AccountInspectorView: some View {
Group {
Form {
Section(header: accountHeader) {
TextField("", text: $inspectorModel.editedName)
Toggle("Active", isOn: $inspectorModel.accountIsActive)

View File

@@ -73,6 +73,12 @@ struct MainApp: App {
Button("Open in Browser", action: {})
.keyboardShortcut(.rightArrow, modifiers: [.command])
})
CommandGroup(after: .help, addition: {
Button("Release Notes", action: {
NSWorkspace.shared.open(ReleaseNotes().url)
})
.keyboardShortcut("V", modifiers: [.shift, .command])
})
}
// Mac Preferences

View File

@@ -0,0 +1,28 @@
//
// ReleaseNotes.swift
// NetNewsWire
//
// Created by Stuart Breckenridge on 13/8/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
struct ReleaseNotes {
var url: URL {
var gitHub = "https://github.com/Ranchero-Software/NetNewsWire/releases/tag/"
#if os(macOS)
gitHub += "mac-\(String(describing: versionString()))"
return URL(string: gitHub)!
#else
gitHub += "ios-\(String(describing: versionString()))"
return URL(string: gitHub)!
#endif
}
private func versionString() -> String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
}
}

View File

@@ -0,0 +1,21 @@
//
// SceneNavigationModel.swift
// NetNewsWire
//
// Created by Stuart Breckenridge on 13/8/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
class SceneNavigationModel: ObservableObject {
@Published var sheetToShow: SidebarSheets = .none {
didSet {
sheetToShow != .none ? (showSheet = true) : (showSheet = false)
}
}
@Published var showSheet = false
@Published var showShareSheet = false
@Published var showAccountSyncErrorAlert = false
}

View File

@@ -12,13 +12,11 @@ import Account
import AppKit
#endif
struct SceneNavigationView: View {
@StateObject private var sceneModel = SceneModel()
@State private var showSheet = false
@State private var showShareSheet = false
@State private var sheetToShow: SidebarSheets = .none
@State private var showAccountSyncErrorAlert = false // multiple sync errors
@StateObject private var sceneNavigationModel = SceneNavigationModel()
#if os(iOS)
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@@ -30,7 +28,7 @@ struct SceneNavigationView: View {
SidebarContainerView()
.frame(minWidth: 100, idealWidth: 150, maxHeight: .infinity)
#else
SidebarContainerView()
SidebarContainerView()
#endif
#if os(iOS)
@@ -47,41 +45,38 @@ struct SceneNavigationView: View {
.onAppear {
sceneModel.startup()
}
.onChange(of: sheetToShow) { value in
value != .none ? (showSheet = true) : (showSheet = false)
}
.onReceive(sceneModel.$accountSyncErrors) { errors in
if errors.count == 0 {
showAccountSyncErrorAlert = false
sceneNavigationModel.showAccountSyncErrorAlert = false
} else {
if errors.count > 1 {
showAccountSyncErrorAlert = true
sceneNavigationModel.showAccountSyncErrorAlert = true
} else {
sheetToShow = .fixCredentials
sceneNavigationModel.sheetToShow = .fixCredentials
}
}
}
.sheet(isPresented: $showSheet,
.sheet(isPresented: $sceneNavigationModel.showSheet,
onDismiss: {
sheetToShow = .none
sceneNavigationModel.sheetToShow = .none
sceneModel.accountSyncErrors = []
}) {
if sheetToShow == .web {
AddWebFeedView()
if sceneNavigationModel.sheetToShow == .web {
AddWebFeedView(isPresented: $sceneNavigationModel.showSheet)
}
if sheetToShow == .folder {
AddFolderView()
if sceneNavigationModel.sheetToShow == .folder {
AddFolderView(isPresented: $sceneNavigationModel.showSheet)
}
#if os(iOS)
if sheetToShow == .settings {
if sceneNavigationModel.sheetToShow == .settings {
SettingsView()
}
#endif
if sheetToShow == .fixCredentials {
if sceneNavigationModel.sheetToShow == .fixCredentials {
FixAccountCredentialView(accountSyncError: sceneModel.accountSyncErrors[0])
}
}
.alert(isPresented: $showAccountSyncErrorAlert, content: {
.alert(isPresented: $sceneNavigationModel.showAccountSyncErrorAlert, content: {
#if os(macOS)
return Alert(title: Text("Account Sync Error"),
message: Text("The following accounts failed to sync: ") + Text(sceneModel.accountSyncErrors.map({ $0.account.nameForDisplay }).joined(separator: ", ")) + Text(". You can update credentials in Preferences"),
@@ -91,7 +86,7 @@ struct SceneNavigationView: View {
message: Text("The following accounts failed to sync: ") + Text(sceneModel.accountSyncErrors.map({ $0.account.nameForDisplay }).joined(separator: ", ")) + Text(". You can update credentials in Settings"),
primaryButton: .default(Text("Show Settings"), action: {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
sheetToShow = .settings
sceneNavigationModel.sheetToShow = .settings
})
}),
@@ -112,10 +107,10 @@ struct SceneNavigationView: View {
}
ToolbarItem() {
Menu {
Button("Add Web Feed", action: { sheetToShow = .web })
Button("Add Web Feed", action: { sceneNavigationModel.sheetToShow = .web })
Button("Add Reddit Feed", action: { })
Button("Add Twitter Feed", action: { })
Button("Add Folder", action: { sheetToShow = .folder})
Button("Add Folder", action: { sceneNavigationModel.sheetToShow = .folder})
} label : {
AppAssets.addMenuImage
}
@@ -197,12 +192,12 @@ struct SceneNavigationView: View {
}
ToolbarItem {
ZStack {
if showShareSheet {
SharingServiceView(articles: sceneModel.selectedArticles, showing: $showShareSheet)
if sceneNavigationModel.showShareSheet {
SharingServiceView(articles: sceneModel.selectedArticles, showing: $sceneNavigationModel.showShareSheet)
.frame(width: 20, height: 20)
}
Button {
showShareSheet = true
sceneNavigationModel.showShareSheet = true
} label: {
AppAssets.shareImage
}

View File

@@ -15,7 +15,7 @@ struct SidebarContainerView: View {
@State var sidebarItems = [SidebarItem]()
@ViewBuilder var body: some View {
var body: some View {
SidebarView(sidebarItems: $sidebarItems)
.modifier(SidebarToolbarModifier())
.modifier(SidebarListStyleModifier())

View File

@@ -19,7 +19,7 @@ struct SidebarContextMenu: View {
var sidebarItem: SidebarItem
@ViewBuilder var body: some View {
var body: some View {
// MARK: Account Context Menu
if sidebarItem.representedType == .account {
Button {

View File

@@ -81,10 +81,10 @@ struct SidebarToolbarModifier: ViewModifier {
}
.sheet(isPresented: $viewModel.showSheet, onDismiss: { viewModel.sheetToShow = .none }) {
if viewModel.sheetToShow == .web {
AddWebFeedView()
AddWebFeedView(isPresented: $viewModel.showSheet)
}
if viewModel.sheetToShow == .folder {
AddFolderView()
AddFolderView(isPresented: $viewModel.showSheet)
}
if viewModel.sheetToShow == .settings {
SettingsView()

View File

@@ -26,7 +26,7 @@ struct SidebarView: View {
@State var pulling: Bool = false
@State var refreshing: Bool = false
@ViewBuilder var body: some View {
var body: some View {
#if os(macOS)
VStack {
HStack {
@@ -182,7 +182,7 @@ struct SidebarView: View {
@EnvironmentObject private var sidebarModel: SidebarModel
var sidebarItem: SidebarItem
@ViewBuilder var body: some View {
var body: some View {
#if os(macOS)
SidebarItemView(sidebarItem: sidebarItem)
.tag(sidebarItem.feed!.feedID!)

View File

@@ -17,7 +17,7 @@ struct TimelineContainerView: View {
@State private var timelineItems = TimelineItems()
@State private var isReadFiltered: Bool? = nil
@ViewBuilder var body: some View {
var body: some View {
TimelineView(timelineItems: $timelineItems, isReadFiltered: $isReadFiltered)
.modifier(TimelineToolbarModifier())
.environmentObject(sceneModel.timelineModel)

View File

@@ -13,7 +13,7 @@ struct TimelineContextMenu: View {
@EnvironmentObject private var timelineModel: TimelineModel
var timelineItem: TimelineItem
@ViewBuilder var body: some View {
var body: some View {
if timelineModel.canMarkIndicatedArticlesAsRead(timelineItem) {
Button {

View File

@@ -13,7 +13,7 @@ struct TimelineItemStatusView: View {
var selected: Bool
var status: TimelineItemStatus
@ViewBuilder var statusView: some View {
var statusView: some View {
ZStack {
Spacer().frame(width: 12)
switch status {

View File

@@ -12,6 +12,10 @@ struct TimelineToolbarModifier: ViewModifier {
@EnvironmentObject private var sceneModel: SceneModel
@EnvironmentObject private var timelineModel: TimelineModel
@Environment(\.presentationMode) var presentationMode
#if os(iOS)
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
#endif
@State private var isReadFiltered: Bool? = nil
func body(content: Content) -> some View {
@@ -40,6 +44,11 @@ struct TimelineToolbarModifier: ViewModifier {
ToolbarItem(placement: .bottomBar) {
Button {
sceneModel.markAllAsRead()
#if os(iOS)
if horizontalSizeClass == .compact {
presentationMode.wrappedValue.dismiss()
}
#endif
} label: {
AppAssets.markAllAsReadImage
}

View File

@@ -17,7 +17,7 @@ struct TimelineView: View {
@State private var timelineItemFrames = [String: CGRect]()
@ViewBuilder var body: some View {
var body: some View {
GeometryReader { geometryReaderProxy in
#if os(macOS)
VStack {

View File

@@ -12,7 +12,7 @@ import Account
class SettingsModel: ObservableObject {
enum HelpSites {
case netNewsWireHelp, netNewsWire, supportNetNewsWire, github, bugTracker, technotes, netNewsWireSlack, none
case netNewsWireHelp, netNewsWire, supportNetNewsWire, github, bugTracker, technotes, netNewsWireSlack, releaseNotes, none
var url: URL? {
switch self {
@@ -30,6 +30,8 @@ class SettingsModel: ObservableObject {
return URL(string: "https://github.com/brentsimmons/NetNewsWire/tree/main/Technotes")!
case .netNewsWireSlack:
return URL(string: "https://ranchero.com/netnewswire/slack")!
case .releaseNotes:
return ReleaseNotes().url
case .none:
return nil
}

View File

@@ -203,6 +203,9 @@ struct SettingsView: View {
Button("NetNewsWire Slack", action: {
viewModel.selectedWebsite = .netNewsWireSlack
}).foregroundColor(.primary)
Button("Release Notes", action: {
viewModel.selectedWebsite = .releaseNotes
}).foregroundColor(.primary)
NavigationLink(
destination: SettingsAboutView(),
label: {
@@ -213,9 +216,8 @@ struct SettingsView: View {
}
private func appVersion() -> String {
let dict = NSDictionary(contentsOf: Bundle.main.url(forResource: "Info", withExtension: "plist")!)
let version = dict?.object(forKey: "CFBundleShortVersionString") as? String ?? ""
let build = dict?.object(forKey: "CFBundleVersion") as? String ?? ""
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? ""
return "NetNewsWire \(version) (Build \(build))"
}

View File

@@ -31,6 +31,8 @@ struct AccountsPreferencesView: View {
switch viewModel.sheetToShow {
case .add:
AddAccountView(preferencesModel: viewModel)
.frame(width: 300, height: 200)
.padding()
case .credentials:
EditAccountCredentialsView(viewModel: viewModel)
case .none:

View File

@@ -15,12 +15,12 @@ struct AddAccountPickerRow: View {
var body: some View {
HStack {
if let img = AppAssets.image(for: accountType) {
Image(rsImage: img)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 15, height: 15)
}
// if let img = AppAssets.image(for: accountType) {
// Image(rsImage: img)
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: 15, height: 15)
// }
switch accountType {
case .onMyMac:

View File

@@ -16,36 +16,35 @@ struct AddAccountView: View {
@StateObject private var viewModel = AddAccountModel()
var body: some View {
VStack(alignment: .leading) {
Form {
Text("Add an Account").font(.headline)
Form {
Picker("Account Type",
selection: $viewModel.selectedAddAccount,
content: {
ForEach(0..<viewModel.addableAccountTypes.count, content: { i in
AddAccountPickerRow(accountType: viewModel.addableAccountTypes[i]).tag(viewModel.addableAccountTypes[i])
})
})
switch viewModel.selectedAddAccount {
case .onMyMac:
addLocalAccountView
case .cloudKit:
iCloudAccountView
case .feedbin:
userNameAndPasswordView
case .feedWrangler:
userNameAndPasswordView
case .freshRSS:
userNamePasswordAndAPIUrlView
case .feedly:
oAuthView
case .newsBlur:
userNameAndPasswordView
}
Picker("Account Type",
selection: $viewModel.selectedAddAccount,
content: {
ForEach(0..<viewModel.addableAccountTypes.count, content: { i in
AddAccountPickerRow(accountType: viewModel.addableAccountTypes[i]).tag(viewModel.addableAccountTypes[i])
})
}).pickerStyle(MenuPickerStyle())
switch viewModel.selectedAddAccount {
case .onMyMac:
addLocalAccountView
case .cloudKit:
iCloudAccountView
case .feedbin:
userNameAndPasswordView
case .feedWrangler:
userNameAndPasswordView
case .freshRSS:
userNamePasswordAndAPIUrlView
case .feedly:
oAuthView
case .newsBlur:
userNameAndPasswordView
}
Spacer()
HStack {
if viewModel.accountIsAuthenticating {
@@ -82,8 +81,8 @@ struct AddAccountView: View {
}
}
}
.frame(width: 300, height: 200, alignment: .top)
.padding()
.onChange(of: viewModel.selectedAddAccount) { _ in
viewModel.resetUserEntries()
}

View File

@@ -59,7 +59,7 @@ struct LayoutPreferencesView: View {
}
@ViewBuilder
var timelineRowPreview: some View {
HStack(alignment: .top) {
Image(systemName: "circle.fill")