Adding navigation from new report to all detail screens. Adding sharing report link.
This commit is contained in:
parent
97e35ac785
commit
37b94aca62
@ -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 = "<group>"; };
|
||||
7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
|
||||
7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = "<group>"; };
|
||||
7ABD1B462D044A3200B43213 /* GalleryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryScreen.swift; sourceTree = "<group>"; };
|
||||
7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewModel.swift; sourceTree = "<group>"; };
|
||||
7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryCoordinator.swift; sourceTree = "<group>"; };
|
||||
7AC3554B29696A1C00889457 /* MainTabController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabController.swift; sourceTree = "<group>"; };
|
||||
7AC3554D29696C4500889457 /* DummyNewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyNewController.swift; sourceTree = "<group>"; };
|
||||
7AC3554F29696D5A00889457 /* NewNumberController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNumberController.swift; sourceTree = "<group>"; };
|
||||
@ -688,6 +694,7 @@
|
||||
7A1441632C297E9800E79018 /* Screens */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7ABD1B452D044A0900B43213 /* GalleryScreen */,
|
||||
7A1E78F42CE9001A0004B740 /* ReportScreen */,
|
||||
7A43228F2CB2CC5D00085CF6 /* FiltersScreen */,
|
||||
7A06E0AA2C706550005731AC /* SettingsScreen */,
|
||||
@ -989,6 +996,16 @@
|
||||
path = ApiService;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7ABD1B452D044A0900B43213 /* GalleryScreen */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7ABD1B462D044A3200B43213 /* GalleryScreen.swift */,
|
||||
7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */,
|
||||
7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */,
|
||||
);
|
||||
path = GalleryScreen;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
||||
31
AutoCat/Screens/GalleryScreen/GalleryCoordinator.swift
Normal file
31
AutoCat/Screens/GalleryScreen/GalleryCoordinator.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
72
AutoCat/Screens/GalleryScreen/GalleryScreen.swift
Normal file
72
AutoCat/Screens/GalleryScreen/GalleryScreen.swift
Normal file
@ -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: []))
|
||||
}
|
||||
21
AutoCat/Screens/GalleryScreen/GalleryViewModel.swift
Normal file
21
AutoCat/Screens/GalleryScreen/GalleryViewModel.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -47,6 +47,14 @@ class ReportViewModel: ACHudContainer {
|
||||
settings.showDebugInfo
|
||||
}
|
||||
|
||||
var shareLink: URL? {
|
||||
guard let jwt = try? JWT<EmptyPayload>.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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user