mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 03:48:55 +00:00
v3.9.2
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
"location" : "https://github.com/alienator88/AlinFoundation",
|
"location" : "https://github.com/alienator88/AlinFoundation",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "a4df813f0186209491a2568d03aab8da8eab7ae0"
|
"revision" : "59b3014a76ad187672b46727168634c606d96953"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,13 +49,6 @@
|
|||||||
ReferencedContainer = "container:Pearcleaner.xcodeproj">
|
ReferencedContainer = "container:Pearcleaner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
<EnvironmentVariables>
|
|
||||||
<EnvironmentVariable
|
|
||||||
key = "OS_ACTIVITY_MODE"
|
|
||||||
value = "disable"
|
|
||||||
isEnabled = "YES">
|
|
||||||
</EnvironmentVariable>
|
|
||||||
</EnvironmentVariables>
|
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ class AppPathFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for skipCondition in skipConditions {
|
for skipCondition in skipConditions {
|
||||||
if itemL.hasPrefix(skipCondition.skipPrefix) {
|
if skipCondition.skipPrefix.contains(where: itemL.hasPrefix) {
|
||||||
let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
||||||
if !isAllowed {
|
if !isAllowed {
|
||||||
return true // Skip because it starts with a base prefix but is not in the allowed list
|
return true // Skip because it starts with a base prefix but is not in the allowed list
|
||||||
@@ -414,276 +414,3 @@ class AppPathFinder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// CLI // =========================================================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
//class AppPathFinderCLI {
|
|
||||||
// private var appInfo: AppInfo
|
|
||||||
// private var locations: Locations
|
|
||||||
// private var collection: [URL] = []
|
|
||||||
// private var containerCollection: [URL] = []
|
|
||||||
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinderCLI.collectionAccess")
|
|
||||||
//
|
|
||||||
// init(appInfo: AppInfo = .empty, locations: Locations) {
|
|
||||||
// self.appInfo = appInfo
|
|
||||||
// self.locations = locations
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Make this function synchronous for the CLI
|
|
||||||
// func findPaths() -> Set<URL> {
|
|
||||||
// if self.appInfo.webApp {
|
|
||||||
// containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
|
||||||
// self.initialURLProcessing()
|
|
||||||
// return finalizeCollection() // Return the collected paths
|
|
||||||
// } else {
|
|
||||||
// containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
|
||||||
// self.initialURLProcessing()
|
|
||||||
// self.collectDirectories() // Synchronous version
|
|
||||||
// self.collectFiles() // Synchronous version
|
|
||||||
// return finalizeCollection() // Return the collected paths
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private func initialURLProcessing() {
|
|
||||||
// if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
|
|
||||||
// let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
|
|
||||||
// self.collection.append(modifiedUrl)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
|
|
||||||
// var containsItem = false
|
|
||||||
// collectionAccessQueue.sync {
|
|
||||||
// containsItem = self.collection.contains(itemURL)
|
|
||||||
// }
|
|
||||||
// if containsItem || !isSupportedFileType(at: itemURL.path) {
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// for skipCondition in skipConditions {
|
|
||||||
// if itemL.hasPrefix(skipCondition.skipPrefix) {
|
|
||||||
// let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
|
||||||
// if !isAllowed {
|
|
||||||
// return true // Skip because it starts with a base prefix but is not in the allowed list
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private func processDirectoryLocation(_ location: String) {
|
|
||||||
// if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
|
|
||||||
// for item in contents {
|
|
||||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
|
||||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
|
||||||
//
|
|
||||||
// var isDirectory: ObjCBool = false
|
|
||||||
// if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue {
|
|
||||||
// // Perform the check to skip the item if needed
|
|
||||||
// if shouldSkipItem(itemL, at: itemURL) {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// collectionAccessQueue.sync {
|
|
||||||
// let alreadyIncluded = self.collection.contains { existingURL in
|
|
||||||
// itemURL.path.hasPrefix(existingURL.path)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if !alreadyIncluded && specificCondition(itemL: itemL, itemURL: itemURL) {
|
|
||||||
// self.collection.append(itemURL)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private func specificCondition(itemL: String, itemURL: URL) -> Bool {
|
|
||||||
// let bundleIdentifierL = self.appInfo.bundleIdentifier.pearFormat()
|
|
||||||
// let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil }
|
|
||||||
// let bundle = bundleComponents.suffix(2).joined()
|
|
||||||
// let nameL = self.appInfo.appName.pearFormat()
|
|
||||||
// let nameLFiltered = nameL.filter { $0.isLetter }
|
|
||||||
//
|
|
||||||
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
|
||||||
//
|
|
||||||
// for condition in conditions {
|
|
||||||
// if bundleIdentifierL.contains(condition.bundle_id) {
|
|
||||||
// // Exclude keywords
|
|
||||||
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
|
|
||||||
// if hasExcludeKeyword {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// // Include keywords
|
|
||||||
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
|
|
||||||
// if hasIncludeKeyword {
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// if self.appInfo.webApp {
|
|
||||||
// return itemL.contains(bundleIdentifierL)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP) || (nameLFiltered.count > 3 && itemL.contains(nameLFiltered)))
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Make collectDirectories synchronous
|
|
||||||
// private func collectDirectories() {
|
|
||||||
// for location in self.locations.apps.paths {
|
|
||||||
// self.processDirectoryLocation(location) // Synchronous call
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private func processFileLocation(_ location: String) {
|
|
||||||
// if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
|
|
||||||
// for item in contents {
|
|
||||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
|
||||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
|
||||||
//
|
|
||||||
// if FileManager.default.fileExists(atPath: itemURL.path),
|
|
||||||
// !shouldSkipItem(itemL, at: itemURL),
|
|
||||||
// specificCondition(itemL: itemL, itemURL: itemURL) {
|
|
||||||
// collectionAccessQueue.sync {
|
|
||||||
// self.collection.append(itemURL)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private func getAllContainers(bundleURL: URL) -> [URL] {
|
|
||||||
// var containers: [URL] = []
|
|
||||||
//
|
|
||||||
// // Extract bundle identifier from bundleURL
|
|
||||||
// let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
|
|
||||||
//
|
|
||||||
// // Ensure the bundleIdentifier is not nil
|
|
||||||
// guard let containerBundleIdentifier = bundleIdentifier else {
|
|
||||||
// printOS("Get Containers: No bundle identifier found for the given bundle URL.")
|
|
||||||
// return containers // Returns whatever was found so far, possibly empty
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Get the regular container URL for the extracted bundle identifier
|
|
||||||
// if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
|
|
||||||
// if FileManager.default.fileExists(atPath: groupContainer.path) {
|
|
||||||
// containers.append(groupContainer)
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
|
|
||||||
//
|
|
||||||
// do {
|
|
||||||
// let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
|
||||||
//
|
|
||||||
// // Define a regular expression to match UUID format
|
|
||||||
// let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
|
|
||||||
//
|
|
||||||
// for directory in containerDirectories {
|
|
||||||
// let directoryName = directory.lastPathComponent
|
|
||||||
//
|
|
||||||
// // Check if the directory name matches the UUID pattern
|
|
||||||
// if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
|
|
||||||
// // Attempt to read the metadata plist file
|
|
||||||
// let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
|
|
||||||
// if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
|
|
||||||
// if applicationBundleID == self.appInfo.bundleIdentifier {
|
|
||||||
// containers.append(directory)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } catch {
|
|
||||||
// printOS("Error accessing the Containers directory: \(error)")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Return all found containers
|
|
||||||
// return containers
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Make collectFiles synchronous
|
|
||||||
// private func collectFiles() {
|
|
||||||
// for location in self.locations.apps.paths {
|
|
||||||
// self.processFileLocation(location) // Synchronous call
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private func handleOutliers(include: Bool = true) -> [URL] {
|
|
||||||
// var outliers: [URL] = []
|
|
||||||
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
|
||||||
//
|
|
||||||
// // Find conditions that match the current app's bundle identifier
|
|
||||||
// let matchingConditions = conditions.filter { condition in
|
|
||||||
// bundleIdentifier.contains(condition.bundle_id)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// for condition in matchingConditions {
|
|
||||||
// if include {
|
|
||||||
// // Handle includeForce
|
|
||||||
// if let forceIncludes = condition.includeForce {
|
|
||||||
// for path in forceIncludes {
|
|
||||||
// outliers.append(path)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// // Handle excludeForce
|
|
||||||
// if let excludeForce = condition.excludeForce {
|
|
||||||
// for path in excludeForce {
|
|
||||||
// outliers.append(path)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return outliers
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // No need to dispatch asynchronously here
|
|
||||||
// private func finalizeCollection() -> Set<URL> {
|
|
||||||
// let outliers = self.handleOutliers()
|
|
||||||
// let outliersEx = self.handleOutliers(include: false)
|
|
||||||
// var tempCollection: [URL] = []
|
|
||||||
// self.collectionAccessQueue.sync {
|
|
||||||
// tempCollection = self.collection
|
|
||||||
// }
|
|
||||||
// tempCollection.append(contentsOf: self.containerCollection)
|
|
||||||
// tempCollection.append(contentsOf: outliers)
|
|
||||||
//
|
|
||||||
// let excludePaths = outliersEx.map { $0.path }
|
|
||||||
// tempCollection.removeAll { url in
|
|
||||||
// excludePaths.contains(url.path)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
|
|
||||||
// var filteredCollection: [URL] = []
|
|
||||||
// var previousUrl: URL?
|
|
||||||
//
|
|
||||||
// for url in sortedCollection {
|
|
||||||
// if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// filteredCollection.append(url)
|
|
||||||
// previousUrl = url
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Handle Trash directory case
|
|
||||||
// var updatedCollection = filteredCollection
|
|
||||||
// if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
|
|
||||||
// // If only one item exists and it's in the Trash, remove it
|
|
||||||
// updatedCollection.removeAll()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return Set(updatedCollection)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//}
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ struct Condition: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SkipCondition {
|
struct SkipCondition {
|
||||||
var skipPrefix: String
|
var skipPrefix: [String]
|
||||||
var allowPrefixes: [String]
|
var allowPrefixes: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ var conditions: [Condition] = [
|
|||||||
// Skip com.apple files/folders since most are system originated, allow some for apps
|
// Skip com.apple files/folders since most are system originated, allow some for apps
|
||||||
let skipConditions: [SkipCondition] = [
|
let skipConditions: [SkipCondition] = [
|
||||||
SkipCondition(
|
SkipCondition(
|
||||||
skipPrefix: "comapple",
|
skipPrefix: ["comapple", "mobiledocuments", "reminders"],
|
||||||
allowPrefixes: ["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"]
|
allowPrefixes: ["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -206,6 +206,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureApplicationSupportFolderExists()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,9 @@
|
|||||||
},
|
},
|
||||||
"Folders" : {
|
"Folders" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Force Refresh" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Functionality" : {
|
"Functionality" : {
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ struct UpdateSettingsTab: View {
|
|||||||
updater.checkForUpdatesForce(showSheet: false)
|
updater.checkForUpdatesForce(showSheet: false)
|
||||||
} label: { EmptyView() }
|
} label: { EmptyView() }
|
||||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", label: String(localized: "Refresh"), help: String(localized: "Refresh updater")))
|
.buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", label: String(localized: "Refresh"), help: String(localized: "Refresh updater")))
|
||||||
|
.contextMenu {
|
||||||
|
Button("Force Refresh") {
|
||||||
|
updater.checkForUpdatesForce(showSheet: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ struct AppSearchView: View {
|
|||||||
|
|
||||||
|
|
||||||
searchBarComponent
|
searchBarComponent
|
||||||
.padding(.horizontal, search.isEmpty ? 10 : 5)
|
.padding(8)
|
||||||
.padding(.bottom, 5)
|
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
@@ -52,15 +51,15 @@ struct AppSearchView: View {
|
|||||||
if updater.updateAvailable {
|
if updater.updateAvailable {
|
||||||
Divider()
|
Divider()
|
||||||
UpdateBadge(updater: updater)
|
UpdateBadge(updater: updater)
|
||||||
.padding(8)
|
.padding()
|
||||||
} else if let _ = permissionManager.results, !permissionManager.allPermissionsGranted {
|
} else if let _ = permissionManager.results, !permissionManager.allPermissionsGranted {
|
||||||
Divider()
|
Divider()
|
||||||
PermissionsBadge()
|
PermissionsBadge()
|
||||||
.padding(8)
|
.padding()
|
||||||
} else if updater.announcementAvailable {
|
} else if updater.announcementAvailable {
|
||||||
Divider()
|
Divider()
|
||||||
FeatureBadge(updater: updater)
|
FeatureBadge(updater: updater)
|
||||||
.padding(8)
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -81,6 +80,7 @@ struct AppSearchView: View {
|
|||||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle", help: String(localized: "Refresh apps (⌘+R)"), size: 16))
|
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle", help: String(localized: "Refresh apps (⌘+R)"), size: 16))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass, sidebar: false)
|
SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass, sidebar: false)
|
||||||
|
|
||||||
|
|
||||||
@@ -92,6 +92,14 @@ struct AppSearchView: View {
|
|||||||
.popover(isPresented: $showMenu) {
|
.popover(isPresented: $showMenu) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
// Button("Refresh") {
|
||||||
|
// withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||||
|
// showPopover = false
|
||||||
|
// reloadAppsList(appState: appState, fsm: fsm)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Refresh List", help: String(localized: "Refresh apps (⌘+R)"), size: 5))
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||||
// Cycle through all enum cases using `CaseIterable`
|
// Cycle through all enum cases using `CaseIterable`
|
||||||
@@ -204,6 +212,8 @@ struct AppSearchView: View {
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.frame(minHeight: 30)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -290,12 +300,12 @@ struct SimpleSearchStyle: TextFieldStyle {
|
|||||||
Button {
|
Button {
|
||||||
text = ""
|
text = ""
|
||||||
} label: { EmptyView() }
|
} label: { EmptyView() }
|
||||||
.buttonStyle(SimpleButtonStyle(icon: "delete.left.fill", help: String(localized: "Clear text"), size: 14, padding: padding))
|
.buttonStyle(SimpleButtonStyle(icon: "delete.left.fill", help: String(localized: "Clear text"), size: 16, padding: 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 5)
|
||||||
|
|
||||||
}
|
}
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
|
|||||||
Reference in New Issue
Block a user