main menu
This commit is contained in:
parent
842033dcb5
commit
1c01c2a340
@ -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;
|
||||||
|
|||||||
@ -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@*/
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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?
|
||||||
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NewHomeTab: View {
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
VStack {
|
||||||
|
Text("Здесь не будут чаты")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .background(Color(.secondarySystemBackground)) // Фон для всей вкладки
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SearchTab: View {
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text("Здесь не будут чаты")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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("Редактировать профиль")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user