diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 19a9e7d..c259511 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -157,6 +157,9 @@ 7AB587412C42FFE200FA7B66 /* ApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587402C42FFE200FA7B66 /* ApiServiceProtocol.swift */; }; 7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; }; 7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8D2435D1A000258F61 /* CustomButton.swift */; }; + 7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B462D044A3200B43213 /* GalleryScreen.swift */; }; + 7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */; }; + 7ABD1B4B2D044A7D00B43213 /* GalleryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */; }; 7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7AC355492969652F00889457 /* SwiftEntryKit */; }; 7AC3554C29696A1C00889457 /* MainTabController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554B29696A1C00889457 /* MainTabController.swift */; }; 7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554D29696C4500889457 /* DummyNewController.swift */; }; @@ -428,6 +431,9 @@ 7AB587402C42FFE200FA7B66 /* ApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceProtocol.swift; sourceTree = ""; }; 7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = ""; }; 7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = ""; }; + 7ABD1B462D044A3200B43213 /* GalleryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryScreen.swift; sourceTree = ""; }; + 7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewModel.swift; sourceTree = ""; }; + 7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryCoordinator.swift; sourceTree = ""; }; 7AC3554B29696A1C00889457 /* MainTabController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabController.swift; sourceTree = ""; }; 7AC3554D29696C4500889457 /* DummyNewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyNewController.swift; sourceTree = ""; }; 7AC3554F29696D5A00889457 /* NewNumberController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNumberController.swift; sourceTree = ""; }; @@ -688,6 +694,7 @@ 7A1441632C297E9800E79018 /* Screens */ = { isa = PBXGroup; children = ( + 7ABD1B452D044A0900B43213 /* GalleryScreen */, 7A1E78F42CE9001A0004B740 /* ReportScreen */, 7A43228F2CB2CC5D00085CF6 /* FiltersScreen */, 7A06E0AA2C706550005731AC /* SettingsScreen */, @@ -989,6 +996,16 @@ path = ApiService; sourceTree = ""; }; + 7ABD1B452D044A0900B43213 /* GalleryScreen */ = { + isa = PBXGroup; + children = ( + 7ABD1B462D044A3200B43213 /* GalleryScreen.swift */, + 7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */, + 7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */, + ); + path = GalleryScreen; + sourceTree = ""; + }; 7AC355552969742800889457 /* ACUIKit */ = { isa = PBXGroup; children = ( @@ -1356,6 +1373,7 @@ 7A1E78F62CE900330004B740 /* ReportScreen.swift in Sources */, 7A10226C2C551EC500B84627 /* LocationEditScreen.swift in Sources */, 7A7158072C44085600852088 /* OsagoScreen.swift in Sources */, + 7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */, 7AAAFAD32C4D0FD00050410D /* ACImageSliderView.swift in Sources */, 7A3F07AB24360DC800E59687 /* Dated.swift in Sources */, 7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */, @@ -1399,6 +1417,7 @@ 7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */, 7A1441702C2998B200E79018 /* Formatters.swift in Sources */, 7A4322912CB2CC8A00085CF6 /* FiltersScreen.swift in Sources */, + 7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */, 7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */, 7A71580C2C44453200852088 /* AdsScreen.swift in Sources */, 7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */, @@ -1407,6 +1426,7 @@ 7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */, 7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */, 7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */, + 7ABD1B4B2D044A7D00B43213 /* GalleryCoordinator.swift in Sources */, 7A64AE732469DFB600ABE48E /* DismissAnimationController.swift in Sources */, 7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */, 7A1022702C551EFD00B84627 /* LocationEditCoordinator.swift in Sources */, diff --git a/AutoCat/Screens/GalleryScreen/GalleryCoordinator.swift b/AutoCat/Screens/GalleryScreen/GalleryCoordinator.swift new file mode 100644 index 0000000..fa82f53 --- /dev/null +++ b/AutoCat/Screens/GalleryScreen/GalleryCoordinator.swift @@ -0,0 +1,31 @@ +// +// GalleryCoordinator.swift +// AutoCat +// +// Created by Selim Mustafaev on 07.12.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import UIKit +import SwiftUI +import AutoCatCore + +@MainActor +class GalleryCoordinator: Coordinator { + + let viewController: UINavigationController + let photos: [VehiclePhotoDto] + + init(navController: UINavigationController, photos: [VehiclePhotoDto]) { + + self.viewController = navController + self.photos = photos + } + + func start() async throws { + + let viewModel = GalleryViewModel(photos: photos) + let controller = UIHostingController(rootView: GalleryScreen(viewModel: viewModel)) + viewController.pushViewController(controller, animated: true) + } +} diff --git a/AutoCat/Screens/GalleryScreen/GalleryScreen.swift b/AutoCat/Screens/GalleryScreen/GalleryScreen.swift new file mode 100644 index 0000000..7497d87 --- /dev/null +++ b/AutoCat/Screens/GalleryScreen/GalleryScreen.swift @@ -0,0 +1,72 @@ +// +// GalleryScreen.swift +// AutoCat +// +// Created by Selim Mustafaev on 07.12.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +struct GalleryScreen: View { + + @State var viewModel: GalleryViewModel + + @State var galleryModel: ACImageSliderModel? + + var body: some View { + ScrollView { + LazyVGrid(columns: columns, spacing: 2) { + ForEach(viewModel.photos) { photo in + ZStack { + AsyncImage(url: URL(string: photo.url)) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFill() + default: + ZStack { + Rectangle() + .foregroundStyle(.quaternary) + Image(systemName: "photo") + .font(.system(size: 48)) + .foregroundStyle(.tertiary) + } + } + } + .layoutPriority(-1) + .onTapGesture { + guard let url = URL(string: photo.url) else { + return + } + + galleryModel = ACImageSliderModel( + urls: viewModel.photos.compactMap { URL(string: $0.url) }, + selected: url + ) + } + + Color.clear + } + .aspectRatio(1, contentMode: .fit) + .clipped() + } + } + } + .imageSlider($galleryModel) + } + + var columns: [GridItem] { + [ + GridItem(.flexible(), spacing: 2), + GridItem(.flexible(), spacing: 2), + GridItem(.flexible(), spacing: 2) + ] + } +} + +#Preview { + GalleryScreen(viewModel: .init(photos: [])) +} diff --git a/AutoCat/Screens/GalleryScreen/GalleryViewModel.swift b/AutoCat/Screens/GalleryScreen/GalleryViewModel.swift new file mode 100644 index 0000000..8616bac --- /dev/null +++ b/AutoCat/Screens/GalleryScreen/GalleryViewModel.swift @@ -0,0 +1,21 @@ +// +// GalleryViewModel.swift +// AutoCat +// +// Created by Selim Mustafaev on 07.12.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +@MainActor +@Observable +class GalleryViewModel { + + let photos: [VehiclePhotoDto] + + init(photos: [VehiclePhotoDto]) { + self.photos = photos + } +} diff --git a/AutoCat/Screens/NotesScreen/NotesCoordinator.swift b/AutoCat/Screens/NotesScreen/NotesCoordinator.swift index f036fdf..aa7faee 100644 --- a/AutoCat/Screens/NotesScreen/NotesCoordinator.swift +++ b/AutoCat/Screens/NotesScreen/NotesCoordinator.swift @@ -22,10 +22,12 @@ class NotesCoordinator: Coordinator { self.vehicle = vehicle } - func start() async throws { + func start() async throws -> VehicleDto { let viewModel = NotesViewModel(vehicle: vehicle) - let controller = UIHostingController(rootView: NotesScreen(viewModel: viewModel)) + let controller = CustomHostingController(rootView: NotesScreen(viewModel: viewModel)) viewController?.pushViewController(controller, animated: true) + await controller.waitForDisappear() + return viewModel.vehicle } } diff --git a/AutoCat/Screens/ReportScreen/ReportCoordinator.swift b/AutoCat/Screens/ReportScreen/ReportCoordinator.swift index a9f16ec..64db1a3 100644 --- a/AutoCat/Screens/ReportScreen/ReportCoordinator.swift +++ b/AutoCat/Screens/ReportScreen/ReportCoordinator.swift @@ -17,7 +17,7 @@ class ReportCoordinator: Coordinator { let vehicle: VehicleDto let isPersistent: Bool - var navController: UINavigationController? + weak var navController: UINavigationController? init(splitController: UISplitViewController?, vehicle: VehicleDto, isPersistent: Bool) { @@ -34,26 +34,26 @@ class ReportCoordinator: Coordinator { let viewModel = ReportViewModel(vehicle: vehicle, isPersistent: isPersistent) viewModel.coordinator = self let controller = UIHostingController(rootView: ReportScreen(viewModel: viewModel)) - navController = UINavigationController(rootViewController: controller) + //navController = UINavigationController(rootViewController: controller) + viewController?.showDetailViewController(controller, sender: self) + navController = controller.navigationController + return } if let navController { - navController.popToRootViewController(animated: true) - let report = navController.viewControllers.first as? ReportController - report?.number = vehicle.getNumber() +// navController.popToRootViewController(animated: true) +// let report = navController.viewControllers.first as? ReportController +// report?.number = vehicle.getNumber() viewController?.showDetailViewController(navController, sender: self) } } - func openEvents(vehicle: VehicleDto) { + func openEvents(vehicle: VehicleDto, onUpdate: @escaping (VehicleDto) -> Void) { let sb = UIStoryboard(name: "Main", bundle: nil) let controller = sb.instantiateViewController(identifier: "EventsController") as EventsController - controller.vehicle = self.vehicle - controller.vehicleUpdated = { vehicle in - // TODO: Propagate vehicle update upwards - //self.vehicle = vehicle - } + controller.vehicle = vehicle + controller.vehicleUpdated = onUpdate navController?.pushViewController(controller, animated: true) } @@ -76,4 +76,35 @@ class ReportCoordinator: Coordinator { try? await coordiantor.start() } } + + func openNotes(vehicle: VehicleDto) async -> VehicleDto? { + guard let navController else { + return nil + } + + let coordinator = NotesCoordinator(navController: navController, vehicle: vehicle) + return try? await coordinator.start() + } + + func openAds(_ ads: [VehicleAdDto]) { + guard let navController else { + return + } + + Task { + let coordinator = AdsCoordinator(navController: navController, ads: ads) + try? await coordinator.start() + } + } + + func openPhotos(_ photos: [VehiclePhotoDto]) { + guard let navController else { + return + } + + Task { + let coordinator = GalleryCoordinator(navController: navController, photos: photos) + try? await coordinator.start() + } + } } diff --git a/AutoCat/Screens/ReportScreen/ReportScreen.swift b/AutoCat/Screens/ReportScreen/ReportScreen.swift index 2c92046..ca9a127 100644 --- a/AutoCat/Screens/ReportScreen/ReportScreen.swift +++ b/AutoCat/Screens/ReportScreen/ReportScreen.swift @@ -53,17 +53,23 @@ struct ReportScreen: View { Section("History") { LabeledContent("Events", value: String(viewModel.vehicle.events.count)) - .navigationLink(onTap: viewModel.openEvents) + .navigationLink(isActive: !viewModel.vehicle.events.isEmpty, + onTap: viewModel.openEvents) LabeledContent("OSAGO", value: String(viewModel.vehicle.osagoContracts.count)) - .navigationLink(onTap: viewModel.openOsago) + .navigationLink(isActive: !viewModel.vehicle.osagoContracts.isEmpty, + onTap: viewModel.openOsago) LabeledContent("Owners", value: String(viewModel.vehicle.ownershipPeriods.count)) - .navigationLink(onTap: viewModel.openOwners) + .navigationLink(isActive: !viewModel.vehicle.ownershipPeriods.isEmpty, + onTap: viewModel.openOwners) LabeledContent("Photos", value: String(viewModel.vehicle.photos.count)) - .navigationLink(onTap: viewModel.openPhotoGallery) + .navigationLink(isActive: !viewModel.vehicle.photos.isEmpty, + onTap: viewModel.openPhotoGallery) LabeledContent("Ads", value: String(viewModel.vehicle.ads.count)) - .navigationLink(onTap: viewModel.openAds) + .navigationLink(isActive: !viewModel.vehicle.ads.isEmpty, + onTap: viewModel.openAds) LabeledContent("Notes", value: String(viewModel.vehicle.notes.count)) - .navigationLink(onTap: viewModel.openNotes) + .navigationLink(isActive: !viewModel.vehicle.notes.isEmpty, + onTap: viewModel.openNotes) } if viewModel.showDebugInfo { @@ -86,6 +92,11 @@ struct ReportScreen: View { Task { await viewModel.onAppear() } } .hud($viewModel.hud) + .toolbar { + if let link = viewModel.shareLink { + ShareLink(item: link) + } + } } @ViewBuilder diff --git a/AutoCat/Screens/ReportScreen/ReportViewModel.swift b/AutoCat/Screens/ReportScreen/ReportViewModel.swift index b825e08..74ad308 100644 --- a/AutoCat/Screens/ReportScreen/ReportViewModel.swift +++ b/AutoCat/Screens/ReportScreen/ReportViewModel.swift @@ -47,6 +47,14 @@ class ReportViewModel: ACHudContainer { settings.showDebugInfo } + var shareLink: URL? { + guard let jwt = try? JWT.generate(for: vehicle.getNumber()) else { + return nil + } + + return URL(string: Constants.reportLinkBaseURL + "?token=" + jwt) + } + init(vehicle: VehicleDto, isPersistent: Bool) { self.vehicle = vehicle self.isPersistent = isPersistent @@ -84,7 +92,9 @@ class ReportViewModel: ACHudContainer { // MARK: Open detail screens func openEvents() { - coordinator?.openEvents(vehicle: vehicle) + coordinator?.openEvents(vehicle: vehicle) { [weak self] vehicle in + self?.vehicle = vehicle + } } func openOsago() { @@ -96,14 +106,18 @@ class ReportViewModel: ACHudContainer { } func openPhotoGallery() { - + coordinator?.openPhotos(vehicle.photos) } func openNotes() { - + Task { + if let vehicle = await coordinator?.openNotes(vehicle: vehicle) { + self.vehicle = vehicle + } + } } func openAds() { - + coordinator?.openAds(vehicle.ads) } } diff --git a/AutoCat/SwiftUI/NavigationLink.swift b/AutoCat/SwiftUI/NavigationLink.swift index d29dad4..9137a76 100644 --- a/AutoCat/SwiftUI/NavigationLink.swift +++ b/AutoCat/SwiftUI/NavigationLink.swift @@ -11,10 +11,14 @@ import SwiftUI struct NavigationLinkModifier: ViewModifier { var onTap: (() -> Void)? + var isActive: Bool func body(content: Content) -> some View { - if let onTap { + if !isActive { + content + .foregroundStyle(.secondary) + } else if let onTap { HStack(spacing: 0) { content Spacer() @@ -33,7 +37,7 @@ struct NavigationLinkModifier: ViewModifier { extension View { - func navigationLink(onTap: (() -> Void)?) -> some View { - modifier(NavigationLinkModifier(onTap: onTap)) + func navigationLink(isActive: Bool = true, onTap: (() -> Void)?) -> some View { + modifier(NavigationLinkModifier(onTap: onTap, isActive: isActive)) } } diff --git a/AutoCatCore/Models/DTO/VehiclePhotoDto.swift b/AutoCatCore/Models/DTO/VehiclePhotoDto.swift index b15895c..c0effd9 100644 --- a/AutoCatCore/Models/DTO/VehiclePhotoDto.swift +++ b/AutoCatCore/Models/DTO/VehiclePhotoDto.swift @@ -8,13 +8,19 @@ import Foundation -public struct VehiclePhotoDto: Decodable, Sendable, Equatable { +public struct VehiclePhotoDto: Decodable, Sendable, Equatable, Identifiable { + public let id = UUID() public var brand: String? public var model: String? public var date: TimeInterval = 0 public var url: String = "" + enum CodingKeys: String, CodingKey { + + case brand, model, date, url + } + public var description: String { let formatter = DateFormatter() formatter.timeZone = TimeZone(identifier:"GMT") @@ -24,4 +30,15 @@ public struct VehiclePhotoDto: Decodable, Sendable, Equatable { let dateStr = formatter.string(from: date) return "\(self.brand ?? "") \(self.model ?? "") (\(dateStr))" } + + public init(brand: String? = nil, + model: String? = nil, + date: TimeInterval, + url: String) { + + self.brand = brand + self.model = model + self.date = date + self.url = url + } }