Compare commits

..

4 Commits

Author SHA1 Message Date
413e276abe translate 2025-10-05 17:56:31 +03:00
47746690d6 translate burger menu 2025-10-05 06:28:29 +03:00
020cd8893a settings to burger menu 2025-10-05 06:08:05 +03:00
102826ac8a burger menu 2025-10-05 06:00:54 +03:00
13 changed files with 430 additions and 180 deletions

View File

@ -187,7 +187,7 @@
}; };
}; };
buildConfigurationList = 1A6D61C72E7CD03E00B9F736 /* Build configuration list for PBXProject "yobble" */; buildConfigurationList = 1A6D61C72E7CD03E00B9F736 /* Build configuration list for PBXProject "yobble" */;
developmentRegion = en; developmentRegion = ru;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
en, en,

View File

@ -7,7 +7,8 @@ struct TopBarView: View {
@Binding var selectedAccount: String @Binding var selectedAccount: String
// @Binding var sheetType: ProfileTab.SheetType? // @Binding var sheetType: ProfileTab.SheetType?
var accounts: [String] var accounts: [String]
var viewModel: LoginViewModel // var viewModel: LoginViewModel
@ObservedObject var viewModel: LoginViewModel
// Привязка для управления боковым меню // Привязка для управления боковым меню
@Binding var isSideMenuPresented: Bool @Binding var isSideMenuPresented: Bool
@ -45,7 +46,7 @@ struct TopBarView: View {
Spacer() Spacer()
Button(action: { }) { Button(action: { }) {
HStack(spacing: 4) { HStack(spacing: 4) {
Text(selectedAccount) Text("@\(viewModel.username)")
.font(.headline) .font(.headline)
.foregroundColor(.primary) .foregroundColor(.primary)
Image(systemName: "chevron.down") Image(systemName: "chevron.down")

View File

@ -104,7 +104,6 @@ class AuthService {
// Сохраняем токены в Keychain // Сохраняем токены в Keychain
KeychainService.shared.save(loginResponse.access_token, forKey: "access_token", service: username) KeychainService.shared.save(loginResponse.access_token, forKey: "access_token", service: username)
KeychainService.shared.save(loginResponse.refresh_token, forKey: "refresh_token", service: username) KeychainService.shared.save(loginResponse.refresh_token, forKey: "refresh_token", service: username)
print("loginResponse.user_id \(loginResponse.user_id)")
KeychainService.shared.save(loginResponse.user_id, forKey: "userId", service: username) KeychainService.shared.save(loginResponse.user_id, forKey: "userId", service: username)
UserDefaults.standard.set(username, forKey: "currentUser") UserDefaults.standard.set(username, forKey: "currentUser")

View File

@ -1,45 +1,56 @@
{ {
"sourceLanguage" : "en", "sourceLanguage" : "ru",
"strings" : { "strings" : {
"@yourusername" : { "@%@" : {
},
"🌍" : {
},
"CATEGORY" : {
},
"Hello, world!" : {
},
"loading_placeholder" : {
},
"LoginView_button_login" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "ебите меня четверо" "value" : "@%@"
} }
} }
} }
}, },
"ok" : { "🌍" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"ru" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "55454545" "value" : "🌍"
}
}
}
},
"Fun Fest" : {
"comment" : "Fun Fest",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fun Fest"
}
}
}
},
"Hello, world!" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hello, world!"
} }
} }
} }
}, },
"OK" : { "OK" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "OK"
}
}
}
}, },
"profile_down_text_1" : { "profile_down_text_1" : {
@ -52,15 +63,16 @@
}, },
"Push-уведомления" : { "Push-уведомления" : {
},
"SERVICES" : {
}, },
"Yobble" : { "Yobble" : {
"localizations" : {
}, "en" : {
"Your Name" : { "stringUnit" : {
"state" : "translated",
"value" : "Yobble"
}
}
}
}, },
"Активные сессии" : { "Активные сессии" : {
@ -69,16 +81,37 @@
}, },
"Войти" : { "Войти" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Log in"
}
}
}
}, },
"Выйти из аккаунта" : { "Выйти из аккаунта" : {
},
"Где найти сохранённые черновики?" : {
"comment" : "FAQ question: drafts"
}, },
"Данные" : { "Данные" : {
}, },
"Двухфакторная аутентификация" : { "Двухфакторная аутентификация" : {
},
"Добавить друзей" : {
"comment" : "Add friends",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add friends"
}
}
}
}, },
"Другое" : { "Другое" : {
@ -104,14 +137,48 @@
"Заглушка: Хранилище данных" : { "Заглушка: Хранилище данных" : {
}, },
"Заглушка: Частые вопросы" : { "Загрузка..." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Loading..."
}
}
}
},
"Заказы" : {
"comment" : "Orders",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Orders"
}
}
}
}, },
"Закрыть" : { "Закрыть" : {
"comment" : "Закрыть" "comment" : "Закрыть",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Close"
}
}
}
}, },
"Зарегистрироваться" : { "Зарегистрироваться" : {
"comment" : "Зарегистрироваться" "comment" : "Зарегистрироваться",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Register"
}
}
}
}, },
"Здесь будут чаты" : { "Здесь будут чаты" : {
@ -120,22 +187,135 @@
}, },
"Инвайт-код (необязательно)" : { "Инвайт-код (необязательно)" : {
"comment" : "Инвайт-код" "comment" : "Инвайт-код",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Invite code (optional)"
}
}
}
},
"История" : {
"comment" : "History",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "History"
}
}
}
},
"Как сбросить пароль?" : {
"comment" : "FAQ question: reset password"
},
"Как связаться с поддержкой?" : {
"comment" : "FAQ question: support"
},
"Корзина" : {
"comment" : "Cart",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cart"
}
}
}
},
"Кошелёк" : {
"comment" : "Wallet",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wallet"
}
}
}
},
"Лента" : {
},
"Лицо" : {
}, },
"Логин" : { "Логин" : {
"comment" : "Логин" "comment" : "Логин",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Login"
}
}
}
}, },
"Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)" : { "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)" : {
"comment" : "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)" "comment" : "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Username must be 3 to 32 characters (letters, digits, or _)"
}
}
}
}, },
"Логин уже занят." : { "Логин уже занят." : {
},
"Мини-приложения" : {
"comment" : "Applets",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Applets"
}
}
}
},
"Мои загрузки" : {
"comment" : "My Downloads",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "My Downloads"
}
}
}
},
"Мои комментарии" : {
"comment" : "My Comments",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "My Comments"
}
}
}
}, },
"Мой профиль" : { "Мой профиль" : {
},
"Напишите нам через форму обратной связи в разделе \"Поддержка\"." : {
"comment" : "FAQ answer: support"
}, },
"Настройки" : { "Настройки" : {
"comment" : "Settings",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Settings"
}
}
}
}, },
"Не удалось обработать ответ сервера." : { "Не удалось обработать ответ сервера." : {
@ -206,12 +386,29 @@
"Пароль должен быть от 8 до 128 символов" : { "Пароль должен быть от 8 до 128 символов" : {
"comment" : "Пароль должен быть от 6 до 32 символов" "comment" : "Пароль должен быть от 6 до 32 символов"
}, },
"Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям." : {
"comment" : "FAQ answer: reset password"
},
"Поддержка" : { "Поддержка" : {
}, },
"Подтверждение пароля" : { "Подтверждение пароля" : {
"comment" : "Подтверждение пароля" "comment" : "Подтверждение пароля"
}, },
"Поиск" : {
},
"Помощь" : {
"comment" : "Help Center",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Help Center"
}
}
}
},
"Приглашение достигло лимита использования." : { "Приглашение достигло лимита использования." : {
}, },
@ -256,6 +453,17 @@
}, },
"Сервер не отвечает. Попробуйте позже." : { "Сервер не отвечает. Попробуйте позже." : {
},
"Скан" : {
"comment" : "Scan",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Scan"
}
}
}
}, },
"Слишком много запросов." : { "Слишком много запросов." : {
@ -268,9 +476,37 @@
}, },
"Уведомления" : { "Уведомления" : {
},
"Центр авторов" : {
"comment" : "Creator Center",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Creator Center"
}
}
}
}, },
"Частые вопросы" : { "Частые вопросы" : {
"comment" : "FAQ navigation title"
},
"Чаты" : {
},
"Черновики" : {
"comment" : "Drafts",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Drafts"
}
}
}
},
"Черновики доступны в боковом меню в разделе Drafts." : {
"comment" : "FAQ answer: drafts"
}, },
"Язык" : { "Язык" : {

View File

@ -25,7 +25,7 @@ class LoginViewModel: ObservableObject {
} }
init() { init() {
loadStoredUser() // loadStoredUser()
// Запускаем автологин // Запускаем автологин
autoLogin() autoLogin()
@ -113,6 +113,6 @@ class LoginViewModel: ObservableObject {
username = defaults.string(forKey: DefaultsKeys.currentUser) ?? "" username = defaults.string(forKey: DefaultsKeys.currentUser) ?? ""
userId = KeychainService.shared.get(forKey: DefaultsKeys.userId, service: username) ?? "" userId = KeychainService.shared.get(forKey: DefaultsKeys.userId, service: username) ?? ""
print("username: \(username) | userId: \(userId)") if AppConfig.DEBUG{ print("username: \(username) | userId: \(userId)")}
} }
} }

