diff --git a/Mythic.xcodeproj/project.pbxproj b/Mythic.xcodeproj/project.pbxproj index 01078e28..4854b68e 100644 --- a/Mythic.xcodeproj/project.pbxproj +++ b/Mythic.xcodeproj/project.pbxproj @@ -703,7 +703,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2754; + CURRENT_PROJECT_VERSION = 2780; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Mythic/Preview Content\""; DEVELOPMENT_TEAM = 67ZBY275P8; @@ -748,7 +748,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2754; + CURRENT_PROJECT_VERSION = 2780; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Mythic/Preview Content\""; DEVELOPMENT_TEAM = 67ZBY275P8; diff --git a/Mythic/AppDelegate.swift b/Mythic/AppDelegate.swift index 2b449ff3..39086087 100644 --- a/Mythic/AppDelegate.swift +++ b/Mythic/AppDelegate.swift @@ -89,7 +89,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { // https://arc.net/l/quote/ alert.addButton(withTitle: "Move") alert.addButton(withTitle: "Cancel") - if case .alertFirstButtonReturn = alert.runModal(), let optimalAppURL = optimalAppURL { + if let window = NSApp.windows.first, + let optimalAppURL = optimalAppURL, + case .alertFirstButtonReturn = alert.beginSheetModal(for: window) { do { _ = try files.replaceItemAt(optimalAppURL, withItemAt: currentAppURL) workspace.open(optimalAppURL) @@ -124,29 +126,41 @@ class AppDelegate: NSObject, NSApplicationDelegate { // https://arc.net/l/quote/ if Engine.needsUpdate() == true { let alert = NSAlert() alert.messageText = "Time for an update!" - alert.informativeText = "A new Mythic Engine update has been pushed." + alert.informativeText = "A new Mythic Engine update has released." alert.addButton(withTitle: "Update") alert.addButton(withTitle: "Cancel") - if case .alertFirstButtonReturn = alert.runModal() { - let confirmation = NSAlert() - confirmation.messageText = "Are you sure you want to update now?" - confirmation.informativeText = "Updating will remove the current version of Mythic Engine before installing the new one." - confirmation.addButton(withTitle: "Update") - confirmation.addButton(withTitle: "Cancel") - - if case .alertFirstButtonReturn = confirmation.runModal() { - do { - try Engine.remove() - let app = MythicApp() // FIXME: is this dangerous or just stupid - app.onboardingPhase = .engineDisclaimer - app.isOnboardingPresented = true - } catch { - let error = NSAlert() - error.messageText = "Unable to remove Mythic Engine." - error.addButton(withTitle: "Quit") - if case .OK = error.runModal() { - exit(1) + alert.showsHelp = true + + if let window = NSApp.windows.first { // no alternative ATM, swift compiler is clueless. + alert.beginSheetModal(for: window) { response in + if case .alertFirstButtonReturn = response { + let confirmation = NSAlert() + confirmation.messageText = "Are you sure you want to update now?" + confirmation.informativeText = "Updating will remove the current version of Mythic Engine before installing the new one." + confirmation.addButton(withTitle: "Update") + confirmation.addButton(withTitle: "Cancel") + + confirmation.beginSheetModal(for: window) { response in + if case .alertFirstButtonReturn = response { + do { + try Engine.remove() + let app = MythicApp() // FIXME: is this dangerous or just stupid + app.onboardingPhase = .engineDisclaimer + app.isOnboardingPresented = true + } catch { + let error = NSAlert() + error.alertStyle = .critical + error.messageText = "Unable to remove Mythic Engine." + error.addButton(withTitle: "Quit") + + error.beginSheetModal(for: window) { response in + if case .OK = response { + exit(1) + } + } + } + } } } } @@ -155,20 +169,28 @@ class AppDelegate: NSObject, NSApplicationDelegate { // https://arc.net/l/quote/ } func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply { + var terminateReply: NSApplication.TerminateReply = .terminateNow + if GameOperation.shared.current != nil || !GameOperation.shared.queue.isEmpty { let alert = NSAlert() alert.messageText = "Are you sure you want to quit?" alert.informativeText = "Mythic is still modifying games." + alert.alertStyle = .warning alert.addButton(withTitle: "Quit") alert.addButton(withTitle: "Cancel") - if alert.runModal() == .alertFirstButtonReturn { - return .terminateNow - } else { - return .terminateCancel + + if let window = NSApp.windows.first { + alert.beginSheetModal(for: window) { response in + if case .alertFirstButtonReturn = response { + terminateReply = .terminateNow + } else { + terminateReply = .terminateLater + } + } } } - return .terminateNow + return terminateReply } func applicationWillTerminate(_: Notification) { diff --git a/Mythic/Localizable.xcstrings b/Mythic/Localizable.xcstrings index 5265b21f..5b91cc6e 100644 --- a/Mythic/Localizable.xcstrings +++ b/Mythic/Localizable.xcstrings @@ -39245,4 +39245,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Mythic/Utilities/Game.swift b/Mythic/Utilities/Game.swift index 4f97ef26..c809771d 100644 --- a/Mythic/Utilities/Game.swift +++ b/Mythic/Utilities/Game.swift @@ -10,6 +10,7 @@ import Combine import OSLog import UserNotifications import SwordRPC +import SwiftUI /// Enumeration containing the two different game platforms available. enum GamePlatform: String, CaseIterable, Codable, Hashable { @@ -89,10 +90,15 @@ class Game: ObservableObject, Hashable, Codable, Identifiable, Equatable { var bottleURL: URL? { get { let key: String = id.appending("_bottleURL") - if !Wine.bottleURLs.isEmpty { - defaults.register(defaults: [key: Wine.bottleURLs.first!]) + if let url = defaults.url(forKey: key), !Wine.bottleExists(bottleURL: url) { + defaults.removeObject(forKey: key) } - return defaults.url(forKey: key) ?? Wine.bottleURLs.first + + if defaults.url(forKey: key) == nil { + defaults.set(Wine.bottleURLs.first, forKey: key) + } + + return defaults.url(forKey: key) } set { let key: String = id.appending("_bottleURL") @@ -211,16 +217,17 @@ class GameOperation: ObservableObject { trigger: nil) ) } catch { - try await notifications.add( - .init(identifier: UUID().uuidString, - content: { - let content = UNMutableNotificationContent() - content.title = "Error \(GameOperation.shared.current?.type.rawValue ?? "modifying") \"\(GameOperation.shared.current?.game.title ?? "Unknown")\"." - content.body = error.localizedDescription - return content - }(), - trigger: nil) - ) + DispatchQueue.main.async { + let alert = NSAlert() + alert.messageText = "Error \(GameOperation.shared.current?.type.rawValue ?? "modifying") \"\(GameOperation.shared.current?.game.title ?? "Unknown")\"." + alert.informativeText = error.localizedDescription + alert.alertStyle = .warning + alert.addButton(withTitle: "OK") + + if let window = NSApp.windows.first { + alert.beginSheetModal(for: window) + } + } } DispatchQueue.main.asyncAndWait { diff --git a/Mythic/Utilities/Legendary/LegendaryInterface.swift b/Mythic/Utilities/Legendary/LegendaryInterface.swift index 7645c1b9..20a60b8b 100644 --- a/Mythic/Utilities/Legendary/LegendaryInterface.swift +++ b/Mythic/Utilities/Legendary/LegendaryInterface.swift @@ -337,7 +337,7 @@ final class Legendary { */ static func launch(game: Mythic.Game, online: Bool) async throws { guard try Legendary.getInstalledGames().contains(game) else { - log.error("Unable to launch game, not installed or missing") // TODO: add alert in unified alert system + log.error("Unable to launch game, not installed or missing") throw GameDoesNotExistError(game) } diff --git a/Mythic/Utilities/Local/LocalGames.swift b/Mythic/Utilities/Local/LocalGames.swift index 2e33a4ef..0a419531 100644 --- a/Mythic/Utilities/Local/LocalGames.swift +++ b/Mythic/Utilities/Local/LocalGames.swift @@ -40,7 +40,7 @@ final class LocalGames { guard let library = library, library.contains(game) else { - log.error("Unable to launch local game, not installed or missing") // TODO: add alert in unified alert system + log.error("Unable to launch local game, not installed or missing") throw GameDoesNotExistError(game) } diff --git a/Mythic/Views/Navigation/DownloadsEvo.swift b/Mythic/Views/Navigation/DownloadsEvo.swift index 3b5960db..bfdfdc4e 100644 --- a/Mythic/Views/Navigation/DownloadsEvo.swift +++ b/Mythic/Views/Navigation/DownloadsEvo.swift @@ -24,7 +24,9 @@ struct DownloadsEvo: View { if let currentGame = operation.current?.game { VStack { DownloadCard(game: currentGame, style: .prominent) + Divider() + if operation.queue.isEmpty { Text("No other downloads are pending.") .bold() diff --git a/Mythic/Views/Navigation/Home.swift b/Mythic/Views/Navigation/Home.swift index 12067b85..db35d713 100644 --- a/Mythic/Views/Navigation/Home.swift +++ b/Mythic/Views/Navigation/Home.swift @@ -57,7 +57,7 @@ struct HomeView: View { if !unifiedGames.filter({ $0.isFavourited == true }).isEmpty { ScrollView(.horizontal) { LazyHGrid(rows: [.init(.adaptive(minimum: 115))]) { - ForEach(unifiedGames.filter({ $0.isFavourited == true }), id: \.self) { game in + ForEach(unifiedGames.filter({ $0.isFavourited == true })) { game in CompactGameCard(game: .constant(game)) .padding(5) } diff --git a/Mythic/Views/Navigation/Library/Library.swift b/Mythic/Views/Navigation/Library/Library.swift index fe263903..7cf688f9 100644 --- a/Mythic/Views/Navigation/Library/Library.swift +++ b/Mythic/Views/Navigation/Library/Library.swift @@ -25,14 +25,9 @@ struct LibraryView: View { // MARK: - State Variables @State private var isGameImportSheetPresented = false - @State private var legendaryStatus: JSON = JSON() - @State private var isDownloadsPopoverPresented: Bool = false - - @State private var searchText: String = .init() // MARK: - Body var body: some View { - // GameListView(isRefreshCalled: $isGameListRefreshCalled, searchText: $searchText) GameListEvo() .navigationTitle("Library") @@ -49,7 +44,7 @@ struct LibraryView: View { // MARK: Refresh Button Button { - _ = unifiedGames // getter updates computer property + } label: { Image(systemName: "arrow.clockwise") } diff --git a/Mythic/Views/Unified/BottleListView.swift b/Mythic/Views/Unified/BottleListView.swift index b96050b2..d6c3dd49 100644 --- a/Mythic/Views/Unified/BottleListView.swift +++ b/Mythic/Views/Unified/BottleListView.swift @@ -159,7 +159,7 @@ struct BottleConfigurationView: View { Button("Launch Registry Editor") { Task { try await Wine.command(arguments: ["regedit"], identifier: "regedit", bottleURL: bottle.url) { _ in } } Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in - registryEditorActive = (try? Process.execute("/bin/bash", arguments: ["-c", "ps aux | grep regedit.exe | grep -v grep"]))?.isEmpty == false // @isaacmarovitz has a better way + registryEditorActive = (try? Process.execute("/bin/bash", arguments: ["-c", "ps aux | grep regedit.exe | grep -v grep"]))?.isEmpty == false // TODO: tasklist if !registryEditorActive { timer.invalidate() } } } diff --git a/Mythic/Views/Unified/GameListEvoView.swift b/Mythic/Views/Unified/GameListEvoView.swift index 9314b96b..304096cd 100644 --- a/Mythic/Views/Unified/GameListEvoView.swift +++ b/Mythic/Views/Unified/GameListEvoView.swift @@ -19,8 +19,21 @@ struct GameListEvo: View { searchString.isEmpty || $0.title.localizedCaseInsensitiveContains(searchString) } - .sorted(by: { $0.title < $1.title }) - .sorted(by: { $0.isFavourited && !$1.isFavourited }) + .sorted { + if $0.isFavourited != $1.isFavourited { + return $0.isFavourited && !$1.isFavourited + } + + if let games = try? Legendary.getInstalledGames(), games.contains($0) != games.contains($1) { + return games.contains($0) + } + + if let games = LocalGames.library, games.contains($0) != games.contains($1) { + return games.contains($0) + } + + return $0.title < $1.title + } } var body: some View { diff --git a/Mythic/Views/Unified/Models/GameCard.swift b/Mythic/Views/Unified/Models/GameCard.swift index 293e1245..9752238f 100644 --- a/Mythic/Views/Unified/Models/GameCard.swift +++ b/Mythic/Views/Unified/Models/GameCard.swift @@ -89,7 +89,7 @@ struct GameCard: View { HStack { Text(game.title) .font(.bold(.title3)()) - // .foregroundStyle(.white) + .foregroundStyle(.white) SubscriptedTextView(game.type.rawValue) @@ -132,19 +132,6 @@ struct GameCard: View { game: game, platform: game.platform!, type: .repair ) ) - - /* - do { - try await Legendary.install( - game: game, - platform: game.platform!, - type: .repair - ) - } catch { - Logger.app.error("Error repairing \(game.title): \(error.localizedDescription)") - // TODO: add repair error - } - */ } } label: { Image(systemName: "checkmark.circle.badge.questionmark") diff --git a/Mythic/Views/Unified/Sheets/InstallGameView.swift b/Mythic/Views/Unified/Sheets/InstallGameView.swift index 3604a319..a4d8b451 100644 --- a/Mythic/Views/Unified/Sheets/InstallGameView.swift +++ b/Mythic/Views/Unified/Sheets/InstallGameView.swift @@ -16,6 +16,8 @@ struct InstallViewEvo: View { @State var selectedOptionalPacks: Set = .init() @State var fetchingOptionalPacks: Bool = true + @State var installSize: Double? + @State private var supportedPlatforms: [GamePlatform]? @State var platform: GamePlatform = .macOS @@ -43,6 +45,12 @@ struct InstallViewEvo: View { } } } + + if output.stderr.contains("Install size:") { + if let match = try? Regex(#"Install size: (\d+(\.\d+)?) MiB"#).firstMatch(in: output.stderr) { + installSize = Double(match[1].substring ?? "") ?? 0.0 + } + } } fetchingOptionalPacks = false @@ -167,6 +175,12 @@ struct InstallViewEvo: View { Spacer() HStack { + if let installSize = installSize { + Text("\(String(format: "%.2f", Double(installSize * (1000000 / 1048576)) / (installSize > 1024 ? 1024 : 1))) \(installSize > 1024 ? "GB" : "MB")") + .font(.footnote) + .foregroundStyle(.placeholder) + } + if fetchingOptionalPacks { ProgressView() .controlSize(.small)