mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'swiftui' of https://github.com/Ranchero-Software/NetNewsWire into swiftui
This commit is contained in:
263
Multiplatform/Shared/Add/AddWebFeedView.swift
Normal file
263
Multiplatform/Shared/Add/AddWebFeedView.swift
Normal file
@@ -0,0 +1,263 @@
|
||||
//
|
||||
// AddWebFeedView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 3/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
fileprivate enum AddWebFeedError: LocalizedError {
|
||||
|
||||
case none, alreadySubscribed, initialDownload, noFeeds
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .alreadySubscribed:
|
||||
return NSLocalizedString("Can’t add this feed because you’ve already subscribed to it.", comment: "Feed finder")
|
||||
case .initialDownload:
|
||||
return NSLocalizedString("Can’t add this feed because of a download error.", comment: "Feed finder")
|
||||
case .noFeeds:
|
||||
return NSLocalizedString("Can’t add a feed because no feed was found.", comment: "Feed finder")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate class AddWebFeedViewModel: ObservableObject {
|
||||
|
||||
@Published var providedURL: String = ""
|
||||
@Published var providedName: String = ""
|
||||
@Published var selectedFolderIndex: Int = 0
|
||||
@Published var addFeedError: AddWebFeedError? {
|
||||
didSet {
|
||||
addFeedError != .none ? (showError = true) : (showError = false)
|
||||
}
|
||||
}
|
||||
@Published var showError: Bool = false
|
||||
@Published var containers: [Container] = []
|
||||
@Published var showProgressIndicator: Bool = false
|
||||
|
||||
init() {
|
||||
for account in AccountManager.shared.sortedActiveAccounts {
|
||||
containers.append(account)
|
||||
if let sortedFolders = account.sortedFolders {
|
||||
containers.append(contentsOf: sortedFolders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct AddWebFeedView: View {
|
||||
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@ObservedObject private var viewModel = AddWebFeedViewModel()
|
||||
|
||||
@ViewBuilder var body: some View {
|
||||
#if os(iOS)
|
||||
iosForm
|
||||
#else
|
||||
macForm
|
||||
.onAppear {
|
||||
pasteUrlFromPasteboard()
|
||||
}.alert(isPresented: $viewModel.showError) {
|
||||
Alert(title: Text("Oops"), message: Text(viewModel.addFeedError!.localizedDescription), dismissButton: Alert.Button.cancel({
|
||||
viewModel.addFeedError = .none
|
||||
}))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
var macForm: some View {
|
||||
Form {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: "globe").foregroundColor(.accentColor).font(.title)
|
||||
Text("Add a Web Feed")
|
||||
.font(.title)
|
||||
Spacer()
|
||||
}
|
||||
urlTextField
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.help("The URL of the feed you want to add.")
|
||||
providedNameTextField
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.help("The name of the feed. (Optional.)")
|
||||
folderPicker
|
||||
.help("Pick the folder you want to add the feed to.")
|
||||
buttonStack
|
||||
}
|
||||
.padding()
|
||||
.frame(minWidth: 450)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
@ViewBuilder var iosForm: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
urlTextField
|
||||
providedNameTextField
|
||||
folderPicker
|
||||
}
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationTitle("Add Web Feed")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(leading:
|
||||
Button("Cancel", action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
})
|
||||
.help("Cancel Add Feed")
|
||||
, trailing:
|
||||
Button("Add", action: {
|
||||
addWebFeed()
|
||||
})
|
||||
.disabled(!viewModel.providedURL.isValidURL)
|
||||
.help("Add Feed")
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var urlTextField: some View {
|
||||
HStack {
|
||||
Text("Feed:")
|
||||
TextField("URL", text: $viewModel.providedURL)
|
||||
}
|
||||
}
|
||||
|
||||
var providedNameTextField: some View {
|
||||
HStack(alignment: .lastTextBaseline) {
|
||||
Text("Name:")
|
||||
TextField("Optional", text: $viewModel.providedName)
|
||||
}
|
||||
}
|
||||
|
||||
var folderPicker: some View {
|
||||
Picker("Folder:", selection: $viewModel.selectedFolderIndex, content: {
|
||||
ForEach(0..<viewModel.containers.count, id: \.self, content: { index in
|
||||
if let containerName = (viewModel.containers[index] as? DisplayNameProvider)?.nameForDisplay {
|
||||
if viewModel.containers[index] is Folder {
|
||||
Text("\(viewModel.containers[index].account?.nameForDisplay ?? "") / \(containerName)").tag(index)
|
||||
} else {
|
||||
Text(containerName).tag(index)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var buttonStack: some View {
|
||||
HStack {
|
||||
if viewModel.showProgressIndicator == true {
|
||||
ProgressView()
|
||||
.frame(width: 25, height: 25)
|
||||
.help("Adding Feed")
|
||||
}
|
||||
Spacer()
|
||||
Button("Cancel", action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
})
|
||||
.help("Cancel Add Feed")
|
||||
|
||||
Button("Add", action: {
|
||||
addWebFeed()
|
||||
})
|
||||
.disabled(!viewModel.providedURL.isValidURL)
|
||||
.help("Add Feed")
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
func pasteUrlFromPasteboard() {
|
||||
guard let stringFromPasteboard = urlStringFromPasteboard, stringFromPasteboard.isValidURL else {
|
||||
return
|
||||
}
|
||||
viewModel.providedURL = stringFromPasteboard
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
private extension AddWebFeedView {
|
||||
|
||||
#if os(macOS)
|
||||
var urlStringFromPasteboard: String? {
|
||||
if let urlString = NSPasteboard.urlString(from: NSPasteboard.general) {
|
||||
return urlString.normalizedURL
|
||||
}
|
||||
return nil
|
||||
}
|
||||
#else
|
||||
var urlStringFromPasteboard: String? {
|
||||
if let urlString = UIPasteboard.general.url?.absoluteString {
|
||||
return urlString.normalizedURL
|
||||
}
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
struct AccountAndFolderSpecifier {
|
||||
let account: Account
|
||||
let folder: Folder?
|
||||
}
|
||||
|
||||
func accountAndFolderFromContainer(_ container: Container) -> AccountAndFolderSpecifier? {
|
||||
if let account = container as? Account {
|
||||
return AccountAndFolderSpecifier(account: account, folder: nil)
|
||||
}
|
||||
if let folder = container as? Folder, let account = folder.account {
|
||||
return AccountAndFolderSpecifier(account: account, folder: folder)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addWebFeed() {
|
||||
if let account = accountAndFolderFromContainer(viewModel.containers[viewModel.selectedFolderIndex])?.account {
|
||||
|
||||
viewModel.showProgressIndicator = true
|
||||
|
||||
let container = viewModel.containers[viewModel.selectedFolderIndex]
|
||||
|
||||
if account.hasWebFeed(withURL: viewModel.providedURL) {
|
||||
viewModel.addFeedError = .alreadySubscribed
|
||||
viewModel.showProgressIndicator = false
|
||||
return
|
||||
}
|
||||
|
||||
account.createWebFeed(url: viewModel.providedURL, name: viewModel.providedName, container: container, completion: { result in
|
||||
viewModel.showProgressIndicator = false
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.webFeed: feed])
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case AccountError.createErrorAlreadySubscribed:
|
||||
self.viewModel.addFeedError = .alreadySubscribed
|
||||
return
|
||||
case AccountError.createErrorNotFound:
|
||||
self.viewModel.addFeedError = .noFeeds
|
||||
return
|
||||
default:
|
||||
print("Error")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct AddFeedView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddWebFeedView()
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ struct MainApp: App {
|
||||
|
||||
@StateObject private var sceneModel = SceneModel()
|
||||
@StateObject private var defaults = AppDefaults.shared
|
||||
@State private var showSheet = false
|
||||
|
||||
@SceneBuilder var body: some Scene {
|
||||
#if os(macOS)
|
||||
@@ -28,12 +29,15 @@ struct MainApp: App {
|
||||
.frame(minWidth: 600, idealWidth: 1000, maxWidth: .infinity, minHeight: 600, idealHeight: 700, maxHeight: .infinity)
|
||||
.environmentObject(sceneModel)
|
||||
.environmentObject(defaults)
|
||||
.sheet(isPresented: $showSheet, onDismiss: { showSheet = false }) {
|
||||
AddWebFeedView()
|
||||
}
|
||||
.toolbar {
|
||||
|
||||
ToolbarItem {
|
||||
Button(action: {}, label: {
|
||||
Button(action: { showSheet = true }, label: {
|
||||
Image(systemName: "plus").foregroundColor(.secondary)
|
||||
}).help("New Feed")
|
||||
}).help("Add Feed")
|
||||
}
|
||||
|
||||
ToolbarItem {
|
||||
|
||||
@@ -8,11 +8,28 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SidebarToolbar: View {
|
||||
fileprivate enum ToolbarSheets {
|
||||
case none, web, twitter, reddit, folder, settings
|
||||
}
|
||||
|
||||
fileprivate class SidebarToolbarViewModel: ObservableObject {
|
||||
|
||||
@Published var showSheet: Bool = false
|
||||
@Published var sheetToShow: ToolbarSheets = .none {
|
||||
didSet {
|
||||
sheetToShow != .none ? (showSheet = true) : (showSheet = false)
|
||||
}
|
||||
}
|
||||
@Published var showActionSheet: Bool = false
|
||||
@Published var showAddSheet: Bool = false
|
||||
}
|
||||
|
||||
|
||||
struct SidebarToolbar: View {
|
||||
|
||||
@EnvironmentObject private var appSettings: AppDefaults
|
||||
@State private var showSettings: Bool = false
|
||||
@State private var showAddSheet: Bool = false
|
||||
@StateObject private var viewModel = SidebarToolbarViewModel()
|
||||
|
||||
|
||||
var addActionSheetButtons = [
|
||||
Button(action: {}, label: { Text("Add Feed") })
|
||||
@@ -23,29 +40,33 @@ struct SidebarToolbar: View {
|
||||
Divider()
|
||||
HStack(alignment: .center) {
|
||||
Button(action: {
|
||||
showSettings = true
|
||||
viewModel.sheetToShow = .settings
|
||||
}, label: {
|
||||
Image(systemName: "gear")
|
||||
.font(.title3)
|
||||
.foregroundColor(.accentColor)
|
||||
}).help("Settings")
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("Last updated")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showAddSheet = true
|
||||
viewModel.showActionSheet = true
|
||||
}, label: {
|
||||
Image(systemName: "plus")
|
||||
.font(.title3)
|
||||
.foregroundColor(.accentColor)
|
||||
})
|
||||
.help("Add")
|
||||
.actionSheet(isPresented: $showAddSheet) {
|
||||
.actionSheet(isPresented: $viewModel.showActionSheet) {
|
||||
ActionSheet(title: Text("Add"), buttons: [
|
||||
.cancel(),
|
||||
.default(Text("Add Web Feed")),
|
||||
.default(Text("Add Web Feed"), action: { viewModel.sheetToShow = .web }),
|
||||
.default(Text("Add Twitter Feed")),
|
||||
.default(Text("Add Reddit Feed")),
|
||||
.default(Text("Add Folder"))
|
||||
@@ -57,8 +78,13 @@ struct SidebarToolbar: View {
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.background(VisualEffectBlur(blurStyle: .systemChromeMaterial).edgesIgnoringSafeArea(.bottom))
|
||||
.sheet(isPresented: $showSettings, onDismiss: { showSettings = false }) {
|
||||
SettingsView().modifier(PreferredColorSchemeModifier(preferredColorScheme: appSettings.userInterfaceColorPalette))
|
||||
.sheet(isPresented: $viewModel.showSheet, onDismiss: { viewModel.sheetToShow = .none }) {
|
||||
if viewModel.sheetToShow == .web {
|
||||
AddWebFeedView()
|
||||
}
|
||||
if viewModel.sheetToShow == .settings {
|
||||
SettingsView().modifier(PreferredColorSchemeModifier(preferredColorScheme: appSettings.userInterfaceColorPalette))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
19
Multiplatform/Shared/String+URLChecker.swift
Normal file
19
Multiplatform/Shared/String+URLChecker.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// String+URLChecker.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 3/7/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
/// Reference: [StackOverflow](https://stackoverflow.com/questions/161738/what-is-the-best-regular-expression-to-check-if-a-string-is-a-valid-url)
|
||||
var isValidURL: Bool {
|
||||
let regEx = "^(http|https|feed)\\://([a-zA-Z0-9\\.\\-]+(\\:[a-zA-Z0-9\\.&%\\$\\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|localhost|([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(\\:[0-9]+)*(/($|[a-zA-Z0-9\\.\\,\\?\\'\\\\\\+&%\\$#\\=~_\\-]+))*$"
|
||||
let predicate = NSPredicate(format:"SELF MATCHES %@", argumentArray:[regEx])
|
||||
return predicate.evaluate(with: self)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user