From fbdf086f1ebdd992a0ae2d80618bc59747537d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vini=CC=81cius=20Chagas?= Date: Sun, 29 Oct 2023 19:50:12 +0000 Subject: [PATCH 1/5] Protect access to programs list with a lock --- Whisky/Models/Bottle.swift | 21 ++++++++++++++----- .../Sources/WhiskyKit/Whisky/Bottle.swift | 2 ++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Whisky/Models/Bottle.swift b/Whisky/Models/Bottle.swift index 2cb5ef5c5..eb5e4e21f 100644 --- a/Whisky/Models/Bottle.swift +++ b/Whisky/Models/Bottle.swift @@ -92,14 +92,19 @@ extension Bottle { let programFilesx86 = url .appending(path: "drive_c") .appending(path: "Program Files (x86)") - programs.removeAll() + + lock.withLock { + programs.removeAll() + } let enumerator64 = FileManager.default.enumerator(at: programFiles, includingPropertiesForKeys: [.isExecutableKey], options: [.skipsHiddenFiles]) while let url = enumerator64?.nextObject() as? URL { if !url.hasDirectoryPath && url.pathExtension == "exe" { - programs.append(Program(name: url.lastPathComponent, url: url, bottle: self)) + lock.withLock { + programs.append(Program(name: url.lastPathComponent, url: url, bottle: self)) + } } } @@ -108,14 +113,20 @@ extension Bottle { options: [.skipsHiddenFiles]) while let url = enumerator32?.nextObject() as? URL { if !url.hasDirectoryPath && url.pathExtension == "exe" { - programs.append(Program(name: url.lastPathComponent, url: url, bottle: self)) + lock.withLock { + programs.append(Program(name: url.lastPathComponent, url: url, bottle: self)) + } } } // Apply blocklist - programs = programs.filter { !settings.blocklist.contains($0.url) } + lock.withLock { + programs = programs.filter { + !settings.blocklist.contains($0.url) + } + programs.sort { $0.name.lowercased() < $1.name.lowercased() } + } - programs.sort { $0.name.lowercased() < $1.name.lowercased() } return programs } diff --git a/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift b/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift index 5af1143a1..89867d8a6 100644 --- a/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift +++ b/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift @@ -42,6 +42,8 @@ public class Bottle: Hashable, Identifiable { self.inFlight = inFlight self.isActive = isActive } + + public let lock = NSLock() } extension Array where Element == Bottle { From 0aedc68e490c480cb486e592894480bd18243be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vini=CC=81cius=20Chagas?= Date: Sun, 29 Oct 2023 19:51:52 +0000 Subject: [PATCH 2/5] Don't reload bottle programs list unless necessary --- Whisky/Views/Bottle Views/ProgramsView.swift | 39 ++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/Whisky/Views/Bottle Views/ProgramsView.swift b/Whisky/Views/Bottle Views/ProgramsView.swift index e57d62dc1..e493330b0 100644 --- a/Whisky/Views/Bottle Views/ProgramsView.swift +++ b/Whisky/Views/Bottle Views/ProgramsView.swift @@ -68,26 +68,43 @@ struct ProgramsView: View { .animation(.easeInOut(duration: 0.2), value: isBlocklistExpanded) .navigationTitle("tab.programs") .onAppear { - programs = bottle.updateInstalledPrograms() - blocklist = bottle.settings.blocklist - sortPrograms() + loadPrograms() } .onChange(of: resortPrograms) { + sortPrograms() reloadStartMenu.toggle() + } + } + + func loadPrograms() { + if bottle.programs.isEmpty { + updatePrograms() + return + } + + programs = bottle.programs + blocklist = bottle.settings.blocklist + sortPrograms() + } + + private func updatePrograms() { + DispatchQueue(label: "whisky.lock.queue").async { programs = bottle.updateInstalledPrograms() blocklist = bottle.settings.blocklist sortPrograms() } } - func sortPrograms() { - var favourites = programs.filter { $0.pinned } - var nonFavourites = programs.filter { !$0.pinned } - favourites = favourites.sorted { $0.name < $1.name } - nonFavourites = nonFavourites.sorted { $0.name < $1.name } - programs.removeAll() - programs.append(contentsOf: favourites) - programs.append(contentsOf: nonFavourites) + private func sortPrograms() { + DispatchQueue(label: "whisky.lock.queue").async { + var favourites = programs.filter { $0.pinned } + var nonFavourites = programs.filter { !$0.pinned } + favourites = favourites.sorted { $0.name < $1.name } + nonFavourites = nonFavourites.sorted { $0.name < $1.name } + programs.removeAll() + programs.append(contentsOf: favourites) + programs.append(contentsOf: nonFavourites) + } } } From bf0e8afd71ac6610ede4f20be9b1c9a40f65fe0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vini=CC=81cius=20Chagas?= Date: Sun, 29 Oct 2023 19:52:45 +0000 Subject: [PATCH 3/5] Add loading indicator for programs list --- Whisky/Views/Bottle Views/ProgramsView.swift | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Whisky/Views/Bottle Views/ProgramsView.swift b/Whisky/Views/Bottle Views/ProgramsView.swift index e493330b0..9f0f09bf5 100644 --- a/Whisky/Views/Bottle Views/ProgramsView.swift +++ b/Whisky/Views/Bottle Views/ProgramsView.swift @@ -30,12 +30,13 @@ struct ProgramsView: View { @State var resortPrograms: Bool = false @State var isExpanded: Bool = true @State var isBlocklistExpanded: Bool = false + @State var isLoadingPrograms: Bool = false @Binding var reloadStartMenu: Bool @Binding var path: NavigationPath var body: some View { Form { - Section("program.title", isExpanded: $isExpanded) { + Section(isExpanded: $isExpanded) { List($programs, id: \.self, selection: $selectedPrograms) { $program in ProgramItemView(program: program, resortPrograms: $resortPrograms, @@ -47,7 +48,18 @@ struct ProgramsView: View { resortPrograms.toggle() } } + } header: { + HStack { + Text("program.title") + + if isLoadingPrograms { + ProgressView() + .scaleEffect(0.5) + .padding(.vertical, -16) + } + } } + Section("program.blocklist", isExpanded: $isBlocklistExpanded) { List($blocklist, id: \.self, selection: $selectedBlockitems) { $blockedUrl in BlocklistItemView(blockedUrl: blockedUrl, @@ -88,10 +100,13 @@ struct ProgramsView: View { } private func updatePrograms() { + isLoadingPrograms = true + DispatchQueue(label: "whisky.lock.queue").async { programs = bottle.updateInstalledPrograms() blocklist = bottle.settings.blocklist sortPrograms() + isLoadingPrograms = false } } @@ -130,8 +145,11 @@ struct ProgramItemView: View { .buttonStyle(.plain) .foregroundColor(isPinned ? .accentColor : .secondary) .opacity(isPinned ? 1 : showButtons ? 1 : 0) + Text(program.name) + Spacer() + if showButtons { if let peFile = program.peFile, let archString = peFile.architecture.toString() { @@ -143,6 +161,7 @@ struct ProgramItemView: View { .stroke(.secondary) ) } + Button { path.append(program) } label: { @@ -151,6 +170,7 @@ struct ProgramItemView: View { .buttonStyle(.plain) .foregroundStyle(.secondary) .help("program.config") + Button { Task { await program.run() From ee792f7af68c01b946935448a2a24509adbf33eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vini=CC=81cius=20Chagas?= Date: Sun, 29 Oct 2023 19:55:47 +0000 Subject: [PATCH 4/5] Avoid reloading bottle programs list in Start Menu unless necessary --- Whisky/Views/Bottle Views/BottleView.swift | 42 +++++++++++++------- Whisky/Views/Bottle Views/ProgramsView.swift | 2 +- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Whisky/Views/Bottle Views/BottleView.swift b/Whisky/Views/Bottle Views/BottleView.swift index 32ea01401..7bd74c9b7 100644 --- a/Whisky/Views/Bottle Views/BottleView.swift +++ b/Whisky/Views/Bottle Views/BottleView.swift @@ -34,6 +34,7 @@ struct BottleView: View { // This just provides a way to trigger a refresh @State var loadStartMenu: Bool = false @State var showWinetricksSheet: Bool = false + @State private var isLoadingInstalledPrograms: Bool = false private let gridLayout = [GridItem(.adaptive(minimum: 100, maximum: .infinity))] @@ -121,6 +122,7 @@ struct BottleView: View { } } .disabled(programLoading) + if programLoading { Spacer() .frame(width: 10) @@ -140,7 +142,7 @@ struct BottleView: View { case .config: ConfigView(bottle: $bottle) case .programs: - ProgramsView(bottle: bottle, + ProgramsView(bottle: $bottle, reloadStartMenu: $loadStartMenu, path: $path) } @@ -159,22 +161,32 @@ struct BottleView: View { } func updateStartMenu() { - bottle.programs = bottle.updateInstalledPrograms() - let startMenuPrograms = bottle.getStartMenuPrograms() - for startMenuProgram in startMenuPrograms { - for program in bottle.programs where - // For some godforsaken reason "foo/bar" != "foo/Bar" so... - program.url.path().caseInsensitiveCompare(startMenuProgram.url.path()) == .orderedSame { - program.pinned = true - if !bottle.settings.pins.contains(where: { $0.url == program.url }) { - bottle.settings.pins.append(PinnedProgram(name: program.name - .replacingOccurrences(of: ".exe", with: ""), - url: program.url)) + guard !isLoadingInstalledPrograms else { return } + + isLoadingInstalledPrograms = true + + DispatchQueue(label: "whisky.lock.queue").async { + if bottle.programs.isEmpty { + bottle.updateInstalledPrograms() + } + + let startMenuPrograms = bottle.getStartMenuPrograms() + for startMenuProgram in startMenuPrograms { + for program in bottle.programs where + // For some godforsaken reason "foo/bar" != "foo/Bar" so... + program.url.path().caseInsensitiveCompare(startMenuProgram.url.path()) == .orderedSame { + program.pinned = true + if !bottle.settings.pins.contains(where: { $0.url == program.url }) { + bottle.settings.pins.append(PinnedProgram(name: program.name + .replacingOccurrences(of: ".exe", with: ""), + url: program.url)) + } } } - } - pins = bottle.settings.pins + pins = bottle.settings.pins + isLoadingInstalledPrograms = false + } } } @@ -289,7 +301,7 @@ struct PinnedProgramView: View { } .onAppear { name = pin.name - Task.detached { + DispatchQueue(label: "whisky.lock.queue").async { program = bottle.programs.first(where: { $0.url == pin.url }) if let program { if let peFile = program.peFile { diff --git a/Whisky/Views/Bottle Views/ProgramsView.swift b/Whisky/Views/Bottle Views/ProgramsView.swift index 9f0f09bf5..8f052f252 100644 --- a/Whisky/Views/Bottle Views/ProgramsView.swift +++ b/Whisky/Views/Bottle Views/ProgramsView.swift @@ -20,7 +20,7 @@ import SwiftUI import WhiskyKit struct ProgramsView: View { - let bottle: Bottle + @Binding var bottle: Bottle @State var programs: [Program] = [] @State var blocklist: [URL] = [] @State private var selectedPrograms = Set() From bcdae7f0d08668020f36e7d01e15eda536c94375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vini=CC=81cius=20Chagas?= Date: Sun, 29 Oct 2023 19:56:48 +0000 Subject: [PATCH 5/5] Add loading indicator in Start Menu programs grid --- Whisky/Views/Bottle Views/BottleView.swift | 30 ++++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/Whisky/Views/Bottle Views/BottleView.swift b/Whisky/Views/Bottle Views/BottleView.swift index 7bd74c9b7..0eecb06ce 100644 --- a/Whisky/Views/Bottle Views/BottleView.swift +++ b/Whisky/Views/Bottle Views/BottleView.swift @@ -41,17 +41,31 @@ struct BottleView: View { var body: some View { NavigationStack(path: $path) { ScrollView { - if pins.count > 0 { - LazyVGrid(columns: gridLayout, alignment: .center) { - ForEach(pins, id: \.url) { pin in - PinnedProgramView(bottle: bottle, - pin: pin, - loadStartMenu: $loadStartMenu, - path: $path) + ZStack { + if pins.count > 0 { + LazyVGrid(columns: gridLayout, alignment: .center) { + ForEach(pins, id: \.url) { pin in + PinnedProgramView(bottle: bottle, + pin: pin, + loadStartMenu: $loadStartMenu, + path: $path) + } + } + .padding() + } + + if isLoadingInstalledPrograms { + HStack { + Spacer() + ProgressView() + .padding() + .background(Material.regular) + .clipShape(RoundedRectangle(cornerSize: .init(width: 16, height: 16))) + Spacer() } } - .padding() } + Form { NavigationLink(value: BottleStage.programs) { HStack {