Update screenshots
This commit is contained in:
1 parent
d1afa1d744
commit
637e87761c
5 files changed
+194
-30
No files matched your search
@@ -21,6 +21,10 @@ dist/
|
||||
*.tsbuildinfo
|
||||
*.tgz
|
||||
|
||||
# Python bytecode caches
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Local environment files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
@@ -72,6 +72,8 @@ The runner defaults to the iOS/iPadOS 26.5 simulator runtime so installing a new
|
||||
runtime cannot silently change the marketing UI or select another same-name
|
||||
device. Set `TERMIND_MARKETING_RUNTIME` to a full CoreSimulator runtime identifier
|
||||
only when intentionally updating the runtime used for a capture set.
|
||||
An iPad-only selection builds on the selected iPad and does not require an iPhone
|
||||
simulator to be installed.
|
||||
|
||||
Each locale launches a separate test process with matching Apple language, region,
|
||||
and locale arguments. `TERMIND_MARKETING_LOCALE` selects that process's fixture
|
||||
@@ -119,10 +121,32 @@ simulated sidebar or back buttons to the top-level Metrics and Assistant pages.
|
||||
Regenerate only the matching nine iPad device frames and App Store boards 04-06;
|
||||
first boards use iPad SFTP and remain unchanged.
|
||||
|
||||
The iPad SSH and SFTP fixtures mount the production `MobileRootView` and workspace
|
||||
pages, with deterministic in-memory data supplied through the existing
|
||||
`MobileWorkspaceConnectionServices`. They select Workspaces, collapse the native
|
||||
sidebar, and show the current top tabs, workspace switcher and action menu. Keep
|
||||
the iPhone-only legacy scene wrappers out of this iPad path. Terminal output stays
|
||||
English in every locale. The approved topbar capture set uses iPadOS 27.0:
|
||||
|
||||
```sh
|
||||
TERMIND_MARKETING_SCENES=ssh,sftp \
|
||||
TERMIND_MARKETING_DEVICES=ipad \
|
||||
TERMIND_MARKETING_RUNTIME=com.apple.CoreSimulator.SimRuntime.iOS-27-0 \
|
||||
Scripts/run-mobile-marketing-screenshots.sh
|
||||
```
|
||||
|
||||
Refresh those six device frames and iPad boards 02–03, plus the iPad SFTP image in
|
||||
all nine three-device first boards. Other frames and boards are outside that set.
|
||||
|
||||
The runner sets a deterministic `9:41` status bar, charged battery, Wi-Fi, and
|
||||
cellular state. It sets the iPad orientation and captures its landscape display
|
||||
with `devicectl`, verifying `2752x2064` before acknowledging each capture. The
|
||||
iPhone uses `simctl` screenshots. No post-capture rotation is required.
|
||||
Each locale process receives a fresh `TERMIND_MARKETING_CAPTURE_ID`; the runner
|
||||
ignores ready markers from earlier processes. Captures are staged under the
|
||||
derived-data directory and copied to the source tree only after the tests and
|
||||
expected capture count pass. Back up raw images and a SHA-256 manifest before
|
||||
running a refresh.
|
||||
|
||||
Whenever SFTP, Metrics, Assistant navigation, or system chrome changes, recapture
|
||||
the affected device and every locale. A marketing fixture may be updated to expose
|
||||
@@ -136,6 +160,8 @@ mounts a second RootView or duplicates a terminal/transcript surface. Before it
|
||||
publishes the ready marker, it requires both `NSApp.isActive` and
|
||||
`window.isKeyWindow`, allows the focused appearance to render, and requires native
|
||||
2× backing scale. Focus is checked again when the capture is acknowledged.
|
||||
The focus retry loop also requires activation to survive the 300 ms appearance
|
||||
render interval. Do not run another Mac app activation alongside a capture.
|
||||
|
||||
Generate all three localized Workspace sources:
|
||||
|
||||
@@ -459,3 +485,57 @@ The following failures persisted in unchanged tests:
|
||||
|
||||
Logs and `.xcresult` bundles remain in `.build/marketing-forwarding-refresh/`.
|
||||
No production behavior or unrelated tests were changed to address these failures.
|
||||
|
||||
The later 2026-09-19 navigation topbar refresh replaced Mac Metrics overview and
|
||||
Vault in all three languages (`2360x1480`) and iPad SSH/SFTP in all three languages
|
||||
(`2752x2064`, iPadOS 27.0, iPad Pro 13-inch M5 landscape, 9:41). All six Mac sources
|
||||
passed active/key checks before capture and after acknowledgement; visual and
|
||||
pixel checks confirmed colored traffic lights and the current `plus` icons.
|
||||
The iPad captures use the actual root/workspace views, with Workspaces selected,
|
||||
the sidebar collapsed and the native topbar and bottom controls fully visible.
|
||||
|
||||
Only the six matching iPad device frames (`2960x2290`) were regenerated. In the
|
||||
verified `Termind` Figma file, the refresh replaced Mac boards 03/05, iPad boards
|
||||
02/03 and the iPad SFTP image in every platform's board 01, across all locales.
|
||||
English was reviewed first. Readback confirmed all 21 image fills match their
|
||||
local source PNG bytes, and every other reported node property stayed unchanged,
|
||||
including copy, fonts, geometry, FIT, shadows and stacking. The 21 JPG exports use
|
||||
`save_screenshots` at 1×, retain the original dimensions and have opaque RGB pixels
|
||||
with embedded sRGB profiles. Profile metadata was added without re-encoding or
|
||||
changing decoded pixels.
|
||||
|
||||
The complete pre-capture image backup, SHA-256 manifests, focus evidence, Figma
|
||||
readbacks, contact sheets and validation logs are in
|
||||
`.build/marketing-topbar-refresh/`. Exact-path auditing found 39 changed and 93
|
||||
unchanged images, with no additions or removals: 45 captures, 33 device frames and
|
||||
54 App Store JPGs. A profile audit also found 23 untouched baseline JPGs without an
|
||||
embedded ICC profile; this corrects the earlier blanket statement about all 54
|
||||
files being tagged. Those 23 originals remain byte-identical to the backup.
|
||||
|
||||
The website rebuilt successfully (17 pages, no errors or warnings), using the
|
||||
existing image references. The English, Simplified Chinese and Traditional Chinese
|
||||
homepages were checked visually, including their hero and SFTP images; `/zh/`
|
||||
redirects to `/zh-hans/`. Full Swift formatting, Chinese localization, screenshot
|
||||
shell syntax, generator Python syntax and whitespace checks passed. The Xcode
|
||||
project and resolved package versions remain unchanged. Python bytecode caches
|
||||
are now ignored by Git.
|
||||
|
||||
All nine opt-in capture runs passed (six Mac runs and three iPad locale runs,
|
||||
producing 12 sources). The complete TFM run finished with 1,719 passed tests,
|
||||
one failed test and 23 skipped tests. Its only failure is the previously recorded
|
||||
`MetricsDetailToolbarTests.legacyCompactSearchOpensTheNativeFieldAndKeepsItsQueryWhenWidened`
|
||||
in all three language cases. `ScreenshotDemoTests`, `AppLanguageTests` and all
|
||||
352 shared-framework tests passed.
|
||||
|
||||
The TFI run completed 328 app tests and all 352 shared-framework tests: 679 passed
|
||||
tests and one failed test. The unchanged
|
||||
`MobileAssistantInteractionRenderingTests.interactionTextRemainsVisibleOnMaterial`
|
||||
reported 12 OCR mismatches on iPadOS 27.0, including `Deployment` read as
|
||||
`Deplovment` and `prod.example` read as `oroo.examole`. The existing capture build
|
||||
excluded `MobileNavigationParityTests`; a normal pinned build and separate run of
|
||||
all four tests passed, and the obsolete exclusion was removed from the runner.
|
||||
After both main suites had finished, Xcode stalled collecting simulator diagnostics;
|
||||
only its `simctl diagnose` child was stopped so the complete `.xcresult` could
|
||||
finalize. No tests were interrupted. Both full-run result bundles and the
|
||||
navigation supplement are retained in the evidence directory. Product code and
|
||||
unrelated test assertions were not changed to address the failures.
|
||||
@@ -54,10 +54,12 @@ run_capture() {
|
||||
zh-Hans) region=CN; apple_locale=zh_CN ;;
|
||||
zh-Hant) region=TW; apple_locale=zh_TW ;;
|
||||
esac
|
||||
local capture_id="$(uuidgen)"
|
||||
local localized_xctestrun="$derived_data/Build/Products/Marketing-TFI-$language.xctestrun"
|
||||
cp "$marketing_xctestrun" "$localized_xctestrun"
|
||||
/usr/libexec/PlistBuddy \
|
||||
-c "Add :TFITests:EnvironmentVariables:TERMIND_MARKETING_LOCALE string $language" \
|
||||
-c "Add :TFITests:EnvironmentVariables:TERMIND_MARKETING_CAPTURE_ID string $capture_id" \
|
||||
-c "Set :TFITests:TestLanguage $language" \
|
||||
-c "Set :TFITests:TestRegion $region" \
|
||||
-c 'Add :TFITests:CommandLineArguments:0 string -AppleLanguages' \
|
||||
@@ -74,25 +76,28 @@ run_capture() {
|
||||
local test_pid=$!
|
||||
local captures=0
|
||||
local last_token=""
|
||||
local capture_directory="$derived_data/captures/$device_id-$language"
|
||||
local -a captured_paths destinations
|
||||
mkdir -p "$capture_directory"
|
||||
|
||||
while kill -0 "$test_pid" 2>/dev/null; do
|
||||
local app_container
|
||||
app_container="$(xcrun simctl get_app_container "$device_id" com.akinokaede.termind data 2>/dev/null || true)"
|
||||
local ready="$app_container/Documents/marketing-capture-control/ready"
|
||||
if [[ -n "$app_container" && -f "$ready" ]]; then
|
||||
local feature device locale token
|
||||
IFS=$'\t' read -r feature device locale token < "$ready"
|
||||
if [[ -n "$token" && "$token" != "$last_token" ]]; then
|
||||
local feature device locale token request_capture_id
|
||||
IFS=$'\t' read -r feature device locale token request_capture_id < "$ready"
|
||||
if [[ "$request_capture_id" == "$capture_id" && -n "$token" && "$token" != "$last_token" ]]; then
|
||||
local destination="$output_directory/$feature/$device-$locale.png"
|
||||
mkdir -p "${destination:h}"
|
||||
local captured="$capture_directory/$feature.png"
|
||||
if [[ "$device" == "ipad" ]]; then
|
||||
rm -f "$destination"
|
||||
rm -f "$captured"
|
||||
xcrun devicectl device capture screenshot \
|
||||
--device "$device_id" \
|
||||
--destination "$destination" >/dev/null
|
||||
--destination "$captured" >/dev/null
|
||||
local width height
|
||||
width="$(sips -g pixelWidth "$destination" | awk '/pixelWidth/ { print $2 }')"
|
||||
height="$(sips -g pixelHeight "$destination" | awk '/pixelHeight/ { print $2 }')"
|
||||
width="$(sips -g pixelWidth "$captured" | awk '/pixelWidth/ { print $2 }')"
|
||||
height="$(sips -g pixelHeight "$captured" | awk '/pixelHeight/ { print $2 }')"
|
||||
if [[ "$width" != "2752" || "$height" != "2064" ]]; then
|
||||
print -u2 "Unexpected iPad screenshot dimensions: ${width}x${height}"
|
||||
kill "$test_pid" 2>/dev/null || true
|
||||
@@ -101,8 +106,10 @@ run_capture() {
|
||||
exit 70
|
||||
fi
|
||||
else
|
||||
xcrun simctl io "$device_id" screenshot --type=png "$destination" >/dev/null
|
||||
xcrun simctl io "$device_id" screenshot --type=png "$captured" >/dev/null
|
||||
fi
|
||||
captured_paths+=("$captured")
|
||||
destinations+=("$destination")
|
||||
touch "$app_container/Documents/marketing-capture-control/ack-$token"
|
||||
last_token="$token"
|
||||
(( captures += 1 ))
|
||||
@@ -123,12 +130,24 @@ run_capture() {
|
||||
print -u2 "Expected $expected captures for $name ($language), received $captures. See $test_log"
|
||||
exit 70
|
||||
fi
|
||||
for (( index = 1; index <= ${#captured_paths}; index++ )); do
|
||||
mkdir -p "${destinations[$index]:h}"
|
||||
cp "$captured_paths[$index]" "$destinations[$index]"
|
||||
done
|
||||
}
|
||||
|
||||
mkdir -p "$output_directory"
|
||||
build_device_id="$(find_device "iPhone 17 Pro Max")"
|
||||
if [[ ",$marketing_devices," == *,iphone,* ]]; then
|
||||
build_device_name="iPhone 17 Pro Max"
|
||||
elif [[ ",$marketing_devices," == *,ipad,* ]]; then
|
||||
build_device_name="iPad Pro 13-inch (M5)"
|
||||
else
|
||||
print -u2 "Select at least one marketing device: iphone,ipad"
|
||||
exit 64
|
||||
fi
|
||||
build_device_id="$(find_device "$build_device_name")"
|
||||
if [[ -z "$build_device_id" ]]; then
|
||||
print -u2 "Required simulator is unavailable: iPhone 17 Pro Max ($marketing_runtime)"
|
||||
print -u2 "Required simulator is unavailable: $build_device_name ($marketing_runtime)"
|
||||
exit 69
|
||||
fi
|
||||
xcodebuild \
|
||||
@@ -137,7 +156,6 @@ xcodebuild \
|
||||
-destination "id=$build_device_id" \
|
||||
-onlyUsePackageVersionsFromResolvedFile \
|
||||
-derivedDataPath "$derived_data" \
|
||||
EXCLUDED_SOURCE_FILE_NAMES=MobileNavigationParityTests.swift \
|
||||
build-for-testing
|
||||
|
||||
generated_xctestrun="$(find "$derived_data/Build/Products" -maxdepth 1 -name 'TFI_*.xctestrun' -print -quit)"
|
||||
|
||||
@@ -4,6 +4,7 @@ import SwiftUI
|
||||
import TermindApplication
|
||||
import TermindDomain
|
||||
import TermindPresentation
|
||||
import TermindRemoteInfrastructure
|
||||
import Testing
|
||||
import UIKit
|
||||
|
||||
@@ -34,7 +35,9 @@ struct MobileMarketingScreenshotTests {
|
||||
}
|
||||
}
|
||||
|
||||
if includes("sftp") {
|
||||
if includes("sftp"), UIDevice.current.userInterfaceIdiom == .pad {
|
||||
try await captureWorkspace(.sftp, language: language)
|
||||
} else if includes("sftp") {
|
||||
let sftp = MobileSFTPSessionModel()
|
||||
try await sftp.attach(
|
||||
fileSystem: MarketingFileSystem(), path: "/srv/termind/releases/2026.09",
|
||||
@@ -63,8 +66,10 @@ struct MobileMarketingScreenshotTests {
|
||||
}
|
||||
}
|
||||
|
||||
if includes("ssh") {
|
||||
let terminal = makeTerminal(language: language)
|
||||
if includes("ssh"), UIDevice.current.userInterfaceIdiom == .pad {
|
||||
try await captureWorkspace(.ssh, language: language)
|
||||
} else if includes("ssh") {
|
||||
let terminal = makeTerminal()
|
||||
try await capture("ssh", language: language) {
|
||||
NavigationStack { MarketingTerminalScene(terminal: terminal, language: language) }
|
||||
}
|
||||
@@ -96,8 +101,10 @@ struct MobileMarketingScreenshotTests {
|
||||
}
|
||||
}
|
||||
|
||||
private func makeApp(language: String) throws -> MobileAppModel {
|
||||
let app = try MobileAppModel(inMemory: true)
|
||||
private func makeApp(
|
||||
language: String, connectionServices: MobileWorkspaceConnectionServices = .live
|
||||
) throws -> MobileAppModel {
|
||||
let app = try MobileAppModel(inMemory: true, workspaceConnectionServices: connectionServices)
|
||||
let production = HostGroup(name: text(language, "Production", "生产环境", "正式環境"))
|
||||
let edge = HostGroup(name: text(language, "Edge", "边缘节点", "邊緣節點"))
|
||||
app.library.upsert(production)
|
||||
@@ -122,12 +129,52 @@ struct MobileMarketingScreenshotTests {
|
||||
return app
|
||||
}
|
||||
|
||||
private func makeTerminal(language: String) -> TerminalViewState {
|
||||
private func captureWorkspace(_ kind: ConnectionHistoryEntry.Kind, language: String) async throws {
|
||||
let output = terminalOutput
|
||||
let services = MobileWorkspaceConnectionServices(
|
||||
makeSSHSession: { receive, exit, shellIntegration in
|
||||
receive(output)
|
||||
return SSHSessionActor(output: receive, exit: exit, shellIntegration: shellIntegration)
|
||||
},
|
||||
connectSSH: { _, _, _, _, _, _ in },
|
||||
connectSFTP: { _, _, _ in MarketingFileSystem() },
|
||||
writeSSH: { _, _ in }
|
||||
)
|
||||
let app = try makeApp(language: language, connectionServices: services)
|
||||
defer { app.closeAll() }
|
||||
let identity = Identity(name: "Deploy", username: "deploy")
|
||||
try app.library.upsert(identity, passwordUpdate: .replace("marketing-fixture-only"))
|
||||
var host = try #require(app.library.hosts.first(where: { $0.name == "web-01" }))
|
||||
host.identityID = identity.id
|
||||
app.library.upsert(host)
|
||||
let id = try #require(app.open(hostID: host.id, kind: kind))
|
||||
let workspace = try #require(app.workspaces.first(where: { $0.id == id }))
|
||||
for _ in 0..<200 where !workspace.connected {
|
||||
try await Task.sleep(for: .milliseconds(25))
|
||||
}
|
||||
try #require(workspace.connected)
|
||||
if kind == .sftp {
|
||||
#expect(app.renameWorkspace(id, to: text(language, "Production files", "生产文件", "正式環境檔案")))
|
||||
try #require(workspace.sftpSession.files.count == 6)
|
||||
}
|
||||
let scene = MobileSceneModel(app: app)
|
||||
scene.selectWorkspace(id)
|
||||
try await capture(kind == .ssh ? "ssh" : "sftp", language: language) {
|
||||
MobileRootView(app: app, scene: scene)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTerminal() -> TerminalViewState {
|
||||
let backend = InMemoryTerminalSession(write: { _ in }, resize: { _ in })
|
||||
let terminal = TerminalViewState(configSource: .none)
|
||||
terminal.configuration = TerminalSurfaceOptions(backend: .inMemory(backend), resizeThrottleMilliseconds: 0)
|
||||
let surface = MobileTerminalSurface()
|
||||
terminal.makePlatformView = { surface.view }
|
||||
backend.receive(terminalOutput)
|
||||
return terminal
|
||||
}
|
||||
|
||||
private var terminalOutput: Data {
|
||||
let prompt = "\u{001B}[36mdeploy@web-01\u{001B}[0m \u{001B}[34m/var/log/nginx\u{001B}[0m %"
|
||||
let lines = [
|
||||
"Last login: Sun Sep 13 14:20:06 from 10.0.0.12",
|
||||
@@ -141,8 +188,7 @@ struct MobileMarketingScreenshotTests {
|
||||
"10.0.4.31 GET /assets/app 304 2ms",
|
||||
prompt,
|
||||
]
|
||||
backend.receive(Data(lines.joined(separator: "\r\n").utf8))
|
||||
return terminal
|
||||
return Data(lines.joined(separator: "\r\n").utf8)
|
||||
}
|
||||
|
||||
private func makeMetrics(language: String) -> [ServerMetric] {
|
||||
@@ -203,7 +249,7 @@ struct MobileMarketingScreenshotTests {
|
||||
|
||||
if isPad, let tabs = tabController(in: controller) {
|
||||
// Keep each scene's navigation presentation independent of restored simulator state.
|
||||
tabs.sidebar.isHidden = name == "metrics"
|
||||
tabs.sidebar.isHidden = ["metrics", "ssh", "sftp"].contains(name)
|
||||
try await Task.sleep(for: .milliseconds(900))
|
||||
}
|
||||
|
||||
@@ -247,7 +293,9 @@ struct MobileMarketingScreenshotTests {
|
||||
let locale = language == "zh-Hans" ? "zh-hans" : language == "zh-Hant" ? "zh-hant" : "en"
|
||||
let ready = control.appending(path: "ready")
|
||||
let ack = control.appending(path: "ack-\(token)")
|
||||
try "\(name)\t\(device)\t\(locale)\t\(token)\n".write(to: ready, atomically: true, encoding: .utf8)
|
||||
let captureID = try #require(ProcessInfo.processInfo.environment["TERMIND_MARKETING_CAPTURE_ID"])
|
||||
try "\(name)\t\(device)\t\(locale)\t\(token)\t\(captureID)\n".write(
|
||||
to: ready, atomically: true, encoding: .utf8)
|
||||
|
||||
for _ in 0..<300 {
|
||||
if FileManager.default.fileExists(atPath: ack.path) {
|
||||
@@ -381,7 +429,7 @@ private struct MarketingMetricsScene: View {
|
||||
}
|
||||
}
|
||||
|
||||
private actor MarketingFileSystem: RemoteFileSystem {
|
||||
private actor MarketingFileSystem: SFTPClientProtocol {
|
||||
private let files: [RemoteFile] = [
|
||||
RemoteFile(
|
||||
path: "/srv/termind/releases/2026.09/bin", name: "bin", kind: .directory, size: 0,
|
||||
@@ -404,6 +452,9 @@ private actor MarketingFileSystem: RemoteFileSystem {
|
||||
modifiedAt: Date(timeIntervalSince1970: 1_788_942_000), permissions: 0o755, ownerUID: 501, ownerGID: 20),
|
||||
]
|
||||
|
||||
func connect(using _: SSHConnectionConfiguration, progress _: SSHRouteProgressHandler?) async throws {}
|
||||
func disconnect() async {}
|
||||
func homeDirectory() async throws -> String { "/srv/termind/releases/2026.09" }
|
||||
func listDirectory(at _: String) async throws -> [RemoteFile] { files }
|
||||
func attributes(at _: String) async throws -> RemoteFile? { nil }
|
||||
func readFile(at _: String, maximumBytes _: Int) async throws -> Data { Data() }
|
||||
@@ -413,4 +464,12 @@ private actor MarketingFileSystem: RemoteFileSystem {
|
||||
func rename(from _: String, to _: String, overwrite _: Bool) async throws {}
|
||||
func removeFile(at _: String) async throws {}
|
||||
func removeDirectory(at _: String) async throws {}
|
||||
func uploadFile(
|
||||
from _: URL, to _: String, overwrite _: Bool, cancellation _: TransferCancellationToken,
|
||||
progress _: @escaping @Sendable (SFTPProgress) -> Void
|
||||
) async throws {}
|
||||
func downloadFile(
|
||||
from _: String, to _: URL, overwrite _: Bool, cancellation _: TransferCancellationToken,
|
||||
progress _: @escaping @Sendable (SFTPProgress) -> Void
|
||||
) async throws {}
|
||||
}
|
||||
@@ -27,6 +27,10 @@ import Testing
|
||||
window.contentView?.layoutSubtreeIfNeeded()
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
window.contentView?.layoutSubtreeIfNeeded()
|
||||
if configuration.scene == .hosts {
|
||||
window.initialFirstResponder = nil
|
||||
window.makeFirstResponder(nil)
|
||||
}
|
||||
|
||||
for _ in 0..<100 {
|
||||
NSRunningApplication.current.activate(options: [.activateAllWindows])
|
||||
@@ -34,19 +38,18 @@ import Testing
|
||||
window.orderFrontRegardless()
|
||||
window.makeMain()
|
||||
window.makeKey()
|
||||
if NSApp.isActive, window.isKeyWindow { break }
|
||||
if NSApp.isActive, window.isKeyWindow {
|
||||
// Activation can be transient while test hosts launch and exit.
|
||||
// Require the focused appearance to stay active through rendering.
|
||||
try await Task.sleep(for: .milliseconds(300))
|
||||
if NSApp.isActive, window.isKeyWindow { break }
|
||||
}
|
||||
try await Task.sleep(for: .milliseconds(50))
|
||||
}
|
||||
guard NSApp.isActive, window.isKeyWindow else {
|
||||
Issue.record("Marketing window could not become the focused key window")
|
||||
return
|
||||
}
|
||||
if configuration.scene == .hosts {
|
||||
window.initialFirstResponder = nil
|
||||
window.makeFirstResponder(nil)
|
||||
}
|
||||
// Allow the active appearance to render before the external capture.
|
||||
try await Task.sleep(for: .milliseconds(300))
|
||||
try #require(NSApp.isActive && window.isKeyWindow)
|
||||
try #require(window.backingScaleFactor == 2)
|
||||
|
||||
|
||||
Reference in new issue
Block a user