world-manager/World Manager for Minecraft/UI/Root/ItemCollectionProjection.swift

190 lines
6.6 KiB
Swift

import Foundation
struct ItemCollectionProjectionRequest: Hashable, Sendable {
let selection: SidebarSelection?
let searchText: String
let sortMode: ItemSortMode
let source: MinecraftSource?
}
struct ItemCollectionProjection: Sendable {
let request: ItemCollectionProjectionRequest
let sourceName: String
let title: String
let subtitle: String
let searchPrompt: String
let items: [MinecraftContentItem]
nonisolated static func placeholder(for request: ItemCollectionProjectionRequest) -> ItemCollectionProjection {
ItemCollectionProjection(
request: request,
sourceName: request.source?.displayName ?? "Library",
title: ItemCollectionProjector.title(
for: request.selection,
isSearching: !ItemCollectionProjector.trimmedSearchText(for: request).isEmpty
),
subtitle: "",
searchPrompt: ItemCollectionProjector.searchPrompt(for: request.selection, source: request.source),
items: []
)
}
}
enum ItemCollectionProjector {
nonisolated static func makeProjection(for request: ItemCollectionProjectionRequest) -> ItemCollectionProjection {
let trimmedSearchText = trimmedSearchText(for: request)
let scopedItems = request.source?.items(matching: request.selection) ?? []
let filteredItems: [MinecraftContentItem]
if trimmedSearchText.isEmpty {
filteredItems = scopedItems
} else {
filteredItems = scopedItems.filter { item in
item.searchText.localizedCaseInsensitiveContains(trimmedSearchText)
}
}
let displayedItems = filteredItems.sorted(by: sortComparator(for: request.sortMode))
let countNoun = collectionCountNoun(for: request.selection, scopedItemCount: scopedItems.count)
let subtitle: String
if trimmedSearchText.isEmpty {
subtitle = "\(scopedItems.count.formatted(.number)) \(countNoun)"
} else {
subtitle = "\(displayedItems.count.formatted(.number)) of \(scopedItems.count.formatted(.number)) \(countNoun)"
}
return ItemCollectionProjection(
request: request,
sourceName: request.source?.displayName ?? "Library",
title: title(for: request.selection, isSearching: !trimmedSearchText.isEmpty),
subtitle: subtitle,
searchPrompt: searchPrompt(for: request.selection, source: request.source),
items: displayedItems
)
}
nonisolated static func title(for selection: SidebarSelection?, isSearching: Bool) -> String {
if isSearching {
return "Searching “\(searchScopeTitle(for: selection))"
}
guard let selection else {
return "Library"
}
switch selection {
case .source, .allContent:
return "All Items"
case .contentType(_, let contentType):
return sidebarTitle(for: contentType)
}
}
nonisolated static func searchPrompt(for selection: SidebarSelection?, source: MinecraftSource?) -> String {
switch selection {
case .some(.source):
return "Search \(source?.displayName ?? "Library")"
case .some(.allContent):
return "Search All Items"
case .some(.contentType(_, let contentType)):
return "Search \(sidebarTitle(for: contentType))"
case .none:
return "Search Library"
}
}
nonisolated private static func searchScopeTitle(for selection: SidebarSelection?) -> String {
switch selection {
case .some(.source):
return "Library"
case .some(.allContent):
return "All"
case .some(.contentType(_, let contentType)):
return sidebarTitle(for: contentType)
case .none:
return "Library"
}
}
nonisolated private static func collectionCountNoun(for selection: SidebarSelection?, scopedItemCount: Int) -> String {
guard let selection else {
return "items"
}
switch selection {
case .source, .allContent:
return scopedItemCount == 1 ? "item" : "items"
case .contentType(_, let contentType):
switch contentType {
case .world:
return scopedItemCount == 1 ? "world" : "worlds"
case .behaviorPack, .resourcePack, .skinPack, .worldTemplate:
return scopedItemCount == 1 ? "pack" : "packs"
}
}
}
nonisolated private static func sidebarTitle(for contentType: MinecraftContentType) -> String {
switch contentType {
case .world:
return "Worlds"
case .behaviorPack:
return "Behavior Packs"
case .resourcePack:
return "Resource Packs"
case .skinPack:
return "Skin Packs"
case .worldTemplate:
return "World Templates"
}
}
nonisolated static func trimmedSearchText(for request: ItemCollectionProjectionRequest) -> String {
request.searchText.trimmingCharacters(in: .whitespacesAndNewlines)
}
nonisolated private static func sortComparator(for mode: ItemSortMode) -> (MinecraftContentItem, MinecraftContentItem) -> Bool {
switch mode {
case .name:
return { lhs, rhs in
lhs.displayName.localizedStandardCompare(rhs.displayName) == .orderedAscending
}
case .modifiedDate:
return { lhs, rhs in
switch (lhs.displayDate, rhs.displayDate) {
case let (lhsDate?, rhsDate?):
if lhsDate != rhsDate {
return lhsDate > rhsDate
}
case (.some, nil):
return true
case (nil, .some):
return false
case (nil, nil):
break
}
return lhs.displayName.localizedStandardCompare(rhs.displayName) == .orderedAscending
}
case .size:
return { lhs, rhs in
switch (lhs.sizeBytes, rhs.sizeBytes) {
case let (lhsSize?, rhsSize?):
if lhsSize != rhsSize {
return lhsSize > rhsSize
}
case (.some, nil):
return true
case (nil, .some):
return false
case (nil, nil):
break
}
return lhs.displayName.localizedStandardCompare(rhs.displayName) == .orderedAscending
}
}
}
}