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:
parent
9218b4d95c
commit
06826a23bb
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue