mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
- Feature: Implement Bulk add for Sources
This commit is contained in:
@@ -43,10 +43,10 @@ extension AddSourceViewController
|
||||
var sourceAddress: String = ""
|
||||
|
||||
@Published
|
||||
var sourceURL: URL?
|
||||
var sourceURLs: [URL] = []
|
||||
|
||||
@Published
|
||||
var sourcePreviewResult: SourcePreviewResult?
|
||||
var sourcePreviewResults: [SourcePreviewResult] = []
|
||||
|
||||
|
||||
/* State */
|
||||
@@ -60,6 +60,8 @@ extension AddSourceViewController
|
||||
|
||||
class AddSourceViewController: UICollectionViewController
|
||||
{
|
||||
private var stagedForAdd: [Source: Bool] = [:]
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var addSourceDataSource = self.makeAddSourceDataSource()
|
||||
private lazy var sourcePreviewDataSource = self.makeSourcePreviewDataSource()
|
||||
@@ -117,6 +119,7 @@ private extension AddSourceViewController
|
||||
layoutConfig.contentInsetsReference = .safeArea
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||
|
||||
guard let self, let section = Section(rawValue: sectionIndex) else { return nil }
|
||||
switch section
|
||||
{
|
||||
@@ -140,14 +143,19 @@ private extension AddSourceViewController
|
||||
configuration.showsSeparators = false
|
||||
configuration.backgroundColor = .clear
|
||||
|
||||
if self.viewModel.sourceURL != nil && self.viewModel.isShowingPreviewStatus
|
||||
if !self.viewModel.sourceURLs.isEmpty && self.viewModel.isShowingPreviewStatus
|
||||
{
|
||||
switch self.viewModel.sourcePreviewResult
|
||||
for result in self.viewModel.sourcePreviewResults
|
||||
{
|
||||
case (_, .success)?: configuration.footerMode = .none
|
||||
case (_, .failure)?: configuration.footerMode = .supplementary
|
||||
case nil where self.viewModel.isLoadingPreview: configuration.footerMode = .supplementary
|
||||
default: configuration.footerMode = .none
|
||||
switch result
|
||||
{
|
||||
case (_, .success): configuration.footerMode = .none
|
||||
case (_, .failure): configuration.footerMode = .supplementary
|
||||
break
|
||||
// case nil where self.viewModel.isLoadingPreview: configuration.footerMode = .supplementary
|
||||
// break
|
||||
// default: configuration.footerMode = .none
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -303,50 +311,58 @@ private extension AddSourceViewController
|
||||
{
|
||||
/* Pipeline */
|
||||
|
||||
// Map UITextField text -> URL
|
||||
// Map UITextField text -> URLs
|
||||
self.viewModel.$sourceAddress
|
||||
.map { [weak self] in self?.sourceURL(from: $0) }
|
||||
.assign(to: &self.viewModel.$sourceURL)
|
||||
|
||||
.map { [weak self] in
|
||||
guard let self else { return [] }
|
||||
print("\n\nStarting pipeline processing...\n\n")
|
||||
|
||||
let lines = $0.split(whereSeparator: { $0.isWhitespace }).map(String.init).compactMap(self.sourceURL)
|
||||
return lines
|
||||
}
|
||||
.assign(to: &self.viewModel.$sourceURLs)
|
||||
|
||||
let showPreviewStatusPublisher = self.viewModel.$isShowingPreviewStatus
|
||||
.filter { $0 == true }
|
||||
|
||||
let sourceURLPublisher = self.viewModel.$sourceURL
|
||||
let sourceURLsPublisher = self.viewModel.$sourceURLs
|
||||
.removeDuplicates()
|
||||
.debounce(for: 0.2, scheduler: RunLoop.main)
|
||||
.receive(on: RunLoop.main)
|
||||
.map { [weak self] sourceURL in
|
||||
.map { [weak self] sourceURLs in
|
||||
// Only set sourcePreviewResult to nil if sourceURL actually changes.
|
||||
self?.viewModel.sourcePreviewResult = nil
|
||||
return sourceURL
|
||||
self?.viewModel.sourcePreviewResults = []
|
||||
return sourceURLs
|
||||
}
|
||||
|
||||
// Map URL -> Source Preview
|
||||
Publishers.CombineLatest(sourceURLPublisher, showPreviewStatusPublisher.prepend(false))
|
||||
Publishers.CombineLatest(sourceURLsPublisher, showPreviewStatusPublisher.prepend(false))
|
||||
.receive(on: RunLoop.main)
|
||||
.map { $0.0 }
|
||||
.compactMap { [weak self] (sourceURL: URL?) -> AnyPublisher<SourcePreviewResult?, Never>? in
|
||||
guard let self else { return nil }
|
||||
|
||||
guard let sourceURL else {
|
||||
// Unlike above guard, this continues the pipeline with nil value.
|
||||
return Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
.flatMap { [weak self] (sourceURLs: [URL]) -> AnyPublisher<[SourcePreviewResult?], Never> in
|
||||
guard let self else { return Just([]).eraseToAnyPublisher() }
|
||||
|
||||
self.viewModel.isLoadingPreview = true
|
||||
return self.fetchSourcePreview(sourceURL: sourceURL).eraseToAnyPublisher()
|
||||
|
||||
let publishers = sourceURLs.map { sourceURL in
|
||||
print("Creating preview for source:", sourceURL, " ...")
|
||||
return self.fetchSourcePreview(sourceURL: sourceURL)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return publishers.isEmpty
|
||||
? Just([]).eraseToAnyPublisher()
|
||||
: Publishers.MergeMany(publishers)
|
||||
.collect()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest() // Cancels previous publisher
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] sourcePreviewResult in
|
||||
.sink { [weak self] sourcePreviewResults in
|
||||
self?.viewModel.isLoadingPreview = false
|
||||
self?.viewModel.sourcePreviewResult = sourcePreviewResult
|
||||
self?.viewModel.sourcePreviewResults = sourcePreviewResults.compactMap{$0}
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
|
||||
/* Update UI */
|
||||
|
||||
Publishers.CombineLatest(self.viewModel.$isLoadingPreview.removeDuplicates(),
|
||||
self.viewModel.$isShowingPreviewStatus.removeDuplicates())
|
||||
.sink { [weak self] _ in
|
||||
@@ -359,7 +375,7 @@ private extension AddSourceViewController
|
||||
|
||||
if let footerView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath) as? PlaceholderCollectionReusableView
|
||||
{
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResult)
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResults)
|
||||
}
|
||||
|
||||
let context = UICollectionViewLayoutInvalidationContext()
|
||||
@@ -370,27 +386,67 @@ private extension AddSourceViewController
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
self.viewModel.$sourcePreviewResult
|
||||
.map { $0?.1 }
|
||||
.map { result -> Managed<Source>? in
|
||||
switch result
|
||||
{
|
||||
case .success(let source): return source
|
||||
case .failure, nil: return nil
|
||||
// self.viewModel.$sourcePreviewResults
|
||||
// .map { sourcePreviewResults -> [Source] in
|
||||
// // Process the full array:
|
||||
// // - For each tuple, extract the `result` (the second element)
|
||||
// // - For each result, convert it to a Managed<Source> if it's successful
|
||||
// // - Remove any nil values from failed results
|
||||
// let managedSources = sourcePreviewResults.compactMap { previewResult -> Managed<Source>? in
|
||||
// switch previewResult.result {
|
||||
// case .success(let source):
|
||||
// return source
|
||||
// case .failure:
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
// // Optionally, remove duplicates based on identifier:
|
||||
// // (This groups by identifier and keeps the first occurrence.)
|
||||
// let uniqueManagedSources = Dictionary(grouping: managedSources, by: { $0.identifier })
|
||||
// .compactMap { $0.value.first }
|
||||
//
|
||||
// // Unwrap Managed<Source> into Source (assuming Managed<Source> has a wrappedValue property)
|
||||
// let sources = uniqueManagedSources.map { $0.wrappedValue }
|
||||
// return sources
|
||||
// }
|
||||
// .receive(on: RunLoop.main)
|
||||
// .sink { [weak self] sources in
|
||||
// self?.updateSourcesPreview(for: sources)
|
||||
// }
|
||||
// .store(in: &self.cancellables)
|
||||
|
||||
self.viewModel.$sourcePreviewResults
|
||||
.map { sourcePreviewResults -> [Source] in
|
||||
var seenIdentifiers = Set<String>()
|
||||
let orderedSources = sourcePreviewResults.compactMap { previewResult -> Source? in
|
||||
switch previewResult.result {
|
||||
case .success(let managedSource):
|
||||
let id = managedSource.identifier
|
||||
guard !seenIdentifiers.contains(id) else { return nil }
|
||||
seenIdentifiers.insert(id)
|
||||
return managedSource.wrappedValue
|
||||
case .failure:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.removeDuplicates { (sourceA: Managed<Source>?, sourceB: Managed<Source>?) in
|
||||
sourceA?.identifier == sourceB?.identifier
|
||||
return orderedSources
|
||||
}
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] source in
|
||||
self?.updateSourcePreview(for: source?.wrappedValue)
|
||||
.sink { [weak self] sources in
|
||||
self?.updateSourcesPreview(for: sources)
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
|
||||
let addPublisher = NotificationCenter.default.publisher(for: AppManager.didAddSourceNotification)
|
||||
let removePublisher = NotificationCenter.default.publisher(for: AppManager.didRemoveSourceNotification)
|
||||
Publishers.Merge(addPublisher, removePublisher)
|
||||
let mergedNotificationPublisher = Publishers.Merge(
|
||||
NotificationCenter.default.publisher(for: AppManager.didAddSourceNotification),
|
||||
NotificationCenter.default.publisher(for: AppManager.didRemoveSourceNotification)
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.share() // Shares the upstream publisher with multiple subscribers
|
||||
|
||||
// Update recommended sources section when sources are added/removed
|
||||
mergedNotificationPublisher
|
||||
.compactMap { notification -> String? in
|
||||
guard let source = notification.object as? Source,
|
||||
let context = source.managedObjectContext
|
||||
@@ -399,7 +455,6 @@ private extension AddSourceViewController
|
||||
let sourceID = context.performAndWait { source.identifier }
|
||||
return sourceID
|
||||
}
|
||||
.receive(on: RunLoop.main)
|
||||
.compactMap { [dataSource = recommendedSourcesDataSource] sourceID -> IndexPath? in
|
||||
guard let index = dataSource.items.firstIndex(where: { $0.identifier == sourceID }) else { return nil }
|
||||
|
||||
@@ -411,6 +466,32 @@ private extension AddSourceViewController
|
||||
self?.collectionView.reloadItems(at: [indexPath])
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
|
||||
// Update previews section when sources are added/removed
|
||||
// mergedNotificationPublisher
|
||||
// .sink { [weak self] _ in
|
||||
// // reload the entire of previews section to get latest state
|
||||
// self?.collectionView.reloadSections(IndexSet(integer: Section.preview.rawValue))
|
||||
// }
|
||||
// .store(in: &self.cancellables)
|
||||
|
||||
mergedNotificationPublisher
|
||||
.compactMap { notification -> String? in
|
||||
guard let source = notification.object as? Source,
|
||||
let context = source.managedObjectContext
|
||||
else { return nil }
|
||||
return context.performAndWait { source.identifier }
|
||||
}
|
||||
.compactMap { [weak self] sourceID -> IndexPath? in
|
||||
guard let dataSource = self?.sourcePreviewDataSource,
|
||||
let index = dataSource.items.firstIndex(where: { $0.identifier == sourceID })
|
||||
else { return nil }
|
||||
return IndexPath(item: index, section: Section.preview.rawValue)
|
||||
}
|
||||
.sink { [weak self] indexPath in
|
||||
self?.collectionView.reloadItems(at: [indexPath])
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
}
|
||||
|
||||
func sourceURL(from address: String) -> URL?
|
||||
@@ -458,35 +539,51 @@ private extension AddSourceViewController
|
||||
})
|
||||
}
|
||||
|
||||
func updateSourcePreview(for source: Source?)
|
||||
{
|
||||
let items = [source].compactMap { $0 }
|
||||
func updateSourcesPreview(for sources: [Source]) {
|
||||
// Calculate changes needed to go from current items to new items
|
||||
let currentItemCount = self.sourcePreviewDataSource.items.count
|
||||
let newItemCount = sources.count
|
||||
|
||||
// Have to provide changes in terms of sourcePreviewDataSource.
|
||||
let indexPath = IndexPath(row: 0, section: 0)
|
||||
var changes: [RSTCellContentChange] = []
|
||||
|
||||
if !items.isEmpty && self.sourcePreviewDataSource.items.isEmpty
|
||||
{
|
||||
let change = RSTCellContentChange(type: .insert, currentIndexPath: nil, destinationIndexPath: indexPath)
|
||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
||||
}
|
||||
else if items.isEmpty && !self.sourcePreviewDataSource.items.isEmpty
|
||||
{
|
||||
let change = RSTCellContentChange(type: .delete, currentIndexPath: indexPath, destinationIndexPath: nil)
|
||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
||||
}
|
||||
else if !items.isEmpty && !self.sourcePreviewDataSource.items.isEmpty
|
||||
{
|
||||
let change = RSTCellContentChange(type: .update, currentIndexPath: indexPath, destinationIndexPath: indexPath)
|
||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
||||
if currentItemCount == 0 && newItemCount > 0 {
|
||||
// Insert all items if we currently have none
|
||||
for i in 0..<newItemCount {
|
||||
let indexPath = IndexPath(row: i, section: 0)
|
||||
let change = RSTCellContentChange(type: .insert,
|
||||
currentIndexPath: nil,
|
||||
destinationIndexPath: indexPath)
|
||||
changes.append(change)
|
||||
}
|
||||
} else if currentItemCount > 0 && newItemCount == 0 {
|
||||
// Delete all items if we're going to have none
|
||||
for i in 0..<currentItemCount {
|
||||
let indexPath = IndexPath(row: i, section: 0)
|
||||
let change = RSTCellContentChange(type: .delete,
|
||||
currentIndexPath: indexPath,
|
||||
destinationIndexPath: nil)
|
||||
changes.append(change)
|
||||
}
|
||||
} else if currentItemCount != newItemCount {
|
||||
// If counts differ, do a section update
|
||||
let change = RSTCellContentChange(type: .update, sectionIndex: 0)
|
||||
changes = [change]
|
||||
} else {
|
||||
// Update existing items in place
|
||||
for i in 0..<newItemCount {
|
||||
let indexPath = IndexPath(row: i, section: 0)
|
||||
let change = RSTCellContentChange(type: .update,
|
||||
currentIndexPath: indexPath,
|
||||
destinationIndexPath: indexPath)
|
||||
changes.append(change)
|
||||
}
|
||||
}
|
||||
|
||||
if source == nil
|
||||
{
|
||||
self.sourcePreviewDataSource.setItems(sources, with: changes)
|
||||
|
||||
if sources.isEmpty {
|
||||
self.collectionView.reloadSections([Section.preview.rawValue])
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
}
|
||||
@@ -510,9 +607,6 @@ private extension AddSourceViewController
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
|
||||
let config = UIImage.SymbolConfiguration(scale: .medium)
|
||||
let image = UIImage(systemName: "plus.circle.fill", withConfiguration: config)?.withTintColor(.white, renderingMode: .alwaysOriginal)
|
||||
cell.bannerView.button.setImage(image, for: .normal)
|
||||
cell.bannerView.button.setImage(image, for: .highlighted)
|
||||
cell.bannerView.button.setTitle(nil, for: .normal)
|
||||
cell.bannerView.button.imageView?.contentMode = .scaleAspectFit
|
||||
cell.bannerView.button.contentHorizontalAlignment = .fill // Fill entire button with imageView
|
||||
@@ -521,28 +615,51 @@ private extension AddSourceViewController
|
||||
cell.bannerView.button.tintColor = .clear
|
||||
cell.bannerView.button.isHidden = false
|
||||
|
||||
func setButtonIcon()
|
||||
{
|
||||
Task<Void, Never>(priority: .userInitiated) { [weak cell] in
|
||||
guard let cell else { return }
|
||||
|
||||
var isSourceAlreadyPersisted = false
|
||||
do
|
||||
{
|
||||
isSourceAlreadyPersisted = try await source.isAdded
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to determine if source is added.", error)
|
||||
}
|
||||
|
||||
// use the plus icon by default
|
||||
var buttonIcon = UIImage(systemName: "plus.circle.fill", withConfiguration: config)?.withTintColor(.white, renderingMode: .alwaysOriginal)
|
||||
|
||||
// if the source is already added/staged for adding, use the checkmark icon
|
||||
let isStagedForAdd = self.stagedForAdd[source] == true
|
||||
if isStagedForAdd || isSourceAlreadyPersisted
|
||||
{
|
||||
buttonIcon = UIImage(systemName: "checkmark.circle.fill", withConfiguration: config)?
|
||||
.withTintColor(isSourceAlreadyPersisted ? .green : .white, renderingMode: .alwaysOriginal)
|
||||
}
|
||||
cell.bannerView.button.setImage(buttonIcon, for: .normal)
|
||||
cell.bannerView.button.isEnabled = !isSourceAlreadyPersisted
|
||||
}
|
||||
}
|
||||
|
||||
// set the icon
|
||||
setButtonIcon()
|
||||
|
||||
let action = UIAction(identifier: .addSource) { [weak self] _ in
|
||||
self?.add(source)
|
||||
guard let self else { return }
|
||||
|
||||
self.stagedForAdd[source, default: false].toggle()
|
||||
|
||||
// update the button icon
|
||||
setButtonIcon()
|
||||
}
|
||||
cell.bannerView.button.addAction(action, for: .primaryActionTriggered)
|
||||
|
||||
Task<Void, Never>(priority: .userInitiated) {
|
||||
do
|
||||
{
|
||||
let isAdded = try await source.isAdded
|
||||
if isAdded
|
||||
{
|
||||
cell.bannerView.button.isHidden = true
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to determine if source is added.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func configure(_ footerView: PlaceholderCollectionReusableView, with sourcePreviewResult: SourcePreviewResult?)
|
||||
func configure(_ footerView: PlaceholderCollectionReusableView, with sourcePreviewResults: [SourcePreviewResult?])
|
||||
{
|
||||
footerView.placeholderView.stackView.isLayoutMarginsRelativeArrangement = false
|
||||
|
||||
@@ -552,23 +669,33 @@ private extension AddSourceViewController
|
||||
|
||||
footerView.placeholderView.detailTextLabel.isHidden = true
|
||||
|
||||
switch sourcePreviewResult
|
||||
var errorText: String? = nil
|
||||
var isError: Bool = false
|
||||
for result in sourcePreviewResults
|
||||
{
|
||||
case (let sourceURL, .failure(let previewError))? where self.viewModel.sourceURL == sourceURL && !self.viewModel.isLoadingPreview:
|
||||
// The current URL matches the error being displayed, and we're not loading another preview, so show error.
|
||||
|
||||
footerView.placeholderView.textLabel.text = (previewError as NSError).localizedDebugDescription ?? previewError.localizedDescription
|
||||
footerView.placeholderView.textLabel.isHidden = false
|
||||
|
||||
footerView.placeholderView.activityIndicatorView.stopAnimating()
|
||||
|
||||
default:
|
||||
// The current URL does not match the URL of the source/error being displayed, so show loading indicator.
|
||||
|
||||
footerView.placeholderView.textLabel.text = nil
|
||||
footerView.placeholderView.textLabel.isHidden = true
|
||||
|
||||
switch result
|
||||
{
|
||||
case (let sourceURL, .failure(let previewError))? where (self.viewModel.sourceURLs.contains(sourceURL) && !self.viewModel.isLoadingPreview):
|
||||
// The current URL matches the error being displayed, and we're not loading another preview, so show error.
|
||||
|
||||
errorText = (previewError as NSError).localizedDebugDescription ?? previewError.localizedDescription
|
||||
footerView.placeholderView.textLabel.text = errorText
|
||||
footerView.placeholderView.textLabel.isHidden = false
|
||||
|
||||
isError = true
|
||||
|
||||
default:
|
||||
// The current URL does not match the URL of the source/error being displayed, so show loading indicator.
|
||||
errorText = nil
|
||||
footerView.placeholderView.textLabel.isHidden = true
|
||||
}
|
||||
}
|
||||
footerView.placeholderView.textLabel.text = errorText
|
||||
|
||||
if !isError{
|
||||
footerView.placeholderView.activityIndicatorView.startAnimating()
|
||||
} else{
|
||||
footerView.placeholderView.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,30 +779,60 @@ private extension AddSourceViewController
|
||||
}
|
||||
}
|
||||
|
||||
func add(@AsyncManaged _ source: Source)
|
||||
@IBAction func commitChanges(_ sender: UIBarButtonItem)
|
||||
{
|
||||
Task<Void, Never> {
|
||||
do
|
||||
{
|
||||
let isRecommended = await $source.isRecommended
|
||||
if isRecommended
|
||||
{
|
||||
try await AppManager.shared.add(source, message: nil, presentingViewController: self)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use default message
|
||||
try await AppManager.shared.add(source, presentingViewController: self)
|
||||
}
|
||||
|
||||
self.dismiss()
|
||||
|
||||
struct StagedSource: Hashable {
|
||||
@AsyncManaged var source: Source
|
||||
|
||||
// Conformance for Equatable/Hashable by comparing the underlying source
|
||||
static func == (lhs: StagedSource, rhs: StagedSource) -> Bool {
|
||||
return lhs.source.identifier == rhs.source.identifier
|
||||
}
|
||||
catch is CancellationError {}
|
||||
catch
|
||||
{
|
||||
let errorTitle = NSLocalizedString("Unable to Add Source", comment: "")
|
||||
await self.presentAlert(title: errorTitle, message: error.localizedDescription)
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(source)
|
||||
}
|
||||
}
|
||||
|
||||
Task<Void, Never> {
|
||||
var isCancelled = false
|
||||
// OK: COMMIT the staged changes now
|
||||
// Convert the stagedForAdd dictionary into an array of StagedSource
|
||||
let stagedSources: [StagedSource] = self.stagedForAdd.filter { $0.value }
|
||||
.map { StagedSource(source: $0.key) }
|
||||
|
||||
for staged in stagedSources {
|
||||
do
|
||||
{
|
||||
// Use the projected value to safely access isRecommended asynchronously
|
||||
let isRecommended = await staged.$source.isRecommended
|
||||
if isRecommended
|
||||
{
|
||||
try await AppManager.shared.add(staged.source, message: nil, presentingViewController: self)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use default message
|
||||
try await AppManager.shared.add(staged.source, presentingViewController: self)
|
||||
}
|
||||
|
||||
// remove this kv pair
|
||||
self.stagedForAdd.removeValue(forKey: staged.source)
|
||||
}
|
||||
catch is CancellationError {
|
||||
isCancelled = true
|
||||
break
|
||||
}
|
||||
catch
|
||||
{
|
||||
let errorTitle = NSLocalizedString("Unable to Add Source", comment: "")
|
||||
await self.presentAlert(title: errorTitle, message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
if !isCancelled {
|
||||
// finally dismiss the sheet/viewcontroller
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -737,7 +894,7 @@ extension AddSourceViewController: UICollectionViewDelegateFlowLayout
|
||||
case (.preview, UICollectionView.elementKindSectionFooter):
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseID.placeholderFooter.rawValue, for: indexPath) as! PlaceholderCollectionReusableView
|
||||
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResult)
|
||||
self.configure(footerView, with: self.viewModel.sourcePreviewResults)
|
||||
|
||||
return footerView
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="collection view cell content view" minToolsVersion="11.0"/>
|
||||
@@ -224,6 +224,11 @@
|
||||
<segue destination="qr8-ss-Ghz" kind="unwind" unwindAction="unwindFromAddSource:" id="Pba-Kh-qfH"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="done" id="oza-rj-JhC">
|
||||
<connections>
|
||||
<action selector="commitChanges:" destination="bbz-wy-kaK" id="4FB-Sj-E14"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="LO9-iP-zZC"/>
|
||||
|
||||
Reference in New Issue
Block a user