Skip to content

Commit

Permalink
Adds support for grouping favorites.
Browse files Browse the repository at this point in the history
  • Loading branch information
mntone committed Dec 30, 2023
1 parent 41e7672 commit 86e95a4
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 78 deletions.
31 changes: 22 additions & 9 deletions src/App/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {

},
"+" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -994,6 +991,22 @@
}
}
},
"All Monsters" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "All Monsters"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "すべてのモンスター"
}
}
}
},
"Amphibian" : {
"comment" : "text.type[amphibian]",
"extractionState" : "manual",
Expand Down Expand Up @@ -3607,34 +3620,34 @@
}
}
},
"Favorite" : {
"Favorited" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Favorite"
"value" : "Favorited"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "お気に入り"
"value" : "お気に入り済み"
}
}
}
},
"Favorited" : {
"Favorites" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Favorited"
"value" : "Favorites"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "お気に入り済み"
"value" : "お気に入り"
}
}
}
Expand Down
13 changes: 4 additions & 9 deletions src/App/ViewModels/GameGroupViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import struct SwiftUI.LocalizedStringKey

@available(watchOS, unavailable)
enum GameGroupType: Hashable {
case inGame
case byName
Expand All @@ -16,7 +14,6 @@ enum GameGroupType: Hashable {
}
}

