Tighten browser import sheet UI
This commit is contained in:
parent
aac8a41ba2
commit
ffcd3fdfaa
4 changed files with 202 additions and 89 deletions
|
|
@ -4740,13 +4740,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": "ブックマーク、設定、拡張機能はまだ利用できません。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5029,13 +5029,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmux destination"
|
||||
"value": "Destination"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmux の保存先"
|
||||
"value": "保存先"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5080,13 +5080,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 +5097,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 +5114,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 +5131,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 +5148,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Keep profiles separate"
|
||||
"value": "Separate profiles"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "プロファイルを分けたまま取り込む"
|
||||
"value": "分けて取り込む"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5233,13 +5233,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Limit to"
|
||||
"value": "Domains"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "対象ドメイン"
|
||||
"value": "ドメイン"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5250,13 +5250,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 +5505,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Source"
|
||||
"value": "Browser"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "インポート元"
|
||||
"value": "ブラウザー"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5539,13 +5539,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Source Profiles"
|
||||
"value": "Profiles"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "元プロファイル"
|
||||
"value": "プロファイル"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5556,13 +5556,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 +5607,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 +5624,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 +5641,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6837,6 +6837,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(
|
||||
|
|
@ -8378,6 +8390,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()
|
||||
|
|
@ -8387,6 +8400,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)
|
||||
|
|
@ -8412,7 +8426,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
|
||||
|
|
@ -8538,6 +8552,7 @@ final class BrowserDataImportCoordinator {
|
|||
guard selectedSourceProfiles.count > 1 else { return }
|
||||
destinationMode = sender == separateProfilesRadio ? .separateProfiles : .mergeIntoOne
|
||||
rebuildStep3DestinationUI()
|
||||
updatePanelSize()
|
||||
}
|
||||
|
||||
@objc
|
||||
|
|
@ -8560,6 +8575,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",
|
||||
|
|
@ -8570,7 +8592,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
|
||||
|
||||
|
|
@ -8580,9 +8602,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()
|
||||
|
|
@ -8594,6 +8616,7 @@ final class BrowserDataImportCoordinator {
|
|||
validationLabel.isHidden = true
|
||||
validationLabel.lineBreakMode = .byWordWrapping
|
||||
validationLabel.maximumNumberOfLines = 3
|
||||
validationLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
backButton.target = self
|
||||
backButton.action = #selector(handleBack)
|
||||
|
|
@ -8631,23 +8654,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),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -8663,23 +8695,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)
|
||||
|
|
@ -8692,17 +8728,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
|
||||
|
|
@ -8721,19 +8757,22 @@ 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
|
||||
sourceProfilesScrollView.widthAnchor.constraint(equalTo: sourceProfilesContainer.widthAnchor).isActive = true
|
||||
|
||||
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)
|
||||
|
|
@ -8758,6 +8797,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")
|
||||
|
|
@ -8782,25 +8827,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(
|
||||
|
|
@ -8808,32 +8857,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)
|
||||
|
|
@ -8843,13 +8892,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()
|
||||
}
|
||||
|
||||
|
|
@ -8858,7 +8908,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
|
||||
|
|
@ -8868,11 +8918,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
|
||||
|
|
@ -8883,11 +8930,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
|
||||
|
|
@ -8899,6 +8943,7 @@ final class BrowserDataImportCoordinator {
|
|||
defaultValue: "Start Import"
|
||||
)
|
||||
}
|
||||
updatePanelSize()
|
||||
}
|
||||
|
||||
private func selectedBrowser() -> InstalledBrowserCandidate {
|
||||
|
|
@ -8925,6 +8970,7 @@ final class BrowserDataImportCoordinator {
|
|||
browser.displayName
|
||||
)
|
||||
sourceProfilesList.addArrangedSubview(sourceProfilesEmptyLabel)
|
||||
updateSourceProfilesPresentation(for: browser)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -8940,6 +8986,8 @@ final class BrowserDataImportCoordinator {
|
|||
sourceProfilesList.addArrangedSubview(checkbox)
|
||||
sourceProfileCheckboxes.append(checkbox)
|
||||
}
|
||||
|
||||
updateSourceProfilesPresentation(for: browser)
|
||||
}
|
||||
|
||||
private func storedSelectedSourceProfileIDs(for browser: InstalledBrowserCandidate) -> Set<String> {
|
||||
|
|
@ -9055,16 +9103,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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -9081,7 +9129,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
|
||||
|
|
@ -9101,11 +9149,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)
|
||||
}
|
||||
}
|
||||
|
|
@ -9137,7 +9188,7 @@ final class BrowserDataImportCoordinator {
|
|||
)
|
||||
)
|
||||
destinationLabel.alignment = .right
|
||||
destinationLabel.frame.size.width = 140
|
||||
destinationLabel.frame.size.width = 110
|
||||
|
||||
mergeDestinationRow.addArrangedSubview(destinationLabel)
|
||||
mergeDestinationRow.addArrangedSubview(mergeDestinationPopup)
|
||||
|
|
@ -9211,6 +9262,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
|
||||
|
|
|
|||
|
|
@ -127,6 +127,23 @@ 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)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testRealizePlanCreatesMissingDestinationProfilesOnlyWhenRequested() throws {
|
||||
let suiteName = "BrowserImportMappingTests-\(UUID().uuidString)"
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
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)
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ final class BrowserImportProfilesUITests: XCTestCase {
|
|||
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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue