mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Randomizes featured source + app order at app launch
This commit is contained in:
@@ -312,7 +312,7 @@ private extension FeaturedViewController
|
|||||||
fetchRequest.returnsObjectsAsFaults = false
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
fetchRequest.sortDescriptors = [
|
fetchRequest.sortDescriptors = [
|
||||||
// Sort by Source first to group into sections.
|
// Sort by Source first to group into sections.
|
||||||
NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
|
NSSortDescriptor(keyPath: \StoreApp._source?.featuredSortID, ascending: true),
|
||||||
|
|
||||||
// Show uninstalled apps first.
|
// Show uninstalled apps first.
|
||||||
// Sorting by StoreApp.installedApp crashes because InstalledApp does not respond to compare:
|
// Sorting by StoreApp.installedApp crashes because InstalledApp does not respond to compare:
|
||||||
@@ -324,8 +324,8 @@ private extension FeaturedViewController
|
|||||||
// Instead, sort by StoreApp.featuringSource.identifier, which will be either nil OR source ID.
|
// Instead, sort by StoreApp.featuringSource.identifier, which will be either nil OR source ID.
|
||||||
NSSortDescriptor(keyPath: \StoreApp.featuringSource?.identifier, ascending: false),
|
NSSortDescriptor(keyPath: \StoreApp.featuringSource?.identifier, ascending: false),
|
||||||
|
|
||||||
// Sort by name.
|
// Randomize order within sections.
|
||||||
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
|
NSSortDescriptor(keyPath: \StoreApp.featuredSortID, ascending: true),
|
||||||
|
|
||||||
// Sanity check to ensure stable ordering
|
// Sanity check to ensure stable ordering
|
||||||
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)
|
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)
|
||||||
@@ -346,14 +346,14 @@ private extension FeaturedViewController
|
|||||||
let primaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>
|
let primaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>
|
||||||
primaryFetchRequest.predicate = sourceHasRemainingAppsPredicate
|
primaryFetchRequest.predicate = sourceHasRemainingAppsPredicate
|
||||||
|
|
||||||
let primaryController = NSFetchedResultsController(fetchRequest: primaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp.sourceIdentifier), cacheName: nil)
|
let primaryController = NSFetchedResultsController(fetchRequest: primaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp._source.featuredSortID), cacheName: nil)
|
||||||
let primaryDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: primaryController)
|
let primaryDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: primaryController)
|
||||||
primaryDataSource.liveFetchLimit = 5
|
primaryDataSource.liveFetchLimit = 5
|
||||||
|
|
||||||
let secondaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>
|
let secondaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>
|
||||||
secondaryFetchRequest.predicate = NSCompoundPredicate(notPredicateWithSubpredicate: sourceHasRemainingAppsPredicate)
|
secondaryFetchRequest.predicate = NSCompoundPredicate(notPredicateWithSubpredicate: sourceHasRemainingAppsPredicate)
|
||||||
|
|
||||||
let secondaryController = NSFetchedResultsController(fetchRequest: secondaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp.sourceIdentifier), cacheName: nil)
|
let secondaryController = NSFetchedResultsController(fetchRequest: secondaryFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(StoreApp._source.featuredSortID), cacheName: nil)
|
||||||
let secondaryDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: secondaryController)
|
let secondaryDataSource = RSTFetchedResultsCollectionViewDataSource<StoreApp>(fetchedResultsController: secondaryController)
|
||||||
secondaryDataSource.liveFetchLimit = 5
|
secondaryDataSource.liveFetchLimit = 5
|
||||||
|
|
||||||
|
|||||||
@@ -217,6 +217,7 @@
|
|||||||
</entity>
|
</entity>
|
||||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||||
|
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
|
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
|
||||||
<attribute name="iconURL" optional="YES" attributeType="URI"/>
|
<attribute name="iconURL" optional="YES" attributeType="URI"/>
|
||||||
@@ -242,6 +243,7 @@
|
|||||||
<attribute name="category" optional="YES" attributeType="String"/>
|
<attribute name="category" optional="YES" attributeType="String"/>
|
||||||
<attribute name="developerName" attributeType="String"/>
|
<attribute name="developerName" attributeType="String"/>
|
||||||
<attribute name="downloadURL" attributeType="URI"/>
|
<attribute name="downloadURL" attributeType="URI"/>
|
||||||
|
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="iconURL" attributeType="URI"/>
|
<attribute name="iconURL" attributeType="URI"/>
|
||||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="isHiddenWithoutPledge" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="isHiddenWithoutPledge" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
|||||||
@@ -178,6 +178,49 @@ public extension DatabaseManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateFeaturedSortIDs() async
|
||||||
|
{
|
||||||
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
|
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // DON'T use our custom merge policy, because that one ignores changes to featuredSortID.
|
||||||
|
await context.performAsync {
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Randomize source order
|
||||||
|
let fetchRequest = Source.fetchRequest()
|
||||||
|
let sources = try context.fetch(fetchRequest)
|
||||||
|
|
||||||
|
for source in sources
|
||||||
|
{
|
||||||
|
source.featuredSortID = UUID().uuidString
|
||||||
|
}
|
||||||
|
|
||||||
|
try context.save()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Logger.main.error("Failed to update source order. \(error.localizedDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Randomize app order
|
||||||
|
let fetchRequest = StoreApp.fetchRequest()
|
||||||
|
let apps = try context.fetch(fetchRequest)
|
||||||
|
|
||||||
|
for app in apps
|
||||||
|
{
|
||||||
|
app.featuredSortID = UUID().uuidString
|
||||||
|
}
|
||||||
|
|
||||||
|
try context.save()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Logger.main.error("Failed to update app order. \(error.localizedDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension DatabaseManager
|
public extension DatabaseManager
|
||||||
@@ -372,7 +415,11 @@ private extension DatabaseManager
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
try context.save()
|
try context.save()
|
||||||
completionHandler(.success(()))
|
|
||||||
|
Task(priority: .high) {
|
||||||
|
await self.updateFeaturedSortIDs()
|
||||||
|
completionHandler(.success(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -234,6 +234,12 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
sortedScreenshotIDsByGlobalAppID[globallyUniqueID] = contextScreenshotIDs
|
sortedScreenshotIDsByGlobalAppID[globallyUniqueID] = contextScreenshotIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Revert contextApp.featuredSortID to database value (if it exists).
|
||||||
|
if let featuredSortID = databaseObject.featuredSortID
|
||||||
|
{
|
||||||
|
contextApp.featuredSortID = featuredSortID
|
||||||
|
}
|
||||||
|
|
||||||
case let databaseObject as Source:
|
case let databaseObject as Source:
|
||||||
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
||||||
|
|
||||||
@@ -263,6 +269,12 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
featuredAppIDsBySourceID[databaseObject.identifier] = contextSource.featuredApps?.map { $0.bundleIdentifier }
|
featuredAppIDsBySourceID[databaseObject.identifier] = contextSource.featuredApps?.map { $0.bundleIdentifier }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Revert conflictedObject.featuredSortID to database value (if it exists).
|
||||||
|
if let featuredSortID = databaseObject.featuredSortID
|
||||||
|
{
|
||||||
|
conflictedObject.featuredSortID = featuredSortID
|
||||||
|
}
|
||||||
|
|
||||||
case let databasePledge as Pledge:
|
case let databasePledge as Pledge:
|
||||||
guard let contextPledge = conflict.conflictingObjects.first as? Pledge else { break }
|
guard let contextPledge = conflict.conflictingObjects.first as? Pledge else { break }
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
|||||||
|
|
||||||
@NSManaged public var error: NSError?
|
@NSManaged public var error: NSError?
|
||||||
|
|
||||||
|
@NSManaged public var featuredSortID: String?
|
||||||
|
|
||||||
/* Non-Core Data Properties */
|
/* Non-Core Data Properties */
|
||||||
public var userInfo: [ALTSourceUserInfoKey: String]?
|
public var userInfo: [ALTSourceUserInfoKey: String]?
|
||||||
|
|
||||||
@@ -350,6 +352,13 @@ public class Source: NSManagedObject, Fetchable, Decodable
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override func awakeFromInsert()
|
||||||
|
{
|
||||||
|
super.awakeFromInsert()
|
||||||
|
|
||||||
|
self.featuredSortID = UUID().uuidString
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Source
|
public extension Source
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
@NSManaged @objc(pledgeAmount) private var _pledgeAmount: NSDecimalNumber?
|
@NSManaged @objc(pledgeAmount) private var _pledgeAmount: NSDecimalNumber?
|
||||||
|
|
||||||
@NSManaged public var sortIndex: Int32
|
@NSManaged public var sortIndex: Int32
|
||||||
|
@NSManaged public var featuredSortID: String?
|
||||||
|
|
||||||
@objc public internal(set) var sourceIdentifier: String? {
|
@objc public internal(set) var sourceIdentifier: String? {
|
||||||
get {
|
get {
|
||||||
@@ -463,6 +464,13 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override func awakeFromInsert()
|
||||||
|
{
|
||||||
|
super.awakeFromInsert()
|
||||||
|
|
||||||
|
self.featuredSortID = UUID().uuidString
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal extension StoreApp
|
internal extension StoreApp
|
||||||
|
|||||||
Reference in New Issue
Block a user