Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 228 additions & 121 deletions src/apps/mobile/ios/OpenBitFun/Features/Account/AccountSettingsView.swift

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@ struct ConversationHeader: View {
@State private var editing = false
@State private var renameDraft = ""

private var resolvedTitle: String {
if let title = model.selectedSession?.title, !title.isEmpty { return title }
if model.surface == .remote { return model.localized("远程") }
return "OpenBitFun"
}

private var resolvedSubtitle: String? {
if let contextTitle, !contextTitle.isEmpty { return contextTitle }
if model.surface == .local && model.localSessionSelected { return model.localized("本地会话") }
if model.remoteConnected && model.remoteSessionSelected {
return model.accountDeviceName ?? model.localized("已连接桌面端")
if model.remoteConnected {
return model.accountDeviceName
?? model.directPairingDeviceName
?? model.localized("已连接桌面端")
}
return nil
}
Expand Down Expand Up @@ -49,7 +57,7 @@ struct ConversationHeader: View {
}

VStack(spacing: 3) {
Text(model.selectedSession?.title ?? "OpenBitFun")
Text(resolvedTitle)
.font(
(resolvedSubtitle == nil
? MobileDesignTypography.titleMedium
Expand Down Expand Up @@ -196,7 +204,7 @@ struct ConversationActionsPopover: View {
.padding(.leading, 8)
if model.surface == .local {
action(
model.selectedSession?.pinned == true ? "取消置顶" : "置顶会话",
model.selectedSession?.pinned == true ? "取消置顶" : "置顶",
icon: "checkmark.circle",
selected: model.selectedSession?.pinned == true,
perform: model.togglePinSelectedSession
Expand All @@ -205,7 +213,7 @@ struct ConversationActionsPopover: View {
action("已上传文件", icon: "cloud", perform: model.showUploadedFiles)
if model.surface == .local {
Divider().overlay(OpenBitFunTheme.line).padding(.vertical, 8)
action("归档会话", icon: "folder", perform: model.archiveSelectedSession)
action("归档", icon: "folder", perform: model.archiveSelectedSession)
action("删除", icon: "gearshape", perform: model.deleteSelectedSession)
} else if model.isSending {
Divider().overlay(OpenBitFunTheme.line).padding(.vertical, 8)
Expand Down
218 changes: 215 additions & 3 deletions src/apps/mobile/ios/OpenBitFun/Features/Pairing/PairingSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import OpenBitFunMobileCore
import SwiftUI

struct PairingSheet: View {
private enum Step { case intro, scan }
private enum Step { case intro, account, scan }

@ObservedObject var model: MobileAppModel
@Environment(\.dismiss) private var dismiss
Expand All @@ -15,11 +15,16 @@ struct PairingSheet: View {
@State private var pairingPassword = ""
@State private var manualOpen = false
@State private var scanError: String?
@State private var switchingDeviceID: String?
@FocusState private var focused: Bool

var body: some View {
return ZStack {
if step == .intro { introPage } else { scanPage }
switch step {
case .intro: introPage
case .account: accountDevicePage
case .scan: scanPage
}
if manualOpen { manualPairingOverlay }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Expand All @@ -33,8 +38,211 @@ struct PairingSheet: View {
step = .scan
manualOpen = true
focused = !MobileLaunchConfiguration.pairingAccountPreview
} else if model.accountUser != nil {
step = .account
model.refreshRemoteDevices()
}
}
.onChange(of: model.accountSelectedDeviceID) { selectedDeviceID in
guard step == .account, selectedDeviceID == switchingDeviceID else { return }
switchingDeviceID = nil
dismiss()
}
.onChange(of: model.coreErrorMessage) { error in
if step == .account, error != nil { switchingDeviceID = nil }
}
}

private var accountDevicePage: some View {
VStack(spacing: 0) {
HStack(spacing: 16) {
Button { dismiss() } label: {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .medium))
.foregroundStyle(OpenBitFunTheme.ink)
.frame(width: 48, height: 48)
.background(OpenBitFunTheme.card)
.clipShape(Circle())
}
.buttonStyle(.plain)
.accessibilityLabel(model.localized("返回"))

VStack(alignment: .leading, spacing: 4) {
Text(model.localized("选择桌面设备"))
.font(MobileDesignTypography.headlineLarge.font)
.foregroundStyle(OpenBitFunTheme.ink)
Text(model.localized("远程"))
.font(MobileDesignTypography.bodySmall.font)
.foregroundStyle(OpenBitFunTheme.muted)
}
Spacer(minLength: 0)
}
.padding(.horizontal, 28)
.padding(.top, 18)
.frame(height: 92, alignment: .top)

ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 18) {
Text(model.localized("选择一台在线桌面继续工作。"))
.font(MobileDesignTypography.bodyMedium.font)
.foregroundStyle(OpenBitFunTheme.muted)
.lineSpacing(MobileDesignTypography.bodyMedium.lineSpacing)

accountDeviceList

Button {
scanError = nil
step = .scan
} label: {
HStack(spacing: 12) {
Image(systemName: "link")
.font(.system(size: 20, weight: .regular))
.foregroundStyle(OpenBitFunTheme.muted.opacity(0.66))
.frame(width: 22, height: 22)
Text(model.localized("扫描二维码连接"))
.font(MobileDesignTypography.bodyLarge.font.weight(.medium))
.foregroundStyle(OpenBitFunTheme.ink)
Spacer(minLength: 0)
Image(systemName: "chevron.right")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(OpenBitFunTheme.muted.opacity(0.44))
}
.padding(.horizontal, 16)
.frame(height: 58)
.background(OpenBitFunTheme.card)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(OpenBitFunTheme.line, lineWidth: 1))
}
.buttonStyle(.plain)
}
.frame(maxWidth: 520)
.padding(.horizontal, 28)
.padding(.top, 10)
.padding(.bottom, 34)
.frame(maxWidth: .infinity)
}
}
.background(OpenBitFunTheme.page)
}

private var accountDeviceList: some View {
VStack(spacing: 4) {
HStack {
Text(model.localized("账号设备"))
.font(MobileDesignTypography.bodyLarge.font.weight(.bold))
.foregroundStyle(OpenBitFunTheme.ink)
Spacer(minLength: 0)
Button(action: model.refreshRemoteDevices) {
Text(model.localized(model.accountRefreshing ? "正在加载" : "刷新"))
.font(MobileDesignTypography.bodyMedium.font)
.foregroundStyle(model.accountRefreshing ? OpenBitFunTheme.muted : OpenBitFunTheme.accent)
.frame(minWidth: 44, minHeight: 38, alignment: .trailing)
}
.buttonStyle(.plain)
.disabled(model.accountRefreshing)
}
.frame(height: 38)

Group {
if model.accountRefreshing && accountDesktopDevices.isEmpty {
VStack(spacing: 0) {
accountDeviceSkeleton
accountDeviceSkeleton
}
} else if accountDesktopDevices.isEmpty {
Text(model.localized("暂无可连接的桌面设备"))
.font(MobileDesignTypography.bodyMedium.font)
.foregroundStyle(OpenBitFunTheme.muted)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
} else {
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
ForEach(accountDesktopDevices) { device in
accountDeviceRow(device)
}
}
}
}
}
.frame(height: 120)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.frame(height: 174)
.background(OpenBitFunTheme.card)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(OpenBitFunTheme.line, lineWidth: 1))
}

private var accountDesktopDevices: [MobileAccountDevice] {
model.accountDevices.filter { model.localDeviceID.isEmpty || $0.id != model.localDeviceID }
}

private var accountDeviceSkeleton: some View {
HStack(spacing: 12) {
RoundedRectangle(cornerRadius: 5).fill(OpenBitFunTheme.soft).frame(width: 26, height: 22)
VStack(alignment: .leading, spacing: 7) {
RoundedRectangle(cornerRadius: 4).fill(OpenBitFunTheme.soft).frame(width: 142, height: 12)
RoundedRectangle(cornerRadius: 4).fill(OpenBitFunTheme.soft).frame(width: 52, height: 9)
}
Spacer(minLength: 0)
}
.padding(.horizontal, 4)
.frame(height: 60)
}

private func accountDeviceRow(_ device: MobileAccountDevice) -> some View {
Button {
guard device.online, switchingDeviceID == nil else { return }
switchingDeviceID = device.id
model.selectRemoteDevice(device)
} label: {
HStack(spacing: 12) {
Image(systemName: "desktopcomputer")
.font(.system(size: 22, weight: .regular))
.foregroundStyle(OpenBitFunTheme.muted.opacity(device.online ? 0.68 : 0.38))
.frame(width: 26, height: 24)
VStack(alignment: .leading, spacing: 3) {
Text(device.name.isEmpty ? device.id : device.name)
.font(MobileDesignTypography.titleSmall.font)
.foregroundStyle(OpenBitFunTheme.ink)
.lineLimit(1)
Text(accountDeviceStatus(device))
.font(MobileDesignTypography.bodySmall.font)
.foregroundStyle(device.online ? OpenBitFunTheme.statusSuccess : OpenBitFunTheme.muted)
}
Spacer(minLength: 0)
if device.online && !(device.selected && model.connectionPhase == .connected) {
Text(model.localized(switchingDeviceID == device.id ? "正在连接" : "连接"))
.font(MobileDesignTypography.bodyMedium.font)
.foregroundStyle(OpenBitFunTheme.ink)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(OpenBitFunTheme.soft)
.clipShape(Capsule())
} else if device.online {
Image(systemName: "chevron.right")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(OpenBitFunTheme.muted.opacity(0.44))
}
}
.padding(.horizontal, 4)
.frame(height: 60)
.contentShape(Rectangle())
.opacity(device.online ? 1 : 0.64)
}
.buttonStyle(.plain)
.disabled(!device.online || switchingDeviceID != nil)
}

private func accountDeviceStatus(_ device: MobileAccountDevice) -> String {
if switchingDeviceID == device.id { return model.localized("正在连接") }
let presence = model.localized(device.online ? "在线" : "离线")
if device.selected && model.connectionPhase == .connected {
return "\(model.localized("当前控制")) · \(presence)"
}
if device.selected { return "\(model.localized("上次连接")) · \(presence)" }
return presence
}

private var introPage: some View {
Expand Down Expand Up @@ -204,7 +412,11 @@ struct PairingSheet: View {
endPoint: .bottomTrailing
)
Button {
if step == .scan { step = .intro } else { dismiss() }
if step == .scan {
step = model.accountUser == nil ? .intro : .account
} else {
dismiss()
}
} label: {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .medium))
Expand Down
Loading