Partially fixes #566

• Adds a preference to show alerts to confirm deletion of feeds (default is true) and this is configurable in Settings / Preferences

• Supports single (iOS/macOS) and multiple selection (macOS) for deletion

• Until folders are selectable (future beta, hopefully), selecting them for deletion is disabled.
This commit is contained in:
Stuart Breckenridge 2020-08-15 09:37:18 +08:00
parent 9218b4d95c
commit 06826a23bb
No known key found for this signature in database
GPG Key ID: 79BD673276AE83CE
6 changed files with 124 additions and 5 deletions

View File

@ -61,6 +61,9 @@ final class AppDefaults: ObservableObject {
static let timelineGroupByFeed = "timelineGroupByFeed" static let timelineGroupByFeed = "timelineGroupByFeed"
static let timelineIconDimensions = "timelineIconDimensions" static let timelineIconDimensions = "timelineIconDimensions"
static let timelineNumberOfLines = "timelineNumberOfLines" static let timelineNumberOfLines = "timelineNumberOfLines"
// Sidebar Defaults
static let sidebarConfirmDelete = "sidebarConfirmDelete"
// iOS Defaults // iOS Defaults
static let refreshClearsReadArticles = "refreshClearsReadArticles" static let refreshClearsReadArticles = "refreshClearsReadArticles"
@ -198,7 +201,7 @@ final class AppDefaults: ObservableObject {
objectWillChange.send() objectWillChange.send()
} }
} }
@AppStorage(wrappedValue: 40.0, Key.timelineIconDimensions, store: store) var timelineIconDimensions: Double { @AppStorage(wrappedValue: 40.0, Key.timelineIconDimensions, store: store) var timelineIconDimensions: Double {
didSet { didSet {
objectWillChange.send() objectWillChange.send()
@ -212,6 +215,14 @@ final class AppDefaults: ObservableObject {
} }
} }
// MARK: Sidebar
@AppStorage(wrappedValue: true, Key.sidebarConfirmDelete, store: store) var sidebarConfirmDelete: Bool {
didSet {
objectWillChange.send()
}
}
// MARK: Refresh // MARK: Refresh
@AppStorage(wrappedValue: false, Key.refreshClearsReadArticles, store: store) var refreshClearsReadArticles: Bool @AppStorage(wrappedValue: false, Key.refreshClearsReadArticles, store: store) var refreshClearsReadArticles: Bool

View File

@ -123,7 +123,12 @@ struct SidebarContextMenu: View {
} }
Divider() Divider()
Button { Button {
sidebarModel.deleteFromAccount.send(sidebarItem.feed!) if AppDefaults.shared.sidebarConfirmDelete == false {
sidebarModel.deleteFromAccount.send(sidebarItem.feed!)
} else {
sidebarModel.sidebarItemToDelete = sidebarItem.feed!
sidebarModel.showDeleteConfirmation = true
}
} label: { } label: {
Text("Delete") Text("Delete")
#if os(iOS) #if os(iOS)
@ -153,15 +158,26 @@ struct SidebarContextMenu: View {
AppAssets.markAllAsReadImage AppAssets.markAllAsReadImage
#endif #endif
} }
/*
You cannot select folder level items in b4. Delete is disabled for the time being.
*/
/*
Divider() Divider()
Button { Button {
sidebarModel.deleteFromAccount.send(sidebarItem.feed!) if AppDefaults.shared.sidebarConfirmDelete == false {
sidebarModel.deleteFromAccount.send(sidebarItem.feed!)
} else {
sidebarModel.sidebarContextMenuItem = sidebarItem.feed
sidebarModel.showDeleteConfirmation = true
}
} label: { } label: {
Text("Delete") Text("Delete")
#if os(iOS) #if os(iOS)
AppAssets.deleteImage AppAssets.deleteImage
#endif #endif
} }
*/
} }
} }

View File

