Consolidate onboarding into single login page with native styling and full localization

Merge the two-page welcome/login flow into a single page with the app icon,
title, subtitle, and login inputs all on one screen. Replace the custom blue
background and white-on-blue styling with native iOS system colors and
button styles. Add missing translations (de, es, fr) for all onboarding
strings and fix localization by using LocalizedStringKey and String(localized:).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 10:44:51 +01:00
parent ce2a814e5a
commit c8d9ab7397
4 changed files with 4178 additions and 3886 deletions

View File

@@ -10,41 +10,59 @@ import OSLog
import SwiftUI
struct OnboardingView: View {
@State var selectedTab: Int = 0
var body: some View {
TabView(selection: $selectedTab) {
WelcomeTab().tag(0)
LoginTab().tag(1)
}
.tabViewStyle(.page)
.background(
selectedTab == 1 ? Color.nextcloudBlue.ignoresSafeArea() : Color(uiColor: .systemBackground).ignoresSafeArea()
)
.animation(.easeInOut, value: selectedTab)
}
}
@State var loginMethod: LoginMethod = .v2
// Login error alert
@State var showAlert: Bool = false
@State var alertMessage: String = String(localized: "Error: Could not connect to server.")
struct WelcomeTab: View {
var body: some View {
VStack(alignment: .center) {
Spacer()
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
Image("cookbook-icon")
.resizable()
.frame(width: 120, height: 120)
.clipShape(RoundedRectangle(cornerRadius: 10))
Text("Thank you for downloading")
.font(.headline)
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 18))
.padding(.top, 48)
Text("Cookbook Client")
.font(.largeTitle)
.bold()
Spacer()
Text("This application is an open source effort. If you're interested in suggesting or contributing new features, or you encounter any problems, please use the support link or visit the GitHub repository in the app settings.")
.padding()
Spacer()
.padding(.top, 10)
Text("Thanks for downloading! Sign in to your Nextcloud server to get started.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
.padding(.top, 6)
VStack(alignment: .leading, spacing: 16) {
Picker("Login Method", selection: $loginMethod) {
Text("Nextcloud Login").tag(LoginMethod.v2)
Text("App Token Login").tag(LoginMethod.token)
}
.pickerStyle(.segmented)
if loginMethod == .token {
TokenLoginView(
showAlert: $showAlert,
alertMessage: $alertMessage
)
} else if loginMethod == .v2 {
V2LoginView(
showAlert: $showAlert,
alertMessage: $alertMessage
)
}
}
.padding(.top, 28)
}
.padding()
.fontDesign(.rounded)
.padding()
.alert(alertMessage, isPresented: $showAlert) {
Button("Ok", role: .cancel) { }
}
}
.background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea())
}
}
@@ -59,7 +77,7 @@ enum LoginMethod {
enum TokenLoginStage: LoginStage {
case serverAddress, userName, appToken, validate
func next() -> TokenLoginStage {
switch self {
case .serverAddress: return .userName
@@ -68,7 +86,7 @@ enum TokenLoginStage: LoginStage {
case .validate: return .validate
}
}
func previous() -> TokenLoginStage {
switch self {
case .serverAddress: return .serverAddress
@@ -79,160 +97,88 @@ enum TokenLoginStage: LoginStage {
}
}
struct LoginTab: View {
@State var loginMethod: LoginMethod = .v2
// Login error alert
@State var showAlert: Bool = false
@State var alertMessage: String = "Error: Could not connect to server."
var body: some View {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading) {
Spacer()
Picker("Login Method", selection: $loginMethod) {
Text("Nextcloud Login").tag(LoginMethod.v2)
Text("App Token Login").tag(LoginMethod.token)
}
.pickerStyle(.segmented)
.foregroundColor(.white)
.padding()
if loginMethod == .token {
TokenLoginView(
showAlert: $showAlert,
alertMessage: $alertMessage
)
}
else if loginMethod == .v2 {
V2LoginView(
showAlert: $showAlert,
alertMessage: $alertMessage
)
}
Spacer()
}
.fontDesign(.rounded)
.padding()
.alert(alertMessage, isPresented: $showAlert) {
Button("Ok", role: .cancel) { }
}
}
}
}
struct LoginLabel: View {
let text: String
let text: LocalizedStringKey
var body: some View {
Text(text)
.foregroundColor(.white)
.font(.headline)
.padding(.vertical, 5)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
struct BorderedLoginTextField: View {
var example: String
var example: LocalizedStringKey
@Binding var text: String
@State var color: Color = .white
var body: some View {
TextField(example, text: $text)
.textFieldStyle(.plain)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.foregroundColor(color)
.tint(color)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(.white, lineWidth: 2)
.foregroundColor(.clear)
)
.background(Color(uiColor: .secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
struct LoginTextField: View {
var example: String
var example: LocalizedStringKey
@Binding var text: String
@State var color: Color = .white
var body: some View {
TextField(example, text: $text)
.textFieldStyle(.plain)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.foregroundColor(color)
.tint(color)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.white.opacity(0.2))
)
.background(Color(uiColor: .secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
struct ServerAddressField: View {
@ObservedObject var userSettings = UserSettings.shared
@State var serverProtocol: ServerProtocol = UserSettings.shared.serverProtocol == ServerProtocol.http.rawValue ? ServerProtocol.http : ServerProtocol.https
enum ServerProtocol: String {
case https="https://", http="http://"
static let all = [https, http]
}
var body: some View {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 6) {
LoginLabel(text: "Server address")
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 10) {
HStack {
Picker(ServerProtocol.https.rawValue, selection: $serverProtocol) {
ForEach(ServerProtocol.all, id: \.self) {
Text($0.rawValue)
}
}.pickerStyle(.menu)
.tint(.white)
.font(.headline)
}
.pickerStyle(.menu)
.tint(.accentColor)
.onChange(of: serverProtocol) { value in
Logger.view.debug("\(value.rawValue)")
userSettings.serverProtocol = value.rawValue
}
TextField("e.g.: example.com", text: $userSettings.serverAddress)
.textFieldStyle(.plain)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.foregroundStyle(.white)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.white.opacity(0.2))
)
.padding(10)
.background(Color(uiColor: .secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
LoginLabel(text: "Full server address")
.padding(.top)
Text(userSettings.serverProtocol + userSettings.serverAddress)
.foregroundColor(.white)
.padding(.vertical, 5)
.font(.footnote)
.foregroundStyle(.secondary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(.white, lineWidth: 2)
.foregroundColor(.clear)
)
.background(Color(uiColor: .secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
@@ -242,6 +188,5 @@ struct ServerAddressField_Preview: PreviewProvider {
ServerAddressField()
.previewLayout(.sizeThatFits)
.padding()
.background(Color.nextcloudBlue)
}
}