main menu

This commit is contained in:
cheykrym 2025-10-05 04:24:43 +03:00
parent 842033dcb5
commit 1c01c2a340
14 changed files with 946 additions and 65 deletions

View File

@ -395,7 +395,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements; CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = V22H44W47J; DEVELOPMENT_TEAM = V22H44W47J;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -435,7 +435,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements; CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = V22H44W47J; DEVELOPMENT_TEAM = V22H44W47J;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;

View File

@ -0,0 +1,104 @@
import SwiftUI
struct TopBarView: View {
var title: String
// Состояния для ProfileTab
@Binding var selectedAccount: String
// @Binding var sheetType: ProfileTab.SheetType?
var accounts: [String]
var viewModel: LoginViewModel
// Привязка для управления боковым меню
@Binding var isSideMenuPresented: Bool
var isHomeTab: Bool {
return title == "Home"
}
var isProfileTab: Bool {
return title == "Profile"
}
var body: some View {
VStack(spacing: 0) {
HStack {
// Кнопка "Гамбургер" для открытия меню
Button(action: {
withAnimation {
isSideMenuPresented.toggle()
}
}) {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
.foregroundColor(.primary)
}
// Spacer()
if isHomeTab{
Text("Yobble")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
} else if isProfileTab {
Spacer()
Button(action: { }) {
HStack(spacing: 4) {
Text(selectedAccount)
.font(.headline)
.foregroundColor(.primary)
Image(systemName: "chevron.down")
.font(.subheadline)
.foregroundColor(.gray)
}
}
Spacer()
} else {
Text(title)
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
}
if isHomeTab{
HStack(spacing: 20) {
// Кнопка поиска
Button(action: {
// пока ничего не делаем
}) {
Image(systemName: "magnifyingglass")
.imageScale(.large)
.foregroundColor(.primary)
}
// Кнопка уведомлений
Button(action: {
// пока ничего не делаем
}) {
Image(systemName: "bell")
.imageScale(.large)
.foregroundColor(.primary)
}
}
} else if isProfileTab {
NavigationLink(destination: SettingsView(viewModel: viewModel)) {
Image(systemName: "wrench")
.imageScale(.large)
.foregroundColor(.primary)
}
}
}
.padding()
.frame(height: 50) // Стандартная высота для нав. бара
Divider()
}
.background(Color(UIColor.systemBackground))
}
}
struct TopBarView_Previews: PreviewProvider {
static var previews: some View {
/*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/
}
}

View File

@ -99,11 +99,12 @@ class AuthService {
do { do {
let decoder = JSONDecoder() let decoder = JSONDecoder()
let loginResponse = try decoder.decode(LoginResponse.self, from: data) let loginResponse = try decoder.decode(APIResponse<LoginPayload>.self, from: data).data
// Сохраняем токены в 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)
KeychainService.shared.save(loginResponse.user_id, forKey: "user_id", service: username)
UserDefaults.standard.set(username, forKey: "currentUser") UserDefaults.standard.set(username, forKey: "currentUser")
completion(true, nil) completion(true, nil)
@ -160,7 +161,7 @@ class AuthService {
if (200...299).contains(httpResponse.statusCode) { if (200...299).contains(httpResponse.statusCode) {
do { do {
let _ = try decoder.decode(RegisterResponse.self, from: data) let _ = try decoder.decode(APIResponse<RegisterPayload>.self, from: data)
if AppConfig.DEBUG{ print("Регистрация успешна. Пытаемся сразу войти...")} if AppConfig.DEBUG{ print("Регистрация успешна. Пытаемся сразу войти...")}
// Сразу логинимся // Сразу логинимся
@ -177,8 +178,8 @@ class AuthService {
} }
} else { } else {
// Ошибка сервера пробуем распарсить message // Ошибка сервера пробуем распарсить message
if let errorResponseMessage = try? decoder.decode(ErrorResponseMessage.self, from: data), if let errorResponseMessage = try? decoder.decode(ErrorResponse.self, from: data),
let message = errorResponseMessage.message { let message = errorResponseMessage.data?.message {
if let jsonString = String(data: data, encoding: .utf8) { if let jsonString = String(data: data, encoding: .utf8) {
if AppConfig.DEBUG{ print("Raw JSON:", jsonString)} if AppConfig.DEBUG{ print("Raw JSON:", jsonString)}
@ -270,25 +271,31 @@ class AuthService {
} }
} }
struct LoginResponse: Decodable { struct APIResponse<T: Decodable>: Decodable {
let status: String let status: String
let data: T
}
struct LoginPayload: Decodable {
let access_token: String let access_token: String
let refresh_token: String let refresh_token: String
let token_type: String let user_id: String
} }
struct TokenRefreshResponse: Decodable { struct TokenRefreshPayload: Decodable {
let status: String
let access_token: String let access_token: String
let token_type: String let token_type: String
} }
struct RegisterResponse: Decodable { struct RegisterPayload: Decodable {
let status: String
let message: String let message: String
} }
struct ErrorResponseMessage: Decodable { struct ErrorPayload: Decodable {
let status: String?
let message: String? let message: String?
} }
struct ErrorResponse: Decodable {
let status: String?
let data: ErrorPayload?
}

View File

@ -1,58 +1,13 @@
{ {
"sourceLanguage" : "en", "sourceLanguage" : "en",
"strings" : { "strings" : {
"@yourusername" : {
},
"🌍" : { "🌍" : {
}, },
"AuthService_error_empty_response" : { "CATEGORY" : {
},
"AuthService_error_invalid_invitation_code" : {
},
"AuthService_error_invalid_request" : {
},
"AuthService_error_invalid_response" : {
},
"AuthService_error_invitation_expired" : {
},
"AuthService_error_invitation_not_active" : {
},
"AuthService_error_invitation_usage_limit" : {
},
"AuthService_error_login_already_registered" : {
},
"AuthService_error_network" : {
},
"AuthService_error_parsing_response" : {
},
"AuthService_error_registration_disabled" : {
},
"AuthService_error_registration_forbidden" : {
},
"AuthService_error_serialization" : {
},
"AuthService_error_server_error" : {
},
"AuthService_error_server_unavailable" : {
},
"AuthService_error_too_many_requests" : {
},
"AuthService_login_success_but_failed" : {
}, },
"Hello, world!" : { "Hello, world!" : {
@ -94,15 +49,75 @@
}, },
"profile_down_text_3" : { "profile_down_text_3" : {
},
"Push-уведомления" : {
},
"SERVICES" : {
},
"Yobble" : {
},
"Your Name" : {
},
"Активные сессии" : {
},
"Безопасность" : {
}, },
"Войти" : { "Войти" : {
},
"Выйти из аккаунта" : {
},
"Данные" : {
},
"Двухфакторная аутентификация" : {
},
"Другое" : {
},
"Заглушка: Push-уведомления" : {
},
"Заглушка: Активные сессии" : {
},
"Заглушка: Двухфакторная аутентификация" : {
},
"Заглушка: Другие настройки" : {
},
"Заглушка: Обратная связь" : {
},
"Заглушка: Сменить пароль" : {
},
"Заглушка: Хранилище данных" : {
},
"Заглушка: Частые вопросы" : {
}, },
"Закрыть" : { "Закрыть" : {
"comment" : "Закрыть" "comment" : "Закрыть"
}, },
"Зарегистрироваться" : { "Зарегистрироваться" : {
"comment" : "Зарегистрироваться" "comment" : "Зарегистрироваться"
},
"Здесь будут чаты" : {
},
"Здесь не будут чаты" : {
}, },
"Инвайт-код (необязательно)" : { "Инвайт-код (необязательно)" : {
"comment" : "Инвайт-код" "comment" : "Инвайт-код"
@ -112,9 +127,27 @@
}, },
"Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)" : { "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)" : {
"comment" : "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)" "comment" : "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)"
},
"Логин уже занят." : {
},
"Мой профиль" : {
},
"Настройки" : {
},
"Не удалось обработать ответ сервера." : {
}, },
"Не удалось сериализовать данные запроса." : { "Не удалось сериализовать данные запроса." : {
},
"Неверный запрос (400)." : {
},
"Неверный код приглашения." : {
}, },
"Неверный логин" : { "Неверный логин" : {
"comment" : "Неверный логин" "comment" : "Неверный логин"
@ -136,6 +169,18 @@
}, },
"Нет аккаунта? Регистрация" : { "Нет аккаунта? Регистрация" : {
"comment" : "Регистрация" "comment" : "Регистрация"
},
"О приложении" : {
},
"Обратная связь" : {
},
"Описание" : {
},
"Отображаемое имя" : {
}, },
"Ошибка авторизации" : { "Ошибка авторизации" : {
@ -160,21 +205,75 @@
}, },
"Пароль должен быть от 8 до 128 символов" : { "Пароль должен быть от 8 до 128 символов" : {
"comment" : "Пароль должен быть от 6 до 32 символов" "comment" : "Пароль должен быть от 6 до 32 символов"
},
"Поддержка" : {
}, },
"Подтверждение пароля" : { "Подтверждение пароля" : {
"comment" : "Подтверждение пароля" "comment" : "Подтверждение пароля"
},
"Приглашение достигло лимита использования." : {
},
"Приглашение истекло." : {
},
"Приглашение не активно." : {
},
"Приложение" : {
},
"Применить" : {
}, },
"Произошла ошибка." : { "Произошла ошибка." : {
},
"Профиль" : {
},
"Публичная информация" : {
},
"Пустой ответ от сервера." : {
}, },
"Регистрация" : { "Регистрация" : {
"comment" : "Регистрация" "comment" : "Регистрация"
},
"Регистрация временно недоступна." : {
},
"Регистрация выполнена, но вход не удался." : {
},
"Регистрация запрещена." : {
},
"Редактировать профиль" : {
}, },
"Сервер не отвечает. Попробуйте позже." : { "Сервер не отвечает. Попробуйте позже." : {
}, },
"Слишком много запросов." : { "Слишком много запросов." : {
},
"Сменить пароль" : {
},
"Тёмная тема" : {
},
"Уведомления" : {
},
"Частые вопросы" : {
},
"Язык" : {
} }
}, },
"version" : "1.0" "version" : "1.0"

View File

@ -0,0 +1,20 @@
//
// ChatsTab.swift
// VolnahubApp
//
// Created by cheykrym on 09/06/2025.
//
import SwiftUI
struct ChatsTab: View {
var body: some View {
VStack {
Text("Здесь будут чаты")
.font(.title)
.foregroundColor(.gray)
Spacer()
}
}
}

View File

@ -0,0 +1,84 @@
import SwiftUI
struct CustomTabBar: View {
@Binding var selectedTab: Int
var onCreate: () -> Void
var body: some View {
HStack {
// Tab 1: Feed
TabBarButton(systemName: "list.bullet.rectangle", text: "Лента", isSelected: selectedTab == 0) {
selectedTab = 0
}
// Tab 2: Search
TabBarButton(systemName: "magnifyingglass", text: "Поиск", isSelected: selectedTab == 1) {
selectedTab = 1
}
// Create Button
CreateButton {
onCreate()
}
// Tab 3: Chats
TabBarButton(systemName: "bubble.left.and.bubble.right.fill", text: "Чаты", isSelected: selectedTab == 2) {
selectedTab = 2
}
// Tab 4: Profile
TabBarButton(systemName: "person.crop.square", text: "Лицо", isSelected: selectedTab == 3) {
selectedTab = 3
}
}
.padding(.horizontal)
.padding(.top, 1)
.padding(.bottom, 30) // Добавляем отступ снизу
// .background(Color(.systemGray6))
}
}
struct TabBarButton: View {
let systemName: String
let text: String
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 4) {
Image(systemName: systemName)
.font(.system(size: 22))
Text(text)
// .font(.caption)
.font(.system(size: 12))
}
.foregroundColor(isSelected ? .accentColor : .gray)
}
.frame(maxWidth: .infinity)
}
}
struct CreateButton: View {
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: "plus")
.font(.system(size: 24, weight: .bold))
.foregroundColor(.white)
.padding(16)
// .background(Color.accentColor)
.background(
LinearGradient(
gradient: Gradient(colors: [.blue, .white]),
startPoint: .top,
endPoint: .bottom
)
)
.clipShape(Circle())
.shadow(radius: 4)
}
.offset(y: -3)
}
}

View File

@ -0,0 +1,136 @@
import SwiftUI
struct MainView: View {
@ObservedObject var viewModel: LoginViewModel
@State private var selectedTab: Int = 0
// @StateObject private var newHomeTabViewModel = NewHomeTabViewModel()
// Состояния для TopBarView
@State private var selectedAccount = "@user1"
@State private var accounts = ["@user1", "@user2", "@user3"]
// @State private var sheetType: ProfileTab.SheetType? = nil
// Состояния для бокового меню
@State private var isSideMenuPresented = false
@State private var menuOffset: CGFloat = 0
private var tabTitle: String {
switch selectedTab {
case 0: return "Home"
case 1: return "Search"
case 2: return "Chats"
case 3: return "Profile"
default: return "Home"
}
}
private var menuWidth: CGFloat {
UIScreen.main.bounds.width * 0.8
}
var body: some View {
NavigationView {
ZStack(alignment: .leading) { // Выравниваем ZStack по левому краю
// Основной контент
VStack(spacing: 0) {
TopBarView(
title: tabTitle,
selectedAccount: $selectedAccount,
accounts: accounts,
viewModel: viewModel,
isSideMenuPresented: $isSideMenuPresented
)
ZStack {
NewHomeTab()
.opacity(selectedTab == 0 ? 1 : 0)
SearchTab()
.opacity(selectedTab == 1 ? 1 : 0)
ChatsTab()
.opacity(selectedTab == 2 ? 1 : 0)
ProfileTab()
.opacity(selectedTab == 3 ? 1 : 0)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
CustomTabBar(selectedTab: $selectedTab) {
print("Create button tapped")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity) // Убедимся, что основной контент занимает все пространство
.ignoresSafeArea(edges: .bottom)
.navigationBarHidden(true)
// .sheet(item: $sheetType) { type in
// // ... sheet presentation logic
// }
// Затемнение и закрытие по тапу
Color.black
.opacity(Double(menuOffset / menuWidth) * 0.4)
.ignoresSafeArea()
.onTapGesture {
withAnimation(.easeInOut) {
isSideMenuPresented = false
}
}
.allowsHitTesting(menuOffset > 0)
// Боковое меню
SideMenuView(isPresented: $isSideMenuPresented)
.frame(width: menuWidth)
.offset(x: -menuWidth + menuOffset) // Новая логика смещения
.ignoresSafeArea(edges: .vertical)
}
.gesture(
DragGesture()
.onChanged { gesture in
if !isSideMenuPresented && gesture.startLocation.x > 60 { return }
let translation = gesture.translation.width
// Определяем базовое смещение в зависимости от того, открыто меню или нет
let baseOffset = isSideMenuPresented ? menuWidth : 0
// Новое смещение это база плюс текущий свайп
let newOffset = baseOffset + translation
// Жестко ограничиваем итоговое смещение между 0 и шириной меню
self.menuOffset = max(0, min(menuWidth, newOffset))
}
.onEnded { gesture in
if !isSideMenuPresented && gesture.startLocation.x > 60 { return }
let threshold = menuWidth * 0.4
withAnimation(.easeInOut) {
if self.menuOffset > threshold {
isSideMenuPresented = true
} else {
isSideMenuPresented = false
}
// Устанавливаем финальное смещение после анимации
self.menuOffset = isSideMenuPresented ? menuWidth : 0
}
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
.onChange(of: isSideMenuPresented) { presented in
// Плавная анимация при нажатии на кнопку, а не только при жесте
withAnimation(.easeInOut) {
menuOffset = presented ? menuWidth : 0
}
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
let mockViewModel = LoginViewModel()
MainView(viewModel: mockViewModel)
.environmentObject(ThemeManager())
}
}

View File

@ -0,0 +1,17 @@
import SwiftUI
struct NewHomeTab: View {
var body: some View {
VStack {
VStack {
Text("Здесь не будут чаты")
.font(.title)
.foregroundColor(.gray)
Spacer()
}
}
// .background(Color(.secondarySystemBackground)) // Фон для всей вкладки
}
}

View File

@ -0,0 +1,20 @@
import SwiftUI
struct ProfileTab: View {
// @Binding var sheetType: SheetType?
// enum SheetType: Identifiable {
// case accountShare
// var id: Int { self.hashValue }
// }
var body: some View {
VStack {
Text("Здесь не будут чаты")
.font(.title)
.foregroundColor(.gray)
Spacer()
}
}
}

View File

@ -0,0 +1,14 @@
import SwiftUI
struct SearchTab: View {
var body: some View {
VStack {
Text("Здесь не будут чаты")
.font(.title)
.foregroundColor(.gray)
Spacer()
}
}
}

View File

@ -0,0 +1,24 @@
import SwiftUI
struct EditProfileView: View {
@State private var displayName = ""
@State private var description = ""
var body: some View {
Form {
Section(header: Text("Публичная информация")) {
TextField("Отображаемое имя", text: $displayName)
TextField("Описание", text: $description)
}
Button(action: {
// Действие для сохранения профиля
print("DisplayName: \(displayName)")
print("Description: \(description)")
}) {
Text("Применить")
}
}
.navigationTitle("Редактировать профиль")
}
}

View File

@ -0,0 +1,98 @@
import SwiftUI
struct SettingsView: View {
@ObservedObject var viewModel: LoginViewModel
@AppStorage("isDarkMode") private var isDarkMode: Bool = true
var body: some View {
Form {
// MARK: - Профиль
Section(header: Text("Профиль")) {
NavigationLink(destination: EditProfileView()) {
Label("Мой профиль", systemImage: "person.crop.circle")
}
}
// MARK: - Безопасность
Section(header: Text("Безопасность")) {
NavigationLink(destination: Text("Заглушка: Сменить пароль")) {
Label("Сменить пароль", systemImage: "key")
}
NavigationLink(destination: Text("Заглушка: Двухфакторная аутентификация")) {
Label("Двухфакторная аутентификация", systemImage: "lock.shield")
}
NavigationLink(destination: Text("Заглушка: Активные сессии")) {
Label("Активные сессии", systemImage: "iphone")
}
}
// MARK: - Приложение
Section(header: Text("Приложение")) {
Button(action: openLanguageSettings) {
Label("Язык", systemImage: "globe")
}
Toggle(isOn: $isDarkMode) {
Label("Тёмная тема", systemImage: "moon.fill")
}
NavigationLink(destination: Text("Заглушка: Хранилище данных")) {
Label("Данные", systemImage: "externaldrive")
}
NavigationLink(destination: Text("Заглушка: Другие настройки")) {
Label("Другое", systemImage: "ellipsis.circle")
}
}
// MARK: - Уведомления
Section(header: Text("Уведомления")) {
NavigationLink(destination: Text("Заглушка: Push-уведомления")) {
Label("Push-уведомления", systemImage: "bell")
}
}
// MARK: - Поддержка
Section(header: Text("Поддержка")) {
NavigationLink(destination: Text("Заглушка: Частые вопросы")) {
Label("Частые вопросы", systemImage: "questionmark.circle")
}
NavigationLink(destination: Text("Заглушка: Обратная связь")) {
Label("Обратная связь", systemImage: "paperplane")
}
}
// MARK: - О приложении
Section(header: Text("О приложении")) {
VStack(alignment: .leading, spacing: 6) {
Text(AppInfo.text_1)
Text(AppInfo.text_2)
Text(AppInfo.text_3)
}
.font(.footnote)
.foregroundColor(.gray)
.padding(.vertical, 4)
}
// MARK: - Выход
Section {
Button(action: {
viewModel.logoutCurrentUser()
}) {
HStack {
Image(systemName: "arrow.backward.square")
Text("Выйти из аккаунта")
}
.foregroundColor(.red)
}
}
}
.navigationTitle("Настройки")
}
private func openLanguageSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
}
}

View File

@ -0,0 +1,259 @@
import SwiftUI
// --- HELPER STRUCTS & EXTENSIONS ---
// Dummy data for the account list
struct Account: Identifiable {
let id = UUID()
let name: String
let username: String
let isCurrent: Bool
}
// Custom Transition for older Xcode versions
extension AnyTransition {
static var slideAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .top).combined(with: .opacity)
let removal = AnyTransition.move(edge: .top).combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
// Helper Views for buttons
struct SideMenuButton: View {
let icon: String
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 15) {
Image(systemName: icon)
.font(.title3)
.frame(width: 30)
Text(title)
.font(.subheadline)
}
.foregroundColor(.primary)
.padding(.vertical, 8)
}
}
}
struct SideMenuFooterButton: View {
let icon: String
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
VStack {
Image(systemName: icon)
.font(.title2)
Text(title)
.font(.caption2)
}
.foregroundColor(.primary)
}
}
}
// --- MAIN VIEW ---
struct SideMenuView: View {
@EnvironmentObject var themeManager: ThemeManager
@Environment(\.colorScheme) var colorScheme
@Binding var isPresented: Bool
@State private var isAccountListExpanded = false
// Adjustable paddings
private let topPadding: CGFloat = 66
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 {
Button(action: {
themeManager.toggleTheme(from: colorScheme)
}) {
Image(systemName: iconName)
.font(.title2)
.foregroundColor(.primary)
}
}
private var iconName: String {
let effectiveScheme: ColorScheme
switch themeManager.theme {
case .system:
effectiveScheme = colorScheme
case .light:
effectiveScheme = .light
case .dark:
effectiveScheme = .dark
}
return effectiveScheme == .dark ? "sun.max.fill" : "moon.fill"
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
ScrollView {
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
VStack(alignment: .leading, spacing: 20) {
// Section 1
VStack(alignment: .leading, spacing: 7) {
SideMenuButton(icon: "person.2.fill", title: "People You May Like", action: {})
SideMenuButton(icon: "star.fill", title: "Fun Fest", action: {})
SideMenuButton(icon: "lightbulb.fill", title: "Creator Center", action: {})
}
Divider()
// Section 2
VStack(alignment: .leading, spacing: 7) {
Text("CATEGORY").font(.caption2).foregroundColor(.secondary)
SideMenuButton(icon: "doc.text", title: "Drafts", action: {})
SideMenuButton(icon: "bubble.left", title: "My Comments", action: {})
SideMenuButton(icon: "clock", title: "History", action: {})
SideMenuButton(icon: "arrow.down.circle", title: "My Downloads", action: {})
}
Divider()
// Section 3
VStack(alignment: .leading, spacing: 7) {
Text("SERVICES").font(.caption2).foregroundColor(.secondary)
SideMenuButton(icon: "shippingbox", title: "Orders", action: {})
SideMenuButton(icon: "cart", title: "Cart", action: {})
SideMenuButton(icon: "wallet.pass", title: "Wallet", action: {})
}
Divider()
// Section 4
VStack(alignment: .leading, spacing: 15) {
SideMenuButton(icon: "square.grid.2x2", title: "Applets", action: {})
}
}
.padding()
}
.frame(maxWidth: .infinity, alignment: .leading) // Align to the left
}
.clipped()
Spacer()
// Footer
HStack(spacing: 20) {
Spacer()
SideMenuFooterButton(icon: "qrcode.viewfinder", title: "Scan", action: {})
SideMenuFooterButton(icon: "questionmark.circle", title: "Help Center", action: {})
SideMenuFooterButton(icon: "gear", title: "Settings", action: {})
Spacer()
}
.padding()
.padding(.bottom, bottomPadding)
}
.background(Color(UIColor.systemBackground))
}
}
// --- PREVIEW ---
struct SideMenuView_Previews: PreviewProvider {
static var previews: some View {
SideMenuView(isPresented: .constant(true))
.environmentObject(ThemeManager())
}
}

View File

@ -18,8 +18,7 @@ struct yobbleApp: App {
if viewModel.isLoading { if viewModel.isLoading {
SplashScreenView() SplashScreenView()
} else if viewModel.isLoggedIn { } else if viewModel.isLoggedIn {
// MainView(viewModel: viewModel) MainView(viewModel: viewModel)
LoginView(viewModel: viewModel)
} else { } else {
LoginView(viewModel: viewModel) LoginView(viewModel: viewModel)
} }