Merge pull request #1582 from manaflow-ai/task-browser-import-followups
fix: browser import profile follow-up regressions
This commit is contained in:
commit
43d1fd419e
13 changed files with 2116 additions and 193 deletions
|
|
@ -805,6 +805,193 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"debug.menu.browserToolbarButtonSpacing": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Browser Toolbar Button Spacing"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザーツールバーのボタン間隔"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.menu.browserProfilePopoverDebug": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Browser Profile Popover Debug…"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザープロファイルポップオーバーのデバッグ…"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.windows.browserProfilePopover.title": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Browser Profile Popover Debug"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザープロファイルポップオーバーのデバッグ"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.browserProfilePopover.heading": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Browser Profile Popover"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザープロファイルポップオーバー"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.browserProfilePopover.note": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Tune the profile popover padding live while comparing it against the browser toolbar menu."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザーツールバーのメニューと見比べながら、プロファイルポップオーバーの余白をライブで調整します。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.browserProfilePopover.group.padding": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Padding"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "余白"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.browserProfilePopover.label.horizontal": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Horizontal"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "水平"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.browserProfilePopover.label.vertical": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Vertical"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "垂直"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.browserProfilePopover.group.preview": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Preview"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "プレビュー"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.browserProfilePopover.reset": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Reset"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "リセット"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.browserProfilePopover.liveNote": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Changes apply live to the browser profile popover."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "変更はブラウザープロファイルポップオーバーにライブで反映されます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug.devBuildBanner.title": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
@ -4740,13 +4927,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Bookmarks, settings, and extensions import are not available yet."
|
||||
"value": "Bookmarks, settings, and extensions are not available yet."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブックマーク、設定、拡張機能のインポートにはまだ対応していません。"
|
||||
"value": "ブックマーク、設定、拡張機能はまだ利用できません。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4797,7 +4984,7 @@
|
|||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザ: %@"
|
||||
"value": "ブラウザー: %@"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5029,13 +5216,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmux destination"
|
||||
"value": "Destination"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmux の保存先"
|
||||
"value": "保存先"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5080,13 +5267,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Imported cookies and history go into the selected cmux browser profile."
|
||||
"value": "Imported data goes into the selected cmux profile."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "インポートしたCookieと履歴は、選択したcmuxブラウザープロファイルに保存されます。"
|
||||
"value": "インポートしたデータは、選択した cmux プロファイルに保存されます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5097,13 +5284,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "All selected source profiles will be merged into the chosen cmux browser profile."
|
||||
"value": "All selected source profiles go into one cmux profile."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "選択した元プロファイルはすべて、選んだ cmux ブラウザープロファイルにまとめて取り込まれます。"
|
||||
"value": "選択した元プロファイルは、1つの cmux プロファイルにまとめて取り込まれます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5114,13 +5301,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Missing cmux profiles are created when import starts."
|
||||
"value": "Missing cmux profiles are created on import."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "不足している cmux プロファイルは、インポート開始時に作成されます。"
|
||||
"value": "不足している cmux プロファイルは、インポート時に作成されます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5131,13 +5318,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Merge all into one cmux profile"
|
||||
"value": "Merge into one"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "すべてを1つの cmux プロファイルにまとめる"
|
||||
"value": "1つにまとめる"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5148,13 +5335,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Keep profiles separate"
|
||||
"value": "Separate profiles"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "プロファイルを分けたまま取り込む"
|
||||
"value": "分けて取り込む"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5233,13 +5420,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Limit to"
|
||||
"value": "Domains"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "対象ドメイン"
|
||||
"value": "ドメイン"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5250,13 +5437,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Optional domains only (e.g. github.com, openai.com)"
|
||||
"value": "Optional domains, comma-separated"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "任意のドメインのみ(例: github.com, openai.com)"
|
||||
"value": "任意のドメインをカンマ区切りで指定"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5505,13 +5692,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Source"
|
||||
"value": "Browser"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "インポート元"
|
||||
"value": "ブラウザー"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5539,13 +5726,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Source Profiles"
|
||||
"value": "Profiles"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "元プロファイル"
|
||||
"value": "プロファイル"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5556,13 +5743,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Choose one or more source profiles. Step 3 lets you keep them separate or merge them into one cmux profile."
|
||||
"value": "Select one or more profiles."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "元プロファイルを1つ以上選択してください。3 / 3 で、分けたまま取り込むか、1つの cmux プロファイルにまとめるかを選べます。"
|
||||
"value": "1つ以上のプロファイルを選択してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5607,13 +5794,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Step 3 of 3: Choose what to import from %@ and where to put it."
|
||||
"value": "Step 3 of 3"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "3 / 3: %@ から何をインポートし、どこに保存するかを選択します。"
|
||||
"value": "3 / 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5624,13 +5811,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Step 1 of 3: Choose the browser to import from."
|
||||
"value": "Step 1 of 3"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "1 / 3: インポート元のブラウザーを選択します。"
|
||||
"value": "1 / 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5641,13 +5828,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Step 2 of 3: Choose source profiles from %@."
|
||||
"value": "Step 2 of 3"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "2 / 3: %@ の元プロファイルを選択します。"
|
||||
"value": "2 / 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5669,6 +5856,125 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.dismiss": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Hide Hint"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ヒントを隠す"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.import": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Import…"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "インポート…"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.settings": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Browser Settings"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザー設定"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.settingsFootnote": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "You can always find this in Settings > Browser."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "あとでいつでも「設定 > ブラウザー」で見つけられます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.title": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Import browser data"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザーデータをインポート"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.toolbar": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Import"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "インポート"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.hint.toolbar.help": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Import browser data"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザーデータをインポート"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser.import.validation.scope": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
@ -37978,7 +38284,7 @@
|
|||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザから取り込む…"
|
||||
"value": "ブラウザーから取り込む…"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50794,7 +51100,7 @@
|
|||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザデータを取り込む"
|
||||
"value": "ブラウザーデータを取り込む"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50924,7 +51230,7 @@
|
|||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ブラウザから取り込む"
|
||||
"value": "ブラウザーから取り込む"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50963,6 +51269,74 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.import.hint.note.hidden": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "The blank-tab import hint is hidden. Turn it back on here any time."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "空タブのインポート案内は非表示です。ここでいつでも再表示できます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.import.hint.note.settingsOnly": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Blank tabs are currently using Settings only mode from the debug window."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "現在、空タブはデバッグウィンドウの「設定のみ」モードになっています。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.import.hint.note.visible": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Blank browser tabs can show this import suggestion. Hide or re-enable it here."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "空のブラウザータブにこのインポート案内を表示できます。ここで非表示や再表示を切り替えられます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.import.hint.show": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Show import hint on blank browser tabs"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "空のブラウザータブにインポート案内を表示"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.browser.history.clearButton": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
|
|||
|
|
@ -2301,6 +2301,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
// In UI tests, `WindowGroup` occasionally fails to materialize a window quickly on the VM.
|
||||
// If there are no windows shortly after launch, force-create one so XCUITest can proceed.
|
||||
if isRunningUnderXCTest {
|
||||
if let rawVariant = env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_VARIANT"] {
|
||||
UserDefaults.standard.set(
|
||||
BrowserImportHintSettings.variant(for: rawVariant).rawValue,
|
||||
forKey: BrowserImportHintSettings.variantKey
|
||||
)
|
||||
}
|
||||
if let rawShow = env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_SHOW"] {
|
||||
UserDefaults.standard.set(
|
||||
rawShow == "1",
|
||||
forKey: BrowserImportHintSettings.showOnBlankTabsKey
|
||||
)
|
||||
}
|
||||
if let rawDismissed = env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_DISMISSED"] {
|
||||
UserDefaults.standard.set(
|
||||
rawDismissed == "1",
|
||||
forKey: BrowserImportHintSettings.dismissedKey
|
||||
)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
|
||||
guard let self else { return }
|
||||
if NSApp.windows.isEmpty {
|
||||
|
|
@ -2309,6 +2327,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
|
||||
self.writeUITestDiagnosticsIfNeeded(stage: "afterForceWindow")
|
||||
}
|
||||
if env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_OPEN_BLANK_BROWSER"] == "1" {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { [weak self] in
|
||||
guard let self else { return }
|
||||
_ = self.openBrowserAndFocusAddressBar(insertAtEnd: true)
|
||||
}
|
||||
}
|
||||
if env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_OPEN_SETTINGS"] == "1" {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.55) { [weak self] in
|
||||
self?.openPreferencesWindow(
|
||||
debugSource: "uiTest.browserImportHint",
|
||||
navigationTarget: .browser
|
||||
)
|
||||
}
|
||||
}
|
||||
if env["CMUX_UI_TEST_BROWSER_IMPORT_AUTO_OPEN"] == "1" {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9144,6 +9144,7 @@ private final class FeedbackComposerMessageEditorView: NSView {
|
|||
}
|
||||
|
||||
private enum SidebarHelpMenuAction {
|
||||
case importBrowserData
|
||||
case keyboardShortcuts
|
||||
case docs
|
||||
case changelog
|
||||
|
|
@ -9714,6 +9715,12 @@ private struct SidebarHelpMenuButton: View {
|
|||
accessibilityIdentifier: "SidebarHelpMenuOptionKeyboardShortcuts",
|
||||
isExternalLink: false
|
||||
)
|
||||
helpOptionButton(
|
||||
title: String(localized: "menu.view.importFromBrowser", defaultValue: "Import From Browser…"),
|
||||
action: .importBrowserData,
|
||||
accessibilityIdentifier: "SidebarHelpMenuOptionImportBrowserData",
|
||||
isExternalLink: false
|
||||
)
|
||||
if docsURL != nil {
|
||||
helpOptionButton(
|
||||
title: String(localized: "about.docs", defaultValue: "Docs"),
|
||||
|
|
@ -9818,6 +9825,11 @@ private struct SidebarHelpMenuButton: View {
|
|||
|
||||
private func perform(_ action: SidebarHelpMenuAction) {
|
||||
switch action {
|
||||
case .importBrowserData:
|
||||
isPopoverPresented = false
|
||||
DispatchQueue.main.async {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
}
|
||||
case .keyboardShortcuts:
|
||||
isPopoverPresented = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) {
|
||||
|
|
|
|||
|
|
@ -212,6 +212,111 @@ enum BrowserThemeSettings {
|
|||
}
|
||||
}
|
||||
|
||||
enum BrowserImportHintVariant: String, CaseIterable, Identifiable {
|
||||
case inlineStrip
|
||||
case floatingCard
|
||||
case toolbarChip
|
||||
case settingsOnly
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
enum BrowserImportHintBlankTabPlacement: Equatable {
|
||||
case hidden
|
||||
case inlineStrip
|
||||
case floatingCard
|
||||
case toolbarChip
|
||||
}
|
||||
|
||||
enum BrowserImportHintSettingsStatus: Equatable {
|
||||
case visible
|
||||
case hidden
|
||||
case settingsOnly
|
||||
}
|
||||
|
||||
struct BrowserImportHintPresentation: Equatable {
|
||||
let blankTabPlacement: BrowserImportHintBlankTabPlacement
|
||||
let settingsStatus: BrowserImportHintSettingsStatus
|
||||
|
||||
init(
|
||||
variant: BrowserImportHintVariant,
|
||||
showOnBlankTabs: Bool,
|
||||
isDismissed: Bool
|
||||
) {
|
||||
if variant == .settingsOnly {
|
||||
blankTabPlacement = .hidden
|
||||
settingsStatus = .settingsOnly
|
||||
return
|
||||
}
|
||||
|
||||
if !showOnBlankTabs || isDismissed {
|
||||
blankTabPlacement = .hidden
|
||||
settingsStatus = .hidden
|
||||
return
|
||||
}
|
||||
|
||||
switch variant {
|
||||
case .inlineStrip:
|
||||
blankTabPlacement = .inlineStrip
|
||||
case .floatingCard:
|
||||
blankTabPlacement = .floatingCard
|
||||
case .toolbarChip:
|
||||
blankTabPlacement = .toolbarChip
|
||||
case .settingsOnly:
|
||||
blankTabPlacement = .hidden
|
||||
}
|
||||
settingsStatus = .visible
|
||||
}
|
||||
}
|
||||
|
||||
enum BrowserImportHintSettings {
|
||||
static let variantKey = "browserImportHintVariant"
|
||||
static let showOnBlankTabsKey = "browserImportHintShowOnBlankTabs"
|
||||
static let dismissedKey = "browserImportHintDismissed"
|
||||
static let defaultVariant: BrowserImportHintVariant = .toolbarChip
|
||||
static let defaultShowOnBlankTabs = true
|
||||
static let defaultDismissed = false
|
||||
|
||||
static func variant(for rawValue: String?) -> BrowserImportHintVariant {
|
||||
guard let rawValue, let variant = BrowserImportHintVariant(rawValue: rawValue) else {
|
||||
return defaultVariant
|
||||
}
|
||||
return variant
|
||||
}
|
||||
|
||||
static func variant(defaults: UserDefaults = .standard) -> BrowserImportHintVariant {
|
||||
variant(for: defaults.string(forKey: variantKey))
|
||||
}
|
||||
|
||||
static func showOnBlankTabs(defaults: UserDefaults = .standard) -> Bool {
|
||||
if defaults.object(forKey: showOnBlankTabsKey) == nil {
|
||||
return defaultShowOnBlankTabs
|
||||
}
|
||||
return defaults.bool(forKey: showOnBlankTabsKey)
|
||||
}
|
||||
|
||||
static func isDismissed(defaults: UserDefaults = .standard) -> Bool {
|
||||
if defaults.object(forKey: dismissedKey) == nil {
|
||||
return defaultDismissed
|
||||
}
|
||||
return defaults.bool(forKey: dismissedKey)
|
||||
}
|
||||
|
||||
static func presentation(defaults: UserDefaults = .standard) -> BrowserImportHintPresentation {
|
||||
BrowserImportHintPresentation(
|
||||
variant: variant(defaults: defaults),
|
||||
showOnBlankTabs: showOnBlankTabs(defaults: defaults),
|
||||
isDismissed: isDismissed(defaults: defaults)
|
||||
)
|
||||
}
|
||||
|
||||
static func reset(defaults: UserDefaults = .standard) {
|
||||
defaults.set(defaultVariant.rawValue, forKey: variantKey)
|
||||
defaults.set(defaultShowOnBlankTabs, forKey: showOnBlankTabsKey)
|
||||
defaults.set(defaultDismissed, forKey: dismissedKey)
|
||||
}
|
||||
}
|
||||
|
||||
struct BrowserProfileDefinition: Codable, Hashable, Identifiable, Sendable {
|
||||
let id: UUID
|
||||
var displayName: String
|
||||
|
|
@ -2391,11 +2496,41 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
webView.onContextMenuOpenLinkInNewTab = { [weak self] url in
|
||||
self?.openLinkInNewTab(url: url)
|
||||
}
|
||||
configureNavigationDelegateCallbacks()
|
||||
webView.navigationDelegate = navigationDelegate
|
||||
webView.uiDelegate = uiDelegate
|
||||
setupObservers(for: webView)
|
||||
}
|
||||
|
||||
private func configureNavigationDelegateCallbacks() {
|
||||
guard let navigationDelegate else { return }
|
||||
let boundWebViewInstanceID = webViewInstanceID
|
||||
let boundHistoryStore = historyStore
|
||||
|
||||
navigationDelegate.didFinish = { [weak self] webView in
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self, self.isCurrentWebView(webView, instanceID: boundWebViewInstanceID) else { return }
|
||||
boundHistoryStore.recordVisit(url: webView.url, title: webView.title)
|
||||
self.refreshFavicon(from: webView)
|
||||
self.applyBrowserThemeModeIfNeeded()
|
||||
// Keep find-in-page open through load completion and refresh matches for the new DOM.
|
||||
self.restoreFindStateAfterNavigation(replaySearch: true)
|
||||
}
|
||||
}
|
||||
navigationDelegate.didFailNavigation = { [weak self] failedWebView, failedURL in
|
||||
Task { @MainActor in
|
||||
guard let self, self.isCurrentWebView(failedWebView, instanceID: boundWebViewInstanceID) else { return }
|
||||
// Clear stale title/favicon from the previous page so the tab
|
||||
// shows the failed URL instead of the old page's branding.
|
||||
self.pageTitle = failedURL.isEmpty ? "" : failedURL
|
||||
self.faviconPNGData = nil
|
||||
self.lastFaviconURLString = nil
|
||||
// Keep find-in-page open and clear stale counters on failed loads.
|
||||
self.restoreFindStateAfterNavigation(replaySearch: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func isCurrentWebView(_ candidate: WKWebView, instanceID: UUID? = nil) -> Bool {
|
||||
guard candidate === webView else { return false }
|
||||
guard let instanceID else { return true }
|
||||
|
|
@ -2438,30 +2573,6 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
|
||||
// Set up navigation delegate
|
||||
let navDelegate = BrowserNavigationDelegate()
|
||||
navDelegate.didFinish = { webView in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.historyStore.recordVisit(url: webView.url, title: webView.title)
|
||||
}
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self, self.isCurrentWebView(webView) else { return }
|
||||
self.refreshFavicon(from: webView)
|
||||
self.applyBrowserThemeModeIfNeeded()
|
||||
// Keep find-in-page open through load completion and refresh matches for the new DOM.
|
||||
self.restoreFindStateAfterNavigation(replaySearch: true)
|
||||
}
|
||||
}
|
||||
navDelegate.didFailNavigation = { [weak self] failedWebView, failedURL in
|
||||
Task { @MainActor in
|
||||
guard let self, self.isCurrentWebView(failedWebView) else { return }
|
||||
// Clear stale title/favicon from the previous page so the tab
|
||||
// shows the failed URL instead of the old page's branding.
|
||||
self.pageTitle = failedURL.isEmpty ? "" : failedURL
|
||||
self.faviconPNGData = nil
|
||||
self.lastFaviconURLString = nil
|
||||
// Keep find-in-page open and clear stale counters on failed loads.
|
||||
self.restoreFindStateAfterNavigation(replaySearch: false)
|
||||
}
|
||||
}
|
||||
navDelegate.openInNewTab = { [weak self] url in
|
||||
self?.openLinkInNewTab(url: url)
|
||||
}
|
||||
|
|
@ -7240,6 +7351,18 @@ struct BrowserImportStep3Presentation: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
struct BrowserImportSourceProfilesPresentation: Equatable {
|
||||
let scrollHeight: CGFloat
|
||||
let showsHelpText: Bool
|
||||
|
||||
init(profileCount: Int) {
|
||||
let visibleRows = min(max(profileCount, 1), 5)
|
||||
let contentHeight = CGFloat(visibleRows * 26 + 14)
|
||||
scrollHeight = max(76, contentHeight)
|
||||
showsHelpText = profileCount > 1
|
||||
}
|
||||
}
|
||||
|
||||
enum BrowserImportPlanResolver {
|
||||
@MainActor
|
||||
static func defaultPlan(
|
||||
|
|
@ -8670,6 +8793,21 @@ final class BrowserDataImportCoordinator {
|
|||
return wizard.runModal()
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
func debugMakeImportWizardWindow(
|
||||
browsers: [InstalledBrowserCandidate],
|
||||
destinationProfiles: [BrowserProfileDefinition]? = nil,
|
||||
defaultDestinationProfileID: UUID? = nil
|
||||
) -> NSWindow {
|
||||
let wizard = ImportWizardWindowController(
|
||||
browsers: browsers,
|
||||
destinationProfiles: destinationProfiles,
|
||||
defaultDestinationProfileID: defaultDestinationProfileID
|
||||
)
|
||||
return wizard.debugPanelWindow
|
||||
}
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
private struct CapturedImportSelection: Encodable {
|
||||
struct Entry: Encodable {
|
||||
|
|
@ -8781,6 +8919,7 @@ final class BrowserDataImportCoordinator {
|
|||
private let sourceProfilesEmptyLabel = NSTextField(wrappingLabelWithString: "")
|
||||
private let sourceProfilesHelpLabel = NSTextField(labelWithString: "")
|
||||
private let sourceProfilesScrollView = NSScrollView()
|
||||
private var sourceProfilesScrollHeightConstraint: NSLayoutConstraint?
|
||||
private let dataTypesContainer = NSStackView()
|
||||
private let validationLabel = NSTextField(labelWithString: "")
|
||||
private let destinationModeContainer = NSStackView()
|
||||
|
|
@ -8790,6 +8929,7 @@ final class BrowserDataImportCoordinator {
|
|||
private let mergeDestinationRow = NSStackView()
|
||||
private let mergeDestinationPopup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||||
private let destinationHelpLabel = NSTextField(wrappingLabelWithString: "")
|
||||
private let additionalDataNoteLabel = NSTextField(wrappingLabelWithString: "")
|
||||
|
||||
private let cookiesCheckbox = NSButton(checkboxWithTitle: "", target: nil, action: nil)
|
||||
private let historyCheckbox = NSButton(checkboxWithTitle: "", target: nil, action: nil)
|
||||
|
|
@ -8815,7 +8955,7 @@ final class BrowserDataImportCoordinator {
|
|||
?? fallbackDestinationProfileID
|
||||
self.mergeDestinationProfileID = self.initialDestinationProfileID
|
||||
self.panel = NSPanel(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 620, height: 420),
|
||||
contentRect: NSRect(x: 0, y: 0, width: 560, height: 292),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
|
|
@ -8839,6 +8979,10 @@ final class BrowserDataImportCoordinator {
|
|||
return selection
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
var debugPanelWindow: NSWindow { panel }
|
||||
#endif
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
finishModal(with: .cancel)
|
||||
}
|
||||
|
|
@ -8941,6 +9085,7 @@ final class BrowserDataImportCoordinator {
|
|||
guard selectedSourceProfiles.count > 1 else { return }
|
||||
destinationMode = sender == separateProfilesRadio ? .separateProfiles : .mergeIntoOne
|
||||
rebuildStep3DestinationUI()
|
||||
updatePanelSize()
|
||||
}
|
||||
|
||||
@objc
|
||||
|
|
@ -8963,6 +9108,13 @@ final class BrowserDataImportCoordinator {
|
|||
validationLabel.isHidden = true
|
||||
}
|
||||
|
||||
@objc
|
||||
private func handleImportOptionChanged(_ sender: NSButton) {
|
||||
validationLabel.isHidden = true
|
||||
updateAdditionalDataNoteVisibility()
|
||||
updatePanelSize()
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
panel.title = String(
|
||||
localized: "browser.import.title",
|
||||
|
|
@ -8973,7 +9125,7 @@ final class BrowserDataImportCoordinator {
|
|||
panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
panel.standardWindowButton(.zoomButton)?.isHidden = true
|
||||
|
||||
let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 620, height: 420))
|
||||
let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 560, height: 292))
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
panel.contentView = contentView
|
||||
|
||||
|
|
@ -8983,9 +9135,9 @@ final class BrowserDataImportCoordinator {
|
|||
defaultValue: "Import Browser Data"
|
||||
)
|
||||
)
|
||||
titleLabel.font = NSFont.systemFont(ofSize: 24, weight: .semibold)
|
||||
titleLabel.font = NSFont.systemFont(ofSize: 22, weight: .semibold)
|
||||
|
||||
stepLabel.font = NSFont.systemFont(ofSize: 15, weight: .medium)
|
||||
stepLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
|
||||
stepLabel.textColor = .secondaryLabelColor
|
||||
|
||||
setupSourceContainer()
|
||||
|
|
@ -8997,6 +9149,7 @@ final class BrowserDataImportCoordinator {
|
|||
validationLabel.isHidden = true
|
||||
validationLabel.lineBreakMode = .byWordWrapping
|
||||
validationLabel.maximumNumberOfLines = 3
|
||||
validationLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
backButton.target = self
|
||||
backButton.action = #selector(handleBack)
|
||||
|
|
@ -9034,23 +9187,32 @@ final class BrowserDataImportCoordinator {
|
|||
validationLabel,
|
||||
])
|
||||
contentStack.orientation = .vertical
|
||||
contentStack.spacing = 10
|
||||
contentStack.spacing = 8
|
||||
contentStack.alignment = .leading
|
||||
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
sourceContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
sourceProfilesContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
dataTypesContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
guard let panelContent = panel.contentView else { return }
|
||||
panelContent.addSubview(contentStack)
|
||||
panelContent.addSubview(buttonRow)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
contentStack.topAnchor.constraint(equalTo: panelContent.topAnchor, constant: 18),
|
||||
contentStack.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 20),
|
||||
contentStack.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -20),
|
||||
contentStack.topAnchor.constraint(equalTo: panelContent.topAnchor, constant: 16),
|
||||
contentStack.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 18),
|
||||
contentStack.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -18),
|
||||
|
||||
buttonRow.topAnchor.constraint(greaterThanOrEqualTo: contentStack.bottomAnchor, constant: 14),
|
||||
buttonRow.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 20),
|
||||
buttonRow.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -20),
|
||||
buttonRow.bottomAnchor.constraint(equalTo: panelContent.bottomAnchor, constant: -16),
|
||||
buttonRow.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 18),
|
||||
buttonRow.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -18),
|
||||
buttonRow.bottomAnchor.constraint(equalTo: panelContent.bottomAnchor, constant: -14),
|
||||
|
||||
sourceContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
|
||||
sourceProfilesContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
|
||||
dataTypesContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
|
||||
validationLabel.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -9066,23 +9228,27 @@ final class BrowserDataImportCoordinator {
|
|||
labelWithString: String(localized: "browser.import.source", defaultValue: "Source")
|
||||
)
|
||||
sourceLabel.alignment = .right
|
||||
sourceLabel.frame.size.width = 80
|
||||
sourceLabel.frame.size.width = 64
|
||||
|
||||
sourcePopup.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
sourcePopup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
let sourceRow = NSStackView(views: [sourceLabel, sourcePopup])
|
||||
sourceRow.orientation = .horizontal
|
||||
sourceRow.spacing = 8
|
||||
sourceRow.alignment = .centerY
|
||||
sourceRow.distribution = .fill
|
||||
|
||||
let detectedLabel = NSTextField(
|
||||
wrappingLabelWithString: InstalledBrowserDetector.summaryText(for: browsers)
|
||||
)
|
||||
detectedLabel.font = NSFont.systemFont(ofSize: 12)
|
||||
detectedLabel.font = NSFont.systemFont(ofSize: 11)
|
||||
detectedLabel.textColor = .secondaryLabelColor
|
||||
detectedLabel.maximumNumberOfLines = 2
|
||||
detectedLabel.preferredMaxLayoutWidth = 500
|
||||
|
||||
sourceContainer.orientation = .vertical
|
||||
sourceContainer.spacing = 10
|
||||
sourceContainer.spacing = 8
|
||||
sourceContainer.alignment = .leading
|
||||
sourceContainer.addArrangedSubview(sourceRow)
|
||||
sourceContainer.addArrangedSubview(detectedLabel)
|
||||
|
|
@ -9095,17 +9261,17 @@ final class BrowserDataImportCoordinator {
|
|||
defaultValue: "Source Profiles"
|
||||
)
|
||||
)
|
||||
sourceProfilesTitle.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
|
||||
sourceProfilesTitle.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
||||
|
||||
sourceProfilesList.orientation = .vertical
|
||||
sourceProfilesList.spacing = 6
|
||||
sourceProfilesList.alignment = .leading
|
||||
sourceProfilesList.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
sourceProfilesEmptyLabel.font = NSFont.systemFont(ofSize: 13)
|
||||
sourceProfilesEmptyLabel.font = NSFont.systemFont(ofSize: 12)
|
||||
sourceProfilesEmptyLabel.textColor = .secondaryLabelColor
|
||||
sourceProfilesEmptyLabel.maximumNumberOfLines = 0
|
||||
sourceProfilesEmptyLabel.preferredMaxLayoutWidth = 520
|
||||
sourceProfilesEmptyLabel.preferredMaxLayoutWidth = 500
|
||||
|
||||
sourceProfilesDocumentView.frame = NSRect(x: 0, y: 0, width: 1, height: 1)
|
||||
sourceProfilesDocumentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
@ -9124,23 +9290,29 @@ final class BrowserDataImportCoordinator {
|
|||
sourceProfilesScrollView.documentView = sourceProfilesDocumentView
|
||||
sourceProfilesScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
sourceProfilesScrollView.contentView.postsBoundsChangedNotifications = true
|
||||
sourceProfilesScrollView.heightAnchor.constraint(equalToConstant: 180).isActive = true
|
||||
sourceProfilesScrollHeightConstraint = sourceProfilesScrollView.heightAnchor.constraint(equalToConstant: 76)
|
||||
sourceProfilesScrollHeightConstraint?.isActive = true
|
||||
let sourceProfilesScrollWidthConstraint = sourceProfilesScrollView.widthAnchor.constraint(
|
||||
equalTo: sourceProfilesContainer.widthAnchor
|
||||
)
|
||||
|
||||
sourceProfilesHelpLabel.font = NSFont.systemFont(ofSize: 12)
|
||||
sourceProfilesHelpLabel.font = NSFont.systemFont(ofSize: 11)
|
||||
sourceProfilesHelpLabel.textColor = .secondaryLabelColor
|
||||
sourceProfilesHelpLabel.maximumNumberOfLines = 2
|
||||
sourceProfilesHelpLabel.lineBreakMode = .byWordWrapping
|
||||
sourceProfilesHelpLabel.preferredMaxLayoutWidth = 500
|
||||
sourceProfilesHelpLabel.stringValue = String(
|
||||
localized: "browser.import.sourceProfiles.help",
|
||||
defaultValue: "Choose one or more source profiles. Step 3 lets you keep them separate or merge them into one cmux profile."
|
||||
)
|
||||
|
||||
sourceProfilesContainer.orientation = .vertical
|
||||
sourceProfilesContainer.spacing = 10
|
||||
sourceProfilesContainer.spacing = 8
|
||||
sourceProfilesContainer.alignment = .leading
|
||||
sourceProfilesContainer.addArrangedSubview(sourceProfilesTitle)
|
||||
sourceProfilesContainer.addArrangedSubview(sourceProfilesScrollView)
|
||||
sourceProfilesContainer.addArrangedSubview(sourceProfilesHelpLabel)
|
||||
sourceProfilesScrollWidthConstraint.isActive = true
|
||||
sourceProfilesContainer.setHuggingPriority(.defaultLow, for: .vertical)
|
||||
sourceProfilesContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
}
|
||||
|
|
@ -9161,6 +9333,12 @@ final class BrowserDataImportCoordinator {
|
|||
localized: "browser.import.additionalData",
|
||||
defaultValue: "Additional data (bookmarks, settings, extensions)"
|
||||
)
|
||||
cookiesCheckbox.target = self
|
||||
cookiesCheckbox.action = #selector(handleImportOptionChanged(_:))
|
||||
historyCheckbox.target = self
|
||||
historyCheckbox.action = #selector(handleImportOptionChanged(_:))
|
||||
additionalDataCheckbox.target = self
|
||||
additionalDataCheckbox.action = #selector(handleImportOptionChanged(_:))
|
||||
cookiesCheckbox.setAccessibilityIdentifier("BrowserImportCookiesCheckbox")
|
||||
historyCheckbox.setAccessibilityIdentifier("BrowserImportHistoryCheckbox")
|
||||
additionalDataCheckbox.setAccessibilityIdentifier("BrowserImportAdditionalDataCheckbox")
|
||||
|
|
@ -9185,25 +9363,29 @@ final class BrowserDataImportCoordinator {
|
|||
|
||||
mergeDestinationPopup.target = self
|
||||
mergeDestinationPopup.action = #selector(handleMergeDestinationChanged(_:))
|
||||
mergeDestinationPopup.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
mergeDestinationPopup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
separateDestinationRows.orientation = .vertical
|
||||
separateDestinationRows.spacing = 8
|
||||
separateDestinationRows.spacing = 6
|
||||
separateDestinationRows.alignment = .leading
|
||||
|
||||
mergeDestinationRow.orientation = .horizontal
|
||||
mergeDestinationRow.spacing = 8
|
||||
mergeDestinationRow.spacing = 6
|
||||
mergeDestinationRow.alignment = .centerY
|
||||
|
||||
destinationHelpLabel.font = NSFont.systemFont(ofSize: 12)
|
||||
destinationHelpLabel.font = NSFont.systemFont(ofSize: 11)
|
||||
destinationHelpLabel.textColor = .secondaryLabelColor
|
||||
destinationHelpLabel.maximumNumberOfLines = 3
|
||||
destinationHelpLabel.preferredMaxLayoutWidth = 540
|
||||
destinationHelpLabel.maximumNumberOfLines = 2
|
||||
destinationHelpLabel.preferredMaxLayoutWidth = 500
|
||||
|
||||
domainField.placeholderString = String(
|
||||
localized: "browser.import.domain.placeholder",
|
||||
defaultValue: "Optional domains only (e.g. github.com, openai.com)"
|
||||
)
|
||||
domainField.stringValue = ""
|
||||
domainField.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
domainField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
let destinationTitleLabel = NSTextField(
|
||||
labelWithString: String(
|
||||
|
|
@ -9211,32 +9393,32 @@ final class BrowserDataImportCoordinator {
|
|||
defaultValue: "cmux destination"
|
||||
)
|
||||
)
|
||||
destinationTitleLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
|
||||
destinationTitleLabel.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
||||
|
||||
let domainLabel = NSTextField(
|
||||
labelWithString: String(localized: "browser.import.domain", defaultValue: "Limit to")
|
||||
)
|
||||
domainLabel.alignment = .right
|
||||
domainLabel.frame.size.width = 80
|
||||
domainLabel.frame.size.width = 72
|
||||
|
||||
let domainRow = NSStackView(views: [domainLabel, domainField])
|
||||
domainRow.orientation = .horizontal
|
||||
domainRow.spacing = 8
|
||||
domainRow.alignment = .centerY
|
||||
domainRow.distribution = .fill
|
||||
|
||||
let noteLabel = NSTextField(
|
||||
wrappingLabelWithString: String(
|
||||
localized: "browser.import.additionalData.note",
|
||||
defaultValue: "Bookmarks, settings, and extensions import are not available yet."
|
||||
)
|
||||
additionalDataNoteLabel.stringValue = String(
|
||||
localized: "browser.import.additionalData.note",
|
||||
defaultValue: "Bookmarks, settings, and extensions import are not available yet."
|
||||
)
|
||||
noteLabel.font = NSFont.systemFont(ofSize: 12)
|
||||
noteLabel.textColor = .secondaryLabelColor
|
||||
noteLabel.maximumNumberOfLines = 2
|
||||
noteLabel.preferredMaxLayoutWidth = 540
|
||||
additionalDataNoteLabel.font = NSFont.systemFont(ofSize: 11)
|
||||
additionalDataNoteLabel.textColor = .secondaryLabelColor
|
||||
additionalDataNoteLabel.maximumNumberOfLines = 2
|
||||
additionalDataNoteLabel.preferredMaxLayoutWidth = 500
|
||||
additionalDataNoteLabel.isHidden = true
|
||||
|
||||
dataTypesContainer.orientation = .vertical
|
||||
dataTypesContainer.spacing = 8
|
||||
dataTypesContainer.spacing = 6
|
||||
dataTypesContainer.alignment = .leading
|
||||
dataTypesContainer.addArrangedSubview(destinationTitleLabel)
|
||||
dataTypesContainer.addArrangedSubview(destinationModeContainer)
|
||||
|
|
@ -9246,13 +9428,14 @@ final class BrowserDataImportCoordinator {
|
|||
dataTypesContainer.addArrangedSubview(cookiesCheckbox)
|
||||
dataTypesContainer.addArrangedSubview(historyCheckbox)
|
||||
dataTypesContainer.addArrangedSubview(additionalDataCheckbox)
|
||||
dataTypesContainer.addArrangedSubview(additionalDataNoteLabel)
|
||||
dataTypesContainer.addArrangedSubview(domainRow)
|
||||
dataTypesContainer.addArrangedSubview(noteLabel)
|
||||
}
|
||||
|
||||
private func configureInitialState() {
|
||||
step = .source
|
||||
refreshSourceProfilesList()
|
||||
updateAdditionalDataNoteVisibility()
|
||||
updateStepUI()
|
||||
}
|
||||
|
||||
|
|
@ -9261,7 +9444,7 @@ final class BrowserDataImportCoordinator {
|
|||
case .source:
|
||||
stepLabel.stringValue = String(
|
||||
localized: "browser.import.step.source",
|
||||
defaultValue: "Step 1 of 3: Choose the browser to import from."
|
||||
defaultValue: "Step 1 of 3"
|
||||
)
|
||||
sourceContainer.isHidden = false
|
||||
sourceProfilesContainer.isHidden = true
|
||||
|
|
@ -9271,11 +9454,8 @@ final class BrowserDataImportCoordinator {
|
|||
primaryButton.title = String(localized: "browser.import.next", defaultValue: "Next")
|
||||
case .sourceProfiles:
|
||||
stepLabel.stringValue = String(
|
||||
format: String(
|
||||
localized: "browser.import.step.sourceProfiles",
|
||||
defaultValue: "Step 2 of 3: Choose source profiles from %@."
|
||||
),
|
||||
selectedBrowser().displayName
|
||||
localized: "browser.import.step.sourceProfiles",
|
||||
defaultValue: "Step 2 of 3"
|
||||
)
|
||||
sourceContainer.isHidden = true
|
||||
sourceProfilesContainer.isHidden = false
|
||||
|
|
@ -9286,11 +9466,8 @@ final class BrowserDataImportCoordinator {
|
|||
case .dataTypes:
|
||||
rebuildStep3DestinationUI()
|
||||
stepLabel.stringValue = String(
|
||||
format: String(
|
||||
localized: "browser.import.step.dataTypes",
|
||||
defaultValue: "Step 3 of 3: Choose what to import from %@ and where to put it."
|
||||
),
|
||||
selectedBrowser().displayName
|
||||
localized: "browser.import.step.dataTypes",
|
||||
defaultValue: "Step 3 of 3"
|
||||
)
|
||||
sourceContainer.isHidden = true
|
||||
sourceProfilesContainer.isHidden = true
|
||||
|
|
@ -9302,6 +9479,7 @@ final class BrowserDataImportCoordinator {
|
|||
defaultValue: "Start Import"
|
||||
)
|
||||
}
|
||||
updatePanelSize()
|
||||
}
|
||||
|
||||
private func selectedBrowser() -> InstalledBrowserCandidate {
|
||||
|
|
@ -9328,6 +9506,7 @@ final class BrowserDataImportCoordinator {
|
|||
browser.displayName
|
||||
)
|
||||
sourceProfilesList.addArrangedSubview(sourceProfilesEmptyLabel)
|
||||
updateSourceProfilesPresentation(for: browser)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -9343,6 +9522,8 @@ final class BrowserDataImportCoordinator {
|
|||
sourceProfilesList.addArrangedSubview(checkbox)
|
||||
sourceProfileCheckboxes.append(checkbox)
|
||||
}
|
||||
|
||||
updateSourceProfilesPresentation(for: browser)
|
||||
}
|
||||
|
||||
private func storedSelectedSourceProfileIDs(for browser: InstalledBrowserCandidate) -> Set<String> {
|
||||
|
|
@ -9458,16 +9639,16 @@ final class BrowserDataImportCoordinator {
|
|||
localized: "browser.import.destinationProfile.separateHelp",
|
||||
defaultValue: "Missing cmux profiles are created when import starts."
|
||||
)
|
||||
destinationHelpLabel.isHidden = false
|
||||
} else if plan.entries.count > 1 {
|
||||
destinationHelpLabel.stringValue = String(
|
||||
localized: "browser.import.destinationProfile.mergeHelp",
|
||||
defaultValue: "All selected source profiles will be merged into the chosen cmux browser profile."
|
||||
)
|
||||
destinationHelpLabel.isHidden = false
|
||||
} else {
|
||||
destinationHelpLabel.stringValue = String(
|
||||
localized: "browser.import.destinationProfile.help",
|
||||
defaultValue: "Imported cookies and history go into the selected cmux browser profile."
|
||||
)
|
||||
destinationHelpLabel.stringValue = ""
|
||||
destinationHelpLabel.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -9484,7 +9665,7 @@ final class BrowserDataImportCoordinator {
|
|||
guard let sourceProfile = entry.sourceProfiles.first else { continue }
|
||||
let sourceLabel = NSTextField(labelWithString: sourceProfile.displayName)
|
||||
sourceLabel.alignment = .right
|
||||
sourceLabel.frame.size.width = 140
|
||||
sourceLabel.frame.size.width = 110
|
||||
|
||||
let popup = NSPopUpButton(frame: .zero, pullsDown: false)
|
||||
popup.target = self
|
||||
|
|
@ -9504,11 +9685,14 @@ final class BrowserDataImportCoordinator {
|
|||
} else {
|
||||
popup.selectItem(at: 0)
|
||||
}
|
||||
popup.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
popup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
let row = NSStackView(views: [sourceLabel, popup])
|
||||
row.orientation = .horizontal
|
||||
row.spacing = 8
|
||||
row.spacing = 6
|
||||
row.alignment = .centerY
|
||||
row.distribution = .fill
|
||||
separateDestinationRows.addArrangedSubview(row)
|
||||
}
|
||||
}
|
||||
|
|
@ -9540,7 +9724,7 @@ final class BrowserDataImportCoordinator {
|
|||
)
|
||||
)
|
||||
destinationLabel.alignment = .right
|
||||
destinationLabel.frame.size.width = 140
|
||||
destinationLabel.frame.size.width = 110
|
||||
|
||||
mergeDestinationRow.addArrangedSubview(destinationLabel)
|
||||
mergeDestinationRow.addArrangedSubview(mergeDestinationPopup)
|
||||
|
|
@ -9614,6 +9798,51 @@ final class BrowserDataImportCoordinator {
|
|||
return base.isEmpty ? "profile-\(index)" : base
|
||||
}
|
||||
|
||||
private func updateSourceProfilesPresentation(for browser: InstalledBrowserCandidate) {
|
||||
let presentation = BrowserImportSourceProfilesPresentation(profileCount: browser.profiles.count)
|
||||
sourceProfilesScrollHeightConstraint?.constant = presentation.scrollHeight
|
||||
sourceProfilesHelpLabel.isHidden = !presentation.showsHelpText
|
||||
}
|
||||
|
||||
private func updateAdditionalDataNoteVisibility() {
|
||||
additionalDataNoteLabel.isHidden = additionalDataCheckbox.state != .on
|
||||
}
|
||||
|
||||
private func updatePanelSize() {
|
||||
let contentSize = preferredContentSize()
|
||||
let targetFrame = panel.frameRect(forContentRect: NSRect(origin: .zero, size: contentSize))
|
||||
|
||||
guard panel.frame.size != targetFrame.size else { return }
|
||||
if !panel.isVisible {
|
||||
panel.setContentSize(contentSize)
|
||||
return
|
||||
}
|
||||
|
||||
var frame = panel.frame
|
||||
frame.origin.x -= (targetFrame.width - frame.width) / 2
|
||||
frame.origin.y -= (targetFrame.height - frame.height) / 2
|
||||
frame.size = targetFrame.size
|
||||
panel.setFrame(frame, display: true)
|
||||
}
|
||||
|
||||
private func preferredContentSize() -> NSSize {
|
||||
switch step {
|
||||
case .source:
|
||||
return NSSize(width: 560, height: 292)
|
||||
case .sourceProfiles:
|
||||
let presentation = BrowserImportSourceProfilesPresentation(profileCount: selectedBrowser().profiles.count)
|
||||
let helpHeight: CGFloat = presentation.showsHelpText ? 24 : 0
|
||||
let height = 214 + presentation.scrollHeight + helpHeight
|
||||
return NSSize(width: 560, height: min(max(height, 292), 360))
|
||||
case .dataTypes:
|
||||
var height: CGFloat = currentExecutionPlan().mode == .separateProfiles ? 412 : 374
|
||||
if additionalDataCheckbox.state == .on {
|
||||
height += 24
|
||||
}
|
||||
return NSSize(width: 560, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
private func finishModal(with response: NSApplication.ModalResponse) {
|
||||
guard !didFinishModal else { return }
|
||||
didFinishModal = true
|
||||
|
|
|
|||
|
|
@ -110,6 +110,45 @@ enum BrowserDevToolsButtonDebugSettings {
|
|||
}
|
||||
}
|
||||
|
||||
enum BrowserToolbarAccessorySpacingDebugSettings {
|
||||
static let key = "browserToolbarAccessorySpacing"
|
||||
static let defaultSpacing = 2
|
||||
static let supportedValues = [0, 2, 4, 6, 8]
|
||||
|
||||
static func resolved(_ rawValue: Int) -> Int {
|
||||
supportedValues.contains(rawValue) ? rawValue : defaultSpacing
|
||||
}
|
||||
|
||||
static func current(defaults: UserDefaults = .standard) -> Int {
|
||||
resolved(defaults.object(forKey: key) as? Int ?? defaultSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
enum BrowserProfilePopoverDebugSettings {
|
||||
static let horizontalPaddingKey = "browserProfilePopoverHorizontalPadding"
|
||||
static let verticalPaddingKey = "browserProfilePopoverVerticalPadding"
|
||||
static let defaultHorizontalPadding = 12.0
|
||||
static let defaultVerticalPadding = 10.0
|
||||
static let horizontalPaddingRange = 8.0...20.0
|
||||
static let verticalPaddingRange = 4.0...14.0
|
||||
|
||||
static func resolvedHorizontalPadding(_ rawValue: Double) -> Double {
|
||||
horizontalPaddingRange.contains(rawValue) ? rawValue : defaultHorizontalPadding
|
||||
}
|
||||
|
||||
static func resolvedVerticalPadding(_ rawValue: Double) -> Double {
|
||||
verticalPaddingRange.contains(rawValue) ? rawValue : defaultVerticalPadding
|
||||
}
|
||||
|
||||
static func currentHorizontalPadding(defaults: UserDefaults = .standard) -> Double {
|
||||
resolvedHorizontalPadding((defaults.object(forKey: horizontalPaddingKey) as? NSNumber)?.doubleValue ?? defaultHorizontalPadding)
|
||||
}
|
||||
|
||||
static func currentVerticalPadding(defaults: UserDefaults = .standard) -> Double {
|
||||
resolvedVerticalPadding((defaults.object(forKey: verticalPaddingKey) as? NSNumber)?.doubleValue ?? defaultVerticalPadding)
|
||||
}
|
||||
}
|
||||
|
||||
struct OmnibarInlineCompletion: Equatable {
|
||||
let typedText: String
|
||||
let displayText: String
|
||||
|
|
@ -249,7 +288,15 @@ struct BrowserPanelView: View {
|
|||
@AppStorage(BrowserSearchSettings.searchSuggestionsEnabledKey) private var searchSuggestionsEnabledStorage = BrowserSearchSettings.defaultSearchSuggestionsEnabled
|
||||
@AppStorage(BrowserDevToolsButtonDebugSettings.iconNameKey) private var devToolsIconNameRaw = BrowserDevToolsButtonDebugSettings.defaultIcon.rawValue
|
||||
@AppStorage(BrowserDevToolsButtonDebugSettings.iconColorKey) private var devToolsIconColorRaw = BrowserDevToolsButtonDebugSettings.defaultColor.rawValue
|
||||
@AppStorage(BrowserToolbarAccessorySpacingDebugSettings.key) private var browserToolbarAccessorySpacingRaw = BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing
|
||||
@AppStorage(BrowserProfilePopoverDebugSettings.horizontalPaddingKey)
|
||||
private var browserProfilePopoverHorizontalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultHorizontalPadding
|
||||
@AppStorage(BrowserProfilePopoverDebugSettings.verticalPaddingKey)
|
||||
private var browserProfilePopoverVerticalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultVerticalPadding
|
||||
@AppStorage(BrowserThemeSettings.modeKey) private var browserThemeModeRaw = BrowserThemeSettings.defaultMode.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.variantKey) private var browserImportHintVariantRaw = BrowserImportHintSettings.defaultVariant.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.showOnBlankTabsKey) private var showBrowserImportHintOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs
|
||||
@AppStorage(BrowserImportHintSettings.dismissedKey) private var isBrowserImportHintDismissed = BrowserImportHintSettings.defaultDismissed
|
||||
@AppStorage(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultsKey)
|
||||
private var toggleBrowserDeveloperToolsShortcutData = Data()
|
||||
@State private var suggestionTask: Task<Void, Never>?
|
||||
|
|
@ -267,6 +314,7 @@ struct BrowserPanelView: View {
|
|||
@State private var focusFlashAnimationGeneration: Int = 0
|
||||
@State private var omnibarPillFrame: CGRect = .zero
|
||||
@State private var addressBarHeight: CGFloat = 0
|
||||
@State private var isBrowserImportHintPopoverPresented = false
|
||||
@State private var lastHandledAddressBarFocusRequestId: UUID?
|
||||
@State private var pendingAddressBarFocusRetryRequestId: UUID?
|
||||
@State private var pendingAddressBarFocusRetryGeneration: UInt64 = 0
|
||||
|
|
@ -321,6 +369,30 @@ struct BrowserPanelView: View {
|
|||
BrowserThemeSettings.mode(for: browserThemeModeRaw)
|
||||
}
|
||||
|
||||
private var browserImportHintVariant: BrowserImportHintVariant {
|
||||
BrowserImportHintSettings.variant(for: browserImportHintVariantRaw)
|
||||
}
|
||||
|
||||
private var browserImportHintPresentation: BrowserImportHintPresentation {
|
||||
BrowserImportHintPresentation(
|
||||
variant: browserImportHintVariant,
|
||||
showOnBlankTabs: showBrowserImportHintOnBlankTabs,
|
||||
isDismissed: isBrowserImportHintDismissed
|
||||
)
|
||||
}
|
||||
|
||||
private var browserToolbarAccessorySpacing: CGFloat {
|
||||
CGFloat(BrowserToolbarAccessorySpacingDebugSettings.resolved(browserToolbarAccessorySpacingRaw))
|
||||
}
|
||||
|
||||
private var browserProfilePopoverHorizontalPadding: CGFloat {
|
||||
CGFloat(BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding(browserProfilePopoverHorizontalPaddingRaw))
|
||||
}
|
||||
|
||||
private var browserProfilePopoverVerticalPadding: CGFloat {
|
||||
CGFloat(BrowserProfilePopoverDebugSettings.resolvedVerticalPadding(browserProfilePopoverVerticalPaddingRaw))
|
||||
}
|
||||
|
||||
private var browserChromeBackground: Color {
|
||||
Color(nsColor: browserChromeStyle.backgroundColor)
|
||||
}
|
||||
|
|
@ -346,6 +418,14 @@ struct BrowserPanelView: View {
|
|||
return "\(base) (\(toggleBrowserDeveloperToolsShortcut.displayString))"
|
||||
}
|
||||
|
||||
private var browserImportHintSummary: String {
|
||||
InstalledBrowserDetector.summaryText(for: emptyStateImportBrowsers)
|
||||
}
|
||||
|
||||
private var shouldShowToolbarImportHintChip: Bool {
|
||||
shouldShowEmptyStateImportOverlay && browserImportHintPresentation.blankTabPlacement == .toolbarChip
|
||||
}
|
||||
|
||||
private var owningWorkspace: Workspace? {
|
||||
guard let app = AppDelegate.shared,
|
||||
let manager = app.tabManagerFor(tabId: panel.workspaceId) else {
|
||||
|
|
@ -451,6 +531,9 @@ struct BrowserPanelView: View {
|
|||
UserDefaults.standard.register(defaults: [
|
||||
BrowserSearchSettings.searchEngineKey: BrowserSearchSettings.defaultSearchEngine.rawValue,
|
||||
BrowserSearchSettings.searchSuggestionsEnabledKey: BrowserSearchSettings.defaultSearchSuggestionsEnabled,
|
||||
BrowserToolbarAccessorySpacingDebugSettings.key: BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing,
|
||||
BrowserProfilePopoverDebugSettings.horizontalPaddingKey: BrowserProfilePopoverDebugSettings.defaultHorizontalPadding,
|
||||
BrowserProfilePopoverDebugSettings.verticalPaddingKey: BrowserProfilePopoverDebugSettings.defaultVerticalPadding,
|
||||
BrowserThemeSettings.modeKey: BrowserThemeSettings.defaultMode.rawValue,
|
||||
])
|
||||
refreshBrowserChromeStyle()
|
||||
|
|
@ -459,6 +542,22 @@ struct BrowserPanelView: View {
|
|||
if browserThemeModeRaw != resolvedThemeMode.rawValue {
|
||||
browserThemeModeRaw = resolvedThemeMode.rawValue
|
||||
}
|
||||
let resolvedHintVariant = BrowserImportHintSettings.variant(for: browserImportHintVariantRaw)
|
||||
if browserImportHintVariantRaw != resolvedHintVariant.rawValue {
|
||||
browserImportHintVariantRaw = resolvedHintVariant.rawValue
|
||||
}
|
||||
let resolvedToolbarAccessorySpacing = BrowserToolbarAccessorySpacingDebugSettings.resolved(browserToolbarAccessorySpacingRaw)
|
||||
if browserToolbarAccessorySpacingRaw != resolvedToolbarAccessorySpacing {
|
||||
browserToolbarAccessorySpacingRaw = resolvedToolbarAccessorySpacing
|
||||
}
|
||||
let resolvedProfilePopoverHorizontalPadding = BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding(browserProfilePopoverHorizontalPaddingRaw)
|
||||
if browserProfilePopoverHorizontalPaddingRaw != resolvedProfilePopoverHorizontalPadding {
|
||||
browserProfilePopoverHorizontalPaddingRaw = resolvedProfilePopoverHorizontalPadding
|
||||
}
|
||||
let resolvedProfilePopoverVerticalPadding = BrowserProfilePopoverDebugSettings.resolvedVerticalPadding(browserProfilePopoverVerticalPaddingRaw)
|
||||
if browserProfilePopoverVerticalPaddingRaw != resolvedProfilePopoverVerticalPadding {
|
||||
browserProfilePopoverVerticalPaddingRaw = resolvedProfilePopoverVerticalPadding
|
||||
}
|
||||
panel.refreshAppearanceDrivenColors()
|
||||
panel.setBrowserThemeMode(browserThemeMode)
|
||||
applyPendingAddressBarFocusRequestIfNeeded()
|
||||
|
|
@ -613,9 +712,14 @@ struct BrowserPanelView: View {
|
|||
.accessibilityIdentifier("BrowserOmnibarPill")
|
||||
.accessibilityLabel("Browser omnibar")
|
||||
|
||||
browserProfileButton
|
||||
browserThemeModeButton
|
||||
developerToolsButton
|
||||
HStack(spacing: browserToolbarAccessorySpacing) {
|
||||
if shouldShowToolbarImportHintChip {
|
||||
browserImportHintToolbarChip
|
||||
}
|
||||
browserProfileButton
|
||||
browserThemeModeButton
|
||||
developerToolsButton
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, addressBarVerticalPadding)
|
||||
|
|
@ -776,6 +880,29 @@ struct BrowserPanelView: View {
|
|||
.accessibilityIdentifier("BrowserThemeModeButton")
|
||||
}
|
||||
|
||||
private var browserImportHintToolbarChip: some View {
|
||||
Button(action: {
|
||||
isBrowserImportHintPopoverPresented.toggle()
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "square.and.arrow.down.on.square")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
Text(String(localized: "browser.import.hint.toolbar", defaultValue: "Import"))
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundStyle(devToolsColorOption.color)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.buttonStyle(OmnibarAddressButtonStyle())
|
||||
.popover(isPresented: $isBrowserImportHintPopoverPresented, arrowEdge: .bottom) {
|
||||
browserImportHintPopover
|
||||
}
|
||||
.safeHelp(String(localized: "browser.import.hint.toolbar.help", defaultValue: "Import browser data"))
|
||||
.accessibilityIdentifier("BrowserImportHintToolbarChip")
|
||||
}
|
||||
|
||||
private var browserProfilePopover: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(String(localized: "browser.profile.menu.title", defaultValue: "Profiles"))
|
||||
|
|
@ -819,6 +946,14 @@ struct BrowserPanelView: View {
|
|||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button {
|
||||
presentImportDialogFromProfileMenu()
|
||||
} label: {
|
||||
Text(String(localized: "menu.view.importFromBrowser", defaultValue: "Import From Browser…"))
|
||||
.font(.system(size: 12))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if browserProfileStore.canRenameProfile(id: panel.profileID) {
|
||||
Button {
|
||||
isBrowserProfileMenuPresented = false
|
||||
|
|
@ -830,7 +965,8 @@ struct BrowserPanelView: View {
|
|||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.padding(.horizontal, browserProfilePopoverHorizontalPadding)
|
||||
.padding(.vertical, browserProfilePopoverVerticalPadding)
|
||||
.frame(minWidth: 208)
|
||||
}
|
||||
|
||||
|
|
@ -1018,9 +1154,16 @@ struct BrowserPanelView: View {
|
|||
setAddressBarFocused(false, reason: "placeholderContent.tapBlur")
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .topLeading) {
|
||||
if shouldShowEmptyStateImportOverlay,
|
||||
browserImportHintPresentation.blankTabPlacement == .inlineStrip {
|
||||
emptyBrowserStateInlineStrip
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if shouldShowEmptyStateImportOverlay {
|
||||
emptyBrowserStateOverlay
|
||||
if shouldShowEmptyStateImportOverlay,
|
||||
browserImportHintPresentation.blankTabPlacement == .floatingCard {
|
||||
emptyBrowserStateCardOverlay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1288,28 +1431,11 @@ struct BrowserPanelView: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
private var emptyBrowserStateOverlay: some View {
|
||||
private var emptyBrowserStateCardOverlay: some View {
|
||||
VStack {
|
||||
Spacer(minLength: 22)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(String(localized: "settings.browser.emptyImport.title", defaultValue: "Import browser data"))
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text(InstalledBrowserDetector.summaryText(for: emptyStateImportBrowsers))
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Button(String(localized: "settings.browser.emptyImport.choose", defaultValue: "Choose What to Import…")) {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog(
|
||||
defaultDestinationProfileID: panel.profileID
|
||||
)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
browserImportHintBody
|
||||
.padding(12)
|
||||
.frame(maxWidth: 360, alignment: .leading)
|
||||
.background(
|
||||
|
|
@ -1329,10 +1455,131 @@ struct BrowserPanelView: View {
|
|||
.padding(.horizontal, 18)
|
||||
}
|
||||
|
||||
private var emptyBrowserStateInlineStrip: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
browserImportHintBody
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
.frame(maxWidth: 520, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color(nsColor: .windowBackgroundColor).opacity(0.84))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous).stroke(
|
||||
Color(nsColor: .separatorColor).opacity(0.35),
|
||||
lineWidth: 1
|
||||
)
|
||||
)
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 6, y: 2)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.top, 14)
|
||||
}
|
||||
|
||||
private var browserImportHintPopover: some View {
|
||||
browserImportHintBody
|
||||
.padding(12)
|
||||
.frame(width: 300, alignment: .leading)
|
||||
}
|
||||
|
||||
private var browserImportHintBody: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(String(localized: "browser.import.hint.title", defaultValue: "Import browser data"))
|
||||
.font(.system(size: 12.5, weight: .semibold))
|
||||
|
||||
Text(browserImportHintSummary)
|
||||
.font(.system(size: 11.5))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(String(localized: "browser.import.hint.settingsFootnote", defaultValue: "You can always find this in Settings > Browser."))
|
||||
.font(.system(size: 10.5))
|
||||
.foregroundStyle(.tertiary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
ViewThatFits(in: .horizontal) {
|
||||
HStack(spacing: 10) {
|
||||
browserImportHintPrimaryButton
|
||||
browserImportHintSettingsButton
|
||||
browserImportHintDismissButton
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
browserImportHintPrimaryButton
|
||||
HStack(spacing: 10) {
|
||||
browserImportHintSettingsButton
|
||||
browserImportHintDismissButton
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
}
|
||||
|
||||
private var browserImportHintPrimaryButton: some View {
|
||||
Button(String(localized: "browser.import.hint.import", defaultValue: "Import…")) {
|
||||
presentImportDialogFromHint()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("BrowserImportHintImportButton")
|
||||
}
|
||||
|
||||
private var browserImportHintSettingsButton: some View {
|
||||
Button(String(localized: "browser.import.hint.settings", defaultValue: "Browser Settings")) {
|
||||
openBrowserImportSettings()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("BrowserImportHintSettingsButton")
|
||||
}
|
||||
|
||||
private var browserImportHintDismissButton: some View {
|
||||
Button(String(localized: "browser.import.hint.dismiss", defaultValue: "Hide Hint")) {
|
||||
dismissBrowserImportHint()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("BrowserImportHintDismissButton")
|
||||
}
|
||||
|
||||
private var shouldShowEmptyStateImportOverlay: Bool {
|
||||
!panel.shouldRenderWebView && isWebViewBlank()
|
||||
}
|
||||
|
||||
private func presentImportDialogFromHint() {
|
||||
isBrowserImportHintPopoverPresented = false
|
||||
// Let the popover fully dismiss before entering the modal import flow.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog(
|
||||
defaultDestinationProfileID: panel.profileID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentImportDialogFromProfileMenu() {
|
||||
isBrowserProfileMenuPresented = false
|
||||
DispatchQueue.main.async {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog(
|
||||
defaultDestinationProfileID: panel.profileID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func openBrowserImportSettings() {
|
||||
isBrowserImportHintPopoverPresented = false
|
||||
AppDelegate.presentPreferencesWindow(navigationTarget: .browserImport)
|
||||
}
|
||||
|
||||
private func dismissBrowserImportHint() {
|
||||
showBrowserImportHintOnBlankTabs = false
|
||||
isBrowserImportHintDismissed = true
|
||||
isBrowserImportHintPopoverPresented = false
|
||||
}
|
||||
|
||||
/// Treat a WebView with no URL (or about:blank) as "blank" for UX purposes.
|
||||
private func isWebViewBlank() -> Bool {
|
||||
guard let url = panel.webView.url else { return true }
|
||||
|
|
@ -1518,8 +1765,9 @@ struct BrowserPanelView: View {
|
|||
|
||||
private func applyBrowserProfileSelection(_ profileID: UUID) {
|
||||
isBrowserProfileMenuPresented = false
|
||||
let didApply = panel.profileID == profileID || panel.switchToProfile(profileID)
|
||||
guard didApply else { return }
|
||||
owningWorkspace?.setPreferredBrowserProfileID(profileID)
|
||||
_ = panel.switchToProfile(profileID)
|
||||
}
|
||||
|
||||
private func presentCreateBrowserProfilePrompt() {
|
||||
|
|
|
|||
|
|
@ -5303,7 +5303,8 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
return preferredProfileID
|
||||
}
|
||||
if let sourcePanelId,
|
||||
let sourceBrowserPanel = browserPanel(for: sourcePanelId) {
|
||||
let sourceBrowserPanel = browserPanel(for: sourcePanelId),
|
||||
BrowserProfileStore.shared.profileDefinition(id: sourceBrowserPanel.profileID) != nil {
|
||||
return sourceBrowserPanel.profileID
|
||||
}
|
||||
if let preferredBrowserProfileID,
|
||||
|
|
@ -6644,7 +6645,6 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
)
|
||||
panels[browserPanel.id] = browserPanel
|
||||
panelTitles[browserPanel.id] = browserPanel.displayTitle
|
||||
setPreferredBrowserProfileID(browserPanel.profileID)
|
||||
|
||||
// Pre-generate the bonsplit tab ID so the mapping exists before the split lands.
|
||||
let newTab = Bonsplit.Tab(
|
||||
|
|
@ -6668,6 +6668,7 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
panelTitles.removeValue(forKey: browserPanel.id)
|
||||
return nil
|
||||
}
|
||||
setPreferredBrowserProfileID(browserPanel.profileID)
|
||||
|
||||
// See newTerminalSplit: suppress old view's becomeFirstResponder during reparenting.
|
||||
let previousHostedView = focusedTerminalPanel?.hostedView
|
||||
|
|
@ -6705,12 +6706,16 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
bypassInsecureHTTPHostOnce: String? = nil
|
||||
) -> BrowserPanel? {
|
||||
let shouldFocusNewTab = focus ?? (bonsplitController.focusedPaneId == paneId)
|
||||
let sourcePanelId = effectiveSelectedPanelId(inPane: paneId)
|
||||
let previousFocusedPanelId = focusedPanelId
|
||||
let previousHostedView = focusedTerminalPanel?.hostedView
|
||||
|
||||
let browserPanel = BrowserPanel(
|
||||
workspaceId: id,
|
||||
profileID: resolvedNewBrowserProfileID(preferredProfileID: preferredProfileID),
|
||||
profileID: resolvedNewBrowserProfileID(
|
||||
preferredProfileID: preferredProfileID,
|
||||
sourcePanelId: sourcePanelId
|
||||
),
|
||||
initialURL: url,
|
||||
bypassInsecureHTTPHostOnce: bypassInsecureHTTPHostOnce,
|
||||
proxyEndpoint: remoteProxyEndpoint,
|
||||
|
|
@ -6719,7 +6724,6 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
)
|
||||
panels[browserPanel.id] = browserPanel
|
||||
panelTitles[browserPanel.id] = browserPanel.displayTitle
|
||||
setPreferredBrowserProfileID(browserPanel.profileID)
|
||||
|
||||
guard let newTabId = bonsplitController.createTab(
|
||||
title: browserPanel.displayTitle,
|
||||
|
|
@ -6736,6 +6740,7 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
}
|
||||
|
||||
surfaceIdToPanelId[newTabId] = browserPanel.id
|
||||
setPreferredBrowserProfileID(browserPanel.profileID)
|
||||
|
||||
// Keyboard/browser-open paths want "new tab at end" regardless of global new-tab placement.
|
||||
if insertAtEnd {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ struct cmuxApp: App {
|
|||
@AppStorage(KeyboardShortcutSettings.Action.prevSidebarTab.defaultsKey) private var prevWorkspaceShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitRight.defaultsKey) private var splitRightShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitDown.defaultsKey) private var splitDownShortcutData = Data()
|
||||
@AppStorage(BrowserToolbarAccessorySpacingDebugSettings.key) private var browserToolbarAccessorySpacingRaw = BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing
|
||||
@AppStorage(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultsKey)
|
||||
private var toggleBrowserDeveloperToolsShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.showBrowserJavaScriptConsole.defaultsKey)
|
||||
|
|
@ -39,6 +40,10 @@ struct cmuxApp: App {
|
|||
@AppStorage(KeyboardShortcutSettings.Action.closeWorkspace.defaultsKey) private var closeWorkspaceShortcutData = Data()
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
|
||||
private var browserToolbarAccessorySpacing: Int {
|
||||
BrowserToolbarAccessorySpacingDebugSettings.resolved(browserToolbarAccessorySpacingRaw)
|
||||
}
|
||||
|
||||
init() {
|
||||
if SocketControlSettings.shouldBlockUntaggedDebugLaunch() {
|
||||
Self.terminateForMissingLaunchTag()
|
||||
|
|
@ -337,6 +342,19 @@ struct cmuxApp: App {
|
|||
DebugWindowControlsWindowController.shared.show()
|
||||
}
|
||||
|
||||
Button("Browser Import Hint Debug…") {
|
||||
BrowserImportHintDebugWindowController.shared.show()
|
||||
}
|
||||
|
||||
Button(
|
||||
String(
|
||||
localized: "debug.menu.browserProfilePopoverDebug",
|
||||
defaultValue: "Browser Profile Popover Debug…"
|
||||
)
|
||||
) {
|
||||
BrowserProfilePopoverDebugWindowController.shared.show()
|
||||
}
|
||||
|
||||
Button("Settings/About Titlebar Debug…") {
|
||||
SettingsAboutTitlebarDebugWindowController.shared.show()
|
||||
}
|
||||
|
|
@ -361,6 +379,29 @@ struct cmuxApp: App {
|
|||
}
|
||||
}
|
||||
|
||||
Menu(
|
||||
String(
|
||||
localized: "debug.menu.browserToolbarButtonSpacing",
|
||||
defaultValue: "Browser Toolbar Button Spacing"
|
||||
)
|
||||
) {
|
||||
ForEach(BrowserToolbarAccessorySpacingDebugSettings.supportedValues, id: \.self) { spacing in
|
||||
Button {
|
||||
browserToolbarAccessorySpacingRaw = spacing
|
||||
} label: {
|
||||
if browserToolbarAccessorySpacing == spacing {
|
||||
Label {
|
||||
Text(verbatim: "\(spacing)")
|
||||
} icon: {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
} else {
|
||||
Text(verbatim: "\(spacing)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Always Show Shortcut Hints", isOn: $alwaysShowShortcutHints)
|
||||
Toggle(
|
||||
String(localized: "debug.devBuildBanner.show", defaultValue: "Show Dev Build Banner"),
|
||||
|
|
@ -588,7 +629,10 @@ struct cmuxApp: App {
|
|||
}
|
||||
|
||||
Button(String(localized: "menu.view.importFromBrowser", defaultValue: "Import From Browser…")) {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
// Defer modal presentation until after AppKit finishes menu tracking.
|
||||
DispatchQueue.main.async {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
}
|
||||
}
|
||||
|
||||
splitCommandButton(title: String(localized: "menu.view.nextWorkspace", defaultValue: "Next Workspace"), shortcut: nextWorkspaceMenuShortcut) {
|
||||
|
|
@ -1057,6 +1101,8 @@ struct cmuxApp: App {
|
|||
}
|
||||
|
||||
private func openAllDebugWindows() {
|
||||
BrowserImportHintDebugWindowController.shared.show()
|
||||
BrowserProfilePopoverDebugWindowController.shared.show()
|
||||
SettingsAboutTitlebarDebugWindowController.shared.show()
|
||||
SidebarDebugWindowController.shared.show()
|
||||
BackgroundDebugWindowController.shared.show()
|
||||
|
|
@ -1071,6 +1117,7 @@ private let cmuxAuxiliaryWindowIdentifiers: Set<String> = [
|
|||
"cmux.browser-popup",
|
||||
"cmux.settingsAboutTitlebarDebug",
|
||||
"cmux.debugWindowControls",
|
||||
"cmux.browserImportHintDebug",
|
||||
"cmux.sidebarDebug",
|
||||
"cmux.menubarDebug",
|
||||
"cmux.backgroundDebug",
|
||||
|
|
@ -1686,6 +1733,17 @@ private struct DebugWindowControlsView: View {
|
|||
|
||||
GroupBox("Open") {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Button("Browser Import Hint Debug…") {
|
||||
BrowserImportHintDebugWindowController.shared.show()
|
||||
}
|
||||
Button(
|
||||
String(
|
||||
localized: "debug.menu.browserProfilePopoverDebug",
|
||||
defaultValue: "Browser Profile Popover Debug…"
|
||||
)
|
||||
) {
|
||||
BrowserProfilePopoverDebugWindowController.shared.show()
|
||||
}
|
||||
Button("Settings/About Titlebar Debug…") {
|
||||
SettingsAboutTitlebarDebugWindowController.shared.show()
|
||||
}
|
||||
|
|
@ -1699,6 +1757,8 @@ private struct DebugWindowControlsView: View {
|
|||
MenuBarExtraDebugWindowController.shared.show()
|
||||
}
|
||||
Button("Open All Debug Windows") {
|
||||
BrowserImportHintDebugWindowController.shared.show()
|
||||
BrowserProfilePopoverDebugWindowController.shared.show()
|
||||
SettingsAboutTitlebarDebugWindowController.shared.show()
|
||||
SidebarDebugWindowController.shared.show()
|
||||
BackgroundDebugWindowController.shared.show()
|
||||
|
|
@ -1902,6 +1962,411 @@ private struct DebugWindowControlsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private final class BrowserImportHintDebugWindowController: NSWindowController, NSWindowDelegate {
|
||||
static let shared = BrowserImportHintDebugWindowController()
|
||||
|
||||
private init() {
|
||||
let window = NSPanel(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 380, height: 420),
|
||||
styleMask: [.titled, .closable, .utilityWindow],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.title = "Browser Import Hint Debug"
|
||||
window.titleVisibility = .visible
|
||||
window.titlebarAppearsTransparent = false
|
||||
window.isMovableByWindowBackground = true
|
||||
window.isReleasedWhenClosed = false
|
||||
window.identifier = NSUserInterfaceItemIdentifier("cmux.browserImportHintDebug")
|
||||
window.center()
|
||||
window.contentView = NSHostingView(rootView: BrowserImportHintDebugView())
|
||||
AppDelegate.shared?.applyWindowDecorations(to: window)
|
||||
super.init(window: window)
|
||||
window.delegate = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func show() {
|
||||
window?.center()
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private final class BrowserProfilePopoverDebugWindowController: NSWindowController, NSWindowDelegate {
|
||||
static let shared = BrowserProfilePopoverDebugWindowController()
|
||||
|
||||
private init() {
|
||||
let window = NSPanel(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 360, height: 340),
|
||||
styleMask: [.titled, .closable, .utilityWindow],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.title = String(
|
||||
localized: "debug.windows.browserProfilePopover.title",
|
||||
defaultValue: "Browser Profile Popover Debug"
|
||||
)
|
||||
window.titleVisibility = .visible
|
||||
window.titlebarAppearsTransparent = false
|
||||
window.isMovableByWindowBackground = true
|
||||
window.isReleasedWhenClosed = false
|
||||
window.identifier = NSUserInterfaceItemIdentifier("cmux.browserProfilePopoverDebug")
|
||||
window.center()
|
||||
window.contentView = NSHostingView(rootView: BrowserProfilePopoverDebugView())
|
||||
AppDelegate.shared?.applyWindowDecorations(to: window)
|
||||
super.init(window: window)
|
||||
window.delegate = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func show() {
|
||||
window?.center()
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private struct BrowserProfilePopoverDebugView: View {
|
||||
@AppStorage(BrowserProfilePopoverDebugSettings.horizontalPaddingKey)
|
||||
private var horizontalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultHorizontalPadding
|
||||
@AppStorage(BrowserProfilePopoverDebugSettings.verticalPaddingKey)
|
||||
private var verticalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultVerticalPadding
|
||||
|
||||
private var horizontalPaddingBinding: Binding<Double> {
|
||||
Binding(
|
||||
get: { BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding(horizontalPaddingRaw) },
|
||||
set: { horizontalPaddingRaw = BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding($0) }
|
||||
)
|
||||
}
|
||||
|
||||
private var verticalPaddingBinding: Binding<Double> {
|
||||
Binding(
|
||||
get: { BrowserProfilePopoverDebugSettings.resolvedVerticalPadding(verticalPaddingRaw) },
|
||||
set: { verticalPaddingRaw = BrowserProfilePopoverDebugSettings.resolvedVerticalPadding($0) }
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text(
|
||||
String(
|
||||
localized: "debug.browserProfilePopover.heading",
|
||||
defaultValue: "Browser Profile Popover"
|
||||
)
|
||||
)
|
||||
.font(.headline)
|
||||
|
||||
Text(
|
||||
String(
|
||||
localized: "debug.browserProfilePopover.note",
|
||||
defaultValue: "Tune the profile popover padding live while comparing it against the browser toolbar menu."
|
||||
)
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
GroupBox(
|
||||
String(
|
||||
localized: "debug.browserProfilePopover.group.padding",
|
||||
defaultValue: "Padding"
|
||||
)
|
||||
) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
sliderRow(
|
||||
String(
|
||||
localized: "debug.browserProfilePopover.label.horizontal",
|
||||
defaultValue: "Horizontal"
|
||||
),
|
||||
value: horizontalPaddingBinding,
|
||||
range: BrowserProfilePopoverDebugSettings.horizontalPaddingRange
|
||||
)
|
||||
sliderRow(
|
||||
String(
|
||||
localized: "debug.browserProfilePopover.label.vertical",
|
||||
defaultValue: "Vertical"
|
||||
),
|
||||
value: verticalPaddingBinding,
|
||||
range: BrowserProfilePopoverDebugSettings.verticalPaddingRange
|
||||
)
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
GroupBox(
|
||||
String(
|
||||
localized: "debug.browserProfilePopover.group.preview",
|
||||
defaultValue: "Preview"
|
||||
)
|
||||
) {
|
||||
profilePopoverPreview
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button(
|
||||
String(
|
||||
localized: "debug.browserProfilePopover.reset",
|
||||
defaultValue: "Reset"
|
||||
)
|
||||
) {
|
||||
horizontalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultHorizontalPadding
|
||||
verticalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultVerticalPadding
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
String(
|
||||
localized: "debug.browserProfilePopover.liveNote",
|
||||
defaultValue: "Changes apply live to the browser profile popover."
|
||||
)
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
|
||||
private var profilePopoverPreview: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(String(localized: "browser.profile.menu.title", defaultValue: "Profiles"))
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.frame(width: 12, alignment: .center)
|
||||
Text(String(localized: "browser.profile.default", defaultValue: "Default"))
|
||||
.font(.system(size: 12))
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.frame(height: 24)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
.fill(Color.primary.opacity(0.12))
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Text(String(localized: "browser.profile.new", defaultValue: "New Profile..."))
|
||||
.font(.system(size: 12))
|
||||
|
||||
Text(String(localized: "menu.view.importFromBrowser", defaultValue: "Import From Browser…"))
|
||||
.font(.system(size: 12))
|
||||
}
|
||||
.padding(.horizontal, BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding(horizontalPaddingRaw))
|
||||
.padding(.vertical, BrowserProfilePopoverDebugSettings.resolvedVerticalPadding(verticalPaddingRaw))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.fill(Color(nsColor: .windowBackgroundColor))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.stroke(Color.primary.opacity(0.08))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func sliderRow(_ label: String, value: Binding<Double>, range: ClosedRange<Double>) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
Text(label)
|
||||
Slider(value: value, in: range, step: 1)
|
||||
Text(String(format: "%.0f", value.wrappedValue))
|
||||
.font(.caption)
|
||||
.monospacedDigit()
|
||||
.frame(width: 32, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct BrowserImportHintDebugView: View {
|
||||
@AppStorage(BrowserImportHintSettings.variantKey)
|
||||
private var variantRaw = BrowserImportHintSettings.defaultVariant.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.showOnBlankTabsKey)
|
||||
private var showOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs
|
||||
@AppStorage(BrowserImportHintSettings.dismissedKey)
|
||||
private var isDismissed = BrowserImportHintSettings.defaultDismissed
|
||||
|
||||
private var selectedVariant: BrowserImportHintVariant {
|
||||
BrowserImportHintSettings.variant(for: variantRaw)
|
||||
}
|
||||
|
||||
private var variantSelection: Binding<String> {
|
||||
Binding(
|
||||
get: { selectedVariant.rawValue },
|
||||
set: { variantRaw = BrowserImportHintSettings.variant(for: $0).rawValue }
|
||||
)
|
||||
}
|
||||
|
||||
private var showOnBlankTabsBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { showOnBlankTabs },
|
||||
set: { newValue in
|
||||
showOnBlankTabs = newValue
|
||||
if newValue {
|
||||
isDismissed = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var presentation: BrowserImportHintPresentation {
|
||||
BrowserImportHintPresentation(
|
||||
variant: selectedVariant,
|
||||
showOnBlankTabs: showOnBlankTabs,
|
||||
isDismissed: isDismissed
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("Browser Import Hint")
|
||||
.font(.headline)
|
||||
|
||||
Text("Try lighter blank-tab import surfaces and dismissal states without touching the permanent Browser settings home.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
GroupBox("Variant") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Picker("Blank Tab Style", selection: variantSelection) {
|
||||
ForEach(BrowserImportHintVariant.allCases) { variant in
|
||||
Text(title(for: variant)).tag(variant.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
|
||||
Text(description(for: selectedVariant))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
GroupBox("State") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Toggle("Show on blank browser tabs", isOn: showOnBlankTabsBinding)
|
||||
Toggle("Pretend the user dismissed it", isOn: $isDismissed)
|
||||
|
||||
Text("Current blank-tab placement: \(placementTitle(presentation.blankTabPlacement))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Settings status: \(settingsStatusTitle(presentation.settingsStatus))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
GroupBox("Quick Actions") {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 10) {
|
||||
Button("Open Browser Settings") {
|
||||
AppDelegate.presentPreferencesWindow(navigationTarget: .browser)
|
||||
}
|
||||
Button("Open Import Dialog") {
|
||||
DispatchQueue.main.async {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button("Reset Hint Debug State") {
|
||||
BrowserImportHintSettings.reset()
|
||||
}
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
GroupBox("Ideas") {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Inline strip: default candidate, visible but quieter than the old floating card.")
|
||||
Text("Floating card: strongest nudge, useful when we want more explanation.")
|
||||
Text("Toolbar chip: most subtle, best when the hint should stay out of the content area.")
|
||||
Text("Settings only: no in-browser nudge, Browser settings becomes the only permanent home.")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
|
||||
private func title(for variant: BrowserImportHintVariant) -> String {
|
||||
switch variant {
|
||||
case .inlineStrip:
|
||||
return "Inline Strip"
|
||||
case .floatingCard:
|
||||
return "Floating Card"
|
||||
case .toolbarChip:
|
||||
return "Toolbar Chip"
|
||||
case .settingsOnly:
|
||||
return "Settings Only"
|
||||
}
|
||||
}
|
||||
|
||||
private func description(for variant: BrowserImportHintVariant) -> String {
|
||||
switch variant {
|
||||
case .inlineStrip:
|
||||
return "Shows a thin hint bar at the top of blank browser tabs."
|
||||
case .floatingCard:
|
||||
return "Shows the fuller callout card inside blank browser tabs."
|
||||
case .toolbarChip:
|
||||
return "Moves the hint into a small toolbar chip beside the browser controls."
|
||||
case .settingsOnly:
|
||||
return "Hides the blank-tab hint and leaves Browser settings as the only home."
|
||||
}
|
||||
}
|
||||
|
||||
private func placementTitle(_ placement: BrowserImportHintBlankTabPlacement) -> String {
|
||||
switch placement {
|
||||
case .hidden:
|
||||
return "Hidden"
|
||||
case .inlineStrip:
|
||||
return "Inline Strip"
|
||||
case .floatingCard:
|
||||
return "Floating Card"
|
||||
case .toolbarChip:
|
||||
return "Toolbar Chip"
|
||||
}
|
||||
}
|
||||
|
||||
private func settingsStatusTitle(_ status: BrowserImportHintSettingsStatus) -> String {
|
||||
switch status {
|
||||
case .visible:
|
||||
return "Visible"
|
||||
case .hidden:
|
||||
return "Hidden"
|
||||
case .settingsOnly:
|
||||
return "Settings Only"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class AboutWindowController: NSWindowController, NSWindowDelegate {
|
||||
static let shared = AboutWindowController()
|
||||
|
||||
|
|
@ -2032,6 +2497,8 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
|||
}
|
||||
|
||||
enum SettingsNavigationTarget: String {
|
||||
case browser
|
||||
case browserImport
|
||||
case keyboardShortcuts
|
||||
}
|
||||
|
||||
|
|
@ -3100,6 +3567,9 @@ struct SettingsView: View {
|
|||
@AppStorage(BrowserSearchSettings.searchEngineKey) private var browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue
|
||||
@AppStorage(BrowserSearchSettings.searchSuggestionsEnabledKey) private var browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled
|
||||
@AppStorage(BrowserThemeSettings.modeKey) private var browserThemeMode = BrowserThemeSettings.defaultMode.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.variantKey) private var browserImportHintVariantRaw = BrowserImportHintSettings.defaultVariant.rawValue
|
||||
@AppStorage(BrowserImportHintSettings.showOnBlankTabsKey) private var showBrowserImportHintOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs
|
||||
@AppStorage(BrowserImportHintSettings.dismissedKey) private var isBrowserImportHintDismissed = BrowserImportHintSettings.defaultDismissed
|
||||
@AppStorage(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) private var openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser
|
||||
@AppStorage(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey)
|
||||
private var interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.initialInterceptTerminalOpenCommandInCmuxBrowserValue()
|
||||
|
|
@ -3146,6 +3616,7 @@ struct SettingsView: View {
|
|||
@AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String?
|
||||
@AppStorage("sidebarTintHexDark") private var sidebarTintHexDark: String?
|
||||
@AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity
|
||||
|
||||
@ObservedObject private var notificationStore = TerminalNotificationStore.shared
|
||||
@State private var shortcutResetToken = UUID()
|
||||
@State private var topBlurOpacity: Double = 0
|
||||
|
|
@ -3202,6 +3673,30 @@ struct SettingsView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private var browserImportHintVariant: BrowserImportHintVariant {
|
||||
BrowserImportHintSettings.variant(for: browserImportHintVariantRaw)
|
||||
}
|
||||
|
||||
private var browserImportHintPresentation: BrowserImportHintPresentation {
|
||||
BrowserImportHintPresentation(
|
||||
variant: browserImportHintVariant,
|
||||
showOnBlankTabs: showBrowserImportHintOnBlankTabs,
|
||||
isDismissed: isBrowserImportHintDismissed
|
||||
)
|
||||
}
|
||||
|
||||
private var browserImportHintVisibilityBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { showBrowserImportHintOnBlankTabs },
|
||||
set: { newValue in
|
||||
showBrowserImportHintOnBlankTabs = newValue
|
||||
if newValue {
|
||||
isBrowserImportHintDismissed = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var socketModeSelection: Binding<String> {
|
||||
Binding(
|
||||
get: { socketControlMode },
|
||||
|
|
@ -3264,6 +3759,17 @@ struct SettingsView: View {
|
|||
InstalledBrowserDetector.summaryText(for: detectedImportBrowsers)
|
||||
}
|
||||
|
||||
private var browserImportHintSettingsNote: String {
|
||||
switch browserImportHintPresentation.settingsStatus {
|
||||
case .visible:
|
||||
return String(localized: "settings.browser.import.hint.note.visible", defaultValue: "Blank browser tabs can show this import suggestion. Hide or re-enable it here.")
|
||||
case .hidden:
|
||||
return String(localized: "settings.browser.import.hint.note.hidden", defaultValue: "The blank-tab import hint is hidden. Turn it back on here any time.")
|
||||
case .settingsOnly:
|
||||
return String(localized: "settings.browser.import.hint.note.settingsOnly", defaultValue: "Blank tabs are currently using Settings only mode from the debug window.")
|
||||
}
|
||||
}
|
||||
|
||||
private var browserInsecureHTTPAllowlistHasUnsavedChanges: Bool {
|
||||
browserInsecureHTTPAllowlistDraft != browserInsecureHTTPAllowlist
|
||||
}
|
||||
|
|
@ -4196,6 +4702,8 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
SettingsSectionHeader(title: String(localized: "settings.section.browser", defaultValue: "Browser"))
|
||||
.id(SettingsNavigationTarget.browser)
|
||||
.accessibilityIdentifier("SettingsBrowserSection")
|
||||
SettingsCard {
|
||||
SettingsPickerRow(
|
||||
String(localized: "settings.browser.searchEngine", defaultValue: "Default Search Engine"),
|
||||
|
|
@ -4370,14 +4878,48 @@ struct SettingsView: View {
|
|||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(String(localized: "settings.browser.import", defaultValue: "Import From Browser"), subtitle: browserImportSubtitle) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(String(localized: "settings.browser.import", defaultValue: "Import From Browser"))
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(String(localized: "browser.import.hint.title", defaultValue: "Import browser data"))
|
||||
.font(.system(size: 12.5, weight: .semibold))
|
||||
|
||||
Text(browserImportSubtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(String(localized: "browser.import.hint.settingsFootnote", defaultValue: "You can always find this in Settings > Browser."))
|
||||
.font(.system(size: 10.5))
|
||||
.foregroundStyle(.tertiary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color(nsColor: .controlBackgroundColor))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.stroke(Color(nsColor: .separatorColor).opacity(0.4), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button(String(localized: "settings.browser.import.choose", defaultValue: "Choose…")) {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
refreshDetectedImportBrowsers()
|
||||
DispatchQueue.main.async {
|
||||
BrowserDataImportCoordinator.shared.presentImportDialog()
|
||||
refreshDetectedImportBrowsers()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("SettingsBrowserImportChooseButton")
|
||||
|
||||
Button(String(localized: "settings.browser.import.refresh", defaultValue: "Refresh")) {
|
||||
refreshDetectedImportBrowsers()
|
||||
|
|
@ -4385,7 +4927,24 @@ struct SettingsView: View {
|
|||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
.accessibilityIdentifier("SettingsBrowserImportActions")
|
||||
|
||||
Toggle(
|
||||
String(localized: "settings.browser.import.hint.show", defaultValue: "Show import hint on blank browser tabs"),
|
||||
isOn: browserImportHintVisibilityBinding
|
||||
)
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("SettingsBrowserImportHintToggle")
|
||||
|
||||
Text(browserImportHintSettingsNote)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.id(SettingsNavigationTarget.browserImport)
|
||||
.accessibilityIdentifier("SettingsBrowserImportSection")
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
|
|
@ -4529,6 +5088,7 @@ struct SettingsView: View {
|
|||
BrowserHistoryStore.shared.loadIfNeeded()
|
||||
notificationStore.refreshAuthorizationStatus()
|
||||
browserThemeMode = BrowserThemeSettings.mode(defaults: .standard).rawValue
|
||||
browserImportHintVariantRaw = BrowserImportHintSettings.variant(for: browserImportHintVariantRaw).rawValue
|
||||
browserHistoryEntryCount = BrowserHistoryStore.shared.entries.count
|
||||
browserInsecureHTTPAllowlistDraft = browserInsecureHTTPAllowlist
|
||||
refreshDetectedImportBrowsers()
|
||||
|
|
@ -4642,6 +5202,9 @@ struct SettingsView: View {
|
|||
browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue
|
||||
browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled
|
||||
browserThemeMode = BrowserThemeSettings.defaultMode.rawValue
|
||||
browserImportHintVariantRaw = BrowserImportHintSettings.defaultVariant.rawValue
|
||||
showBrowserImportHintOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs
|
||||
isBrowserImportHintDismissed = BrowserImportHintSettings.defaultDismissed
|
||||
openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser
|
||||
interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.defaultInterceptTerminalOpenCommandInCmuxBrowser
|
||||
browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist
|
||||
|
|
|
|||
|
|
@ -2693,6 +2693,24 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
|
|||
XCTAssertEqual(activateApplicationCallCount, 1)
|
||||
}
|
||||
|
||||
func testPresentPreferencesWindowForwardsBrowserImportNavigationTarget() {
|
||||
var receivedNavigationTarget: SettingsNavigationTarget?
|
||||
var activateApplicationCallCount = 0
|
||||
|
||||
AppDelegate.presentPreferencesWindow(
|
||||
navigationTarget: .browserImport,
|
||||
showFallbackSettingsWindow: { navigationTarget in
|
||||
receivedNavigationTarget = navigationTarget
|
||||
},
|
||||
activateApplication: {
|
||||
activateApplicationCallCount += 1
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(receivedNavigationTarget, .browserImport)
|
||||
XCTAssertEqual(activateApplicationCallCount, 1)
|
||||
}
|
||||
|
||||
private func makeKeyDownEvent(
|
||||
key: String,
|
||||
modifiers: NSEvent.ModifierFlags,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,68 @@ final class BrowserImportMappingTests: XCTestCase {
|
|||
XCTAssertTrue(presentation.showsSingleDestinationPicker)
|
||||
}
|
||||
|
||||
func testSourceProfilesPresentationShrinksListForSmallProfileCounts() {
|
||||
let presentation = BrowserImportSourceProfilesPresentation(profileCount: 2)
|
||||
|
||||
XCTAssertEqual(presentation.scrollHeight, 76)
|
||||
XCTAssertTrue(presentation.showsHelpText)
|
||||
}
|
||||
|
||||
func testSourceProfilesPresentationCapsListHeightAndHidesHelpForSingleProfile() {
|
||||
let singleProfilePresentation = BrowserImportSourceProfilesPresentation(profileCount: 1)
|
||||
let manyProfilesPresentation = BrowserImportSourceProfilesPresentation(profileCount: 9)
|
||||
|
||||
XCTAssertEqual(singleProfilePresentation.scrollHeight, 76)
|
||||
XCTAssertFalse(singleProfilePresentation.showsHelpText)
|
||||
XCTAssertEqual(manyProfilesPresentation.scrollHeight, 144)
|
||||
XCTAssertTrue(manyProfilesPresentation.showsHelpText)
|
||||
}
|
||||
|
||||
func testBrowserImportHintSettingsDefaultToToolbarChip() throws {
|
||||
let suiteName = "BrowserImportHintDefaults-\(UUID().uuidString)"
|
||||
let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
let presentation = BrowserImportHintSettings.presentation(defaults: defaults)
|
||||
|
||||
XCTAssertEqual(presentation.blankTabPlacement, .toolbarChip)
|
||||
XCTAssertEqual(presentation.settingsStatus, .visible)
|
||||
}
|
||||
|
||||
func testBrowserImportHintPresentationHidesBlankTabHintWhenDismissed() {
|
||||
let presentation = BrowserImportHintPresentation(
|
||||
variant: .floatingCard,
|
||||
showOnBlankTabs: true,
|
||||
isDismissed: true
|
||||
)
|
||||
|
||||
XCTAssertEqual(presentation.blankTabPlacement, .hidden)
|
||||
XCTAssertEqual(presentation.settingsStatus, .hidden)
|
||||
}
|
||||
|
||||
func testBrowserImportHintPresentationUsesToolbarChipWhenEnabled() {
|
||||
let presentation = BrowserImportHintPresentation(
|
||||
variant: .toolbarChip,
|
||||
showOnBlankTabs: true,
|
||||
isDismissed: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(presentation.blankTabPlacement, .toolbarChip)
|
||||
XCTAssertEqual(presentation.settingsStatus, .visible)
|
||||
}
|
||||
|
||||
func testBrowserImportHintPresentationSettingsOnlyVariantStaysInSettings() {
|
||||
let presentation = BrowserImportHintPresentation(
|
||||
variant: .settingsOnly,
|
||||
showOnBlankTabs: true,
|
||||
isDismissed: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(presentation.blankTabPlacement, .hidden)
|
||||
XCTAssertEqual(presentation.settingsStatus, .settingsOnly)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testRealizePlanCreatesMissingDestinationProfilesOnlyWhenRequested() throws {
|
||||
let suiteName = "BrowserImportMappingTests-\(UUID().uuidString)"
|
||||
|
|
@ -222,6 +284,39 @@ final class BrowserImportMappingTests: XCTestCase {
|
|||
XCTAssertTrue(lines.contains("Created cmux profiles: You, austin"))
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testImportWizardCanBeConstructedForSettingsChoosePath() {
|
||||
let destinationProfiles = [
|
||||
BrowserProfileDefinition(
|
||||
id: UUID(uuidString: "52B43C05-4A1D-45D3-8FD5-9EF94952E445")!,
|
||||
displayName: "Default",
|
||||
createdAt: .distantPast,
|
||||
isBuiltInDefault: true
|
||||
)
|
||||
]
|
||||
let browser = makeInstalledBrowserCandidate(
|
||||
descriptorID: "google-chrome",
|
||||
displayName: "Chrome",
|
||||
profiles: [
|
||||
makeSourceProfile(displayName: "Default", path: "/tmp/browser-import-chrome-default", isDefault: true),
|
||||
makeSourceProfile(displayName: "Profile 1", path: "/tmp/browser-import-chrome-profile-1", isDefault: false),
|
||||
]
|
||||
)
|
||||
|
||||
let window = BrowserDataImportCoordinator.shared.debugMakeImportWizardWindow(
|
||||
browsers: [browser],
|
||||
destinationProfiles: destinationProfiles,
|
||||
defaultDestinationProfileID: destinationProfiles[0].id
|
||||
)
|
||||
defer {
|
||||
window.orderOut(nil)
|
||||
window.close()
|
||||
}
|
||||
|
||||
XCTAssertEqual(window.title, "Import Browser Data")
|
||||
XCTAssertNotNil(window.contentView)
|
||||
}
|
||||
|
||||
private func makeSourceProfile(displayName: String, path: String, isDefault: Bool) -> InstalledBrowserProfile {
|
||||
InstalledBrowserProfile(
|
||||
displayName: displayName,
|
||||
|
|
@ -229,4 +324,32 @@ final class BrowserImportMappingTests: XCTestCase {
|
|||
isDefault: isDefault
|
||||
)
|
||||
}
|
||||
|
||||
private func makeInstalledBrowserCandidate(
|
||||
descriptorID: String,
|
||||
displayName: String,
|
||||
profiles: [InstalledBrowserProfile]
|
||||
) -> InstalledBrowserCandidate {
|
||||
let descriptor = try! XCTUnwrap(InstalledBrowserDetector.allBrowserDescriptors.first(where: { $0.id == descriptorID }))
|
||||
return InstalledBrowserCandidate(
|
||||
descriptor: BrowserImportBrowserDescriptor(
|
||||
id: descriptor.id,
|
||||
displayName: displayName,
|
||||
family: descriptor.family,
|
||||
tier: descriptor.tier,
|
||||
bundleIdentifiers: descriptor.bundleIdentifiers,
|
||||
appNames: descriptor.appNames,
|
||||
dataRootRelativePaths: descriptor.dataRootRelativePaths,
|
||||
dataArtifactRelativePaths: descriptor.dataArtifactRelativePaths,
|
||||
supportsDataOnlyDetection: descriptor.supportsDataOnlyDetection
|
||||
),
|
||||
resolvedFamily: descriptor.family,
|
||||
homeDirectoryURL: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true),
|
||||
appURL: nil,
|
||||
dataRootURL: URL(fileURLWithPath: "/tmp/browser-import-\(descriptorID)", isDirectory: true),
|
||||
profiles: profiles,
|
||||
detectionSignals: ["test"],
|
||||
detectionScore: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@ private func drainMainQueue() {
|
|||
XCTWaiter().wait(for: [expectation], timeout: 1.0)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func makeTemporaryBrowserProfile(named prefix: String) throws -> BrowserProfileDefinition {
|
||||
try XCTUnwrap(
|
||||
BrowserProfileStore.shared.createProfile(
|
||||
named: "\(prefix)-\(UUID().uuidString)"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
final class SplitShortcutTransientFocusGuardTests: XCTestCase {
|
||||
func testSuppressesWhenFirstResponderFallsBackAndHostedViewIsTiny() {
|
||||
XCTAssertTrue(
|
||||
|
|
@ -1461,6 +1470,56 @@ final class BrowserDevToolsButtonDebugSettingsTests: XCTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
func testBrowserToolbarAccessorySpacingDefaultsToTwoWhenUnset() {
|
||||
let defaults = makeIsolatedDefaults()
|
||||
defaults.removeObject(forKey: BrowserToolbarAccessorySpacingDebugSettings.key)
|
||||
|
||||
XCTAssertEqual(
|
||||
BrowserToolbarAccessorySpacingDebugSettings.current(defaults: defaults),
|
||||
BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing
|
||||
)
|
||||
}
|
||||
|
||||
func testBrowserToolbarAccessorySpacingFallsBackToDefaultForUnsupportedValue() {
|
||||
let defaults = makeIsolatedDefaults()
|
||||
defaults.set(99, forKey: BrowserToolbarAccessorySpacingDebugSettings.key)
|
||||
|
||||
XCTAssertEqual(
|
||||
BrowserToolbarAccessorySpacingDebugSettings.current(defaults: defaults),
|
||||
BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing
|
||||
)
|
||||
}
|
||||
|
||||
func testBrowserProfilePopoverPaddingDefaultsWhenUnset() {
|
||||
let defaults = makeIsolatedDefaults()
|
||||
defaults.removeObject(forKey: BrowserProfilePopoverDebugSettings.horizontalPaddingKey)
|
||||
defaults.removeObject(forKey: BrowserProfilePopoverDebugSettings.verticalPaddingKey)
|
||||
|
||||
XCTAssertEqual(
|
||||
BrowserProfilePopoverDebugSettings.currentHorizontalPadding(defaults: defaults),
|
||||
BrowserProfilePopoverDebugSettings.defaultHorizontalPadding
|
||||
)
|
||||
XCTAssertEqual(
|
||||
BrowserProfilePopoverDebugSettings.currentVerticalPadding(defaults: defaults),
|
||||
BrowserProfilePopoverDebugSettings.defaultVerticalPadding
|
||||
)
|
||||
}
|
||||
|
||||
func testBrowserProfilePopoverPaddingFallsBackForUnsupportedValues() {
|
||||
let defaults = makeIsolatedDefaults()
|
||||
defaults.set(-3, forKey: BrowserProfilePopoverDebugSettings.horizontalPaddingKey)
|
||||
defaults.set(999, forKey: BrowserProfilePopoverDebugSettings.verticalPaddingKey)
|
||||
|
||||
XCTAssertEqual(
|
||||
BrowserProfilePopoverDebugSettings.currentHorizontalPadding(defaults: defaults),
|
||||
BrowserProfilePopoverDebugSettings.defaultHorizontalPadding
|
||||
)
|
||||
XCTAssertEqual(
|
||||
BrowserProfilePopoverDebugSettings.currentVerticalPadding(defaults: defaults),
|
||||
BrowserProfilePopoverDebugSettings.defaultVerticalPadding
|
||||
)
|
||||
}
|
||||
|
||||
func testCopyPayloadUsesPersistedValues() {
|
||||
let defaults = makeIsolatedDefaults()
|
||||
defaults.set(BrowserDevToolsIconOption.scope.rawValue, forKey: BrowserDevToolsButtonDebugSettings.iconNameKey)
|
||||
|
|
@ -6362,6 +6421,129 @@ final class WorkspaceTerminalConfigInheritanceSelectionTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class WorkspaceBrowserProfileSelectionTests: XCTestCase {
|
||||
private final class RejectingCreateTabDelegate: BonsplitDelegate {
|
||||
func splitTabBar(_ controller: BonsplitController, shouldCreateTab tab: Bonsplit.Tab, inPane pane: PaneID) -> Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private final class RejectingSplitPaneDelegate: BonsplitDelegate {
|
||||
func splitTabBar(_ controller: BonsplitController, shouldSplitPane pane: PaneID, orientation: SplitOrientation) -> Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
func testNewBrowserSurfacePrefersSelectedBrowserProfileInTargetPane() throws {
|
||||
let workspace = Workspace()
|
||||
let profileA = try makeTemporaryBrowserProfile(named: "Alpha")
|
||||
let profileB = try makeTemporaryBrowserProfile(named: "Beta")
|
||||
let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId)
|
||||
let browserA = try XCTUnwrap(
|
||||
workspace.newBrowserSurface(
|
||||
inPane: paneId,
|
||||
focus: true,
|
||||
preferredProfileID: profileA.id
|
||||
)
|
||||
)
|
||||
_ = try XCTUnwrap(
|
||||
workspace.newBrowserSplit(
|
||||
from: browserA.id,
|
||||
orientation: .horizontal,
|
||||
preferredProfileID: profileB.id,
|
||||
focus: true
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
workspace.preferredBrowserProfileID,
|
||||
profileB.id,
|
||||
"Expected workspace preference to drift to the most recently created browser profile"
|
||||
)
|
||||
|
||||
let leftSurfaceId = try XCTUnwrap(workspace.surfaceIdFromPanelId(browserA.id))
|
||||
workspace.bonsplitController.focusPane(paneId)
|
||||
workspace.bonsplitController.selectTab(leftSurfaceId)
|
||||
|
||||
let created = try XCTUnwrap(
|
||||
workspace.newBrowserSurface(
|
||||
inPane: paneId,
|
||||
focus: false
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
created.profileID,
|
||||
profileA.id,
|
||||
"Expected new browser creation to inherit the selected browser profile from the target pane"
|
||||
)
|
||||
}
|
||||
|
||||
func testNewBrowserSurfaceFailureDoesNotMutatePreferredProfile() throws {
|
||||
let workspace = Workspace()
|
||||
let preferredProfile = try makeTemporaryBrowserProfile(named: "Preferred")
|
||||
let unexpectedProfile = try makeTemporaryBrowserProfile(named: "Unexpected")
|
||||
|
||||
let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId)
|
||||
_ = try XCTUnwrap(
|
||||
workspace.newBrowserSurface(
|
||||
inPane: paneId,
|
||||
focus: false,
|
||||
preferredProfileID: preferredProfile.id
|
||||
)
|
||||
)
|
||||
XCTAssertEqual(workspace.preferredBrowserProfileID, preferredProfile.id)
|
||||
|
||||
let rejectingDelegate = RejectingCreateTabDelegate()
|
||||
workspace.bonsplitController.delegate = rejectingDelegate
|
||||
let created = workspace.newBrowserSurface(
|
||||
inPane: paneId,
|
||||
focus: false,
|
||||
preferredProfileID: unexpectedProfile.id
|
||||
)
|
||||
|
||||
XCTAssertNil(created)
|
||||
XCTAssertEqual(
|
||||
workspace.preferredBrowserProfileID,
|
||||
preferredProfile.id,
|
||||
"Expected a failed browser creation to leave the workspace preferred profile unchanged"
|
||||
)
|
||||
}
|
||||
|
||||
func testNewBrowserSplitFailureDoesNotMutatePreferredProfile() throws {
|
||||
let workspace = Workspace()
|
||||
let preferredProfile = try makeTemporaryBrowserProfile(named: "Preferred")
|
||||
let unexpectedProfile = try makeTemporaryBrowserProfile(named: "Unexpected")
|
||||
|
||||
let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId)
|
||||
let browser = try XCTUnwrap(
|
||||
workspace.newBrowserSurface(
|
||||
inPane: paneId,
|
||||
focus: true,
|
||||
preferredProfileID: preferredProfile.id
|
||||
)
|
||||
)
|
||||
XCTAssertEqual(workspace.preferredBrowserProfileID, preferredProfile.id)
|
||||
|
||||
let rejectingDelegate = RejectingSplitPaneDelegate()
|
||||
workspace.bonsplitController.delegate = rejectingDelegate
|
||||
let created = workspace.newBrowserSplit(
|
||||
from: browser.id,
|
||||
orientation: .horizontal,
|
||||
preferredProfileID: unexpectedProfile.id,
|
||||
focus: false
|
||||
)
|
||||
|
||||
XCTAssertNil(created)
|
||||
XCTAssertEqual(
|
||||
workspace.preferredBrowserProfileID,
|
||||
preferredProfile.id,
|
||||
"Expected a failed browser split to leave the workspace preferred profile unchanged"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class TabManagerWorkspaceConfigInheritanceSourceTests: XCTestCase {
|
||||
func testUsesFocusedTerminalWhenTerminalIsFocused() {
|
||||
|
|
@ -6419,6 +6601,52 @@ final class TabManagerWorkspaceConfigInheritanceSourceTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class BrowserPanelProfileIsolationTests: XCTestCase {
|
||||
func testStaleDidFinishDoesNotRecordVisitIntoSwitchedProfileHistory() throws {
|
||||
let alternateProfile = try makeTemporaryBrowserProfile(named: "Switched")
|
||||
let defaultStore = BrowserHistoryStore.shared
|
||||
let alternateStore = BrowserProfileStore.shared.historyStore(for: alternateProfile.id)
|
||||
defaultStore.clearHistory()
|
||||
alternateStore.clearHistory()
|
||||
defer {
|
||||
defaultStore.clearHistory()
|
||||
alternateStore.clearHistory()
|
||||
}
|
||||
|
||||
let panel = BrowserPanel(
|
||||
workspaceId: UUID(),
|
||||
profileID: BrowserProfileStore.shared.builtInDefaultProfileID
|
||||
)
|
||||
let staleWebView = panel.webView
|
||||
let staleDelegate = try XCTUnwrap(staleWebView.navigationDelegate)
|
||||
let staleURL = try XCTUnwrap(URL(string: "https://example.com/stale-finish"))
|
||||
staleWebView.loadHTMLString(
|
||||
"<html><head><title>Stale</title></head><body>stale</body></html>",
|
||||
baseURL: staleURL
|
||||
)
|
||||
|
||||
XCTAssertTrue(
|
||||
panel.switchToProfile(alternateProfile.id),
|
||||
"Expected profile switch to succeed, current=\(panel.profileID) requested=\(alternateProfile.id) exists=\(BrowserProfileStore.shared.profileDefinition(id: alternateProfile.id) != nil)"
|
||||
)
|
||||
defaultStore.clearHistory()
|
||||
alternateStore.clearHistory()
|
||||
|
||||
staleDelegate.webView?(staleWebView, didFinish: nil)
|
||||
drainMainQueue()
|
||||
|
||||
XCTAssertTrue(
|
||||
defaultStore.entries.isEmpty,
|
||||
"Expected stale completion callbacks to avoid writing into the old profile history store, found \(defaultStore.entries.map { $0.url })"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
alternateStore.entries.isEmpty,
|
||||
"Expected stale completion callbacks to avoid writing into the newly selected profile history store, found \(alternateStore.entries.map { $0.url })"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class TabManagerReopenClosedBrowserFocusTests: XCTestCase {
|
||||
func testReopenFromDifferentWorkspaceFocusesReopenedBrowser() {
|
||||
|
|
|
|||
|
|
@ -2540,16 +2540,20 @@ final class BrowserInstallDetectorTests: XCTestCase {
|
|||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(safari.profiles.map(\.displayName), ["Default", "Work", "Travel"])
|
||||
XCTAssertEqual(Set(safari.profiles.map(\.displayName)), Set(["Default", "Work", "Travel"]))
|
||||
XCTAssertEqual(
|
||||
safari.profiles.map { $0.rootURL.path(percentEncoded: false) }.sorted(),
|
||||
safari.profiles
|
||||
.map { $0.rootURL.standardizedFileURL.resolvingSymlinksInPath().path(percentEncoded: false) }
|
||||
.sorted(),
|
||||
[
|
||||
home.appendingPathComponent("Library/Safari", isDirectory: true).path(percentEncoded: false),
|
||||
home.appendingPathComponent("Library/Safari/Profiles/Work", isDirectory: true).path(percentEncoded: false),
|
||||
home.appendingPathComponent("Library/Safari", isDirectory: true)
|
||||
.standardizedFileURL.resolvingSymlinksInPath().path(percentEncoded: false),
|
||||
home.appendingPathComponent("Library/Safari/Profiles/Work", isDirectory: true)
|
||||
.standardizedFileURL.resolvingSymlinksInPath().path(percentEncoded: false),
|
||||
home.appendingPathComponent(
|
||||
"Library/Containers/com.apple.Safari/Data/Library/Safari/Profiles/Travel",
|
||||
isDirectory: true
|
||||
).path(percentEncoded: false),
|
||||
).standardizedFileURL.resolvingSymlinksInPath().path(percentEncoded: false),
|
||||
].sorted()
|
||||
)
|
||||
}
|
||||
|
|
@ -2560,7 +2564,12 @@ final class BrowserInstallDetectorTests: XCTestCase {
|
|||
|
||||
private func createFile(at url: URL, contents: Data) throws {
|
||||
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
_ = FileManager.default.createFile(atPath: url.path, contents: contents)
|
||||
guard FileManager.default.createFile(atPath: url.path, contents: contents) else {
|
||||
throw CocoaError(
|
||||
.fileWriteUnknown,
|
||||
userInfo: [NSFilePathErrorKey: url.path]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ final class SessionPersistenceTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testSessionBrowserPanelSnapshotHistoryRoundTrip() throws {
|
||||
let profileID = UUID(uuidString: "8F03A658-5A84-428B-AD03-5A6D04692F64")
|
||||
let profileID = try XCTUnwrap(UUID(uuidString: "8F03A658-5A84-428B-AD03-5A6D04692F64"))
|
||||
let source = SessionBrowserPanelSnapshot(
|
||||
urlString: "https://example.com/current",
|
||||
profileID: profileID,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,23 @@
|
|||
import XCTest
|
||||
import Foundation
|
||||
|
||||
private func browserImportPollUntil(
|
||||
timeout: TimeInterval,
|
||||
pollInterval: TimeInterval = 0.05,
|
||||
condition: () -> Bool
|
||||
) -> Bool {
|
||||
let start = ProcessInfo.processInfo.systemUptime
|
||||
while true {
|
||||
if condition() {
|
||||
return true
|
||||
}
|
||||
if (ProcessInfo.processInfo.systemUptime - start) >= timeout {
|
||||
return false
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(pollInterval))
|
||||
}
|
||||
}
|
||||
|
||||
final class BrowserImportProfilesUITests: XCTestCase {
|
||||
private var capturePath = ""
|
||||
|
||||
|
|
@ -14,15 +31,14 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
func testMultipleSourceProfilesDefaultToSeparateDestinations() throws {
|
||||
let app = launchApp()
|
||||
|
||||
openImportWizard(app)
|
||||
app.buttons["Next"].click()
|
||||
app.buttons["Next"].click()
|
||||
|
||||
XCTAssertTrue(
|
||||
app.radioButtons["Keep profiles separate"].waitForExistence(timeout: 5.0),
|
||||
app.radioButtons["Separate profiles"].waitForExistence(timeout: 5.0),
|
||||
"Expected Step 3 to show the separate-profiles default"
|
||||
)
|
||||
XCTAssertTrue(app.radioButtons["Merge all into one cmux profile"].exists)
|
||||
XCTAssertTrue(app.radioButtons["Merge into one"].exists)
|
||||
XCTAssertTrue(app.popUpButtons["BrowserImportDestinationPopup-you"].exists)
|
||||
XCTAssertTrue(app.popUpButtons["BrowserImportDestinationPopup-austin"].exists)
|
||||
|
||||
|
|
@ -45,11 +61,10 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
func testMergeModeCapturesSingleMergedDestination() throws {
|
||||
let app = launchApp()
|
||||
|
||||
openImportWizard(app)
|
||||
app.buttons["Next"].click()
|
||||
app.buttons["Next"].click()
|
||||
|
||||
let mergeRadio = app.radioButtons["Merge all into one cmux profile"]
|
||||
let mergeRadio = app.radioButtons["Merge into one"]
|
||||
XCTAssertTrue(mergeRadio.waitForExistence(timeout: 5.0))
|
||||
mergeRadio.click()
|
||||
|
||||
|
|
@ -73,7 +88,6 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
func testAdditionalDataSelectionCapturesEverythingScope() throws {
|
||||
let app = launchApp()
|
||||
|
||||
openImportWizard(app)
|
||||
app.buttons["Next"].click()
|
||||
app.buttons["Next"].click()
|
||||
|
||||
|
|
@ -98,6 +112,45 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
XCTAssertEqual(capture["scope"] as? String, "everything")
|
||||
}
|
||||
|
||||
func testBlankBrowserImportHintCanOpenBrowserSettings() {
|
||||
let app = launchAppForBlankImportHint()
|
||||
|
||||
let settingsButton = app.buttons["BrowserImportHintSettingsButton"]
|
||||
XCTAssertTrue(settingsButton.waitForExistence(timeout: 5.0))
|
||||
settingsButton.click()
|
||||
|
||||
let importSection = app.otherElements["SettingsBrowserImportSection"]
|
||||
XCTAssertTrue(
|
||||
importSection.waitForExistence(timeout: 5.0),
|
||||
"Expected Browser Settings to scroll to the import section"
|
||||
)
|
||||
|
||||
let chooseButton = app.buttons["SettingsBrowserImportChooseButton"]
|
||||
XCTAssertTrue(
|
||||
chooseButton.waitForExistence(timeout: 5.0),
|
||||
"Expected Browser Settings to expose the import actions"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
browserImportPollUntil(timeout: 5.0) {
|
||||
importSection.isHittable && chooseButton.isHittable
|
||||
},
|
||||
"Expected Browser Settings to scroll directly to the import controls"
|
||||
)
|
||||
}
|
||||
|
||||
func testBlankBrowserImportHintCanBeDismissed() {
|
||||
let app = launchAppForBlankImportHint()
|
||||
|
||||
let dismissButton = app.buttons["BrowserImportHintDismissButton"]
|
||||
XCTAssertTrue(dismissButton.waitForExistence(timeout: 5.0))
|
||||
dismissButton.click()
|
||||
|
||||
XCTAssertTrue(
|
||||
browserImportPollUntil(timeout: 2.0) { !dismissButton.exists },
|
||||
"Expected the blank-tab import hint to disappear after dismissal"
|
||||
)
|
||||
}
|
||||
|
||||
private func launchApp() -> XCUIApplication {
|
||||
let app = XCUIApplication()
|
||||
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||
|
|
@ -105,55 +158,79 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_DESTINATIONS"] = #"["Default"]"#
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_MODE"] = "capture-only"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_CAPTURE_PATH"] = capturePath
|
||||
app.launch()
|
||||
XCTAssertTrue(
|
||||
ensureForegroundAfterLaunch(app, timeout: 12.0),
|
||||
"Expected app to launch in the foreground for browser import UI tests"
|
||||
)
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_VARIANT"] = "inlineStrip"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_SHOW"] = "1"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_DISMISSED"] = "0"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_OPEN_BLANK_BROWSER"] = "1"
|
||||
launchAndActivate(app)
|
||||
openImportWizardFromBlankImportHint(app)
|
||||
return app
|
||||
}
|
||||
|
||||
private func openImportWizard(_ app: XCUIApplication) {
|
||||
let viewMenu = app.menuBars.menuBarItems["View"].firstMatch
|
||||
XCTAssertTrue(viewMenu.waitForExistence(timeout: 5.0), "Expected View menu to exist")
|
||||
viewMenu.click()
|
||||
private func launchAppForBlankImportHint() -> XCUIApplication {
|
||||
let app = XCUIApplication()
|
||||
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_VARIANT"] = "inlineStrip"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_SHOW"] = "1"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_DISMISSED"] = "0"
|
||||
app.launchEnvironment["CMUX_UI_TEST_BROWSER_IMPORT_HINT_OPEN_BLANK_BROWSER"] = "1"
|
||||
launchAndActivate(app)
|
||||
waitForBlankImportHint(app)
|
||||
return app
|
||||
}
|
||||
|
||||
let importItem = app.menuItems["Import From Browser…"].firstMatch
|
||||
XCTAssertTrue(importItem.waitForExistence(timeout: 5.0), "Expected Import From Browser menu item to exist")
|
||||
importItem.click()
|
||||
private func waitForImportWizard(_ app: XCUIApplication) {
|
||||
let wizardOpened = browserImportPollUntil(timeout: 5.0) {
|
||||
app.buttons["Next"].exists || app.windows["Import Browser Data"].exists
|
||||
}
|
||||
XCTAssertTrue(wizardOpened, "Expected the import wizard to open")
|
||||
}
|
||||
|
||||
XCTAssertTrue(
|
||||
app.staticTexts["Import Browser Data"].waitForExistence(timeout: 5.0),
|
||||
"Expected the import wizard to open"
|
||||
)
|
||||
private func waitForBlankImportHint(_ app: XCUIApplication) {
|
||||
let hintOpened = browserImportPollUntil(timeout: 5.0) {
|
||||
app.buttons["BrowserImportHintImportButton"].exists
|
||||
}
|
||||
XCTAssertTrue(hintOpened, "Expected the blank browser import hint to appear")
|
||||
}
|
||||
|
||||
private func openImportWizardFromBlankImportHint(_ app: XCUIApplication) {
|
||||
waitForBlankImportHint(app)
|
||||
|
||||
let importButton = app.buttons["BrowserImportHintImportButton"]
|
||||
XCTAssertTrue(importButton.waitForExistence(timeout: 5.0))
|
||||
importButton.click()
|
||||
|
||||
waitForImportWizard(app)
|
||||
}
|
||||
|
||||
private func waitForCapturedSelection(timeout: TimeInterval) -> [String: Any]? {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
let url = URL(fileURLWithPath: capturePath)
|
||||
while Date() < deadline {
|
||||
let foundCapture = browserImportPollUntil(timeout: timeout) {
|
||||
if let data = try? Data(contentsOf: url),
|
||||
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
return object
|
||||
return !object.isEmpty
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
||||
return false
|
||||
}
|
||||
|
||||
guard let data = try? Data(contentsOf: url),
|
||||
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
return nil
|
||||
if foundCapture,
|
||||
let data = try? Data(contentsOf: url),
|
||||
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
return object
|
||||
}
|
||||
return object
|
||||
return nil
|
||||
}
|
||||
|
||||
private func ensureForegroundAfterLaunch(_ app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
||||
if app.wait(for: .runningForeground, timeout: timeout) {
|
||||
return true
|
||||
}
|
||||
if app.state == .runningBackground {
|
||||
private func launchAndActivate(_ app: XCUIApplication, activateTimeout: TimeInterval = 2.0) {
|
||||
app.launch()
|
||||
let activated = browserImportPollUntil(timeout: activateTimeout) {
|
||||
guard app.state != .runningForeground else {
|
||||
return true
|
||||
}
|
||||
app.activate()
|
||||
return app.state == .runningForeground
|
||||
}
|
||||
if !activated {
|
||||
app.activate()
|
||||
return app.wait(for: .runningForeground, timeout: 6.0)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue