mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-20 19:14:39 +08:00
[feat] support adaptive tablet layouts on Android and iPad
This commit is contained in:
@@ -108,6 +108,8 @@ struct AnnouncementInboxView: View {
|
||||
Text("共 \(inbox.state.total) 条 · \(inbox.state.unread) 条未读")
|
||||
}
|
||||
}
|
||||
.readableContentWidth()
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle("通知公告")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
@@ -174,10 +176,12 @@ struct AnnouncementDetailView: View {
|
||||
})
|
||||
}
|
||||
.padding(20)
|
||||
.readableContentWidth()
|
||||
} else {
|
||||
Text("这条公告已更新或删除,请返回公告列表查看。")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(20)
|
||||
.readableContentWidth()
|
||||
}
|
||||
}
|
||||
.navigationTitle("公告详情")
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
enum MobileLayout {
|
||||
static let twoColumnBreakpoint: CGFloat = 900
|
||||
static let homeMaximumWidth: CGFloat = 1200
|
||||
static let readableMaximumWidth: CGFloat = 720
|
||||
}
|
||||
|
||||
extension View {
|
||||
func readableContentWidth() -> some View {
|
||||
frame(maxWidth: MobileLayout.readableMaximumWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
enum HomeDestination: Hashable {
|
||||
case settings
|
||||
case announcements
|
||||
@@ -207,8 +220,11 @@ private struct ConnectionHomeView: View {
|
||||
return "arrow.triangle.2.circlepath"
|
||||
}
|
||||
|
||||
private var recentConnectionColumns: [GridItem] {
|
||||
[
|
||||
private func recentConnectionColumns(windowWidth: CGFloat) -> [GridItem] {
|
||||
if windowWidth >= 600 {
|
||||
return [GridItem(.adaptive(minimum: 180), spacing: 12, alignment: .top)]
|
||||
}
|
||||
return [
|
||||
GridItem(.flexible(), spacing: 12, alignment: .top),
|
||||
GridItem(.flexible(), spacing: 12, alignment: .top)
|
||||
]
|
||||
@@ -229,15 +245,33 @@ private struct ConnectionHomeView: View {
|
||||
ZStack {
|
||||
Color(.systemGroupedBackground).ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
remoteConnectionPanel
|
||||
GeometryReader { proxy in
|
||||
let isWide = proxy.size.width >= MobileLayout.twoColumnBreakpoint
|
||||
// AnyLayout keeps the fields, focus and scroll view alive when
|
||||
// rotation or iPad multitasking crosses the layout breakpoint.
|
||||
let layout = isWide
|
||||
? AnyLayout(HStackLayout(alignment: .top, spacing: 24))
|
||||
: AnyLayout(VStackLayout(spacing: 20))
|
||||
|
||||
ScrollView(.vertical) {
|
||||
layout {
|
||||
remoteConnectionPanel
|
||||
.frame(width: isWide ? 360 : nil)
|
||||
recentConnectionsPanel(windowWidth: proxy.size.width)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.frame(maxWidth: MobileLayout.homeMaximumWidth)
|
||||
// Short multitasking windows and the onscreen keyboard
|
||||
// must not squeeze the recent list below a usable height.
|
||||
.frame(height: max(isWide ? 260 : 420, proxy.size.height - 36),
|
||||
alignment: .top)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 24)
|
||||
recentConnectionsPanel
|
||||
.padding(.top, 20)
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding(.bottom, 12)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
if session.isConnecting {
|
||||
connectionProgressOverlay
|
||||
@@ -430,7 +464,7 @@ private struct ConnectionHomeView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var recentConnectionsPanel: some View {
|
||||
private func recentConnectionsPanel(windowWidth: CGFloat) -> some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text("最近连接")
|
||||
@@ -458,7 +492,7 @@ private struct ConnectionHomeView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView(.vertical) {
|
||||
LazyVGrid(columns: recentConnectionColumns, spacing: 12) {
|
||||
LazyVGrid(columns: recentConnectionColumns(windowWidth: windowWidth), spacing: 12) {
|
||||
ForEach(orderedRecentConnections) { connection in
|
||||
RecentConnectionCard(
|
||||
connection: connection,
|
||||
@@ -872,6 +906,8 @@ private struct ServerSettingsView: View {
|
||||
.accessibilityLabel(appUpdates.updateAvailable ? "关于,有新版本" : "关于")
|
||||
}
|
||||
}
|
||||
.readableContentWidth()
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle("设置")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
|
||||
@@ -2,7 +2,8 @@ import SwiftUI
|
||||
import UIKit
|
||||
|
||||
final class CrossDeskAppDelegate: NSObject, UIApplicationDelegate {
|
||||
static var supportedOrientations: UIInterfaceOrientationMask = .portrait
|
||||
static var supportedOrientations: UIInterfaceOrientationMask =
|
||||
UIDevice.current.userInterfaceIdiom == .pad ? .all : .portrait
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
supportedInterfaceOrientationsFor window: UIWindow?)
|
||||
@@ -14,6 +15,10 @@ final class CrossDeskAppDelegate: NSObject, UIApplicationDelegate {
|
||||
enum AppOrientation {
|
||||
static func update(to orientations: UIInterfaceOrientationMask) {
|
||||
DispatchQueue.main.async {
|
||||
// iPad windows can rotate and resize independently of the session.
|
||||
// Keep all orientations available and let the window manager choose
|
||||
// the geometry; the phone still locks the home screen to portrait.
|
||||
guard UIDevice.current.userInterfaceIdiom != .pad else { return }
|
||||
CrossDeskAppDelegate.supportedOrientations = orientations
|
||||
|
||||
for case let scene as UIWindowScene in UIApplication.shared.connectedScenes {
|
||||
|
||||
@@ -10,6 +10,7 @@ struct PrivacyConsentView: View {
|
||||
PrivacyPolicyDocument()
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 20)
|
||||
.readableContentWidth()
|
||||
}
|
||||
.background(Color(.systemBackground))
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
@@ -57,6 +58,7 @@ struct PrivacyConsentView: View {
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 12)
|
||||
.readableContentWidth()
|
||||
}
|
||||
.background(Color(.systemBackground))
|
||||
}
|
||||
@@ -161,6 +163,8 @@ private struct PrivacyAuthorizationView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.readableContentWidth()
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle("隐私与授权")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.alert("是否撤回隐私授权?", isPresented: $confirmWithdrawal) {
|
||||
@@ -220,7 +224,7 @@ private struct PrivacyPolicyDocument: View {
|
||||
struct PrivacyPolicyView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
PrivacyPolicyDocument().padding()
|
||||
PrivacyPolicyDocument().padding().readableContentWidth()
|
||||
}
|
||||
.navigationTitle("隐私政策")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
||||
@@ -138,6 +138,8 @@ struct ThirdPartyLicensesView: View {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.readableContentWidth()
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle("关于 CrossDesk")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onChange(of: session.hasNetworkConsent) { allowed in
|
||||
@@ -189,6 +191,8 @@ private struct OpenSourceComponentsView: View {
|
||||
Text("各组件遵循所列开源许可证,其授予的权利不受本应用普通使用条款限制。查看源码需要网络连接。")
|
||||
}
|
||||
}
|
||||
.readableContentWidth()
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle("开源组件")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
@@ -217,6 +221,8 @@ private struct SourceCodeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.readableContentWidth()
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle("源码与构建说明")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
@@ -240,6 +246,7 @@ private struct ComponentLicenseView: View {
|
||||
ForEach(component.documents) { document in
|
||||
NavigationLink {
|
||||
LicenseTextView(text: document.text)
|
||||
.readableContentWidth()
|
||||
.navigationTitle(documentTitle(document.title))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
@@ -248,6 +255,8 @@ private struct ComponentLicenseView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.readableContentWidth()
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle(component.name)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
@@ -148,6 +148,17 @@ No Apple signing secrets are needed. Sign the app yourself before installing
|
||||
it on a device. Push any referenced MiniRTC commit before the parent repository
|
||||
so the runner can check out all corresponding sources.
|
||||
|
||||
## iPad and adaptive layout
|
||||
|
||||
iPad supports portrait, upside-down portrait, landscape and resizable multitasking
|
||||
windows on both the home screen and an active remote session. The home screen
|
||||
uses two columns at a window width of 900 points or more, with the connection
|
||||
form beside recent connections; smaller windows use a single column. Content is
|
||||
centered at a maximum width of 1200 points, and recent-connection cards adapt to
|
||||
the available space. Settings and reading pages are limited to 720 points.
|
||||
Resizing preserves the connection fields and active session. iPhone retains its
|
||||
portrait home screen and two-column recent-connection grid.
|
||||
|
||||
## App updates
|
||||
|
||||
After network consent, the app checks the public version manifest once on each
|
||||
|
||||
Reference in New Issue
Block a user