274 lines
8.1 KiB
Swift
274 lines
8.1 KiB
Swift
//
|
|
// CheatBaseView.swift
|
|
// Hthik
|
|
//
|
|
// Created by Hthik on 1/17/23.
|
|
// Copyright © 2024 Hthik
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
import enum SQLite.Result
|
|
|
|
@available(iOS 14, *)
|
|
extension CheatBaseView
|
|
{
|
|
private class ViewModel: ObservableObject
|
|
{
|
|
@Published
|
|
var allCheats: [CheatMetadata]? {
|
|
didSet {
|
|
guard let cheats = allCheats else {
|
|
self.cheatsByCategory = nil
|
|
return
|
|
}
|
|
|
|
let cheatsByCategory = Dictionary(grouping: cheats, by: { $0.category }).sorted { $0.key.id < $1.key.id }
|
|
self.cheatsByCategory = cheatsByCategory
|
|
}
|
|
}
|
|
|
|
@Published
|
|
private(set) var cheatsByCategory: [(CheatCategory, [CheatMetadata])]?
|
|
|
|
@Published
|
|
private(set) var error: Error?
|
|
|
|
@Published
|
|
var searchText: String = "" {
|
|
didSet {
|
|
self.searchCheats()
|
|
}
|
|
}
|
|
|
|
@Published
|
|
private(set) var filteredCheats: [CheatMetadata]?
|
|
|
|
@MainActor
|
|
func fetchCheats(for game: Game) async
|
|
{
|
|
guard self.allCheats == nil else { return }
|
|
|
|
do
|
|
{
|
|
let database = try CheatBase()
|
|
let cheats = try await database.cheats(for: game) ?? []
|
|
self.allCheats = cheats
|
|
}
|
|
catch
|
|
{
|
|
self.error = error
|
|
}
|
|
}
|
|
|
|
private func searchCheats()
|
|
{
|
|
if let cheats = self.allCheats, !self.searchText.isEmpty
|
|
{
|
|
let predicate = NSPredicate(forSearchingForText: self.searchText, inValuesForKeyPaths: [#keyPath(CheatMetadata.name), #keyPath(CheatMetadata.cheatDescription)])
|
|
|
|
let filteredCheats = cheats.filter { predicate.evaluate(with: $0) }
|
|
self.filteredCheats = filteredCheats
|
|
}
|
|
else
|
|
{
|
|
self.filteredCheats = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 14, *)
|
|
struct CheatBaseView: View
|
|
{
|
|
let game: Game?
|
|
|
|
var cancellationHandler: (() -> Void)?
|
|
var selectionHandler: ((CheatMetadata) -> Void)?
|
|
|
|
@StateObject
|
|
private var viewModel = ViewModel()
|
|
|
|
@State
|
|
private var activationHintCheat: CheatMetadata?
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
ZStack {
|
|
if let cheats = viewModel.allCheats, !cheats.isEmpty
|
|
{
|
|
// Only show List if there is at least one cheat for this game.
|
|
cheatList()
|
|
}
|
|
|
|
// Place above List
|
|
placeholderView()
|
|
}
|
|
.alert(item: $activationHintCheat) { cheat in
|
|
Alert(title: Text("How to Activate"),
|
|
message: Text(cheat.activationHint ?? ""),
|
|
dismissButton: .default(Text("OK")))
|
|
}
|
|
.navigationTitle(Text(game?.name ?? "CheatBase"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") {
|
|
cancellationHandler?()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
Task {
|
|
guard let game = self.game else { return }
|
|
await viewModel.fetchCheats(for: game)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func cheatList() -> some View
|
|
{
|
|
VStack {
|
|
if #unavailable(iOS 15)
|
|
{
|
|
LegacySearchBar(text: $viewModel.searchText)
|
|
}
|
|
|
|
let listView = List {
|
|
if let filteredCheats = viewModel.filteredCheats
|
|
{
|
|
ForEach(filteredCheats) { cheat in
|
|
cell(for: cheat)
|
|
}
|
|
}
|
|
else if let cheats = viewModel.cheatsByCategory
|
|
{
|
|
ForEach(cheats, id: \.0.id) { (category, cheats) in
|
|
Section {
|
|
DisclosureGroup {
|
|
ForEach(cheats) { cheat in
|
|
cell(for: cheat)
|
|
}
|
|
} label: {
|
|
Text(category.name)
|
|
}
|
|
} footer: {
|
|
Text(category.categoryDescription)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if #available(iOS 15, *)
|
|
{
|
|
listView.searchable(text: $viewModel.searchText)
|
|
}
|
|
else
|
|
{
|
|
listView
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
|
|
}
|
|
|
|
|
|
private func cell(for cheat: CheatMetadata) -> some View
|
|
{
|
|
ZStack(alignment: .leading) {
|
|
Button(action: { choose(cheat) }) {}
|
|
|
|
HStack {
|
|
// Name + Description
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(cheat.name)
|
|
|
|
if let description = cheat.cheatDescription
|
|
{
|
|
Text(description)
|
|
.font(.caption)
|
|
}
|
|
}
|
|
|
|
// Activation Hint
|
|
if cheat.activationHint != nil
|
|
{
|
|
Spacer()
|
|
|
|
Button(action: { activationHintCheat = cheat }) {
|
|
Image(systemName: "info.circle")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
}
|
|
}
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
}
|
|
|
|
private func placeholderView() -> some View
|
|
{
|
|
VStack(spacing: 8) {
|
|
if let error = viewModel.error
|
|
{
|
|
Text("Unable to Load Cheats")
|
|
.font(.title)
|
|
|
|
if let error = error as? SQLite.Result
|
|
{
|
|
// SQLite.Result implements CustomStringConvertible.description, but not localizedDescription.
|
|
Text(String(describing: error))
|
|
.font(.callout)
|
|
}
|
|
else
|
|
{
|
|
Text(error.localizedDescription)
|
|
.font(.callout)
|
|
}
|
|
}
|
|
else if let filteredCheats = viewModel.filteredCheats, filteredCheats.isEmpty
|
|
{
|
|
Text("Cheat Not Found")
|
|
.font(.title)
|
|
|
|
Text("Please make sure the name is correct, or try searching for another cheat.")
|
|
.font(.callout)
|
|
}
|
|
else if let cheats = viewModel.allCheats, cheats.isEmpty
|
|
{
|
|
Text("No Cheats")
|
|
.font(.title)
|
|
|
|
Text("There are no cheats for this game in Delta's CheatBase. Please try a different game.")
|
|
.font(.callout)
|
|
}
|
|
else if viewModel.allCheats == nil
|
|
{
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
}
|
|
}
|
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
|
|
.foregroundColor(.gray)
|
|
.padding()
|
|
}
|
|
|
|
init(game: Game, cheats: [CheatMetadata]? = nil)
|
|
{
|
|
self.game = game
|
|
|
|
let viewModel = ViewModel()
|
|
viewModel.allCheats = cheats
|
|
self._viewModel = StateObject(wrappedValue: viewModel)
|
|
}
|
|
}
|
|
|
|
@available(iOS 14, *)
|
|
private extension CheatBaseView
|
|
{
|
|
func choose(_ cheatMetadata: CheatMetadata)
|
|
{
|
|
self.selectionHandler?(cheatMetadata)
|
|
}
|
|
}
|