├── .gitignore
├── README.md
├── Scanverter.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
├── Scanverter
├── Helpers
│ ├── Constants.swift
│ ├── DataManager.swift
│ ├── ImageResizer.swift
│ ├── PDFGenerator.swift
│ ├── PhotoCaptureHandler.swift
│ ├── SwiftUIExtensions.swift
│ ├── UIKitExtensions.swift
│ └── VNRectangleFeature.swift
├── Models
│ ├── AlertError.swift
│ ├── Detection.swift
│ ├── DocFile.swift
│ ├── EditTool.swift
│ ├── Folder.swift
│ ├── Language.swift
│ ├── Option.swift
│ ├── PDF.swift
│ ├── Photo.swift
│ ├── ScannedDoc.swift
│ └── Translation.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Services
│ ├── BiometricAuthentication.swift
│ ├── CameraService.swift
│ ├── ServiceLocator.swift
│ └── TranslationService.swift
├── SupportingFiles
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── addPageButton.imageset
│ │ │ ├── Contents.json
│ │ │ ├── addPageButton.png
│ │ │ ├── addPageButton@2x.png
│ │ │ └── addPageButton@3x.png
│ │ ├── add_button_filled.imageset
│ │ │ ├── Contents.json
│ │ │ ├── add_button_filled.png
│ │ │ └── add_button_filled@2x.png
│ │ ├── add_folder_button.imageset
│ │ │ ├── Contents.json
│ │ │ ├── add_folder_button.png
│ │ │ └── add_folder_button@2x.png
│ │ ├── captureButton.imageset
│ │ │ ├── Contents.json
│ │ │ ├── captureButton.png
│ │ │ ├── captureButton@2x.png
│ │ │ └── captureButton@3x.png
│ │ ├── cropButton.imageset
│ │ │ ├── Contents.json
│ │ │ ├── cropButton.png
│ │ │ ├── cropButton@2x.png
│ │ │ └── cropButton@3x.png
│ │ ├── default.imageset
│ │ │ ├── Contents.json
│ │ │ └── default.png
│ │ ├── deletePageButton.imageset
│ │ │ ├── Contents.json
│ │ │ ├── deletePageButton.png
│ │ │ ├── deletePageButton@2x.png
│ │ │ └── deletePageButton@3x.png
│ │ ├── error.imageset
│ │ │ ├── Contents.json
│ │ │ └── error.png
│ │ ├── filterButton.imageset
│ │ │ ├── Contents.json
│ │ │ ├── filterButton.png
│ │ │ ├── filterButton@2x.png
│ │ │ └── filterButton@3x.png
│ │ ├── folder_icon.imageset
│ │ │ ├── Contents.json
│ │ │ ├── folder_icon.png
│ │ │ ├── folder_icon@2x.png
│ │ │ └── folder_icon@3x.png
│ │ ├── folder_image.imageset
│ │ │ ├── Contents.json
│ │ │ ├── folder_image.png
│ │ │ ├── folder_image@2x.png
│ │ │ └── folder_image@3x.png
│ │ ├── imageSave.imageset
│ │ │ ├── Contents.json
│ │ │ ├── imageSave.png
│ │ │ ├── imageSave@2x.png
│ │ │ └── imageSave@3x.png
│ │ ├── lock_icon.imageset
│ │ │ ├── Contents.json
│ │ │ ├── lock_icon.png
│ │ │ └── lock_icon@2x.png
│ │ ├── more_options.imageset
│ │ │ ├── Contents.json
│ │ │ └── more_options.png
│ │ ├── ocrButton.imageset
│ │ │ ├── Contents.json
│ │ │ ├── ocr_button.png
│ │ │ ├── ocr_button@2x.png
│ │ │ └── ocr_button@3x.png
│ │ ├── pdfSave.imageset
│ │ │ ├── Contents.json
│ │ │ ├── pdfSave.png
│ │ │ ├── pdfSave@2x.png
│ │ │ └── pdfSave@3x.png
│ │ ├── pdf_icon.imageset
│ │ │ ├── Contents.json
│ │ │ ├── pdf_icon.png
│ │ │ ├── pdf_icon@2x.png
│ │ │ └── pdf_icon@3x.png
│ │ ├── saveButton.imageset
│ │ │ ├── Contents.json
│ │ │ ├── saveButton.png
│ │ │ ├── saveButton@2x.png
│ │ │ └── saveButton@3x.png
│ │ └── success.imageset
│ │ │ ├── Contents.json
│ │ │ └── sucess.png
│ ├── Images.xcassets
│ │ ├── Contents.json
│ │ ├── imageWithText.imageset
│ │ │ ├── Contents.json
│ │ │ └── imageWithText.jpg
│ │ └── slideWithText.imageset
│ │ │ ├── Contents.json
│ │ │ └── slideWithText.png
│ └── Info.plist
├── System
│ ├── AppContainer.swift
│ └── ScanverterApp.swift
├── Views
│ ├── Alerts
│ │ ├── CreateFolderView.swift
│ │ └── CustomAlert.swift
│ ├── Camera
│ │ ├── CameraPreview.swift
│ │ ├── CameraView.swift
│ │ └── ViewModels
│ │ │ └── CameraViewModel.swift
│ ├── Collections
│ │ ├── Cells
│ │ │ ├── EditImageCell.swift
│ │ │ ├── EditToolCell.swift
│ │ │ ├── FileGridCell.swift
│ │ │ ├── FileListCell.swift
│ │ │ ├── GridCell.swift
│ │ │ └── ListCell.swift
│ │ ├── Custom
│ │ │ ├── DataSource
│ │ │ │ └── PhotoCollectionDataSource.swift
│ │ │ └── Presentation
│ │ │ │ ├── EditorView.swift
│ │ │ │ ├── OCRResultsView.swift
│ │ │ │ └── PhotoCollectionView.swift
│ │ ├── DataSource
│ │ │ ├── EditCellDataSource.swift
│ │ │ ├── EditToolCellDataSource.swift
│ │ │ ├── FileCellDataSource.swift
│ │ │ └── FolderCellDataSource.swift
│ │ └── Generic
│ │ │ └── CollectionView.swift
│ ├── CustomViews
│ │ ├── ActivityIndicator.swift
│ │ ├── CustomProgressConfig.swift
│ │ ├── CustomProgressView.swift
│ │ ├── CustomTextView.swift
│ │ ├── ImagePicker.swift
│ │ ├── PDFViewer.swift
│ │ ├── PageView.swift
│ │ ├── Pager.swift
│ │ ├── SearchBar.swift
│ │ └── TextScannerView.swift
│ ├── Main
│ │ ├── DataSources
│ │ │ ├── DocsDataSource.swift
│ │ │ ├── FoldersDataSource.swift
│ │ │ ├── SearchDataSource.swift
│ │ │ ├── SettingsDataSource.swift
│ │ │ └── TextRecognizer.swift
│ │ └── Screens
│ │ │ ├── CurrentScreen.swift
│ │ │ ├── FolderDetailScreen.swift
│ │ │ ├── FoldersScreen.swift
│ │ │ ├── MainTabBarView.swift
│ │ │ ├── SettingsScreen.swift
│ │ │ └── SettingsScreenTempleate.swift
│ ├── Modals
│ │ ├── ModalCamera.swift
│ │ ├── ModalScreen.swift
│ │ ├── ModalSearchScreen.swift
│ │ └── TextRecognizerScreen.swift
│ ├── Navigation
│ │ ├── NavigationStack.swift
│ │ └── ScreenDetails.swift
│ ├── Protocols
│ │ └── ContainerView.swift
│ ├── Settings
│ │ └── SettingsViews.swift
│ └── Tabs
│ │ ├── EditorTabBar.swift
│ │ ├── ModalScreenShowTabButton.swift
│ │ ├── Tab.swift
│ │ ├── TabBar.swift
│ │ ├── TabBarItem.swift
│ │ └── ViewModels
│ │ └── EditorTabBarDataSource.swift
└── tessdata
│ ├── deu.lstm
│ ├── deu.lstm-number-dawg
│ ├── deu.lstm-punc-dawg
│ ├── deu.lstm-recoder
│ ├── deu.lstm-unicharset
│ ├── deu.lstm-word-dawg
│ ├── deu.traineddata
│ ├── deu.version
│ ├── eng.lstm
│ ├── eng.lstm-number-dawg
│ ├── eng.lstm-punc-dawg
│ ├── eng.lstm-recoder
│ ├── eng.lstm-unicharset
│ ├── eng.lstm-word-dawg
│ ├── eng.traineddata
│ ├── eng.version
│ ├── fra.lstm
│ ├── fra.lstm-number-dawg
│ ├── fra.lstm-punc-dawg
│ ├── fra.lstm-recoder
│ ├── fra.lstm-unicharset
│ ├── fra.lstm-word-dawg
│ ├── fra.traineddata
│ ├── fra.version
│ ├── ita.lstm
│ ├── ita.lstm-number-dawg
│ ├── ita.lstm-punc-dawg
│ ├── ita.lstm-recoder
│ ├── ita.lstm-unicharset
│ ├── ita.lstm-word-dawg
│ ├── ita.traineddata
│ ├── ita.version
│ ├── rus.lstm
│ ├── rus.lstm-number-dawg
│ ├── rus.lstm-punc-dawg
│ ├── rus.lstm-recoder
│ ├── rus.lstm-unicharset
│ ├── rus.lstm-word-dawg
│ ├── rus.traineddata
│ ├── rus.version
│ ├── spa.lstm
│ ├── spa.lstm-number-dawg
│ ├── spa.lstm-punc-dawg
│ ├── spa.lstm-recoder
│ ├── spa.lstm-unicharset
│ ├── spa.lstm-word-dawg
│ ├── spa.traineddata
│ └── spa.version
└── screenshots
├── biomethric_on_folder_locking.png
├── camera_screen.png
├── faceid_permission_request.png
├── folder_details.png
├── folders_grid.png
├── main_screen_with_folders.png
├── recognition_result.png
├── settings_stubs.png
├── tesseract_recognition.png
├── visionkit_pdf_view.png
├── visionkit_result.png
└── visionkit_scan.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,swift,swiftpm,macos
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift,swiftpm,macos
3 |
4 | ### macOS ###
5 | # General
6 | .DS_Store
7 | .AppleDouble
8 | .LSOverride
9 |
10 | # Icon must end with two \r
11 | Icon
12 |
13 |
14 | # Thumbnails
15 | ._*
16 |
17 | # Files that might appear in the root of a volume
18 | .DocumentRevisions-V100
19 | .fseventsd
20 | .Spotlight-V100
21 | .TemporaryItems
22 | .Trashes
23 | .VolumeIcon.icns
24 | .com.apple.timemachine.donotpresent
25 |
26 | # Directories potentially created on remote AFP share
27 | .AppleDB
28 | .AppleDesktop
29 | Network Trash Folder
30 | Temporary Items
31 | .apdisk
32 |
33 | ### Swift ###
34 | # Xcode
35 | #
36 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
37 |
38 | ## User settings
39 | xcuserdata/
40 |
41 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
42 | *.xcscmblueprint
43 | *.xccheckout
44 |
45 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
46 | build/
47 | DerivedData/
48 | *.moved-aside
49 | *.pbxuser
50 | !default.pbxuser
51 | *.mode1v3
52 | !default.mode1v3
53 | *.mode2v3
54 | !default.mode2v3
55 | *.perspectivev3
56 | !default.perspectivev3
57 |
58 | ## Obj-C/Swift specific
59 | *.hmap
60 |
61 | ## App packaging
62 | *.ipa
63 | *.dSYM.zip
64 | *.dSYM
65 |
66 | ## Playgrounds
67 | timeline.xctimeline
68 | playground.xcworkspace
69 |
70 | # Swift Package Manager
71 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
72 | # Packages/
73 | # Package.pins
74 | # Package.resolved
75 | # *.xcodeproj
76 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
77 | # hence it is not needed unless you have added a package configuration file to your project
78 | # .swiftpm
79 |
80 | .build/
81 |
82 | # CocoaPods
83 | # We recommend against adding the Pods directory to your .gitignore. However
84 | # you should judge for yourself, the pros and cons are mentioned at:
85 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
86 | # Pods/
87 | # Add this line if you want to avoid checking in source code from the Xcode workspace
88 | # *.xcworkspace
89 |
90 | # Carthage
91 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
92 | # Carthage/Checkouts
93 |
94 | Carthage/Build/
95 |
96 | # Accio dependency management
97 | Dependencies/
98 | .accio/
99 |
100 | # fastlane
101 | # It is recommended to not store the screenshots in the git repo.
102 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
103 | # For more information about the recommended setup visit:
104 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
105 |
106 | fastlane/report.xml
107 | fastlane/Preview.html
108 | fastlane/screenshots/**/*.png
109 | fastlane/test_output
110 |
111 | # Code Injection
112 | # After new code Injection tools there's a generated folder /iOSInjectionProject
113 | # https://github.com/johnno1962/injectionforxcode
114 |
115 | iOSInjectionProject/
116 |
117 | ### SwiftPM ###
118 | Packages
119 | xcuserdata
120 | #*.xcodeproj
121 |
122 |
123 | ### Xcode ###
124 | # Xcode
125 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
126 |
127 |
128 |
129 |
130 | ## Gcc Patch
131 | /*.gcno
132 |
133 | ### Xcode Patch ###
134 | #*.xcodeproj/*
135 | #!*.xcodeproj/project.pbxproj
136 | !*.xcodeproj/xcshareddata/
137 | !*.xcworkspace/contents.xcworkspacedata
138 | **/xcshareddata/WorkspaceSettings.xcsettings
139 |
140 | # End of https://www.toptal.com/developers/gitignore/api/xcode,swift,swiftpm,macos
141 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## ScanverterApp
2 |
3 | * captures photos (vision kit vs plain camera as service)
4 | * converts them to pdf (and also saves them as images in photo library)
5 | * text recognition with tesseract (vision kit's one disabled)
6 | * SwiftUI with Combine
7 | * SwiftyTesseract, ExyteGrid
8 | * MVVM architecture for presentation layer
9 | * SOA for business logic
10 | * translation (google) service disabled
11 | * not finished yet
12 | * iOS 14
13 |
14 | ### Screenshots
15 |
16 |
17 |  |
18 |  |
19 |  |
20 |  |
21 |
22 |
23 |  |
24 |  |
25 |  |
26 |  |
27 |
28 |
29 |  |
30 |  |
31 |  |
32 |
33 |
--------------------------------------------------------------------------------
/Scanverter.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Scanverter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Scanverter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "ExyteGrid",
6 | "repositoryURL": "https://github.com/exyte/Grid",
7 | "state": {
8 | "branch": null,
9 | "revision": "f6a448eca2188b16ffcc9d42fda39e6a8a62d127",
10 | "version": "1.1.0"
11 | }
12 | },
13 | {
14 | "package": "libtesseract",
15 | "repositoryURL": "https://github.com/SwiftyTesseract/libtesseract.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "d8b5fab45f5dc0b1d34b0ca7bf6620b72426adf1",
19 | "version": "0.1.0"
20 | }
21 | },
22 | {
23 | "package": "PopupView",
24 | "repositoryURL": "https://github.com/dartsyms/PopupView",
25 | "state": {
26 | "branch": null,
27 | "revision": "4cb236c8b8e9de5e93d4ba23354ad64427418935",
28 | "version": "0.0.12"
29 | }
30 | },
31 | {
32 | "package": "SwiftUIVisualEffects",
33 | "repositoryURL": "https://github.com/lucasbrown/swiftui-visual-effects.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "b26f8cebd55ff60ed8953768aa818dfb005b5838",
37 | "version": "1.0.3"
38 | }
39 | },
40 | {
41 | "package": "SwiftyTesseract",
42 | "repositoryURL": "https://github.com/dartsyms/SwiftyTesseract",
43 | "state": {
44 | "branch": null,
45 | "revision": "1b4f0ef0c2ba08ae92254af730f2013af4ebdc4f",
46 | "version": "4.0.0"
47 | }
48 | }
49 | ]
50 | },
51 | "version": 1
52 | }
53 |
--------------------------------------------------------------------------------
/Scanverter/Helpers/Constants.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreGraphics
3 | import UIKit
4 |
5 | public enum ModalOption {
6 | case mic, camera, search, none
7 | }
8 |
9 | public struct Constants {
10 | static let editTools = [
11 | // EditTool.init(.add, image: UIImage(named: "addPageButton")!),
12 | EditTool.init(.crop, image: UIImage(named: "cropButton")!),
13 | EditTool.init(.delete, image: UIImage(named: "deletePageButton")!),
14 | // EditTool.init(.save(.image), image: UIImage(named: "imageSave")!),
15 | EditTool.init(.save(.pdf), image: UIImage(named: "pdfSave")!),
16 | EditTool.init(.ocr, image: UIImage(named: "ocrButton")!)
17 | ]
18 |
19 | static var mockedDocs: [ScannedDoc] {
20 | var docs: [ScannedDoc] = .init()
21 | let names = ["imageWithText", "slideWithText"]
22 | for _ in 0..<2 {
23 | names.forEach {
24 | docs.append(ScannedDoc(image: UIImage(named: $0)!.cgImage!, date: Date()))
25 | }
26 | }
27 | return docs
28 | }
29 |
30 | static let googleTranslationApiKey = "XXX-XXXXXX-XXXXXXXXX"
31 | }
32 |
--------------------------------------------------------------------------------
/Scanverter/Helpers/ImageResizer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | enum ImageResizingError: Error {
5 | case cannotRetrieveFromURL
6 | case cannotRetrieveFromData
7 | }
8 |
9 | public struct ImageResizer {
10 | var targetWidth: CGFloat
11 |
12 | public func resize(at url: URL) -> UIImage? {
13 | guard let image = UIImage(contentsOfFile: url.path) else {
14 | return nil
15 | }
16 |
17 | return self.resize(image: image)
18 | }
19 |
20 | public func resize(image: UIImage) -> UIImage {
21 | let originalSize = image.size
22 | let targetSize = CGSize(width: targetWidth, height: targetWidth * originalSize.height / originalSize.width)
23 | let renderer = UIGraphicsImageRenderer(size: targetSize)
24 | return renderer.image { context in
25 | image.draw(in: CGRect(origin: .zero, size: targetSize))
26 | }
27 | }
28 |
29 | public func resize(data: Data) -> UIImage? {
30 | guard let image = UIImage(data: data) else {return nil}
31 | return resize(image: image )
32 | }
33 | }
34 |
35 | struct MemorySizer {
36 | static func size(of data: Data) -> String {
37 | let bcf = ByteCountFormatter()
38 | bcf.allowedUnits = [.useMB] //[]
39 | bcf.countStyle = .file
40 | let string = bcf.string(fromByteCount: Int64(data.count))
41 | return string
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Scanverter/Helpers/PDFGenerator.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import PDFKit
3 | import Combine
4 |
5 | protocol DocGenerator {
6 | func generatePDF() -> AnyPublisher
7 | func getPDFdata() -> AnyPublisher
8 | var pages: [UIImage] { get set }
9 | }
10 |
11 | class PDFGenerator: NSObject, DocGenerator {
12 | var pages: [UIImage]
13 | private let pdfDocument = PDFDocument()
14 | private var pdfPage: PDFPage?
15 |
16 | var totalPages: Int { return pages.count }
17 |
18 | init(pages: [UIImage]) {
19 | self.pages = pages
20 | }
21 |
22 | func generatePDF() -> AnyPublisher {
23 | return Future { promise in
24 | DispatchQueue.global(qos: .background).async {
25 | for (index, image) in self.pages.enumerated() {
26 | print("Scanned pages in generator: \(self.pages)")
27 | // let A4paperSize = CGSize(width: 595, height: 842)
28 | // let bounds = CGRect.init(origin: .zero, size: A4paperSize)
29 | // let coreImage = image.cgImage!
30 | // let editedImage = UIImage(cgImage: coreImage)
31 | self.pdfPage = PDFPage(image: image)
32 | // self.pdfPage?.setBounds(bounds, for: .cropBox)
33 | self.pdfDocument.insert(self.pdfPage!, at: index)
34 | }
35 | promise(.success(self.pdfDocument))
36 | }
37 | }.eraseToAnyPublisher()
38 | }
39 |
40 | func getPDFdata() -> AnyPublisher {
41 | return Future { promise in
42 | DispatchQueue.global(qos: .background).async { [weak self] in
43 | let data = self?.pdfDocument.dataRepresentation()
44 | promise(.success(data))
45 | }
46 | }.eraseToAnyPublisher()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Scanverter/Helpers/SwiftUIExtensions.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 | func navigatePush(whenTrue toggle: Binding) -> some View {
5 | NavigationLink(destination: self, isActive: toggle) { EmptyView() }
6 | }
7 |
8 | func navigatePush(when binding: Binding, matches: H) -> some View {
9 | NavigationLink(destination: self, tag: matches, selection: Binding(binding)) { EmptyView() }
10 | }
11 |
12 | func navigatePush(when binding: Binding, matches: H) -> some View {
13 | NavigationLink(destination: self, tag: matches, selection: binding) { EmptyView() }
14 | }
15 | }
16 |
17 | extension Binding {
18 | func didSet(execute: @escaping (Value) -> Void) -> Binding {
19 | return Binding(
20 | get: { return self.wrappedValue },
21 | set: { self.wrappedValue = $0; execute($0) }
22 | )
23 | }
24 | }
25 |
26 |
27 | extension Color {
28 | init(hex: String) {
29 | let scanner = Scanner(string: hex)
30 | var rgbValue: UInt64 = 0
31 | scanner.scanHexInt64(&rgbValue)
32 |
33 | let r = (rgbValue & 0xff0000) >> 16
34 | let g = (rgbValue & 0xff00) >> 8
35 | let b = rgbValue & 0xff
36 |
37 | self.init(red: Double(r) / 0xff, green: Double(g) / 0xff, blue: Double(b) / 0xff)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Scanverter/Helpers/VNRectangleFeature.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreGraphics
3 | import CoreImage
4 |
5 | class VNRectangleFeature: CIFeature {
6 | open var topLeft: CGPoint = .zero
7 | open var topRight: CGPoint = .zero
8 | open var bottomLeft: CGPoint = .zero
9 | open var bottomRight: CGPoint = .zero
10 |
11 | class func setValue(topLeft: CGPoint, topRight: CGPoint, bottomLeft: CGPoint, bottomRight: CGPoint) -> VNRectangleFeature {
12 | let object: VNRectangleFeature = VNRectangleFeature()
13 | object.topLeft = topLeft
14 | object.topRight = topRight
15 | object.bottomLeft = bottomLeft
16 | object.bottomRight = bottomRight
17 | return object
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Scanverter/Models/AlertError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct AlertError {
4 | public var title: String = ""
5 | public var message: String = ""
6 | public var primaryButtonTitle = "Accept"
7 | public var secondaryButtonTitle: String?
8 | public var primaryAction: (() -> ())?
9 | public var secondaryAction: (() -> ())?
10 |
11 | public init(title: String = "",
12 | message: String = "",
13 | primaryButtonTitle: String = "Accept",
14 | secondaryButtonTitle: String? = nil,
15 | primaryAction: (() -> ())? = nil,
16 | secondaryAction: (() -> ())? = nil) {
17 |
18 | self.title = title
19 | self.message = message
20 | self.primaryAction = primaryAction
21 | self.primaryButtonTitle = primaryButtonTitle
22 | self.secondaryAction = secondaryAction
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/Scanverter/Models/Detection.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Detections: Codable {
4 | var items: [Detection]
5 |
6 | init(items: [Detection]) {
7 | self.items = items
8 | }
9 |
10 | enum CodingKeys: String, CodingKey, CaseIterable {
11 | case items = "detections"
12 | }
13 |
14 | public init(from decoder: Decoder) throws {
15 | let container = try decoder.container(keyedBy: CodingKeys.self)
16 | self.items = container.decodeSafelyIfPresent([Detection].self, forKey: .items) ?? []
17 | }
18 | }
19 |
20 | public struct Detection: Codable {
21 | let language: String?
22 | let isReliable: Bool?
23 | let confidence: Float?
24 |
25 | init(language: String?, isReliable: Bool?, confidence: Float?) {
26 | self.language = language
27 | self.isReliable = isReliable
28 | self.confidence = confidence
29 | }
30 |
31 | enum CodingKeys: String, CodingKey, CaseIterable {
32 | case language, isReliable, confidence
33 | }
34 |
35 | public init(from decoder: Decoder) throws {
36 | let container = try decoder.container(keyedBy: CodingKeys.self)
37 | self.language = container.decodeSafelyIfPresent(String.self, forKey: .language)
38 | self.isReliable = container.decodeSafelyIfPresent(Bool.self, forKey: .isReliable)
39 | self.confidence = container.decodeSafelyIfPresent(Float.self, forKey: .confidence)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Scanverter/Models/DocFile.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Combine
3 |
4 | struct DocFile: Codable {
5 | var name: String
6 | var date: Date
7 | var uid: UUID
8 | var parent: Folder
9 | }
10 |
11 | extension DocFile: Equatable, Hashable {
12 | static func == (lhs: DocFile, rhs: DocFile) -> Bool {
13 | return lhs.uid == rhs.uid
14 | }
15 |
16 | func hash(into hasher: inout Hasher) {
17 | hasher.combine(uid)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Scanverter/Models/EditTool.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Foundation
3 |
4 | struct EditTool {
5 | let uid: String = UUID().uuidString
6 | let type: EditType
7 | let image: UIImage
8 | init(_ type: EditType, image: UIImage) {
9 | self.type = type
10 | self.image = image
11 | }
12 | }
13 |
14 | extension EditTool: Identifiable {
15 | var id: String {
16 | return uid
17 | }
18 | }
19 |
20 | extension EditTool: Equatable, Hashable {
21 | static func == (lhs: EditTool, rhs: EditTool) -> Bool {
22 | return lhs.uid == rhs.uid
23 | }
24 |
25 | func hash(into hasher: inout Hasher) {
26 | hasher.combine(uid)
27 | }
28 | }
29 |
30 | enum DocumentOrigin {
31 | case camera
32 | case photos
33 | }
34 |
35 | enum ProgressViewStyle {
36 | case status
37 | case progress
38 | case none
39 | }
40 |
41 | enum EditType {
42 | case add
43 | case crop
44 | case delete
45 | case save(SaveType)
46 | case ocr
47 |
48 | var tool: String {
49 | switch self {
50 | case .add:
51 | return "Add"
52 | case .crop:
53 | return "Crop"
54 | case .delete:
55 | return "Delete"
56 | case .save:
57 | return "Save"
58 | case .ocr:
59 | return "OCR"
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Scanverter/Models/Folder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Combine
3 | import PDFKit
4 |
5 | struct Folder: Codable {
6 | var name: String
7 | var date: Date
8 | var isPasswordProtected: Bool
9 | var uid: UUID
10 | var files: [DocFile]
11 | var selected: Bool = false
12 |
13 | @discardableResult
14 | func save() -> AnyPublisher {
15 | DataManager.save(self, withName: uid.uuidString)
16 | }
17 |
18 | @discardableResult
19 | func delete(isDirectory: Bool = false) -> AnyPublisher {
20 | return DataManager.delete(file: uid.uuidString, isDirectory: isDirectory)
21 | }
22 |
23 | mutating func toggleSelection() {
24 | selected.toggle()
25 | }
26 | }
27 |
28 | extension Folder: Equatable, Hashable {
29 | static func == (lhs: Folder, rhs: Folder) -> Bool {
30 | return lhs.uid == rhs.uid
31 | }
32 |
33 | func hash(into hasher: inout Hasher) {
34 | hasher.combine(uid)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Scanverter/Models/Language.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Languages: Codable {
4 | var items: [Language]
5 |
6 | init(items: [Language]) {
7 | self.items = items
8 | }
9 |
10 | enum CodingKeys: String, CodingKey, CaseIterable {
11 | case items = "languages"
12 | }
13 |
14 | public init(from decoder: Decoder) throws {
15 | let container = try decoder.container(keyedBy: CodingKeys.self)
16 | self.items = container.decodeSafelyIfPresent([Language].self, forKey: .items) ?? []
17 | }
18 | }
19 |
20 |
21 | public struct Language: Codable {
22 | public let language: String?
23 | public let name: String?
24 |
25 | init(language: String?, name: String?) {
26 | self.language = language
27 | self.name = name
28 | }
29 |
30 | enum CodingKeys: String, CodingKey, CaseIterable {
31 | case language, name
32 | }
33 |
34 | public init(from decoder: Decoder) throws {
35 | let container = try decoder.container(keyedBy: CodingKeys.self)
36 | self.language = container.decodeSafelyIfPresent(String.self, forKey: .language)
37 | self.name = container.decodeSafelyIfPresent(String.self, forKey: .name)
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/Scanverter/Models/PDF.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | struct PDF {
4 | var name: String
5 | var page: UIImage
6 | }
7 |
--------------------------------------------------------------------------------
/Scanverter/Models/Photo.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public struct Photo: Identifiable, Equatable {
4 | public var id: String
5 | public var originalData: Data
6 |
7 | public init(id: String = UUID().uuidString, originalData: Data) {
8 | self.id = id
9 | self.originalData = originalData
10 | }
11 | }
12 |
13 | extension Photo {
14 | public var compressedData: Data? {
15 | ImageResizer(targetWidth: 800).resize(data: originalData)?.jpegData(compressionQuality: 0.5)
16 | }
17 | public var thumbnailData: Data? {
18 | ImageResizer(targetWidth: 100).resize(data: originalData)?.jpegData(compressionQuality: 0.5)
19 | }
20 | public var thumbnailImage: UIImage? {
21 | guard let data = thumbnailData else { return nil }
22 | return UIImage(data: data)
23 | }
24 | public var image: UIImage? {
25 | guard let data = compressedData else { return nil }
26 | return UIImage(data: data)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Scanverter/Models/ScannedDoc.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreGraphics
3 |
4 | struct ScannedDoc {
5 | let uid: String = UUID().uuidString
6 | var image: CGImage
7 | var date: Date
8 | }
9 |
10 | extension ScannedDoc: Equatable, Hashable {
11 | static func == (lhs: ScannedDoc, rhs: ScannedDoc) -> Bool {
12 | return lhs.uid == rhs.uid
13 | }
14 |
15 | func hash(into hasher: inout Hasher) {
16 | hasher.combine(uid)
17 | }
18 | }
19 |
20 | extension ScannedDoc: Identifiable {
21 | var id: String {
22 | return uid
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Scanverter/Models/Translation.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Translations: Codable {
4 | var items: [Translation]
5 |
6 | init(items: [Translation]) {
7 | self.items = items
8 | }
9 |
10 | enum CodingKeys: String, CodingKey, CaseIterable {
11 | case items = "translations"
12 | }
13 |
14 | public init(from decoder: Decoder) throws {
15 | let container = try decoder.container(keyedBy: CodingKeys.self)
16 | self.items = container.decodeSafelyIfPresent([Translation].self, forKey: .items) ?? []
17 | }
18 | }
19 |
20 |
21 | public struct Translation: Codable {
22 | var text: String?
23 |
24 | init(text: String?) {
25 | self.text = text
26 | }
27 |
28 | enum CodingKeys: String, CodingKey, CaseIterable {
29 | case text = "translatedText"
30 | }
31 |
32 | public init(from decoder: Decoder) throws {
33 | let container = try decoder.container(keyedBy: CodingKeys.self)
34 | self.text = container.decodeSafelyIfPresent(String.self, forKey: .text)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Scanverter/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Scanverter/Services/BiometricAuthentication.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Combine
3 | import LocalAuthentication
4 |
5 | // TODO: port to combine
6 |
7 | protocol TouchIdentification {
8 | func authenticateUser(_ completion: @escaping (String?) -> ())
9 | }
10 |
11 | class BiometricAuthentication: TouchIdentification {
12 | fileprivate let context = LAContext()
13 |
14 | fileprivate func canEvaluatePolicy() -> Bool {
15 | return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
16 | }
17 |
18 | func authenticateUser(_ completion: @escaping (String?) -> ()) {
19 | guard canEvaluatePolicy() else {
20 | completion("Touch ID not available")
21 | return
22 | }
23 |
24 | let reason = NSLocalizedString("Touch ID needed to enable secure access only", comment: "")
25 | context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { (success, error) in
26 | if error != nil {
27 | let message: String
28 | switch error! {
29 | case LAError.authenticationFailed:
30 | message = "There was a problem verifying your identity."
31 | case LAError.userCancel:
32 | message = "You pressed cancel."
33 | case LAError.userFallback:
34 | message = "You pressed password."
35 | case LAError.biometryNotAvailable:
36 | message = "Face ID/Touch ID is not available."
37 | case LAError.biometryNotEnrolled:
38 | message = "Face ID/Touch ID is not set up."
39 | case LAError.biometryLockout:
40 | message = "Face ID/Touch ID is locked."
41 | default:
42 | message = "Face ID/Touch ID may not be configured"
43 | }
44 | completion(message)
45 | return
46 | }
47 | if success {
48 | DispatchQueue.main.async {
49 | completion(nil)
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Scanverter/Services/ServiceLocator.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | typealias Services = [ObjectIdentifier: Any]
4 |
5 | protocol ServiceLocator {
6 | func resolve(type: T.Type) -> T?
7 | func register(_ service: T)
8 | }
9 |
10 | final class Resolver: ServiceLocator {
11 | public static let shared = Resolver()
12 |
13 | private var services: Services = [:]
14 |
15 | func register(_ service: T) {
16 | services[key(for: T.self)] = service
17 | }
18 |
19 | func resolve(type: T.Type) -> T? {
20 | return services[key(for: T.self)] as? T
21 | }
22 |
23 | private func key(for type: T.Type) -> ObjectIdentifier {
24 | return ObjectIdentifier(T.self)
25 | }
26 | }
27 |
28 | @propertyWrapper
29 | struct Injected {
30 | private var service: T!
31 | public var container: ServiceLocator? = nil
32 | public var name: String?
33 |
34 | public init() {}
35 |
36 | public init(name: String? = nil, container: ServiceLocator? = nil) {
37 | self.name = name
38 | self.container = container
39 | }
40 |
41 | public var wrappedValue: T {
42 | mutating get {
43 | if self.service == nil {
44 | self.service = container?.resolve(type: T.self) ?? Resolver.shared.resolve(type: T.self)
45 | }
46 | return service
47 | }
48 | mutating set {
49 | service = newValue
50 | }
51 | }
52 |
53 | public var projectedValue: Injected {
54 | get { return self }
55 | mutating set { self = newValue }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/addPageButton.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "addPageButton.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "addPageButton@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "addPageButton@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/addPageButton.imageset/addPageButton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/addPageButton.imageset/addPageButton.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/addPageButton.imageset/addPageButton@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/addPageButton.imageset/addPageButton@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/addPageButton.imageset/addPageButton@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/addPageButton.imageset/addPageButton@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/add_button_filled.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "add_button_filled.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "add_button_filled@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/add_button_filled.imageset/add_button_filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/add_button_filled.imageset/add_button_filled.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/add_button_filled.imageset/add_button_filled@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/add_button_filled.imageset/add_button_filled@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/add_folder_button.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "add_folder_button.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "add_folder_button@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/add_folder_button.imageset/add_folder_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/add_folder_button.imageset/add_folder_button.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/add_folder_button.imageset/add_folder_button@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/add_folder_button.imageset/add_folder_button@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/captureButton.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "captureButton.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "captureButton@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "captureButton@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/captureButton.imageset/captureButton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/captureButton.imageset/captureButton.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/captureButton.imageset/captureButton@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/captureButton.imageset/captureButton@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/captureButton.imageset/captureButton@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/captureButton.imageset/captureButton@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/cropButton.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "cropButton.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "cropButton@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "cropButton@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/cropButton.imageset/cropButton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/cropButton.imageset/cropButton.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/cropButton.imageset/cropButton@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/cropButton.imageset/cropButton@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/cropButton.imageset/cropButton@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/cropButton.imageset/cropButton@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/default.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "default.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/default.imageset/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/default.imageset/default.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/deletePageButton.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "deletePageButton.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "deletePageButton@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "deletePageButton@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/deletePageButton.imageset/deletePageButton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/deletePageButton.imageset/deletePageButton.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/deletePageButton.imageset/deletePageButton@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/deletePageButton.imageset/deletePageButton@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/deletePageButton.imageset/deletePageButton@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/deletePageButton.imageset/deletePageButton@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/error.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "error.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/error.imageset/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/error.imageset/error.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/filterButton.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "filterButton.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "filterButton@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "filterButton@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/filterButton.imageset/filterButton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/filterButton.imageset/filterButton.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/filterButton.imageset/filterButton@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/filterButton.imageset/filterButton@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/filterButton.imageset/filterButton@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/filterButton.imageset/filterButton@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/folder_icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "folder_icon.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "folder_icon@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "folder_icon@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/folder_icon.imageset/folder_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/folder_icon.imageset/folder_icon.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/folder_icon.imageset/folder_icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/folder_icon.imageset/folder_icon@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/folder_icon.imageset/folder_icon@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/folder_icon.imageset/folder_icon@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/folder_image.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "folder_image.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "folder_image@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "folder_image@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/folder_image.imageset/folder_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/folder_image.imageset/folder_image.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/folder_image.imageset/folder_image@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/folder_image.imageset/folder_image@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/folder_image.imageset/folder_image@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/folder_image.imageset/folder_image@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/imageSave.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "imageSave.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "imageSave@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "imageSave@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/imageSave.imageset/imageSave.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/imageSave.imageset/imageSave.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/imageSave.imageset/imageSave@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/imageSave.imageset/imageSave@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/imageSave.imageset/imageSave@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/imageSave.imageset/imageSave@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/lock_icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "lock_icon.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "lock_icon@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/lock_icon.imageset/lock_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/lock_icon.imageset/lock_icon.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/lock_icon.imageset/lock_icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/lock_icon.imageset/lock_icon@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/more_options.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "more_options.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/more_options.imageset/more_options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/more_options.imageset/more_options.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/ocrButton.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "ocr_button.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "ocr_button@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "ocr_button@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/ocrButton.imageset/ocr_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/ocrButton.imageset/ocr_button.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/ocrButton.imageset/ocr_button@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/ocrButton.imageset/ocr_button@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/ocrButton.imageset/ocr_button@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/ocrButton.imageset/ocr_button@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/pdfSave.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "pdfSave.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "pdfSave@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "pdfSave@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/pdfSave.imageset/pdfSave.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/pdfSave.imageset/pdfSave.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/pdfSave.imageset/pdfSave@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/pdfSave.imageset/pdfSave@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/pdfSave.imageset/pdfSave@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/pdfSave.imageset/pdfSave@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/pdf_icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "pdf_icon.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "pdf_icon@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "pdf_icon@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/pdf_icon.imageset/pdf_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/pdf_icon.imageset/pdf_icon.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/pdf_icon.imageset/pdf_icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/pdf_icon.imageset/pdf_icon@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/pdf_icon.imageset/pdf_icon@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/pdf_icon.imageset/pdf_icon@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/saveButton.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "saveButton.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "saveButton@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "saveButton@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/saveButton.imageset/saveButton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/saveButton.imageset/saveButton.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/saveButton.imageset/saveButton@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/saveButton.imageset/saveButton@2x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/saveButton.imageset/saveButton@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/saveButton.imageset/saveButton@3x.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/success.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "sucess.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Assets.xcassets/success.imageset/sucess.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Assets.xcassets/success.imageset/sucess.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Images.xcassets/imageWithText.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "imageWithText.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Images.xcassets/imageWithText.imageset/imageWithText.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Images.xcassets/imageWithText.imageset/imageWithText.jpg
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Images.xcassets/slideWithText.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "slideWithText.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Images.xcassets/slideWithText.imageset/slideWithText.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/SupportingFiles/Images.xcassets/slideWithText.imageset/slideWithText.png
--------------------------------------------------------------------------------
/Scanverter/SupportingFiles/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 | NSCameraUsageDescription
50 | Scanverter app needs access to camera to take pictures.
51 | NSPhotoLibraryUsageDescription
52 | Scanverter needs acces to photo library to save pics.
53 | NSPhotoLibraryAddUsageDescription
54 | Scanverter wants to use photo lib additions.
55 | NSFaceIDUsageDescription
56 | The app needs access to Face ID to handle folder secure locking feature
57 |
58 |
59 |
--------------------------------------------------------------------------------
/Scanverter/System/AppContainer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | class AppContainer {
4 | static func makeDefault() {
5 | Resolver.shared.register(PDFGenerator(pages: []) as DocGenerator)
6 | Resolver.shared.register(BiometricAuthentication() as TouchIdentification)
7 | Resolver.shared.register(GoogleTranslation(apiKey: Constants.googleTranslationApiKey) as TranslationService)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Scanverter/System/ScanverterApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct ScanverterApp: App {
5 | init() {
6 | AppContainer.makeDefault()
7 | }
8 |
9 | var body: some Scene {
10 | WindowGroup {
11 | MainTabBarView()
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Scanverter/Views/Alerts/CreateFolderView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CreateFolderView: View {
4 | @Binding var showCreateDirectoryModal: Bool
5 | @Binding var folderName: String
6 | @Binding var isSecured: Bool
7 |
8 | @State private var canUseSecureLock: Bool = false
9 | @State var isEditing: Bool = false
10 |
11 | @State var showAlert: Bool = false
12 | @State var alertTitle: String = ""
13 | @State var alertMessage: String = ""
14 |
15 | private var onDismiss: (() -> Void)? = nil
16 |
17 | init(showCreateDirectoryModal: Binding,
18 | folderName: Binding,
19 | isSecured: Binding,
20 | onDismiss: @escaping () -> Void) {
21 | self._showCreateDirectoryModal = showCreateDirectoryModal
22 | self._folderName = folderName
23 | self._isSecured = isSecured
24 | self.onDismiss = onDismiss
25 | }
26 |
27 | var body: some View {
28 | ZStack {
29 | RoundedRectangle(cornerRadius: 0)
30 | .fill(Color(UIColor.systemBackground))
31 | .frame(width: 360, height: 320)
32 | .zIndex(0)
33 | Circle()
34 | .trim(from: 0.5, to: 1)
35 | .fill(Color(UIColor.systemBackground))
36 | .frame(width: 140, height: 140)
37 | .overlay(Circle()
38 | .stroke(Color(UIColor.label), lineWidth: 3))
39 | .offset(y: -160)
40 | .zIndex(1)
41 | Image("folder_icon")
42 | .resizable()
43 | .foregroundColor(.blue)
44 | .frame(width: 100, height: 100, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
45 | .offset(y: -160)
46 | .zIndex(2)
47 |
48 | VStack {
49 | HStack {
50 | Spacer()
51 | Button(action: {
52 | self.showCreateDirectoryModal = false
53 | }, label: {
54 | Image(systemName: "xmark.circle")
55 | .resizable()
56 | .foregroundColor(Color(UIColor.systemGray))
57 | .frame(width: 40, height: 40, alignment: .leading)
58 |
59 | })
60 | }.offset(x: -40, y: -10)
61 | HStack {
62 | Text("Folder Name")
63 | .font(.headline)
64 | .foregroundColor(Color(UIColor.label))
65 | .fontWeight(/*@START_MENU_TOKEN@*/.bold/*@END_MENU_TOKEN@*/)
66 | .offset(x: 40, y: 0)
67 | Spacer()
68 | }
69 | TextField("Enter name", text: $folderName) { isEditing in
70 | self.isEditing = isEditing
71 | } onCommit: {
72 |
73 | }
74 | .textFieldStyle(RoundedBorderTextFieldStyle())
75 | .autocapitalization(.none)
76 | .disableAutocorrection(true)
77 | .padding(EdgeInsets(top: 0, leading: 40, bottom: 20, trailing: 40))
78 |
79 | HStack {
80 | Text("Secure Lock")
81 | .font(.headline)
82 | .foregroundColor(Color(UIColor.label))
83 | .fontWeight(/*@START_MENU_TOKEN@*/.bold/*@END_MENU_TOKEN@*/)
84 | .offset(x: 40, y: 0)
85 | Spacer()
86 | Toggle("", isOn: $isSecured.didSet(execute: { state in
87 | checkIfCanBeLocked()
88 | }))
89 | .toggleStyle(SwitchToggleStyle(tint: Color(UIColor.systemBlue)))
90 | .offset(x: -40, y: 0)
91 | }
92 |
93 | Button(action: {
94 | withAnimation(.easeOut(duration: 0.25)) {
95 | self.showCreateDirectoryModal = false
96 | onDismiss?()
97 | }
98 | }) {
99 | Text("Create Folder")
100 | }
101 | .frame(width: 290, height: 20)
102 | .padding()
103 | .foregroundColor(.white)
104 | .background(LinearGradient(gradient: Gradient(colors: [Color(UIColor.systemBlue), Color(UIColor.systemBlue)]), startPoint: .leading, endPoint: .trailing))
105 | .cornerRadius(6)
106 | .padding(EdgeInsets(top: 20, leading: 10, bottom: 20, trailing: 10))
107 | }
108 | }
109 | .padding(.top, 60)
110 | }
111 |
112 | private func checkIfCanBeLocked() {
113 | if !canUseSecureLock {
114 | let bioAuthentication = BiometricAuthentication()
115 | bioAuthentication.authenticateUser { (errorMessage) in
116 | if errorMessage != nil {
117 | canUseSecureLock = false
118 | isSecured = false
119 | alertTitle = "Unable To Authenticate 😬"
120 | alertMessage = errorMessage!
121 | showAlert = true
122 | } else {
123 | canUseSecureLock = true
124 | isSecured = true
125 | alertTitle = "Secure Access Enabled 😃"
126 | alertMessage = "To access this folder you will be required to use face id"
127 | showAlert = true
128 | }
129 | }
130 | }
131 | }
132 | }
133 |
134 | struct CreateFolderView_Previews: PreviewProvider {
135 | static var previews: some View {
136 | CreateFolderView(showCreateDirectoryModal: .constant(true),
137 | folderName: .constant(""),
138 | isSecured: .constant(false),
139 | onDismiss: {})
140 | .preferredColorScheme(.light)
141 |
142 | CreateFolderView(showCreateDirectoryModal: .constant(true),
143 | folderName: .constant(""),
144 | isSecured: .constant(false),
145 | onDismiss: {})
146 | .preferredColorScheme(.dark)
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Scanverter/Views/Alerts/CustomAlert.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CustomAlert: View {
4 | @Binding var showingAlert: Bool
5 | @Binding var title: String
6 | @Binding var message: String
7 |
8 | var body: some View {
9 | alert(isPresented: $showingAlert, content: {
10 | Alert(title: Text(title), message: Text(message), dismissButton: .default(Text("OK")))
11 | })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Scanverter/Views/Camera/CameraPreview.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import AVFoundation
3 |
4 | struct CameraPreview_Previews: PreviewProvider {
5 | static var previews: some View {
6 | CameraPreview(session: AVCaptureSession())
7 | .frame(height: 300)
8 | }
9 | }
10 |
11 | struct CameraPreview: UIViewRepresentable {
12 | class VideoPreviewView: UIView {
13 | override class var layerClass: AnyClass {
14 | AVCaptureVideoPreviewLayer.self
15 | }
16 |
17 | var videoPreviewLayer: AVCaptureVideoPreviewLayer {
18 | return layer as! AVCaptureVideoPreviewLayer
19 | }
20 | }
21 |
22 | let session: AVCaptureSession
23 |
24 | func makeUIView(context: Context) -> VideoPreviewView {
25 | let view = VideoPreviewView()
26 | view.backgroundColor = .black
27 | view.videoPreviewLayer.cornerRadius = 0
28 | view.videoPreviewLayer.session = session
29 | view.videoPreviewLayer.connection?.videoOrientation = .portrait
30 |
31 | return view
32 | }
33 |
34 | func updateUIView(_ uiView: VideoPreviewView, context: Context) {
35 |
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Scanverter/Views/Camera/CameraView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Combine
3 | import AVFoundation
4 |
5 | struct CameraView_Previews: PreviewProvider {
6 | static var previews: some View {
7 | CameraView(model: CameraViewModel())
8 | }
9 | }
10 |
11 | struct CameraView: View {
12 | @StateObject var model: CameraViewModel
13 | @State var currentZoomFactor: CGFloat = 1.0
14 |
15 | var captureButton: some View {
16 | Button(action: {
17 | model.capturePhoto()
18 | }, label: {
19 | Circle()
20 | .foregroundColor(.white)
21 | .frame(width: 80, height: 80, alignment: .center)
22 | .overlay(
23 | Circle()
24 | .stroke(Color.black.opacity(0.8), lineWidth: 2)
25 | .frame(width: 65, height: 65, alignment: .center)
26 | )
27 | })
28 | }
29 |
30 | var capturedPhotoThumbnail: some View {
31 | Group {
32 | if model.photo != nil {
33 | Image(uiImage: model.photo.image!)
34 | .resizable()
35 | .aspectRatio(contentMode: .fill)
36 | .frame(width: 60, height: 60)
37 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
38 | .animation(.spring())
39 |
40 | } else {
41 | RoundedRectangle(cornerRadius: 10)
42 | .frame(width: 60, height: 60, alignment: .center)
43 | .foregroundColor(.black)
44 | }
45 | }
46 | }
47 |
48 | var flipCameraButton: some View {
49 | Button(action: {
50 | model.flipCamera()
51 | }, label: {
52 | Circle()
53 | .foregroundColor(Color.gray.opacity(0.2))
54 | .frame(width: 45, height: 45, alignment: .center)
55 | .overlay(
56 | Image(systemName: "camera.rotate.fill")
57 | .foregroundColor(.white))
58 | })
59 | }
60 |
61 | var body: some View {
62 | GeometryReader { reader in
63 | ZStack {
64 | Color.black.edgesIgnoringSafeArea(.all)
65 |
66 | VStack {
67 | Button(action: {
68 | model.switchFlash()
69 | }, label: {
70 | Image(systemName: model.isFlashOn ? "bolt.fill" : "bolt.slash.fill")
71 | .font(.system(size: 20, weight: .medium, design: .default))
72 | })
73 | .accentColor(model.isFlashOn ? .yellow : .white)
74 | .padding(.top, 40)
75 |
76 | CameraPreview(session: model.session)
77 | .gesture(
78 | DragGesture().onChanged { (val) in
79 | if abs(val.translation.height) > abs(val.translation.width) {
80 | let percentage: CGFloat = -(val.translation.height / reader.size.height)
81 | let calc = currentZoomFactor + percentage
82 | let zoomFactor: CGFloat = min(max(calc, 1), 5)
83 | currentZoomFactor = zoomFactor
84 | model.zoom(with: zoomFactor)
85 | }
86 | }
87 | )
88 | .onAppear {
89 | model.configure()
90 | }
91 | .alert(isPresented: $model.showAlertError, content: {
92 | Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
93 | model.alertError.primaryAction?()
94 | }))
95 | })
96 | .overlay(
97 | Group {
98 | if model.willCapturePhoto {
99 | Color.black
100 | }
101 | }
102 | )
103 | .animation(.easeInOut)
104 |
105 | HStack {
106 | capturedPhotoThumbnail
107 | Spacer()
108 | captureButton
109 | .opacity(UIDevice.isSimulator ? 0.4 : 1)
110 | .disabled(UIDevice.isSimulator)
111 | Spacer()
112 | flipCameraButton
113 | .opacity(UIDevice.isSimulator ? 0.4 : 1)
114 | .disabled(UIDevice.isSimulator)
115 | }
116 | .padding(.horizontal, 20)
117 | .padding(.bottom, 20)
118 | }
119 | }
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Scanverter/Views/Camera/ViewModels/CameraViewModel.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Combine
3 | import AVFoundation
4 |
5 | final class CameraViewModel: ObservableObject {
6 | private let service = CameraService()
7 |
8 | @Published var photo: Photo!
9 | @Published var showAlertError = false
10 | @Published var isFlashOn = false
11 | @Published var willCapturePhoto = false
12 | @Published var scannedDocs: [ScannedDoc] = .init()
13 | @Published var isPresentingImagePicker: Bool = false
14 |
15 | var alertError: AlertError!
16 | var session: AVCaptureSession
17 |
18 | public let publisher = PassthroughSubject()
19 | private var showOneLevelIn: Bool = false {
20 | willSet {
21 | publisher.send(showOneLevelIn)
22 | }
23 | }
24 |
25 | private var subscriptions = Set()
26 |
27 | init() {
28 | self.session = service.session
29 |
30 | service.$photo.sink { [weak self] photo in
31 | guard let pic = photo else { return }
32 | self?.photo = pic
33 | guard let img = pic.image?.cgImage else { return }
34 | self?.scannedDocs.append(ScannedDoc(image: img, date: Date()))
35 | self?.showOneLevelIn = true
36 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
37 | self?.showOneLevelIn = true
38 | }
39 | }
40 | .store(in: &self.subscriptions)
41 |
42 | service.$shouldShowAlertView.sink { [weak self] val in
43 | self?.alertError = self?.service.alertError
44 | self?.showAlertError = val
45 | }
46 | .store(in: &self.subscriptions)
47 |
48 | service.$flashMode.sink { [weak self] mode in
49 | self?.isFlashOn = mode == .on
50 | }
51 | .store(in: &self.subscriptions)
52 |
53 | service.$willCapturePhoto.sink { [weak self] val in
54 | self?.willCapturePhoto = val
55 | }
56 | .store(in: &self.subscriptions)
57 | }
58 |
59 | func configure() {
60 | service.checkForPermissions()
61 | service.configure()
62 | }
63 |
64 | func capturePhoto() {
65 | service.capturePhoto()
66 | }
67 |
68 | func flipCamera() {
69 | service.changeCamera()
70 | }
71 |
72 | func zoom(with factor: CGFloat) {
73 | service.set(zoom: factor)
74 | }
75 |
76 | func switchFlash() {
77 | service.flashMode = service.flashMode == .on ? .off : .on
78 | }
79 |
80 | func didSelectImage(_ image: UIImage?) {
81 | isPresentingImagePicker = false
82 | guard image != nil, let picData = image!.pngData() else { return }
83 | self.photo = Photo(originalData: picData)
84 | scannedDocs.append(ScannedDoc(image: image!.cgImage!, date: Date()))
85 | showOneLevelIn = true
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/Cells/EditImageCell.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct EditImageCell_Previews: PreviewProvider {
4 | static var previews: some View {
5 | EditImageCell(dataSource: EditCellDataSource(scannedDoc: ScannedDoc(image: UIImage(named: "slideWithText")!.cgImage!, date: Date())))
6 | }
7 | }
8 |
9 | struct EditImageCell: View {
10 | @StateObject var dataSource: EditCellDataSource
11 |
12 | var body: some View {
13 | VStack(alignment: .center) {
14 | Image(uiImage: UIImage(cgImage: dataSource.scannedDoc.image))
15 | .resizable()
16 | .aspectRatio(contentMode: .fit)
17 | .padding()
18 |
19 | Text(dataSource.scannedDoc.date.toString)
20 | .foregroundColor(Color(UIColor.systemGray))
21 | .font(.headline)
22 | }
23 | .padding()
24 | .frame(width: UIScreen.main.bounds.width - 40, height: UIScreen.main.bounds.height - 80)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/Cells/EditToolCell.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct EditToolCell_Previews: PreviewProvider {
4 | static var previews: some View {
5 | let tools = [
6 | // EditTool.init(.add, image: UIImage(named: "addPageButton")!),
7 | EditTool.init(.crop, image: UIImage(named: "cropButton")!),
8 | EditTool.init(.delete, image: UIImage(named: "deletePageButton")!),
9 | EditTool.init(.save(.image), image: UIImage(named: "imageSave")!),
10 | EditTool.init(.save(.pdf), image: UIImage(named: "pdfSave")!),
11 | EditTool.init(.ocr, image: UIImage(named: "ocrButton")!)
12 | ]
13 | EditToolCell(dataSource: EditToolCellDataSource(tool: EditTool(tools.last!.type, image: tools.last!.image)),
14 | photoDataSource: PhotoCollectionDataSource(scannedDocs: Constants.mockedDocs))
15 | }
16 | }
17 |
18 | struct EditToolCell: View {
19 | @StateObject var dataSource: EditToolCellDataSource
20 | @StateObject var photoDataSource: PhotoCollectionDataSource
21 |
22 | var body: some View {
23 | VStack(alignment: .center) {
24 | Image(uiImage: dataSource.editTool.image)
25 | .resizable()
26 | .aspectRatio(contentMode: .fit)
27 | .frame(maxWidth: 60, maxHeight: 60)
28 | Text(dataSource.editTool.type.tool)
29 | .font(.headline)
30 | .fontWeight(.semibold)
31 | .foregroundColor(Color(UIColor.themeIndigoDark()))
32 | .offset(x: 0, y: -10)
33 | }.onTapGesture {
34 | switch dataSource.editTool.type {
35 | case .add:
36 | print("Add doc")
37 | case .crop:
38 | photoDataSource.makeCrop()
39 | case .delete:
40 | print("delete doc")
41 | photoDataSource.deletePage()
42 | case .save(let type):
43 | print("save")
44 | photoDataSource.save(as: type)
45 | case .ocr:
46 | photoDataSource.makeTextRecognition()
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/Cells/FileGridCell.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct FileGridCell_Previews: PreviewProvider {
4 | static var previews: some View {
5 | FileGridCell(dataSource: FileCellDataSource(file: DocFile(name: "Some pdf",
6 | date: Date(),
7 | uid: UUID(),
8 | parent: Folder(name: "Some folder",
9 | date: Date(),
10 | isPasswordProtected: false,
11 | uid: UUID(),
12 | files: []))))
13 | }
14 | }
15 |
16 | struct FileGridCell: View {
17 | @StateObject var dataSource: FileCellDataSource
18 | var body: some View {
19 | VStack(alignment: .center) {
20 | Image(systemName: "newspaper.fill")
21 | .resizable()
22 | .scaledToFit()
23 | .foregroundColor(Color(UIColor.systemBlue))
24 | .frame(maxWidth: 80, maxHeight: 60)
25 | Text(dataSource.file.name.split(separator: ".").dropLast().joined())
26 | .font(.subheadline)
27 | .fontWeight(.semibold)
28 | .lineLimit(1)
29 | .truncationMode(.tail)
30 | .foregroundColor(Color(UIColor.label))
31 | .padding([.leading, .trailing], 5)
32 | VStack(alignment: .center) {
33 | Text(dataSource.file.date.toString)
34 | .font(.caption)
35 | .fontWeight(.regular)
36 | .foregroundColor(Color(UIColor.systemGray))
37 | .padding([.bottom], 5)
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/Cells/FileListCell.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct FileListCell_Previews: PreviewProvider {
4 | static var previews: some View {
5 | FileListCell(dataSource: FileCellDataSource(file: DocFile(name: "Some pdf",
6 | date: Date(),
7 | uid: UUID(),
8 | parent: Folder(name: "Some folder",
9 | date: Date(),
10 | isPasswordProtected: false,
11 | uid: UUID(),
12 | files: []))))
13 | }
14 | }
15 |
16 | struct FileListCell: View {
17 | @StateObject var dataSource: FileCellDataSource
18 |
19 | var body: some View {
20 | HStack(alignment: .center) {
21 | Image(systemName: "newspaper.fill")
22 | .resizable()
23 | .foregroundColor(Color(UIColor.systemBlue))
24 | .frame(maxWidth: 80, maxHeight: 60)
25 | VStack(alignment: .leading, spacing: 15) {
26 | Text(dataSource.file.name)
27 | .font(.headline)
28 | .fontWeight(.semibold)
29 | .foregroundColor(Color(UIColor.label))
30 | .padding([.top, .leading, .trailing, .bottom], 5)
31 | .offset(x: 0, y: /*@START_MENU_TOKEN@*/10.0/*@END_MENU_TOKEN@*/)
32 | HStack {
33 | Text(dataSource.file.date.toString)
34 | .font(.caption)
35 | .fontWeight(.regular)
36 | .foregroundColor(Color(UIColor.systemGray))
37 | .padding([.leading, .bottom], 5)
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/Cells/GridCell.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct GridCell_Previews: PreviewProvider {
4 | static var previews: some View {
5 | GridCell(dataSource: FolderCellDataSource(folder: Folder(name: "TestFolder", date: Date(), isPasswordProtected: false, uid: UUID(), files: [])), folderSelector: FolderSelector())
6 | }
7 | }
8 |
9 | struct GridCell: View {
10 | @StateObject var dataSource: FolderCellDataSource
11 |
12 | @State private var isSelected: Bool = false
13 |
14 | var folderSelector: FolderSelector
15 |
16 | var body: some View {
17 | VStack(alignment: .center) {
18 | Image(systemName: "folder.fill")
19 | .resizable()
20 | .scaledToFit()
21 | .foregroundColor(isSelected ? Color(UIColor.systemRed) : Color(UIColor.systemBlue))
22 | .frame(maxWidth: 80, maxHeight: 60)
23 | Text(dataSource.folder.name)
24 | .font(.subheadline)
25 | .fontWeight(.semibold)
26 | .lineLimit(1)
27 | .truncationMode(.tail)
28 | .foregroundColor(Color(UIColor.label))
29 | .padding([.leading, .trailing], 5)
30 | VStack(alignment: .center) {
31 | Text(dataSource.folder.date.toString)
32 | .font(.caption)
33 | .fontWeight(.regular)
34 | .foregroundColor(Color(UIColor.systemGray))
35 | Text("(\(dataSource.folder.files.count) \(dataSource.folder.files.count == 1 ? "item" : "items"))")
36 | .font(.caption)
37 | .fontWeight(.regular)
38 | .foregroundColor(Color(UIColor.systemGray))
39 | .padding([.bottom], 5)
40 | }
41 | }
42 | .onReceive(folderSelector.publisher) { selection in
43 | isSelected = selection.id == dataSource.folder.uid.uuidString && selection.selected
44 | }
45 | }
46 | }
47 |
48 |
49 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/Cells/ListCell.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ListCell_Previews: PreviewProvider {
4 | static var previews: some View {
5 | ListCell(dataSource: FolderCellDataSource(folder: Folder(name: "TestFolder", date: Date(), isPasswordProtected: false, uid: UUID(), files: [])), folderSelector: FolderSelector())
6 | }
7 | }
8 |
9 | struct ListCell: View {
10 | @StateObject var dataSource: FolderCellDataSource
11 |
12 | @State private var isSelected: Bool = false
13 |
14 | var folderSelector: FolderSelector
15 |
16 | var body: some View {
17 | HStack(alignment: .center) {
18 | Image(systemName: "folder.fill")
19 | .resizable()
20 | .foregroundColor(isSelected ? Color(UIColor.systemRed) : Color(UIColor.systemBlue))
21 | .frame(maxWidth: 80, maxHeight: 60)
22 | VStack(alignment: .leading, spacing: 15) {
23 | Text(dataSource.folder.name)
24 | .font(.headline)
25 | .fontWeight(.semibold)
26 | .foregroundColor(Color(UIColor.label))
27 | .padding([.top, .leading, .trailing, .bottom], 5)
28 | .offset(x: 0, y: /*@START_MENU_TOKEN@*/10.0/*@END_MENU_TOKEN@*/)
29 | HStack {
30 | Text(dataSource.folder.date.toString)
31 | .font(.caption)
32 | .fontWeight(.regular)
33 | .foregroundColor(Color(UIColor.systemGray))
34 | .padding([.leading, .bottom], 5)
35 | Text("(\(dataSource.folder.files.count) \(dataSource.folder.files.count == 1 ? "item" : "items"))")
36 | .font(.caption)
37 | .fontWeight(.regular)
38 | .foregroundColor(Color(UIColor.systemGray))
39 | .padding([.trailing, .bottom], 5)
40 | }
41 | }
42 | }
43 | .onReceive(folderSelector.publisher) { selection in
44 | isSelected = selection.id == dataSource.folder.uid.uuidString && selection.selected
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/Custom/Presentation/EditorView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import PDFKit
3 | import Combine
4 |
5 | struct EditorView: View {
6 | @EnvironmentObject var navStack: NavigationStack
7 | @StateObject var dataSource: PhotoCollectionDataSource
8 |
9 | @State var hudVisible = false
10 | @State var hudConfig = CustomProgressConfig()
11 | @Binding var backToCamera: Bool
12 |
13 | @State var goToOCRResults: Bool = false
14 | @State var recognizedText: String = ""
15 | @State var isPresentingFolderChooser: Bool = false
16 |
17 | @State private var pdfToSave: PDFDocument?
18 | @State private var folderToSaveIn: Folder?
19 |
20 | @State private var saveRequest: AnyCancellable?
21 |
22 | var body: some View {
23 | ZStack {
24 | VStack {
25 | if dataSource.scannedDocs.isEmpty {
26 | VStack {
27 | HStack(alignment: .center) {
28 | Text($dataSource.pageTitle.wrappedValue)
29 | .foregroundColor(Color(UIColor.label))
30 | .padding(.top, 40)
31 | }
32 | Spacer()
33 | }
34 | .padding(EdgeInsets(top: 20, leading: 4, bottom: 10, trailing: 4))
35 | } else {
36 | PhotoPager(dataSource: dataSource,
37 | cells: .init(array: dataSource.scannedDocs.map({ EditImageCell(dataSource: EditCellDataSource(scannedDoc: $0)) })))
38 | }
39 |
40 | EditorTabBar(dataSource: EditTabBarDataSource(tools: Constants.editTools), photoDataSource: dataSource)
41 | }
42 | goBackButton
43 | .fullScreenCover(isPresented: $dataSource.isPresentingImagePicker, content: {
44 | ImagePicker(sourceType: dataSource.sourceType, completionHandler: dataSource.didSelectImage)
45 | })
46 | showPhotoLibrary
47 | .sheet(isPresented: $isPresentingFolderChooser, onDismiss: {
48 | savePDFDocOnDismiss(asFileNamed: "\(folderToSaveIn?.name ?? "folder")-\(Date().toFileNameString)")
49 | }) { FoldersScreen(selectedFolder: $folderToSaveIn, calledFromSaving: true) }
50 | CustomProgressView($hudVisible, config: hudConfig)
51 | PushView(destination: OCRResultsView(message: $recognizedText), isActive: $goToOCRResults) {
52 | EmptyView()
53 | }.hidden()
54 | }
55 | .onReceive(dataSource.progressPublisher) { data in
56 | switch data.showProgressType {
57 | case .error:
58 | hudConfig.title = "Error!"
59 | hudConfig.errorImage = "xmark.circle"
60 | case .info:
61 | hudConfig.title = "Info"
62 | hudConfig.warningImage = "info.circle"
63 | case .success:
64 | hudConfig.title = "Success"
65 | hudConfig.successImage = "checkmark.circle"
66 | }
67 | hudConfig.caption = data.progressViewMessage
68 | hudVisible = data.showProgressView
69 | }
70 | .onReceive(dataSource.pdfGenerationPublisher) { data in
71 | pdfToSave = data.pdfDoc
72 | isPresentingFolderChooser = data.isPresentingFolderChooser
73 | }
74 | .onReceive(dataSource.dismissPublisher) { _ in
75 | print("Should be poped to camera")
76 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
77 | backToCamera = true
78 | navStack.pop()
79 | }
80 | }
81 | .onReceive(dataSource.ocrResultPublisher) { result in
82 | recognizedText = result.recognizedText
83 | goToOCRResults = result.goToOCRResults
84 | }
85 | .onReceive(dataSource.selectionPublisher, perform: { _ in
86 | dataSource.currentPage = dataSource.selection
87 | print("Selection \(dataSource.selection), current page \(dataSource.currentPage)")
88 | })
89 | .onDisappear {
90 | saveRequest?.cancel()
91 | saveRequest = nil
92 | }
93 | .edgesIgnoringSafeArea(.all)
94 | }
95 |
96 | private var showPhotoLibrary: some View {
97 | HStack {
98 | Spacer()
99 | Button(action: {
100 | dataSource.isPresentingImagePicker = true
101 | }, label: {
102 | Image(systemName: "photo.on.rectangle.angled")
103 | .resizable()
104 | .frame(width: 40, height: 30, alignment: .leading)
105 | .foregroundColor(.secondary)
106 | })
107 | }.offset(x: -20, y: -380)
108 | }
109 |
110 | private var goBackButton: some View {
111 | HStack {
112 | Spacer()
113 | Button(action: {
114 | dataSource.recognitionRequest?.cancel()
115 | backToCamera = true
116 | navStack.pop()
117 | }, label: {
118 | HStack {
119 | Text("Back to camera")
120 | .font(.headline)
121 | .foregroundColor(.secondary)
122 | }
123 | })
124 | }.offset(x: -275, y: -380)
125 | }
126 |
127 | private func savePDFDocOnDismiss(asFileNamed named: String) {
128 | if let doc = pdfToSave, let folder = folderToSaveIn {
129 | saveRequest = dataSource.save(pdfDoc: doc, namedAs: named, in: folder)
130 | .receive(on: DispatchQueue.main)
131 | .sink { saved in
132 | DispatchQueue.main.async {
133 | self.dataSource.selectedImages.removeAll()
134 | }
135 | print("pdf file saved in folder")
136 | }
137 | }
138 | }
139 | }
140 |
141 | struct PhotoPager: View {
142 | @ObservedObject var dataSource: PhotoCollectionDataSource
143 | @ObservedObject var cells: ObservableArray
144 |
145 | var body: some View {
146 | return VStack {
147 | HStack(alignment: .center) {
148 | Text($dataSource.pageTitle.wrappedValue)
149 | .foregroundColor(Color(UIColor.label))
150 | .padding(.top, 40)
151 | }
152 | Pager(pages: cells, currentPage: $dataSource.selection)
153 | .tabViewStyle(PageTabViewStyle())
154 | .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
155 | }
156 | .padding(EdgeInsets(top: 20, leading: 4, bottom: 10, trailing: 4))
157 | .border(Color(UIColor.systemPurple).opacity(0.8), width: 1)
158 | }
159 | }
160 |
161 | struct EditorView_Previews: PreviewProvider {
162 | static var mockedDocs: [ScannedDoc] {
163 | var docs: [ScannedDoc] = .init()
164 | let names = ["imageWithText", "slideWithText"]
165 | for _ in 0..<2 {
166 | names.forEach {
167 | docs.append(ScannedDoc(image: UIImage(named: $0)!.cgImage!, date: Date()))
168 | }
169 | }
170 | return docs
171 | }
172 |
173 | static var previews: some View {
174 | EditorView(dataSource: PhotoCollectionDataSource(scannedDocs: mockedDocs), backToCamera: .constant(false))
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/Custom/Presentation/OCRResultsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct OCRResultsView: View {
4 | @EnvironmentObject var navStack: NavigationStack
5 |
6 | @Binding var message: String
7 | @State private var textStyle = UIFont.TextStyle.title2
8 |
9 | var body: some View {
10 | VStack {
11 | ZStack(alignment: .topTrailing) {
12 | TextView(text: $message, textStyle: $textStyle)
13 | .padding(.horizontal)
14 | .padding(.top, 60)
15 | .padding(.leading, 40)
16 | HStack {
17 | Button(action: {
18 | self.navStack.pop()
19 | }, label: {
20 | Image(systemName: "chevron.backward")
21 | .imageScale(.large)
22 | .frame(width: 40, height: 40)
23 | .foregroundColor(Color(UIColor.systemBackground))
24 | .background(Color(UIColor.systemBlue))
25 | .clipShape(Circle())
26 | })
27 | .padding()
28 | Spacer()
29 | Button(action: {
30 | self.textStyle = (self.textStyle == .title2) ? .headline : .title2
31 | }) {
32 | Image(systemName: "textformat")
33 | .imageScale(.large)
34 | .frame(width: 40, height: 40)
35 | .foregroundColor(Color(UIColor.systemBackground))
36 | .background(Color(UIColor.systemBlue))
37 | .clipShape(Circle())
38 |
39 | }
40 | .padding()
41 | }
42 | }
43 | }
44 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
45 | .background(Color(.systemBackground).opacity(0.2))
46 | .navigationBarTitle("Results", displayMode: .inline)
47 | .edgesIgnoringSafeArea(.bottom)
48 | }
49 | }
50 |
51 | struct OCRResultsView_Previews: PreviewProvider {
52 | static var previews: some View {
53 | OCRResultsView(message: .constant("But how do you present it? Since SwiftUI is a declarative UI framework, you don’t present is by reacting to a user action in a callback. Instead, you declare under which state it should be presented. Remember: A SwiftUI view is a function of its state."))
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/Custom/Presentation/PhotoCollectionView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct PhotoCollectionView: View {
4 | @StateObject var dataSource: PhotoCollectionDataSource
5 |
6 | typealias DocRow = CollectionRow
7 | typealias ToolRow = CollectionRow
8 |
9 | @State var docRows: [DocRow] = .init()
10 | @State var toolsRow: [ToolRow] = .init()
11 |
12 | private func composeElements() {
13 | // let result = dataSource.scannedDocs.chunked(into: 5)
14 | let result = mockedDocs.chunked(into: 5)
15 | var section = 0
16 | result.forEach {
17 | docRows.append(DocRow(section: section, items: $0))
18 | section += 1
19 | }
20 | }
21 |
22 | private func composeTools() {
23 | toolsRow.append(ToolRow(section: 0, items: dataSource.tools))
24 | }
25 |
26 | private var mockedDocs: [ScannedDoc] {
27 | var docs: [ScannedDoc] = .init()
28 | let names = ["imageWithText", "slideWithText"]
29 | for _ in 0..<2 {
30 | names.forEach {
31 | docs.append(ScannedDoc(image: UIImage(named: $0)!.cgImage!, date: Date()))
32 | }
33 | }
34 | return docs
35 | }
36 |
37 | var body: some View {
38 | VStack {
39 | CollectionView(rows: docRows) { sectionIndex, layoutEnvironment in
40 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(1.0))
41 | let item = NSCollectionLayoutItem(layoutSize: itemSize)
42 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5))
43 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3)
44 | let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
45 | heightDimension: .absolute(40)),
46 | elementKind: UICollectionView.elementKindSectionHeader,
47 | alignment: .topLeading)
48 | let section = NSCollectionLayoutSection(group: group)
49 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
50 | section.interGroupSpacing = 10
51 | section.boundarySupplementaryItems = [header]
52 | return section
53 | } cell: { indexPath, item in
54 | EditImageCell(dataSource: EditCellDataSource(scannedDoc: item))
55 | } supplementaryView: { kind, indexPath in
56 | Text("Section \(indexPath.section)")
57 | }
58 | .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height - 90)
59 | .ignoresSafeArea(.all)
60 | .onAppear(perform: composeElements)
61 |
62 | Spacer()
63 |
64 | CollectionView(rows: toolsRow) { sectionIndex, layoutEnvironment in
65 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25), heightDimension: .fractionalHeight(1.0))
66 | let item = NSCollectionLayoutItem(layoutSize: itemSize)
67 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1))
68 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
69 | let section = NSCollectionLayoutSection(group: group)
70 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
71 | section.orthogonalScrollingBehavior = .none
72 | return section
73 | } cell: { indexPath, item in
74 | EditToolCell(dataSource: EditToolCellDataSource(tool: item), photoDataSource: dataSource)
75 | } supplementaryView: { kind, indexPath in
76 | Text("Section \(indexPath.section)")
77 | }
78 | .frame(maxWidth: .infinity, maxHeight: 90)
79 | .ignoresSafeArea(.all)
80 | .onAppear(perform: composeTools)
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/DataSource/EditCellDataSource.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | final class EditCellDataSource: ObservableObject {
4 | @Published private(set) var scannedDoc: ScannedDoc
5 |
6 | init(scannedDoc: ScannedDoc) {
7 | self.scannedDoc = scannedDoc
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/DataSource/EditToolCellDataSource.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | final class EditToolCellDataSource: ObservableObject {
4 | @Published private(set) var editTool: EditTool
5 |
6 | init(tool: EditTool) {
7 | self.editTool = tool
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/DataSource/FileCellDataSource.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | final class FileCellDataSource: ObservableObject {
4 | @Published private(set) var file: DocFile
5 |
6 | init(file: DocFile) {
7 | self.file = file
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Scanverter/Views/Collections/DataSource/FolderCellDataSource.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | final class FolderCellDataSource: ObservableObject {
4 | @Published private(set) var folder: Folder
5 |
6 | init(folder: Folder) {
7 | self.folder = folder
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/ActivityIndicator.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ActivityIndicator: UIViewRepresentable {
4 | typealias UIViewType = UIActivityIndicatorView
5 |
6 | let style: UIActivityIndicatorView.Style
7 |
8 | func makeUIView(context: UIViewRepresentableContext) -> ActivityIndicator.UIViewType {
9 | return UIActivityIndicatorView(style: style)
10 | }
11 |
12 | func updateUIView(_ uiView: ActivityIndicator.UIViewType, context: UIViewRepresentableContext) {
13 | uiView.startAnimating()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/CustomProgressConfig.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct CustomProgressConfig: Hashable {
4 | var type = CustomProgressType.loading
5 | var title: String?
6 | var caption: String?
7 |
8 | var minSize: CGSize
9 | var cornerRadius: CGFloat
10 |
11 | var backgroundColor: Color
12 |
13 | var titleForegroundColor: Color
14 | var captionForegroundColor: Color
15 |
16 | var shadowColor: Color
17 | var shadowRadius: CGFloat
18 |
19 | var borderColor: Color
20 | var borderWidth: CGFloat
21 |
22 | var lineWidth: CGFloat
23 |
24 | // indefinite animated view and image share the size
25 | var imageViewSize: CGSize
26 | var imageViewForegroundColor: Color
27 |
28 | var successImage: String
29 | var warningImage: String
30 | var errorImage: String
31 |
32 | // Auto hide
33 | var shouldAutoHide: Bool
34 | var allowsTapToHide: Bool
35 | var autoHideInterval: TimeInterval
36 |
37 | // Haptics
38 | var hapticsEnabled: Bool
39 |
40 | public init(
41 | type: CustomProgressType = .loading,
42 | title: String? = nil,
43 | caption: String? = nil,
44 | minSize: CGSize = CGSize(width: 100.0, height: 100.0),
45 | cornerRadius: CGFloat = 12.0,
46 | backgroundColor: Color = .clear,
47 | titleForegroundColor: Color = .primary,
48 | captionForegroundColor: Color = .secondary,
49 | shadowColor: Color = .clear,
50 | shadowRadius: CGFloat = 0.0,
51 | borderColor: Color = .clear,
52 | borderWidth: CGFloat = 0.0,
53 | lineWidth: CGFloat = 10.0,
54 | imageViewSize: CGSize = CGSize(width: 100, height: 100),
55 | imageViewForegroundColor: Color = .primary,
56 | successImage: String = "checkmark.circle",
57 | warningImage: String = "exclamationmark.circle",
58 | errorImage: String = "xmark.circle",
59 | shouldAutoHide: Bool = false,
60 | allowsTapToHide: Bool = false,
61 | autoHideInterval: TimeInterval = 10.0,
62 | hapticsEnabled: Bool = true
63 | ) {
64 | self.type = type
65 |
66 | self.title = title
67 | self.caption = caption
68 |
69 | self.minSize = minSize
70 | self.cornerRadius = cornerRadius
71 |
72 | self.backgroundColor = backgroundColor
73 |
74 | self.titleForegroundColor = titleForegroundColor
75 | self.captionForegroundColor = captionForegroundColor
76 |
77 | self.shadowColor = shadowColor
78 | self.shadowRadius = shadowRadius
79 |
80 | self.borderColor = borderColor
81 | self.borderWidth = borderWidth
82 |
83 | self.lineWidth = lineWidth
84 |
85 | self.imageViewSize = imageViewSize
86 | self.imageViewForegroundColor = imageViewForegroundColor
87 |
88 | self.successImage = successImage
89 | self.warningImage = warningImage
90 | self.errorImage = errorImage
91 |
92 | self.shouldAutoHide = shouldAutoHide
93 | self.allowsTapToHide = allowsTapToHide
94 | self.autoHideInterval = autoHideInterval
95 |
96 | self.hapticsEnabled = hapticsEnabled
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/CustomProgressView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftUIVisualEffects
3 |
4 | public enum CustomProgressType {
5 | case loading
6 | case success
7 | case warning
8 | case error
9 | }
10 |
11 | private struct IndefiniteAnimatedView: View {
12 | var animatedViewSize: CGSize
13 | var animatedViewForegroundColor: Color
14 |
15 | var lineWidth: CGFloat
16 |
17 | @State private var isAnimating = false
18 |
19 | private var foreverAnimation: Animation {
20 | Animation.linear(duration: 2.0)
21 | .repeatForever(autoreverses: false)
22 | }
23 |
24 | var body: some View {
25 | let gradient = Gradient(colors: [animatedViewForegroundColor, .clear])
26 | let radGradient = AngularGradient(gradient: gradient, center: .center, angle: .degrees(-5))
27 |
28 | Circle()
29 | .trim(from: 0.0, to: 0.97)
30 | .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
31 | .fill(radGradient)
32 | .frame(width: animatedViewSize.width-lineWidth/2, height: animatedViewSize.height-lineWidth/2)
33 | .rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
34 | .animation(self.isAnimating ? foreverAnimation : .default)
35 | .padding(lineWidth/2)
36 | .onAppear {
37 | self.isAnimating = true
38 | }
39 | .onDisappear {
40 | self.isAnimating = false
41 | }
42 | }
43 | }
44 |
45 | private struct ImageView: View {
46 | var type: CustomProgressType
47 |
48 | var imageViewSize: CGSize
49 | var imageViewForegroundColor: Color
50 |
51 | var successImage: String
52 | var warningImage: String
53 | var errorImage: String
54 |
55 | var body: some View {
56 | imageForHUDType?
57 | .resizable()
58 | .frame(width: imageViewSize.width, height: imageViewSize.height)
59 | .foregroundColor(imageViewForegroundColor.opacity(0.8))
60 | }
61 |
62 | var imageForHUDType: Image? {
63 | switch type {
64 | case .success:
65 | return Image(systemName: successImage)
66 | case .warning:
67 | return Image(systemName: warningImage)
68 | case .error:
69 | return Image(systemName: errorImage)
70 | default:
71 | return nil
72 | }
73 | }
74 | }
75 |
76 | private struct LabelView: View {
77 | var title: String?
78 | var caption: String?
79 |
80 | var body: some View {
81 | VStack(spacing: 4) {
82 | if let title = title {
83 | Text(title)
84 | .font(.system(size: 21.0, weight: .semibold))
85 | .lineLimit(1)
86 | .foregroundColor(.primary)
87 | }
88 | if let caption = caption {
89 | Text(caption)
90 | .lineLimit(2)
91 | .font(.headline)
92 | .foregroundColor(.secondary)
93 | }
94 | }
95 | .multilineTextAlignment(.center)
96 | .vibrancyEffect()
97 | .vibrancyEffectStyle(.fill)
98 | }
99 | }
100 |
101 | public struct CustomProgressView: View {
102 | @Binding var isVisible: Bool
103 | var config: CustomProgressConfig
104 |
105 | @Environment(\.colorScheme) private var colorScheme
106 |
107 | public init(_ isVisible: Binding, config: CustomProgressConfig) {
108 | self._isVisible = isVisible
109 | self.config = config
110 | }
111 |
112 | public var body: some View {
113 | let hideTimer = Timer.publish(every: config.autoHideInterval, on: .main, in: .common).autoconnect()
114 |
115 | GeometryReader { geometry in
116 | ZStack {
117 | if isVisible {
118 | config.backgroundColor
119 | .edgesIgnoringSafeArea(.all)
120 |
121 | ZStack {
122 | Color.white
123 | .blurEffect()
124 | .blurEffectStyle(.systemChromeMaterial)
125 |
126 | VStack(spacing: 20) {
127 | if config.type == .loading {
128 | IndefiniteAnimatedView(animatedViewSize: config.imageViewSize,
129 | animatedViewForegroundColor: config.imageViewForegroundColor,
130 | lineWidth: config.lineWidth)
131 | } else {
132 | ImageView(type: config.type,
133 | imageViewSize: config.imageViewSize,
134 | imageViewForegroundColor: config.imageViewForegroundColor,
135 | successImage: config.successImage,
136 | warningImage: config.warningImage,
137 | errorImage: config.errorImage)
138 | }
139 | LabelView(title: config.title, caption: config.caption)
140 | }.padding()
141 | }
142 | .overlay(
143 | // Fix required since .border can not be used with
144 | // RoundedRectangle clip shape
145 | RoundedRectangle(cornerRadius: config.cornerRadius)
146 | .stroke(config.borderColor, lineWidth: config.borderWidth)
147 | )
148 | .aspectRatio(1, contentMode: .fit)
149 | .padding(geometry.size.width / 7)
150 | .shadow(color: config.shadowColor, radius: config.shadowRadius)
151 | }
152 | }
153 | .animation(.spring())
154 | .onTapGesture {
155 | if config.allowsTapToHide {
156 | withAnimation {
157 | isVisible = false
158 | }
159 | }
160 | }
161 | .onReceive(hideTimer) { _ in
162 | if config.shouldAutoHide {
163 | withAnimation {
164 | isVisible = false
165 | }
166 | }
167 | // Only one call required
168 | hideTimer.upstream.connect().cancel()
169 | }
170 | .onAppear {
171 | if config.hapticsEnabled {
172 | generateHapticNotification(for: config.type)
173 | }
174 | }
175 | }
176 | }
177 |
178 | func generateHapticNotification(for type: CustomProgressType) {
179 | let generator = UINotificationFeedbackGenerator()
180 | generator.prepare()
181 |
182 | switch type {
183 | case .success:
184 | generator.notificationOccurred(.success)
185 | case .warning:
186 | generator.notificationOccurred(.warning)
187 | case .error:
188 | generator.notificationOccurred(.error)
189 | default:
190 | return
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/CustomTextView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct TextView: UIViewRepresentable {
4 |
5 | @Binding var text: String
6 | @Binding var textStyle: UIFont.TextStyle
7 |
8 | func makeUIView(context: Context) -> UITextView {
9 | let textView = UITextView()
10 |
11 | textView.delegate = context.coordinator
12 | textView.font = UIFont.preferredFont(forTextStyle: textStyle)
13 | textView.autocapitalizationType = .sentences
14 | textView.isSelectable = true
15 | textView.isUserInteractionEnabled = true
16 | textView.textAlignment = .justified
17 |
18 | return textView
19 | }
20 |
21 | func updateUIView(_ uiView: UITextView, context: Context) {
22 | uiView.text = text
23 | uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
24 | }
25 |
26 | func makeCoordinator() -> Coordinator {
27 | Coordinator($text)
28 | }
29 |
30 | class Coordinator: NSObject, UITextViewDelegate {
31 | var text: Binding
32 |
33 | init(_ text: Binding) {
34 | self.text = text
35 | }
36 |
37 | func textViewDidChange(_ textView: UITextView) {
38 | self.text.wrappedValue = textView.text
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/ImagePicker.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import UIKit
3 |
4 | struct ImagePicker: UIViewControllerRepresentable {
5 | typealias UIViewControllerType = UIImagePickerController
6 | typealias SourceType = UIImagePickerController.SourceType
7 |
8 | let sourceType: SourceType
9 | let completionHandler: (UIImage?) -> Void
10 |
11 | func makeUIViewController(context: Context) -> UIImagePickerController {
12 | let viewController = UIImagePickerController()
13 | viewController.delegate = context.coordinator
14 | viewController.sourceType = sourceType
15 | return viewController
16 | }
17 |
18 | func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
19 |
20 | func makeCoordinator() -> Coordinator {
21 | return Coordinator(completionHandler: completionHandler)
22 | }
23 |
24 | final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
25 | let completionHandler: (UIImage?) -> Void
26 |
27 | init(completionHandler: @escaping (UIImage?) -> Void) {
28 | self.completionHandler = completionHandler
29 | }
30 |
31 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
32 | let image: UIImage? = {
33 | if let image = info[.editedImage] as? UIImage {
34 | return image
35 | }
36 | return info[.originalImage] as? UIImage
37 | }()
38 | completionHandler(image)
39 | }
40 |
41 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
42 | completionHandler(nil)
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/PDFViewer.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import PDFKit
3 |
4 | struct PDFViewverUI: View {
5 | @EnvironmentObject var navStack: NavigationStack
6 |
7 | var url: URL
8 |
9 | var body: some View {
10 | VStack {
11 | PDFViewer(url)
12 | }
13 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
14 | .navigationBarTitle("PDF Viewer", displayMode: .inline)
15 | .navigationBarItems(leading: backButton,
16 | trailing: trailingGroup)
17 | .edgesIgnoringSafeArea(.bottom)
18 |
19 | }
20 |
21 | private var backButton: some View {
22 | Button(action: { self.navStack.pop() }, label: {
23 | HStack {
24 | Image(systemName: "chevron.backward")
25 | .foregroundColor(Color(UIColor.label))
26 | Text("Back")
27 | .font(.callout)
28 | .foregroundColor(Color(UIColor.label))
29 | }
30 | })
31 | }
32 |
33 | private var trailingGroup: some View {
34 | Button(action: {}, label: { })
35 | }
36 | }
37 |
38 | struct PDFViewer: UIViewRepresentable {
39 | var url: URL
40 |
41 | init(_ url: URL) {
42 | self.url = url
43 | }
44 |
45 | func makeUIView(context: Context) -> UIView {
46 | let pdfView = PDFView()
47 | pdfView.document = PDFDocument(url: url)
48 | pdfView.autoScales = true
49 | return pdfView
50 | }
51 |
52 | func updateUIView(_ uiView: UIView, context: Context) {}
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/PageView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct PageView_Previews: PreviewProvider {
4 | static var previews: some View {
5 | PageView(selection: .constant(0), content: { Text("Some text") })
6 | }
7 | }
8 |
9 | struct PageView: View where SelectionValue: Hashable, Content: View {
10 | @Binding var selection: SelectionValue
11 | private let indexDisplayMode: PageTabViewStyle.IndexDisplayMode
12 | private let indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode
13 | private let content: () -> Content
14 |
15 | init(selection: Binding,
16 | indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
17 | indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
18 | @ViewBuilder content: @escaping () -> Content) {
19 |
20 | self._selection = selection
21 | self.indexDisplayMode = indexDisplayMode
22 | self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
23 | self.content = content
24 | }
25 |
26 | var body: some View {
27 | TabView(selection: $selection) { content() }
28 | .tabViewStyle(PageTabViewStyle(indexDisplayMode: indexDisplayMode))
29 | .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: indexBackgroundDisplayMode))
30 | }
31 | }
32 |
33 | extension PageView where SelectionValue == Int {
34 | init(selection: Binding, indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
35 | indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
36 | @ViewBuilder content: @escaping () -> Content) {
37 |
38 | self._selection = selection
39 | self.indexDisplayMode = indexDisplayMode
40 | self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
41 | self.content = content
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/Pager.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct Pager: UIViewControllerRepresentable {
4 | var pages: ObservableArray
5 | @Binding var currentPage: Int
6 |
7 | func makeUIViewController(context: Context) -> UIPageViewController {
8 | let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
9 |
10 | pageViewController.dataSource = context.coordinator
11 | pageViewController.delegate = context.coordinator
12 |
13 | return pageViewController
14 | }
15 |
16 | func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
17 | var direction: UIPageViewController.NavigationDirection = .forward
18 | var animated: Bool = false
19 |
20 | if let previousViewController = pageViewController.viewControllers?.first,
21 | let previousPage = context.coordinator.controllers.firstIndex(of: previousViewController) {
22 | direction = (currentPage >= previousPage) ? .forward : .reverse
23 | animated = (currentPage != previousPage)
24 | }
25 |
26 | let currentViewController = context.coordinator.controllers[currentPage]
27 | pageViewController.setViewControllers([currentViewController], direction: direction, animated: animated)
28 | }
29 |
30 | func makeCoordinator() -> Coordinator {
31 | return Coordinator(parent: self, pages: pages)
32 | }
33 |
34 | class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
35 |
36 | var parent: Pager
37 | var controllers: [UIViewController]
38 |
39 | init(parent: Pager, pages: ObservableArray) {
40 | self.parent = parent
41 | self.controllers = pages.array.map({
42 | let hostingController = UIHostingController(rootView: $0)
43 | hostingController.view.backgroundColor = .clear
44 | return hostingController
45 | })
46 | }
47 |
48 | func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
49 | guard let index = controllers.firstIndex(of: viewController) else {
50 | return nil
51 | }
52 | if index == 0 {
53 | return nil
54 | }
55 | return controllers[index - 1]
56 | }
57 |
58 | func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
59 | guard let index = controllers.firstIndex(of: viewController) else {
60 | return nil
61 | }
62 | if index + 1 == controllers.count {
63 | return nil
64 | }
65 | return controllers[index + 1]
66 | }
67 |
68 | func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
69 | if completed, let currentViewController = pageViewController.viewControllers?.first,
70 | let currentIndex = controllers.firstIndex(of: currentViewController) {
71 | parent.currentPage = currentIndex
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/SearchBar.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SearchBar: UIViewRepresentable {
4 | @Binding var text: String
5 |
6 | var placeholder: String
7 |
8 | class Coordinator: NSObject, UISearchBarDelegate {
9 | @Binding var text: String
10 |
11 | init(text: Binding) {
12 | self._text = text
13 | }
14 |
15 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
16 | self.text = searchText
17 | }
18 | }
19 |
20 | func makeCoordinator() -> SearchBar.Coordinator {
21 | return Coordinator(text: $text)
22 | }
23 |
24 | func makeUIView(context: UIViewRepresentableContext) -> UISearchBar {
25 | let searchBar = UISearchBar(frame: .zero)
26 | searchBar.delegate = context.coordinator
27 | searchBar.placeholder = placeholder
28 | searchBar.searchBarStyle = .minimal
29 | searchBar.autocapitalizationType = .none
30 | return searchBar
31 | }
32 |
33 | func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext) {
34 | uiView.text = text
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Scanverter/Views/CustomViews/TextScannerView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Vision
3 | import VisionKit
4 |
5 | typealias recognizingHandler = ([String]?) -> Void
6 |
7 | struct TextScannerView: UIViewControllerRepresentable {
8 | private let completionHandler: recognizingHandler
9 |
10 | init(completion: @escaping recognizingHandler) {
11 | self.completionHandler = completion
12 | }
13 |
14 | typealias UIViewControllerType = VNDocumentCameraViewController
15 |
16 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> VNDocumentCameraViewController {
17 | let viewController = VNDocumentCameraViewController()
18 | viewController.delegate = context.coordinator
19 | return viewController
20 | }
21 |
22 | func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: UIViewControllerRepresentableContext) {}
23 |
24 | func makeCoordinator() -> Coordinator {
25 | return Coordinator(completion: completionHandler)
26 | }
27 |
28 | final class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
29 | private let completionHandler: ([String]?) -> Void
30 |
31 | init(completion: @escaping ([String]?) -> Void) {
32 | self.completionHandler = completion
33 | }
34 |
35 | func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
36 | print("Document camera view controller did finish with ", scan)
37 | let recognizer = TextRecognizer(cameraScan: scan)
38 | recognizer.recognizeText(withCompletionHandler: completionHandler)
39 | }
40 |
41 | func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
42 | completionHandler(nil)
43 | }
44 |
45 | func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) {
46 | print("Document camera view controller did finish with error ", error)
47 | completionHandler(nil)
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/DataSources/DocsDataSource.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import PDFKit
3 |
4 | typealias Files = [DocFile]
5 |
6 | final class DocsDataSource: ObservableObject {
7 | @Published var files: Files = .init()
8 |
9 | private var subscriptions: Set = .init()
10 |
11 | init(files: Files) {
12 | self.files = files
13 | }
14 |
15 | @discardableResult
16 | func remove(indexSet: IndexSet?) -> AnyPublisher {
17 | return Future { promise in
18 |
19 | }.eraseToAnyPublisher()
20 | }
21 |
22 | func getUrl(forDoc doc: DocFile) -> URL? {
23 | return DataManager.getUrlForDoc(fromFolder: doc.parent.uid.uuidString, withFileName: doc.name)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/DataSources/FoldersDataSource.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 | import PDFKit
4 |
5 | struct FolderSelection {
6 | var id: String = ""
7 | var selected: Bool = false
8 | }
9 |
10 | final class FolderSelector: ObservableObject {
11 | public let publisher = PassthroughSubject()
12 | var selection: FolderSelection = FolderSelection() {
13 | willSet {
14 | publisher.send(selection)
15 | }
16 | }
17 | }
18 |
19 | final class FoldersDataSource: ObservableObject {
20 | @Published var folders: [Folder] = .init()
21 | var folderSelector: FolderSelector = .init()
22 |
23 | private var subscriptions: Set = .init()
24 |
25 | func loadFolders() {
26 | folders.removeAll()
27 | DataManager.loadAll(Folder.self).forEach { folders.append($0) }
28 | folders.sort { $0.date < $1.date }
29 | print(folders)
30 | }
31 |
32 | @discardableResult
33 | func createNewFolder(withName name: String, secureLock: Bool = false) -> AnyPublisher {
34 | return Future { promise in
35 | let folder = Folder(name: name, date: Date(), isPasswordProtected: secureLock, uid: UUID(), files: [])
36 | folder.save()
37 | .receive(on: DispatchQueue.main)
38 | .sink { saved in
39 | if saved {
40 | self.loadFolders()
41 | }
42 | promise(.success(saved))
43 | }
44 | .store(in: &self.subscriptions)
45 | }.eraseToAnyPublisher()
46 | }
47 |
48 | @discardableResult
49 | func remove(indexSet: IndexSet?) -> AnyPublisher {
50 | return Future { promise in
51 | guard let index = indexSet?.first else { promise(.success(false)); return }
52 | let folder = self.folders[index]
53 | folder.delete(isDirectory: true)
54 | .receive(on: DispatchQueue.main)
55 | .sink { deleted in
56 | if deleted {
57 | self.folders.remove(at: index)
58 | }
59 | promise(.success(deleted))
60 | }
61 | .store(in: &self.subscriptions)
62 | }.eraseToAnyPublisher()
63 | }
64 |
65 | func setSelected(folder: Folder) {
66 | for var item in self.folders {
67 | if item.uid == folder.uid {
68 | item.selected.toggle()
69 | } else {
70 | item.selected = false
71 | }
72 | item.save()
73 | .receive(on: DispatchQueue.main)
74 | .sink { _ in
75 | self.loadFolders()
76 | }
77 | .store(in: &subscriptions)
78 | }
79 | folderSelector.selection = FolderSelection(id: folder.uid.uuidString, selected: !folder.selected)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/DataSources/SearchDataSource.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | final class SearchDataSource: ObservableObject {
4 | @Published var files: [DocFile] = .init()
5 |
6 | func loadAll() {
7 | let folders = DataManager.loadAll(Folder.self)
8 | folders
9 | .map { $0.files }
10 | .flatMap { $0 }
11 | .forEach { file in files.append(file)}
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/DataSources/SettingsDataSource.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | class SettingsDataSource: ObservableObject {
4 | var didChange = PassthroughSubject()
5 |
6 | @Published var isBluetoothOn = false { didSet { update() } }
7 |
8 | @Published var types = ["Off","On"]
9 | @Published var type = 0 { didSet { update() } }
10 |
11 | @Published var isToggleOn = false { didSet { update() } }
12 |
13 | func update() {
14 | didChange.send(())
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/DataSources/TextRecognizer.swift:
--------------------------------------------------------------------------------
1 | import Vision
2 | import VisionKit
3 | import PDFKit
4 | import Combine
5 |
6 | final class TextRecognizer {
7 | let cameraScan: VNDocumentCameraScan
8 |
9 | private var subscriptions: Set = .init()
10 |
11 | init(cameraScan: VNDocumentCameraScan) {
12 | self.cameraScan = cameraScan
13 | }
14 |
15 | private let queue = DispatchQueue(label: "ru.otus.scanverter", qos: .default, attributes: [], autoreleaseFrequency: .workItem)
16 |
17 | func recognizeText(withCompletionHandler completionHandler: @escaping ([String]) -> Void) {
18 | queue.async {
19 | let images = (0.. String in
22 | let handler = VNImageRequestHandler(cgImage: image, options: [:])
23 | do {
24 | try handler.perform([request])
25 | guard let observations = request.results as? [VNRecognizedTextObservation] else { return "" }
26 | return observations.compactMap({ $0.topCandidates(1).first?.string }).joined(separator: "\n")
27 | }
28 | catch {
29 | print(error)
30 | return ""
31 | }
32 | }
33 | self.generatePdf()
34 | DispatchQueue.main.async {
35 | completionHandler(textPerPage)
36 | }
37 | }
38 | }
39 |
40 | func generatePdf() {
41 | let pdfDocument = PDFDocument()
42 | for i in 0 ..< self.cameraScan.pageCount {
43 | if let image = self.cameraScan.imageOfPage(at: i).resize(toWidth: UIScreen.main.bounds.width - 40) {
44 | print("image size is \(image.size.width), \(image.size.height)")
45 | let pdfPage = PDFPage(image: image)
46 | pdfDocument.insert(pdfPage!, at: i)
47 | }
48 | }
49 | var folderToSave = Folder(name: "ScannedByVision", date: Date(), isPasswordProtected: false, uid: UUID(), files: [])
50 | folderToSave.save()
51 | .receive(on: DispatchQueue.main)
52 | .sink { done in
53 | if !done { return }
54 | let file = DocFile(name: "vk-\(Date().toFileNameString)", date: Date(), uid: UUID(), parent: folderToSave)
55 | DataManager.save(pdf: pdfDocument, withFileName: file.name, inFolder: folderToSave.uid.uuidString)
56 | .receive(on: DispatchQueue.main)
57 | .sink { saved in
58 | folderToSave.files.append(file)
59 | folderToSave.save()
60 | }
61 | .store(in: &self.subscriptions)
62 | }
63 | .store(in: &subscriptions)
64 | }
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/Screens/CurrentScreen.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CurrentScreen: View {
4 | @Binding var currentView: Tab
5 |
6 | var body: some View {
7 | VStack {
8 | if self.currentView == .folders {
9 | FoldersScreen(selectedFolder: .constant(nil), calledFromSaving: false)
10 | } else {
11 | SettingsScreen()
12 | }
13 | }
14 | }
15 | }
16 |
17 | struct CurrentScreen_Previews: PreviewProvider {
18 | static var previews: some View {
19 | CurrentScreen(currentView: .constant(.folders))
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/Screens/FolderDetailScreen.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import ExyteGrid
3 | import PDFKit
4 |
5 | struct FolderDetailScreen: View {
6 | @EnvironmentObject var navStack: NavigationStack
7 |
8 | @StateObject var dataSource: DocsDataSource
9 | @State private var flow: GridFlow = .rows
10 |
11 | @State private var showDeleteAlert: Bool = false
12 | @State private var offsets: IndexSet?
13 |
14 | var body: some View {
15 | NavigationStackView {
16 | GeometryReader { geometry in
17 | ZStack {
18 | VStack {
19 | if flow == .rows {
20 | filesGrid
21 | } else {
22 | filesList
23 | }
24 | }
25 | }
26 | .alert(isPresented: self.$showDeleteAlert) {
27 | Alert(title: Text("Deletion Alert!"),
28 | message: Text("You're about to delete the file."),
29 | primaryButton: .destructive(Text("Delete")) {
30 | dataSource.remove(indexSet: offsets)
31 | },
32 | secondaryButton: .cancel())
33 | }
34 | }
35 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
36 | .navigationBarTitle("Files (Scanned)", displayMode: .inline)
37 | .navigationBarItems(leading: backButton,
38 | trailing: trailingGroup)
39 | .edgesIgnoringSafeArea(.bottom)
40 | }
41 | }
42 |
43 | private var backButton: some View {
44 | Button(action: { self.navStack.pop() }, label: {
45 | HStack {
46 | Image(systemName: "chevron.backward")
47 | .foregroundColor(Color(UIColor.label))
48 | Text("Back")
49 | .font(.callout)
50 | .foregroundColor(Color(UIColor.label))
51 | }
52 | })
53 | }
54 |
55 | private var trailingGroup: some View {
56 | Button(action: {}, label: { })
57 | }
58 |
59 | private var filesGrid: some View {
60 | VStack {
61 | Grid(tracks: 3) {
62 | ForEach(dataSource.files, id: \.uid) { item in
63 | VStack {
64 | PushView(destination: PDFViewverUI(url: dataSource.getUrl(forDoc: item)!)) {
65 | FileGridCell(dataSource: FileCellDataSource(file: item))
66 | }
67 | }
68 | .padding(EdgeInsets(top: 20, leading: 4, bottom: 0, trailing: 4))
69 | }
70 | }
71 | .padding(.top, 120)
72 | .gridContentMode(.scroll)
73 | .gridFlow(.rows)
74 | Spacer()
75 |
76 | }
77 | }
78 |
79 | private var filesList: some View {
80 | List {
81 | ForEach(dataSource.files, id: \.uid) { item in
82 | HStack {
83 | FileListCell(dataSource: FileCellDataSource(file: item))
84 | }
85 | .padding(EdgeInsets(top: 20, leading: 4, bottom: 0, trailing: 2))
86 | }
87 | .onDelete(perform: delete)
88 | }
89 | .padding(.top, 90)
90 | .listStyle(PlainListStyle())
91 | }
92 |
93 | func delete(at offsets: IndexSet) {
94 | self.showDeleteAlert = true
95 | self.offsets = offsets
96 | }
97 | }
98 |
99 | struct FolderDetailScreen_Previews: PreviewProvider {
100 | static var previews: some View {
101 | DetailScreen()
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/Screens/MainTabBarView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct MainTabBarView: View {
4 | @State private var currentView: Tab = .folders
5 | @State private var showCameraModal: Bool = false
6 | @State private var showMicModal: Bool = false
7 | @State private var showSearchModal: Bool = false
8 |
9 | init() {
10 | UINavigationBar.appearance().backgroundColor = .clear
11 | }
12 |
13 | var body: some View {
14 | NavigationView {
15 | VStack {
16 | CurrentScreen(currentView: self.$currentView)
17 | .fullScreenCover(isPresented: self.$showMicModal, content: TextRecognizerScreen.init)
18 | TabBar(currentView: self.$currentView,
19 | showCameraModal: self.$showCameraModal,
20 | showMicModal: self.$showMicModal,
21 | showSearchModal: self.$showSearchModal)
22 | .fullScreenCover(isPresented: self.$showCameraModal, content: { ModalCamera(model: CameraViewModel()) })
23 | }
24 | .edgesIgnoringSafeArea(.all)
25 | }
26 | .background(Color(.white))
27 | .navigationViewStyle(StackNavigationViewStyle())
28 | .fullScreenCover(isPresented: self.$showSearchModal, content: ModalSearchScreen.init)
29 | }
30 | }
31 |
32 | struct ContentView_Previews: PreviewProvider {
33 | static var previews: some View {
34 | MainTabBarView()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/Screens/SettingsScreen.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SettingsScreen : View {
4 | @ObservedObject var settings = SettingsDataSource()
5 |
6 | var body: some View {
7 | Form {
8 | Section {
9 | SignInView()
10 | }
11 | Section {
12 | BluetoothView(bluetooth: settings)
13 | WiFiView(wifi: settings)
14 | }
15 | ForEach(Option.options,id: \.id) { settingOption in
16 | OptionRow(option: settingOption)
17 | }
18 | }
19 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
20 | .background(Color(.purple).opacity(0.2))
21 | .navigationBarTitle("Settings", displayMode: .inline)
22 | .navigationBarItems(leading: Button(action: {}, label: { }),
23 | trailing: Button(action: {}, label: { }))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Scanverter/Views/Main/Screens/SettingsScreenTempleate.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SettingsTemplateScreen: View {
4 | var body: some View {
5 | VStack {
6 | Spacer()
7 | HStack {
8 | Spacer()
9 | Text("Settings Screen")
10 | .font(.system(size: 20))
11 | .bold()
12 | Spacer()
13 | }
14 | Spacer()
15 | }
16 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
17 | .background(Color(.purple).opacity(0.2))
18 | .navigationBarTitle("Settings", displayMode: .inline)
19 | .navigationBarItems(leading: Button(action: {}, label: { }),
20 | trailing: Button(action: {}, label: { }))
21 | }
22 | }
23 |
24 | struct SettingsScreen_Previews: PreviewProvider {
25 | static var previews: some View {
26 | SettingsTemplateScreen()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Scanverter/Views/Modals/ModalCamera.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ModalCamera: View {
4 | @Environment(\.presentationMode) var presentationMode
5 | @ObservedObject var model: CameraViewModel
6 | @State private var showOneLevelIn: Bool = false
7 | @State private var fromEditor: Bool = false
8 |
9 | init(model: CameraViewModel) {
10 | self.model = model
11 | }
12 |
13 | private var showPhotoLibrary: some View {
14 | HStack {
15 | Spacer()
16 | Button(action: {
17 | model.isPresentingImagePicker = true
18 | }, label: {
19 | Image(systemName: "photo.on.rectangle.angled")
20 | .resizable()
21 | .frame(width: 40, height: 30, alignment: .leading)
22 | .foregroundColor(.gray)
23 | })
24 | }
25 | .offset(x: -280, y: 0)
26 | .fullScreenCover(isPresented: $model.isPresentingImagePicker, content: {
27 | ImagePicker(sourceType: .photoLibrary, completionHandler: model.didSelectImage)
28 | })
29 | }
30 |
31 | var body: some View {
32 | NavigationStackView {
33 | ZStack {
34 | CameraView(model: model)
35 | HStack {
36 | showPhotoLibrary
37 | Spacer()
38 | Button(action: {
39 | presentationMode.wrappedValue.dismiss()
40 | }, label: {
41 | Image(systemName: "xmark.circle")
42 | .resizable()
43 | .frame(width: 35, height: 35, alignment: .leading)
44 | .foregroundColor(.gray)
45 | })
46 | }.offset(x: -20, y: -350)
47 | Text("Not working in simulator!")
48 | .foregroundColor(.gray)
49 | .opacity(UIDevice.isSimulator ? 1 : 0)
50 | PushView(destination: EditorView(dataSource: PhotoCollectionDataSource(scannedDocs: model.scannedDocs), backToCamera: $fromEditor), isActive: $showOneLevelIn) {
51 | EmptyView()
52 | }.hidden()
53 | }.onReceive(model.publisher) { status in
54 | showOneLevelIn = status
55 | }
56 | .onAppear {
57 | if fromEditor {
58 | presentationMode.wrappedValue.dismiss()
59 | }
60 | }
61 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
62 | .edgesIgnoringSafeArea(.all)
63 | }
64 | }
65 | }
66 |
67 | struct ModalCamera_Previews: PreviewProvider {
68 | static var previews: some View {
69 | ModalCamera(model: CameraViewModel())
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Scanverter/Views/Modals/ModalScreen.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ModalScreen: View {
4 | @Environment(\.presentationMode) var presentationMode
5 |
6 | var body: some View {
7 | ZStack {
8 | VStack {
9 | Spacer()
10 | HStack {
11 | Spacer()
12 | Text("Modal Screen").font(.system(size: 20)).bold()
13 | // EditorView(dataSource: PhotoCollectionDataSource(scannedDocs: mockedDocs))
14 | Spacer()
15 | }
16 | Spacer()
17 | }
18 | HStack {
19 | Spacer()
20 | Button(action: {
21 | presentationMode.wrappedValue.dismiss()
22 | }, label: {
23 | Image(systemName: "xmark.circle")
24 | .resizable()
25 | .frame(width: 25, height: 25, alignment: .leading)
26 | .foregroundColor(.gray)
27 |
28 | })
29 | }.offset(x: -30, y: -380)
30 | }
31 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
32 | .edgesIgnoringSafeArea(.all)
33 | }
34 |
35 | private var mockedDocs: [ScannedDoc] {
36 | var docs: [ScannedDoc] = .init()
37 | let names = ["imageWithText", "slideWithText"]
38 | for _ in 0..<2 {
39 | names.forEach {
40 | docs.append(ScannedDoc(image: UIImage(named: $0)!.cgImage!, date: Date()))
41 | }
42 | }
43 | return docs
44 | }
45 | }
46 |
47 | struct ModalScreen_Previews: PreviewProvider {
48 | static var previews: some View {
49 | ModalScreen()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Scanverter/Views/Modals/ModalSearchScreen.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ModalSearchScreen: View {
4 | @Environment(\.presentationMode) var presentationMode
5 | @StateObject var dataSource: SearchDataSource = .init()
6 |
7 | @State private var searchText : String = ""
8 |
9 | var body: some View {
10 | ZStack {
11 | VStack {
12 | HStack {
13 | SearchBar(text: $searchText, placeholder: "Search files")
14 | .padding(EdgeInsets(top: 40, leading: 10, bottom: 20, trailing: 0))
15 | Spacer()
16 | Button(action: {
17 | presentationMode.wrappedValue.dismiss()
18 | }, label: {
19 | Image(systemName: "xmark.circle")
20 | .resizable()
21 | .frame(width: 25, height: 25, alignment: .leading)
22 | .foregroundColor(.gray)
23 | })
24 | .padding(EdgeInsets(top: 40, leading: 0, bottom: 20, trailing: 20))
25 | }
26 |
27 | List {
28 | ForEach(dataSource.files.filter {
29 | self.searchText.isEmpty ? true : $0.name.lowercased().contains(self.searchText.lowercased())
30 | }, id: \.self) { item in
31 | Text(item.name)
32 | }
33 | }.navigationBarTitle(Text("File Search"))
34 | }
35 | .onAppear { dataSource.loadAll() }
36 | }
37 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
38 | .edgesIgnoringSafeArea(.all)
39 | }
40 | }
41 |
42 | struct ModalSearchScreen_Previews: PreviewProvider {
43 | static var previews: some View {
44 | ModalSearchScreen()
45 | .previewDevice("iPhone 11 Pro")
46 | }
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/Scanverter/Views/Modals/TextRecognizerScreen.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import VisionKit
3 | import PDFKit
4 |
5 | struct TextRecognizerScreen: View {
6 | @Environment(\.presentationMode) var presentationMode
7 |
8 | @State private var isShowingScannerSheet = false
9 | @State private var text: String = ""
10 |
11 | @State var isPresentingFolderChooser: Bool = false
12 | @State private var folderToSaveIn: Folder?
13 | @State private var savedScan: VNDocumentCameraScan?
14 |
15 | var body: some View {
16 | ZStack {
17 | VStack(spacing: 32) {
18 | Text("Vision Kit Example")
19 | HStack(spacing: 55) {
20 | saveToPdf
21 | .sheet(isPresented: $isPresentingFolderChooser, onDismiss: {}) {
22 | FoldersScreen(selectedFolder: $folderToSaveIn, calledFromSaving: true)
23 | }
24 | .opacity(0)
25 | Spacer()
26 | scanButton
27 | .opacity(UIDevice.isSimulator ? 0.4 : 1)
28 | .disabled(UIDevice.isSimulator)
29 | Spacer()
30 | closeButton
31 | }
32 | .offset(x: -30, y: 0)
33 | Text("Not working in simulator!").opacity(UIDevice.isSimulator ? 1 : 0)
34 | Spacer()
35 | Text(text)
36 | .font(.largeTitle)
37 | .fontWeight(.heavy)
38 | .lineLimit(nil)
39 | Spacer()
40 | }
41 | .sheet(isPresented: self.$isShowingScannerSheet) { self.makeScannerView() }
42 | }
43 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
44 | .edgesIgnoringSafeArea(.all)
45 | }
46 |
47 | private var scanButton: some View {
48 | Button(action: openCamera) {
49 | Text("Scan")
50 | .foregroundColor(.white)
51 | }
52 | .frame(width: 40, height: 20)
53 | .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
54 | .background(Color(UIColor.darkGray))
55 | .cornerRadius(3.0)
56 | }
57 |
58 | private var closeButton: some View {
59 | Button(action: {
60 | NotificationCenter.default.post(name: NSNotification.Name("TextRecognizerScreenDismissed"), object: nil)
61 | presentationMode.wrappedValue.dismiss()
62 | }, label: {
63 | Image(systemName: "xmark.circle")
64 | .resizable()
65 | .frame(width: 30, height: 30, alignment: .leading)
66 | .foregroundColor(Color(UIColor.systemGray2))
67 | })
68 | }
69 |
70 | private var saveToPdf: some View {
71 | HStack {
72 | Spacer()
73 | Button(action: {
74 | isPresentingFolderChooser = true
75 | }, label: {
76 | Image(systemName: "square.and.arrow.down")
77 | .resizable()
78 | .frame(width: 30, height: 30, alignment: .leading)
79 | .foregroundColor(.gray)
80 | })
81 | }
82 | }
83 |
84 | private func openCamera() {
85 | isShowingScannerSheet = true
86 | }
87 |
88 | private func makeScannerView() -> TextScannerView {
89 | TextScannerView(completion: { textPerPage in
90 | if let text = textPerPage?.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines) {
91 | // self.text = text
92 | self.text = "Saved"
93 | }
94 | self.isShowingScannerSheet = false
95 | })
96 | }
97 | }
98 |
99 | struct TextRecognizerScreen_Previews: PreviewProvider {
100 | static var previews: some View {
101 | TextRecognizerScreen()
102 | }
103 | }
104 |
105 |
--------------------------------------------------------------------------------
/Scanverter/Views/Navigation/ScreenDetails.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct DetailScreen: View {
4 | @EnvironmentObject var navStack: NavigationStack
5 | var body: some View {
6 | VStack {
7 | Spacer()
8 | HStack {
9 | Spacer()
10 | VStack {
11 | Text("Detail Screen")
12 | .font(.system(size: 20))
13 | .bold()
14 |
15 | Button(action: {
16 | self.navStack.pop()
17 | }, label: {
18 | Text("Back to camera")
19 | })
20 | }
21 | Spacer()
22 | }
23 | Spacer()
24 | }
25 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
26 | .background(Color(.green).opacity(0.2))
27 | .navigationBarTitle("Detail Screen", displayMode: .inline)
28 | .edgesIgnoringSafeArea(.bottom)
29 | }
30 | }
31 |
32 | struct DetailScreen_Previews: PreviewProvider {
33 | static var previews: some View {
34 | DetailScreen()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Scanverter/Views/Protocols/ContainerView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | protocol ContainerView: View {
4 | associatedtype Content
5 | init(content: @escaping () -> Content)
6 | }
7 |
8 | extension ContainerView {
9 | init(@ViewBuilder _ content: @escaping () -> Content) {
10 | self.init(content: content)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Scanverter/Views/Tabs/EditorTabBar.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct EditorTabBar: View {
4 | @StateObject var dataSource: EditTabBarDataSource
5 | @StateObject var photoDataSource: PhotoCollectionDataSource
6 |
7 | var body: some View {
8 | HStack(spacing: 40) {
9 | ForEach(dataSource.editTools) { tool in
10 | VStack {
11 | EditToolCell(dataSource: EditToolCellDataSource(tool: tool), photoDataSource: photoDataSource)
12 | .padding(.top, 6)
13 | .padding(.bottom, 6)
14 | }
15 | }
16 | }
17 | .frame(minHeight: 70)
18 | }
19 | }
20 |
21 | struct EditorTabBar_Previews: PreviewProvider {
22 | static var previews: some View {
23 | EditorTabBar(dataSource: EditTabBarDataSource(tools: Constants.editTools),
24 | photoDataSource: PhotoCollectionDataSource(scannedDocs: Constants.mockedDocs))
25 | .preferredColorScheme(.dark)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Scanverter/Views/Tabs/ModalScreenShowTabButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct ModalScreenShowTabButton: View {
4 | let imageName: String
5 | let radius: CGFloat
6 | let action: () -> Void
7 |
8 | public init(name: String, radius: CGFloat, action: @escaping () -> Void) {
9 | self.imageName = name
10 | self.radius = radius
11 | self.action = action
12 | }
13 |
14 | public var body: some View {
15 | VStack(spacing: 0) {
16 | Image(systemName: imageName)
17 | .resizable()
18 | .aspectRatio(contentMode: .fit)
19 | .frame(width: radius, height: radius, alignment: .center)
20 | .foregroundColor(.primary)
21 | }
22 | .frame(width: radius, height: radius)
23 | .onTapGesture(perform: action)
24 | }
25 | }
26 |
27 | struct ModalScreenShowTabButton_Previews_Previews: PreviewProvider {
28 | static var previews: some View {
29 | ModalScreenShowTabButton(name: "gears", radius: 55) { }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Scanverter/Views/Tabs/Tab.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum Tab: Int {
4 | case folders
5 | case settings
6 |
7 | var index: Int {
8 | switch self {
9 | case .folders:
10 | return 1
11 | case .settings:
12 | return 2
13 | }
14 | }
15 |
16 | static func tabByIndex(_ index: Int) -> Tab {
17 | switch index {
18 | case 1:
19 | return folders
20 | case 2:
21 | return settings
22 | default:
23 | return folders
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Scanverter/Views/Tabs/TabBar.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct TabBar_Previews: PreviewProvider {
4 | static var previews: some View {
5 | TabBar(currentView: .constant(.folders), showCameraModal: .constant(false), showMicModal: .constant(false), showSearchModal: .constant(false))
6 | .preferredColorScheme(.dark)
7 | }
8 | }
9 |
10 | struct TabBar: View {
11 | @Binding var currentView: Tab
12 | @Binding var showCameraModal: Bool
13 | @Binding var showMicModal: Bool
14 | @Binding var showSearchModal: Bool
15 |
16 | var body: some View {
17 | HStack(spacing: 30) {
18 | VStack {
19 | TabBarItem(currentView: self.$currentView,
20 | imageName: "folder",
21 | paddingEdges: .leading,
22 | tab: .folders)
23 | Text("Folders")
24 | .foregroundColor(.primary)
25 | .font(.caption2)
26 | }.padding(.leading, 5)
27 |
28 | HStack(spacing: 40) {
29 | VStack {
30 | ModalScreenShowTabButton(name: "eye", radius: 20) {
31 | self.showMicModal.toggle()
32 | }
33 | .padding(.top, 4)
34 | .padding(.bottom, 4)
35 | Text("Vision")
36 | .foregroundColor(.primary)
37 | .font(.caption2)
38 | }
39 | VStack {
40 | ModalScreenShowTabButton(name: "camera.fill", radius: 20) {
41 | self.showCameraModal.toggle()
42 | }
43 | .padding(.top, 4)
44 | .padding(.bottom, 4)
45 | Text("CamScan")
46 | .foregroundColor(.primary)
47 | .font(.caption2)
48 | }
49 | VStack {
50 | ModalScreenShowTabButton(name: "magnifyingglass", radius: 20) {
51 | self.showSearchModal.toggle()
52 | }.foregroundColor(Color(.white))
53 | .padding(.top, 4)
54 | .padding(.bottom, 4)
55 | Text("Search")
56 | .foregroundColor(.primary)
57 | .font(.caption2)
58 | }
59 | }
60 |
61 | VStack {
62 | TabBarItem(currentView: self.$currentView,
63 | imageName: "gearshape",
64 | paddingEdges: .trailing,
65 | tab: .settings)
66 | Text("Settings")
67 | .foregroundColor(.primary)
68 | .font(.caption2)
69 | }.padding(.trailing, 5)
70 | }
71 | .frame(minHeight: 70)
72 | }
73 | }
74 |
75 |
76 |
--------------------------------------------------------------------------------
/Scanverter/Views/Tabs/TabBarItem.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Combine
3 | import SwiftUI
4 |
5 | struct TabBarItem: View {
6 | @Binding var currentView: Tab
7 | let imageName: String
8 | let paddingEdges: Edge.Set
9 | let tab: Tab
10 |
11 | var body: some View {
12 | VStack(spacing: 0) {
13 | Image(systemName: self.currentView == tab ? "\(imageName).fill" : imageName)
14 | .resizable()
15 | .aspectRatio(contentMode: .fit)
16 | .padding(EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0))
17 | .frame(width: 30, height: 30, alignment: .center)
18 | .foregroundColor(self.currentView == tab ? Color(.systemBlue) : Color(.label))
19 | .cornerRadius(6)
20 | }
21 | .frame(width: 30, height: 30)
22 | .onTapGesture { self.currentView = self.tab }
23 | }
24 | }
25 |
26 | struct TabBarItem_Previews: PreviewProvider {
27 | static var previews: some View {
28 | TabBarItem(currentView: .constant(.settings), imageName: "camera", paddingEdges: .leading, tab: .settings)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Scanverter/Views/Tabs/ViewModels/EditorTabBarDataSource.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | final class EditTabBarDataSource: ObservableObject {
4 | @Published private(set) var editTools: [EditTool]
5 |
6 | init(tools: [EditTool]) {
7 | self.editTools = tools
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Scanverter/tessdata/deu.lstm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/tessdata/deu.lstm
--------------------------------------------------------------------------------
/Scanverter/tessdata/deu.lstm-number-dawg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/tessdata/deu.lstm-number-dawg
--------------------------------------------------------------------------------
/Scanverter/tessdata/deu.lstm-punc-dawg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dartsyms/scanverter/d6f303b7f907e84b5bafa5d11d9e0305786cc397/Scanverter/tessdata/deu.lstm-punc-dawg
--------------------------------------------------------------------------------
/Scanverter/tessdata/deu.lstm-recoder:
--------------------------------------------------------------------------------
1 | t r r
2 |
! " # $ % &