Refactor iOS and Android views into separate files
Organized view components by extracting composables and views into separate files following single responsibility principle. iOS Changes: - Split MainTabView → extracted ProfileTabView - Split CompleteTaskView → extracted ImageThumbnailView, CameraPickerView - Split ManageUsersView → extracted ShareCodeCard, UserListItem - Consolidated task action buttons into single TaskActionButtons.swift file - Split HomeScreenView → extracted OverviewCard, StatView, HomeNavigationCard - Split AllTasksView → extracted DynamicTaskColumnView, DynamicTaskCard - Split ContentView → extracted ComposeView, CustomView Android Changes: - Split ResetPasswordScreen → extracted RequirementItem component - Split TasksScreen → extracted TaskPill component - Created TaskDisplayUtils for shared helper functions (getIconFromName, hexToColor) All extracted components properly organized in: - iOS: Subviews/Common, Subviews/Task, Subviews/Residence, Profile - Android: ui/components/auth, ui/components/task, ui/utils 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
package com.mycrib.android.ui.components.auth
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.Circle
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RequirementItem(text: String, satisfied: Boolean) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
if (satisfied) Icons.Default.CheckCircle else Icons.Default.Circle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (satisfied) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = if (satisfied) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.mycrib.android.ui.components.task
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TaskPill(
|
||||||
|
count: Int,
|
||||||
|
label: String,
|
||||||
|
color: Color
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
color = color.copy(alpha = 0.1f),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = count.toString(),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import androidx.compose.ui.text.input.VisualTransformation
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.mycrib.android.ui.components.auth.AuthHeader
|
import com.mycrib.android.ui.components.auth.AuthHeader
|
||||||
|
import com.mycrib.android.ui.components.auth.RequirementItem
|
||||||
import com.mycrib.android.ui.components.common.ErrorCard
|
import com.mycrib.android.ui.components.common.ErrorCard
|
||||||
import com.mycrib.android.viewmodel.PasswordResetViewModel
|
import com.mycrib.android.viewmodel.PasswordResetViewModel
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
@@ -254,23 +255,3 @@ fun ResetPasswordScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RequirementItem(text: String, satisfied: Boolean) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
if (satisfied) Icons.Default.CheckCircle else Icons.Default.Circle,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = if (satisfied) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = if (satisfied) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.mycrib.android.ui.components.CompleteTaskDialog
|
import com.mycrib.android.ui.components.CompleteTaskDialog
|
||||||
import com.mycrib.android.ui.components.task.TaskCard
|
import com.mycrib.android.ui.components.task.TaskCard
|
||||||
|
import com.mycrib.android.ui.components.task.TaskPill
|
||||||
|
import com.mycrib.android.ui.utils.getIconFromName
|
||||||
|
import com.mycrib.android.ui.utils.hexToColor
|
||||||
import com.mycrib.android.viewmodel.TaskCompletionViewModel
|
import com.mycrib.android.viewmodel.TaskCompletionViewModel
|
||||||
import com.mycrib.android.viewmodel.TaskViewModel
|
import com.mycrib.android.viewmodel.TaskViewModel
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
@@ -271,108 +274,3 @@ fun TasksScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TaskPill(
|
|
||||||
count: Int,
|
|
||||||
label: String,
|
|
||||||
color: androidx.compose.ui.graphics.Color
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
color = color.copy(alpha = 0.1f),
|
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = count.toString(),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = color
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = color
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getColumnColor(columnName: String): androidx.compose.ui.graphics.Color {
|
|
||||||
return when (columnName) {
|
|
||||||
"upcoming_tasks" -> MaterialTheme.colorScheme.primary
|
|
||||||
"in_progress_tasks" -> MaterialTheme.colorScheme.tertiary
|
|
||||||
"done_tasks" -> MaterialTheme.colorScheme.secondary
|
|
||||||
"archived_tasks" -> MaterialTheme.colorScheme.outline
|
|
||||||
else -> MaterialTheme.colorScheme.primary // Default color for unknown columns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getColumnIcon(columnName: String): androidx.compose.ui.graphics.vector.ImageVector {
|
|
||||||
return when (columnName) {
|
|
||||||
"upcoming_tasks" -> Icons.Default.CalendarToday
|
|
||||||
"in_progress_tasks" -> Icons.Default.PlayArrow
|
|
||||||
"done_tasks" -> Icons.Default.CheckCircle
|
|
||||||
"archived_tasks" -> Icons.Default.Archive
|
|
||||||
else -> Icons.Default.List // Default icon for unknown columns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to convert icon name string to ImageVector
|
|
||||||
*/
|
|
||||||
private fun getIconFromName(iconName: String): androidx.compose.ui.graphics.vector.ImageVector {
|
|
||||||
return when (iconName) {
|
|
||||||
"CalendarToday" -> Icons.Default.CalendarToday
|
|
||||||
"PlayCircle" -> Icons.Default.PlayCircle
|
|
||||||
"PlayArrow" -> Icons.Default.PlayArrow
|
|
||||||
"CheckCircle" -> Icons.Default.CheckCircle
|
|
||||||
"Archive" -> Icons.Default.Archive
|
|
||||||
"List" -> Icons.Default.List
|
|
||||||
"Unarchive" -> Icons.Default.Unarchive
|
|
||||||
else -> Icons.Default.List // Default fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to convert hex color string to Color
|
|
||||||
* Supports formats: #RGB, #RRGGBB, #AARRGGBB
|
|
||||||
* Platform-independent implementation
|
|
||||||
*/
|
|
||||||
private fun hexToColor(hex: String): androidx.compose.ui.graphics.Color {
|
|
||||||
val cleanHex = hex.removePrefix("#")
|
|
||||||
return try {
|
|
||||||
when (cleanHex.length) {
|
|
||||||
3 -> {
|
|
||||||
// RGB format - expand to RRGGBB
|
|
||||||
val r = cleanHex[0].toString().repeat(2).toInt(16)
|
|
||||||
val g = cleanHex[1].toString().repeat(2).toInt(16)
|
|
||||||
val b = cleanHex[2].toString().repeat(2).toInt(16)
|
|
||||||
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
|
||||||
}
|
|
||||||
6 -> {
|
|
||||||
// RRGGBB format
|
|
||||||
val r = cleanHex.substring(0, 2).toInt(16)
|
|
||||||
val g = cleanHex.substring(2, 4).toInt(16)
|
|
||||||
val b = cleanHex.substring(4, 6).toInt(16)
|
|
||||||
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
|
||||||
}
|
|
||||||
8 -> {
|
|
||||||
// AARRGGBB format
|
|
||||||
val a = cleanHex.substring(0, 2).toInt(16)
|
|
||||||
val r = cleanHex.substring(2, 4).toInt(16)
|
|
||||||
val g = cleanHex.substring(4, 6).toInt(16)
|
|
||||||
val b = cleanHex.substring(6, 8).toInt(16)
|
|
||||||
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f, alpha = a / 255f)
|
|
||||||
}
|
|
||||||
else -> androidx.compose.ui.graphics.Color.Gray // Default fallback
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
androidx.compose.ui.graphics.Color.Gray // Fallback on parse error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.mycrib.android.ui.utils
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to convert icon name string to ImageVector
|
||||||
|
*/
|
||||||
|
fun getIconFromName(iconName: String): ImageVector {
|
||||||
|
return when (iconName) {
|
||||||
|
"CalendarToday" -> Icons.Default.CalendarToday
|
||||||
|
"PlayCircle" -> Icons.Default.PlayCircle
|
||||||
|
"PlayArrow" -> Icons.Default.PlayArrow
|
||||||
|
"CheckCircle" -> Icons.Default.CheckCircle
|
||||||
|
"Archive" -> Icons.Default.Archive
|
||||||
|
"List" -> Icons.Default.List
|
||||||
|
"Unarchive" -> Icons.Default.Unarchive
|
||||||
|
else -> Icons.Default.List // Default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to convert hex color string to Color
|
||||||
|
* Supports formats: #RGB, #RRGGBB, #AARRGGBB
|
||||||
|
* Platform-independent implementation
|
||||||
|
*/
|
||||||
|
fun hexToColor(hex: String): Color {
|
||||||
|
val cleanHex = hex.removePrefix("#")
|
||||||
|
return try {
|
||||||
|
when (cleanHex.length) {
|
||||||
|
3 -> {
|
||||||
|
// RGB format - expand to RRGGBB
|
||||||
|
val r = cleanHex[0].toString().repeat(2).toInt(16)
|
||||||
|
val g = cleanHex[1].toString().repeat(2).toInt(16)
|
||||||
|
val b = cleanHex[2].toString().repeat(2).toInt(16)
|
||||||
|
Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||||
|
}
|
||||||
|
6 -> {
|
||||||
|
// RRGGBB format
|
||||||
|
val r = cleanHex.substring(0, 2).toInt(16)
|
||||||
|
val g = cleanHex.substring(2, 4).toInt(16)
|
||||||
|
val b = cleanHex.substring(4, 6).toInt(16)
|
||||||
|
Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||||
|
}
|
||||||
|
8 -> {
|
||||||
|
// AARRGGBB format
|
||||||
|
val a = cleanHex.substring(0, 2).toInt(16)
|
||||||
|
val r = cleanHex.substring(2, 4).toInt(16)
|
||||||
|
val g = cleanHex.substring(4, 6).toInt(16)
|
||||||
|
val b = cleanHex.substring(6, 8).toInt(16)
|
||||||
|
Color(red = r / 255f, green = g / 255f, blue = b / 255f, alpha = a / 255f)
|
||||||
|
}
|
||||||
|
else -> Color.Gray // Default fallback
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Color.Gray // Fallback on parse error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +1,9 @@
|
|||||||
import UIKit
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ComposeApp
|
import ComposeApp
|
||||||
|
|
||||||
struct ComposeView: UIViewControllerRepresentable {
|
|
||||||
func makeUIViewController(context: Context) -> UIViewController {
|
|
||||||
MainViewControllerKt.MainViewController()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CustomView()
|
CustomView()
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CustomView: View {
|
|
||||||
var body: some View {
|
|
||||||
Text("Custom view")
|
|
||||||
.task {
|
|
||||||
await ViewModel().somethingRandom()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewModel {
|
|
||||||
func somethingRandom() async {
|
|
||||||
TokenStorage().initialize(manager: TokenManager.init())
|
|
||||||
// TokenStorage.initialize(TokenManager.getInstance())
|
|
||||||
|
|
||||||
let api = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
|
|
||||||
|
|
||||||
api.deleteResidence(token: "token", id: 32) { result, error in
|
|
||||||
if let error = error {
|
|
||||||
print("Interop error: \(error)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let result = result else { return }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -62,104 +62,6 @@ struct HomeScreenView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OverviewCard: View {
|
|
||||||
let summary: OverallSummary
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "chart.bar.fill")
|
|
||||||
.font(.title3)
|
|
||||||
Text("Overview")
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: 40) {
|
|
||||||
StatView(
|
|
||||||
icon: "house.fill",
|
|
||||||
value: "\(summary.totalResidences)",
|
|
||||||
label: "Properties"
|
|
||||||
)
|
|
||||||
|
|
||||||
StatView(
|
|
||||||
icon: "list.bullet",
|
|
||||||
value: "\(summary.totalTasks)",
|
|
||||||
label: "Total Tasks"
|
|
||||||
)
|
|
||||||
|
|
||||||
StatView(
|
|
||||||
icon: "clock.fill",
|
|
||||||
value: "\(summary.totalPending)",
|
|
||||||
label: "Pending"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
.background(Color.blue.opacity(0.1))
|
|
||||||
.cornerRadius(16)
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StatView: View {
|
|
||||||
let icon: String
|
|
||||||
let value: String
|
|
||||||
let label: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
|
|
||||||
Text(value)
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
Text(label)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HomeNavigationCard: View {
|
|
||||||
let icon: String
|
|
||||||
let title: String
|
|
||||||
let subtitle: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.system(size: 36))
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.frame(width: 60)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(title)
|
|
||||||
.font(.title3)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Text(subtitle)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
HomeScreenView()
|
HomeScreenView()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,76 +33,6 @@ struct MainTabView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProfileTabView: View {
|
|
||||||
@EnvironmentObject var loginViewModel: LoginViewModel
|
|
||||||
@State private var showingProfileEdit = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
List {
|
|
||||||
Section {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "person.circle.fill")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 60, height: 60)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("User Profile")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
Text("Manage your account")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("Account") {
|
|
||||||
Button(action: {
|
|
||||||
showingProfileEdit = true
|
|
||||||
}) {
|
|
||||||
Label("Edit Profile", systemImage: "person.crop.circle")
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(destination: Text("Notifications")) {
|
|
||||||
Label("Notifications", systemImage: "bell")
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(destination: Text("Privacy")) {
|
|
||||||
Label("Privacy", systemImage: "lock.shield")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
Button(action: {
|
|
||||||
loginViewModel.logout()
|
|
||||||
}) {
|
|
||||||
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("MyCrib")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
Text("Version 1.0.0")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("Profile")
|
|
||||||
.sheet(isPresented: $showingProfileEdit) {
|
|
||||||
ProfileView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
MainTabView()
|
MainTabView()
|
||||||
}
|
}
|
||||||
|
|||||||
71
iosApp/iosApp/Profile/ProfileTabView.swift
Normal file
71
iosApp/iosApp/Profile/ProfileTabView.swift
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ProfileTabView: View {
|
||||||
|
@EnvironmentObject var loginViewModel: LoginViewModel
|
||||||
|
@State private var showingProfileEdit = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "person.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("User Profile")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Text("Manage your account")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Account") {
|
||||||
|
Button(action: {
|
||||||
|
showingProfileEdit = true
|
||||||
|
}) {
|
||||||
|
Label("Edit Profile", systemImage: "person.crop.circle")
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(destination: Text("Notifications")) {
|
||||||
|
Label("Notifications", systemImage: "bell")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(destination: Text("Privacy")) {
|
||||||
|
Label("Privacy", systemImage: "lock.shield")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button(action: {
|
||||||
|
loginViewModel.logout()
|
||||||
|
}) {
|
||||||
|
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("MyCrib")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
Text("Version 1.0.0")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Profile")
|
||||||
|
.sheet(isPresented: $showingProfileEdit) {
|
||||||
|
ProfileView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -155,118 +155,6 @@ struct ManageUsersView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Share Code Card
|
|
||||||
struct ShareCodeCard: View {
|
|
||||||
let shareCode: ResidenceShareCode?
|
|
||||||
let residenceName: String
|
|
||||||
let isGeneratingCode: Bool
|
|
||||||
let onGenerateCode: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Share Code")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
if let shareCode = shareCode {
|
|
||||||
Text(shareCode.code)
|
|
||||||
.font(.title)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
} else {
|
|
||||||
Text("No active code")
|
|
||||||
.font(.body)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: onGenerateCode) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "square.and.arrow.up")
|
|
||||||
Text(shareCode != nil ? "New Code" : "Generate")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.disabled(isGeneratingCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if shareCode != nil {
|
|
||||||
Text("Share this code with others to give them access to \(residenceName)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.blue.opacity(0.1))
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - User List Item
|
|
||||||
struct UserListItem: View {
|
|
||||||
let user: ResidenceUser
|
|
||||||
let isOwner: Bool
|
|
||||||
let isPrimaryOwner: Bool
|
|
||||||
let onRemove: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(alignment: .top, spacing: 12) {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
HStack {
|
|
||||||
Text(user.username)
|
|
||||||
.font(.body)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
|
|
||||||
if isOwner {
|
|
||||||
Text("Owner")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.padding(.horizontal, 6)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.background(Color.blue.opacity(0.1))
|
|
||||||
.cornerRadius(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user.email.isEmpty {
|
|
||||||
Text(user.email)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
let fullName = [user.firstName, user.lastName]
|
|
||||||
.compactMap { $0 }
|
|
||||||
.filter { !$0.isEmpty }
|
|
||||||
.joined(separator: " ")
|
|
||||||
|
|
||||||
if !fullName.isEmpty {
|
|
||||||
Text(fullName)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if isPrimaryOwner && !isOwner {
|
|
||||||
Button(action: onRemove) {
|
|
||||||
Image(systemName: "trash")
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ManageUsersView(residenceId: 1, residenceName: "My Home", isPrimaryOwner: true)
|
ManageUsersView(residenceId: 1, residenceName: "My Home", isPrimaryOwner: true)
|
||||||
}
|
}
|
||||||
|
|||||||
38
iosApp/iosApp/Subviews/Common/CameraPickerView.swift
Normal file
38
iosApp/iosApp/Subviews/Common/CameraPickerView.swift
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CameraPickerView: UIViewControllerRepresentable {
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
let onImageCaptured: (UIImage) -> Void
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||||
|
let picker = UIImagePickerController()
|
||||||
|
picker.sourceType = .camera
|
||||||
|
picker.delegate = context.coordinator
|
||||||
|
return picker
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||||
|
let parent: CameraPickerView
|
||||||
|
|
||||||
|
init(_ parent: CameraPickerView) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||||
|
if let image = info[.originalImage] as? UIImage {
|
||||||
|
parent.onImageCaptured(image)
|
||||||
|
}
|
||||||
|
parent.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||||
|
parent.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
iosApp/iosApp/Subviews/Common/ComposeView.swift
Normal file
11
iosApp/iosApp/Subviews/Common/ComposeView.swift
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
struct ComposeView: UIViewControllerRepresentable {
|
||||||
|
func makeUIViewController(context: Context) -> UIViewController {
|
||||||
|
MainViewControllerKt.MainViewController()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
|
||||||
|
}
|
||||||
28
iosApp/iosApp/Subviews/Common/CustomView.swift
Normal file
28
iosApp/iosApp/Subviews/Common/CustomView.swift
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
struct CustomView: View {
|
||||||
|
var body: some View {
|
||||||
|
Text("Custom view")
|
||||||
|
.task {
|
||||||
|
await ViewModel().somethingRandom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewModel {
|
||||||
|
func somethingRandom() async {
|
||||||
|
TokenStorage().initialize(manager: TokenManager.init())
|
||||||
|
// TokenStorage.initialize(TokenManager.getInstance())
|
||||||
|
|
||||||
|
let api = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
|
||||||
|
|
||||||
|
api.deleteResidence(token: "token", id: 32) { result, error in
|
||||||
|
if let error = error {
|
||||||
|
print("Interop error: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let result = result else { return }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
iosApp/iosApp/Subviews/Common/HomeNavigationCard.swift
Normal file
36
iosApp/iosApp/Subviews/Common/HomeNavigationCard.swift
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct HomeNavigationCard: View {
|
||||||
|
let icon: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: 36))
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.frame(width: 60)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(title)
|
||||||
|
.font(.title3)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.cornerRadius(12)
|
||||||
|
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
32
iosApp/iosApp/Subviews/Common/ImageThumbnailView.swift
Normal file
32
iosApp/iosApp/Subviews/Common/ImageThumbnailView.swift
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ImageThumbnailView: View {
|
||||||
|
let image: UIImage
|
||||||
|
let onRemove: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
Image(uiImage: image)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 100, height: 100)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
|
.overlay {
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.strokeBorder(.quaternary, lineWidth: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: onRemove) {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.background {
|
||||||
|
Circle()
|
||||||
|
.fill(.black.opacity(0.6))
|
||||||
|
.padding(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.offset(x: 8, y: -8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
iosApp/iosApp/Subviews/Common/OverviewCard.swift
Normal file
43
iosApp/iosApp/Subviews/Common/OverviewCard.swift
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
struct OverviewCard: View {
|
||||||
|
let summary: OverallSummary
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "chart.bar.fill")
|
||||||
|
.font(.title3)
|
||||||
|
Text("Overview")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 40) {
|
||||||
|
StatView(
|
||||||
|
icon: "house.fill",
|
||||||
|
value: "\(summary.totalResidences)",
|
||||||
|
label: "Properties"
|
||||||
|
)
|
||||||
|
|
||||||
|
StatView(
|
||||||
|
icon: "list.bullet",
|
||||||
|
value: "\(summary.totalTasks)",
|
||||||
|
label: "Total Tasks"
|
||||||
|
)
|
||||||
|
|
||||||
|
StatView(
|
||||||
|
icon: "clock.fill",
|
||||||
|
value: "\(summary.totalPending)",
|
||||||
|
label: "Pending"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.background(Color.blue.opacity(0.1))
|
||||||
|
.cornerRadius(16)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
iosApp/iosApp/Subviews/Common/StatView.swift
Normal file
23
iosApp/iosApp/Subviews/Common/StatView.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct StatView: View {
|
||||||
|
let icon: String
|
||||||
|
let value: String
|
||||||
|
let label: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
|
||||||
|
Text(value)
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
Text(label)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift
Normal file
53
iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
// MARK: - Share Code Card
|
||||||
|
struct ShareCodeCard: View {
|
||||||
|
let shareCode: ResidenceShareCode?
|
||||||
|
let residenceName: String
|
||||||
|
let isGeneratingCode: Bool
|
||||||
|
let onGenerateCode: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Share Code")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
if let shareCode = shareCode {
|
||||||
|
Text(shareCode.code)
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
} else {
|
||||||
|
Text("No active code")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: onGenerateCode) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "square.and.arrow.up")
|
||||||
|
Text(shareCode != nil ? "New Code" : "Generate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.disabled(isGeneratingCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shareCode != nil {
|
||||||
|
Text("Share this code with others to give them access to \(residenceName)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.blue.opacity(0.1))
|
||||||
|
.cornerRadius(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
63
iosApp/iosApp/Subviews/Residence/UserListItem.swift
Normal file
63
iosApp/iosApp/Subviews/Residence/UserListItem.swift
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
// MARK: - User List Item
|
||||||
|
struct UserListItem: View {
|
||||||
|
let user: ResidenceUser
|
||||||
|
let isOwner: Bool
|
||||||
|
let isPrimaryOwner: Bool
|
||||||
|
let onRemove: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 12) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Text(user.username)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
|
||||||
|
if isOwner {
|
||||||
|
Text("Owner")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(Color.blue.opacity(0.1))
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.email.isEmpty {
|
||||||
|
Text(user.email)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullName = [user.firstName, user.lastName]
|
||||||
|
.compactMap { $0 }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.joined(separator: " ")
|
||||||
|
|
||||||
|
if !fullName.isEmpty {
|
||||||
|
Text(fullName)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if isPrimaryOwner && !isOwner {
|
||||||
|
Button(action: onRemove) {
|
||||||
|
Image(systemName: "trash")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.cornerRadius(12)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
159
iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift
Normal file
159
iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
/// Task card that dynamically renders buttons based on the column's button types
|
||||||
|
struct DynamicTaskCard: View {
|
||||||
|
let task: TaskDetail
|
||||||
|
let buttonTypes: [String]
|
||||||
|
let onEdit: () -> Void
|
||||||
|
let onCancel: () -> Void
|
||||||
|
let onUncancel: () -> Void
|
||||||
|
let onMarkInProgress: () -> Void
|
||||||
|
let onComplete: () -> Void
|
||||||
|
let onArchive: () -> Void
|
||||||
|
let onUnarchive: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(task.title)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
if let status = task.status {
|
||||||
|
StatusBadge(status: status.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
PriorityBadge(priority: task.priority.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let description = task.description_, !description.isEmpty {
|
||||||
|
Text(description)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Label(task.frequency.displayName, systemImage: "repeat")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let due_date = task.dueDate {
|
||||||
|
Label(formatDate(due_date), systemImage: "calendar")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.completions.count > 0 {
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text("Completions (\(task.completions.count))")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(task.completions, id: \.id) { completion in
|
||||||
|
CompletionCardView(completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render buttons based on buttonTypes array
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
|
||||||
|
renderButton(for: buttonType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.cornerRadius(12)
|
||||||
|
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatDate(_ dateString: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
if let date = formatter.date(from: dateString) {
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func renderButton(for buttonType: String) -> some View {
|
||||||
|
switch buttonType {
|
||||||
|
case "mark_in_progress":
|
||||||
|
MarkInProgressButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onMarkInProgress,
|
||||||
|
onError: { error in
|
||||||
|
print("Error marking in progress: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "complete":
|
||||||
|
CompleteTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onComplete,
|
||||||
|
onError: { error in
|
||||||
|
print("Error completing task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "edit":
|
||||||
|
EditTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onEdit,
|
||||||
|
onError: { error in
|
||||||
|
print("Error editing task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "cancel":
|
||||||
|
CancelTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onCancel,
|
||||||
|
onError: { error in
|
||||||
|
print("Error cancelling task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "uncancel":
|
||||||
|
UncancelTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onUncancel,
|
||||||
|
onError: { error in
|
||||||
|
print("Error restoring task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "archive":
|
||||||
|
ArchiveTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onArchive,
|
||||||
|
onError: { error in
|
||||||
|
print("Error archiving task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "unarchive":
|
||||||
|
UnarchiveTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onUnarchive,
|
||||||
|
onError: { error in
|
||||||
|
print("Error unarchiving task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift
Normal file
81
iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
/// Dynamic task column view that adapts based on the column configuration
|
||||||
|
struct DynamicTaskColumnView: View {
|
||||||
|
let column: TaskColumn
|
||||||
|
let onEditTask: (TaskDetail) -> Void
|
||||||
|
let onCancelTask: (Int32) -> Void
|
||||||
|
let onUncancelTask: (Int32) -> Void
|
||||||
|
let onMarkInProgress: (Int32) -> Void
|
||||||
|
let onCompleteTask: (TaskDetail) -> Void
|
||||||
|
let onArchiveTask: (Int32) -> Void
|
||||||
|
let onUnarchiveTask: (Int32) -> Void
|
||||||
|
|
||||||
|
// Get icon from API response, with fallback
|
||||||
|
private var columnIcon: String {
|
||||||
|
column.icons["ios"] ?? "list.bullet"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var columnColor: Color {
|
||||||
|
Color(hex: column.color) ?? .primary
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
// Header
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: columnIcon)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(columnColor)
|
||||||
|
|
||||||
|
Text(column.displayName)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(columnColor)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(column.count)")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(columnColor)
|
||||||
|
.cornerRadius(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
if column.tasks.isEmpty {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Image(systemName: columnIcon)
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.foregroundColor(columnColor.opacity(0.3))
|
||||||
|
|
||||||
|
Text("No tasks")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.top, 40)
|
||||||
|
} else {
|
||||||
|
ForEach(column.tasks, id: \.id) { task in
|
||||||
|
DynamicTaskCard(
|
||||||
|
task: task,
|
||||||
|
buttonTypes: column.buttonTypes,
|
||||||
|
onEdit: { onEditTask(task) },
|
||||||
|
onCancel: { onCancelTask(task.id) },
|
||||||
|
onUncancel: { onUncancelTask(task.id) },
|
||||||
|
onMarkInProgress: { onMarkInProgress(task.id) },
|
||||||
|
onComplete: { onCompleteTask(task) },
|
||||||
|
onArchive: { onArchiveTask(task.id) },
|
||||||
|
onUnarchive: { onUnarchiveTask(task.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -198,242 +198,6 @@ struct AllTasksView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dynamic task column view that adapts based on the column configuration
|
|
||||||
struct DynamicTaskColumnView: View {
|
|
||||||
let column: TaskColumn
|
|
||||||
let onEditTask: (TaskDetail) -> Void
|
|
||||||
let onCancelTask: (Int32) -> Void
|
|
||||||
let onUncancelTask: (Int32) -> Void
|
|
||||||
let onMarkInProgress: (Int32) -> Void
|
|
||||||
let onCompleteTask: (TaskDetail) -> Void
|
|
||||||
let onArchiveTask: (Int32) -> Void
|
|
||||||
let onUnarchiveTask: (Int32) -> Void
|
|
||||||
|
|
||||||
// Get icon from API response, with fallback
|
|
||||||
private var columnIcon: String {
|
|
||||||
column.icons["ios"] ?? "list.bullet"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var columnColor: Color {
|
|
||||||
Color(hex: column.color) ?? .primary
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
// Header
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Image(systemName: columnIcon)
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(columnColor)
|
|
||||||
|
|
||||||
Text(column.displayName)
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(columnColor)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("\(column.count)")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
.background(columnColor)
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
|
|
||||||
if column.tasks.isEmpty {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Image(systemName: columnIcon)
|
|
||||||
.font(.system(size: 40))
|
|
||||||
.foregroundColor(columnColor.opacity(0.3))
|
|
||||||
|
|
||||||
Text("No tasks")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.top, 40)
|
|
||||||
} else {
|
|
||||||
ForEach(column.tasks, id: \.id) { task in
|
|
||||||
DynamicTaskCard(
|
|
||||||
task: task,
|
|
||||||
buttonTypes: column.buttonTypes,
|
|
||||||
onEdit: { onEditTask(task) },
|
|
||||||
onCancel: { onCancelTask(task.id) },
|
|
||||||
onUncancel: { onUncancelTask(task.id) },
|
|
||||||
onMarkInProgress: { onMarkInProgress(task.id) },
|
|
||||||
onComplete: { onCompleteTask(task) },
|
|
||||||
onArchive: { onArchiveTask(task.id) },
|
|
||||||
onUnarchive: { onUnarchiveTask(task.id) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Task card that dynamically renders buttons based on the column's button types
|
|
||||||
struct DynamicTaskCard: View {
|
|
||||||
let task: TaskDetail
|
|
||||||
let buttonTypes: [String]
|
|
||||||
let onEdit: () -> Void
|
|
||||||
let onCancel: () -> Void
|
|
||||||
let onUncancel: () -> Void
|
|
||||||
let onMarkInProgress: () -> Void
|
|
||||||
let onComplete: () -> Void
|
|
||||||
let onArchive: () -> Void
|
|
||||||
let onUnarchive: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(task.title)
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
if let status = task.status {
|
|
||||||
StatusBadge(status: status.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
PriorityBadge(priority: task.priority.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let description = task.description_, !description.isEmpty {
|
|
||||||
Text(description)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Label(task.frequency.displayName, systemImage: "repeat")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if let due_date = task.dueDate {
|
|
||||||
Label(formatDate(due_date), systemImage: "calendar")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if task.completions.count > 0 {
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundColor(.green)
|
|
||||||
Text("Completions (\(task.completions.count))")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
}
|
|
||||||
|
|
||||||
ForEach(task.completions, id: \.id) { completion in
|
|
||||||
CompletionCardView(completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render buttons based on buttonTypes array
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
|
|
||||||
renderButton(for: buttonType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(16)
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func formatDate(_ dateString: String) -> String {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateFormat = "yyyy-MM-dd"
|
|
||||||
if let date = formatter.date(from: dateString) {
|
|
||||||
formatter.dateStyle = .medium
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
|
||||||
return dateString
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func renderButton(for buttonType: String) -> some View {
|
|
||||||
switch buttonType {
|
|
||||||
case "mark_in_progress":
|
|
||||||
MarkInProgressButton(
|
|
||||||
taskId: task.id,
|
|
||||||
onCompletion: onMarkInProgress,
|
|
||||||
onError: { error in
|
|
||||||
print("Error marking in progress: \(error)")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
case "complete":
|
|
||||||
CompleteTaskButton(
|
|
||||||
taskId: task.id,
|
|
||||||
onCompletion: onComplete,
|
|
||||||
onError: { error in
|
|
||||||
print("Error completing task: \(error)")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
case "edit":
|
|
||||||
EditTaskButton(
|
|
||||||
taskId: task.id,
|
|
||||||
onCompletion: onEdit,
|
|
||||||
onError: { error in
|
|
||||||
print("Error editing task: \(error)")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
case "cancel":
|
|
||||||
CancelTaskButton(
|
|
||||||
taskId: task.id,
|
|
||||||
onCompletion: onCancel,
|
|
||||||
onError: { error in
|
|
||||||
print("Error cancelling task: \(error)")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
case "uncancel":
|
|
||||||
UncancelTaskButton(
|
|
||||||
taskId: task.id,
|
|
||||||
onCompletion: onUncancel,
|
|
||||||
onError: { error in
|
|
||||||
print("Error restoring task: \(error)")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
case "archive":
|
|
||||||
ArchiveTaskButton(
|
|
||||||
taskId: task.id,
|
|
||||||
onCompletion: onArchive,
|
|
||||||
onError: { error in
|
|
||||||
print("Error archiving task: \(error)")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
case "unarchive":
|
|
||||||
UnarchiveTaskButton(
|
|
||||||
taskId: task.id,
|
|
||||||
onCompletion: onUnarchive,
|
|
||||||
onError: { error in
|
|
||||||
print("Error unarchiving task: \(error)")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extension to apply corner radius to specific corners
|
// Extension to apply corner radius to specific corners
|
||||||
extension View {
|
extension View {
|
||||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||||
|
|||||||
@@ -310,72 +310,3 @@ extension KotlinByteArray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image Thumbnail View Component
|
|
||||||
struct ImageThumbnailView: View {
|
|
||||||
let image: UIImage
|
|
||||||
let onRemove: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack(alignment: .topTrailing) {
|
|
||||||
Image(uiImage: image)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFill()
|
|
||||||
.frame(width: 100, height: 100)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
||||||
.overlay {
|
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
||||||
.strokeBorder(.quaternary, lineWidth: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: onRemove) {
|
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.background {
|
|
||||||
Circle()
|
|
||||||
.fill(.black.opacity(0.6))
|
|
||||||
.padding(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.offset(x: 8, y: -8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Camera Picker View Component
|
|
||||||
struct CameraPickerView: UIViewControllerRepresentable {
|
|
||||||
@Environment(\.dismiss) var dismiss
|
|
||||||
let onImageCaptured: (UIImage) -> Void
|
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
|
||||||
let picker = UIImagePickerController()
|
|
||||||
picker.sourceType = .camera
|
|
||||||
picker.delegate = context.coordinator
|
|
||||||
return picker
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
|
||||||
Coordinator(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
|
||||||
let parent: CameraPickerView
|
|
||||||
|
|
||||||
init(_ parent: CameraPickerView) {
|
|
||||||
self.parent = parent
|
|
||||||
}
|
|
||||||
|
|
||||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
|
||||||
if let image = info[.originalImage] as? UIImage {
|
|
||||||
parent.onImageCaptured(image)
|
|
||||||
}
|
|
||||||
parent.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
|
||||||
parent.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user