@available(watchOS, unavailable)
struct GameGroupViewModel: Identifiable {
let gameID: String
let type: GameGroupType
Expand All @@ -29,11 +26,11 @@ struct GameGroupViewModel: Identifiable {
self.type = type
switch type {
case .inGame, .byName:
self.label = String(localized: "")
self.sortkey = ""
self.label = String(localized: "All Monsters")
self.sortkey = "1"
case .favorite:
self.label = String(localized: "Favorite")
self.sortkey = ""
self.label = String(localized: "Favorites")
self.sortkey = "0"
case let .type(id):
let baseKey = id.replacingOccurrences(of: "_", with: " ").capitalized
self.label = String(localized: String.LocalizationValue(baseKey))
Expand All @@ -50,14 +47,12 @@ struct GameGroupViewModel: Identifiable {
}
}

@available(watchOS, unavailable)
extension GameGroupViewModel: Equatable {
static func == (lhs: GameGroupViewModel, rhs: GameGroupViewModel) -> Bool {
lhs.type == rhs.type && lhs.gameID == rhs.gameID && lhs.items == rhs.items
}
}

@available(watchOS, unavailable)
extension GameGroupViewModel: Comparable {
static func <(lhs: GameGroupViewModel, rhs: GameGroupViewModel) -> Bool {
lhs.sortkey < rhs.sortkey
Expand Down
157 changes: 103 additions & 54 deletions src/App/ViewModels/GameViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,8 @@ import MonsterAnalyzerCore
final class GameViewModel: ObservableObject, Identifiable {
private let game: Game

#if os(watchOS)
@Published
private(set) var state: StarSwingsState<[GameItemViewModel]> = .ready
#else
@Published
private(set) var state: StarSwingsState<[GameGroupViewModel]> = .ready
#endif

@Published
var sort: Sort {
Expand All @@ -28,68 +23,122 @@ final class GameViewModel: ObservableObject, Identifiable {
self.sort = game.app?.settings.sort ?? .inGame

let getState = game.$state
// Create view model from domain model
.mapData { monsters in
monsters.map(GameItemViewModel.init)
}
let updateSearchText = Just("").merge(with: $searchText.debounce(for: 0.333, scheduler: DispatchQueue.global(qos: .userInitiated)))
#if os(watchOS)
getState.combineLatest(updateSearchText) { (state: StarSwingsState<[GameItemViewModel]>, searchText: String) -> StarSwingsState<[GameItemViewModel]> in
state.mapData { monsters in
Self.filter(searchText, from: monsters, languageService: game.languageService)
}
}
.receive(on: DispatchQueue.main)
.assign(to: &$state)
#else
getState.combineLatest($sort) { (state: StarSwingsState<[GameItemViewModel]>, sort: Sort) -> StarSwingsState<[GameGroupViewModel]> in
state.mapData { (monsters: [GameItemViewModel]) -> [GameGroupViewModel] in
let groups: [GameGroupViewModel]
switch sort {
case .inGame:
groups = [GameGroupViewModel(gameID: game.id, type: .inGame, items: monsters)]
case .name:
groups = [GameGroupViewModel(gameID: game.id, type: .byName, items: monsters.sorted())]
case .type:
groups = monsters
.reduce(into: [:]) { (result: inout [String: [GameItemViewModel]], next: GameItemViewModel) in
if let items = result[next.type] {
result[next.type] = items + [next]
} else {
result[next.type] = [next]

// Favorite Group
let favorites = getState
.flatMap { state in
switch state {
case let .complete(data: monsters):
return monsters
.sorted()
.map { monster in
monster.$isFavorited.map { favorited -> GameItemViewModel? in
favorited ? monster : nil
}
}
.map { id, items in
GameGroupViewModel(gameID: game.id, type: .type(id: id), items: items.sorted())
.combineLatest
.map { monsters -> [GameItemViewModel] in
monsters.compactMap { monster in
monster
}
}
.sorted()
.eraseToAnyPublisher()
default:
return Just<[GameItemViewModel]>([])
.eraseToAnyPublisher()
}
return groups
}
}
.combineLatest($searchText) { (state: StarSwingsState<[GameGroupViewModel]>, searchText: String) -> StarSwingsState<[GameGroupViewModel]> in
state.mapData { groups in
groups.compactMap { group in
let monsters = Self.filter(searchText, from: group.items, languageService: game.languageService)
guard !monsters.isEmpty else {
return nil
.map { items -> GameGroupViewModel? in
guard !items.isEmpty else { return nil }

return GameGroupViewModel(gameID: game.id, type: .favorite, items: items)
}

// Search Text
let searchText = Just("")
.merge(with: $searchText.debounce(for: 0.333, scheduler: DispatchQueue.global(qos: .userInitiated)))
.removeDuplicates()

// All Groups
getState
#if os(watchOS)
// Merge the fav group into groups
.combineLatest(favorites) { (state: StarSwingsState<[GameItemViewModel]>, fav: GameGroupViewModel?) -> StarSwingsState<[GameGroupViewModel]> in
state.mapData { monsters in
let groupsExceptFav = GameGroupViewModel(gameID: game.id, type: .inGame, items: monsters)

let groups: [GameGroupViewModel]
if let fav {
groups = [fav, groupsExceptFav]
} else {
groups = [groupsExceptFav]
}
return GameGroupViewModel(gameID: group.gameID, type: group.type, items: monsters)
return groups
}
}
}
.removeDuplicates { prev, cur in
switch (prev, cur) {
case (.ready, .ready), (.loading, .loading):
return true
case let (.complete(prevData), .complete(curData)):
return prevData == curData
default:
return false
#else
// Merge the fav group into sorted or splited groups
.combineLatest(favorites, $sort) { (state: StarSwingsState<[GameItemViewModel]>, fav: GameGroupViewModel?, sort: Sort) -> StarSwingsState<[GameGroupViewModel]> in
state.mapData { (monsters: [GameItemViewModel]) -> [GameGroupViewModel] in
let groups: [GameGroupViewModel]
switch sort {
case .inGame:
let groupsExceptFav = GameGroupViewModel(gameID: game.id, type: .inGame, items: monsters)
if let fav {
groups = [fav, groupsExceptFav]
} else {
groups = [groupsExceptFav]
}
case .name:
let groupsExceptFav = GameGroupViewModel(gameID: game.id, type: .byName, items: monsters.sorted())
if let fav {
groups = [fav, groupsExceptFav]
} else {
groups = [groupsExceptFav]
}
case .type:
let otherGroups = monsters
.reduce(into: [:]) { (result: inout [String: [GameItemViewModel]], next: GameItemViewModel) in
if let items = result[next.type] {
result[next.type] = items + [next]
} else {
result[next.type] = [next]
}
}
.map { id, items in
GameGroupViewModel(gameID: game.id, type: .type(id: id), items: items.sorted())
}
.sorted()

if let fav {
groups = [fav] + otherGroups
} else {
groups = otherGroups
}
}
return groups
}
}
}
.receive(on: DispatchQueue.main)
.assign(to: &$state)
#endif
// Filter search word
.combineLatest(searchText) { (state: StarSwingsState<[GameGroupViewModel]>, searchText: String) -> StarSwingsState<[GameGroupViewModel]> in
state.mapData { groups in
groups.compactMap { group in
let monsters = Self.filter(searchText, from: group.items, languageService: game.languageService)
guard !monsters.isEmpty else {
return nil
}
return GameGroupViewModel(gameID: group.gameID, type: group.type, items: monsters)
}
}
}
// Receive on the main dispatcher
.receive(on: DispatchQueue.main)
.assign(to: &$state)

game.fetchIfNeeded()
}
Expand Down
6 changes: 0 additions & 6 deletions src/App/Views/GamePage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ struct GamePage<ItemView: View>: View {

@ViewBuilder
private var list: some View {
#if os(watchOS)
List(viewModel.state.data ?? []) { item in
content(item)
}
#else
let items = viewModel.state.data ?? []
if items.count > 1 || items.first?.type.isType == true {
List(items) { group in
Expand All @@ -34,7 +29,6 @@ struct GamePage<ItemView: View>: View {
content(item)
}
}
#endif
}

var body: some View {
Expand Down
Loading

0 comments on commit 86e95a4

Please sign in to comment.