Refactoring whole project to support Swift 6. RxSwift entirely removed. Starting migration of some screens to SwiftUI (preferably ones built with Eureka)
This commit is contained in:
parent
443d924091
commit
01bd7b2f87
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"originHash" : "ae480e95b5199c1834610abd8aa26a821694aa5167d41c0aceddc8513e267012",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "eureka",
|
"identity" : "eureka",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/xmartlabs/Eureka",
|
"location" : "https://github.com/xmartlabs/Eureka",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "b6e35acf77a5551070afa6248935ec68e71f22af",
|
"revision" : "028ef8e3191a256b8f6b8bb6b9496efcb0762dbc",
|
||||||
"version" : "5.4.0"
|
"version" : "5.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -23,8 +24,17 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/onevcat/Kingfisher",
|
"location" : "https://github.com/onevcat/Kingfisher",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "1a0c2df04b31ed7aa318354f3583faea24f006fc",
|
"branch" : "8.0.0-alpha.1",
|
||||||
"version" : "5.15.8"
|
"revision" : "bb4e6ecf6c7a221dfc51c8e69f04fd3757fc519a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "mockable",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/Kolos65/Mockable",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "81ccaead99a3c038c09345caa2888ae74b644ee9",
|
||||||
|
"version" : "0.0.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -41,8 +51,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/realm/realm-core.git",
|
"location" : "https://github.com/realm/realm-core.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "dd91f5f967c4ae89c37e24ab2a0315c31106648f",
|
"revision" : "f3d7ae5f9f31d90b327a64536bb7801cc69fd85b",
|
||||||
"version" : "13.6.0"
|
"version" : "14.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -50,17 +60,17 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/realm/realm-swift.git",
|
"location" : "https://github.com/realm/realm-swift.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "8ac6fe1aa5d0fb0100062d80863416a4d70de8ca",
|
"revision" : "4c4413abd0cd2221f59318f800960fe5bddc1494",
|
||||||
"version" : "10.37.0"
|
"version" : "10.51.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identity" : "rxswift",
|
"identity" : "swift-syntax",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/ReactiveX/RxSwift.git",
|
"location" : "https://github.com/apple/swift-syntax.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8",
|
"revision" : "303e5c5c36d6a558407d364878df131c3546fad8",
|
||||||
"version" : "6.5.0"
|
"version" : "510.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -80,7 +90,16 @@
|
|||||||
"revision" : "5ad36cccf0c4b9fea32f4e9b17a8e38f07563ef0",
|
"revision" : "5ad36cccf0c4b9fea32f4e9b17a8e38f07563ef0",
|
||||||
"version" : "2.0.0"
|
"version" : "2.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftlocation",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/malcommac/SwiftLocation.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "010073e62cea4daefea61042a51b8619d23cdc35",
|
||||||
|
"version" : "6.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 2
|
"version" : 3
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
skipped = "NO">
|
skipped = "NO">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "7AF6D1DC2677A7E00086EA64"
|
BlueprintIdentifier = "7AB587212C42D27F00FA7B66"
|
||||||
BuildableName = "AutoCatTests.xctest"
|
BuildableName = "AutoCatTests.xctest"
|
||||||
BlueprintName = "AutoCatTests"
|
BlueprintName = "AutoCatTests"
|
||||||
ReferencedContainer = "container:AutoCat.xcodeproj">
|
ReferencedContainer = "container:AutoCat.xcodeproj">
|
||||||
@ -42,7 +42,7 @@
|
|||||||
skipped = "NO">
|
skipped = "NO">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "7AF6D1F62677C03B0086EA64"
|
BlueprintIdentifier = "7A2E6FA22C42B3AD00C40DA7"
|
||||||
BuildableName = "AutoCatCoreTests.xctest"
|
BuildableName = "AutoCatCoreTests.xctest"
|
||||||
BlueprintName = "AutoCatCoreTests"
|
BlueprintName = "AutoCatCoreTests"
|
||||||
ReferencedContainer = "container:AutoCat.xcodeproj">
|
ReferencedContainer = "container:AutoCat.xcodeproj">
|
||||||
|
|||||||
@ -20,16 +20,16 @@
|
|||||||
<BreakpointProxy
|
<BreakpointProxy
|
||||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
<BreakpointContent
|
<BreakpointContent
|
||||||
uuid = "2786565A-9610-4232-920E-0763816C4DBF"
|
uuid = "E881DB7D-2F45-4EF0-A237-92D7E40239E7"
|
||||||
shouldBeEnabled = "Yes"
|
shouldBeEnabled = "Yes"
|
||||||
ignoreCount = "0"
|
ignoreCount = "0"
|
||||||
continueAfterRunningActions = "No"
|
continueAfterRunningActions = "No"
|
||||||
filePath = "../../../Library/Developer/Xcode/DerivedData/AutoCat-fhilwnlnsrpirleiajogdcyhyyey/SourcePackages/checkouts/Eureka/Source/Rows/DateInlineRow.swift"
|
filePath = "AutoCatCore/Services/ApiService/ApiServiceTestMock.swift"
|
||||||
startingColumnNumber = "9223372036854775807"
|
startingColumnNumber = "9223372036854775807"
|
||||||
endingColumnNumber = "9223372036854775807"
|
endingColumnNumber = "9223372036854775807"
|
||||||
startingLineNumber = "37"
|
startingLineNumber = "77"
|
||||||
endingLineNumber = "37"
|
endingLineNumber = "77"
|
||||||
landmarkName = "configurePickerStyle(_:_:)"
|
landmarkName = "add(notes:to:)"
|
||||||
landmarkType = "7">
|
landmarkType = "7">
|
||||||
</BreakpointContent>
|
</BreakpointContent>
|
||||||
</BreakpointProxy>
|
</BreakpointProxy>
|
||||||
|
|||||||
@ -13,7 +13,7 @@ extension UIGestureRecognizer {
|
|||||||
typealias Action = ((UIGestureRecognizer) -> ())
|
typealias Action = ((UIGestureRecognizer) -> ())
|
||||||
|
|
||||||
private struct Keys {
|
private struct Keys {
|
||||||
static var actionKey = "ActionKey"
|
@MainActor static var actionKey = "ActionKey"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var block: Action? {
|
private var block: Action? {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ extension UISegmentedControl {
|
|||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func onValueChanged(_ closure: @escaping (Int) -> Void) -> UISegmentedControl {
|
func onValueChanged(_ closure: @MainActor @escaping (Int) -> Void) -> UISegmentedControl {
|
||||||
addActionImpl(for: .valueChanged) { [weak self] in
|
addActionImpl(for: .valueChanged) { [weak self] in
|
||||||
guard let index = self?.selectedSegmentIndex else {
|
guard let index = self?.selectedSegmentIndex else {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class ACButton: UIButton {
|
|||||||
|
|
||||||
private var style: ACButtonStyle = .generic
|
private var style: ACButtonStyle = .generic
|
||||||
|
|
||||||
convenience init(style: ACButtonStyle = .roundedBlue, title: String, onTap: @escaping () -> Void) {
|
convenience init(style: ACButtonStyle = .roundedBlue, title: String, onTap: @MainActor @escaping () -> Void) {
|
||||||
self.init()
|
self.init()
|
||||||
self.style(style)
|
self.style(style)
|
||||||
self.onTap(onTap)
|
self.onTap(onTap)
|
||||||
|
|||||||
@ -1,24 +1,17 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
import RxSwift
|
|
||||||
import RxCocoa
|
|
||||||
import os.log
|
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
extension OSLog {
|
|
||||||
static let startup = OSLog(subsystem: "pro.aliencat.autocat.startup", category: "startup")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum QuickAction {
|
enum QuickAction {
|
||||||
case none
|
case none
|
||||||
case check
|
case check
|
||||||
case checkNumber(String, VehicleEvent?)
|
case checkNumber(String, VehicleEventDto?)
|
||||||
case addVoiceRecord
|
case addVoiceRecord
|
||||||
case openReport(String)
|
case openReport(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@UIApplicationMain
|
@main
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
var quickAction: QuickAction = .none
|
var quickAction: QuickAction = .none
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pme-aR-UNJ">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23086.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pme-aR-UNJ">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="dark"/>
|
<device id="retina4_7" orientation="portrait" appearance="dark"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23076"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
@ -42,21 +42,6 @@
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1095" y="965"/>
|
<point key="canvasLocation" x="1095" y="965"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Owners Controller-->
|
|
||||||
<scene sceneID="0bv-cp-2uj">
|
|
||||||
<objects>
|
|
||||||
<viewController storyboardIdentifier="OwnersController" id="73d-bt-c62" customClass="OwnersController" customModule="AutoCat" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="Eae-Lq-vht">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<viewLayoutGuide key="safeArea" id="bmk-bz-ZBr"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
</view>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="URC-NW-y2j" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="1881" y="965"/>
|
|
||||||
</scene>
|
|
||||||
<!--Osago Add Controller-->
|
<!--Osago Add Controller-->
|
||||||
<scene sceneID="hrL-nC-qbc">
|
<scene sceneID="hrL-nC-qbc">
|
||||||
<objects>
|
<objects>
|
||||||
@ -72,94 +57,6 @@
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1094" y="2353"/>
|
<point key="canvasLocation" x="1094" y="2353"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Notes Controller-->
|
|
||||||
<scene sceneID="MkZ-J0-OzU">
|
|
||||||
<objects>
|
|
||||||
<viewController storyboardIdentifier="NotesController" id="Z9b-nr-Xre" customClass="NotesController" customModule="AutoCat" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="yXB-Zh-8mU">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="P8L-hv-BMJ">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<prototypes>
|
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="VehicleNoteCell" id="J6y-ZM-MCf" customClass="VehicleNoteCell" customModule="AutoCat" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="0.0" y="50" width="375" height="67"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="J6y-ZM-MCf" id="wBh-vB-ORX">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="67"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="wnx-I3-WKd">
|
|
||||||
<rect key="frame" x="16" y="11" width="343" height="45"/>
|
|
||||||
<subviews>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Some note about vehicle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="P90-i3-hFg">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="date" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6jy-cJ-rbF">
|
|
||||||
<rect key="frame" x="0.0" y="28.5" width="343" height="16.5"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
</subviews>
|
|
||||||
</stackView>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="trailingMargin" secondItem="wnx-I3-WKd" secondAttribute="trailing" id="6sm-CE-5i4"/>
|
|
||||||
<constraint firstItem="wnx-I3-WKd" firstAttribute="leading" secondItem="wBh-vB-ORX" secondAttribute="leadingMargin" id="GUA-uS-9Lc"/>
|
|
||||||
<constraint firstAttribute="bottomMargin" secondItem="wnx-I3-WKd" secondAttribute="bottom" id="Vc6-PA-ApE"/>
|
|
||||||
<constraint firstItem="wnx-I3-WKd" firstAttribute="top" secondItem="wBh-vB-ORX" secondAttribute="topMargin" id="yMv-Mu-URo"/>
|
|
||||||
</constraints>
|
|
||||||
</tableViewCellContentView>
|
|
||||||
<connections>
|
|
||||||
<outlet property="date" destination="6jy-cJ-rbF" id="3rT-KG-jbc"/>
|
|
||||||
<outlet property="noteText" destination="P90-i3-hFg" id="L7m-DE-JOe"/>
|
|
||||||
</connections>
|
|
||||||
</tableViewCell>
|
|
||||||
</prototypes>
|
|
||||||
<connections>
|
|
||||||
<outlet property="dataSource" destination="Z9b-nr-Xre" id="5uP-Qe-g06"/>
|
|
||||||
<outlet property="delegate" destination="Z9b-nr-Xre" id="c45-KP-Wem"/>
|
|
||||||
</connections>
|
|
||||||
</tableView>
|
|
||||||
</subviews>
|
|
||||||
<viewLayoutGuide key="safeArea" id="lTb-Q2-gfw"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="P8L-hv-BMJ" secondAttribute="bottom" id="Rmj-6p-gt5"/>
|
|
||||||
<constraint firstItem="P8L-hv-BMJ" firstAttribute="leading" secondItem="lTb-Q2-gfw" secondAttribute="leading" id="UYg-XA-NX0"/>
|
|
||||||
<constraint firstItem="P8L-hv-BMJ" firstAttribute="top" secondItem="lTb-Q2-gfw" secondAttribute="top" id="fgf-Du-DxG"/>
|
|
||||||
<constraint firstItem="P8L-hv-BMJ" firstAttribute="trailing" secondItem="lTb-Q2-gfw" secondAttribute="trailing" id="qh5-eJ-MUc"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<connections>
|
|
||||||
<outlet property="notesTable" destination="P8L-hv-BMJ" id="97A-qf-on9"/>
|
|
||||||
</connections>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="InU-Kk-GEn" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="1880.8" y="2352.7736131934034"/>
|
|
||||||
</scene>
|
|
||||||
<!--Osago Controller-->
|
|
||||||
<scene sceneID="LgB-gR-z4l">
|
|
||||||
<objects>
|
|
||||||
<viewController storyboardIdentifier="OsagoController" id="JW7-4w-Lcx" customClass="OsagoController" customModule="AutoCat" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="OhQ-CO-BRq">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<viewLayoutGuide key="safeArea" id="ujd-gp-iD5"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
</view>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="vDX-Gn-acM" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="1094" y="1660"/>
|
|
||||||
</scene>
|
|
||||||
<!--Events-->
|
<!--Events-->
|
||||||
<scene sceneID="pPZ-gs-kHF">
|
<scene sceneID="pPZ-gs-kHF">
|
||||||
<objects>
|
<objects>
|
||||||
@ -169,21 +66,21 @@
|
|||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="ytQ-Th-luv">
|
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="ytQ-Th-luv">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<prototypes>
|
<prototypes>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="EventCell" id="QIb-Hv-tvk" customClass="EventCell" customModule="AutoCat" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="EventCell" id="QIb-Hv-tvk" customClass="EventCell" customModule="AutoCat" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="50" width="375" height="432"/>
|
<rect key="frame" x="0.0" y="50" width="375" height="434"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="QIb-Hv-tvk" id="Ypt-ch-fGT">
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="QIb-Hv-tvk" id="Ypt-ch-fGT">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="432"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="434"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="HP8-oO-yhP">
|
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="HP8-oO-yhP">
|
||||||
<rect key="frame" x="16" y="8" width="343" height="416"/>
|
<rect key="frame" x="16" y="8" width="343" height="418"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="k4Z-KM-byE">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="k4Z-KM-byE">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="335" height="416"/>
|
<rect key="frame" x="0.0" y="0.0" width="335" height="418"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xcQ-Wz-gJ0">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xcQ-Wz-gJ0">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="335" height="201"/>
|
<rect key="frame" x="0.0" y="0.0" width="335" height="201"/>
|
||||||
@ -192,7 +89,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1tQ-zM-6T9">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1tQ-zM-6T9">
|
||||||
<rect key="frame" x="0.0" y="209" width="335" height="207"/>
|
<rect key="frame" x="0.0" y="209" width="335" height="209"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
@ -200,7 +97,7 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="750" verticalHuggingPriority="251" image="person" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="CFI-xa-eLs">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="750" verticalHuggingPriority="251" image="person" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="CFI-xa-eLs">
|
||||||
<rect key="frame" x="343" y="1.5" width="0.0" height="413.5"/>
|
<rect key="frame" x="343" y="1" width="0.0" height="415.5"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
@ -274,7 +171,7 @@
|
|||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="dB3-iP-QRo">
|
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="dB3-iP-QRo">
|
||||||
<rect key="frame" x="0.0" y="44" width="375" height="574"/>
|
<rect key="frame" x="0.0" y="64" width="375" height="554"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<prototypes>
|
<prototypes>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="VehicleCell" id="VEP-QD-i6y" customClass="VehicleCell" customModule="AutoCat" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="VehicleCell" id="VEP-QD-i6y" customClass="VehicleCell" customModule="AutoCat" customModuleProvider="target">
|
||||||
@ -297,7 +194,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="exclamationmark.arrow.triangle.2.circlepath" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="Qo7-51-ou9">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="exclamationmark.arrow.triangle.2.circlepath" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="Qo7-51-ou9">
|
||||||
<rect key="frame" x="294.5" y="0.5" width="20" height="19"/>
|
<rect key="frame" x="294" y="0.5" width="21" height="19"/>
|
||||||
<color key="tintColor" systemColor="systemOrangeColor"/>
|
<color key="tintColor" systemColor="systemOrangeColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="20" id="Pct-H5-e3e"/>
|
<constraint firstAttribute="width" constant="20" id="Pct-H5-e3e"/>
|
||||||
@ -598,7 +495,7 @@
|
|||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="onDrag" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="JKr-UE-x8f">
|
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="onDrag" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="JKr-UE-x8f">
|
||||||
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
|
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<prototypes>
|
<prototypes>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="VehicleCell" id="3ON-lr-UlV" customClass="VehicleCell" customModule="AutoCat" customModuleProvider="target">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="VehicleCell" id="3ON-lr-UlV" customClass="VehicleCell" customModule="AutoCat" customModuleProvider="target">
|
||||||
@ -621,7 +518,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="exclamationmark.arrow.triangle.2.circlepath" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="pdg-uR-pUn">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="exclamationmark.arrow.triangle.2.circlepath" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="pdg-uR-pUn">
|
||||||
<rect key="frame" x="294.5" y="0.5" width="20" height="19"/>
|
<rect key="frame" x="294" y="0.5" width="21" height="19"/>
|
||||||
<color key="tintColor" systemColor="systemOrangeColor"/>
|
<color key="tintColor" systemColor="systemOrangeColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="20" id="9HI-9d-T5A"/>
|
<constraint firstAttribute="height" constant="20" id="9HI-9d-T5A"/>
|
||||||
@ -734,7 +631,7 @@
|
|||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<wkWebView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0QS-UT-hbi">
|
<wkWebView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0QS-UT-hbi">
|
||||||
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
|
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
|
||||||
<color key="backgroundColor" red="0.36078431370000003" green="0.38823529410000002" blue="0.4039215686" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="0.36078431370000003" green="0.38823529410000002" blue="0.4039215686" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<wkWebViewConfiguration key="configuration">
|
<wkWebViewConfiguration key="configuration">
|
||||||
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
|
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
|
||||||
@ -742,7 +639,7 @@
|
|||||||
</wkWebViewConfiguration>
|
</wkWebViewConfiguration>
|
||||||
</wkWebView>
|
</wkWebView>
|
||||||
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="AMg-mc-MMG">
|
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="AMg-mc-MMG">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
|
||||||
<items>
|
<items>
|
||||||
<navigationItem id="fZb-kM-9an">
|
<navigationItem id="fZb-kM-9an">
|
||||||
<barButtonItem key="rightBarButtonItem" title="Close" id="ZHH-OZ-vHc">
|
<barButtonItem key="rightBarButtonItem" title="Close" id="ZHH-OZ-vHc">
|
||||||
@ -865,19 +762,6 @@
|
|||||||
<action selector="signupTapped:" destination="pme-aR-UNJ" eventType="touchUpInside" id="upt-yE-Xro"/>
|
<action selector="signupTapped:" destination="pme-aR-UNJ" eventType="touchUpInside" id="upt-yE-Xro"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="or" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SXb-1Q-TxY">
|
|
||||||
<rect key="frame" x="0.0" y="196" width="262.5" height="0.0"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nId-Ov-fVe" customClass="ASAuthorizationAppleIDButton">
|
|
||||||
<rect key="frame" x="0.0" y="196" width="262.5" height="0.0"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="appleSignInTapped:" destination="pme-aR-UNJ" eventType="touchUpInside" id="LrP-Rx-JeO"/>
|
|
||||||
</connections>
|
|
||||||
</view>
|
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
@ -890,7 +774,6 @@
|
|||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="appleSignIn" destination="nId-Ov-fVe" id="ifS-g1-O3I"/>
|
|
||||||
<outlet property="login" destination="ltG-B1-UBj" id="Fc0-Cd-BKA"/>
|
<outlet property="login" destination="ltG-B1-UBj" id="Fc0-Cd-BKA"/>
|
||||||
<outlet property="password" destination="G1p-Hz-8yn" id="8VI-cA-YrJ"/>
|
<outlet property="password" destination="G1p-Hz-8yn" id="8VI-cA-YrJ"/>
|
||||||
<outlet property="signup" destination="hRD-Ha-MrP" id="VCG-25-bHL"/>
|
<outlet property="signup" destination="hRD-Ha-MrP" id="VCG-25-bHL"/>
|
||||||
@ -975,7 +858,7 @@
|
|||||||
<tabBarItem key="tabBarItem" title="History" image="clock.arrow.circlepath" catalog="system" id="QJd-35-4OB"/>
|
<tabBarItem key="tabBarItem" title="History" image="clock.arrow.circlepath" catalog="system" id="QJd-35-4OB"/>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="AAc-4d-GNh">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="AAc-4d-GNh">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@ -994,7 +877,7 @@
|
|||||||
<tabBarItem key="tabBarItem" title="Search" image="search" landscapeImage="search-compact" id="gDG-z8-R0t"/>
|
<tabBarItem key="tabBarItem" title="Search" image="search" landscapeImage="search-compact" id="gDG-z8-R0t"/>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="vdY-9n-hjX">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="vdY-9n-hjX">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@ -1012,7 +895,7 @@
|
|||||||
<navigationController storyboardIdentifier="ReportNavController" automaticallyAdjustsScrollViewInsets="NO" id="Km4-b6-SGW" sceneMemberID="viewController">
|
<navigationController storyboardIdentifier="ReportNavController" automaticallyAdjustsScrollViewInsets="NO" id="Km4-b6-SGW" sceneMemberID="viewController">
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="JaO-tp-k6N">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="JaO-tp-k6N">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@ -1031,7 +914,7 @@
|
|||||||
<tabBarItem key="tabBarItem" title="Records" image="record" landscapeImage="record-compact" id="lxF-EY-z8V"/>
|
<tabBarItem key="tabBarItem" title="Records" image="record" landscapeImage="record-compact" id="lxF-EY-z8V"/>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="8YG-pw-LE7">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="8YG-pw-LE7">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@ -1049,7 +932,7 @@
|
|||||||
<navigationController storyboardIdentifier="GlobalEventsNavigation" automaticallyAdjustsScrollViewInsets="NO" id="HWa-Ea-ZKD" sceneMemberID="viewController">
|
<navigationController storyboardIdentifier="GlobalEventsNavigation" automaticallyAdjustsScrollViewInsets="NO" id="HWa-Ea-ZKD" sceneMemberID="viewController">
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="REm-5j-xeL">
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="REm-5j-xeL">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<nil name="viewControllers"/>
|
<nil name="viewControllers"/>
|
||||||
@ -1063,13 +946,13 @@
|
|||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="clock.arrow.circlepath" catalog="system" width="128" height="112"/>
|
<image name="clock.arrow.circlepath" catalog="system" width="128" height="119"/>
|
||||||
<image name="doc.on.doc" catalog="system" width="116" height="128"/>
|
<image name="doc.on.doc" catalog="system" width="116" height="128"/>
|
||||||
<image name="exclamationmark.arrow.triangle.2.circlepath" catalog="system" width="128" height="104"/>
|
<image name="exclamationmark.arrow.triangle.2.circlepath" catalog="system" width="128" height="117"/>
|
||||||
<image name="line.horizontal.3.decrease" catalog="system" width="128" height="73"/>
|
<image name="line.horizontal.3.decrease" catalog="system" width="128" height="73"/>
|
||||||
<image name="map" catalog="system" width="128" height="112"/>
|
<image name="map" catalog="system" width="128" height="112"/>
|
||||||
<image name="person" catalog="system" width="128" height="121"/>
|
<image name="person" catalog="system" width="128" height="121"/>
|
||||||
<image name="play.fill" catalog="system" width="117" height="128"/>
|
<image name="play.fill" catalog="system" width="120" height="128"/>
|
||||||
<image name="plus" catalog="system" width="128" height="113"/>
|
<image name="plus" catalog="system" width="128" height="113"/>
|
||||||
<image name="record" width="31" height="31"/>
|
<image name="record" width="31" height="31"/>
|
||||||
<image name="record-compact" width="23" height="23"/>
|
<image name="record-compact" width="23" height="23"/>
|
||||||
@ -1077,7 +960,7 @@
|
|||||||
<image name="search-compact" width="17" height="17"/>
|
<image name="search-compact" width="17" height="17"/>
|
||||||
<image name="settings" width="25" height="25"/>
|
<image name="settings" width="25" height="25"/>
|
||||||
<image name="settings-compact" width="18" height="18"/>
|
<image name="settings-compact" width="18" height="18"/>
|
||||||
<image name="square.and.arrow.up" catalog="system" width="115" height="128"/>
|
<image name="square.and.arrow.up" catalog="system" width="110" height="128"/>
|
||||||
<image name="text.bubble" catalog="system" width="128" height="110"/>
|
<image name="text.bubble" catalog="system" width="128" height="110"/>
|
||||||
<systemColor name="secondaryLabelColor">
|
<systemColor name="secondaryLabelColor">
|
||||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
@ -13,10 +12,8 @@ class AudioRecordCell: UITableViewCell, ConfigurableCell {
|
|||||||
|
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
let componentsFormatter = DateComponentsFormatter()
|
let componentsFormatter = DateComponentsFormatter()
|
||||||
var stateDisposable: Disposable?
|
|
||||||
var progressDisposable: Disposable?
|
|
||||||
|
|
||||||
var record: AudioRecord?
|
var record: AudioRecordDto?
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
@ -34,39 +31,38 @@ class AudioRecordCell: UITableViewCell, ConfigurableCell {
|
|||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
self.record = nil
|
self.record = nil
|
||||||
self.stateDisposable?.dispose()
|
|
||||||
self.progressDisposable?.dispose()
|
|
||||||
self.progressView.progress = 0
|
self.progressView.progress = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(with record: AudioRecord) {
|
func configure(with record: AudioRecordDto) {
|
||||||
self.record = record
|
self.record = record
|
||||||
self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: record.getAddedDate()))
|
self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: record.getAddedDate()))
|
||||||
self.number.text = record.number ?? "Unrecognized"
|
self.number.text = record.number ?? "Unrecognized"
|
||||||
self.duration.text = self.componentsFormatter.string(from: record.duration)
|
self.duration.text = self.componentsFormatter.string(from: record.duration)
|
||||||
|
|
||||||
self.stateDisposable = AudioPlayer.shared
|
// TODO: Fix player
|
||||||
.stateObservable()
|
// AudioPlayer.shared
|
||||||
.filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
|
// .stateObservable()
|
||||||
.subscribe(onNext: { state in
|
// .filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
|
||||||
let imgName = state == .playing ? "pause.fill" : "play.fill"
|
// .subscribe(onNext: { state in
|
||||||
self.playButton.setImage(UIImage(systemName: imgName), for: .normal)
|
// let imgName = state == .playing ? "pause.fill" : "play.fill"
|
||||||
|
// self.playButton.setImage(UIImage(systemName: imgName), for: .normal)
|
||||||
if state == .stopped {
|
//
|
||||||
self.progressView.progress = 0
|
// if state == .stopped {
|
||||||
}
|
// self.progressView.progress = 0
|
||||||
}, onDisposed: {
|
// }
|
||||||
self.playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
|
// }, onDisposed: {
|
||||||
})
|
// self.playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
|
||||||
|
// })
|
||||||
self.progressDisposable = AudioPlayer.shared
|
//
|
||||||
.progressObservable()
|
// self.progressDisposable = AudioPlayer.shared
|
||||||
.filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
|
// .progressObservable()
|
||||||
.subscribe(onNext: { progress in
|
// .filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
|
||||||
self.progressView.progress = progress
|
// .subscribe(onNext: { progress in
|
||||||
}, onDisposed: {
|
// self.progressView.progress = progress
|
||||||
self.progressView.progress = 0
|
// }, onDisposed: {
|
||||||
})
|
// self.progressView.progress = 0
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func onPlay(_ sender: UIButton) {
|
@IBAction func onPlay(_ sender: UIButton) {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
protocol ConfigurableCell {
|
protocol ConfigurableCell {
|
||||||
associatedtype Item
|
associatedtype Item
|
||||||
func configure(with item: Item)
|
func configure(with item: Item)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ class EventCell: UITableViewCell {
|
|||||||
self.dateFormatter.timeStyle = .short
|
self.dateFormatter.timeStyle = .short
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(with event: VehicleEvent) {
|
func configure(with event: VehicleEventDto) {
|
||||||
if let addressString = event.address {
|
if let addressString = event.address {
|
||||||
self.address.text = addressString
|
self.address.text = addressString
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ class VehicleCell: UITableViewCell, ConfigurableCell {
|
|||||||
formatter.timeStyle = .short
|
formatter.timeStyle = .short
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(with vehicle: Vehicle) {
|
func configure(with vehicle: VehicleDto) {
|
||||||
self.name.text = vehicle.brand?.name?.original ?? "<unknown>"
|
self.name.text = vehicle.brand?.name?.original ?? "<unknown>"
|
||||||
self.plate.number = PlateNumber(vehicle.getNumber())
|
self.plate.number = PlateNumber(vehicle.getNumber())
|
||||||
self.plate.fontSize = 40
|
self.plate.fontSize = 40
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class VehicleNoteCell: UITableViewCell {
|
|||||||
self.dateFormatter.timeStyle = .medium
|
self.dateFormatter.timeStyle = .medium
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(with note: VehicleNote) {
|
func configure(with note: VehicleNoteDto) {
|
||||||
self.noteText.text = note.text
|
self.noteText.text = note.text
|
||||||
self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: note.date))
|
self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: note.date))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import AutoCatCore
|
|||||||
|
|
||||||
class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
|
class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
|
||||||
|
|
||||||
var ads: [VehicleAd] = []
|
var ads: [VehicleAdDto] = []
|
||||||
private var currentAd: VehicleAd?
|
private var currentAd: VehicleAdDto?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
@ -52,27 +52,27 @@ class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let description = ad.adDescription, !description.isEmpty {
|
// if let description = ad.adDescription, !description.isEmpty {
|
||||||
section <<< MultilineLabelRow() { row in
|
// section <<< MultilineLabelRow() { row in
|
||||||
row.title = NSLocalizedString("Description", comment: "")
|
// row.title = NSLocalizedString("Description", comment: "")
|
||||||
row.value = description
|
// row.value = description
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if let urlStr = ad.url, let url = URL(string: urlStr) {
|
// if let urlStr = ad.url, let url = URL(string: urlStr) {
|
||||||
section <<< MultilineLinkRow() { row in
|
// section <<< MultilineLinkRow() { row in
|
||||||
row.title = NSLocalizedString("Link", comment: "")
|
// row.title = NSLocalizedString("Link", comment: "")
|
||||||
row.value = urlStr
|
// row.value = urlStr
|
||||||
}
|
// }
|
||||||
.onCellSelection { _, _ in
|
// .onCellSelection { _, _ in
|
||||||
let safari = SFSafariViewController(url: url)
|
// let safari = SFSafariViewController(url: url)
|
||||||
self.present(safari, animated: true)
|
// self.present(safari, animated: true)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if !ad.photos.isEmpty {
|
if !ad.photos.isEmpty {
|
||||||
section <<< ImageGridRow() { row in
|
section <<< ImageGridRow() { row in
|
||||||
row.value = ad.photos.toArray()
|
row.value = ad.photos
|
||||||
}
|
}
|
||||||
.onDidSelected { index in
|
.onDidSelected { index in
|
||||||
self.currentAd = ad
|
self.currentAd = ad
|
||||||
@ -98,6 +98,8 @@ class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KingfisherManager.shared.retrieveImage(with: url) { result in
|
KingfisherManager.shared.retrieveImage(with: url) { result in
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let res):
|
case .success(let res):
|
||||||
completion(index, res.image, ZoomScale.default, nil)
|
completion(index, res.image, ZoomScale.default, nil)
|
||||||
@ -108,4 +110,5 @@ class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,27 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
|
||||||
import RxCocoa
|
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
import AuthenticationServices
|
import AuthenticationServices
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
class AuthController: UIViewController, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
|
class AuthController: UIViewController {
|
||||||
|
|
||||||
@IBOutlet weak var username: UITextField!
|
@IBOutlet weak var username: UITextField!
|
||||||
@IBOutlet weak var password: UITextField!
|
@IBOutlet weak var password: UITextField!
|
||||||
@IBOutlet weak var login: UIButton!
|
@IBOutlet weak var login: UIButton!
|
||||||
@IBOutlet weak var signup: UIButton!
|
@IBOutlet weak var signup: UIButton!
|
||||||
@IBOutlet weak var appleSignIn: ASAuthorizationAppleIDButton!
|
|
||||||
|
|
||||||
let bag = DisposeBag()
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
self.appleSignIn.cornerRadius = 6
|
// FIX login/password lengt checking
|
||||||
|
// let authValid = Observable.combineLatest(self.username.rx.text, self.password.rx.text) { name, pass -> Bool in
|
||||||
let authValid = Observable.combineLatest(self.username.rx.text, self.password.rx.text) { name, pass -> Bool in
|
// guard let name = name, let pass = pass else { return false }
|
||||||
guard let name = name, let pass = pass else { return false }
|
// return name.count >= 4 && pass.count >= 5
|
||||||
return name.count >= 4 && pass.count >= 5
|
// }
|
||||||
}
|
//
|
||||||
|
// authValid.bind(to: self.login.rx.isEnabled).disposed(by: self.bag)
|
||||||
authValid.bind(to: self.login.rx.isEnabled).disposed(by: self.bag)
|
// authValid.bind(to: self.signup.rx.isEnabled).disposed(by: self.bag)
|
||||||
authValid.bind(to: self.signup.rx.isEnabled).disposed(by: self.bag)
|
|
||||||
|
|
||||||
if Settings.shared.user.email.count > 0 {
|
if Settings.shared.user.email.count > 0 {
|
||||||
self.username.text = Settings.shared.user.email
|
self.username.text = Settings.shared.user.email
|
||||||
@ -37,32 +31,29 @@ class AuthController: UIViewController, ASAuthorizationControllerDelegate, ASAut
|
|||||||
@IBAction func loginTapped(_ sender: UIButton) {
|
@IBAction func loginTapped(_ sender: UIButton) {
|
||||||
guard let email = self.username.text, let pass = self.password.text else { return }
|
guard let email = self.username.text, let pass = self.password.text else { return }
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
Api.login(email: email, password: pass)
|
let user = try await ApiService.shared.login(email: email, password: pass)
|
||||||
.observeOn(MainScheduler.instance)
|
self.goToMainScreen(user: user)
|
||||||
.subscribe(onSuccess: self.goToMainScreen(user:), onError: HUD.show(error:))
|
} catch {
|
||||||
.disposed(by: self.bag)
|
HUD.show(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func signupTapped(_ sender: UIButton) {
|
@IBAction func signupTapped(_ sender: UIButton) {
|
||||||
guard let email = self.username.text, let pass = self.password.text else { return }
|
guard let email = self.username.text, let pass = self.password.text else { return }
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
Api.signUp(email: email, password: pass)
|
let user = try await ApiService.shared.signUp(email: email, password: pass)
|
||||||
.observeOn(MainScheduler.instance)
|
self.goToMainScreen(user: user)
|
||||||
.subscribe(onSuccess: self.goToMainScreen(user:), onError: HUD.show(error:))
|
} catch {
|
||||||
.disposed(by: self.bag)
|
HUD.show(error: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func appleSignInTapped(_ sender: ASAuthorizationAppleIDButton) {
|
|
||||||
let appleIDProvider = ASAuthorizationAppleIDProvider()
|
|
||||||
let request = appleIDProvider.createRequest()
|
|
||||||
request.requestedScopes = [.email]
|
|
||||||
|
|
||||||
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
|
|
||||||
authorizationController.delegate = self
|
|
||||||
authorizationController.presentationContextProvider = self
|
|
||||||
authorizationController.performRequests()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func goToMainScreen(user: AutoCatCore.User) {
|
func goToMainScreen(user: AutoCatCore.User) {
|
||||||
@ -83,38 +74,4 @@ class AuthController: UIViewController, ASAuthorizationControllerDelegate, ASAut
|
|||||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||||
self.view.window?.rootViewController = storyboard.instantiateViewController(identifier: "MainSplitController")
|
self.view.window?.rootViewController = storyboard.instantiateViewController(identifier: "MainSplitController")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Apple SignIn
|
|
||||||
|
|
||||||
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
|
||||||
return self.view.window!
|
|
||||||
}
|
|
||||||
|
|
||||||
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
|
|
||||||
switch authorization.credential {
|
|
||||||
case let appleIDCredential as ASAuthorizationAppleIDCredential:
|
|
||||||
guard let email = appleIDCredential.email else {
|
|
||||||
HUD.flash(.labeledError(title: nil, subtitle: "Cannot get email"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
HUD.show(.progress)
|
|
||||||
Api.signIn(email: email, password: appleIDCredential.user)
|
|
||||||
.observeOn(MainScheduler.instance)
|
|
||||||
.subscribe(onSuccess: self.goToMainScreen(user:), onError: HUD.show(error:))
|
|
||||||
.disposed(by: self.bag)
|
|
||||||
|
|
||||||
if let tokenData = appleIDCredential.identityToken {
|
|
||||||
let token = String(data: tokenData, encoding: .utf8) ?? ""
|
|
||||||
_ = Api.fbVerifyAssertion(provider: "apple.com", idToken: token).subscribe(onSuccess: { _ in
|
|
||||||
print("")
|
|
||||||
}, onError: { error in
|
|
||||||
print(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
HUD.flash(.labeledError(title: nil, subtitle: "Unsupported authorization credential"))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
import RxSwift
|
|
||||||
import SwiftDate
|
import SwiftDate
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
import SwiftLocation
|
||||||
|
|
||||||
enum EventAction: Equatable {
|
enum EventAction: Equatable {
|
||||||
case doNotSend
|
case doNotSend
|
||||||
@ -30,7 +30,6 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
|
|||||||
|
|
||||||
@IBOutlet weak var history: UITableView!
|
@IBOutlet weak var history: UITableView!
|
||||||
|
|
||||||
private let bag = DisposeBag()
|
|
||||||
private var historyDataSource: RealmSectionedDataSource<Vehicle, VehicleCell>!
|
private var historyDataSource: RealmSectionedDataSource<Vehicle, VehicleCell>!
|
||||||
private var historyFilter: HistoryFilter = .all
|
private var historyFilter: HistoryFilter = .all
|
||||||
|
|
||||||
@ -69,12 +68,12 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
|
|||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
self.handleQuickActions()
|
Task { await self.handleQuickActions() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
||||||
func handleQuickActions() {
|
func handleQuickActions() async {
|
||||||
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
|
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
|
||||||
|
|
||||||
switch ad.quickAction {
|
switch ad.quickAction {
|
||||||
@ -87,24 +86,23 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
|
|||||||
case .checkNumber(let number, let event):
|
case .checkNumber(let number, let event):
|
||||||
ad.quickAction = .none
|
ad.quickAction = .none
|
||||||
var action: EventAction = .receiveAndSend
|
var action: EventAction = .receiveAndSend
|
||||||
var events: [VehicleEvent] = []
|
var events: [VehicleEventDto] = []
|
||||||
if let event = event {
|
if let event = event {
|
||||||
events = [event]
|
events = [event]
|
||||||
action = .doNotSend
|
action = .doNotSend
|
||||||
}
|
}
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
self.check(number: number, action: action, notes: [], events: events).subscribe { (vehicle, errors) in
|
let (vehicle, errors) = try await self.check(number: number, action: action, notes: [], events: events)
|
||||||
if !vehicle.unrecognized {
|
if !vehicle.unrecognized {
|
||||||
self.updateDetailController(with: vehicle)
|
self.updateDetailController(with: vehicle)
|
||||||
}
|
}
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
self.showErrors(errors)
|
self.showErrors(errors)
|
||||||
} onFailure: { error in
|
} catch {
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
self.show(error: error)
|
self.show(error: error)
|
||||||
//HUD.show(error: error)
|
|
||||||
}
|
}
|
||||||
.disposed(by: self.bag)
|
|
||||||
break
|
break
|
||||||
case .addVoiceRecord:
|
case .addVoiceRecord:
|
||||||
self.tabBarController?.selectedIndex = 1
|
self.tabBarController?.selectedIndex = 1
|
||||||
@ -112,7 +110,7 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
|
|||||||
case .openReport(let number):
|
case .openReport(let number):
|
||||||
ad.quickAction = .none
|
ad.quickAction = .none
|
||||||
if let sd = self.view.window?.windowScene?.delegate as? SceneDelegate {
|
if let sd = self.view.window?.windowScene?.delegate as? SceneDelegate {
|
||||||
sd.openReport(with: number)
|
Task { await sd.openReport(with: number) }
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@ -218,32 +216,36 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
|
|||||||
func checkTapped(number: String) {
|
func checkTapped(number: String) {
|
||||||
let numberNormalized = number.filter { !$0.isWhitespace }.uppercased()
|
let numberNormalized = number.filter { !$0.isWhitespace }.uppercased()
|
||||||
|
|
||||||
var events: [VehicleEvent] = []
|
var events: [VehicleEventDto] = []
|
||||||
do {
|
do {
|
||||||
let realm = try Realm()
|
let realm = try Realm()
|
||||||
if let dbVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: numberNormalized) {
|
if let dbVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: numberNormalized) {
|
||||||
events.append(contentsOf: dbVehicle.events.map { $0.clone() })
|
events.append(contentsOf: dbVehicle.events.map(\.dto))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
print(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
self.check(number: numberNormalized, action: .receiveAndSend, notes: [], events: events).subscribe { (vehicle, errors) in
|
let (vehicle, errors) = try await self.check(number: numberNormalized,
|
||||||
|
action: .receiveAndSend,
|
||||||
|
notes: [],
|
||||||
|
events: events)
|
||||||
if !vehicle.unrecognized && errors.isEmpty {
|
if !vehicle.unrecognized && errors.isEmpty {
|
||||||
self.updateDetailController(with: vehicle)
|
self.updateDetailController(with: vehicle)
|
||||||
}
|
}
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
self.showErrors(errors)
|
self.showErrors(errors)
|
||||||
} onFailure: { error in
|
} catch {
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
self.show(error: error)
|
self.show(error: error)
|
||||||
}
|
}
|
||||||
.disposed(by: self.bag)
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateDetailController(with vehicle: Vehicle) {
|
func updateDetailController(with vehicle: VehicleDto) {
|
||||||
if let splitViewController = self.view.window?.rootViewController as? UISplitViewController
|
if let splitViewController = self.view.window?.rootViewController as? UISplitViewController
|
||||||
{
|
{
|
||||||
var detail: UINavigationController?
|
var detail: UINavigationController?
|
||||||
@ -330,22 +332,28 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
|
|||||||
|
|
||||||
// MARK: - Contextual actions
|
// MARK: - Contextual actions
|
||||||
|
|
||||||
func update(vehicle: Vehicle) {
|
func update(vehicle: VehicleDto) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
self.check(number: vehicle.getNumber(), action: .doNotSend, notes: Array(vehicle.notes), events: Array(vehicle.events), force: true).subscribe { (vehicle, errors) in
|
let (vehicle, errors) = try await self.check(number: vehicle.getNumber(),
|
||||||
|
action: .doNotSend,
|
||||||
|
notes: Array(vehicle.notes),
|
||||||
|
events: Array(vehicle.events),
|
||||||
|
force: true)
|
||||||
if !vehicle.unrecognized {
|
if !vehicle.unrecognized {
|
||||||
self.updateDetailController(with: vehicle)
|
self.updateDetailController(with: vehicle)
|
||||||
}
|
}
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
self.showErrors(errors)
|
self.showErrors(errors)
|
||||||
} onFailure: { error in
|
} catch {
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
self.show(error: error)
|
self.show(error: error)
|
||||||
}
|
}
|
||||||
.disposed(by: self.bag)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(vehicle: Vehicle) {
|
func remove(vehicle: VehicleDto) {
|
||||||
guard let realm = try? Realm() else { return }
|
guard let realm = try? Realm() else { return }
|
||||||
guard let realmVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) else { return }
|
guard let realmVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) else { return }
|
||||||
|
|
||||||
@ -360,49 +368,66 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
|
|||||||
|
|
||||||
// MARK: - Checking number
|
// MARK: - Checking number
|
||||||
|
|
||||||
func save(vehicle: Vehicle) throws {
|
func save(vehicle: VehicleDto) throws {
|
||||||
let realm = try Realm()
|
let realm = try Realm()
|
||||||
try realm.write {
|
try realm.write {
|
||||||
realm.add(vehicle, update: .all)
|
realm.add(Vehicle(dto: vehicle), update: .all)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEvent(for action: EventAction) -> Single<VehicleEvent> {
|
func getEvent(for action: EventAction) async throws -> VehicleEventDto {
|
||||||
if let event = RxLocationManager.lastEvent, (Date().timeIntervalSince1970 - event.date) < 100 {
|
if let event = await RxLocationManager.getLastEvent(), (Date().timeIntervalSince1970 - event.date) < 100 {
|
||||||
return Single<VehicleEvent>.just(event)
|
return event
|
||||||
} else {
|
} else {
|
||||||
return RxLocationManager.requestCurrentLocation()
|
return try await RxLocationManager.requestCurrentLocation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func check(number: String, action: EventAction, notes: [VehicleNote], events: [VehicleEvent], force: Bool = false) -> Single<(vehicle: Vehicle, errors: [Error])> {
|
func prepareEvent(for action: EventAction) async -> (event: VehicleEventDto?, error: Error?) {
|
||||||
var eventSingle: Single<(event: VehicleEvent?, error: Error?)> = .just((event: nil, error: nil))
|
guard action != .doNotSend else {
|
||||||
if action != .doNotSend {
|
return (event: nil, error: nil)
|
||||||
eventSingle = self.getEvent(for: action)
|
|
||||||
.flatMap { event in event.findAddress().map{ event }.catchAndReturn(event) }
|
|
||||||
.map { event -> (event: VehicleEvent?, error: Error?) in (event: event, error: nil) }
|
|
||||||
.observe(on: MainScheduler.instance)
|
|
||||||
.catch { .just((event: nil, error: $0)) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let checkSingle = Api.checkVehicle(by: number, notes: notes, events: events, force: force)
|
do {
|
||||||
.observe(on: MainScheduler.instance)
|
let event = try await getEvent(for: action)
|
||||||
.map { (vehicle: Vehicle) -> (vehicle: Vehicle, error: Error?) in
|
try? await event.findAddress()
|
||||||
|
return (event: event, error: nil)
|
||||||
|
} catch {
|
||||||
|
return (event: nil, error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVehicle(number: String,
|
||||||
|
notes: [VehicleNoteDto],
|
||||||
|
events: [VehicleEventDto],
|
||||||
|
force: Bool = false) async -> (vehicle: VehicleDto, error: Error?) {
|
||||||
|
|
||||||
|
do {
|
||||||
|
let vehicle = try await ApiService.shared.checkVehicle(by: number, notes: notes, events: events, force: force)
|
||||||
try self.save(vehicle: vehicle)
|
try self.save(vehicle: vehicle)
|
||||||
return (vehicle: vehicle, error: nil)
|
return (vehicle: vehicle, error: nil)
|
||||||
}
|
} catch {
|
||||||
.catch { error in
|
let realm = try? await Realm()
|
||||||
let realm = try Realm()
|
if let existingVehicle = realm?.object(ofType: Vehicle.self, forPrimaryKey: number) {
|
||||||
if let existingVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: number) {
|
return (vehicle: existingVehicle.dto, error: error)
|
||||||
return .just((vehicle: existingVehicle, error: error))
|
|
||||||
} else {
|
} else {
|
||||||
let vehicle = Vehicle(number)
|
let vehicle = Vehicle(number)
|
||||||
try realm.write { realm.add(vehicle, update: .all) }
|
try? realm?.write { realm?.add(vehicle, update: .all) }
|
||||||
return .just((vehicle: vehicle, error: error))
|
return (vehicle: vehicle.dto, error: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Single.zip(eventSingle, checkSingle).flatMap { eventResult, vehicleResult in
|
func check(number: String,
|
||||||
|
action: EventAction,
|
||||||
|
notes: [VehicleNoteDto],
|
||||||
|
events: [VehicleEventDto],
|
||||||
|
force: Bool = false) async throws -> (vehicle: VehicleDto, errors: [Error]) {
|
||||||
|
|
||||||
|
async let eventTask = prepareEvent(for: action)
|
||||||
|
async let vehicleTask = checkVehicle(number: number, notes: notes, events: events, force: force)
|
||||||
|
let (eventResult, vehicleResult) = await (eventTask, vehicleTask)
|
||||||
|
|
||||||
var errors = [eventResult.error, vehicleResult.error].map { error -> Error? in
|
var errors = [eventResult.error, vehicleResult.error].map { error -> Error? in
|
||||||
if let clerror = error as? CLError {
|
if let clerror = error as? CLError {
|
||||||
if clerror.code != .denied {
|
if clerror.code != .denied {
|
||||||
@ -418,49 +443,47 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
|
|||||||
|
|
||||||
RxLocationManager.resetLastEvent()
|
RxLocationManager.resetLastEvent()
|
||||||
|
|
||||||
let realm = try Realm()
|
let realm = try await Realm()
|
||||||
let dbVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicleResult.vehicle.getNumber())
|
let dbVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicleResult.vehicle.getNumber())
|
||||||
if let event = eventResult.event, let vehicle = dbVehicle {
|
if let event = eventResult.event, let vehicle = dbVehicle {
|
||||||
try realm.write {
|
try realm.write {
|
||||||
vehicle.events.append(event)
|
vehicle.events.append(VehicleEvent(dto: event))
|
||||||
vehicle.updatedDate = Date().timeIntervalSince1970
|
vehicle.updatedDate = Date().timeIntervalSince1970
|
||||||
vehicle.synchronized = false
|
vehicle.synchronized = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if vehicleResult.error != nil {
|
if vehicleResult.error != nil {
|
||||||
return .just((vehicle: vehicleResult.vehicle, errors: errors))
|
return (vehicle: vehicleResult.vehicle, errors: errors)
|
||||||
} else {
|
} else {
|
||||||
if let event = eventResult.event {
|
if let event = eventResult.event {
|
||||||
return Api.add(event: event, to: vehicleResult.vehicle.getNumber())
|
do {
|
||||||
.observe(on: MainScheduler.instance)
|
let vehicle = try await ApiService.shared.add(event: event, to: vehicleResult.vehicle.getNumber())
|
||||||
.map {
|
try self.save(vehicle: vehicle)
|
||||||
try self.save(vehicle: $0)
|
return (vehicle: vehicle, errors: errors)
|
||||||
return (vehicle: $0, errors: errors)
|
} catch {
|
||||||
}
|
|
||||||
.catch { error in
|
|
||||||
errors.append(error)
|
errors.append(error)
|
||||||
return .just((vehicle: vehicleResult.vehicle, errors: errors))
|
return (vehicle: vehicleResult.vehicle, errors: errors)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return .just((vehicle: vehicleResult.vehicle, errors: errors))
|
return (vehicle: vehicleResult.vehicle, errors: errors)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showErrors(_ errors: [Error]) {
|
func showErrors(_ errors: [Error]) {
|
||||||
let observables = errors.map(rxShowError)
|
Task {
|
||||||
Observable.from(observables).concat().subscribe().disposed(by: self.bag)
|
for error in errors {
|
||||||
|
await asyncShowError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rxShowError(_ error: Error) -> Observable<Void> {
|
func asyncShowError(_ error: Error) async {
|
||||||
return Observable<Void>.create { observer in
|
await withCheckedContinuation { continuation in
|
||||||
self.show(error: error, animated: true) {
|
self.show(error: error, animated: true) {
|
||||||
observer.on(.next(()))
|
continuation.resume()
|
||||||
observer.on(.completed)
|
|
||||||
}
|
}
|
||||||
return Disposables.create()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Eureka
|
import Eureka
|
||||||
import RxSwift
|
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
class FiltersController: FormViewController {
|
class FiltersController: FormViewController {
|
||||||
@ -10,28 +9,46 @@ class FiltersController: FormViewController {
|
|||||||
var onDone: (() -> Void)?
|
var onDone: (() -> Void)?
|
||||||
var regions: [VehicleRegion] = []
|
var regions: [VehicleRegion] = []
|
||||||
|
|
||||||
let bag = DisposeBag()
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
form +++ Section(NSLocalizedString("Main filters", comment: "")) { $0.tag = "MainFilters" }
|
addMainSection()
|
||||||
<<< PushRow<String>("Brand") { row in
|
addRegionSection()
|
||||||
|
addAddedBySection()
|
||||||
|
addTimeSections()
|
||||||
|
addLocationTimeSection()
|
||||||
|
addSortSection()
|
||||||
|
addClearAllSection()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAsync(_ completion: @escaping () async throws -> Void) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await completion()
|
||||||
|
} catch {
|
||||||
|
print("Error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMainSection() {
|
||||||
|
|
||||||
|
let brandRow = PushRow<String>("Brand") { row in
|
||||||
row.title = NSLocalizedString("Brand", comment: "")
|
row.title = NSLocalizedString("Brand", comment: "")
|
||||||
row.value = self.filter.brand ?? "Any"
|
row.value = self.filter.brand ?? "Any"
|
||||||
row.selectorTitle = NSLocalizedString("Brands", comment: "")
|
row.selectorTitle = NSLocalizedString("Brands", comment: "")
|
||||||
row.optionsProvider = .lazy({ form, completion in
|
row.optionsProvider = .lazy({ form, completion in
|
||||||
Api.getBrands().observeOn(MainScheduler.instance).subscribe(onSuccess: { brands in
|
self.runAsync {
|
||||||
|
let brands = try await ApiService.shared.getBrands()
|
||||||
completion(["Any"] + brands)
|
completion(["Any"] + brands)
|
||||||
}, onError: { error in
|
}
|
||||||
print("Get brands error: ", error)
|
|
||||||
}).disposed(by: self.bag)
|
|
||||||
})
|
})
|
||||||
}.onPresent(removeSectionName(from:to:))
|
}
|
||||||
|
.onPresent(removeSectionName(from:to:))
|
||||||
.onChange { self.filter.brand = $0.value == "Any" ? nil : $0.value }
|
.onChange { self.filter.brand = $0.value == "Any" ? nil : $0.value }
|
||||||
.cellUpdate { $1.value = self.filter.brand ?? "Any" }
|
.cellUpdate { $1.value = self.filter.brand ?? "Any" }
|
||||||
|
|
||||||
<<< PushRow<String>("Model") { row in
|
let modelRow = PushRow<String>("Model") { row in
|
||||||
row.title = NSLocalizedString("Model", comment: "")
|
row.title = NSLocalizedString("Model", comment: "")
|
||||||
row.value = self.filter.model ?? "Any"
|
row.value = self.filter.model ?? "Any"
|
||||||
row.disabled = "$Brand == 'Any'"
|
row.disabled = "$Brand == 'Any'"
|
||||||
@ -40,44 +57,55 @@ class FiltersController: FormViewController {
|
|||||||
completion(["Any"])
|
completion(["Any"])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Api.getModels(of: brand).observeOn(MainScheduler.instance).subscribe(onSuccess: { models in
|
|
||||||
|
self.runAsync {
|
||||||
|
let models = try await ApiService.shared.getModels(of: brand)
|
||||||
completion(["Any"] + models)
|
completion(["Any"] + models)
|
||||||
}, onError: { error in
|
}
|
||||||
print("Get models error: ", error)
|
|
||||||
}).disposed(by: self.bag)
|
|
||||||
})
|
})
|
||||||
}.onPresent(removeSectionName(from:to:))
|
}
|
||||||
|
.onPresent(removeSectionName(from:to:))
|
||||||
.onChange { self.filter.model = $0.value == "Any" ? nil : $0.value }
|
.onChange { self.filter.model = $0.value == "Any" ? nil : $0.value }
|
||||||
.cellUpdate { $1.value = self.filter.model ?? "Any" }
|
.cellUpdate { $1.value = self.filter.model ?? "Any" }
|
||||||
|
|
||||||
<<< PushRow<String>("Color") { row in
|
let colorRow = PushRow<String>("Color") { row in
|
||||||
row.title = NSLocalizedString("Color", comment: "")
|
row.title = NSLocalizedString("Color", comment: "")
|
||||||
row.value = self.filter.color ?? "Any"
|
row.value = self.filter.color ?? "Any"
|
||||||
row.optionsProvider = .lazy({ form, completion in
|
row.optionsProvider = .lazy({ form, completion in
|
||||||
Api.getColors().observeOn(MainScheduler.instance).subscribe(onSuccess: { colors in
|
self.runAsync {
|
||||||
|
let colors = try await ApiService.shared.getColors()
|
||||||
completion(["Any"] + colors)
|
completion(["Any"] + colors)
|
||||||
}, onError: { error in
|
}
|
||||||
print("Get colors error: ", error)
|
|
||||||
}).disposed(by: self.bag)
|
|
||||||
})
|
})
|
||||||
}.onPresent(removeSectionName(from:to:))
|
}
|
||||||
|
.onPresent(removeSectionName(from:to:))
|
||||||
.onChange { self.filter.color = $0.value == "Any" ? nil : $0.value }
|
.onChange { self.filter.color = $0.value == "Any" ? nil : $0.value }
|
||||||
.cellUpdate { $1.value = self.filter.color ?? "Any" }
|
.cellUpdate { $1.value = self.filter.color ?? "Any" }
|
||||||
|
|
||||||
<<< PushRow<String>("Year") { row in
|
let yearRow = PushRow<String>("Year") { row in
|
||||||
row.title = NSLocalizedString("Year", comment: "Manufacturing year")
|
row.title = NSLocalizedString("Year", comment: "Manufacturing year")
|
||||||
row.value = self.filter.year ?? "Any"
|
row.value = self.filter.year ?? "Any"
|
||||||
row.optionsProvider = .lazy({ form, completion in
|
row.optionsProvider = .lazy({ form, completion in
|
||||||
Api.getYears().observeOn(MainScheduler.instance).subscribe { years in
|
self.runAsync {
|
||||||
|
let years = try await ApiService.shared.getYears()
|
||||||
completion(["Any"] + years.map(String.init))
|
completion(["Any"] + years.map(String.init))
|
||||||
} onError: { error in
|
}
|
||||||
print("Get years error: \(error)")
|
|
||||||
}.disposed(by: self.bag)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.onChange { self.filter.year = $0.value == "Any" ? nil : $0.value }
|
.onChange { self.filter.year = $0.value == "Any" ? nil : $0.value }
|
||||||
.cellUpdate { $1.value = self.filter.year ?? "Any" }
|
.cellUpdate { $1.value = self.filter.year ?? "Any" }
|
||||||
|
|
||||||
|
let mainSection = Section(NSLocalizedString("Main filters", comment: "")) { $0.tag = "MainFilters" }
|
||||||
|
|
||||||
|
form +++ mainSection
|
||||||
|
<<< brandRow
|
||||||
|
<<< modelRow
|
||||||
|
<<< colorRow
|
||||||
|
<<< yearRow
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRegionSection() {
|
||||||
|
|
||||||
form +++ Section() { $0.tag = "Regions" }
|
form +++ Section() { $0.tag = "Regions" }
|
||||||
<<< LabelRow("RegionsRow") { row in
|
<<< LabelRow("RegionsRow") { row in
|
||||||
row.title = NSLocalizedString("Regions", comment: "")
|
row.title = NSLocalizedString("Regions", comment: "")
|
||||||
@ -97,6 +125,9 @@ class FiltersController: FormViewController {
|
|||||||
}
|
}
|
||||||
self.navigationController?.pushViewController(vc, animated: true)
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAddedBySection() {
|
||||||
|
|
||||||
form +++ Section() { $0.tag = "AddedByMe" }
|
form +++ Section() { $0.tag = "AddedByMe" }
|
||||||
<<< ActionSheetRow<String>("AddedByMeRow") { row in
|
<<< ActionSheetRow<String>("AddedByMeRow") { row in
|
||||||
@ -115,6 +146,9 @@ class FiltersController: FormViewController {
|
|||||||
.cellUpdate { cell, row in
|
.cellUpdate { cell, row in
|
||||||
row.value = self.filter.addedBy?.description ?? AddedBy.anyone.description
|
row.value = self.filter.addedBy?.description ?? AddedBy.anyone.description
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTimeSections() {
|
||||||
|
|
||||||
form +++ Section(NSLocalizedString("Update time", comment: ""))
|
form +++ Section(NSLocalizedString("Update time", comment: ""))
|
||||||
<<< DateInlineRow("FromDateUpdated") { row in
|
<<< DateInlineRow("FromDateUpdated") { row in
|
||||||
@ -147,6 +181,9 @@ class FiltersController: FormViewController {
|
|||||||
}
|
}
|
||||||
.onChange { self.filter.toDate = self.nullifyTime(of: $0.value) }
|
.onChange { self.filter.toDate = self.nullifyTime(of: $0.value) }
|
||||||
.cellUpdate(self.update(cell:row:))
|
.cellUpdate(self.update(cell:row:))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addLocationTimeSection() {
|
||||||
|
|
||||||
form +++ Section(NSLocalizedString("Location adding time", comment: ""))
|
form +++ Section(NSLocalizedString("Location adding time", comment: ""))
|
||||||
<<< DateInlineRow("FromLocationDate") { row in
|
<<< DateInlineRow("FromLocationDate") { row in
|
||||||
@ -163,6 +200,9 @@ class FiltersController: FormViewController {
|
|||||||
}
|
}
|
||||||
.onChange { self.filter.toLocationDate = self.nullifyTime(of: $0.value) }
|
.onChange { self.filter.toLocationDate = self.nullifyTime(of: $0.value) }
|
||||||
.cellUpdate(self.update(cell:row:))
|
.cellUpdate(self.update(cell:row:))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSortSection() {
|
||||||
|
|
||||||
form +++ Section(NSLocalizedString("Sort", comment: "Header section. Noun."))
|
form +++ Section(NSLocalizedString("Sort", comment: "Header section. Noun."))
|
||||||
<<< PickerInlineRow<SortParameter>("SortBy") { row in
|
<<< PickerInlineRow<SortParameter>("SortBy") { row in
|
||||||
@ -179,6 +219,9 @@ class FiltersController: FormViewController {
|
|||||||
}
|
}
|
||||||
.onChange { self.filter.sortOrder = $0.value }
|
.onChange { self.filter.sortOrder = $0.value }
|
||||||
.cellUpdate { $1.value = self.filter.sortOrder }
|
.cellUpdate { $1.value = self.filter.sortOrder }
|
||||||
|
}
|
||||||
|
|
||||||
|
func addClearAllSection() {
|
||||||
|
|
||||||
form +++ Section()
|
form +++ Section()
|
||||||
<<< ButtonRow("ClearAll") { $0.title = NSLocalizedString("Clear all filters", comment: "") }.onCellSelection { cell, row in
|
<<< ButtonRow("ClearAll") { $0.title = NSLocalizedString("Clear all filters", comment: "") }.onCellSelection { cell, row in
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import WebKit
|
import WebKit
|
||||||
import CommonCrypto
|
import CommonCrypto
|
||||||
import RxSwift
|
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
@ -17,7 +16,6 @@ struct TokenResponse: Codable {
|
|||||||
class GoogleSignInController: UIViewController, WKNavigationDelegate {
|
class GoogleSignInController: UIViewController, WKNavigationDelegate {
|
||||||
@IBOutlet weak var webView: WKWebView!
|
@IBOutlet weak var webView: WKWebView!
|
||||||
|
|
||||||
private var bag = DisposeBag()
|
|
||||||
private var codeVerifier: String = ""
|
private var codeVerifier: String = ""
|
||||||
public var completion: (() -> Void)?
|
public var completion: (() -> Void)?
|
||||||
|
|
||||||
@ -54,16 +52,16 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate {
|
|||||||
if let queryItems = components.queryItems {
|
if let queryItems = components.queryItems {
|
||||||
if let code = queryItems.first(where: { $0.name == "code" })?.value {
|
if let code = queryItems.first(where: { $0.name == "code" })?.value {
|
||||||
decisionHandler(.cancel)
|
decisionHandler(.cancel)
|
||||||
self.getToken(code: code)
|
|
||||||
.flatMap { Api.fbVerifyAssertion(provider: "google.com", idToken: $0.id_token, accessToken: $0.access_token) }
|
Task { @MainActor in
|
||||||
.observeOn(MainScheduler.instance)
|
do {
|
||||||
.subscribe(onSuccess: { _ in
|
let token = try await self.getToken(code: code)
|
||||||
|
await ApiService.shared.fbVerifyAssertion(provider: "google.com", idToken: token.id_token, accessToken: token.access_token)
|
||||||
self.dismiss(animated: true, completion: self.completion)
|
self.dismiss(animated: true, completion: self.completion)
|
||||||
}, onError: { error in
|
} catch {
|
||||||
HUD.flash(.labeledError(title: nil, subtitle: error.localizedDescription))
|
HUD.flash(.labeledError(title: nil, subtitle: error.localizedDescription))
|
||||||
})
|
}
|
||||||
.disposed(by: self.bag)
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,7 +85,7 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate {
|
|||||||
return String(data: Data(Base64FS.encode(data: hash)), encoding: .utf8)?.trimmingCharacters(in: CharacterSet(charactersIn: "="))
|
return String(data: Data(Base64FS.encode(data: hash)), encoding: .utf8)?.trimmingCharacters(in: CharacterSet(charactersIn: "="))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getToken(code: String) -> Single<TokenResponse> {
|
func getToken(code: String) async throws -> TokenResponse {
|
||||||
let tokenUrlString = Constants.googleTokenURL
|
let tokenUrlString = Constants.googleTokenURL
|
||||||
+ "?grant_type=authorization_code"
|
+ "?grant_type=authorization_code"
|
||||||
+ "&code=" + code
|
+ "&code=" + code
|
||||||
@ -98,12 +96,10 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate {
|
|||||||
if let url = URL(string: tokenUrlString) {
|
if let url = URL(string: tokenUrlString) {
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
return URLSession.shared.rx.data(request: request).asSingle().map { data in
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||||||
return try JSONDecoder().decode(TokenResponse.self, from: data)
|
return try JSONDecoder().decode(TokenResponse.self, from: data)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Bad URL"])
|
throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Bad URL"])
|
||||||
return Single.error(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import MapKit
|
import MapKit
|
||||||
import RxSwift
|
|
||||||
import Realm
|
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
@ -21,7 +19,7 @@ class EventPin: NSObject, MKAnnotation {
|
|||||||
self.id = id
|
self.id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(event: VehicleEvent) {
|
convenience init(event: VehicleEventDto) {
|
||||||
let coordinate = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude)
|
let coordinate = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude)
|
||||||
let address = event.address ?? "\(event.latitude), \(event.longitude)"
|
let address = event.address ?? "\(event.latitude), \(event.longitude)"
|
||||||
let date = Date(timeIntervalSince1970: event.date)
|
let date = Date(timeIntervalSince1970: event.date)
|
||||||
@ -48,13 +46,12 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
|
|||||||
@IBOutlet weak var map: MKMapView!
|
@IBOutlet weak var map: MKMapView!
|
||||||
@IBOutlet weak var tableView: UITableView!
|
@IBOutlet weak var tableView: UITableView!
|
||||||
|
|
||||||
let bag = DisposeBag()
|
|
||||||
var modeButton: UIBarButtonItem!
|
var modeButton: UIBarButtonItem!
|
||||||
var addButton: UIBarButtonItem!
|
var addButton: UIBarButtonItem!
|
||||||
var pasteButton: UIBarButtonItem!
|
var pasteButton: UIBarButtonItem!
|
||||||
var mode: EventsMode = .map
|
var mode: EventsMode = .map
|
||||||
|
|
||||||
public var vehicle: Vehicle? {
|
public var vehicle: VehicleDto? {
|
||||||
didSet {
|
didSet {
|
||||||
if self.isViewLoaded {
|
if self.isViewLoaded {
|
||||||
self.updateInterface()
|
self.updateInterface()
|
||||||
@ -241,41 +238,45 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
|
|||||||
|
|
||||||
// MARK: - Event actions
|
// MARK: - Event actions
|
||||||
|
|
||||||
func deleteEvent(event: VehicleEvent, completion: ((Bool) -> Void)? = nil) {
|
func deleteEvent(event: VehicleEventDto, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
Api.remove(event: event.id).observe(on: MainScheduler.instance).subscribe(onSuccess: { vehicle in
|
let vehicle = try await ApiService.shared.remove(event: event.id)
|
||||||
let result = self.update(vehicle: vehicle)
|
let result = self.update(vehicle: vehicle)
|
||||||
completion?(result)
|
completion?(result)
|
||||||
}, onFailure: { error in
|
} catch {
|
||||||
completion?(false)
|
completion?(false)
|
||||||
HUD.show(error: error)
|
HUD.show(error: error)
|
||||||
print(error)
|
}
|
||||||
}).disposed(by: self.bag)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func editEvent(event: VehicleEvent) {
|
func editEvent(event: VehicleEventDto) {
|
||||||
let sb = UIStoryboard(name: "Main", bundle: nil)
|
let sb = UIStoryboard(name: "Main", bundle: nil)
|
||||||
let controller = sb.instantiateViewController(identifier: "LocationEditController") as LocationEditController
|
let controller = sb.instantiateViewController(identifier: "LocationEditController") as LocationEditController
|
||||||
controller.title = NSLocalizedString("Edit event", comment: "")
|
controller.title = NSLocalizedString("Edit event", comment: "")
|
||||||
controller.date = Date(timeIntervalSince1970: event.date)
|
controller.date = Date(timeIntervalSince1970: event.date)
|
||||||
controller.placemark = Placemark(latitude: event.latitude, longitude: event.longitude, address: event.address)
|
controller.placemark = Placemark(latitude: event.latitude, longitude: event.longitude, address: event.address)
|
||||||
controller.onDone = { newEvent in
|
controller.onDone = { newEvent in
|
||||||
newEvent.id = event.id
|
var updatedEvent = newEvent
|
||||||
|
updatedEvent.id = event.id
|
||||||
self.navigationController?.popViewController(animated: true, completion: {
|
self.navigationController?.popViewController(animated: true, completion: {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
Api.edit(event: newEvent)
|
let vehicle = try await ApiService.shared.edit(event: updatedEvent)
|
||||||
.observe(on: MainScheduler.instance)
|
self.update(vehicle: vehicle)
|
||||||
.subscribe(onSuccess: { self.update(vehicle: $0) }, onFailure:
|
} catch {
|
||||||
{ error in
|
|
||||||
HUD.show(error: error)
|
HUD.show(error: error)
|
||||||
})
|
}
|
||||||
.disposed(by: self.bag)
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
self.navigationController?.pushViewController(controller, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyEvent(event: VehicleEvent) {
|
func copyEvent(event: VehicleEventDto) {
|
||||||
var items: [String: Any] = [:]
|
var items: [String: Any] = [:]
|
||||||
|
|
||||||
if let url = event.getMapLink() {
|
if let url = event.getMapLink() {
|
||||||
@ -295,7 +296,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
|
|||||||
self.setupBarButtonItems()
|
self.setupBarButtonItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
func shareEvent(event: VehicleEvent) {
|
func shareEvent(event: VehicleEventDto) {
|
||||||
guard let url = event.getMapLink() else {
|
guard let url = event.getMapLink() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -304,7 +305,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
|
|||||||
self.present(controller, animated: true)
|
self.present(controller, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openInAppleMaps(event: VehicleEvent) {
|
func openInAppleMaps(event: VehicleEventDto) {
|
||||||
let coordinates = CLLocationCoordinate2D(latitude: event.latitude,
|
let coordinates = CLLocationCoordinate2D(latitude: event.latitude,
|
||||||
longitude: event.longitude)
|
longitude: event.longitude)
|
||||||
let placemark = MKPlacemark(coordinate: coordinates)
|
let placemark = MKPlacemark(coordinate: coordinates)
|
||||||
@ -312,7 +313,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
|
|||||||
mapItem.openInMaps()
|
mapItem.openInMaps()
|
||||||
}
|
}
|
||||||
|
|
||||||
func openInYandexMaps(event: VehicleEvent) {
|
func openInYandexMaps(event: VehicleEventDto) {
|
||||||
guard let url = URL(string: "yandexmaps://maps.yandex.ru/?pt=\(event.longitude),\(event.latitude)&z=12") else {
|
guard let url = URL(string: "yandexmaps://maps.yandex.ru/?pt=\(event.longitude),\(event.latitude)&z=12") else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -331,14 +332,15 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
|
|||||||
controller.title = NSLocalizedString("Add new event", comment: "")
|
controller.title = NSLocalizedString("Add new event", comment: "")
|
||||||
controller.onDone = { newEvent in
|
controller.onDone = { newEvent in
|
||||||
self.navigationController?.popViewController(animated: true, completion: {
|
self.navigationController?.popViewController(animated: true, completion: {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
Api.add(event: newEvent, to: vehicle.getNumber())
|
let vehicle = try await ApiService.shared.add(event: newEvent, to: vehicle.getNumber())
|
||||||
.observe(on: MainScheduler.instance)
|
self.update(vehicle: vehicle)
|
||||||
.subscribe(onSuccess: { self.update(vehicle: $0) }, onFailure:
|
} catch {
|
||||||
{ error in
|
|
||||||
HUD.show(error: error)
|
HUD.show(error: error)
|
||||||
})
|
}
|
||||||
.disposed(by: self.bag)
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
self.navigationController?.pushViewController(controller, animated: true)
|
||||||
@ -352,7 +354,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
|
|||||||
guard let data = UIPasteboard.general.data(forPasteboardType: "pro.aliencat.vehicle.event") else { return }
|
guard let data = UIPasteboard.general.data(forPasteboardType: "pro.aliencat.vehicle.event") else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let event = try JSONDecoder().decode(VehicleEvent.self, from: data)
|
let event = try JSONDecoder().decode(VehicleEventDto.self, from: data)
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateStyle = .medium
|
formatter.dateStyle = .medium
|
||||||
formatter.timeStyle = .medium
|
formatter.timeStyle = .medium
|
||||||
@ -360,15 +362,17 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
|
|||||||
|
|
||||||
let alert = UIAlertController(title: NSLocalizedString("Paste event", comment: "from clipboard"), message: msg, preferredStyle: .alert)
|
let alert = UIAlertController(title: NSLocalizedString("Paste event", comment: "from clipboard"), message: msg, preferredStyle: .alert)
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Paste", comment: "from clipboard"), style: .default, handler: { action in
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Paste", comment: "from clipboard"), style: .default, handler: { action in
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
event.id = UUID().uuidString
|
var newEvent = event
|
||||||
Api.add(event: event, to: vehicle.getNumber())
|
newEvent.id = UUID().uuidString
|
||||||
.observe(on: MainScheduler.instance)
|
let vehicle = try await ApiService.shared.add(event: newEvent, to: vehicle.getNumber())
|
||||||
.subscribe(onSuccess: { self.update(vehicle: $0) }, onFailure:
|
self.update(vehicle: vehicle)
|
||||||
{ error in
|
} catch {
|
||||||
HUD.show(error: error)
|
HUD.show(error: error)
|
||||||
})
|
}
|
||||||
.disposed(by: self.bag)
|
}
|
||||||
}))
|
}))
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil))
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil))
|
||||||
self.present(alert, animated: true)
|
self.present(alert, animated: true)
|
||||||
@ -383,17 +387,17 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func update(vehicle: Vehicle) -> Bool {
|
func update(vehicle: VehicleDto) -> Bool {
|
||||||
do {
|
do {
|
||||||
if let v = self.vehicle, let realm = v.realm, !v.isFrozen {
|
let realm = try Realm()
|
||||||
|
if let realmVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) {
|
||||||
try ExceptionCatcher.catch {
|
try ExceptionCatcher.catch {
|
||||||
try realm.write {
|
try realm.write {
|
||||||
realm.add(vehicle, update: .all)
|
realm.add(Vehicle(dto: vehicle), update: .all)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.vehicle?.events.removeAll()
|
self.vehicle?.events = vehicle.events
|
||||||
self.vehicle?.events.append(objectsIn: vehicle.events)
|
|
||||||
}
|
}
|
||||||
self.updateInterface()
|
self.updateInterface()
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import MapKit
|
import MapKit
|
||||||
import RxSwift
|
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
@ -8,7 +7,6 @@ class GlobalEventsController: UIViewController {
|
|||||||
|
|
||||||
@IBOutlet weak var map: MKMapView!
|
@IBOutlet weak var map: MKMapView!
|
||||||
|
|
||||||
let bag = DisposeBag()
|
|
||||||
var filter: Filter!
|
var filter: Filter!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
@ -23,20 +21,22 @@ class GlobalEventsController: UIViewController {
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Task { await loadEvents() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadEvents() async {
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
Api.events(with: self.filter)
|
let events = try await ApiService.shared.events(with: self.filter)
|
||||||
.observe(on: MainScheduler.init())
|
|
||||||
.subscribe(onSuccess: { events in
|
|
||||||
self.title = String.localizedStringWithFormat(NSLocalizedString("events found", comment: ""), events.count)
|
self.title = String.localizedStringWithFormat(NSLocalizedString("events found", comment: ""), events.count)
|
||||||
let pins = events.map(EventPin.init(event:))
|
let pins = events.map(EventPin.init(event:))
|
||||||
self.map.removeAnnotations(self.map.annotations)
|
self.map.removeAnnotations(self.map.annotations)
|
||||||
self.map.addAnnotations(pins)
|
self.map.addAnnotations(pins)
|
||||||
self.map.centerOnPins()
|
self.map.centerOnPins()
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
}, onFailure: { error in
|
} catch {
|
||||||
HUD.show(error: error)
|
HUD.show(error: error)
|
||||||
})
|
}
|
||||||
.disposed(by: self.bag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func close(_ sender: UIBarButtonItem) {
|
@IBAction func close(_ sender: UIBarButtonItem) {
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Eureka
|
import Eureka
|
||||||
import RxSwift
|
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
class LocationEditController: FormViewController {
|
class LocationEditController: FormViewController {
|
||||||
|
|
||||||
private let bag = DisposeBag()
|
|
||||||
private var doneButton: UIBarButtonItem!
|
private var doneButton: UIBarButtonItem!
|
||||||
|
|
||||||
var date = Date()
|
var date = Date()
|
||||||
var placemark: Placemark? = nil
|
var placemark: Placemark? = nil
|
||||||
|
|
||||||
var onDone: ((VehicleEvent) -> Void)?
|
var onDone: ((VehicleEventDto) -> Void)?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
@ -34,12 +32,13 @@ class LocationEditController: FormViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<<< LocationRow() { row in
|
// TODO: Use one of the standard rows (properly)
|
||||||
|
<<< /*LocationRow()*/LabelRow { row in
|
||||||
row.title = NSLocalizedString("Location", comment: "")
|
row.title = NSLocalizedString("Location", comment: "")
|
||||||
row.value = self.placemark
|
row.value = self.placemark?.address
|
||||||
}.onChange { row in
|
}.onChange { row in
|
||||||
if let newPlacemark = row.value {
|
if let newPlacemark = row.value {
|
||||||
self.placemark = newPlacemark
|
//self.placemark = newPlacemark
|
||||||
self.doneButton.isEnabled = true
|
self.doneButton.isEnabled = true
|
||||||
} else {
|
} else {
|
||||||
self.doneButton.isEnabled = false
|
self.doneButton.isEnabled = false
|
||||||
@ -49,7 +48,7 @@ class LocationEditController: FormViewController {
|
|||||||
|
|
||||||
@objc func doneTapped(_ sender: UIBarButtonItem) {
|
@objc func doneTapped(_ sender: UIBarButtonItem) {
|
||||||
guard let placemark = self.placemark else { return }
|
guard let placemark = self.placemark else { return }
|
||||||
let event = VehicleEvent(lat: placemark.latitude, lon: placemark.longitude)
|
var event = VehicleEventDto(lat: placemark.latitude, lon: placemark.longitude)
|
||||||
event.date = self.date.timeIntervalSince1970
|
event.date = self.date.timeIntervalSince1970
|
||||||
if let address = placemark.address {
|
if let address = placemark.address {
|
||||||
event.address = address
|
event.address = address
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import MapKit
|
import MapKit
|
||||||
import Eureka
|
|
||||||
import RxSwift
|
|
||||||
import Intents
|
import Intents
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
@ -11,14 +9,13 @@ public struct Placemark: Equatable {
|
|||||||
var address: String?
|
var address: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LocationPickerController : UIViewController, TypedRowControllerType, MKMapViewDelegate {
|
public class LocationPickerController : UIViewController, MKMapViewDelegate {
|
||||||
|
|
||||||
public var row: RowOf<Placemark>!
|
public var placemark: Placemark?
|
||||||
public var onDismissCallback: ((UIViewController) -> ())?
|
public var onDismissCallback: ((UIViewController) -> ())?
|
||||||
|
|
||||||
private let bag = DisposeBag()
|
|
||||||
private var geocodingDisposable: Disposable?
|
|
||||||
private var address: String?
|
private var address: String?
|
||||||
|
private var geocodingTask: Task<String?,Error>?
|
||||||
|
|
||||||
lazy var mapView : MKMapView = { [unowned self] in
|
lazy var mapView : MKMapView = { [unowned self] in
|
||||||
let v = MKMapView(frame: self.view.bounds)
|
let v = MKMapView(frame: self.view.bounds)
|
||||||
@ -89,7 +86,7 @@ public class LocationPickerController : UIViewController, TypedRowControllerType
|
|||||||
button.title = "Done"
|
button.title = "Done"
|
||||||
navigationItem.rightBarButtonItem = button
|
navigationItem.rightBarButtonItem = button
|
||||||
|
|
||||||
if let value = row.value {
|
if let value = placemark {
|
||||||
let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: value.latitude, longitude: value.longitude), latitudinalMeters: 1000, longitudinalMeters: 1000)
|
let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: value.latitude, longitude: value.longitude), latitudinalMeters: 1000, longitudinalMeters: 1000)
|
||||||
mapView.setRegion(region, animated: true)
|
mapView.setRegion(region, animated: true)
|
||||||
}
|
}
|
||||||
@ -111,11 +108,11 @@ public class LocationPickerController : UIViewController, TypedRowControllerType
|
|||||||
|
|
||||||
@objc func tappedDone(_ sender: UIBarButtonItem){
|
@objc func tappedDone(_ sender: UIBarButtonItem){
|
||||||
let target = mapView.convert(ellipsisLayer.position, toCoordinateFrom: mapView)
|
let target = mapView.convert(ellipsisLayer.position, toCoordinateFrom: mapView)
|
||||||
row.value = Placemark(latitude: target.latitude, longitude: target.longitude, address: self.address)
|
placemark = Placemark(latitude: target.latitude, longitude: target.longitude, address: self.address)
|
||||||
onDismissCallback?(self)
|
onDismissCallback?(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTitle(){
|
func updateTitle() {
|
||||||
let fmt = NumberFormatter()
|
let fmt = NumberFormatter()
|
||||||
fmt.maximumFractionDigits = 4
|
fmt.maximumFractionDigits = 4
|
||||||
fmt.minimumFractionDigits = 4
|
fmt.minimumFractionDigits = 4
|
||||||
@ -124,14 +121,15 @@ public class LocationPickerController : UIViewController, TypedRowControllerType
|
|||||||
title = "\(latitude), \(longitude)"
|
title = "\(latitude), \(longitude)"
|
||||||
|
|
||||||
self.address = nil
|
self.address = nil
|
||||||
self.geocodingDisposable?.dispose()
|
|
||||||
self.geocodingDisposable = RxLocationManager
|
geocodingTask?.cancel()
|
||||||
.getAddressForLocation(latitude: mapView.centerCoordinate.latitude, longitude: mapView.centerCoordinate.longitude)
|
geocodingTask = Task {
|
||||||
.observeOn(MainScheduler.instance)
|
address = try? await RxLocationManager.getAddressForLocation(latitude: mapView.centerCoordinate.latitude,
|
||||||
.subscribe(onSuccess: { address in
|
longitude: mapView.centerCoordinate.longitude)
|
||||||
self.title = address
|
title = address
|
||||||
self.address = address
|
geocodingTask = nil
|
||||||
})
|
return address
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
|
public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Eureka
|
//import Eureka
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
|
|
||||||
|
// TODO: Rewrite Eureka forms to native UIKit/SwiftUI
|
||||||
|
|
||||||
|
/*
|
||||||
public final class LocationRow: OptionsRow<PushSelectorCell<Placemark>>, PresenterRowType, RowType {
|
public final class LocationRow: OptionsRow<PushSelectorCell<Placemark>>, PresenterRowType, RowType {
|
||||||
|
|
||||||
public typealias PresenterRow = LocationPickerController
|
public typealias PresenterRow = LocationPickerController
|
||||||
@ -55,3 +58,4 @@ public final class LocationRow: OptionsRow<PushSelectorCell<Placemark>>, Present
|
|||||||
rowVC.row = self
|
rowVC.row = self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import AutoCatCore
|
|||||||
class ShowEventController: UIViewController {
|
class ShowEventController: UIViewController {
|
||||||
|
|
||||||
private var map = MKMapView()
|
private var map = MKMapView()
|
||||||
var event: VehicleEvent?
|
var event: VehicleEventDto?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import SwiftEntryKit
|
import SwiftEntryKit
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
import RxSwift
|
|
||||||
|
|
||||||
class MainTabController: UITabBarController, UITabBarControllerDelegate {
|
class MainTabController: UITabBarController, UITabBarControllerDelegate {
|
||||||
|
|
||||||
private let bag = DisposeBag()
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
self.delegate = self
|
self.delegate = self
|
||||||
@ -58,6 +55,6 @@ class MainTabController: UITabBarController, UITabBarControllerDelegate {
|
|||||||
|
|
||||||
// User probably just saw a vehicle and is about to start entering plate number
|
// User probably just saw a vehicle and is about to start entering plate number
|
||||||
// Requesting current location ASAP while we still close to initial location
|
// Requesting current location ASAP while we still close to initial location
|
||||||
RxLocationManager.requestCurrentLocation().subscribe().disposed(by: self.bag)
|
Task { try? await RxLocationManager.requestCurrentLocation() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,295 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
import AutoCatCore
|
|
||||||
import MobileCoreServices
|
|
||||||
import PKHUD
|
|
||||||
import RxSwift
|
|
||||||
import ExceptionCatcher
|
|
||||||
import RealmSwift
|
|
||||||
|
|
||||||
class NotesController: UIViewController, UITableViewDataSource, UITableViewDelegate {
|
|
||||||
|
|
||||||
@IBOutlet weak var notesTable: UITableView!
|
|
||||||
|
|
||||||
private var textView = UITextView()
|
|
||||||
private var bag = DisposeBag()
|
|
||||||
|
|
||||||
var vehicle: Vehicle? {
|
|
||||||
didSet {
|
|
||||||
if self.isViewLoaded {
|
|
||||||
self.notesTable.reloadData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
self.title = NSLocalizedString("Notes", comment: "")
|
|
||||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNote(_:)))
|
|
||||||
self.notesTable.reloadData()
|
|
||||||
self.hideKeyboardWhenTappedAround()
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func update(vehicle: Vehicle) -> Bool {
|
|
||||||
do {
|
|
||||||
if let v = self.vehicle, let realm = v.realm, !v.isFrozen {
|
|
||||||
try ExceptionCatcher.catch {
|
|
||||||
try realm.write {
|
|
||||||
realm.add(vehicle, update: .all)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.vehicle?.notes.removeAll()
|
|
||||||
self.vehicle?.notes.append(objectsIn: vehicle.notes)
|
|
||||||
}
|
|
||||||
self.notesTable.reloadData()
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
self.show(error: error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
return self.vehicle?.notes.count ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "VehicleNoteCell", for: indexPath) as? VehicleNoteCell else {
|
|
||||||
return UITableViewCell()
|
|
||||||
}
|
|
||||||
|
|
||||||
if let note = self.vehicle?.notes[indexPath.row] {
|
|
||||||
cell.configure(with: note)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
|
|
||||||
let copy = UIAction(title: NSLocalizedString("Copy", comment: ""), image: UIImage(systemName: "doc.on.doc")) { action in
|
|
||||||
self.copyNote(index: indexPath.row)
|
|
||||||
}
|
|
||||||
|
|
||||||
let edit = UIAction(title: NSLocalizedString("Edit", comment: ""), image: UIImage(systemName: "pencil")) { action in
|
|
||||||
self.editNote(index: indexPath.row)
|
|
||||||
}
|
|
||||||
|
|
||||||
let delete = UIAction(title: NSLocalizedString("Delete", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive) { action in
|
|
||||||
self.deleteNote(index: indexPath.row)
|
|
||||||
}
|
|
||||||
|
|
||||||
return UIMenu(title: NSLocalizedString("Actions", comment: ""), children: [copy, edit, delete])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
let copy = UIContextualAction(style: .normal, title: NSLocalizedString("Copy", comment: "")) { action, view, completion in
|
|
||||||
self.copyNote(index: indexPath.row)
|
|
||||||
completion(true)
|
|
||||||
}
|
|
||||||
copy.image = UIImage(systemName: "doc.on.doc")
|
|
||||||
copy.backgroundColor = .systemBlue
|
|
||||||
|
|
||||||
let delete = UIContextualAction(style: .destructive, title: NSLocalizedString("Delete", comment: "")) { action, view, completion in
|
|
||||||
self.deleteNote(index: indexPath.row, completion: completion)
|
|
||||||
}
|
|
||||||
delete.image = UIImage(systemName: "trash")
|
|
||||||
|
|
||||||
let edit = UIContextualAction(style: .normal, title: NSLocalizedString("Edit", comment: "")) { action, view, completion in
|
|
||||||
self.editNote(index: indexPath.row)
|
|
||||||
completion(true)
|
|
||||||
}
|
|
||||||
edit.image = UIImage(systemName: "pencil")
|
|
||||||
edit.backgroundColor = .systemBlue
|
|
||||||
|
|
||||||
let configuration = UISwipeActionsConfiguration(actions: [delete, edit, copy])
|
|
||||||
configuration.performsFirstActionWithFullSwipe = false
|
|
||||||
return configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Actions
|
|
||||||
|
|
||||||
@objc func addNote(_ sender: UIBarButtonItem) {
|
|
||||||
guard let vehicle = self.vehicle else {
|
|
||||||
HUD.flash(.labeledError(title: nil, subtitle: "Unknown vehicle"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.showAddNoteAlert(text: nil) { noteText in
|
|
||||||
let note = VehicleNote(text: noteText)
|
|
||||||
|
|
||||||
if vehicle.unrecognized {
|
|
||||||
if let realm = vehicle.realm {
|
|
||||||
try? realm.write {
|
|
||||||
vehicle.notes.append(note)
|
|
||||||
vehicle.updatedDate = Date().timeIntervalSince1970
|
|
||||||
}
|
|
||||||
self.notesTable.reloadData()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
HUD.show(.progress)
|
|
||||||
Api.add(notes: [note], to: vehicle.getNumber())
|
|
||||||
.observe(on: MainScheduler.instance)
|
|
||||||
.subscribe(onSuccess: {
|
|
||||||
HUD.hide()
|
|
||||||
self.update(vehicle: $0)
|
|
||||||
}, onFailure: { error in
|
|
||||||
HUD.hide()
|
|
||||||
self.show(error: error)
|
|
||||||
})
|
|
||||||
.disposed(by: self.bag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyNote(index: Int) {
|
|
||||||
guard let vehicle = self.vehicle else {
|
|
||||||
HUD.flash(.labeledError(title: nil, subtitle: "Unknown vehicle"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
UIPasteboard.general.setValue(vehicle.notes[index].text, forPasteboardType: kUTTypePlainText as String)
|
|
||||||
}
|
|
||||||
|
|
||||||
func editNote(index: Int) {
|
|
||||||
guard let vehicle = self.vehicle else {
|
|
||||||
HUD.flash(.labeledError(title: nil, subtitle: "Unknown vehicle"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let note = vehicle.notes[index]
|
|
||||||
self.showAddNoteAlert(text: note.text) { noteText in
|
|
||||||
|
|
||||||
if vehicle.unrecognized {
|
|
||||||
if let realm = vehicle.realm {
|
|
||||||
try? realm.write {
|
|
||||||
note.text = noteText
|
|
||||||
vehicle.updatedDate = Date().timeIntervalSince1970
|
|
||||||
}
|
|
||||||
self.notesTable.reloadData()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
HUD.show(.progress)
|
|
||||||
let newNote = note.clone()
|
|
||||||
newNote.text = noteText
|
|
||||||
Api.edit(note: newNote)
|
|
||||||
.observe(on: MainScheduler.instance)
|
|
||||||
.subscribe(onSuccess: {
|
|
||||||
HUD.hide()
|
|
||||||
self.update(vehicle: $0)
|
|
||||||
}, onFailure: { error in
|
|
||||||
HUD.hide()
|
|
||||||
self.show(error: error)
|
|
||||||
})
|
|
||||||
.disposed(by: self.bag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteNote(index: Int, completion: ((Bool) -> Void)? = nil) {
|
|
||||||
guard let vehicle = self.vehicle else {
|
|
||||||
HUD.flash(.labeledError(title: nil, subtitle: "Unknown vehicle"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let note = vehicle.notes[index]
|
|
||||||
|
|
||||||
if vehicle.unrecognized {
|
|
||||||
if let realm = vehicle.realm {
|
|
||||||
try? realm.write {
|
|
||||||
vehicle.notes.remove(at: index)
|
|
||||||
vehicle.updatedDate = Date().timeIntervalSince1970
|
|
||||||
realm.delete(note)
|
|
||||||
}
|
|
||||||
self.notesTable.reloadData()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
HUD.show(.progress)
|
|
||||||
Api.remove(note: note.id)
|
|
||||||
.observe(on: MainScheduler.instance)
|
|
||||||
.subscribe(onSuccess: { vehicle in
|
|
||||||
HUD.hide()
|
|
||||||
let result = self.update(vehicle: vehicle)
|
|
||||||
completion?(result)
|
|
||||||
}, onFailure: { error in
|
|
||||||
completion?(false)
|
|
||||||
HUD.hide()
|
|
||||||
self.show(error: error)
|
|
||||||
print(error)
|
|
||||||
}).disposed(by: self.bag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Utils
|
|
||||||
|
|
||||||
func showAddNoteAlert(text: String?, completion: @escaping (String) -> Void) {
|
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
showAddNoteAlertCatalyst(text: text, completion: completion)
|
|
||||||
#else
|
|
||||||
showAddNoteAlertIos(text: text, completion: completion)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
func showAddNoteAlertIos(text: String?, completion: @escaping (String) -> Void) {
|
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("New note", comment: ""), message: nil, preferredStyle: .alert)
|
|
||||||
|
|
||||||
let cancelAction = UIAlertAction.init(title: NSLocalizedString("Cancel", comment: ""), style: .default)
|
|
||||||
alertController.addAction(cancelAction)
|
|
||||||
|
|
||||||
let saveAction = UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default) { (action) in
|
|
||||||
let enteredText = self.textView.text ?? ""
|
|
||||||
completion(enteredText)
|
|
||||||
}
|
|
||||||
alertController.addAction(saveAction)
|
|
||||||
self.textView = UITextView()
|
|
||||||
self.textView.text = text
|
|
||||||
self.textView.addDoneButton(title: NSLocalizedString("Done", comment: ""), target: self, selector: #selector(tapDone(sender:)))
|
|
||||||
self.textView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
alertController.view.addSubview(self.textView)
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
self.textView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 60),
|
|
||||||
self.textView.bottomAnchor.constraint(equalTo: alertController.view.bottomAnchor, constant: -60),
|
|
||||||
self.textView.leadingAnchor.constraint(equalTo: alertController.view.leadingAnchor, constant: 8),
|
|
||||||
self.textView.trailingAnchor.constraint(equalTo: alertController.view.trailingAnchor, constant: -8),
|
|
||||||
self.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100)
|
|
||||||
])
|
|
||||||
|
|
||||||
self.present(alertController, animated: true) {
|
|
||||||
self.textView.becomeFirstResponder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func tapDone(sender: Any) {
|
|
||||||
self.textView.endEditing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func showAddNoteAlertCatalyst(text: String?, completion: @escaping (String) -> Void) {
|
|
||||||
let alertController = UIAlertController(title: NSLocalizedString("New note", comment: ""), message: nil, preferredStyle: .alert)
|
|
||||||
|
|
||||||
let cancelAction = UIAlertAction.init(title: NSLocalizedString("Cancel", comment: ""), style: .default)
|
|
||||||
alertController.addAction(cancelAction)
|
|
||||||
|
|
||||||
let saveAction = UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default) { (action) in
|
|
||||||
let enteredText = alertController.textFields?.first?.text ?? ""
|
|
||||||
completion(enteredText)
|
|
||||||
}
|
|
||||||
alertController.addAction(saveAction)
|
|
||||||
|
|
||||||
alertController.addTextField { textField in
|
|
||||||
textField.text = text
|
|
||||||
}
|
|
||||||
|
|
||||||
self.present(alertController, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
import WebKit
|
|
||||||
import PKHUD
|
|
||||||
|
|
||||||
class DkbmController: UIViewController, WKScriptMessageHandlerWithReply {
|
|
||||||
|
|
||||||
private var webView: WKWebView!
|
|
||||||
private var captchaAdded = false
|
|
||||||
|
|
||||||
var onDone: ((String) -> Void)?
|
|
||||||
var checkSource: OsagoCheckSource?
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
let config = WKWebViewConfiguration()
|
|
||||||
if let jsPath = Bundle.main.path(forResource: "dkbm", ofType: "js") {
|
|
||||||
let js = try? String(contentsOfFile: jsPath)
|
|
||||||
let contentController = WKUserContentController()
|
|
||||||
let script = WKUserScript(source: js!, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
|
|
||||||
contentController.addUserScript(script)
|
|
||||||
if #available(iOS 14.0, *) {
|
|
||||||
contentController.addScriptMessageHandler(self, contentWorld: .page, name: "dkbmHandler")
|
|
||||||
}
|
|
||||||
config.userContentController = contentController
|
|
||||||
}
|
|
||||||
|
|
||||||
self.webView = WKWebView(frame: .zero, configuration: config)
|
|
||||||
self.webView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
self.view.addSubview(self.webView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
self.webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
|
|
||||||
self.webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
|
|
||||||
self.webView.topAnchor.constraint(equalTo: self.view.topAnchor),
|
|
||||||
self.webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
|
|
||||||
])
|
|
||||||
|
|
||||||
//self.webView.isHidden = true
|
|
||||||
//HUD.show(.progress)
|
|
||||||
|
|
||||||
let url = URL(string: "https://dkbm-web.autoins.ru/dkbm-web-1.0/policyInfo.htm")!
|
|
||||||
let request = URLRequest(url: url)
|
|
||||||
self.webView.load(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - WKScriptMessageHandler
|
|
||||||
|
|
||||||
func userContentController(_ userContentController: WKUserContentController,
|
|
||||||
didReceive message: WKScriptMessage,
|
|
||||||
replyHandler: @escaping (Any?, String?) -> Void) {
|
|
||||||
|
|
||||||
guard let msg = message.body as? [String:String], let checkSource else { return }
|
|
||||||
|
|
||||||
if msg.contains(where: { $0.key == "loaded" }) {
|
|
||||||
switch checkSource {
|
|
||||||
case .plateNumber(let number):
|
|
||||||
replyHandler(["plateNumber": number], nil)
|
|
||||||
case .vin(let number):
|
|
||||||
replyHandler(["vin", number], nil)
|
|
||||||
}
|
|
||||||
} else if let urlString = msg["url"], let url = URL(string: urlString) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
import Eureka
|
|
||||||
import PKHUD
|
|
||||||
import RxSwift
|
|
||||||
import RxCocoa
|
|
||||||
import AutoCatCore
|
|
||||||
|
|
||||||
enum OsagoCheckSource: Equatable, CustomStringConvertible {
|
|
||||||
case plateNumber(number: String)
|
|
||||||
case vin(number: String)
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
case .plateNumber(let number):
|
|
||||||
return NSLocalizedString("plate number", comment: "Check by") + " (\(number))"
|
|
||||||
case .vin(let number):
|
|
||||||
return "VIN (\(number))"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OsagoAddController: FormViewController {
|
|
||||||
|
|
||||||
private let bag = DisposeBag()
|
|
||||||
var checkSources: [OsagoCheckSource] = []
|
|
||||||
var onDone: ((Vehicle) -> Void)?
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
self.title = NSLocalizedString("OSAGO check", comment: "")
|
|
||||||
|
|
||||||
form +++ Section(NSLocalizedString("Check parameters", comment: ""))
|
|
||||||
<<< DateTimeInlineRow("date") { row in
|
|
||||||
row.title = NSLocalizedString("Check date", comment: "")
|
|
||||||
row.value = Date()
|
|
||||||
}
|
|
||||||
<<< PickerInlineRow<OsagoCheckSource>("SourcePicker") { row in
|
|
||||||
row.title = NSLocalizedString("Check by", comment: "")
|
|
||||||
row.value = self.checkSources.first
|
|
||||||
row.options = self.checkSources
|
|
||||||
}
|
|
||||||
|
|
||||||
form +++ Section()
|
|
||||||
<<< ButtonRow() { $0.title = NSLocalizedString("Check", comment: "verb") }.onCellSelection { _, _ in
|
|
||||||
guard let source = (self.form.rowBy(tag: "SourcePicker") as? PickerInlineRow<OsagoCheckSource>)?.value,
|
|
||||||
let date = (self.form.rowBy(tag: "date") as? DateTimeInlineRow)?.value
|
|
||||||
else { return }
|
|
||||||
|
|
||||||
let controller = DkbmController()
|
|
||||||
controller.checkSource = source
|
|
||||||
controller.onDone = { token in
|
|
||||||
self.navigationController?.popViewController(animated: true, completion: {
|
|
||||||
|
|
||||||
var number, vin: String?
|
|
||||||
switch source {
|
|
||||||
case .plateNumber(let n):
|
|
||||||
number = n
|
|
||||||
case .vin(let v):
|
|
||||||
vin = v
|
|
||||||
}
|
|
||||||
HUD.show(.progress)
|
|
||||||
Api.checkOsago(number: number, vin: vin, date: date, token: token)
|
|
||||||
.observe(on: MainScheduler.instance)
|
|
||||||
.subscribe { vehicle in
|
|
||||||
HUD.hide()
|
|
||||||
self.onDone?(vehicle)
|
|
||||||
} onFailure: { err in
|
|
||||||
HUD.show(error: err)
|
|
||||||
}
|
|
||||||
.disposed(by: self.bag)
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
|
||||||
//self.present(controller, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
import Eureka
|
|
||||||
import PKHUD
|
|
||||||
import AutoCatCore
|
|
||||||
|
|
||||||
class OsagoController: FormViewController {
|
|
||||||
|
|
||||||
var vehicle: Vehicle? {
|
|
||||||
didSet {
|
|
||||||
self.updateInterface()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
self.title = NSLocalizedString("OSAGO contracts", comment: "")
|
|
||||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(checkNewDate(_:)))
|
|
||||||
|
|
||||||
self.tableView.rowHeight = UITableView.automaticDimension
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func checkNewDate(_ sender: UIBarButtonItem) {
|
|
||||||
guard let vehicle = self.vehicle else { return }
|
|
||||||
|
|
||||||
let sb = UIStoryboard(name: "Main", bundle: nil)
|
|
||||||
let controller = sb.instantiateViewController(identifier: "OsagoAddController") as OsagoAddController
|
|
||||||
controller.checkSources = [.plateNumber(number: vehicle.getNumber())]
|
|
||||||
if let vin = vehicle.vin1, !vin.contains("*") {
|
|
||||||
controller.checkSources.append(.vin(number: vin))
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.onDone = { vehicle in
|
|
||||||
self.navigationController?.popViewController(animated: true, completion: {
|
|
||||||
self.update(vehicle: vehicle)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateInterface() {
|
|
||||||
guard let vehicle = self.vehicle else { return }
|
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .medium
|
|
||||||
formatter.timeStyle = .none
|
|
||||||
|
|
||||||
self.form.removeAll()
|
|
||||||
for osago in vehicle.osagoContracts.sorted(by: { $0.date < $1.date }) {
|
|
||||||
self.form +++ Section(formatter.string(from: Date(timeIntervalSince1970: osago.date)))
|
|
||||||
<<< self.multilineRow(NSLocalizedString("Contract series and number", comment: ""), value: osago.number)
|
|
||||||
<<< self.multilineRow(NSLocalizedString("Insurance organization name", comment: ""), value: osago.name)
|
|
||||||
<<< self.multilineRow(NSLocalizedString("OSAGO contract status", comment: ""), value: osago.status)
|
|
||||||
<<< self.multilineRow(NSLocalizedString("Insurant", comment: ""), value: osago.insurant)
|
|
||||||
<<< self.multilineRow(NSLocalizedString("Owner", comment: ""), value: osago.owner)
|
|
||||||
<<< self.multilineRow(NSLocalizedString("Birthday", comment: ""), value: osago.birthday)
|
|
||||||
<<< self.multilineRow(NSLocalizedString("Vehicle usage region", comment: ""), value: osago.usageRegion)
|
|
||||||
<<< self.multilineRow(NSLocalizedString("Contract restrictions", comment: ""), value: osago.restrictions)
|
|
||||||
<<< self.row(NSLocalizedString("Plate number", comment: ""), value: osago.plateNumber)
|
|
||||||
<<< self.row(NSLocalizedString("VIN", comment: ""), value: osago.vin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func row(_ title: String, value: String?) -> LabelRow {
|
|
||||||
LabelRow() { row in
|
|
||||||
if let cell = row.cell, let label = cell.detailTextLabel, let titleLabel = cell.textLabel {
|
|
||||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
titleLabel.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 8).isActive = true
|
|
||||||
titleLabel.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor).isActive = true
|
|
||||||
|
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
label.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 8).isActive = true
|
|
||||||
label.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -8).isActive = true
|
|
||||||
label.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8).isActive = true
|
|
||||||
label.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor).isActive = true
|
|
||||||
label.numberOfLines = 0
|
|
||||||
label.font = UIFont.preferredFont(forTextStyle: .subheadline)
|
|
||||||
}
|
|
||||||
|
|
||||||
row.title = title
|
|
||||||
row.value = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func multilineRow(_ title: String, value: String?) -> MultilineLabelRow {
|
|
||||||
MultilineLabelRow() { row in
|
|
||||||
row.title = title
|
|
||||||
row.value = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(vehicle: Vehicle) {
|
|
||||||
do {
|
|
||||||
if let realm = self.vehicle?.realm {
|
|
||||||
try realm.write {
|
|
||||||
realm.add(vehicle, update: .all)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.vehicle?.osagoContracts.removeAll()
|
|
||||||
self.vehicle?.osagoContracts.append(objectsIn: vehicle.osagoContracts)
|
|
||||||
}
|
|
||||||
self.updateInterface()
|
|
||||||
} catch {
|
|
||||||
HUD.show(error: error)
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
import Eureka
|
|
||||||
import AutoCatCore
|
|
||||||
|
|
||||||
class OwnersController: FormViewController {
|
|
||||||
|
|
||||||
public var owners: [VehicleOwnershipPeriod] = []
|
|
||||||
|
|
||||||
private var formatter = DateFormatter()
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
self.tableView.rowHeight = UITableView.automaticDimension
|
|
||||||
|
|
||||||
self.formatter.dateStyle = .long
|
|
||||||
self.formatter.timeStyle = .none
|
|
||||||
|
|
||||||
self.title = String.localizedStringWithFormat(NSLocalizedString("owners count", comment: ""), self.owners.count)
|
|
||||||
|
|
||||||
for (index, owner) in self.owners.enumerated() {
|
|
||||||
|
|
||||||
let fromDate = Date(timeIntervalSince1970: TimeInterval(owner.from/1000))
|
|
||||||
let from = self.formatter.string(from: fromDate)
|
|
||||||
var to = NSLocalizedString("now", comment: "")
|
|
||||||
if owner.to > 0 {
|
|
||||||
let toDate = Date(timeIntervalSince1970: TimeInterval(owner.to/1000))
|
|
||||||
to = self.formatter.string(from: toDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
let section = Section(header: from + " - " + to, footer: owner.lastOperation)
|
|
||||||
form +++ section
|
|
||||||
<<< LabelRow("Owner\(index)") { row in
|
|
||||||
row.title = NSLocalizedString("Owner type", comment: "")
|
|
||||||
row.value = NSLocalizedString(owner.ownerType, comment: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let vehicleRegistrationRegion = owner.region {
|
|
||||||
section <<< MultilineLabelRow("VehicleRegion\(index)") { row in
|
|
||||||
row.title = NSLocalizedString("Vehicle region", comment: "")
|
|
||||||
row.value = vehicleRegistrationRegion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if let driverLocality = owner.locality {
|
|
||||||
var dRegion = driverLocality
|
|
||||||
if let driverRegion = owner.registrationRegion {
|
|
||||||
dRegion += " (\(driverRegion))"
|
|
||||||
}
|
|
||||||
section <<< MultilineLabelRow("DriverRegion\(index)") { row in
|
|
||||||
row.title = NSLocalizedString("Driver region", comment: "")
|
|
||||||
row.value = dRegion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let code = owner.code {
|
|
||||||
section <<< MultilineLabelRow("Code\(index)") { row in
|
|
||||||
row.title = NSLocalizedString("ZIP (or OKTMO) code", comment: "")
|
|
||||||
row.value = code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
self.navigationController?.setNavigationBarHidden(false, animated: animated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
import RxSwift
|
|
||||||
import Intents
|
import Intents
|
||||||
import CoreSpotlight
|
import CoreSpotlight
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
@ -15,8 +14,6 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
|
|
||||||
var recorder: Recorder?
|
var recorder: Recorder?
|
||||||
var addButton: UIBarButtonItem!
|
var addButton: UIBarButtonItem!
|
||||||
let bag = DisposeBag()
|
|
||||||
var recordDisposable: Disposable?
|
|
||||||
var audioSessionObserver: NSObjectProtocol?
|
var audioSessionObserver: NSObjectProtocol?
|
||||||
var recordsDataSource: RealmSectionedDataSource<AudioRecord, AudioRecordCell>!
|
var recordsDataSource: RealmSectionedDataSource<AudioRecord, AudioRecordCell>!
|
||||||
|
|
||||||
@ -94,19 +91,19 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
var alert: UIAlertController?
|
var alert: UIAlertController?
|
||||||
var url: URL!
|
var url: URL!
|
||||||
|
|
||||||
let locationObservable = RxLocationManager.requestCurrentLocation()
|
Task {
|
||||||
.map(Optional.init)
|
do {
|
||||||
.catchAndReturn(nil)
|
async let locationTask = RxLocationManager.requestCurrentLocation()
|
||||||
|
async let permissionTask: () = recorder.requestPermissions()
|
||||||
|
let (event, _) = try await (locationTask, permissionTask)
|
||||||
|
|
||||||
let recordObservable: Single<String> = recorder.requestPermissions()
|
await makeStartSoundIfNeeded()
|
||||||
.observe(on: MainScheduler.instance)
|
|
||||||
.flatMap(self.makeStartSoundIfNeeded)
|
#if targetEnvironment(macCatalyst) || targetEnvironment(simulator)
|
||||||
.flatMap {
|
|
||||||
#if targetEnvironment(macCatalyst) || targetEnvironment(simulator)
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
alert = self.showRecordingAlert()
|
alert = self.showRecordingAlert()
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if let observer = self.audioSessionObserver {
|
if let observer = self.audioSessionObserver {
|
||||||
NotificationCenter.default.removeObserver(observer, name: AVAudioSession.routeChangeNotification, object: nil)
|
NotificationCenter.default.removeObserver(observer, name: AVAudioSession.routeChangeNotification, object: nil)
|
||||||
}
|
}
|
||||||
@ -120,27 +117,28 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
alert = self.showRecordingAlert()
|
alert = self.showRecordingAlert()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
let date = Date()
|
let date = Date()
|
||||||
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
|
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
|
||||||
url = try FileManager.default.url(for: fileName, in: "recordings")
|
url = try FileManager.default.url(for: fileName, in: "recordings")
|
||||||
|
|
||||||
return recorder.startRecording(to: url)
|
let text = try await recorder.startRecording(to: url)
|
||||||
}
|
|
||||||
|
|
||||||
self.recordDisposable = Single.zip(locationObservable, recordObservable) { event, text -> AudioRecord in
|
|
||||||
let asset = AVURLAsset(url: url)
|
let asset = AVURLAsset(url: url)
|
||||||
let duration = TimeInterval(CMTimeGetSeconds(asset.duration))
|
let duration = TimeInterval(CMTimeGetSeconds(asset.duration))
|
||||||
return AudioRecord(path: url.lastPathComponent, number: self.getPlateNumber(from: text), raw: text, duration: duration, event: event)
|
let record = AudioRecordDto(path: url.lastPathComponent,
|
||||||
}
|
number: self.getPlateNumber(from: text),
|
||||||
.subscribe(onSuccess: { record in
|
raw: text,
|
||||||
let realm = try? Realm()
|
duration: duration,
|
||||||
try? realm?.write {
|
event: event)
|
||||||
realm?.add(record)
|
|
||||||
|
let realm = try await Realm()
|
||||||
|
try realm.write {
|
||||||
|
realm.add(AudioRecord(dto: record))
|
||||||
}
|
}
|
||||||
alert?.dismiss(animated: true)
|
alert?.dismiss(animated: true)
|
||||||
self.addButton.isEnabled = true
|
self.addButton.isEnabled = true
|
||||||
}, onFailure: { error in
|
} catch {
|
||||||
if let alert = alert {
|
if let alert = alert {
|
||||||
alert.dismiss(animated: true) {
|
alert.dismiss(animated: true) {
|
||||||
HUD.show(error: error)
|
HUD.show(error: error)
|
||||||
@ -149,12 +147,13 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
HUD.show(error: error)
|
HUD.show(error: error)
|
||||||
}
|
}
|
||||||
self.addButton.isEnabled = true
|
self.addButton.isEnabled = true
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showRecordingAlert() -> UIAlertController {
|
func showRecordingAlert() -> UIAlertController {
|
||||||
let alert = UIAlertController(title: NSLocalizedString("Recording...", comment: ""), message: nil, preferredStyle: .alert)
|
let alert = UIAlertController(title: NSLocalizedString("Recording...", comment: ""), message: nil, preferredStyle: .alert)
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { _ in self.recordDisposable?.dispose() }))
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { _ in self.recorder?.cancelRecording() }))
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default, handler: { _ in self.recorder?.stopRecording() }))
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default, handler: { _ in self.recorder?.stopRecording() }))
|
||||||
self.present(alert, animated: true)
|
self.present(alert, animated: true)
|
||||||
return alert
|
return alert
|
||||||
@ -207,18 +206,17 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
&& region! < 1000
|
&& region! < 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeStartSoundIfNeeded() -> Single<Void> {
|
func makeStartSoundIfNeeded() async {
|
||||||
if !Settings.shared.recordBeep {
|
guard Settings.shared.recordBeep else {
|
||||||
return .just(())
|
return
|
||||||
} else {
|
}
|
||||||
return Single<Void>.create { observer in
|
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
var soundId = SystemSoundID()
|
var soundId = SystemSoundID()
|
||||||
let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/short_double_high.caf")
|
let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/short_double_high.caf")
|
||||||
AudioServicesCreateSystemSoundID(url as CFURL, &soundId)
|
AudioServicesCreateSystemSoundID(url as CFURL, &soundId)
|
||||||
AudioServicesPlaySystemSoundWithCompletion(soundId) {
|
AudioServicesPlaySystemSoundWithCompletion(soundId) {
|
||||||
observer(.success(()))
|
continuation.resume()
|
||||||
}
|
|
||||||
return Disposables.create()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -297,7 +295,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
return configuration
|
return configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
func moreActions(for record: AudioRecord, cell: UITableViewCell) {
|
func moreActions(for record: AudioRecordDto, cell: UITableViewCell) {
|
||||||
let sheet = UIAlertController(title: NSLocalizedString("More actions", comment: ""), message: nil, preferredStyle: .actionSheet)
|
let sheet = UIAlertController(title: NSLocalizedString("More actions", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||||
let cancel = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in sheet.dismiss(animated: true, completion: nil) }
|
let cancel = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in sheet.dismiss(animated: true, completion: nil) }
|
||||||
let share = UIAlertAction(title: NSLocalizedString("Share", comment: ""), style: .default) { _ in
|
let share = UIAlertAction(title: NSLocalizedString("Share", comment: ""), style: .default) { _ in
|
||||||
@ -325,19 +323,19 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
self.present(sheet, animated: true, completion: nil)
|
self.present(sheet, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func check(number: String, event: VehicleEvent?) {
|
func check(number: String, event: VehicleEventDto?) {
|
||||||
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
|
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
|
||||||
ad.quickAction = .checkNumber(number, event)
|
ad.quickAction = .checkNumber(number, event)
|
||||||
self.tabBarController?.selectedIndex = 0
|
self.tabBarController?.selectedIndex = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func edit(record: AudioRecord) {
|
func edit(record: AudioRecordDto) {
|
||||||
let alert = UIAlertController(title: NSLocalizedString("Edit plate number", comment: ""), message: nil, preferredStyle: .alert)
|
let alert = UIAlertController(title: NSLocalizedString("Edit plate number", comment: ""), message: nil, preferredStyle: .alert)
|
||||||
let done = UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default) { action in
|
let done = UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default) { action in
|
||||||
guard let tf = alert.textFields?.first else { return }
|
guard let tf = alert.textFields?.first else { return }
|
||||||
if let realm = try? Realm() {
|
if let realm = try? Realm(), let realmRecord = realm.object(ofType: AudioRecord.self, forPrimaryKey: record.path) {
|
||||||
try? realm.write {
|
try? realm.write {
|
||||||
record.number = tf.text?.uppercased()
|
realmRecord.number = tf.text?.uppercased()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -347,18 +345,20 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
}))
|
}))
|
||||||
alert.addTextField { tf in
|
alert.addTextField { tf in
|
||||||
tf.text = record.number ?? record.rawText.replacingOccurrences(of: " ", with: "")
|
tf.text = record.number ?? record.rawText.replacingOccurrences(of: " ", with: "")
|
||||||
NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: tf, queue: OperationQueue.main) { _ in
|
NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: tf, queue: nil) { _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
done.isEnabled = self.valid(number: tf.text?.uppercased() ?? "")
|
done.isEnabled = self.valid(number: tf.text?.uppercased() ?? "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
self.present(alert, animated: true)
|
self.present(alert, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(record: AudioRecord) {
|
func delete(record: AudioRecordDto) {
|
||||||
do {
|
do {
|
||||||
if let realm = record.realm {
|
if let realm = try? Realm(), let realmRecord = realm.object(ofType: AudioRecord.self, forPrimaryKey: record.path) {
|
||||||
try realm.write {
|
try realm.write {
|
||||||
realm.delete(record)
|
realm.delete(realmRecord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -366,7 +366,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func share(record: AudioRecord) {
|
func share(record: AudioRecordDto) {
|
||||||
do {
|
do {
|
||||||
let url = try FileManager.default.url(for: record.path, in: "recordings")
|
let url = try FileManager.default.url(for: record.path, in: "recordings")
|
||||||
let controller = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
let controller = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
||||||
@ -377,7 +377,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showOnMap(_ record: AudioRecord) {
|
func showOnMap(_ record: AudioRecordDto) {
|
||||||
let controller = ShowEventController()
|
let controller = ShowEventController()
|
||||||
controller.event = record.event
|
controller.event = record.event
|
||||||
controller.hidesBottomBarWhenPushed = true
|
controller.hidesBottomBarWhenPushed = true
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
class RegionsDataSourse: UITableViewDiffableDataSource<VehicleRegion, Int> {
|
class RegionsDataSourse: UITableViewDiffableDataSource<VehicleRegion, Int> {
|
||||||
@ -20,7 +19,6 @@ class RegionsController: UIViewController, UISearchResultsUpdating, UITableViewD
|
|||||||
var datasource: RegionsDataSourse!
|
var datasource: RegionsDataSourse!
|
||||||
|
|
||||||
let searchController = UISearchController(searchResultsController: nil)
|
let searchController = UISearchController(searchResultsController: nil)
|
||||||
let bag = DisposeBag()
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
@ -47,14 +45,16 @@ class RegionsController: UIViewController, UISearchResultsUpdating, UITableViewD
|
|||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
Api.getRegions().observeOn(MainScheduler.instance).subscribe(onSuccess: { regions in
|
Task {
|
||||||
self.regions = regions
|
do {
|
||||||
|
self.regions = try await ApiService.shared.getRegions()
|
||||||
self.regionsFiltered = regions
|
self.regionsFiltered = regions
|
||||||
self.updateTableView()
|
self.updateTableView()
|
||||||
self.applySelection()
|
self.applySelection()
|
||||||
}, onError: { error in
|
} catch {
|
||||||
print("Get regions error: ", error)
|
print("Get regions error: ", error)
|
||||||
}).disposed(by: self.bag)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
|||||||
@ -7,21 +7,19 @@ import AutoCatCore
|
|||||||
import SwiftEntryKit
|
import SwiftEntryKit
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import RxSwift
|
|
||||||
|
|
||||||
class ReportController: FormViewController, MediaBrowserViewControllerDataSource, MediaBrowserViewControllerDelegate, UIActivityItemSource {
|
class ReportController: FormViewController, MediaBrowserViewControllerDataSource, MediaBrowserViewControllerDelegate {
|
||||||
|
|
||||||
@IBOutlet weak var actionBarItem: UIBarButtonItem!
|
@IBOutlet weak var actionBarItem: UIBarButtonItem!
|
||||||
@IBOutlet weak var copyBarItem: UIBarButtonItem!
|
@IBOutlet weak var copyBarItem: UIBarButtonItem!
|
||||||
|
|
||||||
private var reportImageUrl: URL?
|
|
||||||
private let logoPlaceholder = UIImage(named: "SteeringWheel")
|
private let logoPlaceholder = UIImage(named: "SteeringWheel")
|
||||||
|
|
||||||
private let copyableTags = ["Model", "Year", "Color", "Category", "STP", "Japanese",
|
private let copyableTags = ["Model", "Year", "Color", "Category", "STP", "Japanese",
|
||||||
"PlateNumber", "VIN", "STS", "PTS",
|
"PlateNumber", "VIN", "STS", "PTS",
|
||||||
"EngineNumber", "FuelType", "Volume", "PowerHP", "PowerKw"];
|
"EngineNumber", "FuelType", "Volume", "PowerHP", "PowerKw"];
|
||||||
|
|
||||||
var vehicle: Vehicle? {
|
var vehicle: VehicleDto? {
|
||||||
didSet {
|
didSet {
|
||||||
if isViewLoaded && self.view.window != nil {
|
if isViewLoaded && self.view.window != nil {
|
||||||
self.updateReport()
|
self.updateReport()
|
||||||
@ -37,15 +35,13 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
if let realm = try? Realm(), let num = number {
|
if let realm = try? Realm(), let num = number {
|
||||||
let vehicles = realm.objects(Vehicle.self).filter("number = %@", num)
|
let vehicles = realm.objects(Vehicle.self).filter("number = %@", num)
|
||||||
self.notificationToken?.invalidate()
|
self.notificationToken?.invalidate()
|
||||||
self.notificationToken = vehicles.observe { _ in self.vehicle = vehicles.first }
|
self.notificationToken = vehicles.observe { _ in self.vehicle = vehicles.first?.dto }
|
||||||
} else {
|
} else {
|
||||||
self.vehicle = nil
|
self.vehicle = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bag = DisposeBag()
|
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
@ -93,10 +89,10 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
<<< LabelRow("OSAGO") { $0.title = NSLocalizedString("OSAGO", comment: "") }
|
<<< LabelRow("OSAGO") { $0.title = NSLocalizedString("OSAGO", comment: "") }
|
||||||
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
|
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
|
||||||
.onCellSelection { _, _ in
|
.onCellSelection { _, _ in
|
||||||
let sb = UIStoryboard(name: "Main", bundle: nil)
|
if let contracts = self.vehicle?.osagoContracts, let navController = self.navigationController {
|
||||||
let controller = sb.instantiateViewController(identifier: "OsagoController") as OsagoController
|
let coordinator = OsagoCoordinator(navController: navController, contracts: contracts)
|
||||||
controller.vehicle = self.vehicle
|
Task { try await coordinator.start() }
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<<< LabelRow("Owners") { row in
|
<<< LabelRow("Owners") { row in
|
||||||
@ -106,10 +102,10 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
|
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
|
||||||
.onCellSelection { _, row in
|
.onCellSelection { _, row in
|
||||||
if row.value != "0" {
|
if row.value != "0" {
|
||||||
let sb = UIStoryboard(name: "Main", bundle: nil)
|
if let ownerships = self.vehicle?.ownershipPeriods, let navController = self.navigationController {
|
||||||
let controller = sb.instantiateViewController(identifier: "OwnersController") as OwnersController
|
let coordinator = OwnersCoordinator(navController: navController, ownerships: ownerships)
|
||||||
controller.owners = self.vehicle?.ownershipPeriods.toArray() ?? []
|
Task { try await coordinator.start() }
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +130,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
|
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
|
||||||
.onCellSelection { _, row in
|
.onCellSelection { _, row in
|
||||||
let controller = AdsController()
|
let controller = AdsController()
|
||||||
controller.ads = self.vehicle?.ads.toArray() ?? []
|
controller.ads = self.vehicle?.ads ?? []
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
self.navigationController?.pushViewController(controller, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,10 +139,10 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
}
|
}
|
||||||
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
|
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
|
||||||
.onCellSelection { _, row in
|
.onCellSelection { _, row in
|
||||||
let sb = UIStoryboard(name: "Main", bundle: nil)
|
if let vehicle = self.vehicle, let navController = self.navigationController {
|
||||||
let controller = sb.instantiateViewController(identifier: "NotesController") as NotesController
|
let coordinator = NotesCoordinator(navController: navController, vehicle: vehicle)
|
||||||
controller.vehicle = self.vehicle
|
Task { try await coordinator.start() }
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if Settings.shared.showDebugInfo {
|
if Settings.shared.showDebugInfo {
|
||||||
@ -160,7 +156,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
|
|
||||||
form +++ Section("")
|
form +++ Section("")
|
||||||
<<< ButtonRow("CheckGB") { $0.title = NSLocalizedString("Check GB", comment: "") }.onCellSelection { cell, row in
|
<<< ButtonRow("CheckGB") { $0.title = NSLocalizedString("Check GB", comment: "") }.onCellSelection { cell, row in
|
||||||
self.checkGB()
|
Task { await self.checkGB() }
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCopyBehaviour()
|
setupCopyBehaviour()
|
||||||
@ -213,7 +209,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(sourceStatusRow tag: String, with value: DebugInfoEntry) {
|
func update(sourceStatusRow tag: String, with value: DebugInfoEntryDto) {
|
||||||
if let row = self.form.rowBy(tag: tag) as? SourceStatusRow {
|
if let row = self.form.rowBy(tag: tag) as? SourceStatusRow {
|
||||||
row.value = value
|
row.value = value
|
||||||
row.reload()
|
row.reload()
|
||||||
@ -279,27 +275,31 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
self.updateReport()
|
self.updateReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGB() {
|
func checkGB() async {
|
||||||
guard let vehicle = self.vehicle else { return }
|
guard let vehicle = self.vehicle else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
Api.checkVehicleGb(by: vehicle.getNumber()).observe(on: MainScheduler.instance).subscribe(onSuccess: { newVehicle in
|
let newVehicle = try await ApiService.shared.checkVehicleGb(by: vehicle.getNumber())
|
||||||
if let realm = vehicle.realm, !vehicle.isFrozen {
|
|
||||||
|
let realm = try await Realm()
|
||||||
|
if let realmVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) {
|
||||||
try? realm.write {
|
try? realm.write {
|
||||||
realm.add(newVehicle, update: .all)
|
realm.add(Vehicle(dto: newVehicle), update: .all)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.vehicle?.vin1 = newVehicle.vin1
|
self.vehicle?.vin1 = newVehicle.vin1
|
||||||
self.vehicle?.color = newVehicle.color
|
self.vehicle?.color = newVehicle.color
|
||||||
self.vehicle?.sts = newVehicle.sts
|
self.vehicle?.sts = newVehicle.sts
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateReport()
|
self.updateReport()
|
||||||
self.form.allSections.forEach { $0.reload() }
|
self.form.allSections.forEach { $0.reload() }
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
}, onFailure: { error in
|
} catch {
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
self.show(error: error)
|
self.show(error: error)
|
||||||
}).disposed(by: self.bag)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MediaBrowserViewControllerDataSource & MediaBrowserViewControllerDelegate
|
// MARK: - MediaBrowserViewControllerDataSource & MediaBrowserViewControllerDelegate
|
||||||
@ -316,6 +316,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
}
|
}
|
||||||
|
|
||||||
KingfisherManager.shared.retrieveImage(with: url) { result in
|
KingfisherManager.shared.retrieveImage(with: url) { result in
|
||||||
|
Task { @MainActor in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let res):
|
case .success(let res):
|
||||||
completion(index, res.image, ZoomScale.default, nil)
|
completion(index, res.image, ZoomScale.default, nil)
|
||||||
@ -326,6 +327,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, didChangeFocusTo index: Int) {
|
func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, didChangeFocusTo index: Int) {
|
||||||
guard let photo = self.vehicle?.photos[index] else { return }
|
guard let photo = self.vehicle?.photos[index] else { return }
|
||||||
@ -350,10 +352,11 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
let fileURL = documentDirectory.appendingPathComponent("report.png")
|
let fileURL = documentDirectory.appendingPathComponent("report.png")
|
||||||
if let imageData = image.pngData() {
|
if let imageData = image.pngData() {
|
||||||
try imageData.write(to: fileURL)
|
try imageData.write(to: fileURL)
|
||||||
self.reportImageUrl = fileURL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = UIActivityViewController(activityItems: [self], applicationActivities: nil)
|
let item = ActivityItemSource(url: fileURL, title: vehicle.getNumber())
|
||||||
|
|
||||||
|
let controller = UIActivityViewController(activityItems: [item], applicationActivities: nil)
|
||||||
controller.popoverPresentationController?.barButtonItem = sender
|
controller.popoverPresentationController?.barButtonItem = sender
|
||||||
self.present(controller, animated: true)
|
self.present(controller, animated: true)
|
||||||
} catch {
|
} catch {
|
||||||
@ -365,12 +368,13 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
guard let vehicle = self.vehicle else { return }
|
guard let vehicle = self.vehicle else { return }
|
||||||
var items: [Any] = [vehicle.reportText()]
|
var items: [Any] = [vehicle.reportText()]
|
||||||
for photo in vehicle.photos {
|
for photo in vehicle.photos {
|
||||||
if let url = URL(string: photo.url) {
|
// TODO: Fix sharing
|
||||||
if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) {
|
// if let url = URL(string: photo.url) {
|
||||||
items.append(image)
|
// if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) {
|
||||||
}
|
// items.append(image)
|
||||||
|
// }
|
||||||
}
|
//
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||||
@ -408,26 +412,6 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
|
|||||||
self.present(sheet, animated: true, completion: nil)
|
self.present(sheet, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
|
||||||
return UIImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
|
||||||
return self.reportImageUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
|
|
||||||
guard let url = self.reportImageUrl else { return nil }
|
|
||||||
|
|
||||||
let metadata = LPLinkMetadata()
|
|
||||||
metadata.title = self.vehicle?.getNumber()
|
|
||||||
metadata.originalURL = url
|
|
||||||
metadata.url = url
|
|
||||||
metadata.imageProvider = NSItemProvider.init(contentsOf: url)
|
|
||||||
metadata.iconProvider = NSItemProvider.init(contentsOf: url)
|
|
||||||
return metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Copy
|
// MARK: - Copy
|
||||||
|
|
||||||
@IBAction func onCopy(_ sender: UIBarButtonItem) {
|
@IBAction func onCopy(_ sender: UIBarButtonItem) {
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
|
||||||
import RxCocoa
|
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import ExceptionCatcher
|
import ExceptionCatcher
|
||||||
@ -15,8 +13,6 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
private var refreshIndicator: UIBarButtonItem!
|
private var refreshIndicator: UIBarButtonItem!
|
||||||
private var moreActionsButton: UIBarButtonItem?
|
private var moreActionsButton: UIBarButtonItem?
|
||||||
|
|
||||||
private let bag = DisposeBag()
|
|
||||||
|
|
||||||
private lazy var searchController: UISearchController = .default
|
private lazy var searchController: UISearchController = .default
|
||||||
.placeholder(NSLocalizedString("Search plate numbers", comment: ""))
|
.placeholder(NSLocalizedString("Search plate numbers", comment: ""))
|
||||||
.resultsUpdater(self)
|
.resultsUpdater(self)
|
||||||
@ -25,11 +21,10 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
.scopeButtons(SearchScope.allCases.map(\.title))
|
.scopeButtons(SearchScope.allCases.map(\.title))
|
||||||
|
|
||||||
private var refreshControl = UIRefreshControl()
|
private var refreshControl = UIRefreshControl()
|
||||||
private var datasource: RxSectionedDataSource<Vehicle,VehicleCell>!
|
private var datasource: SectionedDataSource<VehicleDto,VehicleCell>!
|
||||||
private var isLoadingPage = false
|
private var isLoadingPage = false
|
||||||
private var pageLoadingIndicator = UIActivityIndicatorView(style: .medium)
|
private var pageLoadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
|
||||||
var filterRelay = BehaviorRelay<Filter>(value: Filter())
|
|
||||||
var filter = Filter()
|
var filter = Filter()
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
@ -58,37 +53,31 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
|
self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
|
||||||
self.tableView.addSubview(self.refreshControl)
|
self.tableView.addSubview(self.refreshControl)
|
||||||
|
|
||||||
self.datasource = RxSectionedDataSource(table: self.tableView)
|
self.datasource = SectionedDataSource(table: self.tableView)
|
||||||
self.tableView.delegate = self
|
self.tableView.delegate = self
|
||||||
self.tableView.keyboardDismissMode = .onDrag
|
self.tableView.keyboardDismissMode = .onDrag
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSearchResults(with filter: Filter) {
|
||||||
|
Task {
|
||||||
|
showProgress()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.filterRelay
|
|
||||||
.debounce(.milliseconds(500), scheduler: MainScheduler.instance)
|
|
||||||
.do(onNext: { _ in
|
|
||||||
self.showProgress()
|
|
||||||
})
|
|
||||||
.flatMapLatest { filter in
|
|
||||||
if filter.needReset {
|
if filter.needReset {
|
||||||
self.datasource.reset()
|
self.datasource.reset()
|
||||||
}
|
}
|
||||||
return Api.getVehicles(with: filter, pageToken: self.datasource.pageToken)
|
|
||||||
.do(onError: { print($0) })
|
let vehicles = (try? await ApiService.shared.getVehicles(with: filter, pageToken: self.datasource.pageToken, pageSize: 50)) ?? PagedResponse<VehicleDto>()
|
||||||
.catchAndReturn(PagedResponse<Vehicle>())
|
|
||||||
}
|
if let count = vehicles.count {
|
||||||
.observe(on: MainScheduler.instance)
|
|
||||||
.do(onNext: {
|
|
||||||
if let count = $0.count {
|
|
||||||
self.navigationItem.title = String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), count)
|
self.navigationItem.title = String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), count)
|
||||||
self.showMapButton?.isEnabled = count > 0
|
self.showMapButton?.isEnabled = count > 0
|
||||||
}
|
}
|
||||||
self.refreshControl.endRefreshing()
|
|
||||||
self.isLoadingPage = false
|
refreshControl.endRefreshing()
|
||||||
self.pageLoadingIndicator.stopAnimating()
|
isLoadingPage = false
|
||||||
self.hideProgress()
|
pageLoadingIndicator.stopAnimating()
|
||||||
})
|
hideProgress()
|
||||||
.bind(to: self.datasource.data)
|
datasource.update(with: vehicles)
|
||||||
.disposed(by: self.bag)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +98,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Code duplication
|
// FIXME: Code duplication
|
||||||
func updateDetailController(with vehicle: Vehicle) {
|
func updateDetailController(with vehicle: VehicleDto) {
|
||||||
if let splitViewController = self.view.window?.rootViewController as? UISplitViewController
|
if let splitViewController = self.view.window?.rootViewController as? UISplitViewController
|
||||||
{
|
{
|
||||||
var detail: UINavigationController?
|
var detail: UINavigationController?
|
||||||
@ -140,7 +129,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
self.filter.needReset = true
|
self.filter.needReset = true
|
||||||
self.filter.scope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber
|
self.filter.scope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber
|
||||||
|
|
||||||
self.filterRelay.accept(self.filter)
|
updateSearchResults(with: filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||||
@ -149,7 +138,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
filter.scope = scope
|
filter.scope = scope
|
||||||
filterRelay.accept(filter)
|
updateSearchResults(with: filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: NavigationBar actions
|
// MARK: NavigationBar actions
|
||||||
@ -185,7 +174,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
@objc func refresh(_ sender: AnyObject) {
|
@objc func refresh(_ sender: AnyObject) {
|
||||||
self.showMapButton?.isEnabled = false
|
self.showMapButton?.isEnabled = false
|
||||||
self.filter.needReset = true
|
self.filter.needReset = true
|
||||||
self.filterRelay.accept(self.filter)
|
updateSearchResults(with: filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showFilter() {
|
func showFilter() {
|
||||||
@ -197,7 +186,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
self.datasource.setSortParameter(self.filter.sortBy ?? .updatedDate)
|
self.datasource.setSortParameter(self.filter.sortBy ?? .updatedDate)
|
||||||
self.filter.needReset = true
|
self.filter.needReset = true
|
||||||
self.filter.scope = SearchScope(rawValue: self.searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber
|
self.filter.scope = SearchScope(rawValue: self.searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber
|
||||||
self.filterRelay.accept(self.filter)
|
self.updateSearchResults(with: self.filter)
|
||||||
}
|
}
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
self.navigationController?.pushViewController(controller, animated: true)
|
||||||
}
|
}
|
||||||
@ -214,22 +203,19 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func exportSearchResults() {
|
func exportSearchResults() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
showProgress()
|
showProgress()
|
||||||
|
let resp = try await ApiService.shared.getVehicles(with: filter, pageSize: 0)
|
||||||
Api.getVehicles(with: filter, pageSize: 0)
|
|
||||||
.observe(on: MainScheduler.instance)
|
|
||||||
.subscribe(onSuccess: { resp in
|
|
||||||
self.hideProgress()
|
|
||||||
|
|
||||||
let newLine = "\r\n"
|
let newLine = "\r\n"
|
||||||
var csvString = Vehicle.csvHeader + newLine
|
var csvString = VehicleDto.csvHeader + newLine
|
||||||
|
|
||||||
for vehicle in resp.items {
|
for vehicle in resp.items {
|
||||||
csvString.append(vehicle.csvLine)
|
csvString.append(vehicle.csvLine)
|
||||||
csvString.append(newLine)
|
csvString.append(newLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
|
||||||
let tmpUrl = FileManager.default.tmpUrl(name: "search", ext: "csv")
|
let tmpUrl = FileManager.default.tmpUrl(name: "search", ext: "csv")
|
||||||
try csvString.write(to: tmpUrl, atomically: true, encoding: .utf8)
|
try csvString.write(to: tmpUrl, atomically: true, encoding: .utf8)
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
@ -237,14 +223,13 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
#else
|
#else
|
||||||
self.share(file: tmpUrl)
|
self.share(file: tmpUrl)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
hideProgress()
|
||||||
} catch {
|
} catch {
|
||||||
|
hideProgress()
|
||||||
HUD.show(error: error)
|
HUD.show(error: error)
|
||||||
}
|
}
|
||||||
}, onFailure: { error in
|
}
|
||||||
self.hideProgress()
|
|
||||||
HUD.show(error: error)
|
|
||||||
})
|
|
||||||
.disposed(by: bag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func share(file url: URL) {
|
func share(file url: URL) {
|
||||||
@ -290,28 +275,26 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(vehicle: Vehicle, at indexPath: IndexPath) {
|
func update(vehicle: VehicleDto, at indexPath: IndexPath) {
|
||||||
HUD.show(.progress)
|
|
||||||
Api.checkVehicle(by: vehicle.getNumber(), notes: Array(vehicle.notes), events: [], force: true).observe(on: MainScheduler.instance).subscribe { newVehicle in
|
Task {
|
||||||
HUD.hide()
|
|
||||||
do {
|
do {
|
||||||
let realm = try Realm()
|
HUD.show(.progress)
|
||||||
|
let newVehicle = try await ApiService.shared.checkVehicle(by: vehicle.getNumber(), notes: vehicle.notes, events: [], force: true)
|
||||||
|
HUD.hide()
|
||||||
|
let realm = try await Realm()
|
||||||
if realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) != nil {
|
if realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) != nil {
|
||||||
try realm.write {
|
try realm.write {
|
||||||
realm.add(newVehicle, update: .all)
|
realm.add(Vehicle(dto: newVehicle), update: .all)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
datasource.set(item: newVehicle, at: indexPath)
|
||||||
|
updateDetailController(with: newVehicle)
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
HUD.hide()
|
||||||
self.show(error: error)
|
show(error: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let frozenVehicle = newVehicle.realm != nil ? newVehicle.clone() : newVehicle
|
|
||||||
self.datasource.set(item: frozenVehicle, at: indexPath)
|
|
||||||
self.updateDetailController(with: frozenVehicle)
|
|
||||||
} onFailure: { err in
|
|
||||||
HUD.show(error: err)
|
|
||||||
}.disposed(by: self.bag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
@ -326,7 +309,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
|
|||||||
if toBottom < 100 && !self.isLoadingPage && self.datasource.needMoreData() {
|
if toBottom < 100 && !self.isLoadingPage && self.datasource.needMoreData() {
|
||||||
self.isLoadingPage = true
|
self.isLoadingPage = true
|
||||||
self.filter.needReset = false
|
self.filter.needReset = false
|
||||||
self.filterRelay.accept(self.filter)
|
updateSearchResults(with: filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import AVFoundation
|
|
||||||
import RxSwift
|
|
||||||
|
|
||||||
extension AVAudioSession {
|
|
||||||
func setCategoryAsync(_ category: AVAudioSession.Category) -> Single<Void> {
|
|
||||||
if self.category == category {
|
|
||||||
return .just(())
|
|
||||||
} else {
|
|
||||||
return Single.create { observer in
|
|
||||||
NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: self, queue: .main) { notification in
|
|
||||||
print("")
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try self.setCategory(category, mode: .default, options: [])
|
|
||||||
} catch {
|
|
||||||
observer(.failure(error))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Disposables.create()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,7 +9,7 @@ protocol Dated {
|
|||||||
var updatedTimestamp: TimeInterval { get }
|
var updatedTimestamp: TimeInterval { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Vehicle: Dated {
|
extension VehicleDto: Dated {
|
||||||
|
|
||||||
var updated: Date {
|
var updated: Date {
|
||||||
Date(timeIntervalSince1970: self.updatedDate)
|
Date(timeIntervalSince1970: self.updatedDate)
|
||||||
@ -28,7 +28,7 @@ extension Vehicle: Dated {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AudioRecord: Dated {
|
extension AudioRecordDto: Dated {
|
||||||
var updated: Date {
|
var updated: Date {
|
||||||
Date(timeIntervalSince1970: self.getAddedDate())
|
Date(timeIntervalSince1970: self.getAddedDate())
|
||||||
}
|
}
|
||||||
@ -53,7 +53,10 @@ extension RandomAccessCollection where Element: Dated & Identifiable {
|
|||||||
var key: TimeInterval = 0
|
var key: TimeInterval = 0
|
||||||
var keyNext: TimeInterval = 0
|
var keyNext: TimeInterval = 0
|
||||||
var index = self.index(before: endIndex)
|
var index = self.index(before: endIndex)
|
||||||
let currentMonthStart = DateCache.shared.monthStart.timeIntervalSince1970
|
|
||||||
|
let now = DateInRegion(Date(), region: Region.current)
|
||||||
|
let monthStart = now.dateAtStartOf(.month)
|
||||||
|
let weekStart = now.dateAtStartOf(.weekOfMonth)
|
||||||
|
|
||||||
while index >= startIndex {
|
while index >= startIndex {
|
||||||
|
|
||||||
@ -65,12 +68,16 @@ extension RandomAccessCollection where Element: Dated & Identifiable {
|
|||||||
|
|
||||||
if keyNext == 0 || timestamp > keyNext {
|
if keyNext == 0 || timestamp > keyNext {
|
||||||
|
|
||||||
let component: Calendar.Component = timestamp >= currentMonthStart ? .day : .month
|
let component: Calendar.Component = timestamp >= monthStart.timeIntervalSince1970 ? .day : .month
|
||||||
let dateInRegion = DateInRegion(seconds: timestamp, region: .current)
|
let dateInRegion = DateInRegion(seconds: timestamp, region: .current)
|
||||||
let startOfPeriod = dateInRegion.dateAtStartOf(component)
|
let startOfPeriod = dateInRegion.dateAtStartOf(component)
|
||||||
key = startOfPeriod.timeIntervalSince1970
|
key = startOfPeriod.timeIntervalSince1970
|
||||||
|
|
||||||
sectionsArray.insert(DateSection<Element>(timestamp: key, items: []), at: 0)
|
let section = DateSection<Element>(timestamp: key,
|
||||||
|
items: [],
|
||||||
|
monthStart: monthStart,
|
||||||
|
weekStart: weekStart)
|
||||||
|
sectionsArray.insert(section, at: 0)
|
||||||
keyNext = startOfPeriod.dateByAdding(1, component).timeIntervalSince1970
|
keyNext = startOfPeriod.dateByAdding(1, component).timeIntervalSince1970
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import UIKit
|
|||||||
import Kingfisher
|
import Kingfisher
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
extension Vehicle {
|
extension VehicleDto {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func drawLine(y: CGFloat, width: CGFloat, margin: CGFloat = 15,context: CGContext) {
|
func drawLine(y: CGFloat, width: CGFloat, margin: CGFloat = 15,context: CGContext) {
|
||||||
let lineWidth = 1/UIScreen.main.scale
|
let lineWidth = 1/UIScreen.main.scale
|
||||||
context.move(to: CGPoint(x: margin, y: y + lineWidth/2))
|
context.move(to: CGPoint(x: margin, y: y + lineWidth/2))
|
||||||
@ -13,6 +15,7 @@ extension Vehicle {
|
|||||||
context.strokePath()
|
context.strokePath()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func drawCell(y: CGFloat, width: CGFloat, height: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) {
|
func drawCell(y: CGFloat, width: CGFloat, height: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) {
|
||||||
let fontSize: CGFloat = 17
|
let fontSize: CGFloat = 17
|
||||||
let font = UIFont.systemFont(ofSize: fontSize)
|
let font = UIFont.systemFont(ofSize: fontSize)
|
||||||
@ -29,6 +32,7 @@ extension Vehicle {
|
|||||||
self.drawLine(y: y + height - 1/UIScreen.main.scale, width: width, margin: lineMargin, context: context)
|
self.drawLine(y: y + height - 1/UIScreen.main.scale, width: width, margin: lineMargin, context: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func drawBigTextCell(y: CGFloat, width: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) -> CGFloat {
|
func drawBigTextCell(y: CGFloat, width: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) -> CGFloat {
|
||||||
let pStyle = NSMutableParagraphStyle()
|
let pStyle = NSMutableParagraphStyle()
|
||||||
pStyle.alignment = .right
|
pStyle.alignment = .right
|
||||||
@ -50,6 +54,7 @@ extension Vehicle {
|
|||||||
return height
|
return height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func reportImage(width: CGFloat) -> UIImage {
|
func reportImage(width: CGFloat) -> UIImage {
|
||||||
var realHeight: CGFloat = 0
|
var realHeight: CGFloat = 0
|
||||||
let rect = CGRect(origin: .zero, size: CGSize(width: width, height: CGFloat(10000)))
|
let rect = CGRect(origin: .zero, size: CGSize(width: width, height: CGFloat(10000)))
|
||||||
@ -178,27 +183,28 @@ extension Vehicle {
|
|||||||
self.drawLine(y: y, width: w, margin: 0, context: ctx)
|
self.drawLine(y: y, width: w, margin: 0, context: ctx)
|
||||||
y += 1/UIScreen.main.scale
|
y += 1/UIScreen.main.scale
|
||||||
|
|
||||||
for photo in self.photos {
|
// TODO: Fix drawing photos in report image
|
||||||
let date = Date(timeIntervalSince1970: TimeInterval(photo.date/1000))
|
// for photo in self.photos {
|
||||||
var name = "<Unknown model>"
|
// let date = Date(timeIntervalSince1970: TimeInterval(photo.date/1000))
|
||||||
if let brand = photo.brand, let model = photo.model {
|
// var name = "<Unknown model>"
|
||||||
name = "\(brand) \(model)"
|
// if let brand = photo.brand, let model = photo.model {
|
||||||
}
|
// name = "\(brand) \(model)"
|
||||||
|
// }
|
||||||
if let url = URL(string: photo.url) {
|
//
|
||||||
if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) {
|
// if let url = URL(string: photo.url) {
|
||||||
let imgHeight = image.size.height*w/image.size.width
|
// if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) {
|
||||||
let rect = CGRect(x: 0, y: y, width: w, height: imgHeight)
|
// let imgHeight = image.size.height*w/image.size.width
|
||||||
image.draw(in: rect)
|
// let rect = CGRect(x: 0, y: y, width: w, height: imgHeight)
|
||||||
y += imgHeight
|
// image.draw(in: rect)
|
||||||
}
|
// y += imgHeight
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
self.drawCell(y: y, width: w, height: cellHeight, title: name, value: formatter.string(from: date), lineMargin: 0, context: ctx)
|
//
|
||||||
y += cellHeight
|
// self.drawCell(y: y, width: w, height: cellHeight, title: name, value: formatter.string(from: date), lineMargin: 0, context: ctx)
|
||||||
|
// y += cellHeight
|
||||||
y += 16
|
//
|
||||||
}
|
// y += 16
|
||||||
|
// }
|
||||||
|
|
||||||
realHeight = y
|
realHeight = y
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
//const sitekey = '6Lf2uycUAAAAALo3u8D10FqNuSpUvUXlfP7BzHOk';
|
|
||||||
|
|
||||||
let verifyCallback = (response) => {
|
|
||||||
console.log('verifyCallback: ', response);
|
|
||||||
window.webkit.messageHandlers.dkbmHandler.postMessage({ token: response });
|
|
||||||
};
|
|
||||||
|
|
||||||
var timerId;
|
|
||||||
|
|
||||||
function getImages() {
|
|
||||||
let nodes = document.querySelectorAll(".policies-tbl img");
|
|
||||||
let urls = Array(...nodes).map(img => img.src);
|
|
||||||
if(urls.length > 1) {
|
|
||||||
window.webkit.messageHandlers.dkbmHandler.postMessage({ "url": urls[1] });
|
|
||||||
clearInterval(timerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timerId = setInterval(getImages, 1000);
|
|
||||||
|
|
||||||
window.addEventListener('load', async (event) => {
|
|
||||||
if(window.top == window.self) {
|
|
||||||
|
|
||||||
let { plateNumber, vin } = await window.webkit.messageHandlers.dkbmHandler.postMessage({ "loaded": "true" });
|
|
||||||
switchTab('tsBlock');
|
|
||||||
|
|
||||||
if(plateNumber) {
|
|
||||||
let licencePlateInput = document.getElementById('licensePlate');
|
|
||||||
licencePlateInput.value = plateNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(vin) {
|
|
||||||
let vinInput = document.getElementById('vin');
|
|
||||||
vinInput.value = vin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
26
AutoCat/Preview/ApiServiceStub.swift
Normal file
26
AutoCat/Preview/ApiServiceStub.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// ApiServiceStub.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 13.07.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
actor ApiServiceStub: ApiServiceProtocol {
|
||||||
|
|
||||||
|
let vehicle = VehicleDto()
|
||||||
|
|
||||||
|
func add(notes: [VehicleNoteDto], to number: String) async throws -> VehicleDto {
|
||||||
|
vehicle
|
||||||
|
}
|
||||||
|
|
||||||
|
func edit(note: VehicleNoteDto) async throws -> VehicleDto {
|
||||||
|
vehicle
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(note id: String) async throws -> VehicleDto {
|
||||||
|
vehicle
|
||||||
|
}
|
||||||
|
}
|
||||||
28
AutoCat/Preview/StorageServiceStub.swift
Normal file
28
AutoCat/Preview/StorageServiceStub.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// StorageServiceStub.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 13.07.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
actor StorageServiceStub: StorageServiceProtocol {
|
||||||
|
|
||||||
|
let vehicle = VehicleDto()
|
||||||
|
|
||||||
|
func addNote(text: String, to number: String) async throws -> AutoCatCore.VehicleDto {
|
||||||
|
vehicle
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteNote(id: String, for number: String) async throws -> AutoCatCore.VehicleDto {
|
||||||
|
vehicle
|
||||||
|
}
|
||||||
|
|
||||||
|
func editNote(id: String, text: String, for number: String) async throws -> AutoCatCore.VehicleDto {
|
||||||
|
vehicle
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateVehicleIfExists(dto: AutoCatCore.VehicleDto) async throws { }
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import os.log
|
import os.log
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import RxSwift
|
|
||||||
import PKHUD
|
import PKHUD
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
@ -112,7 +111,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
if tabvc.selectedIndex == 0 {
|
if tabvc.selectedIndex == 0 {
|
||||||
if let nav = tabvc.selectedViewController as? UINavigationController, let child = nav.topViewController {
|
if let nav = tabvc.selectedViewController as? UINavigationController, let child = nav.topViewController {
|
||||||
if let check = child as? CheckController {
|
if let check = child as? CheckController {
|
||||||
check.handleQuickActions()
|
Task { await check.handleQuickActions() }
|
||||||
} else {
|
} else {
|
||||||
nav.popToRootViewController(animated: false)
|
nav.popToRootViewController(animated: false)
|
||||||
}
|
}
|
||||||
@ -134,7 +133,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
if let url = userActivity.webpageURL {
|
if let url = userActivity.webpageURL {
|
||||||
if let param = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.first, let token = param.value {
|
if let param = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.first, let token = param.value {
|
||||||
if let jwt = JWT<NumberPayload>(string: token) {
|
if let jwt = JWT<NumberPayload>(string: token) {
|
||||||
self.openReport(with: jwt.payload.plateNumber)
|
Task { await self.openReport(with: jwt.payload.plateNumber) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,11 +158,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func openReport(with number: String) {
|
func openReport(with number: String) async {
|
||||||
guard let rootController = self.window?.rootViewController else { return }
|
guard let rootController = self.window?.rootViewController else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
HUD.show(.progress)
|
HUD.show(.progress)
|
||||||
_ = Api.getReport(for: number).observe(on: MainScheduler.instance).subscribe { vehicle in
|
let vehicle = try await ApiService.shared.getReport(for: number)
|
||||||
let sb = UIStoryboard(name: "Main", bundle: nil)
|
let sb = UIStoryboard(name: "Main", bundle: nil)
|
||||||
let controller = sb.instantiateViewController(identifier: "ReportController") as ReportController
|
let controller = sb.instantiateViewController(identifier: "ReportController") as ReportController
|
||||||
controller.vehicle = vehicle
|
controller.vehicle = vehicle
|
||||||
@ -172,7 +172,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
controller.navigationItem.leftBarButtonItem = BlockBarButtonItem(barButtonSystemItem: .close) { _ in nav.dismiss(animated: true) }
|
controller.navigationItem.leftBarButtonItem = BlockBarButtonItem(barButtonSystemItem: .close) { _ in nav.dismiss(animated: true) }
|
||||||
rootController.present(nav, animated: true)
|
rootController.present(nav, animated: true)
|
||||||
HUD.hide()
|
HUD.hide()
|
||||||
} onFailure: { error in
|
} catch {
|
||||||
HUD.show(error: error)
|
HUD.show(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
AutoCat/Screens/NotesScreen/NoteAlertModifier.swift
Normal file
44
AutoCat/Screens/NotesScreen/NoteAlertModifier.swift
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// NoteAlertModifier.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 07.07.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NoteAlertModifier: ViewModifier {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
let isPresented: Binding<Bool>
|
||||||
|
let action: ((String) -> Void)?
|
||||||
|
|
||||||
|
@Binding var text: String
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
|
||||||
|
content
|
||||||
|
.alert(title, isPresented: isPresented) {
|
||||||
|
TextField("", text: $text)
|
||||||
|
Button("Cancel") {}
|
||||||
|
Button("Done") {
|
||||||
|
action?(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
func noteAlert(title: String,
|
||||||
|
body: Binding<String>,
|
||||||
|
isPresented: Binding<Bool>,
|
||||||
|
action: ((String) -> Void)?) -> some View {
|
||||||
|
|
||||||
|
modifier(NoteAlertModifier(title: title,
|
||||||
|
isPresented: isPresented,
|
||||||
|
action: action,
|
||||||
|
text: body))
|
||||||
|
}
|
||||||
|
}
|
||||||
32
AutoCat/Screens/NotesScreen/NotesCoordinator.swift
Normal file
32
AutoCat/Screens/NotesScreen/NotesCoordinator.swift
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// NotesCoordinator.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 24.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
class NotesCoordinator: Coordinator {
|
||||||
|
|
||||||
|
let viewController: UINavigationController?
|
||||||
|
let vehicle: VehicleDto
|
||||||
|
|
||||||
|
init(navController: UINavigationController, vehicle: VehicleDto) {
|
||||||
|
|
||||||
|
self.viewController = navController
|
||||||
|
self.vehicle = vehicle
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() async throws {
|
||||||
|
|
||||||
|
let viewModel = await NotesViewModel(vehicle: vehicle,
|
||||||
|
storageService: try await StorageService.shared,
|
||||||
|
apiService: ApiService.shared)
|
||||||
|
let controller = await UIHostingController(rootView: NotesScreen(viewModel: viewModel))
|
||||||
|
await viewController?.pushViewController(controller, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
104
AutoCat/Screens/NotesScreen/NotesScreen.swift
Normal file
104
AutoCat/Screens/NotesScreen/NotesScreen.swift
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
//
|
||||||
|
// NotesScreen.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 24.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
struct NotesScreen: View {
|
||||||
|
|
||||||
|
@StateObject var viewModel: NotesViewModel
|
||||||
|
|
||||||
|
@State var showNewNoteAlert = false
|
||||||
|
@State var showEditNoteAlert = false
|
||||||
|
@State var selectedNoteId = ""
|
||||||
|
@State var noteText = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
|
||||||
|
List(viewModel.vehicle.notes) { note in
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(note.text)
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(Formatters.standard.string(from: Date(timeIntervalSince1970: note.date)))
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.swipeActions(allowsFullSwipe: false) {
|
||||||
|
makeActions(for: note)
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
makeActions(for: note, useLabels: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.inset)
|
||||||
|
.navigationTitle("Notes")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button {
|
||||||
|
noteText = ""
|
||||||
|
showNewNoteAlert = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
.noteAlert(title: "New note", body: $noteText, isPresented: $showNewNoteAlert) { text in
|
||||||
|
Task { await viewModel.addNote(text: text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hud($viewModel.hud)
|
||||||
|
.noteAlert(title: "Edit note",
|
||||||
|
body: $noteText,
|
||||||
|
isPresented: $showEditNoteAlert) { text in
|
||||||
|
Task { await viewModel.editNote(id: selectedNoteId, text: text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeActions(for note: VehicleNoteDto, useLabels: Bool = false) -> some View {
|
||||||
|
Button() {
|
||||||
|
Task { await viewModel.deleteNote(id: note.id) }
|
||||||
|
} label: {
|
||||||
|
Label(useLabels ? "Delete" : "", systemImage: "trash")
|
||||||
|
}
|
||||||
|
.tint(.red)
|
||||||
|
|
||||||
|
Button() {
|
||||||
|
selectedNoteId = note.id
|
||||||
|
noteText = note.text
|
||||||
|
showEditNoteAlert = true
|
||||||
|
} label: {
|
||||||
|
Label(useLabels ? "Edit" : "", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button() {
|
||||||
|
viewModel.copyNote(note)
|
||||||
|
} label: {
|
||||||
|
Label(useLabels ? "Copy" : "", systemImage: "doc.on.doc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
|
||||||
|
var vehicle = VehicleDto()
|
||||||
|
|
||||||
|
vehicle.notes = [
|
||||||
|
.init(text: "qwe"),
|
||||||
|
.init(text: "asdf"),
|
||||||
|
.init(text: "zxcv")
|
||||||
|
]
|
||||||
|
|
||||||
|
let vm = NotesViewModel(vehicle: vehicle,
|
||||||
|
storageService: StorageServiceStub(),
|
||||||
|
apiService: ApiServiceStub())
|
||||||
|
|
||||||
|
return NotesScreen(viewModel: vm)
|
||||||
|
}
|
||||||
91
AutoCat/Screens/NotesScreen/NotesViewModel.swift
Normal file
91
AutoCat/Screens/NotesScreen/NotesViewModel.swift
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// NotesViewModel.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 24.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AutoCatCore
|
||||||
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class NotesViewModel: ObservableObject, ACHudContainer {
|
||||||
|
|
||||||
|
let storageService: StorageServiceProtocol
|
||||||
|
let apiService: ApiServiceProtocol
|
||||||
|
|
||||||
|
@Published var vehicle: VehicleDto
|
||||||
|
@Published var hud: ACHud?
|
||||||
|
|
||||||
|
init(vehicle: VehicleDto, storageService: StorageServiceProtocol, apiService: ApiServiceProtocol) {
|
||||||
|
|
||||||
|
self.vehicle = vehicle
|
||||||
|
self.storageService = storageService
|
||||||
|
self.apiService = apiService
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNote(text: String) async {
|
||||||
|
if vehicle.unrecognized {
|
||||||
|
await wrapWithToast(showProgress: false) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
vehicle = try await storageService.addNote(text: text, to: vehicle.getNumber())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let note = VehicleNoteDto(text: text)
|
||||||
|
|
||||||
|
await wrapWithToast { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let vehicle = try await apiService.add(notes: [note], to: vehicle.getNumber())
|
||||||
|
try await storageService.updateVehicleIfExists(dto: vehicle)
|
||||||
|
self.vehicle = vehicle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteNote(id: String) async {
|
||||||
|
if vehicle.unrecognized {
|
||||||
|
await wrapWithToast(showProgress: false) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
vehicle = try await storageService.deleteNote(id: id, for: vehicle.getNumber())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await wrapWithToast { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let vehicle = try await apiService.remove(note: id)
|
||||||
|
try await storageService.updateVehicleIfExists(dto: vehicle)
|
||||||
|
self.vehicle = vehicle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func editNote(id: String, text: String) async {
|
||||||
|
if vehicle.unrecognized {
|
||||||
|
await wrapWithToast(showProgress: false) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
vehicle = try await storageService.editNote(id: id, text: text, for: vehicle.getNumber())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var note = VehicleNoteDto(text: text)
|
||||||
|
note.id = id
|
||||||
|
|
||||||
|
await wrapWithToast { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let vehicle = try await apiService.edit(note: note)
|
||||||
|
try await storageService.updateVehicleIfExists(dto: vehicle)
|
||||||
|
self.vehicle = vehicle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyNote(_ note: VehicleNoteDto) {
|
||||||
|
|
||||||
|
UIPasteboard.general.setValue(note.text,
|
||||||
|
forPasteboardType: UTType.plainText.identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
29
AutoCat/Screens/OsagoScreen/OsagoCoordinator.swift
Normal file
29
AutoCat/Screens/OsagoScreen/OsagoCoordinator.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// OsagoCoordinator.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 14.07.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
class OsagoCoordinator: Coordinator {
|
||||||
|
|
||||||
|
let viewController: UINavigationController?
|
||||||
|
let contracts: [OsagoDto]
|
||||||
|
|
||||||
|
init(navController: UINavigationController, contracts: [OsagoDto]) {
|
||||||
|
|
||||||
|
self.viewController = navController
|
||||||
|
self.contracts = contracts
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() async throws {
|
||||||
|
|
||||||
|
let controller = await UIHostingController(rootView: OsagoScreen(contracts: contracts))
|
||||||
|
await viewController?.pushViewController(controller, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
84
AutoCat/Screens/OsagoScreen/OsagoScreen.swift
Normal file
84
AutoCat/Screens/OsagoScreen/OsagoScreen.swift
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// OsagoScreen.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 14.07.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
struct OsagoScreen: View {
|
||||||
|
|
||||||
|
let contracts: [OsagoDto]
|
||||||
|
|
||||||
|
let formatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .none
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List(contracts) { contract in
|
||||||
|
Section(header: Text(makeHeader(from: contract))) {
|
||||||
|
OsagoPropertyView(title: "Contract series and number", value: contract.number)
|
||||||
|
OsagoPropertyView(title: "Insurance organization name", value: contract.name)
|
||||||
|
OsagoPropertyView(title: "OSAGO contract status", value: contract.status)
|
||||||
|
OsagoPropertyView(title: "Insurant", value: contract.insurant)
|
||||||
|
OsagoPropertyView(title: "Owner", value: contract.owner)
|
||||||
|
OsagoPropertyView(title: "Birthday", value: contract.birthday)
|
||||||
|
OsagoPropertyView(title: "Vehicle usage region", value: contract.usageRegion)
|
||||||
|
OsagoPropertyView(title: "Contract restrictions", value: contract.restrictions)
|
||||||
|
OsagoPropertyView(title: "Plate number", value: contract.plateNumber)
|
||||||
|
OsagoPropertyView(title: "VIN", value: contract.vin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHeader(from contract: OsagoDto) -> String {
|
||||||
|
|
||||||
|
let date = Date(timeIntervalSince1970: contract.date)
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OsagoPropertyView: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
let value: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Text(title)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.font(.caption)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Text(value ?? "")
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
|
||||||
|
let contracts: [OsagoDto] = [
|
||||||
|
.init(date: 1720963133,
|
||||||
|
number: "XXX 12345678",
|
||||||
|
name: "Some organization",
|
||||||
|
restrictions: "Restrictions"),
|
||||||
|
.init(date: 1720964133,
|
||||||
|
number: "XXX 12345678",
|
||||||
|
name: "Some organization",
|
||||||
|
restrictions: "Restrictions")
|
||||||
|
]
|
||||||
|
|
||||||
|
return OsagoScreen(contracts: contracts)
|
||||||
|
}
|
||||||
29
AutoCat/Screens/OwnersScreen/OwnersCoordinator.swift
Normal file
29
AutoCat/Screens/OwnersScreen/OwnersCoordinator.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// OwnersCoordinator.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 14.07.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
class OwnersCoordinator: Coordinator {
|
||||||
|
|
||||||
|
let viewController: UINavigationController?
|
||||||
|
let ownerships: [VehicleOwnershipPeriodDto]
|
||||||
|
|
||||||
|
init(navController: UINavigationController, ownerships: [VehicleOwnershipPeriodDto]) {
|
||||||
|
|
||||||
|
self.viewController = navController
|
||||||
|
self.ownerships = ownerships
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() async throws {
|
||||||
|
|
||||||
|
let controller = await UIHostingController(rootView: OwnersScreen(ownerships: ownerships))
|
||||||
|
await viewController?.pushViewController(controller, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
75
AutoCat/Screens/OwnersScreen/OwnersScreen.swift
Normal file
75
AutoCat/Screens/OwnersScreen/OwnersScreen.swift
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
//
|
||||||
|
// OwnersScreen.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 14.07.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
struct OwnersScreen: View {
|
||||||
|
|
||||||
|
let formatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .none
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
let ownerships: [VehicleOwnershipPeriodDto]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
|
||||||
|
List(ownerships) { ownership in
|
||||||
|
Section(header: Text(makeHeader(for: ownership))) {
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Owner type")
|
||||||
|
Spacer()
|
||||||
|
Text(ownership.ownerType)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(ownership.lastOperation)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(String.localizedStringWithFormat(NSLocalizedString("owners count", comment: ""), ownerships.count))
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHeader(for owner: VehicleOwnershipPeriodDto) -> String {
|
||||||
|
|
||||||
|
let fromDate = Date(timeIntervalSince1970: TimeInterval(owner.from/1000))
|
||||||
|
let from = self.formatter.string(from: fromDate)
|
||||||
|
var to = NSLocalizedString("now", comment: "")
|
||||||
|
if owner.to > 0 {
|
||||||
|
let toDate = Date(timeIntervalSince1970: TimeInterval(owner.to/1000))
|
||||||
|
to = self.formatter.string(from: toDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(from) - \(to)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
|
||||||
|
let ownerships: [VehicleOwnershipPeriodDto] = [
|
||||||
|
.init(lastOperation: "в связи с прекращением права собственности (отчуждение, конфискация ТС)",
|
||||||
|
ownerType: "individual",
|
||||||
|
from: 1018051200000,
|
||||||
|
to: 1094515200000),
|
||||||
|
.init(lastOperation: "в связи с прекращением права собственности (отчуждение, конфискация ТС)",
|
||||||
|
ownerType: "individual",
|
||||||
|
from: 1099440000000,
|
||||||
|
to: 1105488000000),
|
||||||
|
.init(lastOperation: "регистрация снятых с учета",
|
||||||
|
ownerType: "individual",
|
||||||
|
from: 1105747200000,
|
||||||
|
to: 1323129600000)
|
||||||
|
]
|
||||||
|
|
||||||
|
return OwnersScreen(ownerships: ownerships)
|
||||||
|
}
|
||||||
33
AutoCat/SwiftUI/ACProgressHud/ACHud.swift
Normal file
33
AutoCat/SwiftUI/ACProgressHud/ACHud.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// ACHud.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 29.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
enum ACHud: Equatable, Identifiable, Sendable {
|
||||||
|
|
||||||
|
case progress
|
||||||
|
case error(Error)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .progress:
|
||||||
|
"progress"
|
||||||
|
case .error(let error):
|
||||||
|
error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: ACHud, rhs: ACHud) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.progress, .progress):
|
||||||
|
true
|
||||||
|
case (let .error(lerr), let .error(rerr)):
|
||||||
|
lerr.localizedDescription == rerr.localizedDescription
|
||||||
|
default:
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift
Normal file
34
AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
//
|
||||||
|
// ACHudContainer.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 29.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
protocol ACHudContainer: AnyObject {
|
||||||
|
|
||||||
|
var hud: ACHud? { get set }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ACHudContainer where Self: AnyObject {
|
||||||
|
|
||||||
|
func wrapWithToast(showProgress: Bool = true, closure: @escaping () async throws -> Void) async {
|
||||||
|
do {
|
||||||
|
if showProgress {
|
||||||
|
hud = .progress
|
||||||
|
}
|
||||||
|
|
||||||
|
try await closure()
|
||||||
|
|
||||||
|
if showProgress {
|
||||||
|
hud = nil
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
hud = .error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
AutoCat/SwiftUI/ACProgressHud/ACMessageView.swift
Normal file
84
AutoCat/SwiftUI/ACProgressHud/ACMessageView.swift
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// ACMessageView.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 29.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ACMessageView: View {
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
|
||||||
|
case success
|
||||||
|
case warning
|
||||||
|
case error
|
||||||
|
case info
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: String
|
||||||
|
let type: MessageType
|
||||||
|
let action: (() -> Void)?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
Rectangle()
|
||||||
|
.fill(.black.opacity(0.7))
|
||||||
|
.ignoresSafeArea()
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer(minLength: 40)
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Image(systemName: iconName)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 48, height: 48)
|
||||||
|
.foregroundColor(iconColor)
|
||||||
|
Text(message)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
Button("OK") {
|
||||||
|
action?()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.background(.thickMaterial,
|
||||||
|
in: RoundedRectangle(cornerRadius: 16))
|
||||||
|
Spacer(minLength: 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var iconName: String {
|
||||||
|
switch type {
|
||||||
|
case .success: "checkmark.circle.fill"
|
||||||
|
case .warning: "exclamationmark.circle.fill"
|
||||||
|
case .error: "xmark.circle.fill"
|
||||||
|
case .info: "info.circle.fill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var iconColor: Color {
|
||||||
|
switch type {
|
||||||
|
case .success: .green
|
||||||
|
case .warning: .orange
|
||||||
|
case .error: .red
|
||||||
|
case .info: .blue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
VStack {
|
||||||
|
ACMessageView(message: "Some error with long text",
|
||||||
|
type: .error,
|
||||||
|
action: nil)
|
||||||
|
ACMessageView(message: "Some error with very very very very very long text",
|
||||||
|
type: .error,
|
||||||
|
action: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
50
AutoCat/SwiftUI/ACProgressHud/ACProgressHud+Modifiers.swift
Normal file
50
AutoCat/SwiftUI/ACProgressHud/ACProgressHud+Modifiers.swift
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// ACProgressHud+Modifiers.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 29.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ACProgressHudModifier: ViewModifier {
|
||||||
|
|
||||||
|
@Binding var item: ACHud?
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
|
||||||
|
content
|
||||||
|
.fullScreenCover(item: $item) { item in
|
||||||
|
if #available(iOS 16.4, *) {
|
||||||
|
makeHud(for: item)
|
||||||
|
.presentationBackground(.clear)
|
||||||
|
} else {
|
||||||
|
makeHud(for: item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transaction { transaction in
|
||||||
|
transaction.disablesAnimations = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeHud(for item: ACHud) -> some View {
|
||||||
|
switch item {
|
||||||
|
case .progress:
|
||||||
|
ACProgressView()
|
||||||
|
case .error(let error):
|
||||||
|
ACMessageView(message: error.localizedDescription,
|
||||||
|
type: .error) {
|
||||||
|
self.item = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
func hud(_ item: Binding<ACHud?>) -> some View {
|
||||||
|
modifier(ACProgressHudModifier(item: item))
|
||||||
|
}
|
||||||
|
}
|
||||||
37
AutoCat/SwiftUI/ACProgressHud/ACProgressHud.swift
Normal file
37
AutoCat/SwiftUI/ACProgressHud/ACProgressHud.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// ACProgressHud.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 29.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AutoCatCore
|
||||||
|
|
||||||
|
struct ACProgressHud: View {
|
||||||
|
|
||||||
|
@State var toast: ACHud?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Rectangle()
|
||||||
|
.fill(.orange)
|
||||||
|
.frame(width: 200, height: 200)
|
||||||
|
Button("Show progress") {
|
||||||
|
toast = .progress
|
||||||
|
}
|
||||||
|
Button("Show error") {
|
||||||
|
toast = .error(ApiError.generic)
|
||||||
|
}
|
||||||
|
Rectangle()
|
||||||
|
.fill(.orange)
|
||||||
|
.frame(width: 200, height: 200)
|
||||||
|
}
|
||||||
|
.hud($toast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ACProgressHud()
|
||||||
|
}
|
||||||
31
AutoCat/SwiftUI/ACProgressHud/ACProgressView.swift
Normal file
31
AutoCat/SwiftUI/ACProgressHud/ACProgressView.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// ACProgressView.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 29.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ACProgressView: View {
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
Rectangle()
|
||||||
|
.fill(.black.opacity(0.7))
|
||||||
|
.ignoresSafeArea()
|
||||||
|
VStack {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.large)
|
||||||
|
.padding(20)
|
||||||
|
.background(.thickMaterial,
|
||||||
|
in: RoundedRectangle(cornerRadius: 16))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ACProgressView()
|
||||||
|
}
|
||||||
@ -48,6 +48,7 @@ public typealias ContentTransformer = (_ contentView: UIView, _ position: CGFloa
|
|||||||
// MARK: - Default Transitions
|
// MARK: - Default Transitions
|
||||||
|
|
||||||
/// An enumeration to hold default content transformers
|
/// An enumeration to hold default content transformers
|
||||||
|
@MainActor
|
||||||
public enum DefaultContentTransformers {
|
public enum DefaultContentTransformers {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
internal class DismissAnimationController: NSObject {
|
internal class DismissAnimationController: NSObject {
|
||||||
|
|
||||||
private enum Constants {
|
private enum Constants {
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import UIKit
|
|||||||
|
|
||||||
// MARK: - MediaBrowserViewControllerDataSource protocol
|
// MARK: - MediaBrowserViewControllerDataSource protocol
|
||||||
/// Protocol to supply media browser contents.
|
/// Protocol to supply media browser contents.
|
||||||
|
@MainActor
|
||||||
public protocol MediaBrowserViewControllerDataSource: AnyObject {
|
public protocol MediaBrowserViewControllerDataSource: AnyObject {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,7 +39,7 @@ public protocol MediaBrowserViewControllerDataSource: AnyObject {
|
|||||||
Remember to pass the index received in the datasource method back.
|
Remember to pass the index received in the datasource method back.
|
||||||
This index is used to set the image to the correct image view.
|
This index is used to set the image to the correct image view.
|
||||||
*/
|
*/
|
||||||
typealias CompletionBlock = (_ index: Int, _ image: UIImage?, _ zoomScale: ZoomScale?, _ error: Error?) -> Void
|
typealias CompletionBlock = @MainActor @Sendable (_ index: Int, _ image: UIImage?, _ zoomScale: ZoomScale?, _ error: Error?) -> Void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Method to supply number of items to be shown in media browser.
|
Method to supply number of items to be shown in media browser.
|
||||||
@ -88,6 +89,7 @@ extension MediaBrowserViewControllerDataSource {
|
|||||||
|
|
||||||
// MARK: - MediaBrowserViewControllerDelegate protocol
|
// MARK: - MediaBrowserViewControllerDelegate protocol
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public protocol MediaBrowserViewControllerDelegate: AnyObject {
|
public protocol MediaBrowserViewControllerDelegate: AnyObject {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -767,6 +769,7 @@ extension MediaBrowserViewController {
|
|||||||
|
|
||||||
// MARK: - Updating View Positions
|
// MARK: - Updating View Positions
|
||||||
|
|
||||||
|
@MainActor
|
||||||
extension MediaBrowserViewController {
|
extension MediaBrowserViewController {
|
||||||
|
|
||||||
@objc private func update(_ timeInterval: TimeInterval) {
|
@objc private func update(_ timeInterval: TimeInterval) {
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// Holds the value of minimumZoomScale and maximumZoomScale of the image.
|
/// Holds the value of minimumZoomScale and maximumZoomScale of the image.
|
||||||
public struct ZoomScale {
|
public struct ZoomScale: Sendable {
|
||||||
|
|
||||||
/// Minimum zoom level, the image can be zoomed out to.
|
/// Minimum zoom level, the image can be zoomed out to.
|
||||||
public var minimumZoomScale: CGFloat
|
public var minimumZoomScale: CGFloat
|
||||||
|
|||||||
269
AutoCat/ThirdParty/SwiftMaskTextfield.swift
vendored
269
AutoCat/ThirdParty/SwiftMaskTextfield.swift
vendored
@ -1,269 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2016 Gabriel Maccori Kozma
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
//**********************************************************************************************************
|
|
||||||
//
|
|
||||||
// MARK: - Constants -
|
|
||||||
//
|
|
||||||
//**********************************************************************************************************
|
|
||||||
|
|
||||||
//**********************************************************************************************************
|
|
||||||
//
|
|
||||||
// MARK: - Definitions -
|
|
||||||
//
|
|
||||||
//**********************************************************************************************************
|
|
||||||
|
|
||||||
//**********************************************************************************************************
|
|
||||||
//
|
|
||||||
// MARK: - Class -
|
|
||||||
//
|
|
||||||
//**********************************************************************************************************
|
|
||||||
|
|
||||||
open class SwiftMaskTextfield : UITextField {
|
|
||||||
|
|
||||||
//**************************************************
|
|
||||||
// MARK: - Properties
|
|
||||||
//**************************************************
|
|
||||||
|
|
||||||
public let lettersAndDigitsReplacementChar: String = "*"
|
|
||||||
public let anyLetterReplecementChar: String = "@"
|
|
||||||
public let lowerCaseLetterReplecementChar: String = "a"
|
|
||||||
public let upperCaseLetterReplecementChar: String = "A"
|
|
||||||
public let digitsReplecementChar: String = "#"
|
|
||||||
|
|
||||||
/**
|
|
||||||
Var that holds the format pattern that you wish to apply
|
|
||||||
to some text
|
|
||||||
|
|
||||||
If the pattern is set to "" no mask would be applied and
|
|
||||||
the textfield would behave like a normal one
|
|
||||||
*/
|
|
||||||
@IBInspectable open var formatPattern: String = ""
|
|
||||||
|
|
||||||
/**
|
|
||||||
Var that holds the prefix to be added to the textfield
|
|
||||||
|
|
||||||
If the prefix is set to "" no string will be added to the beggining
|
|
||||||
of the text
|
|
||||||
*/
|
|
||||||
@IBInspectable open var prefix: String = ""
|
|
||||||
|
|
||||||
/**
|
|
||||||
Var that have the maximum length, based on the mask set
|
|
||||||
*/
|
|
||||||
open var maxLength: Int {
|
|
||||||
get {
|
|
||||||
return formatPattern.count + prefix.count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Overriding the var text from UITextField so if any text
|
|
||||||
is applied programmatically by calling formatText
|
|
||||||
*/
|
|
||||||
override open var text: String? {
|
|
||||||
set {
|
|
||||||
super.text = newValue
|
|
||||||
self.formatText()
|
|
||||||
}
|
|
||||||
|
|
||||||
get {
|
|
||||||
return super.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//**************************************************
|
|
||||||
// MARK: - Constructors
|
|
||||||
//**************************************************
|
|
||||||
|
|
||||||
public required init?(coder aDecoder: NSCoder) {
|
|
||||||
super.init(coder: aDecoder)
|
|
||||||
self.setup()
|
|
||||||
}
|
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
self.setup()
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
self.deRegisterForNotifications()
|
|
||||||
}
|
|
||||||
|
|
||||||
//**************************************************
|
|
||||||
// MARK: - Private Methods
|
|
||||||
//**************************************************
|
|
||||||
|
|
||||||
fileprivate func setup() {
|
|
||||||
self.registerForNotifications()
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func registerForNotifications() {
|
|
||||||
NotificationCenter.default.addObserver(self,
|
|
||||||
selector: #selector(textDidChange),
|
|
||||||
name: NSNotification.Name(rawValue: "UITextFieldTextDidChangeNotification"),
|
|
||||||
object: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func deRegisterForNotifications() {
|
|
||||||
NotificationCenter.default.removeObserver(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc fileprivate func textDidChange() {
|
|
||||||
self.undoManager?.removeAllActions()
|
|
||||||
self.formatText()
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func getOnlyDigitsString(_ string: String) -> String {
|
|
||||||
let charactersArray = string.components(separatedBy: CharacterSet.decimalDigits.inverted)
|
|
||||||
return charactersArray.joined(separator: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func getOnlyLettersString(_ string: String) -> String {
|
|
||||||
let charactersArray = string.components(separatedBy: CharacterSet.letters.inverted)
|
|
||||||
return charactersArray.joined(separator: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func getUppercaseLettersString(_ string: String) -> String {
|
|
||||||
let charactersArray = string.components(separatedBy: CharacterSet.uppercaseLetters.inverted)
|
|
||||||
return charactersArray.joined(separator: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func getLowercaseLettersString(_ string: String) -> String {
|
|
||||||
let charactersArray = string.components(separatedBy: CharacterSet.lowercaseLetters.inverted)
|
|
||||||
return charactersArray.joined(separator: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func getFilteredString(_ string: String) -> String {
|
|
||||||
let charactersArray = string.components(separatedBy: CharacterSet.alphanumerics.inverted)
|
|
||||||
return charactersArray.joined(separator: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func getStringWithoutPrefix(_ string: String) -> String {
|
|
||||||
if string.range(of: self.prefix) != nil {
|
|
||||||
if string.count > self.prefix.count {
|
|
||||||
let prefixIndex = string.index(string.endIndex, offsetBy: (string.count - self.prefix.count) * -1)
|
|
||||||
return String(string[prefixIndex...])
|
|
||||||
} else if string.count == self.prefix.count {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
|
|
||||||
//**************************************************
|
|
||||||
// MARK: - Self Public Methods
|
|
||||||
//**************************************************
|
|
||||||
|
|
||||||
/**
|
|
||||||
Func that formats the text based on formatPattern
|
|
||||||
|
|
||||||
Override this function if you want to customize the behaviour of
|
|
||||||
the class
|
|
||||||
*/
|
|
||||||
open func formatText() {
|
|
||||||
var currentTextForFormatting = ""
|
|
||||||
|
|
||||||
if let text = super.text {
|
|
||||||
if text.count > 0 {
|
|
||||||
currentTextForFormatting = self.getStringWithoutPrefix(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.maxLength > 0 {
|
|
||||||
var formatterIndex = self.formatPattern.startIndex, currentTextForFormattingIndex = currentTextForFormatting.startIndex
|
|
||||||
var finalText = ""
|
|
||||||
|
|
||||||
currentTextForFormatting = self.getFilteredString(currentTextForFormatting)
|
|
||||||
|
|
||||||
if currentTextForFormatting.count > 0 {
|
|
||||||
while true {
|
|
||||||
let formatPatternRange = formatterIndex ..< formatPattern.index(after: formatterIndex)
|
|
||||||
let currentFormatCharacter = String(self.formatPattern[formatPatternRange])
|
|
||||||
|
|
||||||
let currentTextForFormattingPatterRange = currentTextForFormattingIndex ..< currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
|
||||||
let currentTextForFormattingCharacter = String(currentTextForFormatting[currentTextForFormattingPatterRange])
|
|
||||||
|
|
||||||
switch currentFormatCharacter {
|
|
||||||
case self.lettersAndDigitsReplacementChar:
|
|
||||||
finalText += currentTextForFormattingCharacter
|
|
||||||
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
|
||||||
formatterIndex = formatPattern.index(after: formatterIndex)
|
|
||||||
case self.anyLetterReplecementChar:
|
|
||||||
let filteredChar = self.getOnlyLettersString(currentTextForFormattingCharacter)
|
|
||||||
if !filteredChar.isEmpty {
|
|
||||||
finalText += filteredChar
|
|
||||||
formatterIndex = formatPattern.index(after: formatterIndex)
|
|
||||||
}
|
|
||||||
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
|
||||||
case self.lowerCaseLetterReplecementChar:
|
|
||||||
let filteredChar = self.getLowercaseLettersString(currentTextForFormattingCharacter)
|
|
||||||
if !filteredChar.isEmpty {
|
|
||||||
finalText += filteredChar
|
|
||||||
formatterIndex = formatPattern.index(after: formatterIndex)
|
|
||||||
}
|
|
||||||
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
|
||||||
case self.upperCaseLetterReplecementChar:
|
|
||||||
let filteredChar = self.getUppercaseLettersString(currentTextForFormattingCharacter)
|
|
||||||
if !filteredChar.isEmpty {
|
|
||||||
finalText += filteredChar
|
|
||||||
formatterIndex = formatPattern.index(after: formatterIndex)
|
|
||||||
}
|
|
||||||
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
|
||||||
case self.digitsReplecementChar:
|
|
||||||
let filteredChar = self.getOnlyDigitsString(currentTextForFormattingCharacter)
|
|
||||||
if !filteredChar.isEmpty {
|
|
||||||
finalText += filteredChar
|
|
||||||
formatterIndex = formatPattern.index(after: formatterIndex)
|
|
||||||
}
|
|
||||||
currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex)
|
|
||||||
default:
|
|
||||||
finalText += currentFormatCharacter
|
|
||||||
formatterIndex = formatPattern.index(after: formatterIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
if formatterIndex >= self.formatPattern.endIndex ||
|
|
||||||
currentTextForFormattingIndex >= currentTextForFormatting.endIndex {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if finalText.count > 0 {
|
|
||||||
super.text = "\(self.prefix)\(finalText)"
|
|
||||||
} else {
|
|
||||||
super.text = finalText
|
|
||||||
}
|
|
||||||
|
|
||||||
if let text = self.text {
|
|
||||||
if text.count > self.maxLength {
|
|
||||||
super.text = String(text[text.index(text.startIndex, offsetBy: self.maxLength)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
40
AutoCat/Utils/ActivityItemSource.swift
Normal file
40
AutoCat/Utils/ActivityItemSource.swift
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// ActivityItemSource.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 11.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import LinkPresentation
|
||||||
|
|
||||||
|
class ActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
|
|
||||||
|
let url: URL
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
init(url: URL, title: String) {
|
||||||
|
self.url = url
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||||
|
return UIImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
|
||||||
|
|
||||||
|
let metadata = LPLinkMetadata()
|
||||||
|
metadata.title = title
|
||||||
|
metadata.originalURL = url
|
||||||
|
metadata.url = url
|
||||||
|
metadata.imageProvider = NSItemProvider(contentsOf: url)
|
||||||
|
metadata.iconProvider = NSItemProvider(contentsOf: url)
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import RxSwift
|
|
||||||
import RxRelay
|
|
||||||
|
|
||||||
enum PlayerState {
|
enum PlayerState {
|
||||||
case stopped
|
case stopped
|
||||||
@ -11,12 +9,12 @@ enum PlayerState {
|
|||||||
|
|
||||||
class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
||||||
|
|
||||||
static let shared = AudioPlayer()
|
@MainActor static let shared = AudioPlayer()
|
||||||
|
|
||||||
private var player: AVAudioPlayer?
|
private var player: AVAudioPlayer?
|
||||||
private var url: URL?
|
private var url: URL?
|
||||||
private var state = BehaviorRelay<PlayerState>(value: .stopped)
|
private var state: PlayerState = .stopped
|
||||||
private var progress = BehaviorRelay<Double>(value: 0)
|
private var progress: Double = 0
|
||||||
private var progressTimer: Timer?
|
private var progressTimer: Timer?
|
||||||
|
|
||||||
func set(url: URL) throws {
|
func set(url: URL) throws {
|
||||||
@ -35,12 +33,12 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
|||||||
if player.isPlaying {
|
if player.isPlaying {
|
||||||
player.pause()
|
player.pause()
|
||||||
try self.deactivateSession()
|
try self.deactivateSession()
|
||||||
self.state.accept(.paused)
|
self.state = .paused
|
||||||
} else {
|
} else {
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.duckOthers])
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.duckOthers])
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
player.play()
|
player.play()
|
||||||
self.state.accept(.playing)
|
self.state = .playing
|
||||||
if self.progressTimer == nil {
|
if self.progressTimer == nil {
|
||||||
self.progressTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(progressTick), userInfo: nil, repeats: true)
|
self.progressTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(progressTick), userInfo: nil, repeats: true)
|
||||||
}
|
}
|
||||||
@ -57,7 +55,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
|||||||
if let player = self.player {
|
if let player = self.player {
|
||||||
player.pause()
|
player.pause()
|
||||||
try? self.deactivateSession()
|
try? self.deactivateSession()
|
||||||
self.state.accept(.paused)
|
self.state = .paused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,18 +63,18 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
|||||||
if let player = self.player {
|
if let player = self.player {
|
||||||
player.stop()
|
player.stop()
|
||||||
try? self.deactivateSession()
|
try? self.deactivateSession()
|
||||||
self.state.accept(.stopped)
|
self.state = .stopped
|
||||||
self.progressTimer?.invalidate()
|
self.progressTimer?.invalidate()
|
||||||
self.progressTimer = nil
|
self.progressTimer = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getState() -> PlayerState {
|
func getState() -> PlayerState {
|
||||||
return self.state.value
|
return self.state
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProgress() -> Double {
|
func getProgress() -> Double {
|
||||||
return self.progress.value
|
return self.progress
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUrl() -> URL? {
|
func getUrl() -> URL? {
|
||||||
@ -87,14 +85,6 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
|||||||
return self.player?.duration ?? 0
|
return self.player?.duration ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func stateObservable() -> Observable<PlayerState> {
|
|
||||||
return self.state.asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
func progressObservable() -> Observable<Double> {
|
|
||||||
return self.progress.asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
func deactivateSession() throws {
|
func deactivateSession() throws {
|
||||||
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
}
|
}
|
||||||
@ -103,15 +93,15 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
|||||||
|
|
||||||
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
||||||
try? self.deactivateSession()
|
try? self.deactivateSession()
|
||||||
self.progress.accept(1)
|
self.progress = 1
|
||||||
self.stop()
|
self.stop()
|
||||||
self.state.accept(.stopped)
|
self.state = .stopped
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func progressTick() {
|
@objc func progressTick() {
|
||||||
if let player = self.player {
|
if let player = self.player {
|
||||||
let progress = player.currentTime/player.duration
|
let progress = player.currentTime/player.duration
|
||||||
self.progress.accept(progress)
|
self.progress = progress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
AutoCat/Utils/Coordinator.swift
Normal file
27
AutoCat/Utils/Coordinator.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// Coordinator.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 24.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol Coordinator {
|
||||||
|
|
||||||
|
associatedtype ResultType
|
||||||
|
//associatedtype Controller: UIViewController
|
||||||
|
|
||||||
|
var viewController: UIViewController? { get }
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func start() async throws -> ResultType
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Coordinator {
|
||||||
|
|
||||||
|
var viewController: UIViewController? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
||||||
19
AutoCat/Utils/Formatters.swift
Normal file
19
AutoCat/Utils/Formatters.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Formatters.swift
|
||||||
|
// AutoCat
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 24.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Formatters {
|
||||||
|
|
||||||
|
static let standard: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .medium
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
}
|
||||||
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import Speech
|
import Speech
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
import RxSwift
|
|
||||||
import os.log
|
import os.log
|
||||||
import ExceptionCatcher
|
import ExceptionCatcher
|
||||||
|
|
||||||
@ -34,58 +33,53 @@ class Recorder {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestPermissions() -> Single<Void> {
|
func requestPermissions() async throws {
|
||||||
return Single<Void>.create { observer in
|
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
AVAudioSession.sharedInstance().requestRecordPermission { allowed in
|
AVAudioSession.sharedInstance().requestRecordPermission { allowed in
|
||||||
if allowed {
|
if allowed {
|
||||||
SFSpeechRecognizer.requestAuthorization { status in
|
SFSpeechRecognizer.requestAuthorization { status in
|
||||||
switch status {
|
switch status {
|
||||||
case .authorized:
|
case .authorized:
|
||||||
observer(.success(()))
|
continuation.resume()
|
||||||
break
|
break
|
||||||
case .denied:
|
case .denied:
|
||||||
let error = CocoaError.error("Access error", reason: "Access to speech recognition is denied", suggestion: "Please give permission to use speech recognition in system settings")
|
let error = CocoaError.error("Access error", reason: "Access to speech recognition is denied", suggestion: "Please give permission to use speech recognition in system settings")
|
||||||
observer(.failure(error))
|
continuation.resume(throwing: error)
|
||||||
break
|
|
||||||
case .restricted:
|
case .restricted:
|
||||||
let error = CocoaError.error("Access error", reason: "Speech recognition is restricted on this device")
|
let error = CocoaError.error("Access error", reason: "Speech recognition is restricted on this device")
|
||||||
observer(.failure(error))
|
continuation.resume(throwing: error)
|
||||||
break
|
|
||||||
case .notDetermined:
|
case .notDetermined:
|
||||||
let error = CocoaError.error("Access error", reason: "Speech recognition status is not yet determined")
|
let error = CocoaError.error("Access error", reason: "Speech recognition status is not yet determined")
|
||||||
observer(.failure(error))
|
continuation.resume(throwing: error)
|
||||||
break
|
|
||||||
@unknown default:
|
@unknown default:
|
||||||
let error = CocoaError.error("Access error", reason: "Unknown error accessing speech recognizer")
|
let error = CocoaError.error("Access error", reason: "Unknown error accessing speech recognizer")
|
||||||
observer(.failure(error))
|
continuation.resume(throwing: error)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let error = CocoaError.error("Access error", reason: "Access to microphone is denied", suggestion: "Please give permission to use microphone in system settings")
|
let error = CocoaError.error("Access error", reason: "Access to microphone is denied", suggestion: "Please give permission to use microphone in system settings")
|
||||||
observer(.failure(error))
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Disposables.create()
|
func startRecording(to file: URL) async throws -> String {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startRecording(to file: URL) -> Single<String> {
|
|
||||||
guard self.microphoneAvailable() else {
|
guard self.microphoneAvailable() else {
|
||||||
return Single.error(CocoaError.error("Recording error", reason: "Microphone not found"))
|
throw CocoaError.error("Recording error", reason: "Microphone not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return Single<String>.create { observer in
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
guard let aac = AVAudioFormat(settings: self.recordingSettings) else {
|
guard let aac = AVAudioFormat(settings: self.recordingSettings) else {
|
||||||
observer(.failure(CocoaError.error("Recording error", reason: "Format not supported")))
|
continuation.resume(throwing: CocoaError.error("Recording error", reason: "Format not supported"))
|
||||||
return Disposables.create()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtAudioFileCreateWithURL(file as CFURL, kAudioFileM4AType, aac.streamDescription, nil, AudioFileFlags.eraseFile.rawValue, &self.fileRef)
|
ExtAudioFileCreateWithURL(file as CFURL, kAudioFileM4AType, aac.streamDescription, nil, AudioFileFlags.eraseFile.rawValue, &self.fileRef)
|
||||||
guard let fileRef = self.fileRef else {
|
guard let fileRef = self.fileRef else {
|
||||||
observer(.failure(CocoaError.error(CocoaError.Code.fileWriteUnknown)))
|
continuation.resume(throwing: CocoaError.error(CocoaError.Code.fileWriteUnknown))
|
||||||
return Disposables.create()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@ -106,26 +100,24 @@ class Recorder {
|
|||||||
if let transcription = result?.bestTranscription {
|
if let transcription = result?.bestTranscription {
|
||||||
self.result = transcription.formattedString
|
self.result = transcription.formattedString
|
||||||
self.endRecognitionTimer?.invalidate()
|
self.endRecognitionTimer?.invalidate()
|
||||||
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
|
|
||||||
|
// TODO: Check if it actually works
|
||||||
|
RunLoop.current.schedule(after: .init(Date()).advanced(by: .seconds(5)), tolerance: .seconds(1), options: nil) {
|
||||||
self.finishRecording()
|
self.finishRecording()
|
||||||
observer(.success(self.result))
|
continuation.resume(returning: self.result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
|
RunLoop.current.schedule(after: .init(Date()).advanced(by: .seconds(5)), tolerance: .seconds(1), options: nil) {
|
||||||
self.finishRecording()
|
self.finishRecording()
|
||||||
observer(.success(self.result))
|
continuation.resume(returning: self.result)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.engine.prepare()
|
self.engine.prepare()
|
||||||
try self.engine.start()
|
try self.engine.start()
|
||||||
} catch {
|
} catch {
|
||||||
observer(.failure(error))
|
continuation.resume(throwing: error)
|
||||||
}
|
|
||||||
|
|
||||||
return Disposables.create {
|
|
||||||
self.cancelRecording()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,20 +6,21 @@ import AutoCatCore
|
|||||||
typealias FilterPredicate<T> = (T) -> Bool
|
typealias FilterPredicate<T> = (T) -> Bool
|
||||||
|
|
||||||
class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
||||||
where Item: Object & Identifiable & Dated & Cloneable,
|
where Item: Object & DtoConvertible,
|
||||||
|
Item.Dto: Identifiable & Dated,
|
||||||
Cell: UITableViewCell & ConfigurableCell,
|
Cell: UITableViewCell & ConfigurableCell,
|
||||||
Cell.Item == Item {
|
Cell.Item == Item.Dto {
|
||||||
|
|
||||||
private var tv: UITableView
|
private var tv: UITableView
|
||||||
private var data: Results<Item>
|
private var data: Results<Item>
|
||||||
private var notificationToken: NotificationToken?
|
private var notificationToken: NotificationToken?
|
||||||
private var sections: [DateSection<Item>] = []
|
private var sections: [DateSection<Item.Dto>] = []
|
||||||
private var cellIdentifier: String
|
private var cellIdentifier: String
|
||||||
private var filterPredicate: FilterPredicate<Item>?
|
private var filterPredicate: FilterPredicate<Item.Dto>?
|
||||||
private var searchPredicate: FilterPredicate<Item>?
|
private var searchPredicate: FilterPredicate<Item.Dto>?
|
||||||
private let groupQueue = DispatchQueue(label: "group")
|
private let groupQueue = DispatchQueue(label: "group")
|
||||||
|
|
||||||
private var objects: [Item] = []
|
private var objects: [Item.Dto] = []
|
||||||
|
|
||||||
private let onSizeChanged: ((Int) -> Void)?
|
private let onSizeChanged: ((Int) -> Void)?
|
||||||
|
|
||||||
@ -39,13 +40,13 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
|||||||
self.notificationToken = self.data.observe { changes in
|
self.notificationToken = self.data.observe { changes in
|
||||||
switch changes {
|
switch changes {
|
||||||
case .initial:
|
case .initial:
|
||||||
self.objects = self.data.toArray().map { $0.shallowClone() }
|
self.objects = self.data.toArray().map(\.shallowDto)
|
||||||
self.reload(animated: false)
|
self.reload(animated: false)
|
||||||
case .update(_, let deletions, let insertions, let modifications):
|
case .update(_, let deletions, let insertions, let modifications):
|
||||||
|
|
||||||
deletions.forEach { self.objects.remove(at: $0) }
|
deletions.forEach { self.objects.remove(at: $0) }
|
||||||
insertions.forEach { self.objects.insert(self.data[$0].shallowClone(), at: $0) }
|
insertions.forEach { self.objects.insert(self.data[$0].shallowDto, at: $0) }
|
||||||
modifications.forEach { self.objects[$0] = self.data[$0].shallowClone() }
|
modifications.forEach { self.objects[$0] = self.data[$0].shallowDto }
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
// if deletions.isEmpty && modifications.isEmpty && insertions.count == 1 && insertions.first == 0 {
|
// if deletions.isEmpty && modifications.isEmpty && insertions.count == 1 && insertions.first == 0 {
|
||||||
@ -93,7 +94,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
|||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
func item(at indexPath: IndexPath) -> Item {
|
func item(at indexPath: IndexPath) -> Item.Dto {
|
||||||
return self.sections[indexPath.section].elements[indexPath.row]
|
return self.sections[indexPath.section].elements[indexPath.row]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +115,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
|||||||
}
|
}
|
||||||
|
|
||||||
func insertFirst() {
|
func insertFirst() {
|
||||||
guard let item = data.first?.shallowClone() else {
|
guard let item = data.first?.shallowDto else {
|
||||||
reload(animated: false)
|
reload(animated: false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -129,7 +130,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reloadFirst() {
|
func reloadFirst() {
|
||||||
guard !sections.isEmpty, let item = data.first?.clone() else {
|
guard !sections.isEmpty, let item = data.first?.dto else {
|
||||||
reload(animated: false)
|
reload(animated: false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -147,7 +148,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
|||||||
}
|
}
|
||||||
|
|
||||||
sections[index].remove(at: itemIndex)
|
sections[index].remove(at: itemIndex)
|
||||||
sections[0].insert(data[0].clone(), at: 0)
|
sections[0].insert(data[0].dto, at: 0)
|
||||||
let fromIndexPath = IndexPath(row: itemIndex, section: index)
|
let fromIndexPath = IndexPath(row: itemIndex, section: index)
|
||||||
let toIndexPath = IndexPath(row: 0, section: 0)
|
let toIndexPath = IndexPath(row: 0, section: 0)
|
||||||
tv.moveRow(at: fromIndexPath, to: toIndexPath)
|
tv.moveRow(at: fromIndexPath, to: toIndexPath)
|
||||||
@ -161,7 +162,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setFilterPredicate(_ predicate: FilterPredicate<Item>?) {
|
func setFilterPredicate(_ predicate: FilterPredicate<Item.Dto>?) {
|
||||||
guard self.filterPredicate != nil || predicate != nil else {
|
guard self.filterPredicate != nil || predicate != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -170,7 +171,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
|||||||
self.reload()
|
self.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSearchPredicate(_ predicate: FilterPredicate<Item>?) {
|
func setSearchPredicate(_ predicate: FilterPredicate<Item.Dto>?) {
|
||||||
guard self.searchPredicate != nil || predicate != nil else {
|
guard self.searchPredicate != nil || predicate != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -184,7 +185,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
|
|||||||
throw CocoaError.error("Type \(Item.self) is not exportable")
|
throw CocoaError.error("Type \(Item.self) is not exportable")
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = self.data.toArray().map { $0.clone() }
|
var items = self.data.toArray().map(\.dto)
|
||||||
if let predicate = self.filterPredicate {
|
if let predicate = self.filterPredicate {
|
||||||
items = items.filter(predicate)
|
items = items.filter(predicate)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
|
||||||
import RxCocoa
|
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
class RxSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource where Item: Dated & Decodable & Identifiable, Cell: UITableViewCell & ConfigurableCell, Cell.Item == Item {
|
class SectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource where Item: Dated & Decodable & Identifiable & Sendable, Cell: UITableViewCell & ConfigurableCell, Cell.Item == Item {
|
||||||
private var tv: UITableView
|
private var tv: UITableView
|
||||||
private var cellIdentifier: String
|
private var cellIdentifier: String
|
||||||
private var sections: [DateSection<Item>] = []
|
private var sections: [DateSection<Item>] = []
|
||||||
@ -53,8 +51,7 @@ class RxSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource where I
|
|||||||
self.sections[indexPath.section].elements[indexPath.row] = item
|
self.sections[indexPath.section].elements[indexPath.row] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
var data: Binder<PagedResponse<Item>> {
|
func update(with data: PagedResponse<Item>) {
|
||||||
return Binder(self) { datasource, data in
|
|
||||||
if let count = data.count {
|
if let count = data.count {
|
||||||
self.count = count
|
self.count = count
|
||||||
self.items = data.items
|
self.items = data.items
|
||||||
@ -63,14 +60,14 @@ class RxSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource where I
|
|||||||
}
|
}
|
||||||
self.pageToken = data.pageToken
|
self.pageToken = data.pageToken
|
||||||
|
|
||||||
DispatchQueue.global().async {
|
// TODO: Grouping on background thread
|
||||||
|
// DispatchQueue.global().async {
|
||||||
let newSections = self.items.groupedByDate(type: self.sortParam)
|
let newSections = self.items.groupedByDate(type: self.sortParam)
|
||||||
DispatchQueue.main.async {
|
// DispatchQueue.main.async {
|
||||||
self.sections = newSections
|
self.sections = newSections
|
||||||
self.tv.reloadData()
|
self.tv.reloadData()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func needMoreData() -> Bool {
|
func needMoreData() -> Bool {
|
||||||
|
|||||||
@ -1,12 +1,22 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class FlagLayer: CALayer {
|
class FlagLayer: CALayer {
|
||||||
let pixelWidth = 1/UIScreen.main.scale
|
let pixelWidth: CGFloat
|
||||||
|
|
||||||
// Flag colors - https://ru.wikipedia.org/wiki/%D0%A4%D0%BB%D0%B0%D0%B3_%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D0%B8
|
// Flag colors - https://ru.wikipedia.org/wiki/%D0%A4%D0%BB%D0%B0%D0%B3_%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D0%B8
|
||||||
let blue = CGColor(srgbRed: 0, green: 57/256.0, blue: 166/256.0, alpha: 1)
|
let blue = CGColor(srgbRed: 0, green: 57/256.0, blue: 166/256.0, alpha: 1)
|
||||||
let red = CGColor(srgbRed: 213/256.0, green: 43/256.0, blue: 30/256.0, alpha: 1)
|
let red = CGColor(srgbRed: 213/256.0, green: 43/256.0, blue: 30/256.0, alpha: 1)
|
||||||
|
|
||||||
|
init(scale: CGFloat) {
|
||||||
|
|
||||||
|
self.pixelWidth = 1/scale
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
override func draw(in ctx: CGContext) {
|
override func draw(in ctx: CGContext) {
|
||||||
ctx.saveGState()
|
ctx.saveGState()
|
||||||
super.draw(in: ctx)
|
super.draw(in: ctx)
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
|
@MainActor
|
||||||
protocol PNKeyboardDelegate: AnyObject {
|
protocol PNKeyboardDelegate: AnyObject {
|
||||||
func returnClicked()
|
func returnClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
protocol PNButtonDelegate: AnyObject {
|
protocol PNButtonDelegate: AnyObject {
|
||||||
func buttonTapped(_ button: PNButton)
|
func buttonTapped(_ button: PNButton)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import UIKit
|
|||||||
import AutoCatCore
|
import AutoCatCore
|
||||||
|
|
||||||
private class CALayerAnimationsDisablingDelegate: NSObject, CALayerDelegate {
|
private class CALayerAnimationsDisablingDelegate: NSObject, CALayerDelegate {
|
||||||
static let shared = CALayerAnimationsDisablingDelegate()
|
@MainActor static let shared = CALayerAnimationsDisablingDelegate()
|
||||||
private let null = NSNull()
|
private let null = NSNull()
|
||||||
|
|
||||||
func action(for layer: CALayer, forKey event: String) -> CAAction? {
|
func action(for layer: CALayer, forKey event: String) -> CAAction? {
|
||||||
@ -22,7 +22,7 @@ class PlateView: UIView {
|
|||||||
private var numberLayer = CenterTextLayer(coeff: fontHeightCoeff)
|
private var numberLayer = CenterTextLayer(coeff: fontHeightCoeff)
|
||||||
private var regionLayer = CenterTextLayer(coeff: fontHeightCoeff)
|
private var regionLayer = CenterTextLayer(coeff: fontHeightCoeff)
|
||||||
private var countryLayer = CenterTextLayer()
|
private var countryLayer = CenterTextLayer()
|
||||||
private var flagLayer = FlagLayer()
|
private var flagLayer = FlagLayer(scale: UIScreen.main.scale)
|
||||||
|
|
||||||
var number: PlateNumber? {
|
var number: PlateNumber? {
|
||||||
didSet {
|
didSet {
|
||||||
@ -42,7 +42,7 @@ class PlateView: UIView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var onChange: (() -> Void)?
|
var onChange: (@MainActor () -> Void)?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
import Eureka
|
|
||||||
|
|
||||||
class MultilineLabelCell: Cell<String>, CellType {
|
|
||||||
private(set) var title: UILabel!
|
|
||||||
private(set) var value: UILabel!
|
|
||||||
|
|
||||||
required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
super.init(coder: aDecoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func setup() {
|
|
||||||
super.setup()
|
|
||||||
|
|
||||||
self.selectionStyle = .none
|
|
||||||
|
|
||||||
self.title = UILabel()
|
|
||||||
self.contentView.addSubview(self.title)
|
|
||||||
self.title.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
self.title.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
||||||
self.title.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor).isActive = true
|
|
||||||
self.title.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor).isActive = true
|
|
||||||
self.title.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 8).isActive = true
|
|
||||||
|
|
||||||
self.value = UILabel()
|
|
||||||
self.contentView.addSubview(self.value)
|
|
||||||
self.value.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
self.value.textColor = .secondaryLabel
|
|
||||||
self.value.numberOfLines = 0
|
|
||||||
self.value.textAlignment = .right
|
|
||||||
self.value.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor).isActive = true
|
|
||||||
self.value.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor).isActive = true
|
|
||||||
self.value.topAnchor.constraint(equalTo: self.title.bottomAnchor, constant: 8).isActive = true
|
|
||||||
self.value.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8).isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func update() {
|
|
||||||
super.update()
|
|
||||||
|
|
||||||
self.textLabel?.text = nil
|
|
||||||
self.detailTextLabel?.text = nil
|
|
||||||
|
|
||||||
self.title.text = row.title
|
|
||||||
self.value.text = row.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class MultilineLabelRow: Row<MultilineLabelCell>, RowType {
|
|
||||||
required init(tag: String?) {
|
|
||||||
super.init(tag: tag)
|
|
||||||
cellProvider = CellProvider<MultilineLabelCell>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
import Eureka
|
|
||||||
|
|
||||||
class MultilineLinkCell: MultilineLabelCell {
|
|
||||||
required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
super.init(coder: aDecoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func update() {
|
|
||||||
super.update()
|
|
||||||
|
|
||||||
self.textLabel?.text = nil
|
|
||||||
self.detailTextLabel?.text = nil
|
|
||||||
|
|
||||||
self.title.text = row.title
|
|
||||||
|
|
||||||
if let url = row.value {
|
|
||||||
self.value.attributedText = NSAttributedString(string: url, attributes: [
|
|
||||||
.link: url
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
self.title.attributedText = NSAttributedString(string: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class MultilineLinkRow: Row<MultilineLinkCell>, RowType {
|
|
||||||
required init(tag: String?) {
|
|
||||||
super.init(tag: tag)
|
|
||||||
cellProvider = CellProvider<MultilineLinkCell>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -12,7 +12,7 @@ extension DebugInfoStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourceStatusCell: Cell<DebugInfoEntry>, CellType {
|
class SourceStatusCell: Cell<DebugInfoEntryDto>, CellType {
|
||||||
private var circle: UIImageView!
|
private var circle: UIImageView!
|
||||||
|
|
||||||
required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
@ -43,7 +43,7 @@ class SourceStatusCell: Cell<DebugInfoEntry>, CellType {
|
|||||||
|
|
||||||
self.detailTextLabel?.text = nil
|
self.detailTextLabel?.text = nil
|
||||||
if let value = row.value {
|
if let value = row.value {
|
||||||
self.circle.tintColor = value.statusEnum.color
|
self.circle.tintColor = value.status.color
|
||||||
//self.accessoryType = value.error == nil ? .none : .disclosureIndicator
|
//self.accessoryType = value.error == nil ? .none : .disclosureIndicator
|
||||||
} else {
|
} else {
|
||||||
self.circle.tintColor = .systemGray
|
self.circle.tintColor = .systemGray
|
||||||
@ -57,11 +57,12 @@ final class SourceStatusRow: Row<SourceStatusCell>, RowType {
|
|||||||
super.init(tag: tag)
|
super.init(tag: tag)
|
||||||
cellProvider = CellProvider<SourceStatusCell>()
|
cellProvider = CellProvider<SourceStatusCell>()
|
||||||
self.onCellSelection { cell, row in
|
self.onCellSelection { cell, row in
|
||||||
if let error = row.value?.error, let controller = cell.parentViewController {
|
// TODO: Fix isolation problems
|
||||||
let alert = UIAlertController(title: row.title, message: error, preferredStyle: .alert)
|
// if let error = row.value?.error, let controller = cell.parentViewController {
|
||||||
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
|
// let alert = UIAlertController(title: row.title, message: error, preferredStyle: .alert)
|
||||||
controller.present(alert, animated: true)
|
// alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
|
||||||
}
|
// controller.present(alert, animated: true)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ extension NSError {
|
|||||||
return (title: failure, body: reason)
|
return (title: failure, body: reason)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return (title: "Error", body: "")
|
return (title: "Error", body: localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
public protocol Cloneable {
|
|
||||||
init(copy: Self)
|
|
||||||
func shallowClone() -> Self
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Cloneable {
|
|
||||||
public func clone() -> Self {
|
|
||||||
return Self.init(copy: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func shallowClone() -> Self {
|
|
||||||
return clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
AutoCatCore/Models/DTO/AudioRecordDto.swift
Normal file
37
AutoCatCore/Models/DTO/AudioRecordDto.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// AudioRecordDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct AudioRecordDto: Decodable {
|
||||||
|
|
||||||
|
public var path: String = ""
|
||||||
|
public var number: String?
|
||||||
|
public var rawText: String = ""
|
||||||
|
public var addedDate: TimeInterval = 0
|
||||||
|
public var duration: TimeInterval = 0
|
||||||
|
public var event: VehicleEventDto?
|
||||||
|
|
||||||
|
public init(path: String, number: String?, raw: String, duration: TimeInterval, event: VehicleEventDto?) {
|
||||||
|
self.path = path
|
||||||
|
self.number = number
|
||||||
|
self.duration = duration
|
||||||
|
self.rawText = raw
|
||||||
|
self.event = event
|
||||||
|
self.addedDate = Date().timeIntervalSince1970
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getAddedDate() -> TimeInterval {
|
||||||
|
addedDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AudioRecordDto: Identifiable {
|
||||||
|
|
||||||
|
public var id: TimeInterval { addedDate }
|
||||||
|
}
|
||||||
31
AutoCatCore/Models/DTO/DebugInfoDto.swift
Normal file
31
AutoCatCore/Models/DTO/DebugInfoDto.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// DebugInfoDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum DebugInfoStatus: Int, Sendable, Decodable {
|
||||||
|
case success = 0
|
||||||
|
case error = 1
|
||||||
|
case warning = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DebugInfoDto: Decodable, Sendable {
|
||||||
|
|
||||||
|
public var autocod: DebugInfoEntryDto
|
||||||
|
public var vin01vin: DebugInfoEntryDto
|
||||||
|
public var vin01base: DebugInfoEntryDto
|
||||||
|
public var vin01history: DebugInfoEntryDto
|
||||||
|
public var nomerogram: DebugInfoEntryDto
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DebugInfoEntryDto: Decodable, Sendable, Equatable {
|
||||||
|
|
||||||
|
public var fields: Int64 = 0
|
||||||
|
public var error: String?
|
||||||
|
public var status: DebugInfoStatus = .success
|
||||||
|
}
|
||||||
54
AutoCatCore/Models/DTO/OsagoDto.swift
Normal file
54
AutoCatCore/Models/DTO/OsagoDto.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// OsagoDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct OsagoDto: Decodable, Sendable {
|
||||||
|
|
||||||
|
public var date: TimeInterval = 0
|
||||||
|
public var number: String = ""
|
||||||
|
public var vin: String?
|
||||||
|
public var plateNumber: String?
|
||||||
|
public var name: String = ""
|
||||||
|
public var status: String?
|
||||||
|
public var restrictions: String = ""
|
||||||
|
public var insurant: String?
|
||||||
|
public var owner: String?
|
||||||
|
public var usageRegion: String?
|
||||||
|
public var birthday: String?
|
||||||
|
|
||||||
|
public init(date: TimeInterval,
|
||||||
|
number: String,
|
||||||
|
vin: String? = nil,
|
||||||
|
plateNumber: String? = nil,
|
||||||
|
name: String,
|
||||||
|
status: String? = nil,
|
||||||
|
restrictions: String,
|
||||||
|
insurant: String? = nil,
|
||||||
|
owner: String? = nil,
|
||||||
|
usageRegion: String? = nil,
|
||||||
|
birthday: String? = nil) {
|
||||||
|
|
||||||
|
self.date = date
|
||||||
|
self.number = number
|
||||||
|
self.vin = vin
|
||||||
|
self.plateNumber = plateNumber
|
||||||
|
self.name = name
|
||||||
|
self.status = status
|
||||||
|
self.restrictions = restrictions
|
||||||
|
self.insurant = insurant
|
||||||
|
self.owner = owner
|
||||||
|
self.usageRegion = usageRegion
|
||||||
|
self.birthday = birthday
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OsagoDto: Identifiable {
|
||||||
|
|
||||||
|
public var id: TimeInterval { date }
|
||||||
|
}
|
||||||
22
AutoCatCore/Models/DTO/VehicleAdDto.swift
Normal file
22
AutoCatCore/Models/DTO/VehicleAdDto.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// VehicleAdDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehicleAdDto: Decodable, Sendable {
|
||||||
|
|
||||||
|
public var id: Int = 0
|
||||||
|
public var url: String?
|
||||||
|
public var price: String?
|
||||||
|
public var date: TimeInterval = Date().timeIntervalSince1970
|
||||||
|
public var mileage: String?
|
||||||
|
public var region: String?
|
||||||
|
public var city: String?
|
||||||
|
public var adDescription: String?
|
||||||
|
public var photos: [String]
|
||||||
|
}
|
||||||
23
AutoCatCore/Models/DTO/VehicleBrandDto.swift
Normal file
23
AutoCatCore/Models/DTO/VehicleBrandDto.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// VehicleBrandDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehicleBrandDto: Decodable, Sendable {
|
||||||
|
|
||||||
|
public var name: VehicleNameDto?
|
||||||
|
public var logo: String?
|
||||||
|
|
||||||
|
public init() { }
|
||||||
|
|
||||||
|
public init(name: VehicleNameDto?, logo: String?) {
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.logo = logo
|
||||||
|
}
|
||||||
|
}
|
||||||
170
AutoCatCore/Models/DTO/VehicleDto.swift
Normal file
170
AutoCatCore/Models/DTO/VehicleDto.swift
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
//
|
||||||
|
// VehicleDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehicleDto: Sendable {
|
||||||
|
|
||||||
|
public var brand: VehicleBrandDto?
|
||||||
|
public var model: VehicleModelDto?
|
||||||
|
public var color: String?
|
||||||
|
public var year: Int = 0
|
||||||
|
public var category: String?
|
||||||
|
public var engine: VehicleEngineDto?
|
||||||
|
public var number: String = ""
|
||||||
|
public var currentNumber: String?
|
||||||
|
public var vin1: String?
|
||||||
|
public var vin2: String?
|
||||||
|
public var sts: String?
|
||||||
|
public var pts: String?
|
||||||
|
public var isRightWheel: Bool?
|
||||||
|
public var isJapanese: Bool?
|
||||||
|
public var addedDate: TimeInterval = 0
|
||||||
|
public var updatedDate: TimeInterval = 0
|
||||||
|
public var addedBy: String = ""
|
||||||
|
public var photos: [VehiclePhotoDto] = []
|
||||||
|
public var ownershipPeriods: [VehicleOwnershipPeriodDto] = []
|
||||||
|
public var events: [VehicleEventDto] = []
|
||||||
|
public var osagoContracts: [OsagoDto] = []
|
||||||
|
public var ads: [VehicleAdDto] = []
|
||||||
|
public var notes: [VehicleNoteDto] = []
|
||||||
|
public var debugInfo: DebugInfoDto?
|
||||||
|
public var synchronized: Bool = true
|
||||||
|
|
||||||
|
public init() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VehicleDto: Identifiable {
|
||||||
|
|
||||||
|
public var id: String { number }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VehicleDto: Decodable {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case brand
|
||||||
|
case model
|
||||||
|
case color
|
||||||
|
case year
|
||||||
|
case category
|
||||||
|
case engine
|
||||||
|
case number
|
||||||
|
case currentNumber
|
||||||
|
case vin1
|
||||||
|
case vin2
|
||||||
|
case sts
|
||||||
|
case pts
|
||||||
|
case isRightWheel
|
||||||
|
case isJapanese
|
||||||
|
case addedDate
|
||||||
|
case updatedDate
|
||||||
|
case addedBy
|
||||||
|
case photos
|
||||||
|
case ownershipPeriods
|
||||||
|
case events
|
||||||
|
case osagoContracts
|
||||||
|
case ads
|
||||||
|
case notes
|
||||||
|
case debugInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.brand = try container.decodeIfPresent(VehicleBrandDto.self, forKey: .brand)
|
||||||
|
self.model = try container.decodeIfPresent(VehicleModelDto.self, forKey: .model)
|
||||||
|
self.color = try container.decodeIfPresent(String.self, forKey: .color)
|
||||||
|
self.year = try container.decodeIfPresent(Int.self, forKey: .year) ?? 0
|
||||||
|
self.category = try container.decodeIfPresent(String.self, forKey: .category)
|
||||||
|
self.number = try container.decode(String.self, forKey: .number)
|
||||||
|
self.engine = try container.decodeIfPresent(VehicleEngineDto.self, forKey: .engine)
|
||||||
|
self.currentNumber = try container.decodeIfPresent(String.self, forKey: .currentNumber)
|
||||||
|
self.vin1 = try container.decodeIfPresent(String.self, forKey: .vin1)
|
||||||
|
self.vin2 = try container.decodeIfPresent(String.self, forKey: .vin2)
|
||||||
|
self.sts = try container.decodeIfPresent(String.self, forKey: .sts)
|
||||||
|
self.pts = try container.decodeIfPresent(String.self, forKey: .pts)
|
||||||
|
self.isRightWheel = try container.decodeIfPresent(Bool.self, forKey: .isRightWheel)
|
||||||
|
self.isJapanese = try container.decodeIfPresent(Bool.self, forKey: .isJapanese)
|
||||||
|
self.addedDate = (try container.decode(TimeInterval.self, forKey: .addedDate))/1000
|
||||||
|
self.addedBy = try container.decode(String.self, forKey: .addedBy)
|
||||||
|
self.debugInfo = try container.decodeIfPresent(DebugInfoDto.self, forKey: .debugInfo)
|
||||||
|
self.updatedDate = (try container.decode(TimeInterval.self, forKey: .updatedDate))/1000
|
||||||
|
|
||||||
|
self.photos = try container.decodeIfPresent([VehiclePhotoDto].self, forKey: .photos) ?? []
|
||||||
|
self.ownershipPeriods = try container.decodeIfPresent([VehicleOwnershipPeriodDto].self, forKey: .ownershipPeriods) ?? []
|
||||||
|
self.events = try container.decodeIfPresent([VehicleEventDto].self, forKey: .events) ?? []
|
||||||
|
self.osagoContracts = try container.decodeIfPresent([OsagoDto].self, forKey: .osagoContracts) ?? []
|
||||||
|
self.ads = try container.decodeIfPresent([VehicleAdDto].self, forKey: .ads) ?? []
|
||||||
|
self.notes = try container.decodeIfPresent([VehicleNoteDto].self, forKey: .notes) ?? []
|
||||||
|
|
||||||
|
// All vehicles received from API are synchronized by definition
|
||||||
|
self.synchronized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VehicleDto: Exportable {
|
||||||
|
|
||||||
|
public static var csvHeader: String {
|
||||||
|
return "Plate Number, Model, Color, Year, Power (HP), Events, Owners, VIN, STS, PTS, Engine number, Added Date, Updated date, Locations, Notes"
|
||||||
|
}
|
||||||
|
|
||||||
|
public var csvLine: String {
|
||||||
|
let model = self.brand?.name?.original ?? "<unknown>"
|
||||||
|
let added = Formatters.standard.string(from: Date(timeIntervalSince1970: self.addedDate))
|
||||||
|
let updated = Formatters.standard.string(from: Date(timeIntervalSince1970: self.updatedDate))
|
||||||
|
|
||||||
|
var eventsString = ""
|
||||||
|
for event in events {
|
||||||
|
let location = event.address ?? "lat: \(event.latitude), lon: \(event.longitude)"
|
||||||
|
let date = Formatters.standard.string(from: Date(timeIntervalSince1970: event.date))
|
||||||
|
eventsString.append(location + "; " + date + "\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
let notesString = self.notes.reduce("") { partialResult, note in
|
||||||
|
partialResult + note.text + "\r\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
let number = self.currentNumber == nil ? self.number : "\(self.number) (\(self.currentNumber ?? ""))"
|
||||||
|
|
||||||
|
return String(format: "%@, \"%@\", %@, %d, %f, %d, %d, %@, %@, %@, %@, \"%@\", \"%@\", \"%@\", \"%@\"",
|
||||||
|
number,
|
||||||
|
model,
|
||||||
|
self.color ?? "",
|
||||||
|
self.year,
|
||||||
|
self.engine?.powerHp ?? 0.0,
|
||||||
|
self.events.count,
|
||||||
|
self.ownershipPeriods.count,
|
||||||
|
self.vin1 ?? "",
|
||||||
|
self.sts ?? "",
|
||||||
|
self.pts ?? "",
|
||||||
|
self.engine?.number ?? "",
|
||||||
|
added,
|
||||||
|
updated,
|
||||||
|
eventsString,
|
||||||
|
notesString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VehicleDto {
|
||||||
|
|
||||||
|
public func getNumber() -> String {
|
||||||
|
return self.number
|
||||||
|
}
|
||||||
|
|
||||||
|
public var unrecognized: Bool {
|
||||||
|
return self.brand == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public var outdated: Bool {
|
||||||
|
if let current = self.currentNumber {
|
||||||
|
return current != self.number
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
AutoCatCore/Models/DTO/VehicleEngineDto.swift
Normal file
18
AutoCatCore/Models/DTO/VehicleEngineDto.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// VehicleEngineDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehicleEngineDto: Decodable, Sendable {
|
||||||
|
|
||||||
|
public var number: String?
|
||||||
|
public var volume: Int? = 0
|
||||||
|
public var powerHp: Float? = 0
|
||||||
|
public var powerKw: Float? = 0
|
||||||
|
public var fuelType: String?
|
||||||
|
}
|
||||||
61
AutoCatCore/Models/DTO/VehicleEventDto.swift
Normal file
61
AutoCatCore/Models/DTO/VehicleEventDto.swift
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// VehicleEventDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehicleEventDto: Codable, Sendable {
|
||||||
|
|
||||||
|
public var id: String = UUID().uuidString
|
||||||
|
public var date: TimeInterval = Date().timeIntervalSince1970
|
||||||
|
public var latitude: Double = 0
|
||||||
|
public var longitude: Double = 0
|
||||||
|
public var address: String? = nil
|
||||||
|
public var addedBy: String? = nil
|
||||||
|
public var number: String?
|
||||||
|
|
||||||
|
public init(lat: Double, lon: Double) {
|
||||||
|
self.latitude = lat
|
||||||
|
self.longitude = lon
|
||||||
|
self.addedBy = Settings.shared.user.email
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getMapLink() -> URL? {
|
||||||
|
var urlString = "http://maps.apple.com/?sll=\(self.latitude),\(self.longitude)"
|
||||||
|
if let address = self.address?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
|
||||||
|
urlString = urlString + "&address=" + address
|
||||||
|
}
|
||||||
|
return URL(string: urlString)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getLocationString() -> String {
|
||||||
|
let coordinates = "Lat: \(self.latitude), Lon: \(self.longitude)"
|
||||||
|
if let addressString = self.address {
|
||||||
|
return "\(addressString) (\(coordinates)"
|
||||||
|
} else {
|
||||||
|
return coordinates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func findAddress() async throws {
|
||||||
|
guard address == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
return RxLocationManager
|
||||||
|
.getAddressForLocation(latitude: self.latitude, longitude: self.longitude)
|
||||||
|
.map { addr in
|
||||||
|
if let realm = self.realm {
|
||||||
|
try realm.write { self.address = addr }
|
||||||
|
} else {
|
||||||
|
self.address = addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
14
AutoCatCore/Models/DTO/VehicleModelDto.swift
Normal file
14
AutoCatCore/Models/DTO/VehicleModelDto.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// VehicleModelDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehicleModelDto: Decodable, Sendable {
|
||||||
|
|
||||||
|
let name: VehicleNameDto?
|
||||||
|
}
|
||||||
15
AutoCatCore/Models/DTO/VehicleNameDto.swift
Normal file
15
AutoCatCore/Models/DTO/VehicleNameDto.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// VehicleNameDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehicleNameDto: Decodable, Sendable {
|
||||||
|
|
||||||
|
public var original: String?
|
||||||
|
public var normalized: String?
|
||||||
|
}
|
||||||
22
AutoCatCore/Models/DTO/VehicleNoteDto.swift
Normal file
22
AutoCatCore/Models/DTO/VehicleNoteDto.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// VehicleNoteDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehicleNoteDto: Codable, Sendable, Identifiable {
|
||||||
|
|
||||||
|
public var id: String = UUID().uuidString
|
||||||
|
public var user: String = ""
|
||||||
|
public var date: TimeInterval = Date().timeIntervalSince1970
|
||||||
|
public var text: String = ""
|
||||||
|
|
||||||
|
public init(text: String) {
|
||||||
|
self.text = text
|
||||||
|
self.user = Settings.shared.user.email
|
||||||
|
}
|
||||||
|
}
|
||||||
54
AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift
Normal file
54
AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// VehicleOwnershipPeriodDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehicleOwnershipPeriodDto: Decodable, Sendable {
|
||||||
|
|
||||||
|
public var lastOperation: String = ""
|
||||||
|
public var ownerType: String = ""
|
||||||
|
public var from: Int64 = 0
|
||||||
|
public var to: Int64 = 0
|
||||||
|
public var region: String?
|
||||||
|
public var registrationRegion: String?
|
||||||
|
public var locality: String?
|
||||||
|
public var code: String?
|
||||||
|
public var street: String?
|
||||||
|
public var building: String?
|
||||||
|
public var inn: String?
|
||||||
|
|
||||||
|
public init(lastOperation: String,
|
||||||
|
ownerType: String,
|
||||||
|
from: Int64,
|
||||||
|
to: Int64,
|
||||||
|
region: String? = nil,
|
||||||
|
registrationRegion: String? = nil,
|
||||||
|
locality: String? = nil,
|
||||||
|
code: String? = nil,
|
||||||
|
street: String? = nil,
|
||||||
|
building: String? = nil,
|
||||||
|
inn: String? = nil) {
|
||||||
|
|
||||||
|
self.lastOperation = lastOperation
|
||||||
|
self.ownerType = ownerType
|
||||||
|
self.from = from
|
||||||
|
self.to = to
|
||||||
|
self.region = region
|
||||||
|
self.registrationRegion = registrationRegion
|
||||||
|
self.locality = locality
|
||||||
|
self.code = code
|
||||||
|
self.street = street
|
||||||
|
self.building = building
|
||||||
|
self.inn = inn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VehicleOwnershipPeriodDto: Identifiable {
|
||||||
|
|
||||||
|
public var id: Int64 { from }
|
||||||
|
}
|
||||||
27
AutoCatCore/Models/DTO/VehiclePhotoDto.swift
Normal file
27
AutoCatCore/Models/DTO/VehiclePhotoDto.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// VehiclePhotoDto.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 12.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct VehiclePhotoDto: Decodable, Sendable {
|
||||||
|
|
||||||
|
public var brand: String?
|
||||||
|
public var model: String?
|
||||||
|
public var date: TimeInterval = 0
|
||||||
|
public var url: String = ""
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.timeZone = TimeZone(identifier:"GMT")
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .none
|
||||||
|
let date = Date(timeIntervalSince1970: self.date/1000)
|
||||||
|
let dateStr = formatter.string(from: date)
|
||||||
|
return "\(self.brand ?? "") \(self.model ?? "") (\(dateStr))"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,14 +22,11 @@ public struct DateSection<T: Identifiable> {
|
|||||||
self.elements = items
|
self.elements = items
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(timestamp: Double, items: [T]) {
|
public init(timestamp: Double, items: [T], monthStart: DateInRegion, weekStart: DateInRegion) {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateStyle = .medium
|
formatter.dateStyle = .medium
|
||||||
formatter.timeStyle = .medium
|
formatter.timeStyle = .medium
|
||||||
|
|
||||||
let monthStart = DateCache.shared.monthStart
|
|
||||||
let weekStart = DateCache.shared.weekStart
|
|
||||||
|
|
||||||
let date = Date(timeIntervalSince1970: timestamp)
|
let date = Date(timeIntervalSince1970: timestamp)
|
||||||
let dateInRegion = DateInRegion(date, region: Region.current)
|
let dateInRegion = DateInRegion(date, region: Region.current)
|
||||||
if dateInRegion.isToday {
|
if dateInRegion.isToday {
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import RealmSwift
|
|
||||||
|
|
||||||
public enum DebugInfoStatus: Int {
|
|
||||||
case success = 0
|
|
||||||
case error = 1
|
|
||||||
case warning = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DebugInfo: Object, Decodable {
|
|
||||||
@Persisted public var autocod: DebugInfoEntry!
|
|
||||||
@Persisted public var vin01vin: DebugInfoEntry!
|
|
||||||
@Persisted public var vin01base: DebugInfoEntry!
|
|
||||||
@Persisted public var vin01history: DebugInfoEntry!
|
|
||||||
@Persisted public var nomerogram: DebugInfoEntry!
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DebugInfoEntry: Object, Decodable {
|
|
||||||
@Persisted public var fields: Int64 = 0
|
|
||||||
@Persisted public var error: String?
|
|
||||||
@Persisted public var status: Int = 0
|
|
||||||
|
|
||||||
public var statusEnum: DebugInfoStatus {
|
|
||||||
get { DebugInfoStatus(rawValue: self.status)! }
|
|
||||||
set { self.status = newValue.rawValue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum AddedBy: String, CustomStringConvertible, CaseIterable {
|
public enum AddedBy: String, CustomStringConvertible, CaseIterable, Sendable {
|
||||||
case anyone
|
case anyone
|
||||||
case me
|
case me
|
||||||
case anyoneButMe
|
case anyoneButMe
|
||||||
@ -14,7 +14,7 @@ public enum AddedBy: String, CustomStringConvertible, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SortParameter: String, CustomStringConvertible, CaseIterable {
|
public enum SortParameter: String, CustomStringConvertible, CaseIterable, Sendable {
|
||||||
case addedDate
|
case addedDate
|
||||||
case updatedDate
|
case updatedDate
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ public enum SortParameter: String, CustomStringConvertible, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SortOrder: String, CustomStringConvertible, CaseIterable {
|
public enum SortOrder: String, CustomStringConvertible, CaseIterable, Sendable {
|
||||||
case ascending
|
case ascending
|
||||||
case descending
|
case descending
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ public enum SortOrder: String, CustomStringConvertible, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SearchScope: Int, CaseIterable {
|
public enum SearchScope: Int, CaseIterable, Sendable {
|
||||||
|
|
||||||
case plateNumber = 0
|
case plateNumber = 0
|
||||||
case vin = 1
|
case vin = 1
|
||||||
@ -61,7 +61,7 @@ public enum SearchScope: Int, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Filter {
|
public struct Filter: Sendable {
|
||||||
public var searchString = ""
|
public var searchString = ""
|
||||||
public var brand: String?
|
public var brand: String?
|
||||||
public var model: String?
|
public var model: String?
|
||||||
|
|||||||
15
AutoCatCore/Models/Firebase/FbRefreshTokenModel.swift
Normal file
15
AutoCatCore/Models/Firebase/FbRefreshTokenModel.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// FbRefreshTokenModel.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 11.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct FbRefreshTokenModel: Decodable {
|
||||||
|
|
||||||
|
let id_token: String?
|
||||||
|
let refresh_token: String?
|
||||||
|
}
|
||||||
15
AutoCatCore/Models/Firebase/FbVerifyTokenModel.swift
Normal file
15
AutoCatCore/Models/Firebase/FbVerifyTokenModel.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// FbVerifyTokenModel.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 11.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct FbVerifyTokenModel: Decodable {
|
||||||
|
|
||||||
|
let idToken: String?
|
||||||
|
let refreshToken: String?
|
||||||
|
}
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import RealmSwift
|
|
||||||
|
|
||||||
public class Osago: Object, Decodable, Cloneable {
|
|
||||||
@Persisted public var date: TimeInterval = 0
|
|
||||||
@Persisted public var number: String = ""
|
|
||||||
@Persisted public var vin: String?
|
|
||||||
@Persisted public var plateNumber: String?
|
|
||||||
@Persisted public var name: String = ""
|
|
||||||
@Persisted public var status: String?
|
|
||||||
@Persisted public var restrictions: String = ""
|
|
||||||
@Persisted public var insurant: String?
|
|
||||||
@Persisted public var owner: String?
|
|
||||||
@Persisted public var usageRegion: String?
|
|
||||||
@Persisted public var birthday: String?
|
|
||||||
|
|
||||||
public required init(copy: Osago) {
|
|
||||||
self.date = copy.date
|
|
||||||
self.number = copy.number
|
|
||||||
self.vin = copy.vin
|
|
||||||
self.plateNumber = copy.plateNumber
|
|
||||||
self.name = copy.name
|
|
||||||
self.status = copy.status
|
|
||||||
self.restrictions = copy.restrictions
|
|
||||||
self.insurant = copy.insurant
|
|
||||||
self.owner = copy.owner
|
|
||||||
self.usageRegion = copy.usageRegion
|
|
||||||
}
|
|
||||||
|
|
||||||
required override init() {
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class PagedResponse<T>: Decodable where T: Decodable {
|
public final class PagedResponse<T>: Decodable, Sendable where T: Decodable & Sendable {
|
||||||
public let count: Int?
|
public let count: Int?
|
||||||
public let pageToken: String?
|
public let pageToken: String?
|
||||||
public let items: [T]
|
public let items: [T]
|
||||||
|
|||||||
33
AutoCatCore/Models/Protocols/DtoConvertible.swift
Normal file
33
AutoCatCore/Models/Protocols/DtoConvertible.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// DtoConvertible.swift
|
||||||
|
// AutoCatCore
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 13.06.2024.
|
||||||
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol DtoConvertible {
|
||||||
|
|
||||||
|
associatedtype Dto
|
||||||
|
|
||||||
|
var dto: Dto { get }
|
||||||
|
var shallowDto: Dto { get }
|
||||||
|
|
||||||
|
init(dto: Dto)
|
||||||
|
init?(dto: Dto?)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DtoConvertible {
|
||||||
|
|
||||||
|
public var shallowDto: Dto { dto }
|
||||||
|
|
||||||
|
public init?(dto: Dto?) {
|
||||||
|
guard let dto else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(dto: dto)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
|
|
||||||
public class AudioRecord: Object, Identifiable, Cloneable {
|
public final class AudioRecord: Object {
|
||||||
|
|
||||||
@Persisted public var path: String = ""
|
@Persisted public var path: String = ""
|
||||||
@Persisted public var number: String?
|
@Persisted public var number: String?
|
||||||
@ -10,21 +10,12 @@ public class AudioRecord: Object, Identifiable, Cloneable {
|
|||||||
@Persisted public var duration: TimeInterval = 0
|
@Persisted public var duration: TimeInterval = 0
|
||||||
@Persisted public var event: VehicleEvent?
|
@Persisted public var event: VehicleEvent?
|
||||||
|
|
||||||
public var identifier: TimeInterval = 0
|
|
||||||
public var id: TimeInterval {
|
|
||||||
if self.identifier == 0 {
|
|
||||||
self.identifier = self.addedDate
|
|
||||||
}
|
|
||||||
return self.identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
public var differenceIdentifier: TimeInterval { id }
|
|
||||||
|
|
||||||
public func isContentEqual(to source: AudioRecord) -> Bool {
|
public func isContentEqual(to source: AudioRecord) -> Bool {
|
||||||
return self == source
|
return self == source
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(path: String, number: String?, raw: String, duration: TimeInterval, event: VehicleEvent?) {
|
public convenience init(path: String, number: String?, raw: String, duration: TimeInterval, event: VehicleEvent?) {
|
||||||
|
self.init()
|
||||||
self.path = path
|
self.path = path
|
||||||
self.number = number
|
self.number = number
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
@ -33,10 +24,6 @@ public class AudioRecord: Object, Identifiable, Cloneable {
|
|||||||
self.addedDate = Date().timeIntervalSince1970
|
self.addedDate = Date().timeIntervalSince1970
|
||||||
}
|
}
|
||||||
|
|
||||||
required override init() {
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
public override static func primaryKey() -> String? {
|
public override static func primaryKey() -> String? {
|
||||||
return "path"
|
return "path"
|
||||||
}
|
}
|
||||||
@ -44,23 +31,29 @@ public class AudioRecord: Object, Identifiable, Cloneable {
|
|||||||
public override class func ignoredProperties() -> [String] {
|
public override class func ignoredProperties() -> [String] {
|
||||||
return ["id", "identifier", "differenceIdentifier"]
|
return ["id", "identifier", "differenceIdentifier"]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func getAddedDate() -> TimeInterval {
|
extension AudioRecord: DtoConvertible {
|
||||||
if self.identifier == 0 {
|
|
||||||
self.identifier = self.addedDate
|
public var dto: AudioRecordDto {
|
||||||
|
|
||||||
|
var record = AudioRecordDto(path: path,
|
||||||
|
number: number,
|
||||||
|
raw: rawText,
|
||||||
|
duration: duration,
|
||||||
|
event: event?.dto)
|
||||||
|
record.addedDate = addedDate
|
||||||
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.addedDate
|
public convenience init(dto: AudioRecordDto) {
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Cloneable
|
self.init(path: dto.path,
|
||||||
|
number: dto.number,
|
||||||
|
raw: dto.rawText,
|
||||||
|
duration: dto.duration,
|
||||||
|
event: VehicleEvent(dto: dto.event))
|
||||||
|
|
||||||
required public init(copy: AudioRecord) {
|
self.addedDate = dto.addedDate
|
||||||
self.path = copy.path
|
|
||||||
self.number = copy.number
|
|
||||||
self.rawText = copy.rawText
|
|
||||||
self.addedDate = copy.addedDate
|
|
||||||
self.duration = copy.duration
|
|
||||||
self.event = copy.event?.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
60
AutoCatCore/Models/Realm/DebugInfo.swift
Normal file
60
AutoCatCore/Models/Realm/DebugInfo.swift
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import Foundation
|
||||||
|
import RealmSwift
|
||||||
|
|
||||||
|
public final class DebugInfo: Object {
|
||||||
|
|
||||||
|
@Persisted public var autocod: DebugInfoEntry!
|
||||||
|
@Persisted public var vin01vin: DebugInfoEntry!
|
||||||
|
@Persisted public var vin01base: DebugInfoEntry!
|
||||||
|
@Persisted public var vin01history: DebugInfoEntry!
|
||||||
|
@Persisted public var nomerogram: DebugInfoEntry!
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DebugInfo: DtoConvertible {
|
||||||
|
|
||||||
|
public var dto: DebugInfoDto {
|
||||||
|
|
||||||
|
DebugInfoDto(autocod: autocod.dto,
|
||||||
|
vin01vin: vin01vin.dto,
|
||||||
|
vin01base: vin01base.dto,
|
||||||
|
vin01history: vin01history.dto,
|
||||||
|
nomerogram: nomerogram.dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init(dto: DebugInfoDto) {
|
||||||
|
|
||||||
|
self.init()
|
||||||
|
|
||||||
|
autocod = DebugInfoEntry(dto: dto.autocod)
|
||||||
|
vin01vin = DebugInfoEntry(dto: dto.vin01vin)
|
||||||
|
vin01base = DebugInfoEntry(dto: dto.vin01base)
|
||||||
|
vin01history = DebugInfoEntry(dto: dto.vin01history)
|
||||||
|
nomerogram = DebugInfoEntry(dto: dto.nomerogram)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class DebugInfoEntry: Object, Decodable {
|
||||||
|
|
||||||
|
@Persisted public var fields: Int64 = 0
|
||||||
|
@Persisted public var error: String?
|
||||||
|
@Persisted public var status: Int = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DebugInfoEntry: DtoConvertible {
|
||||||
|
|
||||||
|
public var dto: DebugInfoEntryDto {
|
||||||
|
|
||||||
|
DebugInfoEntryDto(fields: fields,
|
||||||
|
error: error,
|
||||||
|
status: DebugInfoStatus(rawValue: status) ?? .success)
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init(dto: DebugInfoEntryDto) {
|
||||||
|
|
||||||
|
self.init()
|
||||||
|
|
||||||
|
fields = dto.fields
|
||||||
|
error = dto.error
|
||||||
|
status = dto.status.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
51
AutoCatCore/Models/Realm/Osago.swift
Normal file
51
AutoCatCore/Models/Realm/Osago.swift
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import Foundation
|
||||||
|
import RealmSwift
|
||||||
|
|
||||||
|
public final class Osago: Object {
|
||||||
|
|
||||||
|
@Persisted public var date: TimeInterval = 0
|
||||||
|
@Persisted public var number: String = ""
|
||||||
|
@Persisted public var vin: String?
|
||||||
|
@Persisted public var plateNumber: String?
|
||||||
|
@Persisted public var name: String = ""
|
||||||
|
@Persisted public var status: String?
|
||||||
|
@Persisted public var restrictions: String = ""
|
||||||
|
@Persisted public var insurant: String?
|
||||||
|
@Persisted public var owner: String?
|
||||||
|
@Persisted public var usageRegion: String?
|
||||||
|
@Persisted public var birthday: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Osago: DtoConvertible {
|
||||||
|
|
||||||
|
public var dto: OsagoDto {
|
||||||
|
|
||||||
|
OsagoDto(date: date,
|
||||||
|
number: number,
|
||||||
|
vin: vin,
|
||||||
|
plateNumber: plateNumber,
|
||||||
|
name: name,
|
||||||
|
status: status,
|
||||||
|
restrictions: restrictions,
|
||||||
|
insurant: insurant,
|
||||||
|
owner: owner,
|
||||||
|
usageRegion: usageRegion,
|
||||||
|
birthday: birthday)
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init(dto: OsagoDto) {
|
||||||
|
|
||||||
|
self.init()
|
||||||
|
|
||||||
|
self.date = dto.date
|
||||||
|
self.number = dto.number
|
||||||
|
self.vin = dto.vin
|
||||||
|
self.plateNumber = dto.plateNumber
|
||||||
|
self.name = dto.name
|
||||||
|
self.status = dto.status
|
||||||
|
self.restrictions = dto.restrictions
|
||||||
|
self.insurant = dto.insurant
|
||||||
|
self.owner = dto.owner
|
||||||
|
self.usageRegion = dto.usageRegion
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user