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" : [
{
"identity" : "eureka",
"kind" : "remoteSourceControl",
"location" : "https://github.com/xmartlabs/Eureka",
"state" : {
"revision" : "b6e35acf77a5551070afa6248935ec68e71f22af",
"version" : "5.4.0"
"revision" : "028ef8e3191a256b8f6b8bb6b9496efcb0762dbc",
"version" : "5.5.0"
}
},
{
@ -23,8 +24,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher",
"state" : {
"revision" : "1a0c2df04b31ed7aa318354f3583faea24f006fc",
"version" : "5.15.8"
"branch" : "8.0.0-alpha.1",
"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",
"location" : "https://github.com/realm/realm-core.git",
"state" : {
"revision" : "dd91f5f967c4ae89c37e24ab2a0315c31106648f",
"version" : "13.6.0"
"revision" : "f3d7ae5f9f31d90b327a64536bb7801cc69fd85b",
"version" : "14.9.0"
}
},
{
@ -50,17 +60,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/realm/realm-swift.git",
"state" : {
"revision" : "8ac6fe1aa5d0fb0100062d80863416a4d70de8ca",
"version" : "10.37.0"
"revision" : "4c4413abd0cd2221f59318f800960fe5bddc1494",
"version" : "10.51.0"
}
},
{
"identity" : "rxswift",
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ReactiveX/RxSwift.git",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8",
"version" : "6.5.0"
"revision" : "303e5c5c36d6a558407d364878df131c3546fad8",
"version" : "510.0.2"
}
},
{
@ -80,7 +90,16 @@
"revision" : "5ad36cccf0c4b9fea32f4e9b17a8e38f07563ef0",
"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">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7AF6D1DC2677A7E00086EA64"
BlueprintIdentifier = "7AB587212C42D27F00FA7B66"
BuildableName = "AutoCatTests.xctest"
BlueprintName = "AutoCatTests"
ReferencedContainer = "container:AutoCat.xcodeproj">
@ -42,7 +42,7 @@
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7AF6D1F62677C03B0086EA64"
BlueprintIdentifier = "7A2E6FA22C42B3AD00C40DA7"
BuildableName = "AutoCatCoreTests.xctest"
BlueprintName = "AutoCatCoreTests"
ReferencedContainer = "container:AutoCat.xcodeproj">

View File

@ -20,16 +20,16 @@
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "2786565A-9610-4232-920E-0763816C4DBF"
uuid = "E881DB7D-2F45-4EF0-A237-92D7E40239E7"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "../../../Library/Developer/Xcode/DerivedData/AutoCat-fhilwnlnsrpirleiajogdcyhyyey/SourcePackages/checkouts/Eureka/Source/Rows/DateInlineRow.swift"
filePath = "AutoCatCore/Services/ApiService/ApiServiceTestMock.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "37"
endingLineNumber = "37"
landmarkName = "configurePickerStyle(_:_:)"
startingLineNumber = "77"
endingLineNumber = "77"
landmarkName = "add(notes:to:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>

View File

@ -13,7 +13,7 @@ extension UIGestureRecognizer {
typealias Action = ((UIGestureRecognizer) -> ())
private struct Keys {
static var actionKey = "ActionKey"
@MainActor static var actionKey = "ActionKey"
}
private var block: Action? {

View File

@ -22,7 +22,7 @@ extension UISegmentedControl {
return view
}
func onValueChanged(_ closure: @escaping (Int) -> Void) -> UISegmentedControl {
func onValueChanged(_ closure: @MainActor @escaping (Int) -> Void) -> UISegmentedControl {
addActionImpl(for: .valueChanged) { [weak self] in
guard let index = self?.selectedSegmentIndex else {
return

View File

@ -9,7 +9,7 @@ class ACButton: UIButton {
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.style(style)
self.onTap(onTap)

View File

@ -1,24 +1,17 @@
import UIKit
import RealmSwift
import RxSwift
import RxCocoa
import os.log
import PKHUD
import AutoCatCore
extension OSLog {
static let startup = OSLog(subsystem: "pro.aliencat.autocat.startup", category: "startup")
}
enum QuickAction {
case none
case check
case checkNumber(String, VehicleEvent?)
case checkNumber(String, VehicleEventDto?)
case addVoiceRecord
case openReport(String)
}
@UIApplicationMain
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var quickAction: QuickAction = .none

View File

@ -1,9 +1,9 @@
<?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"/>
<dependencies>
<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="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -42,21 +42,6 @@
</objects>
<point key="canvasLocation" x="1095" y="965"/>
</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-->
<scene sceneID="hrL-nC-qbc">
<objects>
@ -72,94 +57,6 @@
</objects>
<point key="canvasLocation" x="1094" y="2353"/>
</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-->
<scene sceneID="pPZ-gs-kHF">
<objects>
@ -169,21 +66,21 @@
<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="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"/>
<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">
<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"/>
<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"/>
<subviews>
<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>
<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>
<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"/>
@ -192,7 +89,7 @@
<nil key="highlightedColor"/>
</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">
<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"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
@ -200,7 +97,7 @@
</subviews>
</stackView>
<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>
</subviews>
</stackView>
@ -274,7 +171,7 @@
<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="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"/>
<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">
@ -297,7 +194,7 @@
<nil key="highlightedColor"/>
</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">
<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"/>
<constraints>
<constraint firstAttribute="width" constant="20" id="Pct-H5-e3e"/>
@ -598,7 +495,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<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">
<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"/>
<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">
@ -621,7 +518,7 @@
<nil key="highlightedColor"/>
</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">
<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"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="9HI-9d-T5A"/>
@ -734,7 +631,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<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"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
@ -742,7 +639,7 @@
</wkWebViewConfiguration>
</wkWebView>
<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>
<navigationItem id="fZb-kM-9an">
<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"/>
</connections>
</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>
</stackView>
</subviews>
@ -890,7 +774,6 @@
</constraints>
</view>
<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="password" destination="G1p-Hz-8yn" id="8VI-cA-YrJ"/>
<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"/>
<toolbarItems/>
<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"/>
</navigationBar>
<nil name="viewControllers"/>
@ -994,7 +877,7 @@
<tabBarItem key="tabBarItem" title="Search" image="search" landscapeImage="search-compact" id="gDG-z8-R0t"/>
<toolbarItems/>
<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"/>
</navigationBar>
<nil name="viewControllers"/>
@ -1012,7 +895,7 @@
<navigationController storyboardIdentifier="ReportNavController" automaticallyAdjustsScrollViewInsets="NO" id="Km4-b6-SGW" sceneMemberID="viewController">
<toolbarItems/>
<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"/>
</navigationBar>
<nil name="viewControllers"/>
@ -1031,7 +914,7 @@
<tabBarItem key="tabBarItem" title="Records" image="record" landscapeImage="record-compact" id="lxF-EY-z8V"/>
<toolbarItems/>
<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"/>
</navigationBar>
<nil name="viewControllers"/>
@ -1049,7 +932,7 @@
<navigationController storyboardIdentifier="GlobalEventsNavigation" automaticallyAdjustsScrollViewInsets="NO" id="HWa-Ea-ZKD" sceneMemberID="viewController">
<toolbarItems/>
<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"/>
</navigationBar>
<nil name="viewControllers"/>
@ -1063,13 +946,13 @@
</scene>
</scenes>
<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="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="map" catalog="system" width="128" height="112"/>
<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="record" width="31" height="31"/>
<image name="record-compact" width="23" height="23"/>
@ -1077,7 +960,7 @@
<image name="search-compact" width="17" height="17"/>
<image name="settings" width="25" height="25"/>
<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"/>
<systemColor name="secondaryLabelColor">
<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 RxSwift
import PKHUD
import AutoCatCore
@ -13,10 +12,8 @@ class AudioRecordCell: UITableViewCell, ConfigurableCell {
let dateFormatter = DateFormatter()
let componentsFormatter = DateComponentsFormatter()
var stateDisposable: Disposable?
var progressDisposable: Disposable?
var record: AudioRecord?
var record: AudioRecordDto?
override func awakeFromNib() {
super.awakeFromNib()
@ -34,39 +31,38 @@ class AudioRecordCell: UITableViewCell, ConfigurableCell {
override func prepareForReuse() {
super.prepareForReuse()
self.record = nil
self.stateDisposable?.dispose()
self.progressDisposable?.dispose()
self.progressView.progress = 0
}
func configure(with record: AudioRecord) {
func configure(with record: AudioRecordDto) {
self.record = record
self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: record.getAddedDate()))
self.number.text = record.number ?? "Unrecognized"
self.duration.text = self.componentsFormatter.string(from: record.duration)
self.stateDisposable = AudioPlayer.shared
.stateObservable()
.filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
.subscribe(onNext: { state in
let imgName = state == .playing ? "pause.fill" : "play.fill"
self.playButton.setImage(UIImage(systemName: imgName), for: .normal)
if state == .stopped {
self.progressView.progress = 0
}
}, onDisposed: {
self.playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
})
self.progressDisposable = AudioPlayer.shared
.progressObservable()
.filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
.subscribe(onNext: { progress in
self.progressView.progress = progress
}, onDisposed: {
self.progressView.progress = 0
})
// TODO: Fix player
// AudioPlayer.shared
// .stateObservable()
// .filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
// .subscribe(onNext: { state in
// let imgName = state == .playing ? "pause.fill" : "play.fill"
// self.playButton.setImage(UIImage(systemName: imgName), for: .normal)
//
// if state == .stopped {
// self.progressView.progress = 0
// }
// }, onDisposed: {
// self.playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
// })
//
// self.progressDisposable = AudioPlayer.shared
// .progressObservable()
// .filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
// .subscribe(onNext: { progress in
// self.progressView.progress = progress
// }, onDisposed: {
// self.progressView.progress = 0
// })
}
@IBAction func onPlay(_ sender: UIButton) {

View File

@ -1,5 +1,6 @@
import UIKit
@MainActor
protocol ConfigurableCell {
associatedtype Item
func configure(with item: Item)

View File

@ -15,7 +15,7 @@ class EventCell: UITableViewCell {
self.dateFormatter.timeStyle = .short
}
func configure(with event: VehicleEvent) {
func configure(with event: VehicleEventDto) {
if let addressString = event.address {
self.address.text = addressString
} else {

View File

@ -20,7 +20,7 @@ class VehicleCell: UITableViewCell, ConfigurableCell {
formatter.timeStyle = .short
}
func configure(with vehicle: Vehicle) {
func configure(with vehicle: VehicleDto) {
self.name.text = vehicle.brand?.name?.original ?? "<unknown>"
self.plate.number = PlateNumber(vehicle.getNumber())
self.plate.fontSize = 40

View File

@ -14,7 +14,7 @@ class VehicleNoteCell: UITableViewCell {
self.dateFormatter.timeStyle = .medium
}
func configure(with note: VehicleNote) {
func configure(with note: VehicleNoteDto) {
self.noteText.text = note.text
self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: note.date))
}

View File

@ -6,8 +6,8 @@ import AutoCatCore
class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
var ads: [VehicleAd] = []
private var currentAd: VehicleAd?
var ads: [VehicleAdDto] = []
private var currentAd: VehicleAdDto?
override func viewDidLoad() {
super.viewDidLoad()
@ -52,27 +52,27 @@ class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
}
}
if let description = ad.adDescription, !description.isEmpty {
section <<< MultilineLabelRow() { row in
row.title = NSLocalizedString("Description", comment: "")
row.value = description
}
}
if let urlStr = ad.url, let url = URL(string: urlStr) {
section <<< MultilineLinkRow() { row in
row.title = NSLocalizedString("Link", comment: "")
row.value = urlStr
}
.onCellSelection { _, _ in
let safari = SFSafariViewController(url: url)
self.present(safari, animated: true)
}
}
// if let description = ad.adDescription, !description.isEmpty {
// section <<< MultilineLabelRow() { row in
// row.title = NSLocalizedString("Description", comment: "")
// row.value = description
// }
// }
//
// if let urlStr = ad.url, let url = URL(string: urlStr) {
// section <<< MultilineLinkRow() { row in
// row.title = NSLocalizedString("Link", comment: "")
// row.value = urlStr
// }
// .onCellSelection { _, _ in
// let safari = SFSafariViewController(url: url)
// self.present(safari, animated: true)
// }
// }
if !ad.photos.isEmpty {
section <<< ImageGridRow() { row in
row.value = ad.photos.toArray()
row.value = ad.photos
}
.onDidSelected { index in
self.currentAd = ad
@ -98,6 +98,8 @@ class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
}
KingfisherManager.shared.retrieveImage(with: url) { result in
Task { @MainActor in
switch result {
case .success(let res):
completion(index, res.image, ZoomScale.default, nil)
@ -109,3 +111,4 @@ class AdsController: FormViewController, MediaBrowserViewControllerDataSource {
}
}
}
}

View File

@ -1,33 +1,27 @@
import UIKit
import RxSwift
import RxCocoa
import RealmSwift
import AuthenticationServices
import PKHUD
import AutoCatCore
class AuthController: UIViewController, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
class AuthController: UIViewController {
@IBOutlet weak var username: UITextField!
@IBOutlet weak var password: UITextField!
@IBOutlet weak var login: UIButton!
@IBOutlet weak var signup: UIButton!
@IBOutlet weak var appleSignIn: ASAuthorizationAppleIDButton!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.appleSignIn.cornerRadius = 6
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 }
return name.count >= 4 && pass.count >= 5
}
authValid.bind(to: self.login.rx.isEnabled).disposed(by: self.bag)
authValid.bind(to: self.signup.rx.isEnabled).disposed(by: self.bag)
// FIX login/password lengt checking
// 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 }
// return name.count >= 4 && pass.count >= 5
// }
//
// authValid.bind(to: self.login.rx.isEnabled).disposed(by: self.bag)
// authValid.bind(to: self.signup.rx.isEnabled).disposed(by: self.bag)
if Settings.shared.user.email.count > 0 {
self.username.text = Settings.shared.user.email
@ -37,32 +31,29 @@ class AuthController: UIViewController, ASAuthorizationControllerDelegate, ASAut
@IBAction func loginTapped(_ sender: UIButton) {
guard let email = self.username.text, let pass = self.password.text else { return }
Task {
do {
HUD.show(.progress)
Api.login(email: email, password: pass)
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: self.goToMainScreen(user:), onError: HUD.show(error:))
.disposed(by: self.bag)
let user = try await ApiService.shared.login(email: email, password: pass)
self.goToMainScreen(user: user)
} catch {
HUD.show(error: error)
}
}
}
@IBAction func signupTapped(_ sender: UIButton) {
guard let email = self.username.text, let pass = self.password.text else { return }
Task {
do {
HUD.show(.progress)
Api.signUp(email: email, password: pass)
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: self.goToMainScreen(user:), onError: HUD.show(error:))
.disposed(by: self.bag)
let user = try await ApiService.shared.signUp(email: email, password: pass)
self.goToMainScreen(user: user)
} catch {
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) {
@ -83,38 +74,4 @@ class AuthController: UIViewController, ASAuthorizationControllerDelegate, ASAut
let storyboard = UIStoryboard(name: "Main", bundle: nil)
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 RealmSwift
import RxSwift
import SwiftDate
import PKHUD
import CoreLocation
import AutoCatCore
import SwiftLocation
enum EventAction: Equatable {
case doNotSend
@ -30,7 +30,6 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
@IBOutlet weak var history: UITableView!
private let bag = DisposeBag()
private var historyDataSource: RealmSectionedDataSource<Vehicle, VehicleCell>!
private var historyFilter: HistoryFilter = .all
@ -69,12 +68,12 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.handleQuickActions()
Task { await self.handleQuickActions() }
}
// MARK: -
func handleQuickActions() {
func handleQuickActions() async {
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
switch ad.quickAction {
@ -87,24 +86,23 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
case .checkNumber(let number, let event):
ad.quickAction = .none
var action: EventAction = .receiveAndSend
var events: [VehicleEvent] = []
var events: [VehicleEventDto] = []
if let event = event {
events = [event]
action = .doNotSend
}
do {
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 {
self.updateDetailController(with: vehicle)
}
HUD.hide()
self.showErrors(errors)
} onFailure: { error in
} catch {
HUD.hide()
self.show(error: error)
//HUD.show(error: error)
}
.disposed(by: self.bag)
break
case .addVoiceRecord:
self.tabBarController?.selectedIndex = 1
@ -112,7 +110,7 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
case .openReport(let number):
ad.quickAction = .none
if let sd = self.view.window?.windowScene?.delegate as? SceneDelegate {
sd.openReport(with: number)
Task { await sd.openReport(with: number) }
}
break
default:
@ -218,32 +216,36 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
func checkTapped(number: String) {
let numberNormalized = number.filter { !$0.isWhitespace }.uppercased()
var events: [VehicleEvent] = []
var events: [VehicleEventDto] = []
do {
let realm = try Realm()
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 {
print(error)
}
Task {
do {
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 {
self.updateDetailController(with: vehicle)
}
HUD.hide()
self.showErrors(errors)
} onFailure: { error in
} catch {
HUD.hide()
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
{
var detail: UINavigationController?
@ -330,22 +332,28 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
// MARK: - Contextual actions
func update(vehicle: Vehicle) {
func update(vehicle: VehicleDto) {
Task {
do {
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 {
self.updateDetailController(with: vehicle)
}
HUD.hide()
self.showErrors(errors)
} onFailure: { error in
} catch {
HUD.hide()
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 realmVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) else { return }
@ -360,49 +368,66 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
// MARK: - Checking number
func save(vehicle: Vehicle) throws {
func save(vehicle: VehicleDto) throws {
let realm = try Realm()
try realm.write {
realm.add(vehicle, update: .all)
realm.add(Vehicle(dto: vehicle), update: .all)
}
}
func getEvent(for action: EventAction) -> Single<VehicleEvent> {
if let event = RxLocationManager.lastEvent, (Date().timeIntervalSince1970 - event.date) < 100 {
return Single<VehicleEvent>.just(event)
func getEvent(for action: EventAction) async throws -> VehicleEventDto {
if let event = await RxLocationManager.getLastEvent(), (Date().timeIntervalSince1970 - event.date) < 100 {
return event
} 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])> {
var eventSingle: Single<(event: VehicleEvent?, error: Error?)> = .just((event: nil, error: nil))
if action != .doNotSend {
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)) }
func prepareEvent(for action: EventAction) async -> (event: VehicleEventDto?, error: Error?) {
guard action != .doNotSend else {
return (event: nil, error: nil)
}
let checkSingle = Api.checkVehicle(by: number, notes: notes, events: events, force: force)
.observe(on: MainScheduler.instance)
.map { (vehicle: Vehicle) -> (vehicle: Vehicle, error: Error?) in
do {
let event = try await getEvent(for: action)
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)
return (vehicle: vehicle, error: nil)
}
.catch { error in
let realm = try Realm()
if let existingVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: number) {
return .just((vehicle: existingVehicle, error: error))
} catch {
let realm = try? await Realm()
if let existingVehicle = realm?.object(ofType: Vehicle.self, forPrimaryKey: number) {
return (vehicle: existingVehicle.dto, error: error)
} else {
let vehicle = Vehicle(number)
try realm.write { realm.add(vehicle, update: .all) }
return .just((vehicle: vehicle, error: error))
try? realm?.write { realm?.add(vehicle, update: .all) }
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
if let clerror = error as? CLError {
if clerror.code != .denied {
@ -418,49 +443,47 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd
RxLocationManager.resetLastEvent()
let realm = try Realm()
let realm = try await Realm()
let dbVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicleResult.vehicle.getNumber())
if let event = eventResult.event, let vehicle = dbVehicle {
try realm.write {
vehicle.events.append(event)
vehicle.events.append(VehicleEvent(dto: event))
vehicle.updatedDate = Date().timeIntervalSince1970
vehicle.synchronized = false
}
}
if vehicleResult.error != nil {
return .just((vehicle: vehicleResult.vehicle, errors: errors))
return (vehicle: vehicleResult.vehicle, errors: errors)
} else {
if let event = eventResult.event {
return Api.add(event: event, to: vehicleResult.vehicle.getNumber())
.observe(on: MainScheduler.instance)
.map {
try self.save(vehicle: $0)
return (vehicle: $0, errors: errors)
}
.catch { error in
do {
let vehicle = try await ApiService.shared.add(event: event, to: vehicleResult.vehicle.getNumber())
try self.save(vehicle: vehicle)
return (vehicle: vehicle, errors: errors)
} catch {
errors.append(error)
return .just((vehicle: vehicleResult.vehicle, errors: errors))
return (vehicle: vehicleResult.vehicle, errors: errors)
}
} else {
return .just((vehicle: vehicleResult.vehicle, errors: errors))
}
return (vehicle: vehicleResult.vehicle, errors: errors)
}
}
}
func showErrors(_ errors: [Error]) {
let observables = errors.map(rxShowError)
Observable.from(observables).concat().subscribe().disposed(by: self.bag)
Task {
for error in errors {
await asyncShowError(error)
}
}
}
func rxShowError(_ error: Error) -> Observable<Void> {
return Observable<Void>.create { observer in
func asyncShowError(_ error: Error) async {
await withCheckedContinuation { continuation in
self.show(error: error, animated: true) {
observer.on(.next(()))
observer.on(.completed)
}
return Disposables.create()
continuation.resume()
}
}
}
}

View File

@ -1,6 +1,5 @@
import UIKit
import Eureka
import RxSwift
import AutoCatCore
class FiltersController: FormViewController {
@ -10,28 +9,46 @@ class FiltersController: FormViewController {
var onDone: (() -> Void)?
var regions: [VehicleRegion] = []
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
form +++ Section(NSLocalizedString("Main filters", comment: "")) { $0.tag = "MainFilters" }
<<< PushRow<String>("Brand") { row in
addMainSection()
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.value = self.filter.brand ?? "Any"
row.selectorTitle = NSLocalizedString("Brands", comment: "")
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)
}, 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 }
.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.value = self.filter.model ?? "Any"
row.disabled = "$Brand == 'Any'"
@ -40,44 +57,55 @@ class FiltersController: FormViewController {
completion(["Any"])
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)
}, 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 }
.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.value = self.filter.color ?? "Any"
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)
}, 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 }
.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.value = self.filter.year ?? "Any"
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))
} onError: { error in
print("Get years error: \(error)")
}.disposed(by: self.bag)
}
})
}
.onChange { self.filter.year = $0.value == "Any" ? nil : $0.value }
.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" }
<<< LabelRow("RegionsRow") { row in
row.title = NSLocalizedString("Regions", comment: "")
@ -97,6 +125,9 @@ class FiltersController: FormViewController {
}
self.navigationController?.pushViewController(vc, animated: true)
}
}
func addAddedBySection() {
form +++ Section() { $0.tag = "AddedByMe" }
<<< ActionSheetRow<String>("AddedByMeRow") { row in
@ -115,6 +146,9 @@ class FiltersController: FormViewController {
.cellUpdate { cell, row in
row.value = self.filter.addedBy?.description ?? AddedBy.anyone.description
}
}
func addTimeSections() {
form +++ Section(NSLocalizedString("Update time", comment: ""))
<<< DateInlineRow("FromDateUpdated") { row in
@ -147,6 +181,9 @@ class FiltersController: FormViewController {
}
.onChange { self.filter.toDate = self.nullifyTime(of: $0.value) }
.cellUpdate(self.update(cell:row:))
}
func addLocationTimeSection() {
form +++ Section(NSLocalizedString("Location adding time", comment: ""))
<<< DateInlineRow("FromLocationDate") { row in
@ -163,6 +200,9 @@ class FiltersController: FormViewController {
}
.onChange { self.filter.toLocationDate = self.nullifyTime(of: $0.value) }
.cellUpdate(self.update(cell:row:))
}
func addSortSection() {
form +++ Section(NSLocalizedString("Sort", comment: "Header section. Noun."))
<<< PickerInlineRow<SortParameter>("SortBy") { row in
@ -179,6 +219,9 @@ class FiltersController: FormViewController {
}
.onChange { self.filter.sortOrder = $0.value }
.cellUpdate { $1.value = self.filter.sortOrder }
}
func addClearAllSection() {
form +++ Section()
<<< ButtonRow("ClearAll") { $0.title = NSLocalizedString("Clear all filters", comment: "") }.onCellSelection { cell, row in

View File

@ -1,7 +1,6 @@
import UIKit
import WebKit
import CommonCrypto
import RxSwift
import PKHUD
import AutoCatCore
@ -17,7 +16,6 @@ struct TokenResponse: Codable {
class GoogleSignInController: UIViewController, WKNavigationDelegate {
@IBOutlet weak var webView: WKWebView!
private var bag = DisposeBag()
private var codeVerifier: String = ""
public var completion: (() -> Void)?
@ -54,16 +52,16 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate {
if let queryItems = components.queryItems {
if let code = queryItems.first(where: { $0.name == "code" })?.value {
decisionHandler(.cancel)
self.getToken(code: code)
.flatMap { Api.fbVerifyAssertion(provider: "google.com", idToken: $0.id_token, accessToken: $0.access_token) }
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { _ in
Task { @MainActor in
do {
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)
}, onError: { error in
} catch {
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: "="))
}
func getToken(code: String) -> Single<TokenResponse> {
func getToken(code: String) async throws -> TokenResponse {
let tokenUrlString = Constants.googleTokenURL
+ "?grant_type=authorization_code"
+ "&code=" + code
@ -98,12 +96,10 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate {
if let url = URL(string: tokenUrlString) {
var request = URLRequest(url: url)
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)
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Bad URL"])
return Single.error(error)
throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Bad URL"])
}
}
}

View File

@ -1,7 +1,5 @@
import UIKit
import MapKit
import RxSwift
import Realm
import RealmSwift
import PKHUD
import MobileCoreServices
@ -21,7 +19,7 @@ class EventPin: NSObject, MKAnnotation {
self.id = id
}
convenience init(event: VehicleEvent) {
convenience init(event: VehicleEventDto) {
let coordinate = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude)
let address = event.address ?? "\(event.latitude), \(event.longitude)"
let date = Date(timeIntervalSince1970: event.date)
@ -48,13 +46,12 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
@IBOutlet weak var map: MKMapView!
@IBOutlet weak var tableView: UITableView!
let bag = DisposeBag()
var modeButton: UIBarButtonItem!
var addButton: UIBarButtonItem!
var pasteButton: UIBarButtonItem!
var mode: EventsMode = .map
public var vehicle: Vehicle? {
public var vehicle: VehicleDto? {
didSet {
if self.isViewLoaded {
self.updateInterface()
@ -241,41 +238,45 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
// MARK: - Event actions
func deleteEvent(event: VehicleEvent, completion: ((Bool) -> Void)? = nil) {
func deleteEvent(event: VehicleEventDto, completion: ((Bool) -> Void)? = nil) {
Task {
do {
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)
completion?(result)
}, onFailure: { error in
} catch {
completion?(false)
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 controller = sb.instantiateViewController(identifier: "LocationEditController") as LocationEditController
controller.title = NSLocalizedString("Edit event", comment: "")
controller.date = Date(timeIntervalSince1970: event.date)
controller.placemark = Placemark(latitude: event.latitude, longitude: event.longitude, address: event.address)
controller.onDone = { newEvent in
newEvent.id = event.id
var updatedEvent = newEvent
updatedEvent.id = event.id
self.navigationController?.popViewController(animated: true, completion: {
Task {
do {
HUD.show(.progress)
Api.edit(event: newEvent)
.observe(on: MainScheduler.instance)
.subscribe(onSuccess: { self.update(vehicle: $0) }, onFailure:
{ error in
let vehicle = try await ApiService.shared.edit(event: updatedEvent)
self.update(vehicle: vehicle)
} catch {
HUD.show(error: error)
})
.disposed(by: self.bag)
}
}
})
}
self.navigationController?.pushViewController(controller, animated: true)
}
func copyEvent(event: VehicleEvent) {
func copyEvent(event: VehicleEventDto) {
var items: [String: Any] = [:]
if let url = event.getMapLink() {
@ -295,7 +296,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
self.setupBarButtonItems()
}
func shareEvent(event: VehicleEvent) {
func shareEvent(event: VehicleEventDto) {
guard let url = event.getMapLink() else {
return
}
@ -304,7 +305,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
self.present(controller, animated: true)
}
func openInAppleMaps(event: VehicleEvent) {
func openInAppleMaps(event: VehicleEventDto) {
let coordinates = CLLocationCoordinate2D(latitude: event.latitude,
longitude: event.longitude)
let placemark = MKPlacemark(coordinate: coordinates)
@ -312,7 +313,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
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 {
return
}
@ -331,14 +332,15 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
controller.title = NSLocalizedString("Add new event", comment: "")
controller.onDone = { newEvent in
self.navigationController?.popViewController(animated: true, completion: {
Task {
do {
HUD.show(.progress)
Api.add(event: newEvent, to: vehicle.getNumber())
.observe(on: MainScheduler.instance)
.subscribe(onSuccess: { self.update(vehicle: $0) }, onFailure:
{ error in
let vehicle = try await ApiService.shared.add(event: newEvent, to: vehicle.getNumber())
self.update(vehicle: vehicle)
} catch {
HUD.show(error: error)
})
.disposed(by: self.bag)
}
}
})
}
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 }
do {
let event = try JSONDecoder().decode(VehicleEvent.self, from: data)
let event = try JSONDecoder().decode(VehicleEventDto.self, from: data)
let formatter = DateFormatter()
formatter.dateStyle = .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)
alert.addAction(UIAlertAction(title: NSLocalizedString("Paste", comment: "from clipboard"), style: .default, handler: { action in
Task {
do {
HUD.show(.progress)
event.id = UUID().uuidString
Api.add(event: event, to: vehicle.getNumber())
.observe(on: MainScheduler.instance)
.subscribe(onSuccess: { self.update(vehicle: $0) }, onFailure:
{ error in
var newEvent = event
newEvent.id = UUID().uuidString
let vehicle = try await ApiService.shared.add(event: newEvent, to: vehicle.getNumber())
self.update(vehicle: vehicle)
} catch {
HUD.show(error: error)
})
.disposed(by: self.bag)
}
}
}))
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil))
self.present(alert, animated: true)
@ -383,17 +387,17 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele
}
@discardableResult
func update(vehicle: Vehicle) -> Bool {
func update(vehicle: VehicleDto) -> Bool {
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 realm.write {
realm.add(vehicle, update: .all)
realm.add(Vehicle(dto: vehicle), update: .all)
}
}
} else {
self.vehicle?.events.removeAll()
self.vehicle?.events.append(objectsIn: vehicle.events)
self.vehicle?.events = vehicle.events
}
self.updateInterface()
HUD.hide()

View File

@ -1,6 +1,5 @@
import UIKit
import MapKit
import RxSwift
import PKHUD
import AutoCatCore
@ -8,7 +7,6 @@ class GlobalEventsController: UIViewController {
@IBOutlet weak var map: MKMapView!
let bag = DisposeBag()
var filter: Filter!
override func viewDidLoad() {
@ -23,20 +21,22 @@ class GlobalEventsController: UIViewController {
#endif
Task { await loadEvents() }
}
func loadEvents() async {
do {
HUD.show(.progress)
Api.events(with: self.filter)
.observe(on: MainScheduler.init())
.subscribe(onSuccess: { events in
let events = try await ApiService.shared.events(with: self.filter)
self.title = String.localizedStringWithFormat(NSLocalizedString("events found", comment: ""), events.count)
let pins = events.map(EventPin.init(event:))
self.map.removeAnnotations(self.map.annotations)
self.map.addAnnotations(pins)
self.map.centerOnPins()
HUD.hide()
}, onFailure: { error in
} catch {
HUD.show(error: error)
})
.disposed(by: self.bag)
}
}
@IBAction func close(_ sender: UIBarButtonItem) {

View File

@ -1,18 +1,16 @@
import UIKit
import Eureka
import RxSwift
import CoreLocation
import AutoCatCore
class LocationEditController: FormViewController {
private let bag = DisposeBag()
private var doneButton: UIBarButtonItem!
var date = Date()
var placemark: Placemark? = nil
var onDone: ((VehicleEvent) -> Void)?
var onDone: ((VehicleEventDto) -> Void)?
override func 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.value = self.placemark
row.value = self.placemark?.address
}.onChange { row in
if let newPlacemark = row.value {
self.placemark = newPlacemark
//self.placemark = newPlacemark
self.doneButton.isEnabled = true
} else {
self.doneButton.isEnabled = false
@ -49,7 +48,7 @@ class LocationEditController: FormViewController {
@objc func doneTapped(_ sender: UIBarButtonItem) {
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
if let address = placemark.address {
event.address = address

View File

@ -1,7 +1,5 @@
import Foundation
import MapKit
import Eureka
import RxSwift
import Intents
import AutoCatCore
@ -11,14 +9,13 @@ public struct Placemark: Equatable {
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) -> ())?
private let bag = DisposeBag()
private var geocodingDisposable: Disposable?
private var address: String?
private var geocodingTask: Task<String?,Error>?
lazy var mapView : MKMapView = { [unowned self] in
let v = MKMapView(frame: self.view.bounds)
@ -89,7 +86,7 @@ public class LocationPickerController : UIViewController, TypedRowControllerType
button.title = "Done"
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)
mapView.setRegion(region, animated: true)
}
@ -111,7 +108,7 @@ public class LocationPickerController : UIViewController, TypedRowControllerType
@objc func tappedDone(_ sender: UIBarButtonItem){
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)
}
@ -124,14 +121,15 @@ public class LocationPickerController : UIViewController, TypedRowControllerType
title = "\(latitude), \(longitude)"
self.address = nil
self.geocodingDisposable?.dispose()
self.geocodingDisposable = RxLocationManager
.getAddressForLocation(latitude: mapView.centerCoordinate.latitude, longitude: mapView.centerCoordinate.longitude)
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { address in
self.title = address
self.address = address
})
geocodingTask?.cancel()
geocodingTask = Task {
address = try? await RxLocationManager.getAddressForLocation(latitude: mapView.centerCoordinate.latitude,
longitude: mapView.centerCoordinate.longitude)
title = address
geocodingTask = nil
return address
}
}
public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {

View File

@ -1,7 +1,10 @@
import UIKit
import Eureka
//import Eureka
import CoreLocation
// TODO: Rewrite Eureka forms to native UIKit/SwiftUI
/*
public final class LocationRow: OptionsRow<PushSelectorCell<Placemark>>, PresenterRowType, RowType {
public typealias PresenterRow = LocationPickerController
@ -55,3 +58,4 @@ public final class LocationRow: OptionsRow<PushSelectorCell<Placemark>>, Present
rowVC.row = self
}
}
*/

View File

@ -5,7 +5,7 @@ import AutoCatCore
class ShowEventController: UIViewController {
private var map = MKMapView()
var event: VehicleEvent?
var event: VehicleEventDto?
override func viewDidLoad() {
super.viewDidLoad()

View File

@ -1,12 +1,9 @@
import UIKit
import SwiftEntryKit
import AutoCatCore
import RxSwift
class MainTabController: UITabBarController, UITabBarControllerDelegate {
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
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
// 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 AVFoundation
import RealmSwift
import RxSwift
import Intents
import CoreSpotlight
import MobileCoreServices
@ -15,8 +14,6 @@ class RecordsController: UIViewController, UITableViewDelegate {
var recorder: Recorder?
var addButton: UIBarButtonItem!
let bag = DisposeBag()
var recordDisposable: Disposable?
var audioSessionObserver: NSObjectProtocol?
var recordsDataSource: RealmSectionedDataSource<AudioRecord, AudioRecordCell>!
@ -94,14 +91,14 @@ class RecordsController: UIViewController, UITableViewDelegate {
var alert: UIAlertController?
var url: URL!
let locationObservable = RxLocationManager.requestCurrentLocation()
.map(Optional.init)
.catchAndReturn(nil)
Task {
do {
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)
DispatchQueue.main.async {
alert = self.showRecordingAlert()
@ -125,22 +122,23 @@ class RecordsController: UIViewController, UITableViewDelegate {
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
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 duration = TimeInterval(CMTimeGetSeconds(asset.duration))
return AudioRecord(path: url.lastPathComponent, number: self.getPlateNumber(from: text), raw: text, duration: duration, event: event)
}
.subscribe(onSuccess: { record in
let realm = try? Realm()
try? realm?.write {
realm?.add(record)
let record = AudioRecordDto(path: url.lastPathComponent,
number: self.getPlateNumber(from: text),
raw: text,
duration: duration,
event: event)
let realm = try await Realm()
try realm.write {
realm.add(AudioRecord(dto: record))
}
alert?.dismiss(animated: true)
self.addButton.isEnabled = true
}, onFailure: { error in
} catch {
if let alert = alert {
alert.dismiss(animated: true) {
HUD.show(error: error)
@ -149,12 +147,13 @@ class RecordsController: UIViewController, UITableViewDelegate {
HUD.show(error: error)
}
self.addButton.isEnabled = true
})
}
}
}
func showRecordingAlert() -> UIAlertController {
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() }))
self.present(alert, animated: true)
return alert
@ -207,18 +206,17 @@ class RecordsController: UIViewController, UITableViewDelegate {
&& region! < 1000
}
func makeStartSoundIfNeeded() -> Single<Void> {
if !Settings.shared.recordBeep {
return .just(())
} else {
return Single<Void>.create { observer in
func makeStartSoundIfNeeded() async {
guard Settings.shared.recordBeep else {
return
}
return await withCheckedContinuation { continuation in
var soundId = SystemSoundID()
let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/short_double_high.caf")
AudioServicesCreateSystemSoundID(url as CFURL, &soundId)
AudioServicesPlaySystemSoundWithCompletion(soundId) {
observer(.success(()))
}
return Disposables.create()
continuation.resume()
}
}
}
@ -297,7 +295,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
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 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
@ -325,19 +323,19 @@ class RecordsController: UIViewController, UITableViewDelegate {
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 }
ad.quickAction = .checkNumber(number, event)
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 done = UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default) { action in
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 {
record.number = tf.text?.uppercased()
realmRecord.number = tf.text?.uppercased()
}
}
}
@ -347,18 +345,20 @@ class RecordsController: UIViewController, UITableViewDelegate {
}))
alert.addTextField { tf in
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() ?? "")
}
}
}
self.present(alert, animated: true)
}
func delete(record: AudioRecord) {
func delete(record: AudioRecordDto) {
do {
if let realm = record.realm {
if let realm = try? Realm(), let realmRecord = realm.object(ofType: AudioRecord.self, forPrimaryKey: record.path) {
try realm.write {
realm.delete(record)
realm.delete(realmRecord)
}
}
} catch {
@ -366,7 +366,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
}
}
func share(record: AudioRecord) {
func share(record: AudioRecordDto) {
do {
let url = try FileManager.default.url(for: record.path, in: "recordings")
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()
controller.event = record.event
controller.hidesBottomBarWhenPushed = true

View File

@ -1,5 +1,4 @@
import UIKit
import RxSwift
import AutoCatCore
class RegionsDataSourse: UITableViewDiffableDataSource<VehicleRegion, Int> {
@ -20,7 +19,6 @@ class RegionsController: UIViewController, UISearchResultsUpdating, UITableViewD
var datasource: RegionsDataSourse!
let searchController = UISearchController(searchResultsController: nil)
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
@ -47,14 +45,16 @@ class RegionsController: UIViewController, UISearchResultsUpdating, UITableViewD
return cell
}
Api.getRegions().observeOn(MainScheduler.instance).subscribe(onSuccess: { regions in
self.regions = regions
Task {
do {
self.regions = try await ApiService.shared.getRegions()
self.regionsFiltered = regions
self.updateTableView()
self.applySelection()
}, onError: { error in
} catch {
print("Get regions error: ", error)
}).disposed(by: self.bag)
}
}
}
override func viewWillDisappear(_ animated: Bool) {

View File

@ -7,21 +7,19 @@ import AutoCatCore
import SwiftEntryKit
import MobileCoreServices
import PKHUD
import RxSwift
class ReportController: FormViewController, MediaBrowserViewControllerDataSource, MediaBrowserViewControllerDelegate, UIActivityItemSource {
class ReportController: FormViewController, MediaBrowserViewControllerDataSource, MediaBrowserViewControllerDelegate {
@IBOutlet weak var actionBarItem: UIBarButtonItem!
@IBOutlet weak var copyBarItem: UIBarButtonItem!
private var reportImageUrl: URL?
private let logoPlaceholder = UIImage(named: "SteeringWheel")
private let copyableTags = ["Model", "Year", "Color", "Category", "STP", "Japanese",
"PlateNumber", "VIN", "STS", "PTS",
"EngineNumber", "FuelType", "Volume", "PowerHP", "PowerKw"];
var vehicle: Vehicle? {
var vehicle: VehicleDto? {
didSet {
if isViewLoaded && self.view.window != nil {
self.updateReport()
@ -37,15 +35,13 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
if let realm = try? Realm(), let num = number {
let vehicles = realm.objects(Vehicle.self).filter("number = %@", num)
self.notificationToken?.invalidate()
self.notificationToken = vehicles.observe { _ in self.vehicle = vehicles.first }
self.notificationToken = vehicles.observe { _ in self.vehicle = vehicles.first?.dto }
} else {
self.vehicle = nil
}
}
}
let bag = DisposeBag()
// MARK: - Lifecycle
override func viewDidLoad() {
@ -93,10 +89,10 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
<<< LabelRow("OSAGO") { $0.title = NSLocalizedString("OSAGO", comment: "") }
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
.onCellSelection { _, _ in
let sb = UIStoryboard(name: "Main", bundle: nil)
let controller = sb.instantiateViewController(identifier: "OsagoController") as OsagoController
controller.vehicle = self.vehicle
self.navigationController?.pushViewController(controller, animated: true)
if let contracts = self.vehicle?.osagoContracts, let navController = self.navigationController {
let coordinator = OsagoCoordinator(navController: navController, contracts: contracts)
Task { try await coordinator.start() }
}
}
<<< LabelRow("Owners") { row in
@ -106,10 +102,10 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
.onCellSelection { _, row in
if row.value != "0" {
let sb = UIStoryboard(name: "Main", bundle: nil)
let controller = sb.instantiateViewController(identifier: "OwnersController") as OwnersController
controller.owners = self.vehicle?.ownershipPeriods.toArray() ?? []
self.navigationController?.pushViewController(controller, animated: true)
if let ownerships = self.vehicle?.ownershipPeriods, let navController = self.navigationController {
let coordinator = OwnersCoordinator(navController: navController, ownerships: ownerships)
Task { try await coordinator.start() }
}
}
}
@ -134,7 +130,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
.onCellSelection { _, row in
let controller = AdsController()
controller.ads = self.vehicle?.ads.toArray() ?? []
controller.ads = self.vehicle?.ads ?? []
self.navigationController?.pushViewController(controller, animated: true)
}
@ -143,10 +139,10 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
}
.cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator }
.onCellSelection { _, row in
let sb = UIStoryboard(name: "Main", bundle: nil)
let controller = sb.instantiateViewController(identifier: "NotesController") as NotesController
controller.vehicle = self.vehicle
self.navigationController?.pushViewController(controller, animated: true)
if let vehicle = self.vehicle, let navController = self.navigationController {
let coordinator = NotesCoordinator(navController: navController, vehicle: vehicle)
Task { try await coordinator.start() }
}
}
if Settings.shared.showDebugInfo {
@ -160,7 +156,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
form +++ Section("")
<<< ButtonRow("CheckGB") { $0.title = NSLocalizedString("Check GB", comment: "") }.onCellSelection { cell, row in
self.checkGB()
Task { await self.checkGB() }
}
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 {
row.value = value
row.reload()
@ -279,27 +275,31 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
self.updateReport()
}
func checkGB() {
func checkGB() async {
guard let vehicle = self.vehicle else { return }
do {
HUD.show(.progress)
Api.checkVehicleGb(by: vehicle.getNumber()).observe(on: MainScheduler.instance).subscribe(onSuccess: { newVehicle in
if let realm = vehicle.realm, !vehicle.isFrozen {
let newVehicle = try await ApiService.shared.checkVehicleGb(by: vehicle.getNumber())
let realm = try await Realm()
if let realmVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) {
try? realm.write {
realm.add(newVehicle, update: .all)
realm.add(Vehicle(dto: newVehicle), update: .all)
}
} else {
self.vehicle?.vin1 = newVehicle.vin1
self.vehicle?.color = newVehicle.color
self.vehicle?.sts = newVehicle.sts
}
self.updateReport()
self.form.allSections.forEach { $0.reload() }
HUD.hide()
}, onFailure: { error in
} catch {
HUD.hide()
self.show(error: error)
}).disposed(by: self.bag)
}
}
// MARK: - MediaBrowserViewControllerDataSource & MediaBrowserViewControllerDelegate
@ -316,6 +316,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
}
KingfisherManager.shared.retrieveImage(with: url) { result in
Task { @MainActor in
switch result {
case .success(let res):
completion(index, res.image, ZoomScale.default, nil)
@ -326,6 +327,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
}
}
}
}
func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, didChangeFocusTo index: Int) {
guard let photo = self.vehicle?.photos[index] else { return }
@ -350,10 +352,11 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
let fileURL = documentDirectory.appendingPathComponent("report.png")
if let imageData = image.pngData() {
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
self.present(controller, animated: true)
} catch {
@ -365,12 +368,13 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
guard let vehicle = self.vehicle else { return }
var items: [Any] = [vehicle.reportText()]
for photo in vehicle.photos {
if let url = URL(string: photo.url) {
if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) {
items.append(image)
}
}
// TODO: Fix sharing
// if let url = URL(string: photo.url) {
// if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) {
// items.append(image)
// }
//
// }
}
let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
@ -408,26 +412,6 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource
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
@IBAction func onCopy(_ sender: UIBarButtonItem) {

View File

@ -1,6 +1,4 @@
import UIKit
import RxSwift
import RxCocoa
import RealmSwift
import PKHUD
import ExceptionCatcher
@ -15,8 +13,6 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
private var refreshIndicator: UIBarButtonItem!
private var moreActionsButton: UIBarButtonItem?
private let bag = DisposeBag()
private lazy var searchController: UISearchController = .default
.placeholder(NSLocalizedString("Search plate numbers", comment: ""))
.resultsUpdater(self)
@ -25,11 +21,10 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
.scopeButtons(SearchScope.allCases.map(\.title))
private var refreshControl = UIRefreshControl()
private var datasource: RxSectionedDataSource<Vehicle,VehicleCell>!
private var datasource: SectionedDataSource<VehicleDto,VehicleCell>!
private var isLoadingPage = false
private var pageLoadingIndicator = UIActivityIndicatorView(style: .medium)
var filterRelay = BehaviorRelay<Filter>(value: Filter())
var filter = Filter()
override func viewDidLoad() {
@ -58,37 +53,31 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
self.tableView.addSubview(self.refreshControl)
self.datasource = RxSectionedDataSource(table: self.tableView)
self.datasource = SectionedDataSource(table: self.tableView)
self.tableView.delegate = self
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 {
self.datasource.reset()
}
return Api.getVehicles(with: filter, pageToken: self.datasource.pageToken)
.do(onError: { print($0) })
.catchAndReturn(PagedResponse<Vehicle>())
}
.observe(on: MainScheduler.instance)
.do(onNext: {
if let count = $0.count {
let vehicles = (try? await ApiService.shared.getVehicles(with: filter, pageToken: self.datasource.pageToken, pageSize: 50)) ?? PagedResponse<VehicleDto>()
if let count = vehicles.count {
self.navigationItem.title = String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), count)
self.showMapButton?.isEnabled = count > 0
}
self.refreshControl.endRefreshing()
self.isLoadingPage = false
self.pageLoadingIndicator.stopAnimating()
self.hideProgress()
})
.bind(to: self.datasource.data)
.disposed(by: self.bag)
refreshControl.endRefreshing()
isLoadingPage = false
pageLoadingIndicator.stopAnimating()
hideProgress()
datasource.update(with: vehicles)
}
}
@ -109,7 +98,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
}
// FIXME: Code duplication
func updateDetailController(with vehicle: Vehicle) {
func updateDetailController(with vehicle: VehicleDto) {
if let splitViewController = self.view.window?.rootViewController as? UISplitViewController
{
var detail: UINavigationController?
@ -140,7 +129,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
self.filter.needReset = true
self.filter.scope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber
self.filterRelay.accept(self.filter)
updateSearchResults(with: filter)
}
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
@ -149,7 +138,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
}
filter.scope = scope
filterRelay.accept(filter)
updateSearchResults(with: filter)
}
// MARK: NavigationBar actions
@ -185,7 +174,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
@objc func refresh(_ sender: AnyObject) {
self.showMapButton?.isEnabled = false
self.filter.needReset = true
self.filterRelay.accept(self.filter)
updateSearchResults(with: filter)
}
func showFilter() {
@ -197,7 +186,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
self.datasource.setSortParameter(self.filter.sortBy ?? .updatedDate)
self.filter.needReset = true
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)
}
@ -214,22 +203,19 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
}
func exportSearchResults() {
Task {
do {
showProgress()
Api.getVehicles(with: filter, pageSize: 0)
.observe(on: MainScheduler.instance)
.subscribe(onSuccess: { resp in
self.hideProgress()
let resp = try await ApiService.shared.getVehicles(with: filter, pageSize: 0)
let newLine = "\r\n"
var csvString = Vehicle.csvHeader + newLine
var csvString = VehicleDto.csvHeader + newLine
for vehicle in resp.items {
csvString.append(vehicle.csvLine)
csvString.append(newLine)
}
do {
let tmpUrl = FileManager.default.tmpUrl(name: "search", ext: "csv")
try csvString.write(to: tmpUrl, atomically: true, encoding: .utf8)
#if targetEnvironment(macCatalyst)
@ -237,14 +223,13 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
#else
self.share(file: tmpUrl)
#endif
hideProgress()
} catch {
hideProgress()
HUD.show(error: error)
}
}, onFailure: { error in
self.hideProgress()
HUD.show(error: error)
})
.disposed(by: bag)
}
}
func share(file url: URL) {
@ -290,28 +275,26 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
}
}
func update(vehicle: Vehicle, 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
HUD.hide()
func update(vehicle: VehicleDto, at indexPath: IndexPath) {
Task {
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 {
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 {
print(error)
self.show(error: error)
HUD.hide()
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) {
@ -326,7 +309,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe
if toBottom < 100 && !self.isLoadingPage && self.datasource.needMoreData() {
self.isLoadingPage = true
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 }
}
extension Vehicle: Dated {
extension VehicleDto: Dated {
var updated: Date {
Date(timeIntervalSince1970: self.updatedDate)
@ -28,7 +28,7 @@ extension Vehicle: Dated {
}
}
extension AudioRecord: Dated {
extension AudioRecordDto: Dated {
var updated: Date {
Date(timeIntervalSince1970: self.getAddedDate())
}
@ -53,7 +53,10 @@ extension RandomAccessCollection where Element: Dated & Identifiable {
var key: TimeInterval = 0
var keyNext: TimeInterval = 0
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 {
@ -65,12 +68,16 @@ extension RandomAccessCollection where Element: Dated & Identifiable {
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 startOfPeriod = dateInRegion.dateAtStartOf(component)
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
}

View File

@ -2,7 +2,9 @@ import UIKit
import Kingfisher
import AutoCatCore
extension Vehicle {
extension VehicleDto {
@MainActor
func drawLine(y: CGFloat, width: CGFloat, margin: CGFloat = 15,context: CGContext) {
let lineWidth = 1/UIScreen.main.scale
context.move(to: CGPoint(x: margin, y: y + lineWidth/2))
@ -13,6 +15,7 @@ extension Vehicle {
context.strokePath()
}
@MainActor
func drawCell(y: CGFloat, width: CGFloat, height: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) {
let fontSize: CGFloat = 17
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)
}
@MainActor
func drawBigTextCell(y: CGFloat, width: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) -> CGFloat {
let pStyle = NSMutableParagraphStyle()
pStyle.alignment = .right
@ -50,6 +54,7 @@ extension Vehicle {
return height
}
@MainActor
func reportImage(width: CGFloat) -> UIImage {
var realHeight: CGFloat = 0
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)
y += 1/UIScreen.main.scale
for photo in self.photos {
let date = Date(timeIntervalSince1970: TimeInterval(photo.date/1000))
var name = "<Unknown 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) {
let imgHeight = image.size.height*w/image.size.width
let rect = CGRect(x: 0, y: y, width: w, height: 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
y += 16
}
// TODO: Fix drawing photos in report image
// for photo in self.photos {
// let date = Date(timeIntervalSince1970: TimeInterval(photo.date/1000))
// var name = "<Unknown 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) {
// let imgHeight = image.size.height*w/image.size.width
// let rect = CGRect(x: 0, y: y, width: w, height: 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
//
// y += 16
// }
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 os.log
import AVFoundation
import RxSwift
import PKHUD
import AutoCatCore
@ -112,7 +111,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if tabvc.selectedIndex == 0 {
if let nav = tabvc.selectedViewController as? UINavigationController, let child = nav.topViewController {
if let check = child as? CheckController {
check.handleQuickActions()
Task { await check.handleQuickActions() }
} else {
nav.popToRootViewController(animated: false)
}
@ -134,7 +133,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if let url = userActivity.webpageURL {
if let param = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.first, let token = param.value {
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 }
do {
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 controller = sb.instantiateViewController(identifier: "ReportController") as ReportController
controller.vehicle = vehicle
@ -172,7 +172,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
controller.navigationItem.leftBarButtonItem = BlockBarButtonItem(barButtonSystemItem: .close) { _ in nav.dismiss(animated: true) }
rootController.present(nav, animated: true)
HUD.hide()
} onFailure: { error in
} catch {
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
/// An enumeration to hold default content transformers
@MainActor
public enum DefaultContentTransformers {
/**

View File

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

View File

@ -25,6 +25,7 @@ import UIKit
// MARK: - MediaBrowserViewControllerDataSource protocol
/// Protocol to supply media browser contents.
@MainActor
public protocol MediaBrowserViewControllerDataSource: AnyObject {
/**
@ -38,7 +39,7 @@ public protocol MediaBrowserViewControllerDataSource: AnyObject {
Remember to pass the index received in the datasource method back.
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.
@ -88,6 +89,7 @@ extension MediaBrowserViewControllerDataSource {
// MARK: - MediaBrowserViewControllerDelegate protocol
@MainActor
public protocol MediaBrowserViewControllerDelegate: AnyObject {
/**
@ -767,6 +769,7 @@ extension MediaBrowserViewController {
// MARK: - Updating View Positions
@MainActor
extension MediaBrowserViewController {
@objc private func update(_ timeInterval: TimeInterval) {

View File

@ -24,7 +24,7 @@
import UIKit
/// 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.
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 AVFoundation
import RxSwift
import RxRelay
enum PlayerState {
case stopped
@ -11,12 +9,12 @@ enum PlayerState {
class AudioPlayer: NSObject, AVAudioPlayerDelegate {
static let shared = AudioPlayer()
@MainActor static let shared = AudioPlayer()
private var player: AVAudioPlayer?
private var url: URL?
private var state = BehaviorRelay<PlayerState>(value: .stopped)
private var progress = BehaviorRelay<Double>(value: 0)
private var state: PlayerState = .stopped
private var progress: Double = 0
private var progressTimer: Timer?
func set(url: URL) throws {
@ -35,12 +33,12 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
if player.isPlaying {
player.pause()
try self.deactivateSession()
self.state.accept(.paused)
self.state = .paused
} else {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.duckOthers])
try AVAudioSession.sharedInstance().setActive(true)
player.play()
self.state.accept(.playing)
self.state = .playing
if self.progressTimer == nil {
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 {
player.pause()
try? self.deactivateSession()
self.state.accept(.paused)
self.state = .paused
}
}
@ -65,18 +63,18 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
if let player = self.player {
player.stop()
try? self.deactivateSession()
self.state.accept(.stopped)
self.state = .stopped
self.progressTimer?.invalidate()
self.progressTimer = nil
}
}
func getState() -> PlayerState {
return self.state.value
return self.state
}
func getProgress() -> Double {
return self.progress.value
return self.progress
}
func getUrl() -> URL? {
@ -87,14 +85,6 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
return self.player?.duration ?? 0
}
func stateObservable() -> Observable<PlayerState> {
return self.state.asObservable()
}
func progressObservable() -> Observable<Double> {
return self.progress.asObservable()
}
func deactivateSession() throws {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
}
@ -103,15 +93,15 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
try? self.deactivateSession()
self.progress.accept(1)
self.progress = 1
self.stop()
self.state.accept(.stopped)
self.state = .stopped
}
@objc func progressTick() {
if let player = self.player {
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 AVFoundation
import AudioToolbox
import RxSwift
import os.log
import ExceptionCatcher
@ -34,58 +33,53 @@ class Recorder {
return true
}
func requestPermissions() -> Single<Void> {
return Single<Void>.create { observer in
func requestPermissions() async throws {
try await withCheckedThrowingContinuation { continuation in
AVAudioSession.sharedInstance().requestRecordPermission { allowed in
if allowed {
SFSpeechRecognizer.requestAuthorization { status in
switch status {
case .authorized:
observer(.success(()))
continuation.resume()
break
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")
observer(.failure(error))
break
continuation.resume(throwing: error)
case .restricted:
let error = CocoaError.error("Access error", reason: "Speech recognition is restricted on this device")
observer(.failure(error))
break
continuation.resume(throwing: error)
case .notDetermined:
let error = CocoaError.error("Access error", reason: "Speech recognition status is not yet determined")
observer(.failure(error))
break
continuation.resume(throwing: error)
@unknown default:
let error = CocoaError.error("Access error", reason: "Unknown error accessing speech recognizer")
observer(.failure(error))
break
continuation.resume(throwing: error)
}
}
} else {
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) -> Single<String> {
func startRecording(to file: URL) async throws -> String {
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 {
observer(.failure(CocoaError.error("Recording error", reason: "Format not supported")))
return Disposables.create()
continuation.resume(throwing: CocoaError.error("Recording error", reason: "Format not supported"))
return
}
ExtAudioFileCreateWithURL(file as CFURL, kAudioFileM4AType, aac.streamDescription, nil, AudioFileFlags.eraseFile.rawValue, &self.fileRef)
guard let fileRef = self.fileRef else {
observer(.failure(CocoaError.error(CocoaError.Code.fileWriteUnknown)))
return Disposables.create()
continuation.resume(throwing: CocoaError.error(CocoaError.Code.fileWriteUnknown))
return
}
do {
@ -106,26 +100,24 @@ class Recorder {
if let transcription = result?.bestTranscription {
self.result = transcription.formattedString
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()
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()
observer(.success(self.result))
continuation.resume(returning: self.result)
}
self.engine.prepare()
try self.engine.start()
} catch {
observer(.failure(error))
}
return Disposables.create {
self.cancelRecording()
continuation.resume(throwing: error)
}
}
}

View File

@ -6,20 +6,21 @@ import AutoCatCore
typealias FilterPredicate<T> = (T) -> Bool
class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
where Item: Object & Identifiable & Dated & Cloneable,
where Item: Object & DtoConvertible,
Item.Dto: Identifiable & Dated,
Cell: UITableViewCell & ConfigurableCell,
Cell.Item == Item {
Cell.Item == Item.Dto {
private var tv: UITableView
private var data: Results<Item>
private var notificationToken: NotificationToken?
private var sections: [DateSection<Item>] = []
private var sections: [DateSection<Item.Dto>] = []
private var cellIdentifier: String
private var filterPredicate: FilterPredicate<Item>?
private var searchPredicate: FilterPredicate<Item>?
private var filterPredicate: FilterPredicate<Item.Dto>?
private var searchPredicate: FilterPredicate<Item.Dto>?
private let groupQueue = DispatchQueue(label: "group")
private var objects: [Item] = []
private var objects: [Item.Dto] = []
private let onSizeChanged: ((Int) -> Void)?
@ -39,13 +40,13 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
self.notificationToken = self.data.observe { changes in
switch changes {
case .initial:
self.objects = self.data.toArray().map { $0.shallowClone() }
self.objects = self.data.toArray().map(\.shallowDto)
self.reload(animated: false)
case .update(_, let deletions, let insertions, let modifications):
deletions.forEach { self.objects.remove(at: $0) }
insertions.forEach { self.objects.insert(self.data[$0].shallowClone(), at: $0) }
modifications.forEach { self.objects[$0] = self.data[$0].shallowClone() }
insertions.forEach { self.objects.insert(self.data[$0].shallowDto, at: $0) }
modifications.forEach { self.objects[$0] = self.data[$0].shallowDto }
self.reload()
// if deletions.isEmpty && modifications.isEmpty && insertions.count == 1 && insertions.first == 0 {
@ -93,7 +94,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
// MARK: - Public
func item(at indexPath: IndexPath) -> Item {
func item(at indexPath: IndexPath) -> Item.Dto {
return self.sections[indexPath.section].elements[indexPath.row]
}
@ -114,7 +115,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
}
func insertFirst() {
guard let item = data.first?.shallowClone() else {
guard let item = data.first?.shallowDto else {
reload(animated: false)
return
}
@ -129,7 +130,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
}
func reloadFirst() {
guard !sections.isEmpty, let item = data.first?.clone() else {
guard !sections.isEmpty, let item = data.first?.dto else {
reload(animated: false)
return
}
@ -147,7 +148,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
}
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 toIndexPath = IndexPath(row: 0, section: 0)
tv.moveRow(at: fromIndexPath, to: toIndexPath)
@ -161,7 +162,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
return nil
}
func setFilterPredicate(_ predicate: FilterPredicate<Item>?) {
func setFilterPredicate(_ predicate: FilterPredicate<Item.Dto>?) {
guard self.filterPredicate != nil || predicate != nil else {
return
}
@ -170,7 +171,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
self.reload()
}
func setSearchPredicate(_ predicate: FilterPredicate<Item>?) {
func setSearchPredicate(_ predicate: FilterPredicate<Item.Dto>?) {
guard self.searchPredicate != nil || predicate != nil else {
return
}
@ -184,7 +185,7 @@ class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
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 {
items = items.filter(predicate)
}

View File

@ -1,9 +1,7 @@
import UIKit
import RxSwift
import RxCocoa
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 cellIdentifier: String
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
}
var data: Binder<PagedResponse<Item>> {
return Binder(self) { datasource, data in
func update(with data: PagedResponse<Item>) {
if let count = data.count {
self.count = count
self.items = data.items
@ -63,14 +60,14 @@ class RxSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource where I
}
self.pageToken = data.pageToken
DispatchQueue.global().async {
// TODO: Grouping on background thread
// DispatchQueue.global().async {
let newSections = self.items.groupedByDate(type: self.sortParam)
DispatchQueue.main.async {
// DispatchQueue.main.async {
self.sections = newSections
self.tv.reloadData()
}
}
}
// }
// }
}
func needMoreData() -> Bool {

View File

@ -1,12 +1,22 @@
import UIKit
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
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)
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) {
ctx.saveGState()
super.draw(in: ctx)

View File

@ -1,10 +1,12 @@
import UIKit
import AutoCatCore
@MainActor
protocol PNKeyboardDelegate: AnyObject {
func returnClicked()
}
@MainActor
protocol PNButtonDelegate: AnyObject {
func buttonTapped(_ button: PNButton)
}

View File

@ -2,7 +2,7 @@ import UIKit
import AutoCatCore
private class CALayerAnimationsDisablingDelegate: NSObject, CALayerDelegate {
static let shared = CALayerAnimationsDisablingDelegate()
@MainActor static let shared = CALayerAnimationsDisablingDelegate()
private let null = NSNull()
func action(for layer: CALayer, forKey event: String) -> CAAction? {
@ -22,7 +22,7 @@ class PlateView: UIView {
private var numberLayer = CenterTextLayer(coeff: fontHeightCoeff)
private var regionLayer = CenterTextLayer(coeff: fontHeightCoeff)
private var countryLayer = CenterTextLayer()
private var flagLayer = FlagLayer()
private var flagLayer = FlagLayer(scale: UIScreen.main.scale)
var number: PlateNumber? {
didSet {
@ -42,7 +42,7 @@ class PlateView: UIView {
}
}
var onChange: (() -> Void)?
var onChange: (@MainActor () -> Void)?
override init(frame: CGRect) {
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!
required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@ -43,7 +43,7 @@ class SourceStatusCell: Cell<DebugInfoEntry>, CellType {
self.detailTextLabel?.text = nil
if let value = row.value {
self.circle.tintColor = value.statusEnum.color
self.circle.tintColor = value.status.color
//self.accessoryType = value.error == nil ? .none : .disclosureIndicator
} else {
self.circle.tintColor = .systemGray
@ -57,11 +57,12 @@ final class SourceStatusRow: Row<SourceStatusCell>, RowType {
super.init(tag: tag)
cellProvider = CellProvider<SourceStatusCell>()
self.onCellSelection { cell, row in
if let error = row.value?.error, let controller = cell.parentViewController {
let alert = UIAlertController(title: row.title, message: error, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
controller.present(alert, animated: true)
}
// TODO: Fix isolation problems
// if let error = row.value?.error, let controller = cell.parentViewController {
// let alert = UIAlertController(title: row.title, message: error, preferredStyle: .alert)
// 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)
}
} 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
}
public init(timestamp: Double, items: [T]) {
public init(timestamp: Double, items: [T], monthStart: DateInRegion, weekStart: DateInRegion) {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .medium
let monthStart = DateCache.shared.monthStart
let weekStart = DateCache.shared.weekStart
let date = Date(timeIntervalSince1970: timestamp)
let dateInRegion = DateInRegion(date, region: Region.current)
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
public enum AddedBy: String, CustomStringConvertible, CaseIterable {
public enum AddedBy: String, CustomStringConvertible, CaseIterable, Sendable {
case anyone
case me
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 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 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 vin = 1
@ -61,7 +61,7 @@ public enum SearchScope: Int, CaseIterable {
}
}
public struct Filter {
public struct Filter: Sendable {
public var searchString = ""
public var brand: 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
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 pageToken: String?
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 RealmSwift
public class AudioRecord: Object, Identifiable, Cloneable {
public final class AudioRecord: Object {
@Persisted public var path: 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 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 {
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.number = number
self.duration = duration
@ -33,10 +24,6 @@ public class AudioRecord: Object, Identifiable, Cloneable {
self.addedDate = Date().timeIntervalSince1970
}
required override init() {
super.init()
}
public override static func primaryKey() -> String? {
return "path"
}
@ -44,23 +31,29 @@ public class AudioRecord: Object, Identifiable, Cloneable {
public override class func ignoredProperties() -> [String] {
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.path = copy.path
self.number = copy.number
self.rawText = copy.rawText
self.addedDate = copy.addedDate
self.duration = copy.duration
self.event = copy.event?.clone()
self.init(path: dto.path,
number: dto.number,
raw: dto.rawText,
duration: dto.duration,
event: VehicleEvent(dto: dto.event))
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