Merge pull request #2330 from stuartbreckenridge/ios-multiplat-updates
Multiplatform updates
This commit is contained in:
commit
9218b4d95c
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import Articles
|
|||
|
||||
struct ArticleContainerView: View {
|
||||
|
||||
@ViewBuilder var body: some View {
|
||||
var body: some View {
|
||||
ArticleView()
|
||||
.modifier(ArticleToolbarModifier())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ?? ""
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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!)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))"
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ struct LayoutPreferencesView: View {
|
|||
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
var timelineRowPreview: some View {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "circle.fill")
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1704053424E5985A00A00787 /* SceneNavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1704053324E5985A00A00787 /* SceneNavigationModel.swift */; };
|
||||
1704053524E5985A00A00787 /* SceneNavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1704053324E5985A00A00787 /* SceneNavigationModel.swift */; };
|
||||
1717535624BADF33004498C6 /* GeneralPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1717535524BADF33004498C6 /* GeneralPreferencesModel.swift */; };
|
||||
171BCB8C24CB08A3006E22D9 /* FixAccountCredentialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */; };
|
||||
171BCB8D24CB08A3006E22D9 /* FixAccountCredentialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */; };
|
||||
|
@ -44,6 +46,18 @@
|
|||
1799E6CE24C320D600511E91 /* InspectorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1799E6CC24C320D600511E91 /* InspectorModel.swift */; };
|
||||
179DB1DFBCF9177104B12E0F /* AccountsNewsBlurWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */; };
|
||||
179DB3CE822BFCC2D774D9F4 /* AccountsNewsBlurWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */; };
|
||||
17A1597C24E3DEDD005DA32A /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1597B24E3DEDD005DA32A /* RSCore */; };
|
||||
17A1597D24E3DEDD005DA32A /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1597B24E3DEDD005DA32A /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
17A1597F24E3DEDD005DA32A /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1597E24E3DEDD005DA32A /* RSTree */; };
|
||||
17A1598024E3DEDD005DA32A /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1597E24E3DEDD005DA32A /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
17A1598224E3DEDD005DA32A /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598124E3DEDD005DA32A /* RSWeb */; };
|
||||
17A1598324E3DEDD005DA32A /* RSWeb in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598124E3DEDD005DA32A /* RSWeb */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
17A1598524E3DEDD005DA32A /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598424E3DEDD005DA32A /* RSDatabase */; };
|
||||
17A1598624E3DEDD005DA32A /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598424E3DEDD005DA32A /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
17A1598824E3DEDD005DA32A /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598724E3DEDD005DA32A /* RSParser */; };
|
||||
17A1598924E3DEDD005DA32A /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598724E3DEDD005DA32A /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
17AC0ABB24E4B65E004C1231 /* ReleaseNotes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AC0ABA24E4B65E004C1231 /* ReleaseNotes.swift */; };
|
||||
17AC0ABC24E4B65E004C1231 /* ReleaseNotes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AC0ABA24E4B65E004C1231 /* ReleaseNotes.swift */; };
|
||||
17D232A824AFF10A0005F075 /* AddWebFeedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D232A724AFF10A0005F075 /* AddWebFeedModel.swift */; };
|
||||
17D232A924AFF10A0005F075 /* AddWebFeedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D232A724AFF10A0005F075 /* AddWebFeedModel.swift */; };
|
||||
17D5F17124B0BC6700375168 /* SidebarToolbarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */; };
|
||||
|
@ -1323,6 +1337,11 @@
|
|||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
17A1598924E3DEDD005DA32A /* RSParser in Embed Frameworks */,
|
||||
17A1598624E3DEDD005DA32A /* RSDatabase in Embed Frameworks */,
|
||||
17A1598324E3DEDD005DA32A /* RSWeb in Embed Frameworks */,
|
||||
17A1597D24E3DEDD005DA32A /* RSCore in Embed Frameworks */,
|
||||
17A1598024E3DEDD005DA32A /* RSTree in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1428,6 +1447,7 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1704053324E5985A00A00787 /* SceneNavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneNavigationModel.swift; sourceTree = "<group>"; };
|
||||
1717535524BADF33004498C6 /* GeneralPreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPreferencesModel.swift; sourceTree = "<group>"; };
|
||||
171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixAccountCredentialView.swift; sourceTree = "<group>"; };
|
||||
172199C824AB228900A31D04 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1454,6 +1474,7 @@
|
|||
1799E6A824C2F93F00511E91 /* InspectorPlatformModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorPlatformModifier.swift; sourceTree = "<group>"; };
|
||||
1799E6CC24C320D600511E91 /* InspectorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorModel.swift; sourceTree = "<group>"; };
|
||||
179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsNewsBlurWindowController.swift; sourceTree = "<group>"; };
|
||||
17AC0ABA24E4B65E004C1231 /* ReleaseNotes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseNotes.swift; sourceTree = "<group>"; };
|
||||
17B223DB24AC24D2001E4592 /* TimelineLayoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLayoutView.swift; sourceTree = "<group>"; };
|
||||
17D232A724AFF10A0005F075 /* AddWebFeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedModel.swift; sourceTree = "<group>"; };
|
||||
17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarToolbarModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -2043,7 +2064,12 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
17A1598224E3DEDD005DA32A /* RSWeb in Frameworks */,
|
||||
17A1597F24E3DEDD005DA32A /* RSTree in Frameworks */,
|
||||
17A1598824E3DEDD005DA32A /* RSParser in Frameworks */,
|
||||
17A1597C24E3DEDD005DA32A /* RSCore in Frameworks */,
|
||||
516B695D24D2F28E00B5702F /* Account in Frameworks */,
|
||||
17A1598524E3DEDD005DA32A /* RSDatabase in Frameworks */,
|
||||
51E4989724A8065700B667CB /* CloudKit.framework in Frameworks */,
|
||||
51E4989924A8067000B667CB /* WebKit.framework in Frameworks */,
|
||||
);
|
||||
|
@ -2256,6 +2282,14 @@
|
|||
path = Add;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
17AC0ACB24E4B66A004C1231 /* Release Notes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
17AC0ABA24E4B65E004C1231 /* ReleaseNotes.swift */,
|
||||
);
|
||||
path = "Release Notes";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
510289CE2451BA1E00426DDF /* Twitter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2660,6 +2694,7 @@
|
|||
FF64D0C424AF53EE0084080A /* RefreshProgressModel.swift */,
|
||||
51E499D724A912C200B667CB /* SceneModel.swift */,
|
||||
51E49A0224A91FF600B667CB /* SceneNavigationView.swift */,
|
||||
1704053324E5985A00A00787 /* SceneNavigationModel.swift */,
|
||||
51C0513824A77DF800194D5E /* Assets.xcassets */,
|
||||
17930ED224AF10CD00A9BA52 /* Add */,
|
||||
51A576B924AE617B00078888 /* Article */,
|
||||
|
@ -2671,6 +2706,7 @@
|
|||
514E6C0424AD2B0400AC6F6E /* SwiftUI Extensions */,
|
||||
51919FCB24AB855000541E64 /* Timeline */,
|
||||
171BCBB124CBD569006E22D9 /* Account Management */,
|
||||
17AC0ACB24E4B66A004C1231 /* Release Notes */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3657,6 +3693,11 @@
|
|||
name = "Multiplatform iOS";
|
||||
packageProductDependencies = (
|
||||
516B695C24D2F28E00B5702F /* Account */,
|
||||
17A1597B24E3DEDD005DA32A /* RSCore */,
|
||||
17A1597E24E3DEDD005DA32A /* RSTree */,
|
||||
17A1598124E3DEDD005DA32A /* RSWeb */,
|
||||
17A1598424E3DEDD005DA32A /* RSDatabase */,
|
||||
17A1598724E3DEDD005DA32A /* RSParser */,
|
||||
);
|
||||
productName = iOS;
|
||||
productReference = 51C0513D24A77DF800194D5E /* NetNewsWire.app */;
|
||||
|
@ -4493,6 +4534,7 @@
|
|||
51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */,
|
||||
51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */,
|
||||
51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */,
|
||||
1704053424E5985A00A00787 /* SceneNavigationModel.swift in Sources */,
|
||||
514E6C0924AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */,
|
||||
6594CA3B24AF6F2A005C7D7C /* OPMLExporter.swift in Sources */,
|
||||
FA80C13E24B072AA00974098 /* AddFolderModel.swift in Sources */,
|
||||
|
@ -4533,6 +4575,7 @@
|
|||
51E4991724A8090400B667CB /* ArticleUtilities.swift in Sources */,
|
||||
51E4991B24A8091000B667CB /* IconImage.swift in Sources */,
|
||||
51E4995424A8734D00B667CB /* ExtensionPointIdentifer.swift in Sources */,
|
||||
17AC0ABB24E4B65E004C1231 /* ReleaseNotes.swift in Sources */,
|
||||
51E4996924A8760C00B667CB /* ArticleStylesManager.swift in Sources */,
|
||||
5177471E24B387E100EB0F74 /* ImageTransition.swift in Sources */,
|
||||
51E498F324A8085D00B667CB /* PseudoFeed.swift in Sources */,
|
||||
|
@ -4716,6 +4759,7 @@
|
|||
514E6C0324AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */,
|
||||
51B54A6524B549B20014348B /* WrapperScriptMessageHandler.swift in Sources */,
|
||||
51E4996D24A8762D00B667CB /* ArticleExtractor.swift in Sources */,
|
||||
1704053524E5985A00A00787 /* SceneNavigationModel.swift in Sources */,
|
||||
51E4994024A8713B00B667CB /* AccountRefreshTimer.swift in Sources */,
|
||||
51E49A0424A91FF600B667CB /* SceneNavigationView.swift in Sources */,
|
||||
51E498CC24A8085D00B667CB /* SearchFeedDelegate.swift in Sources */,
|
||||
|
@ -4748,6 +4792,7 @@
|
|||
51E4990724A808C300B667CB /* AuthorAvatarDownloader.swift in Sources */,
|
||||
51E4997424A8784400B667CB /* DefaultFeedsImporter.swift in Sources */,
|
||||
51919FF524AB869C00541E64 /* TimelineItem.swift in Sources */,
|
||||
17AC0ABC24E4B65E004C1231 /* ReleaseNotes.swift in Sources */,
|
||||
51E4992024A8095000B667CB /* RSImage-Extensions.swift in Sources */,
|
||||
51E499FE24A9137600B667CB /* SidebarModel.swift in Sources */,
|
||||
51E498FE24A808BA00B667CB /* FaviconDownloader.swift in Sources */,
|
||||
|
@ -5887,6 +5932,31 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
17A1597B24E3DEDD005DA32A /* RSCore */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */;
|
||||
productName = RSCore;
|
||||
};
|
||||
17A1597E24E3DEDD005DA32A /* RSTree */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
|
||||
productName = RSTree;
|
||||
};
|
||||
17A1598124E3DEDD005DA32A /* RSWeb */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 51383A3024D1F90E0027E272 /* XCRemoteSwiftPackageReference "RSWeb" */;
|
||||
productName = RSWeb;
|
||||
};
|
||||
17A1598424E3DEDD005DA32A /* RSDatabase */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */;
|
||||
productName = RSDatabase;
|
||||
};
|
||||
17A1598724E3DEDD005DA32A /* RSParser */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */;
|
||||
productName = RSParser;
|
||||
};
|
||||
5102AE6824D17F7C0050839C /* RSCore */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */;
|
||||
|
|
Loading…
Reference in New Issue