From b167dd474ef53e0a26a445c9bb8fcf7423455fce Mon Sep 17 00:00:00 2001 From: blackxfiied <41133734+blackxfiied@users.noreply.github.com> Date: Sun, 19 May 2024 08:15:34 +0800 Subject: [PATCH] (finally!!) add favourites to homeview --- Mythic.xcodeproj/project.pbxproj | 4 + Mythic/Extensions/Global.swift | 12 +- Mythic/Localizable.xcstrings | 4 + .../Views/GameListEvo/CompactGameCard.swift | 130 ++++++++++++++++++ Mythic/Views/GameListEvo/GameCard.swift | 3 +- Mythic/Views/GameListEvo/GameListEvo.swift | 2 - Mythic/Views/Navigation/Home.swift | 26 ++-- 7 files changed, 164 insertions(+), 17 deletions(-) create mode 100644 Mythic/Views/GameListEvo/CompactGameCard.swift diff --git a/Mythic.xcodeproj/project.pbxproj b/Mythic.xcodeproj/project.pbxproj index 57af60f6..828b1deb 100644 --- a/Mythic.xcodeproj/project.pbxproj +++ b/Mythic.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ 6ADF5ABB2B98B9710041D04E /* UninstallGameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ADF5ABA2B98B9710041D04E /* UninstallGameView.swift */; }; 6ADFD97B2B975B3300557086 /* GameCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ADFD97A2B975B3300557086 /* GameCard.swift */; }; 6AE4A9A32B44682E0060D483 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE4A9A22B44682E0060D483 /* Lock.swift */; }; + 6AE842122BF96D0400016A58 /* CompactGameCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE842112BF96D0400016A58 /* CompactGameCard.swift */; }; 6AEF7BC62BA6D873002FFE26 /* Downloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AEF7BC52BA6D873002FFE26 /* Downloads.swift */; }; 6AF0EB1F2AAD4E0A0044C09C /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AF0EB1E2AAD4E0A0044C09C /* Onboarding.swift */; }; 6AF0EB212AAD4F1E0044C09C /* NotImplemented.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AF0EB202AAD4F1E0044C09C /* NotImplemented.swift */; }; @@ -151,6 +152,7 @@ 6ADF5ABA2B98B9710041D04E /* UninstallGameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UninstallGameView.swift; sourceTree = ""; }; 6ADFD97A2B975B3300557086 /* GameCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameCard.swift; sourceTree = ""; }; 6AE4A9A22B44682E0060D483 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = ""; }; + 6AE842112BF96D0400016A58 /* CompactGameCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactGameCard.swift; sourceTree = ""; }; 6AEF7BC52BA6D873002FFE26 /* Downloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloads.swift; sourceTree = ""; }; 6AF0EB1E2AAD4E0A0044C09C /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = ""; }; 6AF0EB202AAD4F1E0044C09C /* NotImplemented.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotImplemented.swift; sourceTree = ""; }; @@ -520,6 +522,7 @@ 6ADF5ABA2B98B9710041D04E /* UninstallGameView.swift */, 6A02BDE92B99FC2900B70656 /* InstallGameView.swift */, 6A6D03ED2B9EFBD30064CEBB /* GameSettingsView.swift */, + 6AE842112BF96D0400016A58 /* CompactGameCard.swift */, ); path = GameListEvo; sourceTree = ""; @@ -694,6 +697,7 @@ 6AD506562AAFF671008A28C1 /* Home.swift in Sources */, 6A0BB7AB2B501A2200C8A693 /* LocalImport.swift in Sources */, 6A1C6F462AEFD9B100F89AE7 /* WineInterfaceExt.swift in Sources */, + 6AE842122BF96D0400016A58 /* CompactGameCard.swift in Sources */, 6AA6CCE72AD571C500F664A5 /* LegendaryInterfaceExt.swift in Sources */, 6AD5065C2AAFF6F0008A28C1 /* Support.swift in Sources */, 6A1322382B1C98AE0049BB64 /* InstallStatus.swift in Sources */, diff --git a/Mythic/Extensions/Global.swift b/Mythic/Extensions/Global.swift index 3dc31a75..3111a0bb 100644 --- a/Mythic/Extensions/Global.swift +++ b/Mythic/Extensions/Global.swift @@ -36,6 +36,8 @@ let mainLock: NSRecursiveLock = .init() let discordRPC: SwordRPC = .init(appId: "1191343317749870712") // Mythic's discord application ID +let unifiedGames = (LocalGames.library ?? []) + ((try? Legendary.getInstallable()) ?? []) + struct UnknownError: LocalizedError { var errorDescription: String? = "An unknown error occurred." } @@ -70,16 +72,18 @@ class Game: ObservableObject, Hashable, Codable, Identifiable, Equatable { hasher.combine(id) hasher.combine(platform) hasher.combine(imageURL) + hasher.combine(wideImageURL) hasher.combine(path) } // MARK: Initializer - init(type: GameType, title: String, id: String? = nil, platform: GamePlatform? = nil, imageURL: URL? = nil, path: String? = nil) { + init(type: GameType, title: String, id: String? = nil, platform: GamePlatform? = nil, imageURL: URL? = nil, wideImageURL: URL? = nil, path: String? = nil) { self.type = type self.title = title self.id = id ?? UUID().uuidString self.platform = platform self.imageURL = imageURL + self.wideImageURL = wideImageURL self.path = path } @@ -99,6 +103,12 @@ class Game: ObservableObject, Hashable, Codable, Identifiable, Equatable { get { _imageURL ?? (self.type == .epic ? .init(string: Legendary.getImage(of: self, type: .tall)) : nil) } set { _imageURL = newValue } } + + private var _wideImageURL: URL? + var wideImageURL: URL? { + get { _imageURL ?? (self.type == .epic ? .init(string: Legendary.getImage(of: self, type: .normal)) : nil) } + set { _imageURL = newValue } + } private var _path: String? var path: String? { diff --git a/Mythic/Localizable.xcstrings b/Mythic/Localizable.xcstrings index 60072cc5..4e05676d 100644 --- a/Mythic/Localizable.xcstrings +++ b/Mythic/Localizable.xcstrings @@ -12814,6 +12814,9 @@ } } } + }, + "Enter New Thumbnail URL here..." : { + }, "Enter the 'authorisationCode' from the JSON response in the field below." : { "localizations" : { @@ -15673,6 +15676,7 @@ } }, "Favourites (Not implemented yet)" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { diff --git a/Mythic/Views/GameListEvo/CompactGameCard.swift b/Mythic/Views/GameListEvo/CompactGameCard.swift new file mode 100644 index 00000000..57b23596 --- /dev/null +++ b/Mythic/Views/GameListEvo/CompactGameCard.swift @@ -0,0 +1,130 @@ +// +// CompactGameCard.swift +// Mythic +// +// Created by Esiayo Alegbe on 19/5/2024. +// + +import SwiftUI +import CachedAsyncImage + +struct CompactGameCard: View { + @Binding var game: Game + + @EnvironmentObject var networkMonitor: NetworkMonitor + @ObservedObject private var operation: GameOperation = .shared + @AppStorage("minimiseOnGameLaunch") private var minimizeOnGameLaunch: Bool = false + + @State private var isLaunchErrorAlertPresented: Bool = false + @State private var launchError: Error? + + var body: some View { + RoundedRectangle(cornerRadius: 20) + .fill(.background) + .aspectRatio(1, contentMode: .fit) + .overlay { // MARK: Image + CachedAsyncImage(url: game.wideImageURL ?? game.imageURL) { phase in + switch phase { + case .empty: + if case .local = game.type, game.imageURL == nil { + let image = Image(nsImage: workspace.icon(forFile: game.path ?? .init())) + + image + .resizable() + .aspectRatio(1, contentMode: .fill) + .blur(radius: 20.0) + } else { + RoundedRectangle(cornerRadius: 20) + .fill(.windowBackground) + .shimmering( + animation: .easeInOut(duration: 1) + .repeatForever(autoreverses: false), + bandSize: 1 + ) + } + case .success(let image): + image + .resizable() + .aspectRatio(1, contentMode: .fill) + .clipShape(.rect(cornerRadius: 20)) + .blur(radius: 20.0) + case .failure: + // fallthrough + RoundedRectangle(cornerRadius: 20) + .fill(.windowBackground) + @unknown default: + RoundedRectangle(cornerRadius: 20) + .fill(.windowBackground) + } + } + } + .overlay(alignment: .bottom) { + VStack { + // MARK: Game Title Stack + HStack { + Text(game.title) + .font(.bold(.title3)()) + + Spacer() + + // ! Changes made here must also be reflected in GameCard's play button + if operation.launching == game { + ProgressView() + .controlSize(.small) + .padding(5) + .clipShape(.circle) + + } else { + Button { + Task(priority: .userInitiated) { + do { + switch game.type { + case .epic: + try await Legendary.launch( + game: game, + online: networkMonitor.isEpicAccessible + ) + case .local: + try await LocalGames.launch(game: game) + } + + if minimizeOnGameLaunch { NSApp.windows.first?.miniaturize(nil) } + } catch { + launchError = error + isLaunchErrorAlertPresented = true + } + } + } label: { + Image(systemName: "play") + .padding(5) + } + .clipShape(.circle) + .help(game.path != nil ? "Play \"\(game.title)\"" : "Unable to locate \(game.title) at its specified path (\(game.path ?? "Unknown"))") + .disabled(game.path != nil ? !files.fileExists(atPath: game.path!) : false) + .disabled(operation.runningGames.contains(game)) + .alert(isPresented: $isLaunchErrorAlertPresented) { + Alert( + title: .init("Error launching \"\(game.title)\"."), + message: .init(launchError?.localizedDescription ?? "Unknown Error.") + ) + } + } + } + .padding(.horizontal) + } + .padding(.bottom) + .scaledToFit() + } + .overlay(alignment: .topLeading) { + if game.isFavourited { + Image(systemName: "star.fill") + .padding() + } + } + } +} + +#Preview { + CompactGameCard(game: .constant(.init(type: .epic, title: "firtbite;", wideImageURL: .init(string: "https://i.imgur.com/CZt2F4s.png")))) + .padding() +} diff --git a/Mythic/Views/GameListEvo/GameCard.swift b/Mythic/Views/GameListEvo/GameCard.swift index 5c4ee781..4aef20c4 100644 --- a/Mythic/Views/GameListEvo/GameCard.swift +++ b/Mythic/Views/GameListEvo/GameCard.swift @@ -90,7 +90,6 @@ struct GameCard: View { Text(game.title) .font(.bold(.title3)()) // .foregroundStyle(.white) - .padding(.leading) SubscriptedTextView(game.type.rawValue) @@ -101,6 +100,7 @@ struct GameCard: View { Spacer() } + .padding(.leading) // MARK: Button Stack HStack { @@ -156,6 +156,7 @@ struct GameCard: View { .help("Game verification is required for \"\(game.title)\".") } else { // MARK: Play Button + // ! Changes made here must also be reflected in CompactGameCard's play button if operation.launching == game { ProgressView() .controlSize(.small) diff --git a/Mythic/Views/GameListEvo/GameListEvo.swift b/Mythic/Views/GameListEvo/GameListEvo.swift index ee98f0bc..0934957d 100644 --- a/Mythic/Views/GameListEvo/GameListEvo.swift +++ b/Mythic/Views/GameListEvo/GameListEvo.swift @@ -13,8 +13,6 @@ struct GameListEvo: View { @State private var isGameImportViewPresented: Bool = false - private let unifiedGames = (LocalGames.library ?? []) + ((try? Legendary.getInstallable()) ?? []) - private var games: [Game] { return unifiedGames .filter { diff --git a/Mythic/Views/Navigation/Home.swift b/Mythic/Views/Navigation/Home.swift index c042198d..2e0f71c0 100644 --- a/Mythic/Views/Navigation/Home.swift +++ b/Mythic/Views/Navigation/Home.swift @@ -49,8 +49,6 @@ struct HomeView: View { @State private var isAlertPresented: Bool = false @State private var activeAlert: ActiveAlert = .launchError - @State private var animateStar: Bool = false - let animateStarTimer = Timer.publish(every: 2, on: .main, in: .common).autoconnect() // why on god's green earth is it so lengthy on swift to repeat something every 2 seconds @Environment(\.colorScheme) var colorScheme // MARK: - Body @@ -67,18 +65,20 @@ struct HomeView: View { VStack { // MARK: View 1 (Top) VStack { - Image(systemName: animateStar ? "star.fill" : "calendar.badge.clock") - .resizable() - .symbolRenderingMode(.palette) - .contentTransition(.symbolEffect(.replace.upUp.byLayer)) - .foregroundStyle(animateStar ? .yellow : .yellow, (colorScheme == .light ? .black : .white)) - .aspectRatio(contentMode: .fit) - .frame(width: 35, height: 35) - .onReceive(animateStarTimer) { _ in - animateStar.toggle() + if !unifiedGames.filter({ $0.isFavourited == true }).isEmpty { + ScrollView(.horizontal) { + LazyHStack { + ForEach(unifiedGames.filter({ $0.isFavourited == true }), id: \.self) { game in + CompactGameCard(game: .constant(game)) + } + } } - - Text("Favourites (Not implemented yet)") + } else { + HStack { + Image(systemName: "star.fill") + Text("No games are favourited.") + } + } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.background)