mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Article themes moved to SwiftUI
This commit is contained in:
236
iOS/Settings/General/SettingsRows.swift
Normal file
236
iOS/Settings/General/SettingsRows.swift
Normal file
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// SettingsRows.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
|
||||
// MARK: - Rows
|
||||
|
||||
struct SettingsViewRows {
|
||||
|
||||
/// This row, when tapped, will open iOS System Settings.
|
||||
static var openSystemSettings: some View {
|
||||
Label {
|
||||
Text("OPEN_SYSTEM_SETTINGS", tableName: "Settings")
|
||||
} icon: {
|
||||
Image("system.settings")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30.0, height: 30.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
.onTapGesture {
|
||||
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the New Article Notifications
|
||||
/// screen in to view.
|
||||
static var configureNewArticleNotifications: some View {
|
||||
NavigationLink(destination: NewArticleNotificationsView()) {
|
||||
Label {
|
||||
Text("NEW_ARTICLE_NOTIFICATIONS", tableName: "Settings")
|
||||
} icon: {
|
||||
Image("notifications.sounds")
|
||||
.resizable()
|
||||
.frame(width: 30.0, height: 30.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the the Add Account screen
|
||||
/// in to view.
|
||||
static var addAccount: some View {
|
||||
NavigationLink(destination: AccountsManagementView()) {
|
||||
Label {
|
||||
Text("MANAGE_ACCOUNTS", tableName: "Settings")
|
||||
} icon: {
|
||||
Image("app.account")
|
||||
.resizable()
|
||||
.frame(width: 30.0, height: 30.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the the Manage Extension screen
|
||||
/// in to view.
|
||||
static var manageExtensions: some View {
|
||||
NavigationLink(destination: ExtensionsManagementView()) {
|
||||
Label {
|
||||
Text("MANAGE_EXTENSIONS", tableName: "Settings")
|
||||
} icon: {
|
||||
Image("app.extension")
|
||||
.resizable()
|
||||
.frame(width: 30.0, height: 30.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will present the Import
|
||||
/// Subscriptions Action Sheet.
|
||||
static func importOPML(showImportActionSheet: Binding<Bool>) -> some View {
|
||||
Button {
|
||||
showImportActionSheet.wrappedValue.toggle()
|
||||
} label: {
|
||||
Label {
|
||||
Text("IMPORT_SUBSCRIPTIONS", tableName: "Settings")
|
||||
.foregroundColor(.primary)
|
||||
|
||||
} icon: {
|
||||
Image("app.import.opml")
|
||||
.resizable()
|
||||
.frame(width: 30.0, height: 30.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will present the Export
|
||||
/// Subscriptions Action Sheet.
|
||||
static func exportOPML(showExportActionSheet: Binding<Bool>) -> some View {
|
||||
Button {
|
||||
showExportActionSheet.wrappedValue.toggle()
|
||||
} label: {
|
||||
Label {
|
||||
Text("EXPORT_SUBSCRIPTIONS", tableName: "Settings")
|
||||
.foregroundColor(.primary)
|
||||
|
||||
} icon: {
|
||||
Image("app.export.opml")
|
||||
.resizable()
|
||||
.frame(width: 30.0, height: 30.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Toggle` which triggers changes to the user's sort order preference.
|
||||
/// - Parameter preference: `Binding<Bool>`
|
||||
/// - Returns: `Toggle`
|
||||
static func sortOldestToNewest(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("SORT_OLDEST_NEWEST", tableName: "Settings")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Toggle` which triggers changes to the user's grouping preference.
|
||||
/// - Parameter preference: `Binding<Bool>`
|
||||
/// - Returns: `Toggle`
|
||||
static func groupByFeed(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("GROUP_BY_FEED", tableName: "Settings")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Toggle` which triggers changes to the user's refresh to clear preferences.
|
||||
/// - Parameter preference: `Binding<Bool>`
|
||||
/// - Returns: `Toggle`
|
||||
static func refreshToClearReadArticles(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("REFRESH_TO_CLEAR_READ_ARTICLES", tableName: "Settings")
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the the Timeline Layout screen
|
||||
/// in to view.
|
||||
static var timelineLayout: some View {
|
||||
NavigationLink {
|
||||
TimelineCustomizerView()
|
||||
} label: {
|
||||
Text("TIMELINE_LAYOUT", tableName: "Settings")
|
||||
}
|
||||
}
|
||||
|
||||
/// This row, when tapped, will push the the Theme Selector screen
|
||||
/// in to view.
|
||||
static var themeSelection: some View {
|
||||
NavigationLink(destination: ArticleThemeManagerView()) {
|
||||
HStack {
|
||||
Text("ARTICLE_THEME", tableName: "Settings")
|
||||
Spacer()
|
||||
Text(ArticleThemesManager.shared.currentTheme.name)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func confirmMarkAllAsRead(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("CONFIRM_MARK_ALL_AS_READ", tableName: "Settings")
|
||||
}
|
||||
}
|
||||
|
||||
static func openLinksInNetNewsWire(_ preference: Binding<Bool>) -> some View {
|
||||
Toggle(isOn: preference) {
|
||||
Text("OPEN_LINKS_IN_APP", tableName: "Settings")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add Reader Mode Defaults here. See #3684.
|
||||
|
||||
/// This row, when tapped, will push the New Article Notifications
|
||||
/// screen in to view.
|
||||
static func configureAppearance(_ isShown: Binding<Bool>) -> some View {
|
||||
NavigationLink(destination: DisplayAndBehaviorsView(), isActive: isShown) {
|
||||
Label {
|
||||
Text("DISPLAY_BEHAVIORS_HEADER", tableName: "Settings")
|
||||
} icon: {
|
||||
Image("app.appearance")
|
||||
.resizable()
|
||||
.frame(width: 30.0, height: 30.0)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the help sheet the user wishes to see.
|
||||
/// - Parameters:
|
||||
/// - sheet: The sheet provided to create the view.
|
||||
/// - selectedSheet: A `Binding` to the currently selected sheet. This is set, followed by `show`.
|
||||
/// - show: A `Binding` to `Bool` which triggers the sheet to display.
|
||||
/// - Returns: `View`
|
||||
static func showHelpSheet(sheet: HelpSheet, selectedSheet: Binding<HelpSheet>, _ show: Binding<Bool>) -> some View {
|
||||
Label {
|
||||
Text(sheet.description)
|
||||
} icon: {
|
||||
Image(systemName: sheet.systemImage)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30.0, height: 30.0)
|
||||
}
|
||||
.onTapGesture {
|
||||
selectedSheet.wrappedValue = sheet
|
||||
show.wrappedValue.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
static var aboutNetNewsWire: some View {
|
||||
NavigationLink {
|
||||
AboutView()
|
||||
} label: {
|
||||
Label {
|
||||
Text("ABOUT", tableName: "Settings")
|
||||
} icon: {
|
||||
Image(systemName: "info.circle")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30.0, height: 30.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
157
iOS/Settings/General/SettingsView.swift
Normal file
157
iOS/Settings/General/SettingsView.swift
Normal file
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 12/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
import UserNotifications
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@StateObject private var appDefaults = AppDefaults.shared
|
||||
@StateObject private var viewModel = SettingsViewModel()
|
||||
|
||||
@Binding var isConfigureAppearanceShown: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
// Device Permissions
|
||||
Section(header: Text("DEVICE_PERMISSIONS_HEADER", tableName: "Settings"),
|
||||
footer: Text("DEVICE_PERMISSIONS_FOOTER", tableName: "Settings")) {
|
||||
SettingsViewRows.openSystemSettings
|
||||
}
|
||||
|
||||
// Account/Extensions/OPML Management
|
||||
Section(header: Text("ACCOUNTS_EXTENSIONS_HEADER", tableName: "Settings"),
|
||||
footer: Text("ACCOUNTS_EXTENSIONS_FOOTER", tableName: "Settings")) {
|
||||
SettingsViewRows.addAccount
|
||||
SettingsViewRows.manageExtensions
|
||||
SettingsViewRows.importOPML(showImportActionSheet: $viewModel.showImportActionSheet)
|
||||
.confirmationDialog(Text("IMPORT_OPML_CONFIRMATION", tableName: "Settings"),
|
||||
isPresented: $viewModel.showImportActionSheet,
|
||||
titleVisibility: .visible) {
|
||||
ForEach(AccountManager.shared.sortedActiveAccounts, id: \.self) { account in
|
||||
Button(account.nameForDisplay) {
|
||||
viewModel.importAccount = account
|
||||
viewModel.showImportView = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsViewRows.exportOPML(showExportActionSheet: $viewModel.showExportActionSheet)
|
||||
.confirmationDialog(Text("EXPORT_OPML_CONFIRMATION", tableName: "Settings"),
|
||||
isPresented: $viewModel.showExportActionSheet,
|
||||
titleVisibility: .visible) {
|
||||
ForEach(AccountManager.shared.sortedAccounts, id: \.self) { account in
|
||||
Button(account.nameForDisplay) {
|
||||
do {
|
||||
let document = try OPMLDocument(account)
|
||||
viewModel.exportDocument = document
|
||||
viewModel.showExportView = true
|
||||
} catch {
|
||||
viewModel.importExportError = error
|
||||
viewModel.showImportExportError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Appearance
|
||||
Section(header: Text("APPEARANCE_HEADER", tableName: "Settings"),
|
||||
footer: Text("APPEARANCE_FOOTER", tableName: "Settings")) {
|
||||
SettingsViewRows.configureAppearance($isConfigureAppearanceShown)
|
||||
if viewModel.notificationPermissions == .authorized {
|
||||
SettingsViewRows.configureNewArticleNotifications
|
||||
}
|
||||
}
|
||||
|
||||
// Help
|
||||
Section {
|
||||
ForEach(0..<HelpSheet.allCases.count, id: \.self) { i in
|
||||
SettingsViewRows.showHelpSheet(sheet: HelpSheet.allCases[i], selectedSheet: $viewModel.helpSheet, $viewModel.showHelpSheet)
|
||||
}
|
||||
SettingsViewRows.aboutNetNewsWire
|
||||
}
|
||||
}
|
||||
.tint(Color(uiColor: AppAssets.primaryAccentColor))
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle(Text("SETTINGS_TITLE", tableName: "Settings"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading, content: {
|
||||
Button(action: { dismiss() }, label: { Text("DONE_BUTTON_TITLE", tableName: "Buttons") })
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showAddAccountView) {
|
||||
AddAccountListView()
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showHelpSheet) {
|
||||
SafariView(url: viewModel.helpSheet.url)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showAbout) {
|
||||
AboutView()
|
||||
}
|
||||
.task {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
DispatchQueue.main.async {
|
||||
self.viewModel.notificationPermissions = settings.authorizationStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIScene.willEnterForegroundNotification)) { _ in
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
DispatchQueue.main.async {
|
||||
self.viewModel.notificationPermissions = settings.authorizationStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
.dismissOnExternalContextLaunch()
|
||||
.fileImporter(isPresented: $viewModel.showImportView, allowedContentTypes: OPMLDocument.readableContentTypes) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
viewModel.importAccount!.importOPML(url) { importResult in
|
||||
switch importResult {
|
||||
case .success(_):
|
||||
viewModel.showImportSuccess = true
|
||||
case .failure(let error):
|
||||
viewModel.importExportError = error
|
||||
viewModel.showImportExportError = true
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
viewModel.importExportError = error
|
||||
viewModel.showImportExportError = true
|
||||
}
|
||||
}
|
||||
.fileExporter(isPresented: $viewModel.showExportView, document: viewModel.exportDocument, contentType: OPMLDocument.writableContentTypes.first!, onCompletion: { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
viewModel.showExportSuccess = true
|
||||
case .failure(let error):
|
||||
viewModel.importExportError = error
|
||||
viewModel.showImportExportError = true
|
||||
}
|
||||
})
|
||||
.alert(Text("IMPORT_OPML_SUCCESS_TITLE", tableName: "Settings"),
|
||||
isPresented: $viewModel.showImportSuccess,
|
||||
actions: {},
|
||||
message: { Text("IMPORT_OPML_SUCCESS_MESSAGE \(viewModel.importAccount?.nameForDisplay ?? "")", tableName: "Settings") })
|
||||
.alert(Text("EXPORT_OPML_SUCCESS_TITLE", tableName: "Settings"),
|
||||
isPresented: $viewModel.showExportSuccess,
|
||||
actions: {},
|
||||
message: { Text("EXPORT_OPML_SUCCESS_MESSAGE", tableName: "Settings") })
|
||||
.alert(Text("ERROR_TITLE", tableName: "Errors"),
|
||||
isPresented: $viewModel.showImportExportError,
|
||||
actions: {},
|
||||
message: { Text(viewModel.importExportError?.localizedDescription ?? "Import/Export Error") } )
|
||||
}.navigationViewStyle(.stack)
|
||||
}
|
||||
}
|
||||
34
iOS/Settings/General/SettingsViewModel.swift
Normal file
34
iOS/Settings/General/SettingsViewModel.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// SettingsViewModel.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Stuart Breckenridge on 29/11/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Account
|
||||
import UniformTypeIdentifiers
|
||||
import UserNotifications
|
||||
|
||||
|
||||
public final class SettingsViewModel: ObservableObject {
|
||||
|
||||
@Published public var showAddAccountView: Bool = false
|
||||
@Published public var helpSheet: HelpSheet = .help
|
||||
@Published public var showHelpSheet: Bool = false
|
||||
@Published public var showAbout: Bool = false
|
||||
@Published public var notificationPermissions: UNAuthorizationStatus = .notDetermined
|
||||
@Published public var importAccount: Account? = nil
|
||||
@Published public var exportAccount: Account? = nil
|
||||
@Published public var showImportView: Bool = false
|
||||
@Published public var showExportView: Bool = false
|
||||
@Published public var showImportActionSheet: Bool = false
|
||||
@Published public var showExportActionSheet: Bool = false
|
||||
@Published public var showImportExportError: Bool = false
|
||||
@Published public var importExportError: Error?
|
||||
@Published public var showImportSuccess: Bool = false
|
||||
@Published public var showExportSuccess: Bool = false
|
||||
@Published public var exportDocument: OPMLDocument?
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user