View File

@ -13,6 +13,12 @@ struct LoginView: View {
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@State private var isShowingRegistration = false @State private var isShowingRegistration = false
@FocusState private var focusedField: Field?
private enum Field: Hashable {
case username
case password
}
private var isUsernameValid: Bool { private var isUsernameValid: Bool {
let pattern = "^[A-Za-z0-9_]{3,32}$" let pattern = "^[A-Za-z0-9_]{3,32}$"
@ -29,7 +35,7 @@ struct LoginView: View {
Color.clear // чтобы поймать тап Color.clear // чтобы поймать тап
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
hideKeyboard() focusedField = nil
} }
VStack { VStack {
@ -46,7 +52,7 @@ struct LoginView: View {
} }
} }
.onTapGesture { .onTapGesture {
hideKeyboard() focusedField = nil
} }
Spacer() Spacer()
@ -57,6 +63,7 @@ struct LoginView: View {
.cornerRadius(8) .cornerRadius(8)
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.focused($focusedField, equals: .username)
.onChange(of: viewModel.username) { newValue in .onChange(of: viewModel.username) { newValue in
if newValue.count > 32 { if newValue.count > 32 {
viewModel.username = String(newValue.prefix(32)) viewModel.username = String(newValue.prefix(32))
@ -76,6 +83,7 @@ struct LoginView: View {
.background(Color(.secondarySystemBackground)) .background(Color(.secondarySystemBackground))
.cornerRadius(8) .cornerRadius(8)
.autocapitalization(.none) .autocapitalization(.none)
.focused($focusedField, equals: .password)
.onChange(of: viewModel.password) { newValue in .onChange(of: viewModel.password) { newValue in
if newValue.count > 32 { if newValue.count > 32 {
viewModel.password = String(newValue.prefix(32)) viewModel.password = String(newValue.prefix(32))
@ -140,7 +148,7 @@ struct LoginView: View {
) )
} }
.onTapGesture { .onTapGesture {
hideKeyboard() focusedField = nil
} }
} }
} }
@ -169,10 +177,6 @@ struct LoginView: View {
UIApplication.shared.open(url) UIApplication.shared.open(url)
} }
private func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
} }
struct LoginView_Previews: PreviewProvider { struct LoginView_Previews: PreviewProvider {

View File

@ -22,6 +22,15 @@ struct RegistrationView: View {
@State private var showError: Bool = false @State private var showError: Bool = false
@State private var errorMessage: String = "" @State private var errorMessage: String = ""
@FocusState private var focusedField: Field?
private enum Field: Hashable {
case username
case password
case confirmPassword
case invite
}
private var isUsernameValid: Bool { private var isUsernameValid: Bool {
let pattern = "^[A-Za-z0-9_]{3,32}$" let pattern = "^[A-Za-z0-9_]{3,32}$"
return username.range(of: pattern, options: .regularExpression) != nil return username.range(of: pattern, options: .regularExpression) != nil
@ -45,7 +54,7 @@ struct RegistrationView: View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
Color.clear Color.clear
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { hideKeyboard() } .onTapGesture { focusedField = nil }
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
Group { Group {
@ -53,6 +62,7 @@ struct RegistrationView: View {
TextField(NSLocalizedString("Логин", comment: "Логин"), text: $username) TextField(NSLocalizedString("Логин", comment: "Логин"), text: $username)
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.focused($focusedField, equals: .username)
Spacer() Spacer()
if !username.isEmpty { if !username.isEmpty {
Image(systemName: isUsernameValid ? "checkmark.circle" : "xmark.circle") Image(systemName: isUsernameValid ? "checkmark.circle" : "xmark.circle")
@ -79,6 +89,7 @@ struct RegistrationView: View {
HStack { HStack {
SecureField(NSLocalizedString("Пароль", comment: "Пароль"), text: $password) SecureField(NSLocalizedString("Пароль", comment: "Пароль"), text: $password)
.autocapitalization(.none) .autocapitalization(.none)
.focused($focusedField, equals: .password)
Spacer() Spacer()
if !password.isEmpty { if !password.isEmpty {
Image(systemName: isPasswordValid ? "checkmark.circle" : "xmark.circle") Image(systemName: isPasswordValid ? "checkmark.circle" : "xmark.circle")
@ -104,6 +115,7 @@ struct RegistrationView: View {
HStack { HStack {
SecureField(NSLocalizedString("Подтверждение пароля", comment: "Подтверждение пароля"), text: $confirmPassword) SecureField(NSLocalizedString("Подтверждение пароля", comment: "Подтверждение пароля"), text: $confirmPassword)
.autocapitalization(.none) .autocapitalization(.none)
.focused($focusedField, equals: .confirmPassword)
Spacer() Spacer()
if !confirmPassword.isEmpty { if !confirmPassword.isEmpty {
Image(systemName: isConfirmPasswordValid ? "checkmark.circle" : "xmark.circle") Image(systemName: isConfirmPasswordValid ? "checkmark.circle" : "xmark.circle")
@ -132,6 +144,7 @@ struct RegistrationView: View {
.cornerRadius(8) .cornerRadius(8)
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.focused($focusedField, equals: .invite)
} }
Button(action: registerUser) { Button(action: registerUser) {
@ -189,14 +202,10 @@ struct RegistrationView: View {
} }
private func dismissSheet() { private func dismissSheet() {
hideKeyboard() focusedField = nil
isPresented = false isPresented = false
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
} }
private func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
} }

View File

@ -6,7 +6,7 @@ struct SplashScreenView: View {
ProgressView() ProgressView()
.progressViewStyle(CircularProgressViewStyle()) .progressViewStyle(CircularProgressViewStyle())
.scaleEffect(1.5) .scaleEffect(1.5)
Text(NSLocalizedString("loading_placeholder", comment: "")) Text(NSLocalizedString("Загрузка...", comment: ""))
.padding(.top, 10) .padding(.top, 10)
} }
} }

View File

@ -7,12 +7,12 @@ struct CustomTabBar: View {
var body: some View { var body: some View {
HStack { HStack {
// Tab 1: Feed // Tab 1: Feed
TabBarButton(systemName: "list.bullet.rectangle", text: "Лента", isSelected: selectedTab == 0) { TabBarButton(systemName: "list.bullet.rectangle", text: NSLocalizedString("Лента", comment: ""), isSelected: selectedTab == 0) {
selectedTab = 0 selectedTab = 0
} }
// Tab 2: Search // Tab 2: Search
TabBarButton(systemName: "magnifyingglass", text: "Поиск", isSelected: selectedTab == 1) { TabBarButton(systemName: "magnifyingglass", text: NSLocalizedString("Поиск", comment: ""), isSelected: selectedTab == 1) {
selectedTab = 1 selectedTab = 1
} }
@ -22,12 +22,12 @@ struct CustomTabBar: View {
} }
// Tab 3: Chats // Tab 3: Chats
TabBarButton(systemName: "bubble.left.and.bubble.right.fill", text: "Чаты", isSelected: selectedTab == 2) { TabBarButton(systemName: "bubble.left.and.bubble.right.fill", text: NSLocalizedString("Чаты", comment: ""), isSelected: selectedTab == 2) {
selectedTab = 2 selectedTab = 2
} }
// Tab 4: Profile // Tab 4: Profile
TabBarButton(systemName: "person.crop.square", text: "Лицо", isSelected: selectedTab == 3) { TabBarButton(systemName: "person.crop.square", text: NSLocalizedString("Лицо", comment: ""), isSelected: selectedTab == 3) {
selectedTab = 3 selectedTab = 3
} }
} }

View File

@ -79,7 +79,7 @@ struct MainView: View {
.allowsHitTesting(menuOffset > 0) .allowsHitTesting(menuOffset > 0)
// Боковое меню // Боковое меню
SideMenuView(isPresented: $isSideMenuPresented) SideMenuView(viewModel: viewModel, isPresented: $isSideMenuPresented)
.frame(width: menuWidth) .frame(width: menuWidth)
.offset(x: -menuWidth + menuOffset) // Новая логика смещения .offset(x: -menuWidth + menuOffset) // Новая логика смещения
.ignoresSafeArea(edges: .vertical) .ignoresSafeArea(edges: .vertical)

View File

@ -0,0 +1,47 @@
import SwiftUI
struct FAQView: View {
private struct FAQItem: Identifiable {
let id = UUID()
let question: String
let answer: String
}
private let faqItems: [FAQItem] = [
FAQItem(
question: NSLocalizedString("Как сбросить пароль?", comment: "FAQ question: reset password"),
answer: NSLocalizedString("Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям.", comment: "FAQ answer: reset password")
),
FAQItem(
question: NSLocalizedString("Где найти сохранённые черновики?", comment: "FAQ question: drafts"),
answer: NSLocalizedString("Черновики доступны в боковом меню в разделе Drafts.", comment: "FAQ answer: drafts")
),
FAQItem(
question: NSLocalizedString("Как связаться с поддержкой?", comment: "FAQ question: support"),
answer: NSLocalizedString("Напишите нам через форму обратной связи в разделе \"Поддержка\".", comment: "FAQ answer: support")
)
]
var body: some View {
List(faqItems) { item in
VStack(alignment: .leading, spacing: 6) {
Text(item.question)
.font(.headline)
Text(item.answer)
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.vertical, 6)
}
.listStyle(.insetGrouped)
.navigationTitle(NSLocalizedString("Частые вопросы", comment: "FAQ navigation title"))
}
}
struct FAQView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
FAQView()
}
}
}

View File

@ -54,7 +54,7 @@ struct SettingsView: View {
// MARK: - Поддержка // MARK: - Поддержка
Section(header: Text("Поддержка")) { Section(header: Text("Поддержка")) {
NavigationLink(destination: Text("Заглушка: Частые вопросы")) { NavigationLink(destination: FAQView()) {
Label("Частые вопросы", systemImage: "questionmark.circle") Label("Частые вопросы", systemImage: "questionmark.circle")
} }
NavigationLink(destination: Text("Заглушка: Обратная связь")) { NavigationLink(destination: Text("Заглушка: Обратная связь")) {

View File

@ -53,8 +53,10 @@ struct SideMenuFooterButton: View {
Text(title) Text(title)
.font(.caption2) .font(.caption2)
} }
.frame(maxWidth: .infinity)
.foregroundColor(.primary) .foregroundColor(.primary)
} }
.frame(maxWidth: .infinity)
} }
} }
@ -62,24 +64,18 @@ struct SideMenuFooterButton: View {
// --- MAIN VIEW --- // --- MAIN VIEW ---
struct SideMenuView: View { struct SideMenuView: View {
@ObservedObject var viewModel: LoginViewModel
@EnvironmentObject var themeManager: ThemeManager @EnvironmentObject var themeManager: ThemeManager
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Binding var isPresented: Bool @Binding var isPresented: Bool
@State private var isAccountListExpanded = false @State private var isAccountListExpanded = false
@State private var navigateToSettings = false
@State private var navigateToFAQ = false
// Adjustable paddings // Adjustable paddings
private let topPadding: CGFloat = 66 private let topPadding: CGFloat = 66
private let bottomPadding: CGFloat = 34 private let bottomPadding: CGFloat = 34
// Dummy account data
private let accounts: [Account] = [
Account(name: "Your Name", username: "@yourusername", isCurrent: true),
Account(name: "Second Account", username: "@second", isCurrent: false),
Account(name: "Another One", username: "@another", isCurrent: false),
Account(name: "Test User", username: "@test", isCurrent: false),
Account(name: "Creative Profile", username: "@creative", isCurrent: false)
]
private var themeToggleButton: some View { private var themeToggleButton: some View {
Button(action: { Button(action: {
themeManager.toggleTheme(from: colorScheme) themeManager.toggleTheme(from: colorScheme)
@ -106,124 +102,58 @@ struct SideMenuView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
NavigationLink(
destination: SettingsView(viewModel: viewModel),
isActive: $navigateToSettings
) {
EmptyView()
}
NavigationLink(
destination: FAQView(),
isActive: $navigateToFAQ
) {
EmptyView()
}
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) { // Parent VStack VStack(alignment: .leading, spacing: 0) { // Parent VStack
// --- Header ---
HStack(alignment: .top) {
Button(action: { }) {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 60, height: 60)
.foregroundColor(.gray)
}
Spacer()
themeToggleButton
}
.padding(.horizontal, 20)
.padding(.top, topPadding)
.padding(.bottom, 10)
// --- Header Button ---
Button(action: {
withAnimation(.spring()) {
isAccountListExpanded.toggle()
}
}) {
HStack {
VStack(alignment: .leading) {
Text("Your Name")
.font(.title3).bold()
Text("@yourusername")
.font(.footnote)
}
.foregroundColor(.primary)
Spacer()
Image(systemName: isAccountListExpanded ? "chevron.up" : "chevron.down")
.font(.headline)
.foregroundColor(.secondary)
}
}
.padding(.horizontal, 20)
.padding(.bottom, 10)
// --- Collapsible Account List in a clipped container ---
VStack {
if isAccountListExpanded {
VStack(alignment: .leading, spacing: 15) {
ForEach(accounts) { account in
HStack {
Button(action: { }) {
ZStack {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 32, height: 32) // Smaller icon
.foregroundColor(.secondary)
if account.isCurrent {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.blue)
.background(Circle().fill(Color(UIColor.systemBackground)))
.font(.body) // Smaller checkmark
.offset(x: 11, y: 11) // Adjusted offset
}
}
VStack(alignment: .leading) {
Text(account.name).font(.footnote).bold() // Smaller text
Text(account.username).font(.caption2) // Smaller text
}
.foregroundColor(.primary)
}
Spacer()
}
}
}
.padding(.horizontal, 20)
.padding(.vertical, 10)
.transition(.slideAndFade)
}
}
.clipped()
// Menu Items // Menu Items
VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 20) {
// Section 1 // Section 1
VStack(alignment: .leading, spacing: 7) { VStack(alignment: .leading, spacing: 7) {
SideMenuButton(icon: "person.2.fill", title: "People You May Like", action: {}) SideMenuButton(icon: "person.2.fill", title: NSLocalizedString("Добавить друзей", comment: "Add friends"), action: {})
SideMenuButton(icon: "star.fill", title: "Fun Fest", action: {}) SideMenuButton(icon: "star.fill", title: NSLocalizedString("Fun Fest", comment: "Fun Fest"), action: {})
SideMenuButton(icon: "lightbulb.fill", title: "Creator Center", action: {}) SideMenuButton(icon: "lightbulb.fill", title: NSLocalizedString("Центр авторов", comment: "Creator Center"), action: {})
} }
.padding(.top, topPadding)
Divider() Divider()
// Section 2 // Section 2
VStack(alignment: .leading, spacing: 7) { VStack(alignment: .leading, spacing: 7) {
Text("CATEGORY").font(.caption2).foregroundColor(.secondary) // Text("CATEGORY").font(.caption2).foregroundColor(.secondary)
SideMenuButton(icon: "doc.text", title: "Drafts", action: {}) SideMenuButton(icon: "doc.text", title: NSLocalizedString("Черновики", comment: "Drafts"), action: {})
SideMenuButton(icon: "bubble.left", title: "My Comments", action: {}) SideMenuButton(icon: "bubble.left", title: NSLocalizedString("Мои комментарии", comment: "My Comments"), action: {})
SideMenuButton(icon: "clock", title: "History", action: {}) SideMenuButton(icon: "clock", title: NSLocalizedString("История", comment: "History"), action: {})
SideMenuButton(icon: "arrow.down.circle", title: "My Downloads", action: {}) SideMenuButton(icon: "arrow.down.circle", title: NSLocalizedString("Мои загрузки", comment: "My Downloads"), action: {})
} }
Divider() Divider()
// Section 3 // Section 3
VStack(alignment: .leading, spacing: 7) { VStack(alignment: .leading, spacing: 7) {
Text("SERVICES").font(.caption2).foregroundColor(.secondary) // Text("SERVICES").font(.caption2).foregroundColor(.secondary)
SideMenuButton(icon: "shippingbox", title: "Orders", action: {}) SideMenuButton(icon: "shippingbox", title: NSLocalizedString("Заказы", comment: "Orders"), action: {})
SideMenuButton(icon: "cart", title: "Cart", action: {}) SideMenuButton(icon: "cart", title: NSLocalizedString("Корзина", comment: "Cart"), action: {})
SideMenuButton(icon: "wallet.pass", title: "Wallet", action: {}) SideMenuButton(icon: "wallet.pass", title: NSLocalizedString("Кошелёк", comment: "Wallet"), action: {})
} }
Divider() Divider()
// Section 4 // Section 4
VStack(alignment: .leading, spacing: 15) { VStack(alignment: .leading, spacing: 15) {
SideMenuButton(icon: "square.grid.2x2", title: "Applets", action: {}) SideMenuButton(icon: "square.grid.2x2", title: NSLocalizedString("Мини-приложения", comment: "Applets"), action: {})
} }
} }
.padding() .padding()
@ -235,14 +165,35 @@ struct SideMenuView: View {
Spacer() Spacer()
// Footer // Footer
HStack(spacing: 20) { HStack(spacing: 0) {
Spacer() SideMenuFooterButton(
SideMenuFooterButton(icon: "qrcode.viewfinder", title: "Scan", action: {}) icon: "qrcode.viewfinder",
SideMenuFooterButton(icon: "questionmark.circle", title: "Help Center", action: {}) title: NSLocalizedString("Скан", comment: "Scan"),
SideMenuFooterButton(icon: "gear", title: "Settings", action: {}) action: {}
Spacer() )
Spacer(minLength: 40)
SideMenuFooterButton(
icon: "questionmark.circle",
title: NSLocalizedString("Помощь", comment: "Help Center")
) {
withAnimation(.easeInOut) {
isPresented = false
} }
.padding() navigateToFAQ = true
}
Spacer(minLength: 40)
SideMenuFooterButton(
icon: "gear",
title: NSLocalizedString("Настройки", comment: "Settings")
) {
withAnimation(.easeInOut) {
isPresented = false
}
navigateToSettings = true
}
}
.padding(.horizontal, 28)
.padding(.vertical)
.padding(.bottom, bottomPadding) .padding(.bottom, bottomPadding)
} }
.background(Color(UIColor.systemBackground)) .background(Color(UIColor.systemBackground))
@ -253,7 +204,10 @@ struct SideMenuView: View {
struct SideMenuView_Previews: PreviewProvider { struct SideMenuView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SideMenuView(isPresented: .constant(true)) let mockViewModel = LoginViewModel()
NavigationView {
SideMenuView(viewModel: mockViewModel, isPresented: .constant(true))
.environmentObject(ThemeManager()) .environmentObject(ThemeManager())
} }
} }
}