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:
Selim Mustafaev 2024-07-14 17:15:38 +03:00
parent 443d924091
commit 01bd7b2f87
139 changed files with 4654 additions and 3586 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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
} }

View File

@ -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">

View File

@ -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>

View File

@ -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? {

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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"/>

View File

@ -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) {

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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))
} }

View File

@ -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)
@ -109,3 +111,4 @@ class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
} }
} }
} }
}

View File

@ -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
}
}
} }

View File

@ -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()
} }
} }
} }

View File

@ -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

View File

@ -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)
} }
} }
} }

View File

@ -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()

View File

@ -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) {

View File

@ -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

View File

@ -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,7 +108,7 @@ 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)
} }
@ -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) {

View File

@ -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
} }
} }
*/

View File

@ -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()

View File

@ -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() }
} }
} }

View File

@ -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)
}
}

View File

@ -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) {
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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,14 +91,14 @@ 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)
await makeStartSoundIfNeeded()
let recordObservable: Single<String> = recorder.requestPermissions()
.observe(on: MainScheduler.instance)
.flatMap(self.makeStartSoundIfNeeded)
.flatMap {
#if targetEnvironment(macCatalyst) || targetEnvironment(simulator) #if targetEnvironment(macCatalyst) || targetEnvironment(simulator)
DispatchQueue.main.async { DispatchQueue.main.async {
alert = self.showRecordingAlert() alert = self.showRecordingAlert()
@ -125,22 +122,23 @@ class RecordsController: UIViewController, UITableViewDelegate {
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

View File

@ -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) {

View File

@ -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) {

View File

@ -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)
} }
} }
} }

View File

@ -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()
}
}
}
}

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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;
}
}
})

View 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
}
}

View 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 { }
}

View File

@ -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)
} }
} }

View 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))
}
}

View 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)
}
}

View 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)
}

View 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)
}
}

View 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)
}
}

View 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)
}

View 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)
}
}

View 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)
}

View 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
}
}
}

View 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)
}
}
}

View 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)
}
}

View 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))
}
}

View 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()
}

View 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()
}

View File

@ -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 {
/** /**

View File

@ -23,6 +23,7 @@
import UIKit import UIKit
@MainActor
internal class DismissAnimationController: NSObject { internal class DismissAnimationController: NSObject {
private enum Constants { private enum Constants {

View File

@ -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) {

View File

@ -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

View File

@ -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)])
}
}
}
}
}

View 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
}
}

View File

@ -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
} }
} }
} }

View 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
}
}

View 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
}()
}

View File

@ -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()
} }
} }
} }

View File

@ -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)
} }

View File

@ -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 {

View File

@ -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)

View File

@ -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)
} }

View File

@ -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)

View File

@ -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>()
}
}

View File

@ -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>()
}
}

View File

@ -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)
// }
} }
} }
} }

View File

@ -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)
} }
} }
} }

View File

@ -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()
}
}

View 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 }
}

View 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
}

View 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 }
}

View 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]
}

View 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
}
}

View 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
}
}
}

View 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?
}

View 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
}
}
*/
}
}

View 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?
}

View 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?
}

View 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
}
}

View 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 }
}

View 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))"
}
}

View File

@ -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 {

View File

@ -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 }
}
}

View File

@ -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?

View 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?
}

View 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?
}

View File

@ -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()
}
}

View File

@ -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]

View 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)
}
}

View File

@ -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 {
if self.identifier == 0 {
self.identifier = self.addedDate
} }
return self.addedDate extension AudioRecord: DtoConvertible {
public var dto: AudioRecordDto {
var record = AudioRecordDto(path: path,
number: number,
raw: rawText,
duration: duration,
event: event?.dto)
record.addedDate = addedDate
return record
} }
// MARK: - Cloneable public convenience init(dto: AudioRecordDto) {
required public init(copy: AudioRecord) { self.init(path: dto.path,
self.path = copy.path number: dto.number,
self.number = copy.number raw: dto.rawText,
self.rawText = copy.rawText duration: dto.duration,
self.addedDate = copy.addedDate event: VehicleEvent(dto: dto.event))
self.duration = copy.duration
self.event = copy.event?.clone() self.addedDate = dto.addedDate
} }
} }

View 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
}
}

View 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