@ -22,6 +22,7 @@ class SidebarModel: ObservableObject, UndoableCommandRunner {
@Published var selectedFeedIdentifier: FeedIdentifier? = .none @Published var selectedFeedIdentifier: FeedIdentifier? = .none
@Published var isReadFiltered = false @Published var isReadFiltered = false
@Published var expandedContainers = SidebarExpandedContainers() @Published var expandedContainers = SidebarExpandedContainers()
@Published var showDeleteConfirmation: Bool = false
weak var delegate: SidebarModelDelegate? weak var delegate: SidebarModelDelegate?
@ -33,6 +34,8 @@ class SidebarModel: ObservableObject, UndoableCommandRunner {
var markAllAsReadInAccount = PassthroughSubject<Account, Never>() var markAllAsReadInAccount = PassthroughSubject<Account, Never>()
var deleteFromAccount = PassthroughSubject<Feed, Never>() var deleteFromAccount = PassthroughSubject<Feed, Never>()
var sidebarItemToDelete: Feed?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
var undoManager: UndoManager? var undoManager: UndoManager?
@ -50,6 +53,37 @@ class SidebarModel: ObservableObject, UndoableCommandRunner {
} }
extension SidebarModel {
func countOfFeedsToDelete() -> Int {
var selectedFeeds = selectedFeedIdentifiers
if sidebarItemToDelete != nil {
selectedFeeds.insert(sidebarItemToDelete!.feedID!)
}
return selectedFeeds.count
}
func namesOfFeedsToDelete() -> String {
var selectedFeeds = selectedFeedIdentifiers
if sidebarItemToDelete != nil {
selectedFeeds.insert(sidebarItemToDelete!.feedID!)
}
let feeds: [Feed] = selectedFeeds
.compactMap({ AccountManager.shared.existingFeed(with: $0) })
return feeds
.map({ $0.nameForDisplay })
.joined(separator: ", ")
}
}
// MARK: Private // MARK: Private
private extension SidebarModel { private extension SidebarModel {
@ -57,10 +91,12 @@ private extension SidebarModel {
// MARK: Subscriptions // MARK: Subscriptions
func subscribeToSelectedFeedChanges() { func subscribeToSelectedFeedChanges() {
let selectedFeedIdentifersPublisher = $selectedFeedIdentifiers let selectedFeedIdentifersPublisher = $selectedFeedIdentifiers
.map { [weak self] feedIDs -> [Feed] in .map { [weak self] feedIDs -> [Feed] in
return feedIDs.compactMap { self?.findFeed($0) } return feedIDs.compactMap { self?.findFeed($0) }
} }
let selectedFeedIdentiferPublisher = $selectedFeedIdentifier let selectedFeedIdentiferPublisher = $selectedFeedIdentifier
.compactMap { [weak self] feedID -> [Feed]? in .compactMap { [weak self] feedID -> [Feed]? in
@ -168,7 +204,7 @@ private extension SidebarModel {
func subscribeToDeleteFromAccount() { func subscribeToDeleteFromAccount() {
guard let selectedFeedsPublisher = selectedFeedsPublisher else { return } guard let selectedFeedsPublisher = selectedFeedsPublisher else { return }
deleteFromAccount deleteFromAccount
.withLatestFrom(selectedFeedsPublisher.prepend([Feed]()), resultSelector: { givenFeed, selectedFeeds -> [Feed] in .withLatestFrom(selectedFeedsPublisher.prepend([Feed]()), resultSelector: { givenFeed, selectedFeeds -> [Feed] in
if selectedFeeds.contains(where: { $0.feedID == givenFeed.feedID }) { if selectedFeeds.contains(where: { $0.feedID == givenFeed.feedID }) {
@ -180,7 +216,19 @@ private extension SidebarModel {
.sink { feeds in .sink { feeds in
for feed in feeds { for feed in feeds {
if let webFeed = feed as? WebFeed { if let webFeed = feed as? WebFeed {
webFeed.account?.removeWebFeed(webFeed) guard let account = webFeed.account,
let containerID = account.containerID,
let container = AccountManager.shared.existingContainer(with: containerID) else {
return
}
account.removeWebFeed(webFeed, from: container, completion: { result in
switch result {
case .success:
break
case .failure(let err):
print(err)
}
})
} }
if let folder = feed as? Folder { if let folder = feed as? Folder {
folder.account?.removeFolder(folder) { _ in } folder.account?.removeFolder(folder) { _ in }
@ -304,4 +352,6 @@ private extension SidebarModel {
selectedFeedIdentifier = feedID selectedFeedIdentifier = feedID
} }
} }

View File

@ -62,6 +62,23 @@ struct SidebarView: View {
.transition(.move(edge: .bottom)) .transition(.move(edge: .bottom))
} }
} }
.alert(isPresented: $sidebarModel.showDeleteConfirmation, content: {
Alert(title: sidebarModel.countOfFeedsToDelete() > 1 ?
(Text("Delete multiple items?")) :
(Text("Delete \(sidebarModel.namesOfFeedsToDelete())?")),
message: Text("Are you sure you wish to delete \(sidebarModel.namesOfFeedsToDelete())?"),
primaryButton: .destructive(Text("Delete"),
action: {
sidebarModel.deleteFromAccount.send(sidebarModel.sidebarItemToDelete!)
sidebarModel.sidebarItemToDelete = nil
sidebarModel.selectedFeedIdentifiers.removeAll()
sidebarModel.showDeleteConfirmation = false
}),
secondaryButton: .cancel(Text("Cancel"), action: {
sidebarModel.sidebarItemToDelete = nil
sidebarModel.showDeleteConfirmation = false
}))
})
#else #else
ZStack(alignment: .top) { ZStack(alignment: .top) {
List { List {
@ -76,6 +93,23 @@ struct SidebarView: View {
ProgressView().offset(y: -40) ProgressView().offset(y: -40)
} }
} }
.alert(isPresented: $sidebarModel.showDeleteConfirmation, content: {
Alert(title: sidebarModel.countOfFeedsToDelete() > 1 ?
(Text("Delete multiple items?")) :
(Text("Delete \(sidebarModel.namesOfFeedsToDelete())?")),
message: Text("Are you sure you wish to delete \(sidebarModel.namesOfFeedsToDelete())?"),
primaryButton: .destructive(Text("Delete"),
action: {
sidebarModel.deleteFromAccount.send(sidebarModel.sidebarItemToDelete!)
sidebarModel.sidebarItemToDelete = nil
sidebarModel.selectedFeedIdentifiers.removeAll()
sidebarModel.showDeleteConfirmation = false
}),
secondaryButton: .cancel(Text("Cancel"), action: {
sidebarModel.sidebarItemToDelete = nil
sidebarModel.showDeleteConfirmation = false
}))
})
#endif #endif
// .onAppear { // .onAppear {

View File

@ -26,6 +26,7 @@ struct SettingsView: View {
systemSettings systemSettings
accounts accounts
importExport importExport
sidebarFeedManagement
timeline timeline
articles articles
appearance appearance
@ -143,6 +144,12 @@ struct SettingsView: View {
.navigationBarTitle("Export Subscriptions", displayMode: .inline) .navigationBarTitle("Export Subscriptions", displayMode: .inline)
} }
var sidebarFeedManagement: some View {
Section(header: Text("Feed Management")) {
Toggle("Confirm When Deleting Feeds and Folders", isOn: $settings.sidebarConfirmDelete)
}.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
var timeline: some View { var timeline: some View {
Section(header: Text("Timeline"), content: { Section(header: Text("Timeline"), content: {
Toggle("Sort Oldest to Newest", isOn: $settings.timelineSortDirection) Toggle("Sort Oldest to Newest", isOn: $settings.timelineSortDirection)

View File

@ -35,6 +35,7 @@ struct GeneralPreferencesView: View {
}) })
}) })
Toggle("Confirm when deleting feeds and folders", isOn: $defaults.sidebarConfirmDelete)
Toggle("Open webpages in background in browser", isOn: $defaults.openInBrowserInBackground) Toggle("Open webpages in background in browser", isOn: $defaults.openInBrowserInBackground)