diff --git a/.github/funding.yml b/.github/funding.yml deleted file mode 100644 index 7ede066..0000000 --- a/.github/funding.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: sindresorhus -custom: https://sindresorhus.com/donate diff --git a/Plash.xcodeproj/project.pbxproj b/Plash.xcodeproj/project.pbxproj index bef360a..e652512 100644 --- a/Plash.xcodeproj/project.pbxproj +++ b/Plash.xcodeproj/project.pbxproj @@ -114,13 +114,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - E31C8C5F2A9FCC7E00C01756 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; E32421272384E9D700D28A91 = { isa = PBXGroup; children = ( @@ -128,7 +121,6 @@ E32421322384E9D700D28A91 /* Plash */, E32AD6AA26D2718B0008D900 /* ShareExtension */, E32421312384E9D700D28A91 /* Products */, - E31C8C5F2A9FCC7E00C01756 /* Frameworks */, ); sourceTree = ""; tabWidth = 4; @@ -432,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.5; + MACOSX_DEPLOYMENT_TARGET = 14.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -492,7 +484,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.5; + MACOSX_DEPLOYMENT_TARGET = 14.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/Plash/AddWebsiteScreen.swift b/Plash/AddWebsiteScreen.swift index a9c4c3c..c1c0d4b 100644 --- a/Plash/AddWebsiteScreen.swift +++ b/Plash/AddWebsiteScreen.swift @@ -132,27 +132,27 @@ struct AddWebsiteScreen: View { private var topView: some View { Section { - // TODO: Use `.textContentType(.URL)` when targeting macOS 14. TextField("URL", text: $urlString) + .textContentType(.URL) .lineLimit(1) // This change listener is used to respond to URL changes from the outside, like the "Revert" button or the Shortcuts actions. - .onChange(of: website.wrappedValue.url) { + .onChange(of: website.wrappedValue.url) { _, url in guard - $0.absoluteString != "-", - $0.absoluteString != urlString + url.absoluteString != "-", + url.absoluteString != urlString else { return } - urlString = $0.absoluteString + urlString = url.absoluteString } .onChange(of: urlString) { - guard let url = URL(humanString: $0) else { + guard let url = URL(humanString: urlString) else { // Makes the “Revert” button work if the user clears the URL field. if urlString.trimmed.isEmpty { website.wrappedValue.url = "-" } else if - let url = URL(string: $0), + let url = URL(string: urlString, encodingInvalidCharacters: false), url.isValid { website.wrappedValue.url = url @@ -315,7 +315,7 @@ struct AddWebsiteScreen: View { panel.directoryURL = url } - // TODO: Make it a sheet instead when targeting the macOS bug is fixed. (macOS 13.1) + // TODO: Make it a sheet instead when targeting the macOS bug is fixed. (macOS 14.2) // let result = await panel.beginSheet(hostingWindow) let result = await panel.begin() diff --git a/Plash/App.swift b/Plash/App.swift index 98f9611..74bef0f 100644 --- a/Plash/App.swift +++ b/Plash/App.swift @@ -1,17 +1,13 @@ import SwiftUI /** -TODO macOS 14: -- Focus filter support. -- Set `!` as menu bar item text when there is an error. -- handle loose `URL(string: $0)` - TODO macOS 15: -- Use SwiftUI for the desktop window and the web view. -- Use `MenuBarExtra`. +- Use `MenuBarExtra` and afterwards switch to `@Observable`. - Remove `Combine` and `Defaults.publisher` usage. - Use `EnvironmentValues#requestReview`. - Remove `ensureRunning()` from some intents that don't require Plash to stay open. +- Focus filter support. +- Use SwiftUI for the desktop window and the web view. */ @main diff --git a/Plash/AppState.swift b/Plash/AppState.swift index 06e4fb4..8d2ffab 100644 --- a/Plash/AppState.swift +++ b/Plash/AppState.swift @@ -28,6 +28,10 @@ final class AppState: ObservableObject { var isBrowsingMode = false { didSet { + guard isEnabled else { + return + } + desktopWindow.isInteractive = isBrowsingMode desktopWindow.alphaValue = isBrowsingMode ? 1 : Defaults[.opacity] resetTimer() @@ -94,25 +98,12 @@ final class AppState: ObservableObject { _ = desktopWindow setUpEvents() showWelcomeScreenIfNeeded() - SSApp.requestReviewAfterBeingCalledThisManyTimes([8, 50, 500]) + SSApp.requestReviewAfterBeingCalledThisManyTimes([6, 50, 500]) #if DEBUG // SSApp.showSettingsWindow() // Constants.openWebsitesWindow() #endif - - SSApp.runOnce(identifier: "warnAboutSettingDisplaySetting") { - guard - !SSApp.isFirstLaunch, - UserDefaults.standard.string(forKey: "display") != #"{"id":1}"# - else { - return - } - - SSApp.activateIfAccessory() - NSAlert.showModal(title: "Because of a bug, you need to select the display to show Plash on again in the settings.") - SSApp.showSettingsWindow() - } } func handleMenuBarIcon() { diff --git a/Plash/Events.swift b/Plash/Events.swift index 0ad2768..edbeac4 100644 --- a/Plash/Events.swift +++ b/Plash/Events.swift @@ -36,13 +36,13 @@ extension AppState { } .store(in: &cancellables) - SSPublishers.deviceDidWake + SSEvents.deviceDidWake .sink { [self] in loadUserURL() } .store(in: &cancellables) - SSPublishers.isScreenLocked + SSEvents.isScreenLocked .sink { [self] in isScreenLocked = $0 setEnabledStatus() @@ -122,8 +122,8 @@ extension AppState { Defaults[.isBrowsingMode].toggle() } - KeyboardShortcuts.onKeyUp(for: .toggleEnabled) { - self.isManuallyDisabled.toggle() + KeyboardShortcuts.onKeyUp(for: .toggleEnabled) { [self] in + isManuallyDisabled.toggle() } KeyboardShortcuts.onKeyUp(for: .reload) { [self] in diff --git a/Plash/Intents.swift b/Plash/Intents.swift index 4251846..15b0a5f 100644 --- a/Plash/Intents.swift +++ b/Plash/Intents.swift @@ -1,17 +1,16 @@ import AppIntents import AppKit -struct AddWebsiteIntent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "AddWebsiteIntent" - +struct AddWebsiteIntent: AppIntent { static let title: LocalizedStringResource = "Add Website" static let description = IntentDescription( -""" -Adds a website to Plash. + """ + Adds a website to Plash. -Returns the added website. -""" + Returns the added website. + """, + resultValueName: "Added Website" ) @Parameter(title: "URL") @@ -33,10 +32,7 @@ Returns the added website. } } -// Typo in the name, but we cannot fix it as this point. -struct RemoveWebsitesItent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "RemoveWebsitesIntent" - +struct RemoveWebsitesIntent: AppIntent { static let title: LocalizedStringResource = "Remove Websites" static let description = IntentDescription("Removes the given websites from Plash.") @@ -66,7 +62,7 @@ struct RemoveWebsitesItent: AppIntent, CustomIntentMigratedAppIntent { struct SetEnabledStateIntent: AppIntent { static let title: LocalizedStringResource = "Set Enabled State" - static let description = IntentDescription("Set the state of Plash.") + static let description = IntentDescription("Sets the enabled state of Plash.") @Parameter( title: "Action", @@ -87,6 +83,8 @@ struct SetEnabledStateIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + ensureRunning() + if shouldToggle { AppState.shared.isManuallyDisabled.toggle() } else { @@ -100,7 +98,10 @@ struct SetEnabledStateIntent: AppIntent { struct GetEnabledStateIntent: AppIntent { static let title: LocalizedStringResource = "Get Enabled State" - static let description = IntentDescription("Returns whether Plash is currently enabled.") + static let description = IntentDescription( + "Returns whether Plash is currently enabled.", + resultValueName: "Enabled State" + ) static var parameterSummary: some ParameterSummary { Summary("Get the current enabled state of Plash") @@ -112,91 +113,13 @@ struct GetEnabledStateIntent: AppIntent { } } -@available(macOS, deprecated: 13, message: "Replaced by the “Find Website” action.") -struct GetWebsitesIntent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "GetWebsitesIntent" - - static let title: LocalizedStringResource = "Get Websites" - - static let description = IntentDescription("Returns all the websites in Plash or just some based on a filter.") - - @Parameter( - title: "Filter", - default: false, - displayName: Bool.IntentDisplayName(true: "websites where", false: "all websites") - ) - var shouldFilter: Bool - - @Parameter(title: "Condition", default: .titleEquals) - var condition: FilterConditionAppEnum? - - @Parameter(title: "Text") - var matchText: String? - - @Parameter(title: "Maximum Results") - var limit: Int? - - static var parameterSummary: some ParameterSummary { - When(\.$shouldFilter, .equalTo, true) { - Summary("Get \(\.$shouldFilter) \(\.$condition) \(\.$matchText)") { - \.$limit - } - } otherwise: { - Summary("Get \(\.$shouldFilter)") { - \.$limit - } - } - } - - func perform() async throws -> some IntentResult & ReturnsValue<[WebsiteAppEntity]> { - ensureRunning() - - var websites = WebsiteAppEntity.all - - if - shouldFilter, - let condition, - let matchText = matchText?.trimmed.lowercased() - { - websites = websites.filter { - let title = $0.title.lowercased() - let urlString = $0.url.absoluteString.lowercased() - - guard let url = URL(string: urlString) else { - return false - } - - switch condition { - case .titleEquals: - return title == matchText - case .titleContains: - return title.contains(matchText) - case .titleBeginsWith: - return title.hasPrefix(matchText) - case .titleEndsWith: - return title.hasSuffix(matchText) - case .urlEquals: - return url.absoluteString == matchText - case .urlHostEquals: - return url.host == matchText - } - } - } - - if let limit { - websites = Array(websites.prefix(limit)) - } - - return .result(value: websites) - } -} - -struct GetCurrentWebsiteItent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "GetCurrentWebsiteIntent" - +struct GetCurrentWebsiteIntent: AppIntent { static let title: LocalizedStringResource = "Get Current Website" - static let description = IntentDescription("Returns the current website in Plash.") + static let description = IntentDescription( + "Returns the current website in Plash.", + resultValueName: "Current Website" + ) func perform() async throws -> some IntentResult & ReturnsValue { ensureRunning() @@ -204,9 +127,7 @@ struct GetCurrentWebsiteItent: AppIntent, CustomIntentMigratedAppIntent { } } -struct SetCurrentWebsiteItent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "SetCurrentWebsiteIntent" - +struct SetCurrentWebsiteIntent: AppIntent { static let title: LocalizedStringResource = "Set Current Website" static let description = IntentDescription("Sets the current website in Plash to the given website.") @@ -225,27 +146,25 @@ struct SetCurrentWebsiteItent: AppIntent, CustomIntentMigratedAppIntent { } } -struct ReloadWebsiteIntent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "ReloadWebsiteIntent" - +struct ReloadWebsiteIntent: AppIntent { static let title: LocalizedStringResource = "Reload Website" static let description = IntentDescription("Reloads the current website in Plash.") + @MainActor func perform() async throws -> some IntentResult { ensureRunning() - await AppState.shared.reloadWebsite() + AppState.shared.reloadWebsite() return .result() } } -struct NextWebsiteIntent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "NextWebsiteIntent" - +struct NextWebsiteIntent: AppIntent { static let title: LocalizedStringResource = "Switch to Next Website" static let description = IntentDescription("Switches Plash to the next website in the list.") + @MainActor func perform() async throws -> some IntentResult { ensureRunning() WebsitesController.shared.makeNextCurrent() @@ -253,13 +172,12 @@ struct NextWebsiteIntent: AppIntent, CustomIntentMigratedAppIntent { } } -struct PreviousWebsiteIntent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "PreviousWebsiteIntent" - +struct PreviousWebsiteIntent: AppIntent { static let title: LocalizedStringResource = "Switch to Previous Website" static let description = IntentDescription("Switches Plash to the previous website in the list.") + @MainActor func perform() async throws -> some IntentResult { ensureRunning() WebsitesController.shared.makePreviousCurrent() @@ -267,13 +185,12 @@ struct PreviousWebsiteIntent: AppIntent, CustomIntentMigratedAppIntent { } } -struct RandomWebsiteIntent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "RandomWebsiteIntent" - +struct RandomWebsiteIntent: AppIntent { static let title: LocalizedStringResource = "Switch to Random Website" static let description = IntentDescription("Switches Plash to a random website in the list.") + @MainActor func perform() async throws -> some IntentResult { ensureRunning() WebsitesController.shared.makeRandomCurrent() @@ -281,134 +198,40 @@ struct RandomWebsiteIntent: AppIntent, CustomIntentMigratedAppIntent { } } -struct ToggleBrowsingModeIntent: AppIntent, CustomIntentMigratedAppIntent { - static let intentClassName = "ToggleBrowsingModeIntent" - +struct ToggleBrowsingModeIntent: AppIntent { static let title: LocalizedStringResource = "Toggle Browsing Mode" static let description = IntentDescription("Toggles “Browsing Mode” for Plash.") + @MainActor func perform() async throws -> some IntentResult { ensureRunning() - await AppState.shared.toggleBrowsingMode() + AppState.shared.toggleBrowsingMode() return .result() } } -enum FilterConditionAppEnum: String, AppEnum { - case titleEquals - case titleContains - case titleBeginsWith - case titleEndsWith - case urlEquals - case urlHostEquals - - static let typeDisplayRepresentation: TypeDisplayRepresentation = "Filter Condition" - - static let caseDisplayRepresentations: [Self: DisplayRepresentation] = [ - .titleEquals: "title equals", - .titleContains: "title contains", - .titleBeginsWith: "title begins with", - .titleEndsWith: "title ends with", - .urlEquals: "URL equals", - .urlHostEquals: "URL host equals" - ] -} - -// TODO: When targeting macOS 14, use `EnumerableEntityQuery` to simplify it? -// Note: It's a class so we can use `NSPredicate`. -//struct WebsiteAppEntity: AppEntity { -final class WebsiteAppEntity: NSObject, AppEntity { - struct WebsiteAppEntityQuery: EntityStringQuery, EntityPropertyQuery { - static var sortingOptions = SortingOptions {} - - static var properties = QueryProperties { - Property(\.$title) { - EqualToComparator { NSPredicate(format: "title ==[cd] %@", $0) } - NotEqualToComparator { NSPredicate(format: "title !=[cd] %@", $0) } - ContainsComparator { NSPredicate(format: "title CONTAINS[cd] %@", $0) } - HasPrefixComparator { NSPredicate(format: "title BEGINSWITH[cd] %@", $0) } - HasSuffixComparator { NSPredicate(format: "title ENDSWITH[cd] %@", $0) } - } - Property(\.$url) { - EqualToComparator { NSPredicate(format: "url ==[cd] %@", $0.absoluteString) } - NotEqualToComparator { NSPredicate(format: "url !=[cd] %@", $0.absoluteString) } - // TODO: Find a way to make these work on `URL`. -// ContainsComparator { NSPredicate(format: "title CONTAINS[cd] %@", $0.absoluteString) } -// HasPrefixComparator { NSPredicate(format: "title BEGINSWITH[cd] %@", $0.absoluteString) } -// HasSuffixComparator { NSPredicate(format: "title ENDSWITH[cd] %@", $0.absoluteString) } - } - Property(\.$urlHost) { - EqualToComparator { NSPredicate(format: "urlHost ==[cd] %@", $0) } - NotEqualToComparator { NSPredicate(format: "urlHost !=[cd] %@", $0) } - ContainsComparator { NSPredicate(format: "urlHost CONTAINS[cd] %@", $0) } - HasPrefixComparator { NSPredicate(format: "urlHost BEGINSWITH[cd] %@", $0) } - HasSuffixComparator { NSPredicate(format: "urlHost ENDSWITH[cd] %@", $0) } - } - } - - private func allEntities() -> [WebsiteAppEntity] { - WebsiteAppEntity.all - } - - func entities(for identifiers: [WebsiteAppEntity.ID]) async throws -> [WebsiteAppEntity] { - allEntities().filter { identifiers.contains($0.id) } - } - - func entities(matching query: String) async throws -> [WebsiteAppEntity] { - allEntities().filter { - $0.title.localizedCaseInsensitiveContains(query) - || $0.url.absoluteString.localizedCaseInsensitiveContains(query) - } - } - - func entities( - matching comparators: [NSPredicate], - mode: ComparatorMode, - sortedBy: [Sort], - limit: Int? - ) async throws -> [WebsiteAppEntity] { - let predicate = NSCompoundPredicate(type: mode == .and ? .and : .or, subpredicates: comparators) - var result = allEntities().filter { predicate.evaluate(with: $0) } - - if let limit { - result = Array(result.prefix(limit)) - } - - return result - } - - func suggestedEntities() async throws -> [WebsiteAppEntity] { - allEntities() - } - } - +struct WebsiteAppEntity: AppEntity { static let typeDisplayRepresentation: TypeDisplayRepresentation = "Website" - static let defaultQuery = WebsiteAppEntityQuery() + static let defaultQuery = Query() - // TODO: Use `Self` here when it's a struct again. - static var all: [WebsiteAppEntity] { WebsitesController.shared.all.map(Self.init) } + let id: UUID @Property(title: "Title") - @objc var title: String + var title: String @Property(title: "URL") - @objc var url: URL + var url: URL @Property(title: "URL Host") - @objc var urlHost: String + var urlHost: String @Property(title: "Is Current") var isCurrent: Bool - let id: UUID - init(_ website: Website) { self.id = website.id - - super.init() - self.title = website.title self.url = website.url self.urlHost = website.url.host ?? "" @@ -432,16 +255,39 @@ extension WebsiteAppEntity { } } +extension WebsiteAppEntity { + struct Query: EnumerableEntityQuery, EntityStringQuery { + static let findIntentDescription = IntentDescription( + "Returns the websites in Plash.", + resultValueName: "Websites" + ) + + func allEntities() -> [WebsiteAppEntity] { + WebsitesController.shared.all.map(WebsiteAppEntity.init) + } + + func suggestedEntities() async throws -> [WebsiteAppEntity] { + allEntities() + } + + func entities(for identifiers: [WebsiteAppEntity.ID]) async throws -> [WebsiteAppEntity] { + allEntities().filter { identifiers.contains($0.id) } + } + + func entities(matching query: String) async throws -> [WebsiteAppEntity] { + allEntities().filter { + $0.title.localizedCaseInsensitiveContains(query) + || $0.url.absoluteString.localizedCaseInsensitiveContains(query) + } + } + } +} + func ensureRunning() { // It's `prohibited` if the app was not already launched. // We activate it so that it will not quit right away if it was not already launched. (macOS 13.4) // We don't use `static let openAppWhenRun = true` as it activates (and steals focus) even if the app is already launched. - // Note: Activate no longer works in macOS 14. if NSApp.activationPolicy() == .prohibited { - if #available(macOS 14, *) { - SSApp.url.open() - } else { - SSApp.forceActivate() - } + SSApp.url.open() } } diff --git a/Plash/Menus.swift b/Plash/Menus.swift index c7d84a7..118971a 100644 --- a/Plash/Menus.swift +++ b/Plash/Menus.swift @@ -161,8 +161,8 @@ extension AppState { if (isEnabled || isManuallyDisabled) || (!Defaults[.deactivateOnBattery] && powerSourceWatcher?.powerSource.isUsingBattery == false) { menu.addCallbackItem( isManuallyDisabled ? "Enable" : "Disable" - ) { - self.isManuallyDisabled.toggle() + ) { [self] in + isManuallyDisabled.toggle() } } diff --git a/Plash/SettingsScreen.swift b/Plash/SettingsScreen.swift index ea0b75b..71bd56a 100644 --- a/Plash/SettingsScreen.swift +++ b/Plash/SettingsScreen.swift @@ -42,8 +42,8 @@ private struct ShortcutsSettings: View { var body: some View { Form { - KeyboardShortcuts.Recorder("Toggle browsing mode", name: .toggleBrowsingMode) KeyboardShortcuts.Recorder("Toggle enabled state", name: .toggleEnabled) + KeyboardShortcuts.Recorder("Toggle browsing mode", name: .toggleBrowsingMode) KeyboardShortcuts.Recorder("Reload website", name: .reload) KeyboardShortcuts.Recorder("Next website", name: .nextWebsite) KeyboardShortcuts.Recorder("Previous website", name: .previousWebsite) @@ -151,14 +151,14 @@ private struct ReloadIntervalSetting: View { Text("minutes") .textSelection(.disabled) } - .contentShape(.rectangle) + .contentShape(.rect) Toggle("Reload every", isOn: $reloadInterval.isNotNil(trueSetValue: Self.defaultReloadInterval)) .labelsHidden() .controlSize(.mini) .toggleStyle(.switch) } .accessibilityLabel("Reload interval in minutes") - .contentShape(.rectangle) + .contentShape(.rect) } private var reloadIntervalInMinutes: Binding { @@ -215,7 +215,6 @@ private struct DisplaySetting: View { } private struct ClearWebsiteDataSetting: View { - @EnvironmentObject private var appState: AppState @State private var hasCleared = false var body: some View { @@ -223,7 +222,7 @@ private struct ClearWebsiteDataSetting: View { Task { hasCleared = true WebsitesController.shared.thumbnailCache.removeAllImages() - await appState.webViewController.webView.clearWebsiteData() + await AppState.shared.webViewController.webView.clearWebsiteData() } } .help("Clears all cookies, local storage, caches, etc.") diff --git a/Plash/URLCommands.swift b/Plash/URLCommands.swift index bf555a5..da746e4 100644 --- a/Plash/URLCommands.swift +++ b/Plash/URLCommands.swift @@ -2,7 +2,7 @@ import Cocoa extension AppState { func setUpURLCommands() { - SSPublishers.appOpenURL + SSEvents.appOpenURL .sink { [self] in handleURLCommands($0) } @@ -26,7 +26,7 @@ extension AppState { case "add": guard let urlString = parameters["url"]?.trimmed, - let url = URL(string: urlString), + let url = URL(string: urlString, encodingInvalidCharacters: false), url.isValid else { showMessage("Invalid URL for the “add” command.") diff --git a/Plash/Utilities.swift b/Plash/Utilities.swift index b3e5cad..571f231 100644 --- a/Plash/Utilities.swift +++ b/Plash/Utilities.swift @@ -48,7 +48,7 @@ extension CGFloat { /** Get a Double from a CGFloat. This makes it easier to work with optionals. */ - var double: Double { Double(self) } + var toDouble: Double { Double(self) } } @@ -445,12 +445,8 @@ enum SSApp { // @MainActor static func forceActivate() { - if #available(macOS 14, *) { - NSApp.yieldActivation(toApplicationWithBundleIdentifier: idString) - NSApp.activate() - } else { - NSApp.activate(ignoringOtherApps: true) - } + NSApp.yieldActivation(toApplicationWithBundleIdentifier: idString) + NSApp.activate() } } @@ -463,12 +459,7 @@ extension SSApp { // Run in the next runloop so it doesn't conflict with SwiftUI if run at startup. DispatchQueue.main.async { activateIfAccessory() - - if #available(macOS 14, *) { - NSApp.mainMenu?.items.first?.submenu?.item(withTitle: "Settings…")?.performAction() - } else { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - } + NSApp.mainMenu?.items.first?.submenu?.item(withTitle: "Settings…")?.performAction() } } } @@ -875,7 +866,7 @@ extension URL { `URL(string:)` doesn't strictly validate the input. This one at least ensures there's a `scheme` and that the `host` has a TLD. */ static func isValid(string: String) -> Bool { - guard let url = URL(string: string) else { + guard let url = URL(string: string, encodingInvalidCharacters: false) else { return false } @@ -1514,6 +1505,7 @@ extension WKWebView { @MainActor func defaultUploadPanelHandler(parameters: WKOpenPanelParameters) async -> [URL]? { // swiftlint:disable:this discouraged_optional_collection let openPanel = NSOpenPanel() + openPanel.identifier = .init("WKWebView_defaultUploadPanelHandler") openPanel.level = .floating openPanel.prompt = "Choose" openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection @@ -2013,9 +2005,9 @@ extension NSScreen { */ static var publisher: AnyPublisher { Publishers.Merge( - SSPublishers.screenParametersDidChange, + SSEvents.screenParametersDidChange, // We use a wake up notification as the screen setup might have changed during sleep. For example, a screen could have been unplugged. - SSPublishers.deviceDidWake + SSEvents.deviceDidWake ) .eraseToAnyPublisher() } @@ -2055,7 +2047,7 @@ extension NSScreen { - Note: There is a 1 point gap between the status bar and a maximized window. You may want to handle that. */ var statusBarThickness: Double { - let value = (frame.height - visibleFrame.height - (visibleFrame.origin.y - frame.origin.y) - 1).double + let value = (frame.height - visibleFrame.height - (visibleFrame.origin.y - frame.origin.y) - 1).toDouble return max(0, value) } @@ -2870,6 +2862,7 @@ enum SecurityScopedBookmarkManager { let delegate = NSOpenSavePanelDelegateHandler(url: directoryURL) let openPanel = with(NSOpenPanel()) { + $0.identifier = .init("SecurityScopedBookmarkManager") $0.delegate = delegate $0.directoryURL = directoryURL $0.allowsMultipleSelection = false @@ -3076,7 +3069,7 @@ extension URL { let urlString = absoluteString.replacing(encodedPlaceholder, with: replacement) - guard let newURL = URL(string: urlString) else { + guard let newURL = URL(string: urlString, encodingInvalidCharacters: false) else { throw PlaceholderError.invalidURLAfterSubstitution(urlString) } @@ -3611,12 +3604,6 @@ extension Sequence where Element: Equatable { } -extension Color { - static let tertiary = Color(NSColor.tertiaryLabelColor) - static let quaternary = Color(NSColor.quaternaryLabelColor) -} - - extension View { func eraseToAnyView() -> AnyView { AnyView(self) @@ -4553,9 +4540,9 @@ extension OperatingSystem { /** - Note: Only use this when you cannot use an `if #available` check. For example, inline in function calls. */ - static let isMacOS15OrLater: Bool = { + static let isMacOS16OrLater: Bool = { #if os(macOS) - if #available(macOS 15, *) { + if #available(macOS 16, *) { return true } @@ -4568,9 +4555,9 @@ extension OperatingSystem { /** - Note: Only use this when you cannot use an `if #available` check. For example, inline in function calls. */ - static let isMacOS14OrLater: Bool = { + static let isMacOS15OrLater: Bool = { #if os(macOS) - if #available(macOS 14, *) { + if #available(macOS 15, *) { return true } @@ -4895,7 +4882,7 @@ extension View { Corner radius with a custom corner style. */ func cornerRadius(_ radius: Double, style: RoundedCornerStyle) -> some View { - clipShape(.roundedRectangle(cornerRadius: radius, style: style)) + clipShape(.rect(cornerRadius: radius, style: style)) } /** @@ -5179,7 +5166,7 @@ extension Defaults { } -// TODO: Remove when targeting macOS 14. +// TODO: Remove when targeting macOS 15. extension Publisher { /** Convert a publisher to a `Result`. @@ -5312,7 +5299,7 @@ extension Notification.Name { } -enum SSPublishers { +enum SSEvents { /** Publishes when the machine wakes from sleep. */ @@ -5340,7 +5327,7 @@ enum SSPublishers { } -extension SSPublishers { +extension SSEvents { private struct AppOpenURLPublisher: Publisher { // We need this abstraction as `kAEGetURL` can only be subscribed to once. private final class EventManager { @@ -5601,60 +5588,21 @@ struct HideableInfoBox: View { .padding(.vertical, 6) .padding(.horizontal, 8) .backgroundColor(.primary.opacity(0.05)) - .clipShape(.roundedRectangle(cornerRadius: 8, style: .continuous)) + .clipShape(.rect(cornerRadius: 8)) } } } -extension Shape where Self == Rectangle { - static var rectangle: Self { .init() } -} - -extension Shape where Self == Circle { - static var circle: Self { .init() } -} - -extension Shape where Self == Capsule { - static var capsule: Self { .init() } -} - -extension Shape where Self == Ellipse { - static var ellipse: Self { .init() } -} - -extension Shape where Self == ContainerRelativeShape { - static var containerRelative: Self { .init() } -} - -extension Shape where Self == RoundedRectangle { - static func roundedRectangle(cornerRadius: Double, style: RoundedCornerStyle = .circular) -> Self { - .init(cornerRadius: cornerRadius, style: style) - } - - static func roundedRectangle(cornerSize: CGSize, style: RoundedCornerStyle = .circular) -> Self { - .init(cornerSize: cornerSize, style: style) - } -} - - -extension Button> { - init( - _ title: String, - systemImage: String, - role: ButtonRole? = nil, - action: @escaping () -> Void - ) { - self.init( - role: role, - action: action - ) { - Label(title, systemImage: systemImage) - } +extension View { + /** + Make `Button` and `Menu` be borderless and only show the icon. + */ + func iconButtonStyle() -> some View { + modifier(IconButtonStyle()) } } - private struct IconButtonStyle: ViewModifier { func body(content: Content) -> some View { content @@ -5664,15 +5612,6 @@ private struct IconButtonStyle: ViewModifier { } } -extension View { - /** - Make `Button` and `Menu` be borderless and only show the icon. - */ - func iconButtonStyle() -> some View { - modifier(IconButtonStyle()) - } -} - /** An icon button used for closing or clearing something. diff --git a/Plash/WebViewController.swift b/Plash/WebViewController.swift index ac4a055..ebe1531 100644 --- a/Plash/WebViewController.swift +++ b/Plash/WebViewController.swift @@ -221,11 +221,13 @@ extension WebViewController: WKUIDelegate { var styleMask: NSWindow.StyleMask = [ .titled, - .closable + .closable, + .resizable ] - if windowFeatures.allowsResizing?.boolValue == true { - styleMask.insert(.resizable) + // We default the window to be resizable to make it user-friendly. + if windowFeatures.allowsResizing?.boolValue == false { + styleMask.remove(.resizable) } let window = NSWindow( diff --git a/Plash/Website.swift b/Plash/Website.swift index 0bb38ca..aa0a460 100644 --- a/Plash/Website.swift +++ b/Plash/Website.swift @@ -1,6 +1,6 @@ import Foundation -struct Website: Hashable, Codable, Identifiable, Defaults.Serializable { +struct Website: Hashable, Codable, Identifiable, Sendable, Defaults.Serializable { let id: UUID var isCurrent: Bool var url: URL diff --git a/Plash/WebsitesController.swift b/Plash/WebsitesController.swift index ddd1533..df0e07e 100644 --- a/Plash/WebsitesController.swift +++ b/Plash/WebsitesController.swift @@ -165,7 +165,7 @@ final class WebsitesController { return } - Task { @MainActor in // TODO: Not sure if this is needed. + Task { let metadataProvider = LPMetadataProvider() metadataProvider.shouldFetchSubresources = false diff --git a/Plash/WebsitesScreen.swift b/Plash/WebsitesScreen.swift index 7fa6eb9..25a838b 100644 --- a/Plash/WebsitesScreen.swift +++ b/Plash/WebsitesScreen.swift @@ -19,7 +19,7 @@ struct WebsitesScreen: View { // .onKeyboardShortcut(.defaultAction) { // editedWebsite = selection // } - .onChange(of: websites) { [oldWebsites = websites] websites in + .onChange(of: websites) { oldWebsites, websites in // Check that a website was added. guard websites.count > oldWebsites.count else { return @@ -108,7 +108,7 @@ private struct RowView: View { } .disabled(website.isCurrent) } - .contentShape(.rectangle) + .contentShape(.rect) .onDoubleClick { selection = website.id } @@ -158,7 +158,7 @@ private struct IconView: View { } } .frame(width: 32, height: 32) - .clipShape(.roundedRectangle(cornerRadius: 4, style: .continuous)) + .clipShape(.rect(cornerRadius: 4)) .task(id: website.url) { guard let image = await fetchIcons() else { return diff --git a/Plash/WelcomeScreen.swift b/Plash/WelcomeScreen.swift index f07842e..87410d9 100644 --- a/Plash/WelcomeScreen.swift +++ b/Plash/WelcomeScreen.swift @@ -16,7 +16,7 @@ extension AppState { Use “Browsing Mode” if you need to log into a website or interact with it in some way. - Note: Support for multiple displays is currently limited to the ability to choose which display to show the website on. Support for setting a separate website for each display is planned. + Note: Support for multiple displays is currently limited to the ability to choose which display to show the website on. """, buttonTitles: [ "Continue" diff --git a/app-store-description.txt b/app-store-description.txt index a458eb9..c76a035 100644 --- a/app-store-description.txt +++ b/app-store-description.txt @@ -72,4 +72,4 @@ Make a shortcut in the Shortcuts app that uses the “Set Current Website” act ■ Support -Click the “Send Feedback” button in the app or email me at sindresorhus@gmail.com +Click the “Send Feedback” button in the app. diff --git a/license b/license index e7af2f7..fa7ceba 100644 --- a/license +++ b/license @@ -1,6 +1,6 @@ MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) Sindre Sorhus (https://sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/readme.md b/readme.md index 8732787..cab48e4 100644 --- a/readme.md +++ b/readme.md @@ -68,7 +68,7 @@ Requires macOS 13 or later. A special version for users that cannot access the App Store. It won't receive automatic updates. I will update it here once a year. -[Download](https://dsc.cloud/sindresorhus/Plash-2.13.1-1675003980) *(2.13.1 · macOS 13+)* +[Download](https://www.dropbox.com/scl/fi/lr55dpcqmwxz1hkn5big8/Plash-2.15.0-1705418289.zip?rlkey=smxibxghlw9c153i2kbzhoela&raw=1) *(2.15.0 · macOS 14+)* ## Tips