├── .gitignore
├── Jottre
├── de.lproj
│ ├── LaunchScreen.strings
│ └── Localizable.strings
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── icon_20x20.png
│ │ ├── icon_29x29.png
│ │ ├── icon_40x40.png
│ │ ├── icon_76x76.png
│ │ ├── icon_20x20@2x.png
│ │ ├── icon_20x20@3x.png
│ │ ├── icon_29x29@2x.png
│ │ ├── icon_29x29@3x.png
│ │ ├── icon_40x40@2x.png
│ │ ├── icon_40x40@3x.png
│ │ ├── icon_60x60@2x.png
│ │ ├── icon_60x60@3x.png
│ │ ├── icon_76x76@2x.png
│ │ ├── icon_1024x1024.png
│ │ ├── icon_20x20@2x-1.png
│ │ ├── icon_29x29@2x-1.png
│ │ ├── icon_40x40@2x-1.png
│ │ ├── icon_83.5x83.5@2x.png
│ │ └── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.dark.imageset
│ │ ├── Bildschirmfoto 2021-01-04 um 17.22.41 09.19.56.png
│ │ └── Contents.json
├── Jottre
│ └── en.xcloc
│ │ ├── Source Contents
│ │ └── Jottre
│ │ │ ├── en.lproj
│ │ │ ├── Localizable.strings
│ │ │ └── InfoPlist.strings
│ │ │ └── Base.lproj
│ │ │ └── LaunchScreen.storyboard
│ │ ├── contents.json
│ │ └── Localized Contents
│ │ └── en.xliff
├── Extensions
│ ├── Logger.swift
│ ├── Data.swift
│ ├── UIView.swift
│ ├── String.swift
│ ├── PKDrawing.swift
│ └── URL.swift
├── Views
│ ├── BarButtonItems
│ │ ├── AddButton.swift
│ │ ├── SettingsButton.swift
│ │ ├── ShareButton.swift
│ │ ├── SpaceButtonBarItem.swift
│ │ ├── RedoButton.swift
│ │ ├── UndoButton.swift
│ │ ├── CustomButtonBarItem.swift
│ │ └── NavigationTextButton.swift
│ ├── SettingsCell
│ │ ├── IconSettingsCell.swift
│ │ ├── AppearanceSettingsCell.swift
│ │ ├── CloudSettingsCell.swift
│ │ └── SettingsCell.swift
│ ├── LoadingView.swift
│ └── NodeCell.swift
├── Helpers
│ ├── UIDevice.swift
│ ├── ThumbnailGenerator.swift
│ └── Downloader.swift
├── Controllers
│ ├── NavigationViewController.swift
│ ├── SettingsScene
│ │ ├── SettingsNavigationViewController.swift
│ │ ├── SettingsExtensions.swift
│ │ └── SettingsViewController.swift
│ ├── InitialScene
│ │ ├── MenuActions.swift
│ │ ├── InitialExtensions.swift
│ │ └── InitialViewController.swift
│ └── DrawScene
│ │ ├── ExportActions.swift
│ │ ├── DrawExtensions.swift
│ │ └── DrawViewController.swift
├── Models
│ ├── Node
│ │ ├── NodeCodable.swift
│ │ ├── NodeDecoderExtension.swift
│ │ ├── NodeExtensions.swift
│ │ ├── Node.swift
│ │ └── NodeCollector.swift
│ └── Settings
│ │ └── Settings.swift
├── Jottre.entitlements
├── AppDelegate.swift
├── Base.lproj
│ └── LaunchScreen.storyboard
├── en.lproj
│ └── Localizable.strings
├── Info.plist
└── SceneDelegate.swift
├── privacy
├── ge.txt
└── en.txt
├── images
├── cloud.jpg
├── mini.jpg
├── mode.jpg
└── app_icon.png
├── Jottre.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── antonlorani.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | exports/*
2 | resources/*
3 |
--------------------------------------------------------------------------------
/Jottre/de.lproj/LaunchScreen.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/privacy/ge.txt:
--------------------------------------------------------------------------------
1 | Wir erfassen keine deiner Daten.
2 |
--------------------------------------------------------------------------------
/privacy/en.txt:
--------------------------------------------------------------------------------
1 | Jottre does not store your data on non-apple servers, period.
2 |
--------------------------------------------------------------------------------
/images/cloud.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/images/cloud.jpg
--------------------------------------------------------------------------------
/images/mini.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/images/mini.jpg
--------------------------------------------------------------------------------
/images/mode.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/images/mode.jpg
--------------------------------------------------------------------------------
/images/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/images/app_icon.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_20x20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_20x20.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_29x29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_29x29.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_40x40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_40x40.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_76x76.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_20x20@3x.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_29x29@3x.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x-1.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x-1.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x-1.png
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png
--------------------------------------------------------------------------------
/Jottre/Jottre/en.xcloc/Source Contents/Jottre/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Jottre/en.xcloc/Source Contents/Jottre/en.lproj/Localizable.strings
--------------------------------------------------------------------------------
/Jottre.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Jottre/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 |
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.dark.imageset/Bildschirmfoto 2021-01-04 um 17.22.41 09.19.56.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonlorani/jottre/HEAD/Jottre/Assets.xcassets/AppIcon.dark.imageset/Bildschirmfoto 2021-01-04 um 17.22.41 09.19.56.png
--------------------------------------------------------------------------------
/Jottre.xcodeproj/xcuserdata/antonlorani.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/Jottre/Jottre/en.xcloc/contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "developmentRegion" : "en",
3 | "targetLocale" : "en",
4 | "toolInfo" : {
5 | "toolBuildNumber" : "12C33",
6 | "toolID" : "com.apple.dt.xcode",
7 | "toolName" : "Xcode",
8 | "toolVersion" : "12.3"
9 | },
10 | "version" : "1.0"
11 | }
--------------------------------------------------------------------------------
/Jottre/Jottre/en.xcloc/Source Contents/Jottre/en.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | /* Bundle name */
2 | "CFBundleName" = "Jottre";
3 | "Jottre document" = "Jottre document";
4 | /* Privacy - Photo Library Additions Usage Description */
5 | "NSPhotoLibraryAddUsageDescription" = "Jottre wants to store the image in your photo-library";
6 |
--------------------------------------------------------------------------------
/Jottre.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Jottre/Extensions/Logger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | extension Logger {
12 |
13 | private static var subsystem = Bundle.main.bundleIdentifier!
14 |
15 | static let main = Logger(subsystem: subsystem, category: "main")
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Jottre/Extensions/Data.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Data.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 03.02.21.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Data {
11 |
12 | func writeToReturingBoolean(url: URL) -> Bool {
13 |
14 | do {
15 | try self.write(to: url)
16 | return true
17 | } catch {
18 | return false
19 | }
20 |
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Jottre.xcodeproj/xcuserdata/antonlorani.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Jottre.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.dark.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Bildschirmfoto 2021-01-04 um 17.22.41 09.19.56.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 |
--------------------------------------------------------------------------------
/Jottre/Views/BarButtonItems/AddButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddButton.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 07.02.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class AddButton: CustomButtonBarItem {
12 |
13 | // MARK: - Override Methods
14 |
15 | override func setupViews() {
16 | super.setupViews()
17 |
18 | setImage(UIImage(systemName: "plus"), for: .normal)
19 |
20 | }
21 |
22 | }
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Jottre/Views/BarButtonItems/SettingsButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsButton.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 17.01.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class SettingsButton: CustomButtonBarItem {
12 |
13 | // MARK: - Override methods
14 |
15 | override func setupViews() {
16 | super.setupViews()
17 |
18 | setImage(UIImage(systemName: "gear"), for: .normal)
19 |
20 | }
21 |
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/Jottre/Views/BarButtonItems/ShareButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShareButton.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 07.02.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class ShareButton: CustomButtonBarItem {
12 |
13 | // MARK: - Override Methods
14 |
15 | override func setupViews() {
16 | super.setupViews()
17 |
18 | setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
19 |
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/Jottre/Helpers/UIDevice.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIDevice.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIDevice {
11 |
12 | /// Checks if device can be to edit Jots
13 | /// - Returns: A boolish value that indicates potential limitations (Such as device is not an iPad)
14 | static func isLimited() -> Bool {
15 |
16 | #if targetEnvironment(macCatalyst)
17 | return true
18 | #else
19 | return !(UIDevice.current.userInterfaceIdiom == .pad)
20 | #endif
21 |
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Jottre/Controllers/NavigationViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InitialNavigationViewController.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class NavigationViewController: UINavigationController {
12 |
13 | // MARK: - Main
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 |
18 | setupViews()
19 |
20 | }
21 |
22 |
23 |
24 | // MARK: - Methods
25 |
26 | func setupViews() {
27 |
28 | navigationItem.title = "Jottre"
29 | navigationBar.prefersLargeTitles = true
30 |
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Jottre/Views/BarButtonItems/SpaceButtonBarItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpaceButtonBarItem.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 02.03.21.
6 | //
7 |
8 | import UIKit
9 |
10 | class SpaceButtonBarItem: CustomButtonBarItem {
11 |
12 | // MARK: - Main
13 |
14 | init() {
15 | super.init(target: nil, action: nil)
16 | }
17 |
18 | required init?(coder: NSCoder) {
19 | fatalError("init(coder:) has not been implemented")
20 | }
21 |
22 |
23 |
24 | // MARK: - Methods
25 |
26 | override func setupViews() {
27 | super.setupViews()
28 |
29 | backgroundColor = .clear
30 |
31 | }
32 |
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Jottre/Extensions/UIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIView {
11 |
12 | // MARK: - Methods
13 |
14 | /// Round corners only for the specified regions
15 | /// - Parameters:
16 | /// - corners: An array that takes the regions to be cornered
17 | /// - radius: Affects each selected corner
18 | func roundCorners(corners: UIRectCorner, radius: CGFloat) {
19 | let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
20 | let mask = CAShapeLayer()
21 | mask.path = path.cgPath
22 |
23 | layer.mask = mask
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Jottre/Models/Node/NodeCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCodable.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 30.01.21.
6 | //
7 |
8 | import Foundation
9 | import PencilKit
10 |
11 |
12 | /// V1 of the NodeCodable The content of this struct will be serialized to a binary file
13 | struct NodeCodableV1: Codable {
14 |
15 | var drawing: PKDrawing = PKDrawing()
16 |
17 | var width: CGFloat = 1200
18 |
19 | }
20 |
21 |
22 | /// V2 of the NodeCodable The content of this struct will be serialized to a binary file
23 | struct NodeCodableV2: Codable {
24 |
25 | var drawing: PKDrawing = PKDrawing()
26 |
27 | var width: CGFloat = 1200
28 |
29 | var version: Int = 0
30 |
31 | var lastModified: Double = NSDate().timeIntervalSince1970
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Jottre/Controllers/SettingsScene/SettingsNavigationViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsNavigationViewController.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 17.01.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class SettingsNavigationViewController: UINavigationController {
12 |
13 | // MARK: - Override methods
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 |
18 | setupViews()
19 |
20 | }
21 |
22 |
23 |
24 | // MARK: - Methods
25 |
26 | func setupViews() {
27 |
28 | modalPresentationStyle = .formSheet
29 |
30 | navigationItem.title = "Settings"
31 | navigationBar.prefersLargeTitles = true
32 |
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Jottre/Jottre.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.icloud-container-identifiers
6 |
7 | iCloud.com.antonlorani.jottre
8 |
9 | com.apple.developer.icloud-services
10 |
11 | CloudDocuments
12 |
13 | com.apple.developer.ubiquity-container-identifiers
14 |
15 | iCloud.com.antonlorani.jottre
16 |
17 | com.apple.security.app-sandbox
18 |
19 | com.apple.security.files.user-selected.read-write
20 |
21 | com.apple.security.network.client
22 |
23 | com.apple.security.personal-information.photos-library
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Jottre/Views/BarButtonItems/RedoButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RedoButton.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 02.03.21.
6 | //
7 |
8 | import UIKit
9 |
10 | class RedoButton: CustomButtonBarItem {
11 |
12 | // MARK: - Properties
13 |
14 | override var isEnabled: Bool {
15 | didSet {
16 | if isEnabled {
17 | backgroundColor = .systemGray5
18 | tintColor = .lightGray
19 | } else {
20 | backgroundColor = .systemGray4
21 | tintColor = .systemGray
22 | }
23 | }
24 | }
25 |
26 |
27 |
28 | // MARK: - Override Methods
29 |
30 | override func setupViews() {
31 | super.setupViews()
32 |
33 | setImage(UIImage(systemName: "arrow.uturn.forward", withConfiguration: UIImage.SymbolConfiguration(pointSize:15, weight: .semibold)), for: .normal)
34 | isEnabled = !(!isEnabled)
35 |
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Jottre/Views/BarButtonItems/UndoButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UndoButton.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 02.03.21.
6 | //
7 |
8 | import UIKit
9 |
10 | class UndoButton: CustomButtonBarItem {
11 |
12 | // MARK: - Properties
13 |
14 | override var isEnabled: Bool {
15 | didSet {
16 |
17 | if isEnabled {
18 | backgroundColor = .systemGray5
19 | tintColor = .lightGray
20 | } else {
21 | backgroundColor = .systemGray4
22 | tintColor = .systemGray
23 | }
24 | }
25 | }
26 |
27 |
28 |
29 | // MARK: - Override Methods
30 |
31 | override func setupViews() {
32 | super.setupViews()
33 |
34 | setImage(UIImage(systemName: "arrow.uturn.backward", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .semibold)), for: .normal)
35 | isEnabled = !(!isEnabled)
36 |
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/Jottre/Views/SettingsCell/IconSettingsCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IconSettingsCell.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 18.01.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class IconSettingsCell: SettingsCell {
12 |
13 | override func setupViews() {
14 | super.setupViews()
15 |
16 | imageView.constraints.forEach({ imageView.removeConstraint($0) })
17 |
18 | imageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
19 | imageView.rightAnchor.constraint(equalTo: rightAnchor, constant: -10).isActive = true
20 | imageView.widthAnchor.constraint(equalToConstant: 40).isActive = true
21 | imageView.heightAnchor.constraint(equalToConstant: 40).isActive = true
22 |
23 | imageView.layer.cornerRadius = 10
24 | imageView.layer.masksToBounds = true
25 | imageView.layer.borderWidth = 1
26 | imageView.layer.borderColor = UIColor.label.cgColor
27 |
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/Jottre/Models/Node/NodeDecoderExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeDecoderExtension.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 30.01.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 | import OSLog
11 |
12 | extension Node {
13 |
14 | // FIXME: - Is there a way to do this recursive?
15 |
16 | /// Decodes the latest NodeCodable from given data
17 | /// - Parameter data: Data of a NodeCodable object
18 | /// - Returns: NodeCodable2
19 | func decode(from data: Data) -> NodeCodableV2? {
20 | let decoder = PropertyListDecoder()
21 |
22 | do {
23 | return try decoder.decode(NodeCodableV2.self, from: data)
24 | } catch {
25 |
26 | do {
27 | let codableV1 = try decoder.decode(NodeCodableV1.self, from: data)
28 | return NodeCodableV2(drawing: codableV1.drawing, width: codableV1.width, version: 1, lastModified: NSDate().timeIntervalSince1970)
29 | } catch {
30 | Logger.main.error("Could not decode data as NodeCodable(V1, V2)")
31 | return nil
32 | }
33 |
34 | }
35 |
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Jottre/Extensions/String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 |
10 | extension String {
11 |
12 | /// Computes the width of the text inside a UILabel
13 | /// - Parameter label: UILabel that will contain the text
14 | /// - Returns: Width in px
15 | func width(inside label: UILabel) -> CGFloat {
16 | guard let font = label.font else {
17 | return 0
18 | }
19 |
20 | let fontAttributes = [NSAttributedString.Key.font: font]
21 | let size = self.size(withAttributes: fontAttributes)
22 |
23 | return size.width
24 | }
25 |
26 |
27 | /// Computes the width of the text inside a UIButton
28 | /// - Parameter label: UIButton that will contain the text
29 | /// - Returns: Width in px
30 | func width(inside button: UIButton) -> CGFloat {
31 | guard let font = button.titleLabel?.font else {
32 | return 0
33 | }
34 |
35 | let fontAttributes = [NSAttributedString.Key.font: font]
36 | let size = self.size(withAttributes: fontAttributes)
37 |
38 | return size.width
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Jottre/Views/BarButtonItems/CustomButtonBarItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomButtonBarItem.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 07.02.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class CustomButtonBarItem: UIButton {
12 |
13 | // MARK: - Init
14 |
15 | init(target: Any?, action: Selector?) {
16 | super.init(frame: CGRect(origin: .zero, size: CGSize(width: 30, height: 30)))
17 |
18 | if let target = target, let action = action {
19 | self.addTarget(target, action: action, for: .touchUpInside)
20 | }
21 |
22 | }
23 |
24 | required init?(coder: NSCoder) {
25 | fatalError("init(coder:) has not been implemented")
26 | }
27 |
28 |
29 |
30 | // MARK: - Override methods
31 |
32 | override func didMoveToSuperview() {
33 | super.didMoveToSuperview()
34 | setupViews()
35 | }
36 |
37 |
38 |
39 | // MARK: - Methods
40 |
41 | func setupViews() {
42 |
43 | backgroundColor = .systemBlue
44 |
45 | tintColor = UIColor.white
46 |
47 | layer.cornerRadius = 15
48 | layer.masksToBounds = true
49 |
50 | }
51 |
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/Jottre/Extensions/PKDrawing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PKDrawing.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 21.01.21.
6 | //
7 |
8 | import PencilKit
9 |
10 | extension PKDrawing {
11 |
12 | /// Converts PKDrawing to a UIImage using a presetted appearance
13 | /// NOTE: - The returned image has 3 channels (4th channel (alpha) will be just white color)
14 | /// - Parameters:
15 | /// - rect: The portion of the drawing that you want to capture. Specify a rectangle in the canvas' coordinate system.
16 | /// - scale: The scale factor at which to create the image. Specifying scale factors greater than 1.0 creates an image with more detail. For example, you might specify a scale factor of 2.0 or 3.0 when displaying the image on a Retina display.
17 | /// - userInterfaceStyle: Prefered user-interface style such as dark or light
18 | /// - Returns:
19 | func image(from rect: CGRect, scale: CGFloat, userInterfaceStyle: UIUserInterfaceStyle) -> UIImage {
20 | let currentTraits = UITraitCollection.current
21 | UITraitCollection.current = UITraitCollection(userInterfaceStyle: userInterfaceStyle)
22 |
23 | let image = self.image(from: rect, scale: scale)
24 | UITraitCollection.current = currentTraits
25 |
26 | return image
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Jottre/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 | import OSLog
10 |
11 | @main
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | // Override point for customization after application launch.
16 | return true
17 | }
18 |
19 |
20 |
21 | // MARK: UISceneSession Lifecycle
22 |
23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
24 | // Called when a new scene session is being created.
25 | // Use this method to select a configuration to create the new scene with.
26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
27 | }
28 |
29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
30 | // Called when the user discards a scene session.
31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
33 | }
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/Jottre/Extensions/URL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URK.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 21.01.21.
6 | //
7 |
8 | import Foundation
9 |
10 | extension URL {
11 |
12 | /// Checks if the file-path is a valid Jot
13 | /// - Returns: True if file is a potential jot
14 | func isJot() -> Bool {
15 | return self.pathExtension == "jot"
16 | }
17 |
18 |
19 | /// Checks if the file-path is a valid icloud file
20 | /// - Returns: True if file is a potential cloud file (Which then needs to be downloaded)
21 | func isCloud() -> Bool {
22 | return self.pathExtension == "icloud"
23 | }
24 |
25 |
26 | /// Converts the ugly iCloud file-path to a valid .jot path
27 | /// - Returns: Actual file-path for a readable node
28 | func cloudToJot() -> URL {
29 | if !isCloud() { return self }
30 |
31 | var tmpURL = self.deletingPathExtension()
32 |
33 | var tmpFileName = tmpURL.lastPathComponent
34 | tmpFileName.removeFirst()
35 |
36 | tmpURL.deleteLastPathComponent()
37 | tmpURL.appendPathComponent(tmpFileName)
38 |
39 | return tmpURL
40 | }
41 |
42 |
43 | /// Checks if the file-path is a valid icloud and jot file
44 | /// - Returns: True if file is a potential cloud file (Which then needs to be downloaded)
45 | func isCloudAndJot() -> Bool {
46 | if self.isCloud() {
47 | let tmpURL = self.deletingPathExtension()
48 | return tmpURL.isJot()
49 | }
50 | return false
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Jottre/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Jottre/Jottre/en.xcloc/Source Contents/Jottre/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Jottre/Views/SettingsCell/AppearanceSettingsCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppearanceSettingsCell.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 17.01.21.
6 | //
7 |
8 | import UIKit
9 |
10 | class AppearanceSettingsCell: SettingsCell {
11 |
12 | // MARK: - Subviews
13 |
14 | var colorLabel: UILabel = {
15 | let darkAppearance = NSLocalizedString("Dark", comment: "Dark appearance")
16 | let systemAppearance = NSLocalizedString("System", comment: "System appearance")
17 |
18 | var title = NSLocalizedString("Light", comment: "Light appearance")
19 |
20 | if settings.codable.preferedAppearance == 0 {
21 | title = darkAppearance
22 | } else if settings.codable.preferedAppearance == 2 {
23 | title = systemAppearance
24 | }
25 |
26 | let label = UILabel()
27 | label.translatesAutoresizingMaskIntoConstraints = false
28 | label.font = UIFont.systemFont(ofSize: 20, weight: .regular)
29 | label.text = title
30 | label.textColor = .secondaryLabel
31 | return label
32 | }()
33 |
34 |
35 |
36 | // MARK: - Override methods
37 |
38 | override func setupViews() {
39 | super.setupViews()
40 |
41 | addSubview(colorLabel)
42 | colorLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
43 | colorLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -10).isActive = true
44 | colorLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
45 | colorLabel.heightAnchor.constraint(equalToConstant: 40).isActive = true
46 |
47 | title = NSLocalizedString("Interface appearance", comment: "")
48 |
49 | }
50 |
51 | }
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Jottre/Views/BarButtonItems/NavigationTextButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationButton.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 |
10 | class NavigationTextButton: UILabel {
11 |
12 | // MARK: - Properties
13 |
14 | var title: String = "Button"
15 |
16 | private var tapGesture: UITapGestureRecognizer!
17 |
18 |
19 |
20 | // MARK: - Init
21 |
22 | init(title: String, target: Any, action: Selector) {
23 | super.init(frame: CGRect(origin: .zero, size: CGSize(width: 0, height: 30)))
24 |
25 | self.title = title
26 |
27 | self.tapGesture = UITapGestureRecognizer(target: target, action: action)
28 | self.tapGesture.numberOfTouchesRequired = 1
29 | self.tapGesture.numberOfTapsRequired = 1
30 |
31 | }
32 |
33 | required init?(coder: NSCoder) {
34 | fatalError("init(coder:) has not been implemented")
35 | }
36 |
37 |
38 |
39 | // MARK: - Override methods
40 |
41 | override func didMoveToSuperview() {
42 | super.didMoveToSuperview()
43 | setupViews()
44 | }
45 |
46 |
47 |
48 | // MARK: - Methods
49 |
50 | func setupViews() {
51 |
52 | addGestureRecognizer(tapGesture)
53 |
54 | backgroundColor = .systemBlue
55 |
56 | isUserInteractionEnabled = true
57 |
58 | textColor = .white
59 | text = title
60 | textAlignment = .center
61 |
62 | font = UIFont.boldSystemFont(ofSize: 18)
63 |
64 | layer.cornerRadius = 15
65 | layer.masksToBounds = true
66 |
67 | frame.size.width = title.width(inside: self) + 20
68 |
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/Jottre/Helpers/ThumbnailGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThumbnailGenerator.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 | import OSLog
10 |
11 | class ThumbnailGenerator {
12 |
13 | // MARK: - Properties
14 |
15 | private let thumbnailQueue = DispatchQueue(label: "ThumbnailQueue", qos: .default)
16 |
17 | var size: CGSize
18 |
19 |
20 |
21 | // MARK: - Init
22 |
23 | /// Initializer of the ThumbnailGenerator class
24 | /// - Parameter size: Target-size (width, height) of thumbnail
25 | init(size: CGSize) {
26 | self.size = size
27 | }
28 |
29 |
30 |
31 | // MARK: - Methods
32 |
33 | /// Creates thumbnail for a given node
34 | /// - Parameters:
35 | /// - node: Object of class Node. Requires that nodeCodable.drawing is not nil
36 | /// - completion: A boolean that indicates success or failure and the resulting image (nil on failure)
37 | func execute(for node: Node, _ completion: @escaping (_ success: Bool, _ image: UIImage?) -> Void) {
38 |
39 | thumbnailQueue.async {
40 |
41 | guard let drawing = node.codable?.drawing, let width = node.codable?.width else {
42 | Logger.main.error("Cannot create thumbnail")
43 | completion(false, nil)
44 | return
45 | }
46 |
47 | let aspectRatio = self.size.width / self.size.height
48 |
49 | let thumbnailRect = CGRect(x: 0, y: 0, width: width, height: width / aspectRatio)
50 |
51 | let thumbnailScale = UIScreen.main.scale * self.size.width / width
52 |
53 | let traitCollection = UITraitCollection(userInterfaceStyle: settings.preferredUserInterfaceStyle())
54 |
55 | traitCollection.performAsCurrent {
56 | let image = drawing.image(from: thumbnailRect, scale: thumbnailScale)
57 | completion(true, image)
58 | }
59 |
60 | }
61 |
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Jottre/Views/SettingsCell/CloudSettingsCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CloudSettingsCell.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 17.01.21.
6 | //
7 |
8 | import UIKit
9 | import OSLog
10 |
11 | protocol CloudSettingsCellDelegate {
12 |
13 | func didMoveSwitch(cell: CloudSettingsCell, to state: Bool)
14 |
15 | }
16 |
17 |
18 | class CloudSettingsCell: SettingsCell {
19 |
20 | // MARK: - Properties
21 |
22 | var delegate: CloudSettingsCellDelegate?
23 |
24 | var usesCloud: Bool = false {
25 | didSet {
26 | switchView.setOn(usesCloud, animated: true)
27 | }
28 | }
29 |
30 |
31 |
32 | // MARK: - Subviews
33 |
34 | var switchView: UISwitch = {
35 | let switchView = UISwitch()
36 | switchView.translatesAutoresizingMaskIntoConstraints = false
37 | switchView.onTintColor = UIColor.systemBlue
38 | return switchView
39 | }()
40 |
41 |
42 |
43 | // MARK: - Override methods
44 |
45 | override func setupViews() {
46 | super.setupViews()
47 |
48 | addSubview(switchView)
49 | switchView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
50 | switchView.rightAnchor.constraint(equalTo: rightAnchor, constant: -10).isActive = true
51 | switchView.widthAnchor.constraint(greaterThanOrEqualToConstant: 10).isActive = true
52 | switchView.heightAnchor.constraint(greaterThanOrEqualToConstant: 10).isActive = true
53 |
54 | title = NSLocalizedString("Synchronize with iCloud", comment: "")
55 |
56 | if UIDevice.isLimited() || !Downloader.isCloudEnabled {
57 | switchView.isEnabled = false
58 | }
59 |
60 | setupDelegates()
61 |
62 | }
63 |
64 |
65 | func setupDelegates() {
66 | switchView.addTarget(self, action: #selector(self.handleSwitch), for: .valueChanged)
67 | }
68 |
69 |
70 | @objc func handleSwitch() {
71 | delegate?.didMoveSwitch(cell: self, to: switchView.isOn)
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | **Available on the [App Store](https://apps.apple.com/us/app/jottre/id1550272319)**
10 |
11 | # Jottre
12 |
13 | Simple and minimalistic handwriting app for iPadOS (and iOS, macOS).
14 |
15 | At the moment the full version of Jottre only supports iPadOS. This means that 'Jottre for iPhone' and 'Jottre for Mac' are intended as read-only applications.
16 |
17 |
18 |
19 | ## Features
20 |
21 | - Builds on the powerful PencilKit framework (iPadOS only)
22 | - iCloud documents synchronization
23 | - Support for dark and light mode
24 | - Minimalistic design
25 |
26 |
27 |
28 | ## Update 1.5
29 |
30 | - Added support for multi-window
31 |
32 |
33 | ## Preview
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ## Known issues
54 |
55 | - [x] Drawings are not rendered in the correct userInterfaceStyle (when starting the app)
56 | - [x] iCloud synchronization is not fast enough (documents are not uploaded directly)
57 | - [x] Animation of CollectionViewCells (Deleting, Inserting...) not implemented
58 | - [x] Deleting items takes too much time
59 | - [x] If removing items from collectionview, certain thumbnails were not shown correctly
60 | - [x] Changing the userInterface style will not affect upcoming application-scenes
61 | - [x] As the ViewControllers widths shrunk the PencilKit Toolbar removes the forward/backwards button
62 |
63 | ## Up next
64 |
65 | - Support for Widgets
66 | > How to render the thumbnails, so that most of the content is visible inside the viewable Widget area
67 |
68 | - PDF Viewer
69 | - PDF AnkAnnotations (Drawing on PDF via PDFKit)
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | `© Anton Lorani, 2021`
80 |
--------------------------------------------------------------------------------
/Jottre/Views/SettingsCell/SettingsCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsCell.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 17.01.21.
6 | //
7 |
8 | import UIKit
9 |
10 | class SettingsCell: UICollectionViewCell {
11 |
12 | // MARK: - Properties
13 |
14 | var title: String = "Label" {
15 | didSet {
16 | titleLabel.text = title
17 | }
18 | }
19 |
20 | var image: UIImage = UIImage() {
21 | didSet {
22 | imageView.image = image
23 | }
24 | }
25 |
26 |
27 |
28 | // MARK: - Subviews
29 |
30 | var titleLabel: UILabel = {
31 | let label = UILabel()
32 | label.translatesAutoresizingMaskIntoConstraints = false
33 | label.text = "Label"
34 | label.font = UIFont.systemFont(ofSize: 19, weight: .bold)
35 | return label
36 | }()
37 |
38 | var imageView: UIImageView = {
39 | let imageView = UIImageView()
40 | imageView.translatesAutoresizingMaskIntoConstraints = false
41 | imageView.tintColor = UIColor.gray
42 | return imageView
43 | }()
44 |
45 |
46 |
47 | // MARK: - Override methods
48 |
49 | override func didMoveToSuperview() {
50 | super.didMoveToSuperview()
51 |
52 | setupViews()
53 |
54 | }
55 |
56 |
57 |
58 | // MARK: - Methods
59 |
60 | func setupViews() {
61 |
62 | backgroundColor = .systemGray5
63 |
64 | layer.cornerRadius = 15
65 | layer.masksToBounds = true
66 |
67 | addSubview(titleLabel)
68 | titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
69 | titleLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 10).isActive = true
70 | titleLabel.heightAnchor.constraint(equalToConstant: 50).isActive = true
71 | titleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
72 |
73 | addSubview(imageView)
74 | imageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
75 | imageView.rightAnchor.constraint(equalTo: rightAnchor, constant: -10).isActive = true
76 | imageView.widthAnchor.constraint(equalToConstant: 35).isActive = true
77 | imageView.heightAnchor.constraint(equalToConstant: 35).isActive = true
78 |
79 | }
80 |
81 | }
82 |
83 |
--------------------------------------------------------------------------------
/Jottre/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_20x20@2x.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "icon_20x20@3x.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "icon_29x29@2x.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "icon_29x29@3x.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "icon_40x40@2x.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "icon_40x40@3x.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "icon_60x60@2x.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "icon_60x60@3x.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "icon_20x20.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "icon_20x20@2x-1.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "icon_29x29.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "icon_29x29@2x-1.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "icon_40x40.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "icon_40x40@2x-1.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "icon_76x76.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "icon_76x76@2x.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "icon_83.5x83.5@2x.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "icon_1024x1024.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | }
111 | ],
112 | "info" : {
113 | "author" : "xcode",
114 | "version" : 1
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Jottre/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Jottre
4 |
5 | Created by Anton Lorani on 17.01.21.
6 |
7 | German
8 |
9 | */
10 |
11 | /// - Alert
12 |
13 | "Cancel" = "Cancel";
14 |
15 |
16 | /// - Files
17 |
18 | "My note" = "My note";
19 |
20 | "(copy)" = "(copy)";
21 |
22 |
23 |
24 | /// - Appearance
25 |
26 | "Dark" = "Dark";
27 |
28 | "Light" = "Light";
29 |
30 | "System" = "System";
31 |
32 |
33 | /// - Actions
34 |
35 | "Export note" = "Export note";
36 |
37 | "Add note" = "Add note";
38 |
39 | "Save" = "Save";
40 |
41 | "Share" = "Share";
42 |
43 | "Create" = "Create";
44 |
45 | "Delete" = "Delete";
46 |
47 | "Rename" = "Rename";
48 |
49 | "Edit" = "Edit";
50 |
51 |
52 |
53 | /// - Create alert
54 |
55 | "New note" = "New note";
56 |
57 | "Enter a name for the new note" = "Enter a name for the new note";
58 |
59 |
60 |
61 | /// - Rename alert
62 |
63 | "Rename note" = "Rename note";
64 |
65 | "Enter a name for the selected note" = "Enter a name for the selected note";
66 |
67 |
68 |
69 | /// - Info View
70 |
71 | "No documents available yet. Click 'Add note' to create a new file." = "No documents available yet. Click 'Add note' to create a new file.";
72 |
73 | "Documents created with the 'Jottre for iPad' App can be viewed here." = "Documents created with the 'Jottre for iPad' App can be viewed here.";
74 |
75 | "Enable iCloud to view files created with 'Jottre for iPad'" = "Enable iCloud to view files created with 'Jottre for iPad'";
76 |
77 | "Enable iCloud to unlock the full potential of Jottre" = "Enable iCloud to unlock the full potential of Jottre";
78 |
79 |
80 |
81 | /// - Info Alert iCloud
82 |
83 | "iCloud disabled" = "iCloud disabled";
84 |
85 | "While iCloud is disabled, you can only open files that are locally on this device." = "While iCloud is disabled, you can only open files that are locally on this device.";
86 |
87 | "How to enable iCloud" = "How to enable iCloud";
88 |
89 | "https://support.apple.com/en-us/HT208681" = "https://support.apple.com/en-us/HT208681";
90 |
91 |
92 |
93 | /// - File-Conflict alert
94 |
95 | "File conflict found" = "File conflict found";
96 |
97 | "The file could not be saved. It seems that the original file (%d.jot) on the disk has changed. (Maybe it was edited on another device at the same time?). Use one of the following options to fix the problem." = "The file could not be saved. It seems that the original file (%d.jot) on the disk has changed. (Maybe it was edited on another device at the same time?). Use one of the following options to fix the problem.";
98 |
99 | "Overwrite" = "Overwrite";
100 |
101 | "Close without saving" = "Close without saving";
102 |
103 |
104 |
105 | /// - Settings
106 |
107 | "Settings" = "Settings";
108 |
109 | "Interface appearance" = "Interface appearance";
110 |
111 | "Synchronize with iCloud" = "Synchronize with iCloud";
112 |
--------------------------------------------------------------------------------
/Jottre/Views/LoadingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoadingView.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 21.01.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class LoadingView: UIView {
12 |
13 | // MARK: - Properties
14 |
15 | var isAnimating: Bool = false {
16 | didSet {
17 | isAnimating ? activityIndicatorView.startAnimating() : activityIndicatorView.stopAnimating()
18 |
19 | UIView.animate(withDuration: 0.3) {
20 | self.transform = self.isAnimating ? .identity : CGAffineTransform(scaleX: 0.9, y: 0.9)
21 | self.alpha = self.isAnimating ? 1 : 0
22 | }
23 |
24 | }
25 | }
26 |
27 |
28 |
29 | // MARK: - Subviews
30 |
31 | var blurView: UIVisualEffectView = {
32 | let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
33 | view.translatesAutoresizingMaskIntoConstraints = false
34 | return view
35 | }()
36 |
37 | var activityIndicatorView: UIActivityIndicatorView = {
38 | let activityIndicatorView = UIActivityIndicatorView(style: .large)
39 | activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
40 | return activityIndicatorView
41 | }()
42 |
43 |
44 |
45 | // MARK: - Override methods
46 |
47 | override func didMoveToSuperview() {
48 | super.didMoveToSuperview()
49 |
50 | setupViews()
51 |
52 | }
53 |
54 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
55 |
56 | layer.shadowColor = UIColor.label.cgColor
57 | backgroundColor = traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? UIColor.secondarySystemBackground : UIColor.systemBackground
58 |
59 | }
60 |
61 |
62 |
63 | // MARK: - Methods
64 |
65 | func setupViews() {
66 |
67 | isAnimating = false
68 |
69 | translatesAutoresizingMaskIntoConstraints = false
70 |
71 | backgroundColor = .clear
72 |
73 | layer.cornerRadius = 15
74 | layer.masksToBounds = true
75 |
76 | addSubview(blurView)
77 | blurView.topAnchor.constraint(equalTo: topAnchor).isActive = true
78 | blurView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
79 | blurView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
80 | blurView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
81 |
82 | addSubview(activityIndicatorView)
83 | activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
84 | activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
85 | activityIndicatorView.widthAnchor.constraint(equalToConstant: 90).isActive = true
86 | activityIndicatorView.heightAnchor.constraint(equalToConstant: 90).isActive = true
87 |
88 | }
89 |
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/Jottre/de.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | Jottre
4 |
5 | Created by Anton Lorani on 17.01.21.
6 |
7 | German
8 |
9 | */
10 |
11 | /// - Alert
12 |
13 | "Cancel" = "Abbrechen";
14 |
15 |
16 | /// - Files
17 |
18 | "My note" = "Meine Notiz";
19 |
20 | "(copy)" = "(Kopie)";
21 |
22 |
23 | /// - Appearance
24 |
25 | "Dark" = "Dunkel";
26 |
27 | "Light" = "Hell";
28 |
29 | "System" = "Automatisch";
30 |
31 |
32 | /// - Actions
33 |
34 | "Export note" = "Exportieren";
35 |
36 | "Add note" = "Hinzufügen";
37 |
38 | "Save" = "Sichern";
39 |
40 |
41 | "Share" = "Teilen";
42 |
43 | "Create" = "Erstellen";
44 |
45 | "Delete" = "Löschen";
46 |
47 | "Rename" = "Umbenennen";
48 |
49 | "Edit" = "Bearbeiten";
50 |
51 |
52 |
53 | /// - Create alert
54 |
55 | "New note" = "Neue Notiz";
56 |
57 | "Enter a name for the new note" = "Gebe einen Namen für die neue Notiz ein";
58 |
59 |
60 |
61 | /// - Rename alert
62 |
63 | "Rename note" = "Notiz neubenennen";
64 |
65 | "Enter a name for the selected note" = "Gebe einen neuen Namen für die neue Notiz ein";
66 |
67 |
68 |
69 | /// - Info View
70 |
71 | "No documents available yet. Click 'Add note' to create a new file." = "Noch keine Dokumente vorhanden. Klicke auf 'Hinzufügen', um eine neue Datei zu erstellen.";
72 |
73 | "Documents created with the 'Jottre for iPad' App can be viewed here." = "Dokumente die mit der 'Jottre for iPad' App erstellt wurden, werden hier angezeigt.";
74 |
75 | "Enable iCloud to view files created with 'Jottre for iPad'" = "Aktiviere iCloud, damit Dokumente die mit 'Jottre for iPad' erstellt wurden hier angezeigt werden können";
76 |
77 | "Enable iCloud to unlock the full potential of Jottre" = "Aktiviere iCloud um das volle potenzial von Jottre auszuschöpfen";
78 |
79 |
80 |
81 | /// - Info Alert iCloud
82 |
83 | "iCloud disabled" = "iCloud inaktiv";
84 |
85 | "While iCloud is disabled, you can only open files that are locally on this device." = "Während iCloud deaktiviert ist, können nur Dateien geöffnet werden, die sich lokal auf diesem Gerät befinden.";
86 |
87 | "How to enable iCloud" = "Wie aktiviere ich iCloud";
88 |
89 | "https://support.apple.com/en-us/HT208681" = "https://support.apple.com/de-de/HT208681";
90 |
91 |
92 |
93 | /// - File-Conflict alert
94 |
95 | "File conflict found" = "Dateikonflikt gefunden";
96 |
97 | "The file could not be saved. It seems that the original file (%d.jot) on the disk has changed. (Maybe it was edited on another device at the same time?). Use one of the following options to fix the problem." = "Die Datei konnte nicht gespeichert werden. Es scheint, als hätte sich die Originaldatei (%d.jot) auf dem Datenträger geändert. (Vielleicht wurde sie zur gleichen Zeit auf einem anderen Gerät bearbeitet?). Verwende eine der folgenden Optionen, um das Problem zu beheben.";
98 |
99 | "Overwrite" = "Überschreiben";
100 |
101 | "Close without saving" = "Schließen ohne Speichern";
102 |
103 |
104 |
105 | /// - Settings
106 |
107 | "Settings" = "Einstellungen";
108 |
109 | "Interface appearance" = "Erscheinungsbild";
110 |
111 | "Synchronize with iCloud" = "Mit iCloud synchronisieren";
112 |
--------------------------------------------------------------------------------
/Jottre/Controllers/InitialScene/MenuActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuActions.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 | import OSLog
10 |
11 | extension InitialViewController {
12 |
13 | func createEditAction(indexPath: IndexPath) -> UIAction {
14 |
15 | let localizedAlertActionTitle = NSLocalizedString("Edit", comment: "")
16 |
17 | return UIAction(title: localizedAlertActionTitle, image: UIImage(systemName: "square.and.pencil")) { (action) in
18 | self.collectionView.delegate?.collectionView?(self.collectionView, didSelectItemAt: indexPath)
19 | }
20 | }
21 |
22 |
23 | func createRenameAction(indexPath: IndexPath) -> UIAction {
24 |
25 | let localizedAlertTitle = NSLocalizedString("Rename note", comment: "")
26 |
27 | let localizedAlertMessage = NSLocalizedString("Enter a name for the selected note", comment: "")
28 |
29 | let localizedAlertPrimaryActionTitle = NSLocalizedString("Rename", comment: "")
30 |
31 | let localizedAlertSecondaryActionTitle = NSLocalizedString("Cancel", comment: "")
32 |
33 | return UIAction(title: localizedAlertPrimaryActionTitle, image: UIImage(systemName: "rectangle.and.pencil.and.ellipsis")) { (action) in
34 |
35 | guard let currentName = self.nodeCollector.nodes[indexPath.row].name, let url = self.nodeCollector.nodes[indexPath.row].url else {
36 | return
37 | }
38 |
39 | let alertController = UIAlertController(title: localizedAlertTitle, message: localizedAlertMessage, preferredStyle: .alert)
40 |
41 | alertController.addTextField { (textField) in
42 | textField.placeholder = currentName
43 | }
44 |
45 | alertController.addAction(UIAlertAction(title: localizedAlertPrimaryActionTitle, style: UIAlertAction.Style.default, handler: { (action) in
46 |
47 | guard let textFields = alertController.textFields, var updatedName = textFields[0].text else {
48 | return
49 | }
50 | updatedName = updatedName == "" ? currentName : updatedName
51 |
52 | self.nodeCollector.nodes[indexPath.row].rename(to: NodeCollector.computeCopyName(baseName: updatedName, path: url.deletingLastPathComponent()))
53 |
54 | }))
55 | alertController.addAction(UIAlertAction(title: localizedAlertSecondaryActionTitle, style: .cancel, handler: nil))
56 |
57 | self.present(alertController, animated: true, completion: nil)
58 |
59 | }
60 | }
61 |
62 |
63 | func createDeleteAction(indexPath: IndexPath) -> UIMenu {
64 | let (title, image) = (NSLocalizedString("Delete", comment: ""), UIImage(systemName: "trash"))
65 |
66 | let confirmDeleteAction = UIAction(title: "\(title)?", image: image, attributes: .destructive) { (action) in
67 |
68 | self.nodeCollector.nodes[indexPath.row].delete()
69 |
70 | }
71 |
72 | return UIMenu(title: title, image: image, options: .destructive, children: [confirmDeleteAction])
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Jottre/Helpers/Downloader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Downloader.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 22.01.21.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | class Downloader {
12 |
13 | // MARK: - Properties
14 |
15 | var url: URL?
16 |
17 | static var isCloudEnabled: Bool {
18 | return FileManager.default.url(forUbiquityContainerIdentifier: nil) != nil
19 | }
20 |
21 |
22 |
23 | // MARK: - Init
24 |
25 | init(url: URL) {
26 | self.url = url
27 | }
28 |
29 |
30 |
31 | // MARK: - Methods
32 |
33 | /// Starts force downloads file from iCloud Drive (If file exists)
34 | /// - Parameter completion: Returns true if download is completed
35 | func execute(completion: ((Bool) -> Void)? = nil) {
36 |
37 | guard let url = self.url else {
38 | completion?(false)
39 | return
40 | }
41 |
42 | do {
43 | try FileManager.default.startDownloadingUbiquitousItem(at: url)
44 | handleProgress { (success) in
45 | if success {
46 | completion?(true)
47 | }
48 | }
49 | } catch {
50 | completion?(false)
51 | }
52 |
53 | }
54 |
55 |
56 | // TODO: - Add timeout
57 | /// Looks at the current progress of a downloading file (Initialized via startDownloadingUbiquitousItem)
58 | /// - Parameter completion: Returns true if download is completed. False if download failed
59 | private func handleProgress(completion: ((Bool) -> Void)? = nil) {
60 |
61 | guard let url = self.url else {
62 | completion?(false)
63 | return
64 | }
65 |
66 | DispatchQueue.main.async {
67 |
68 | Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
69 |
70 | guard let status = Downloader.getStatus(url: url) else {
71 | completion?(false)
72 | return
73 | }
74 |
75 | if status == URLUbiquitousItemDownloadingStatus.current {
76 | timer.invalidate()
77 | completion?(true)
78 | }
79 |
80 | })
81 |
82 | }
83 |
84 | }
85 |
86 |
87 | /// Method that returns the status of the current file-url (current, download or not downloaded)
88 | /// - Returns: URLUbiquitousItemDownloadingStatus. This value will indiciate the status of the file in the file-system
89 | static func getStatus(url: URL?) -> URLUbiquitousItemDownloadingStatus? {
90 |
91 | guard let url = url else {
92 | return nil
93 | }
94 |
95 | do {
96 | let attributes = try url.resourceValues(forKeys: [URLResourceKey.ubiquitousItemDownloadingStatusKey])
97 |
98 | guard let status: URLUbiquitousItemDownloadingStatus = attributes.allValues[URLResourceKey.ubiquitousItemDownloadingStatusKey] as? URLUbiquitousItemDownloadingStatus else {
99 | return nil
100 | }
101 |
102 | return status
103 | } catch {
104 | return nil
105 | }
106 |
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/Jottre/Controllers/SettingsScene/SettingsExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsExtensions.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 17.01.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | extension SettingsViewController: CloudSettingsCellDelegate {
12 |
13 | func didMoveSwitch(cell: CloudSettingsCell, to state: Bool) {
14 | settings.set(usesCloud: state)
15 | }
16 |
17 | }
18 |
19 | extension SettingsViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
20 |
21 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
22 | return CGSize(width: collectionView.frame.width - 40, height: 60)
23 | }
24 |
25 |
26 | func numberOfSections(in collectionView: UICollectionView) -> Int {
27 | return 1
28 | }
29 |
30 |
31 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
32 | return 3
33 | }
34 |
35 |
36 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
37 |
38 | if indexPath.row == 0 {
39 |
40 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "appearanceSettingsCell", for: indexPath) as? AppearanceSettingsCell else {
41 | fatalError("Cell is not of type AppearanceSettingsCell")
42 | }
43 | return cell
44 |
45 | } else if indexPath.row == 1 {
46 |
47 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cloudSettingsCell", for: indexPath) as? CloudSettingsCell else {
48 | fatalError("Cell is not of type CloudSettingsCell")
49 | }
50 | cell.delegate = self
51 | cell.usesCloud = UIDevice.isLimited() ? true : settings.codable.usesCloud
52 | cell.switchView.isEnabled = !(UIDevice.isLimited() || !Downloader.isCloudEnabled)
53 | return cell
54 |
55 | } else if indexPath.row == 2 {
56 |
57 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "settingsCell", for: indexPath) as? SettingsCell else {
58 | fatalError("Cell is not of type SettingsCell")
59 | }
60 | cell.title = "Github repository"
61 | cell.image = UIImage(systemName: "arrow.up.right.square.fill")!
62 | return cell
63 |
64 | }
65 |
66 | return UICollectionViewCell()
67 | }
68 |
69 |
70 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
71 |
72 | let cell = collectionView.cellForItem(at: indexPath)
73 |
74 | if indexPath.row == 0 {
75 |
76 | guard let cell = cell as? AppearanceSettingsCell else { return }
77 |
78 | if settings.codable.preferedAppearance == 0 {
79 | cell.colorLabel.text = NSLocalizedString("Light", comment: "Light appearance")
80 | settings.set(preferedAppearance: 1)
81 | } else if settings.codable.preferedAppearance == 1 {
82 | cell.colorLabel.text = NSLocalizedString("System", comment: "System appearance")
83 | settings.set(preferedAppearance: 2)
84 | } else if settings.codable.preferedAppearance == 2 {
85 | cell.colorLabel.text = NSLocalizedString("Dark", comment: "Dark appearance")
86 | settings.set(preferedAppearance: 0)
87 | }
88 |
89 | } else if indexPath.row == 2 {
90 |
91 | guard let url = URL(string: "https://github.com/AntonAmes/jottre") else { return }
92 | UIApplication.shared.open(url, options: [:], completionHandler: nil)
93 |
94 | }
95 |
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/Jottre/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDocumentTypes
8 |
9 |
10 | CFBundleTypeName
11 | Jottre
12 | LSHandlerRank
13 | Owner
14 | LSItemContentTypes
15 |
16 | com.antonlorani.jottre.jot
17 |
18 |
19 |
20 | CFBundleExecutable
21 | $(EXECUTABLE_NAME)
22 | CFBundleIdentifier
23 | $(PRODUCT_BUNDLE_IDENTIFIER)
24 | CFBundleInfoDictionaryVersion
25 | 6.0
26 | CFBundleName
27 | $(PRODUCT_NAME)
28 | CFBundlePackageType
29 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
30 | CFBundleShortVersionString
31 | $(MARKETING_VERSION)
32 | CFBundleVersion
33 | $(CURRENT_PROJECT_VERSION)
34 | ITSAppUsesNonExemptEncryption
35 |
36 | LSApplicationCategoryType
37 | public.app-category.productivity
38 | LSRequiresIPhoneOS
39 |
40 | LSSupportsOpeningDocumentsInPlace
41 |
42 | NSPhotoLibraryAddUsageDescription
43 | Jottre wants to store the image in your photo-library
44 | NSUbiquitousContainers
45 |
46 | iCloud.com.antonlorani.jottre
47 |
48 | NSUbiquitousContainerIsDocumentScopePublic
49 |
50 | NSUbiquitousContainerName
51 | Jottre
52 | NSUbiquitousContainerSupportedFolderLevels
53 | Any
54 |
55 |
56 | NSUserActivityTypes
57 |
58 | com.antonlorani.jottre.openDetail
59 |
60 | UIApplicationSceneManifest
61 |
62 | UIApplicationSupportsMultipleScenes
63 |
64 | UISceneConfigurations
65 |
66 | UIWindowSceneSessionRoleApplication
67 |
68 |
69 | UISceneConfigurationName
70 | Default Configuration
71 | UISceneDelegateClassName
72 | $(PRODUCT_MODULE_NAME).SceneDelegate
73 |
74 |
75 |
76 |
77 | UIApplicationSupportsIndirectInputEvents
78 |
79 | UIFileSharingEnabled
80 |
81 | UILaunchStoryboardName
82 | LaunchScreen
83 | UIRequiredDeviceCapabilities
84 |
85 | armv7
86 |
87 | UIRequiresFullScreen
88 |
89 | UIStatusBarHidden
90 |
91 | UISupportedInterfaceOrientations
92 |
93 | UIInterfaceOrientationPortrait
94 | UIInterfaceOrientationLandscapeLeft
95 | UIInterfaceOrientationLandscapeRight
96 | UIInterfaceOrientationPortraitUpsideDown
97 |
98 | UISupportedInterfaceOrientations~ipad
99 |
100 | UIInterfaceOrientationPortrait
101 | UIInterfaceOrientationPortraitUpsideDown
102 | UIInterfaceOrientationLandscapeLeft
103 | UIInterfaceOrientationLandscapeRight
104 |
105 | UTExportedTypeDeclarations
106 |
107 |
108 | UTTypeConformsTo
109 |
110 | public.data
111 | public.content
112 |
113 | UTTypeDescription
114 | Jottre document
115 | UTTypeIconFiles
116 |
117 | UTTypeIdentifier
118 | com.antonlorani.jottre.jot
119 | UTTypeTagSpecification
120 |
121 | public.filename-extension
122 |
123 | jot
124 |
125 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/Jottre/Controllers/SettingsScene/SettingsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsViewController.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 17.01.21.
6 | //
7 |
8 | import UIKit
9 |
10 | class SettingsViewController: UIViewController {
11 |
12 | // MARK: - Subviews
13 |
14 | var collectionView: UICollectionView = {
15 | let layout = UICollectionViewFlowLayout()
16 | layout.sectionInset = UIEdgeInsets(top: 10, left: 0, bottom: 30, right: 0)
17 | layout.minimumLineSpacing = 10
18 | layout.minimumInteritemSpacing = 10
19 |
20 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
21 | collectionView.translatesAutoresizingMaskIntoConstraints = false
22 | collectionView.backgroundColor = .clear
23 | collectionView.register(SettingsCell.self, forCellWithReuseIdentifier: "settingsCell")
24 | collectionView.register(AppearanceSettingsCell.self, forCellWithReuseIdentifier: "appearanceSettingsCell")
25 | collectionView.register(CloudSettingsCell.self, forCellWithReuseIdentifier: "cloudSettingsCell")
26 | collectionView.register(IconSettingsCell.self, forCellWithReuseIdentifier: "iconSettingsCell")
27 | return collectionView
28 | }()
29 |
30 | var versionLabel: UILabel = {
31 | let label = UILabel()
32 | label.translatesAutoresizingMaskIntoConstraints = false
33 | label.font = UIFont.systemFont(ofSize: 18, weight: .regular)
34 | label.textAlignment = .center
35 | label.text = "Preview v1.5"
36 | label.textColor = .secondaryLabel
37 | return label
38 | }()
39 |
40 |
41 |
42 | // MARK: - Override methods
43 |
44 | override func viewDidLoad() {
45 | super.viewDidLoad()
46 |
47 | setupViews()
48 | setupDelegates()
49 |
50 | }
51 |
52 |
53 |
54 | // MARK: - Methods
55 |
56 | func setupViews() {
57 |
58 | navigationItem.title = NSLocalizedString("Settings", comment: "")
59 |
60 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.handleDone))
61 |
62 | view.addSubview(collectionView)
63 | collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
64 | collectionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
65 | collectionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
66 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
67 |
68 | view.addSubview(versionLabel)
69 | versionLabel.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: -10).isActive = true
70 | versionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
71 | versionLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
72 | versionLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
73 |
74 | }
75 |
76 |
77 | func setupDelegates() {
78 |
79 | collectionView.delegate = self
80 | collectionView.dataSource = self
81 |
82 | NotificationCenter.default.addObserver(self, selector: #selector(settingsDidChange(_:)), name: Settings.didUpdateNotificationName, object: nil)
83 | settingsDidChange(Notification(name: Settings.didUpdateNotificationName, object: settings))
84 |
85 | }
86 |
87 |
88 | @objc func handleDone() {
89 | dismiss(animated: true, completion: nil)
90 | }
91 |
92 |
93 | @objc func settingsDidChange(_ notification: Notification) {
94 |
95 | guard let updatedSettings = notification.object as? Settings else { return }
96 |
97 | navigationController?.navigationBar.overrideUserInterfaceStyle = updatedSettings.preferredUserInterfaceStyle()
98 | overrideUserInterfaceStyle = updatedSettings.preferredUserInterfaceStyle()
99 | view.backgroundColor = updatedSettings.preferredUserInterfaceBackgroundColor()
100 |
101 | UIView.transition(with: view, duration: 0.5, options: .transitionCrossDissolve, animations: {
102 | self.view.overrideUserInterfaceStyle = updatedSettings.preferredUserInterfaceStyle()
103 | }, completion: nil)
104 |
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/Jottre/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 | import OSLog
10 |
11 | /// - Make the Settings of this App globally available
12 | let settings: Settings = Settings()
13 |
14 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
15 |
16 | var window: UIWindow?
17 |
18 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
19 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
20 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
21 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
22 | guard let windowScene = (scene as? UIWindowScene) else { return }
23 |
24 | #if targetEnvironment(macCatalyst)
25 | windowScene.titlebar?.toolbar?.isVisible = false
26 | windowScene.titlebar?.titleVisibility = .hidden
27 | #endif
28 |
29 | NotificationCenter.default.addObserver(self, selector: #selector(settingsDidChange(_:)), name: Settings.didUpdateNotificationName, object: nil)
30 |
31 | let initialController = InitialViewController()
32 | let initialNavigationController = NavigationViewController(rootViewController: initialController)
33 |
34 | window = UIWindow()
35 | window?.windowScene = windowScene
36 | window?.rootViewController = initialNavigationController
37 | window?.makeKeyAndVisible()
38 |
39 | presentDocument(urlContext: connectionOptions.urlContexts)
40 |
41 | settings.didUpdate()
42 |
43 | if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
44 | configure(window: window, with: userActivity)
45 | }
46 |
47 | }
48 |
49 | func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
50 | return scene.userActivity
51 | }
52 |
53 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) {
54 | presentDocument(urlContext: URLContexts)
55 | }
56 |
57 |
58 | func presentDocument(urlContext: Set) {
59 |
60 | guard let urlContext = urlContext.first else { return }
61 |
62 | let url = urlContext.url
63 | _ = url.startAccessingSecurityScopedResource()
64 |
65 | let node = Node(url: url)
66 | node.pull { (success) in
67 |
68 | if !success { return }
69 |
70 | DispatchQueue.main.async {
71 | let drawController = DrawViewController(node: node)
72 | self.window?.rootViewController?.children[0].navigationController?.pushViewController(drawController, animated: true)
73 | }
74 |
75 | }
76 |
77 | }
78 |
79 |
80 |
81 | // MARK: - Observer methods
82 |
83 | @objc func settingsDidChange(_ notification: Notification) {
84 |
85 | guard let window = window, let updatedSettings = notification.object as? Settings else { return }
86 |
87 | UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: {
88 | window.overrideUserInterfaceStyle = updatedSettings.preferredUserInterfaceStyle()
89 | }, completion: nil)
90 |
91 | }
92 |
93 |
94 |
95 | // MARK: - Drag methods
96 |
97 | func configure(window: UIWindow?, with activity: NSUserActivity) {
98 |
99 | if activity.title == Node.NodeOpenDetailPath {
100 |
101 | if let nodeURL = activity.userInfo?[Node.NodeOpenDetailActivityType] as? URL {
102 |
103 | let node = Node(url: nodeURL)
104 | node.pull { (success) in
105 | if !success { return }
106 |
107 | DispatchQueue.main.async {
108 |
109 | let drawViewController = DrawViewController(node: node)
110 |
111 | if let navigationController = window?.rootViewController as? UINavigationController {
112 | navigationController.pushViewController(drawViewController, animated: true)
113 | }
114 |
115 | }
116 |
117 | }
118 |
119 | }
120 |
121 | }
122 |
123 | }
124 |
125 |
126 | }
127 |
128 |
--------------------------------------------------------------------------------
/Jottre/Views/NodeCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCell.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 |
10 | class NodeCell: UICollectionViewCell {
11 |
12 | // MARK: - Properties
13 |
14 | var node: Node! {
15 | didSet {
16 |
17 | titleLabel.text = node?.name
18 | updateMeta()
19 |
20 | }
21 | }
22 |
23 |
24 |
25 | // MARK: - Subviews
26 |
27 | var titleLabel: UILabel = {
28 | let label = UILabel()
29 | label.translatesAutoresizingMaskIntoConstraints = false
30 | label.text = "Label"
31 | label.font = UIFont.systemFont(ofSize: 19, weight: .bold)
32 | return label
33 | }()
34 |
35 | var imageView: UIImageView = {
36 | let imageView = UIImageView()
37 | imageView.translatesAutoresizingMaskIntoConstraints = false
38 | imageView.contentMode = .scaleAspectFit
39 | imageView.roundCorners(corners: [.topLeft, .topRight], radius: 15)
40 | return imageView
41 | }()
42 |
43 | var overlay: UIView = {
44 | let view = UIView()
45 | view.translatesAutoresizingMaskIntoConstraints = false
46 | view.backgroundColor = UIColor.systemGray5
47 | view.roundCorners(corners: [.bottomLeft, .bottomRight], radius: 15)
48 | return view
49 | }()
50 |
51 |
52 |
53 | // MARK: - Override methods
54 |
55 | override func didMoveToSuperview() {
56 | super.didMoveToSuperview()
57 |
58 | setupViews()
59 |
60 | }
61 |
62 |
63 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
64 |
65 | backgroundColor = traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? UIColor.secondarySystemBackground : UIColor.systemBackground
66 |
67 | }
68 |
69 |
70 | override func prepareForReuse() {
71 | super.prepareForReuse()
72 |
73 | imageView.image = nil
74 | titleLabel.text = nil
75 |
76 | }
77 |
78 |
79 | override func layoutSubviews() {
80 | super.layoutSubviews()
81 |
82 | overlay.roundCorners(corners: [.bottomLeft, .bottomRight], radius: 15)
83 | imageView.roundCorners(corners: [.topLeft, .topRight], radius: 15)
84 |
85 | }
86 |
87 |
88 |
89 | // MARK: - Methods
90 |
91 | func setupViews() {
92 |
93 | transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
94 | alpha = 0
95 |
96 | backgroundColor = traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? UIColor.secondarySystemBackground : UIColor.systemBackground
97 |
98 | layer.shadowColor = UIColor.black.cgColor
99 | layer.shadowPath = UIBezierPath(rect: bounds).cgPath
100 | layer.shadowOpacity = 0.05
101 |
102 | layer.shadowOffset = CGSize(width: 0, height: 0)
103 | layer.shadowRadius = 15
104 | layer.cornerRadius = 15
105 |
106 | addSubview(imageView)
107 | imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
108 | imageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
109 | imageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
110 | imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
111 |
112 | addSubview(overlay)
113 | overlay.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
114 | overlay.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
115 | overlay.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
116 | overlay.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
117 |
118 | overlay.addSubview(titleLabel)
119 | titleLabel.bottomAnchor.constraint(equalTo: overlay.bottomAnchor, constant: -15).isActive = true
120 | titleLabel.leftAnchor.constraint(equalTo: overlay.leftAnchor, constant: 15).isActive = true
121 | titleLabel.rightAnchor.constraint(equalTo: overlay.rightAnchor, constant: -15).isActive = true
122 | titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
123 |
124 | UIView.animate(withDuration: 0.4) {
125 | self.transform = .identity
126 | self.alpha = 1
127 | }
128 |
129 | }
130 |
131 |
132 | func updateMeta() {
133 |
134 | let thumbnailGenerator = ThumbnailGenerator(size: frame.size)
135 | thumbnailGenerator.execute(for: node) { (success, thumbnail) in
136 | if success {
137 | DispatchQueue.main.async {
138 | self.imageView.image = thumbnail
139 | }
140 | }
141 | }
142 |
143 | }
144 |
145 | }
146 |
147 |
148 | extension NodeCell: NodeObserver {
149 |
150 | func didUpdate(node: Node) {
151 | updateMeta()
152 | }
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/Jottre/Jottre/en.xcloc/Localized Contents/en.xliff:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 | Jottre
10 | Jottre
11 | Bundle name
12 |
13 |
14 | Jottre document
15 | Jottre document
16 |
17 |
18 |
19 | Jottre wants to store the image in your photo-library
20 | Jottre wants to store the image in your photo-library
21 | Privacy - Photo Library Additions Usage Description
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 | (copy)
32 | (copy)
33 | No comment provided by engineer.
34 |
35 |
36 | Add note
37 | Add note
38 | No comment provided by engineer.
39 |
40 |
41 | Cancel
42 | Cancel
43 | No comment provided by engineer.
44 |
45 |
46 | Create
47 | Create
48 | No comment provided by engineer.
49 |
50 |
51 | Delete
52 | Delete
53 | No comment provided by engineer.
54 |
55 |
56 | Documents created with the 'Jottre for iPad' App can be viewed here.
57 | Documents created with the 'Jottre for iPad' App can be viewed here.
58 | No comment provided by engineer.
59 |
60 |
61 | Edit
62 | Edit
63 | No comment provided by engineer.
64 |
65 |
66 | Enter a name for the new note
67 | Enter a name for the new note
68 | No comment provided by engineer.
69 |
70 |
71 | Enter a name for the selected note
72 | Enter a name for the selected note
73 | No comment provided by engineer.
74 |
75 |
76 | Export note
77 | Export note
78 | No comment provided by engineer.
79 |
80 |
81 | My note
82 | My note
83 | No comment provided by engineer.
84 |
85 |
86 | New note
87 | New note
88 | No comment provided by engineer.
89 |
90 |
91 | No documents available yet. Click 'Add note' to create a new file.
92 | No documents available yet. Click 'Add note' to create a new file.
93 | No comment provided by engineer.
94 |
95 |
96 | Rename
97 | Rename
98 | No comment provided by engineer.
99 |
100 |
101 | Rename note
102 | Rename note
103 | No comment provided by engineer.
104 |
105 |
106 | Share
107 | Share
108 | No comment provided by engineer.
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/Jottre/Controllers/DrawScene/ExportActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExportActions.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 | import OSLog
10 |
11 | extension DrawViewController {
12 |
13 | func createExportToPDFAction() -> UIAlertAction {
14 | return UIAlertAction(title: "PDF", style: .default, handler: { (action) in
15 | self.startLoading()
16 |
17 | self.drawingToPDF { (data, _, _) in
18 |
19 | guard let data = data else {
20 | self.stopLoading()
21 | return
22 | }
23 |
24 | let fileURL = Settings.tmpDirectory.appendingPathComponent(self.node.name!).appendingPathExtension("pdf")
25 |
26 | if !data.writeToReturingBoolean(url: fileURL) {
27 | self.stopLoading()
28 | return
29 | }
30 |
31 | let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
32 |
33 | DispatchQueue.main.async {
34 | self.stopLoading()
35 | self.presentActivityViewController(activityViewController: activityViewController)
36 | }
37 |
38 | }
39 |
40 | })
41 | }
42 |
43 | func createExportToPNGAction() -> UIAlertAction {
44 | return UIAlertAction(title: "PNG", style: .default, handler: { (action) in
45 | self.startLoading()
46 |
47 | guard let drawing = self.node.codable?.drawing else {
48 | self.stopLoading()
49 | return
50 | }
51 |
52 | var bounds = drawing.bounds
53 | bounds.size.height = drawing.bounds.maxY + 100
54 |
55 | guard let data = drawing.image(from: bounds, scale: 1, userInterfaceStyle: .light).jpegData(compressionQuality: 1) else {
56 | self.stopLoading()
57 | return
58 | }
59 |
60 | let fileURL = Settings.tmpDirectory.appendingPathComponent(self.node.name!).appendingPathExtension("png")
61 |
62 | if !data.writeToReturingBoolean(url: fileURL) {
63 | self.stopLoading()
64 | return
65 | }
66 |
67 | let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
68 |
69 | DispatchQueue.main.async {
70 | self.stopLoading()
71 | self.presentActivityViewController(activityViewController: activityViewController)
72 | }
73 |
74 | })
75 | }
76 |
77 | func createExportToJPGAction() -> UIAlertAction {
78 | return UIAlertAction(title: "JPG", style: .default, handler: { (action) in
79 | self.startLoading()
80 |
81 | guard let drawing = self.node.codable?.drawing else {
82 | self.stopLoading()
83 | return
84 | }
85 |
86 | var bounds = drawing.bounds
87 | bounds.size.height = drawing.bounds.maxY + 100
88 |
89 | guard let data = drawing.image(from: bounds, scale: 1, userInterfaceStyle: .light).jpegData(compressionQuality: 1) else {
90 | self.stopLoading()
91 | return
92 | }
93 |
94 | let fileURL = Settings.tmpDirectory.appendingPathComponent(self.node.name!).appendingPathExtension("jpg")
95 |
96 | if !data.writeToReturingBoolean(url: fileURL) {
97 | self.stopLoading()
98 | return
99 | }
100 |
101 | let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
102 |
103 | DispatchQueue.main.async {
104 | self.stopLoading()
105 | self.presentActivityViewController(activityViewController: activityViewController)
106 | }
107 |
108 | })
109 | }
110 |
111 | func createShareAction() -> UIAlertAction {
112 | return UIAlertAction(title: NSLocalizedString("Share", comment: ""), style: .default, handler: { (action) in
113 | self.startLoading()
114 | self.node.push()
115 |
116 | guard let url = self.node.url else {
117 | self.stopLoading()
118 | return
119 | }
120 |
121 | let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
122 |
123 | DispatchQueue.main.async {
124 | self.stopLoading()
125 | self.presentActivityViewController(activityViewController: activityViewController)
126 | }
127 |
128 | })
129 | }
130 |
131 | fileprivate func presentActivityViewController(activityViewController: UIActivityViewController, animated: Bool = true) {
132 | if let popoverController = activityViewController.popoverPresentationController {
133 | popoverController.barButtonItem = navigationItem.rightBarButtonItem
134 | }
135 | self.present(activityViewController, animated: animated, completion: nil)
136 | }
137 |
138 | }
139 |
140 |
141 |
--------------------------------------------------------------------------------
/Jottre/Models/Node/NodeExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeExtensions.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 02.02.21.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | extension Node {
12 |
13 | // MARK: - Methods
14 |
15 | /// Some helper methods so that we can use the methods below without the completion handler
16 |
17 | func push() { push { (_) in } }
18 |
19 | func prepareData() { prepareData { (_) in } }
20 |
21 | func pullData() { pullData { (_) in } }
22 |
23 | func pull() { pull { (_) in } }
24 |
25 | func pullHandler() { pullHandler { (_) in } }
26 |
27 | }
28 |
29 |
30 | extension Node {
31 |
32 | // MARK: - Filesystem methods
33 |
34 | /// Moves all files in root directoy to cloud or local path
35 | /// - Returns: Success or failure (true, false)
36 | func moveFilesIfNeeded() -> Bool {
37 |
38 | guard var cloudURL = Settings.getCloudPath(), let url = self.url else {
39 | return true
40 | }
41 | var localURL = Settings.getLocalPath()
42 |
43 | /// - Checks if this file is relevant to our iCloud | Local Storage discussion
44 | if url.deletingPathExtension().deletingLastPathComponent() != cloudURL && url.deletingPathExtension().deletingLastPathComponent() != localURL {
45 | return true
46 | }
47 |
48 | cloudURL = cloudURL.appendingPathComponent(self.name!).appendingPathExtension("jot")
49 | localURL = localURL.appendingPathComponent(self.name!).appendingPathExtension("jot")
50 |
51 | let sourceURL = settings.codable.usesCloud ? localURL : cloudURL
52 | let destinationURL = settings.codable.usesCloud ? cloudURL : localURL
53 |
54 |
55 | /// - If the destinationURL already exists, there is no need to run the setUbiquitous method
56 | if FileManager.default.fileExists(atPath: destinationURL.path) {
57 | return true
58 | }
59 |
60 | do {
61 | try FileManager.default.moveItem(at: sourceURL, to: destinationURL)
62 | } catch {
63 | Logger.main.error("\(error.localizedDescription)")
64 | }
65 |
66 | return true
67 |
68 | }
69 |
70 |
71 | /// Checks wether there the node-file has changed after we read into memory
72 | /// - Parameter completion: Boolean value that indicates an inConflict or a notInConflict
73 | func inConflict(completion: ((Bool) -> Void)? = nil) {
74 |
75 | guard let url = self.url else {
76 | completion?(false)
77 | return
78 | }
79 |
80 | let cloudURL = url.deletingPathExtension().deletingLastPathComponent().appendingPathComponent(".\(self.name!)").appendingPathExtension("jot").appendingPathExtension("icloud")
81 |
82 | let tmpURL = FileManager.default.fileExists(atPath: cloudURL.path) ? cloudURL : url
83 |
84 | let tmpNode = Node(url: tmpURL)
85 | tmpNode.pull { (success) in
86 |
87 | if !success {
88 | completion?(false)
89 | return
90 | }
91 |
92 | if tmpNode.initialDataHash != self.initialDataHash {
93 | completion?(true)
94 | } else {
95 | completion?(false)
96 | }
97 |
98 | }
99 |
100 | }
101 |
102 |
103 | /// Renames the Nodes name
104 | /// IMPORTANT: - name = name.jot; name.jot = name
105 | /// - Parameters:
106 | /// - name: New name of the Node
107 | /// - completion: Returns a boolean that indicates success or failure
108 | func rename(to name: String, completion: ((Bool) -> Void)? = nil) {
109 |
110 | guard let originURL = url, let destinationURL = url?.deletingLastPathComponent().appendingPathComponent(name).appendingPathExtension("jot") else {
111 | completion?(false)
112 | return
113 | }
114 |
115 | do {
116 | try FileManager.default.moveItem(at: originURL, to: destinationURL)
117 | } catch {
118 | Logger.main.error("\(error.localizedDescription)")
119 | completion?(false)
120 | }
121 |
122 | self.url = destinationURL
123 | self.name = name
124 |
125 | completion?(true)
126 |
127 | }
128 |
129 |
130 | /// Clones the Node's content. Name will be updated automatically with suffix 'copy'
131 | /// - Parameter completion: Returns a boolean that indicates success or failure
132 | func duplicate(completion: ((Bool) -> Void)? = nil) {
133 |
134 | guard let name = self.name, let url = self.url, let collector = self.collector else {
135 | completion?(false)
136 | return
137 | }
138 |
139 | let croppedURL = url.deletingPathExtension().deletingLastPathComponent()
140 |
141 | let updatedName = NodeCollector.computeCopyName(baseName: name, path: croppedURL)
142 |
143 | let node = Node(url: croppedURL.appendingPathComponent(updatedName).appendingPathExtension("jot"))
144 | node.setDrawing(drawing: self.codable!.drawing)
145 | node.pull { (success) in
146 | if success {
147 | collector.nodes.append(node)
148 | completion?(true)
149 | return
150 | }
151 | completion?(false)
152 | }
153 |
154 | }
155 |
156 |
157 | /// Deletes the Node from the filesystem
158 | /// - Parameter completion: Returns a boolean that indicates success or failure
159 | func delete(completion: ((Bool) -> Void)? = nil) {
160 |
161 | guard let url = url else {
162 | completion?(false)
163 | return
164 | }
165 |
166 | do {
167 | try FileManager.default.removeItem(at: url)
168 | } catch {
169 | Logger.main.error("Could not delete Node at \(url). Reason: \(error.localizedDescription)")
170 | completion?(false)
171 | }
172 |
173 | completion?(true)
174 |
175 | }
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/Jottre/Controllers/InitialScene/InitialExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InitialViewControllerExtensions.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 | import OSLog
10 |
11 |
12 | // MARK: - NodeCollector
13 |
14 | extension InitialViewController: NodeCollectorObserver {
15 |
16 | func didInsertNode(nodeCollector: NodeCollector, at index: Int) {
17 |
18 | collectionView.performBatchUpdates({
19 | let indexPath = IndexPath(item: index, section: 0)
20 | self.collectionView.insertItems(at: [indexPath])
21 | }, completion: nil)
22 |
23 | }
24 |
25 | func didDeleteNode(nodeCollector: NodeCollector, at index: Int) {
26 |
27 | collectionView.performBatchUpdates({
28 | let indexPath = IndexPath(item: index, section: 0)
29 | self.collectionView.deleteItems(at: [indexPath])
30 | }, completion: nil)
31 |
32 | }
33 |
34 | }
35 |
36 |
37 |
38 | // MARK: - Settings
39 |
40 | extension InitialViewController {
41 |
42 | @objc func settingsDidChange(_ notification: Notification) {
43 | if initialLoad {
44 | initialLoad = false
45 | return
46 | }
47 | nodeCollector.update()
48 | nodeCollector.push()
49 |
50 | }
51 |
52 | }
53 |
54 |
55 |
56 | // MARK: - UICollectionView
57 |
58 | extension InitialViewController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
59 |
60 | func numberOfSections(in collectionView: UICollectionView) -> Int {
61 | return 1
62 | }
63 |
64 |
65 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
66 | hasDocuments = nodeCollector.nodes.count != 0
67 | return nodeCollector.nodes.count
68 | }
69 |
70 |
71 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
72 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "nodeCell", for: indexPath) as? NodeCell else {
73 | fatalError("Cell is not of type NodeCell.")
74 | }
75 | cell.node = nodeCollector.nodes[indexPath.row]
76 | cell.node.observer = cell
77 | return cell
78 | }
79 |
80 |
81 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
82 | let drawController = DrawViewController(node: nodeCollector.nodes[indexPath.row])
83 | navigationController?.pushViewController(drawController, animated: true)
84 | }
85 |
86 | func collectionView(_ collectionView: UICollectionView, willEndContextMenuInteraction configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
87 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.7) {
88 | self.nodeCollector.continueBackgroundFetch()
89 | }
90 | }
91 |
92 | func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
93 | nodeCollector.pauseBackgroundFetch()
94 | return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (actions) -> UIMenu? in
95 |
96 | let actions = [
97 | self.createEditAction(indexPath: indexPath),
98 | self.createRenameAction(indexPath: indexPath),
99 | self.createDeleteAction(indexPath: indexPath)
100 | ]
101 |
102 | return UIMenu(title: "", children: actions)
103 | }
104 |
105 | }
106 |
107 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
108 |
109 | let minWidth: CGFloat = 232
110 | let numberOfColumns: Int = Int((view.frame.width - 40) / minWidth)
111 | let space: CGFloat = (view.frame.width - 40).truncatingRemainder(dividingBy: minWidth)
112 |
113 | var width: CGFloat = minWidth
114 |
115 | if numberOfColumns == 1 {
116 | width = view.frame.width - 40
117 | } else {
118 | let spaces: CGFloat = 15 * CGFloat(numberOfColumns)
119 | width = minWidth + (space - spaces) / CGFloat(numberOfColumns)
120 | }
121 |
122 | return CGSize(width: width, height: 291)
123 | }
124 |
125 | }
126 |
127 | extension InitialViewController: UICollectionViewDragDelegate {
128 |
129 | func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
130 | var dragItems = [UIDragItem]()
131 | let selectedNode = nodeCollector.nodes[indexPath.row]
132 | if let imageToDrag = selectedNode.thumbnail {
133 |
134 | let userActivity = selectedNode.openDetailUserActivity
135 |
136 | let itemProvider = NSItemProvider(object: imageToDrag)
137 | itemProvider.registerObject(userActivity, visibility: .all)
138 |
139 | let dragItem = UIDragItem(itemProvider: itemProvider)
140 | dragItem.localObject = selectedNode
141 | dragItems.append(dragItem)
142 |
143 | }
144 |
145 | return dragItems
146 | }
147 |
148 | func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
149 | var dragItems = [UIDragItem]()
150 | let selectedNode = nodeCollector.nodes[indexPath.row]
151 | if let imageToDrag = selectedNode.thumbnail {
152 |
153 | let userActivity = selectedNode.openDetailUserActivity
154 |
155 | let itemProvider = NSItemProvider(object: imageToDrag)
156 | itemProvider.registerObject(userActivity, visibility: .all)
157 |
158 | let dragItem = UIDragItem(itemProvider: itemProvider)
159 | dragItem.localObject = selectedNode
160 | dragItems.append(dragItem)
161 |
162 | }
163 |
164 | return dragItems
165 | }
166 |
167 | }
168 |
--------------------------------------------------------------------------------
/Jottre/Controllers/DrawScene/DrawExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DrawExtensions.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import Foundation
9 | import PencilKit
10 |
11 | extension DrawViewController {
12 |
13 | func reloadNavigationItems() {
14 |
15 | navigationItem.hidesBackButton = hasModifiedDrawing
16 |
17 | if hasModifiedDrawing {
18 |
19 | navigationItem.setLeftBarButton(UIBarButtonItem(customView: NavigationTextButton(title: NSLocalizedString("Save", comment: "Save the document"), target: self, action: #selector(self.writeDrawingHandler))), animated: true)
20 |
21 | } else {
22 |
23 | navigationItem.leftBarButtonItem = nil
24 |
25 | if isUndoEnabled {
26 |
27 | let spaceButton = UIBarButtonItem(customView: SpaceButtonBarItem())
28 | undoButton = UIBarButtonItem(customView: UndoButton(target: self, action: #selector(undoHandler)))
29 | redoButton = UIBarButtonItem(customView: RedoButton(target: self, action: #selector(redoHandler)))
30 |
31 | navigationItem.leftItemsSupplementBackButton = true
32 | navigationItem.setLeftBarButtonItems([spaceButton, undoButton, redoButton], animated: true)
33 |
34 | guard let undoManager = canvasView.undoManager else {
35 | return
36 | }
37 |
38 | undoButton.isEnabled = undoManager.canUndo
39 | redoButton.isEnabled = undoManager.canRedo
40 |
41 | } else {
42 |
43 | navigationItem.setLeftBarButtonItems(nil, animated: true)
44 |
45 | }
46 |
47 | }
48 |
49 | }
50 |
51 |
52 | @objc func undoHandler() {
53 | canvasView.undoManager?.undo()
54 | reloadNavigationItems()
55 | }
56 |
57 |
58 | @objc func redoHandler() {
59 | canvasView.undoManager?.redo()
60 | reloadNavigationItems()
61 | }
62 |
63 | }
64 |
65 |
66 | extension DrawViewController: PKCanvasViewDelegate {
67 |
68 | func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
69 |
70 | updateContentSizeForDrawing()
71 | reloadNavigationItems()
72 |
73 | if modifiedCount == 1 {
74 | hasModifiedDrawing = true
75 | } else {
76 | modifiedCount += 1
77 | }
78 |
79 | }
80 |
81 | }
82 |
83 |
84 | extension DrawViewController: UIScreenshotServiceDelegate {
85 |
86 | func startLoading() {
87 | toolPicker.setVisible(false, forFirstResponder: canvasView)
88 | canvasView.isUserInteractionEnabled = false
89 | loadingView.isAnimating = true
90 | }
91 |
92 | func stopLoading() {
93 | toolPicker.setVisible(true, forFirstResponder: canvasView)
94 | canvasView.isUserInteractionEnabled = true
95 | loadingView.isAnimating = false
96 | }
97 |
98 | func drawingToPDF(_ completion: @escaping (_ PDFData: Data?, _ indexOfCurrentPage: Int, _ rectInCurrentPage: CGRect) -> Void) {
99 |
100 | let drawing = canvasView.drawing
101 |
102 | let visibleRect = canvasView.bounds
103 |
104 | let pdfWidth: CGFloat = node.codable!.width
105 | let pdfHeight = drawing.bounds.maxY + 100
106 | let canvasContentSize = canvasView.contentSize.height
107 |
108 | let xOffsetInPDF = pdfWidth - (pdfWidth * visibleRect.minX / canvasView.contentSize.width)
109 | let yOffsetInPDF = pdfHeight - (pdfHeight * visibleRect.maxY / canvasContentSize)
110 | let rectWidthInPDF = pdfWidth * visibleRect.width / canvasView.contentSize.width
111 | let rectHeightInPDF = pdfHeight * visibleRect.height / canvasContentSize
112 |
113 | let visibleRectInPDF = CGRect(x: xOffsetInPDF, y: yOffsetInPDF, width: rectWidthInPDF, height: rectHeightInPDF)
114 |
115 | DispatchQueue.global(qos: .background).async {
116 |
117 | let bounds = CGRect(x: 0, y: 0, width: pdfWidth, height: pdfHeight)
118 | let mutableData = NSMutableData()
119 |
120 | UIGraphicsBeginPDFContextToData(mutableData, bounds, nil)
121 | UIGraphicsBeginPDFPage()
122 |
123 | var yOrigin: CGFloat = 0
124 | let imageHeight: CGFloat = 1024
125 | while yOrigin < bounds.maxY {
126 | let imageBounds = CGRect(x: 0, y: yOrigin, width: pdfWidth, height: min(imageHeight, bounds.maxY - yOrigin))
127 | let img = drawing.image(from: imageBounds, scale: 2, userInterfaceStyle: .light)
128 | img.draw(in: imageBounds)
129 | yOrigin += imageHeight
130 | }
131 |
132 | UIGraphicsEndPDFContext()
133 |
134 | completion(mutableData as Data, 0, visibleRectInPDF)
135 |
136 | }
137 |
138 | }
139 |
140 | func screenshotService(_ screenshotService: UIScreenshotService, generatePDFRepresentationWithCompletion completion: @escaping (_ PDFData: Data?, _ indexOfCurrentPage: Int, _ rectInCurrentPage: CGRect) -> Void) {
141 |
142 | drawingToPDF { (data, indexOfCurrentPage, rectInCurrentPage) in
143 | completion(data, indexOfCurrentPage, rectInCurrentPage)
144 | }
145 |
146 | }
147 |
148 | }
149 |
150 |
151 | extension DrawViewController: PKToolPickerObserver {
152 |
153 | func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) {
154 | updateLayout(for: toolPicker)
155 | }
156 |
157 | func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
158 | updateLayout(for: toolPicker)
159 | }
160 |
161 |
162 | func updateLayout(for toolPicker: PKToolPicker) {
163 | let obscuredFrame = toolPicker.frameObscured(in: view)
164 |
165 | if obscuredFrame.isNull {
166 | canvasView.contentInset = .zero
167 | } else {
168 | canvasView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: view.bounds.maxX - obscuredFrame.minY, right: 0)
169 | }
170 |
171 | canvasView.scrollIndicatorInsets = canvasView.contentInset
172 |
173 | if isUndoEnabled != !obscuredFrame.isNull {
174 | isUndoEnabled = !obscuredFrame.isNull
175 | reloadNavigationItems()
176 | }
177 |
178 | }
179 |
180 | }
181 |
--------------------------------------------------------------------------------
/Jottre/Models/Settings/Settings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Settings.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 17.01.21.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 | import OSLog
11 |
12 | struct SettingsCodable: Codable {
13 |
14 | var usesCloud: Bool = false
15 |
16 | var preferedAppearance: Int = 0 /// 0=dark, 1=light, 2=auto
17 |
18 | }
19 |
20 |
21 | class Settings: NSObject {
22 |
23 | // MARK: - Properties
24 |
25 | var codable: SettingsCodable!
26 |
27 | public static let tmpDirectory: URL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
28 |
29 | public static let didUpdateNotificationName = Notification.Name("didUpdateSettings")
30 |
31 |
32 |
33 | // MARK: - Init
34 |
35 | override init() {
36 | super.init()
37 | _ = pull()
38 | }
39 |
40 |
41 |
42 | // MARK: - Methods
43 |
44 | /// Loading Settings from file
45 | /// - Parameter completion: Returns a boolean that indicates success or failure
46 | func pull() -> Bool {
47 |
48 | if UserDefaults.standard.data(forKey: "settings") == nil {
49 | createSettings()
50 | return true
51 | }
52 |
53 | do {
54 | let decoder = PropertyListDecoder()
55 | let data = UserDefaults.standard.data(forKey: "settings")!
56 | self.codable = try decoder.decode(SettingsCodable.self, from: data)
57 | } catch {
58 | return false
59 | }
60 |
61 | didUpdate()
62 | return true
63 |
64 | }
65 |
66 |
67 | /// Writing Settings to file
68 | /// - Parameter completion: Returns a boolean that indicates success or failure
69 | func push() -> Bool {
70 |
71 | guard let codable = self.codable else {
72 | return false
73 | }
74 |
75 | do {
76 | let encoder = PropertyListEncoder()
77 | let data = try encoder.encode(codable)
78 | UserDefaults.standard.set(data, forKey: "settings")
79 | } catch {
80 | Logger.main.error("Could not write NodeCodable to file: \(error.localizedDescription)")
81 | return false
82 | }
83 |
84 | return true
85 |
86 | }
87 |
88 |
89 | /// Generates a .settings file
90 | /// - Parameter completion: Returns a boolean that indicates success or failure
91 | func createSettings(completion: ((Bool) -> Void)? = nil) {
92 | Logger.main.info("Creating settings file")
93 |
94 | codable = SettingsCodable(usesCloud: UIDevice.isLimited(), preferedAppearance: 2)
95 | _ = push()
96 |
97 | }
98 |
99 |
100 |
101 | // MARK: - Set methods
102 |
103 | /// Sets the appearance
104 | /// - Parameter preferedAppearance: 0 = dark, light = 1, auto = 2
105 | func set(preferedAppearance: Int) {
106 | self.codable.preferedAppearance = preferedAppearance
107 | _ = push()
108 | didUpdate()
109 | }
110 |
111 |
112 | /// Sets a boolish value that indicates wether the files were loaded/stored localy or via iCloud Driver
113 | /// - Parameter usesCloud: -
114 | func set(usesCloud: Bool) {
115 | self.codable.usesCloud = usesCloud
116 | _ = push()
117 | didUpdate()
118 | }
119 |
120 |
121 |
122 | // MARK: - Get methods
123 |
124 | /// Returns a preferedUserInterfaceStyle object (Depending on the settings.codable.preferedAppearance property)
125 | /// - Returns: Prefered appearance as UIUserInterfaceStyle object
126 | func preferredUserInterfaceStyle() -> UIUserInterfaceStyle {
127 | if self.codable.preferedAppearance == 0 {
128 | return UIUserInterfaceStyle.dark
129 | } else if self.codable.preferedAppearance == 1 {
130 | return UIUserInterfaceStyle.light
131 | } else if self.codable.preferedAppearance == 2 {
132 | return UIUserInterfaceStyle.unspecified
133 | } else {
134 | return UIUserInterfaceStyle.unspecified
135 | }
136 | }
137 |
138 |
139 | /// Returns the correct primaryBackgroundColor for the selected preferredUserInterface
140 | /// - Returns: Prefered appearance as UIColor object
141 | func preferredUserInterfaceBackgroundColor() -> UIColor {
142 | if settings.preferredUserInterfaceStyle() == .dark {
143 | return UIColor(red: 28/255, green: 28/255, blue: 30/255, alpha: 1)
144 | } else if settings.preferredUserInterfaceStyle() == .light {
145 | return UIColor.white
146 | } else {
147 | return UIColor.systemBackground
148 | }
149 | }
150 |
151 |
152 |
153 | /// Returns the "root" path of this application. (Depending of user-prefered storage (local vs iCloud))
154 | /// - Returns: Main directory-path. This is the directory where the files were primarly stored.
155 | /// - Discussion:
156 | func getPath() -> URL {
157 |
158 | if codable == nil {
159 | _ = pull()
160 | }
161 |
162 | if codable.usesCloud {
163 | guard let url = Settings.getCloudPath() else {
164 | return Settings.getLocalPath()
165 | }
166 | return url
167 | } else {
168 | return Settings.getLocalPath()
169 | }
170 |
171 | }
172 |
173 |
174 | /// Returns the root iCloud path for the Jottre Container
175 | /// - Returns: Local iCloud path
176 | static func getCloudPath() -> URL? {
177 | guard let url = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents") else {
178 | return nil
179 | }
180 |
181 | var isDirectory = ObjCBool(true)
182 |
183 | if !FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) {
184 |
185 | do {
186 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false)
187 | return url
188 | } catch {
189 | Logger.main.error("Could not create directory at \(url.path)")
190 | return nil
191 | }
192 |
193 | }
194 |
195 | return url
196 | }
197 |
198 |
199 | /// Returns the root local path for the Jottre Container
200 | /// - Returns: Local path
201 | static func getLocalPath() -> URL {
202 | return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
203 | }
204 |
205 |
206 |
207 | // MARK: - Observer methods
208 |
209 | /// Sends a message to each observer, that there happened changes inside this object.
210 | func didUpdate() {
211 | NotificationCenter.default.post(name: Settings.didUpdateNotificationName, object: self)
212 | }
213 |
214 | }
215 |
--------------------------------------------------------------------------------
/Jottre/Controllers/InitialScene/InitialViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InitialViewController.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 | import OSLog
10 |
11 | class InitialViewController: UIViewController {
12 |
13 | // MARK: - Properties
14 |
15 | var initialLoad: Bool = true
16 |
17 | var nodeCollector: NodeCollector = NodeCollector()
18 |
19 | var hasDocumentsDelayFinished: Bool = false
20 |
21 | var hasDocuments: Bool = false {
22 | didSet {
23 | if !hasDocumentsDelayFinished { return }
24 | UIView.animate(withDuration: 0.5) {
25 | self.infoTextView.alpha = self.hasDocuments ? 0 : 1
26 | self.collectionView.alpha = self.hasDocuments ? 1 : 0
27 | }
28 | }
29 | }
30 |
31 |
32 |
33 | // MARK: - Subviews
34 |
35 | var infoTextView: UITextView = {
36 | let textView = UITextView()
37 | textView.translatesAutoresizingMaskIntoConstraints = false
38 | textView.isEditable = false
39 | textView.isSelectable = false
40 | textView.font = UIFont.systemFont(ofSize: 25, weight: .regular)
41 | textView.textColor = UIColor.secondaryLabel
42 | textView.text = UIDevice.isLimited() ? NSLocalizedString("Documents created with the 'Jottre for iPad' App can be viewed here.", comment: "") : NSLocalizedString("No documents available yet. Click 'Add note' to create a new file.", comment: "")
43 | textView.text = Downloader.isCloudEnabled ? textView.text : UIDevice.isLimited() ? NSLocalizedString("Enable iCloud to view files created with 'Jottre for iPad'", comment: "") : NSLocalizedString("Enable iCloud to unlock the full potential of Jottre", comment: "")
44 | textView.textAlignment = .center
45 | textView.isScrollEnabled = false
46 | textView.backgroundColor = .clear
47 | textView.alpha = 0
48 | return textView
49 | }()
50 |
51 | var collectionView: UICollectionView = {
52 | let layout = UICollectionViewFlowLayout()
53 | layout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
54 | layout.minimumLineSpacing = 20
55 | layout.minimumInteritemSpacing = 20
56 |
57 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
58 | collectionView.translatesAutoresizingMaskIntoConstraints = false
59 | collectionView.backgroundColor = .clear
60 | collectionView.register(NodeCell.self, forCellWithReuseIdentifier: "nodeCell")
61 | collectionView.alpha = 0
62 |
63 | return collectionView
64 | }()
65 |
66 |
67 |
68 | // MARK: - Override methods
69 |
70 | override func viewDidLoad() {
71 | super.viewDidLoad()
72 |
73 | setupViews()
74 | setupDelegates()
75 |
76 | }
77 |
78 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
79 | super.traitCollectionDidChange(previousTraitCollection)
80 |
81 | nodeCollector.traitCollection = traitCollection
82 |
83 | }
84 |
85 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
86 | super.viewWillTransition(to: size, with: coordinator)
87 |
88 | collectionView.collectionViewLayout.invalidateLayout()
89 |
90 | }
91 |
92 |
93 |
94 | // MARK: - Methods
95 |
96 | private func setupViews() {
97 |
98 | navigationItem.title = "Jottre"
99 |
100 | view.backgroundColor = .systemBackground
101 |
102 | if !UIDevice.isLimited() {
103 | navigationItem.rightBarButtonItem = UIBarButtonItem(customView: NavigationTextButton(title: NSLocalizedString("Add note", comment: ""), target: self, action: #selector(createNode)))
104 | }
105 |
106 | if !Downloader.isCloudEnabled {
107 | presentInfoAlert()
108 | }
109 |
110 | navigationItem.leftBarButtonItem = UIBarButtonItem(customView: SettingsButton(target: self, action: #selector(presentSettings)))
111 |
112 | view.addSubview(collectionView)
113 | collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
114 | collectionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
115 | collectionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
116 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
117 |
118 | view.addSubview(infoTextView)
119 | infoTextView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
120 | infoTextView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
121 | infoTextView.widthAnchor.constraint(equalToConstant: 300).isActive = true
122 | infoTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
123 |
124 | }
125 |
126 |
127 | private func setupDelegates() {
128 |
129 | collectionView.delegate = self
130 | collectionView.dataSource = self
131 | collectionView.dragDelegate = self
132 |
133 | nodeCollector.traitCollection = traitCollection
134 | nodeCollector.addObserver(self)
135 |
136 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.5) {
137 | self.hasDocumentsDelayFinished = true
138 | self.hasDocuments = !(!self.hasDocuments) /// A simple solution to reassign the value to call didSet
139 | }
140 |
141 | NotificationCenter.default.addObserver(self, selector: #selector(settingsDidChange(_:)), name: Settings.didUpdateNotificationName, object: nil)
142 |
143 | }
144 |
145 |
146 | @objc func createNode() {
147 |
148 | let alertTitle = NSLocalizedString("New note", comment: "")
149 | let alertMessage = NSLocalizedString("Enter a name for the new note", comment: "")
150 |
151 | let noteName = NSLocalizedString("My note", comment: "")
152 |
153 | let alertPrimaryActionTitle = NSLocalizedString("Create", comment: "")
154 | let alertCancelTitle = NSLocalizedString("Cancel", comment: "")
155 |
156 | let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
157 |
158 | alertController.addTextField { (textField) in
159 | textField.placeholder = noteName
160 | }
161 |
162 | alertController.addAction(UIAlertAction(title: alertPrimaryActionTitle, style: .default, handler: { (action) in
163 |
164 | guard let textFields = alertController.textFields, var name = textFields[0].text else {
165 | return
166 | }
167 | name = name == "" ? noteName : name
168 |
169 | self.nodeCollector.createNode(name: name) { (node) in }
170 |
171 | }))
172 |
173 | alertController.addAction(UIAlertAction(title: alertCancelTitle, style: .cancel, handler: nil))
174 |
175 | present(alertController, animated: true, completion: nil)
176 |
177 | }
178 |
179 |
180 | func presentInfoAlert() {
181 |
182 | let alertTitle = NSLocalizedString("iCloud disabled", comment: "")
183 | let alertMessage = NSLocalizedString("While iCloud is disabled, you can only open files that are locally on this device.", comment: "")
184 |
185 | let alertPrimaryActionTitle = NSLocalizedString("How to enable iCloud", comment: "")
186 | let supportURL = NSLocalizedString("https://support.apple.com/en-us/HT208681", comment: "URL for iCloud setup")
187 |
188 | let alertCancelTitle = NSLocalizedString("Cancel", comment: "")
189 |
190 |
191 | let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
192 |
193 | alertController.addAction(UIAlertAction(title: alertPrimaryActionTitle, style: .default, handler: { (action) in
194 | UIApplication.shared.open(URL(string: supportURL)!, options: [:], completionHandler: nil)
195 | }))
196 |
197 | alertController.addAction(UIAlertAction(title: alertCancelTitle, style: .cancel, handler: nil))
198 |
199 | present(alertController, animated: true, completion: nil)
200 |
201 | }
202 |
203 |
204 | @objc func presentSettings() {
205 |
206 | let settingsController = SettingsViewController()
207 |
208 | let settingsNavigationController = SettingsNavigationViewController(rootViewController: settingsController)
209 |
210 | present(settingsNavigationController, animated: true, completion: nil)
211 |
212 | }
213 |
214 | }
215 |
--------------------------------------------------------------------------------
/Jottre/Controllers/DrawScene/DrawViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DrawViewController.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 | import PencilKit
10 | import OSLog
11 |
12 | class DrawViewController: UIViewController {
13 |
14 | // MARK: - Properties
15 |
16 | var node: Node!
17 |
18 | var isUndoEnabled: Bool = false
19 |
20 | var modifiedCount: Int = 0
21 |
22 | var hasModifiedDrawing: Bool = false {
23 | didSet {
24 | reloadNavigationItems()
25 | }
26 | }
27 |
28 | override var prefersHomeIndicatorAutoHidden: Bool {
29 | return true
30 | }
31 |
32 |
33 |
34 | // MARK: - Subviews
35 |
36 | var loadingView: LoadingView = {
37 | return LoadingView()
38 | }()
39 |
40 | var canvasView: PKCanvasView = {
41 | let canvasView = PKCanvasView()
42 | canvasView.translatesAutoresizingMaskIntoConstraints = false
43 | canvasView.drawingPolicy = .default
44 | canvasView.alwaysBounceVertical = true
45 | canvasView.maximumZoomScale = 3
46 | return canvasView
47 | }()
48 |
49 | var toolPicker: PKToolPicker = {
50 | return PKToolPicker()
51 | }()
52 |
53 | var redoButton: UIBarButtonItem!
54 |
55 | var undoButton: UIBarButtonItem!
56 |
57 |
58 |
59 | // MARK: - Init
60 |
61 | init(node: Node) {
62 | super.init(nibName: nil, bundle: nil)
63 | self.node = node
64 | }
65 |
66 | required init?(coder: NSCoder) {
67 | fatalError("init(coder:) has not been implemented")
68 | }
69 |
70 |
71 |
72 | // MARK: - Override methods
73 |
74 | override func viewDidLoad() {
75 | super.viewDidLoad()
76 |
77 | setupViews()
78 | setupDelegates()
79 |
80 | }
81 |
82 |
83 | override func viewDidLayoutSubviews() {
84 | super.viewDidLayoutSubviews()
85 |
86 | let canvasScale = canvasView.bounds.width / node.codable!.width
87 | canvasView.minimumZoomScale = canvasScale
88 | canvasView.zoomScale = canvasScale
89 |
90 | updateContentSizeForDrawing()
91 | canvasView.contentOffset = CGPoint(x: 0, y: -canvasView.adjustedContentInset.top)
92 |
93 | }
94 |
95 |
96 | override func viewDidAppear(_ animated: Bool) {
97 | super.viewDidAppear(animated)
98 |
99 | node.isOpened = true
100 |
101 | guard let parent = parent, let window = parent.view.window, let windowScene = window.windowScene else { return }
102 |
103 | if let screenshotService = windowScene.screenshotService { screenshotService.delegate = self }
104 |
105 | windowScene.userActivity = node.openDetailUserActivity
106 |
107 | }
108 |
109 |
110 | override func viewWillDisappear(_ animated: Bool) {
111 | super.viewWillDisappear(animated)
112 |
113 | view.window?.windowScene?.screenshotService?.delegate = nil
114 | view.window?.windowScene?.userActivity = nil
115 |
116 | node.isOpened = false
117 |
118 | }
119 |
120 |
121 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
122 |
123 | navigationController?.navigationBar.isTranslucent = true
124 | navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
125 | navigationController?.navigationBar.shadowImage = UIImage()
126 |
127 | view.backgroundColor = (traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark) ? .black : .white
128 |
129 | }
130 |
131 |
132 | override func updateUserActivityState(_ activity: NSUserActivity) {
133 | userActivity!.addUserInfoEntries(from: [ Node.NodeOpenDetailIdKey: node.url! ])
134 | }
135 |
136 |
137 |
138 | // MARK: - Methods
139 |
140 | func setupViews() {
141 |
142 | traitCollectionDidChange(traitCollection)
143 |
144 | view.backgroundColor = (traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark) ? .black : .white
145 |
146 | navigationItem.largeTitleDisplayMode = .never
147 | navigationItem.title = node.name
148 | navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "square.and.arrow.up"), style: .plain, target: self, action: #selector(exportDrawing))
149 |
150 | reloadNavigationItems()
151 |
152 | view.addSubview(canvasView)
153 | canvasView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
154 | canvasView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
155 | canvasView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
156 | canvasView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
157 |
158 | view.addSubview(loadingView)
159 | loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
160 | loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
161 | loadingView.widthAnchor.constraint(equalToConstant: 120).isActive = true
162 | loadingView.heightAnchor.constraint(equalToConstant: 120).isActive = true
163 |
164 | updateContentSizeForDrawing()
165 |
166 | }
167 |
168 |
169 | private func setupDelegates() {
170 |
171 | guard let nodeCodable = node.codable else { return }
172 |
173 | canvasView.delegate = self
174 | canvasView.drawing = nodeCodable.drawing
175 |
176 | if !UIDevice.isLimited() {
177 | toolPicker.setVisible(true, forFirstResponder: canvasView)
178 | toolPicker.addObserver(canvasView)
179 | toolPicker.addObserver(self)
180 | updateLayout(for: toolPicker)
181 | canvasView.becomeFirstResponder()
182 | }
183 |
184 | }
185 |
186 |
187 | func updateContentSizeForDrawing() {
188 |
189 | let drawing = canvasView.drawing
190 | let contentHeight: CGFloat
191 |
192 | if !drawing.bounds.isNull {
193 | contentHeight = max(canvasView.bounds.height, (drawing.bounds.maxY + 500) * canvasView.zoomScale)
194 | } else {
195 | contentHeight = canvasView.bounds.height
196 | }
197 | canvasView.contentSize = CGSize(width: node.codable!.width * canvasView.zoomScale, height: contentHeight)
198 |
199 | }
200 |
201 |
202 | @objc func exportDrawing() {
203 |
204 | toolPicker.setVisible(false, forFirstResponder: canvasView)
205 |
206 | let alertTitle = NSLocalizedString("Export note", comment: "")
207 | let alertCancelTitle = NSLocalizedString("Cancel", comment: "")
208 |
209 | let alertController = UIAlertController(title: alertTitle, message: "", preferredStyle: .actionSheet)
210 |
211 | if let popoverController = alertController.popoverPresentationController {
212 | popoverController.barButtonItem = navigationItem.rightBarButtonItem
213 | }
214 |
215 | alertController.addAction(createExportToPDFAction())
216 | alertController.addAction(createExportToJPGAction())
217 | alertController.addAction(createExportToPNGAction())
218 | alertController.addAction(createShareAction())
219 | alertController.addAction(UIAlertAction(title: alertCancelTitle, style: .cancel, handler: { (action) in
220 | self.toolPicker.setVisible(true, forFirstResponder: self.canvasView)
221 | }))
222 |
223 | present(alertController, animated: true, completion: nil)
224 |
225 | }
226 |
227 |
228 | @objc func writeDrawingHandler() {
229 |
230 | node.inConflict { (conflict) in
231 |
232 | if !conflict {
233 | Logger.main.info("Files not in conflict")
234 | self.writeDrawing()
235 | return
236 | }
237 |
238 | Logger.main.warning("Files in conflict")
239 |
240 | DispatchQueue.main.async {
241 |
242 | let alertTitle = NSLocalizedString("File conflict found", comment: "")
243 | let alertMessage = String(format: NSLocalizedString("The file could not be saved. It seems that the original file (%s.jot) on the disk has changed. (Maybe it was edited on another device at the same time?). Use one of the following options to fix the problem.", comment: "File conflict found (What happened, How to fix)"), self.node.name ?? "?")
244 | let alertActionOverwriteTitle = NSLocalizedString("Overwrite", comment: "")
245 | let alertActionCloseTitle = NSLocalizedString("Close without saving", comment: "")
246 | let alertCancelTitle = NSLocalizedString("Cancel", comment: "")
247 |
248 | let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
249 | alertController.addAction(UIAlertAction(title: alertActionOverwriteTitle, style: .destructive, handler: { (action) in
250 | self.writeDrawing()
251 | }))
252 | alertController.addAction(UIAlertAction(title: alertActionCloseTitle, style: .destructive, handler: { (action) in
253 | self.navigationController?.popViewController(animated: true)
254 | }))
255 | alertController.addAction(UIAlertAction(title: alertCancelTitle, style: .cancel, handler: nil))
256 |
257 | self.present(alertController, animated: true, completion: nil)
258 |
259 | }
260 |
261 | }
262 |
263 | }
264 |
265 | func writeDrawing() {
266 | DispatchQueue.main.async {
267 | self.hasModifiedDrawing = false
268 | self.node.setDrawing(drawing: self.canvasView.drawing)
269 | }
270 | }
271 |
272 | }
273 |
--------------------------------------------------------------------------------
/Jottre/Models/Node/Node.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Node.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import Foundation
9 | import PencilKit
10 | import OSLog
11 |
12 |
13 | protocol NodeObserver {
14 | func didUpdate(node: Node)
15 | }
16 |
17 | struct NodeObserverReusable {
18 |
19 | var reuseIdentifier: String
20 |
21 | var observer: NodeObserver
22 |
23 | }
24 |
25 |
26 |
27 | /// This class will manage the processes of a NodeCodable
28 | /// This includes: encoding, decoding and basic file system related methods
29 | class Node: NSObject {
30 |
31 | // MARK: - Properties
32 |
33 | var name: String?
34 |
35 | var isOpened: Bool = false
36 |
37 | var url: URL? {
38 | didSet {
39 | guard let url = url else { return }
40 | name = url.deletingPathExtension().lastPathComponent
41 | }
42 | }
43 |
44 | var status: URLUbiquitousItemDownloadingStatus? {
45 | return Downloader.getStatus(url: self.url)
46 | }
47 |
48 | private var observersEnabledValue: Bool = true
49 |
50 | var observersEnabled: Bool {
51 | return observersEnabledValue
52 | }
53 |
54 | var initialDataHash: Int?
55 |
56 | var currentDataHash: Int?
57 |
58 | var thumbnail: UIImage?
59 |
60 | var codable: NodeCodableV2?
61 |
62 | var collector: NodeCollector?
63 |
64 | var observer: NodeObserver?
65 |
66 | var observers: [NodeObserverReusable] = []
67 |
68 | private var serializationQueue = DispatchQueue(label: "NodeSerializationQueue", qos: .background)
69 |
70 |
71 | static let NodeOpenDetailActivityType = "com.antonlorani.jottre.openDetail"
72 |
73 | static let NodeOpenDetailPath = "openDetail"
74 |
75 | static let NodeOpenDetailIdKey = "nodeURL"
76 |
77 | var openDetailUserActivity: NSUserActivity {
78 | let userActivity = NSUserActivity(activityType: Node.NodeOpenDetailActivityType)
79 | userActivity.title = Node.NodeOpenDetailPath
80 | userActivity.userInfo = [Node.NodeOpenDetailIdKey: url!]
81 | userActivity.requiredUserInfoKeys = [Node.NodeOpenDetailIdKey]
82 | return userActivity
83 | }
84 |
85 |
86 |
87 | // MARK: - Init
88 |
89 | /// Initializer for a Node from a custom url
90 | /// - Parameter url: Should point to a .jot file on the users file-system
91 | init(url: URL) {
92 | super.init()
93 | setupValues(url: url, nodeCodable: NodeCodableV2())
94 | }
95 |
96 |
97 | /// Helper method for the initializer
98 | /// - Parameters:
99 | /// - url: url passed via initializer
100 | /// - nodeCodable: A valid NodeCodable object
101 | private func setupValues(url: URL, nodeCodable: NodeCodableV2) {
102 | self.url = url
103 | self.codable = nodeCodable
104 | }
105 |
106 |
107 |
108 | // MARK: - Methods
109 |
110 | /// Prepares pull of the node from drive.
111 | /// - Parameter completion: Returns a boolean that indicates success or failure
112 | func pull(completion: @escaping (_ success: Bool) -> Void) {
113 |
114 | serializationQueue.async {
115 |
116 | guard let url = self.url else {
117 | completion(false)
118 | return
119 | }
120 |
121 | if !url.isCloudAndJot() && !url.isJot() {
122 | completion(false)
123 | return
124 | }
125 |
126 | if !settings.codable.usesCloud {
127 | self.pullHandler { (success) in
128 | completion(success)
129 | }
130 | return
131 | }
132 |
133 | guard let status = self.status else {
134 | completion(false)
135 | return
136 | }
137 |
138 | if status == .current {
139 | self.pullHandler { (success) in
140 | completion(success)
141 | }
142 | return
143 | }
144 |
145 | let downloader = Downloader(url: url)
146 | downloader.execute { (success) in
147 |
148 | if !success {
149 | Logger.main.error("Could not download node from file: \(url.path)")
150 | completion(false)
151 | return
152 | }
153 |
154 | self.url = url.cloudToJot()
155 |
156 | self.pullHandler { (success) in
157 | completion(success)
158 | }
159 |
160 | }
161 |
162 | }
163 |
164 | }
165 |
166 |
167 | /// Loads the Node from file
168 | /// - Parameter url: URL to load the file from.
169 | func pullHandler(completion: @escaping (_ success: Bool) -> Void) {
170 |
171 | serializationQueue.async {
172 |
173 | guard let url = self.url, FileManager.default.fileExists(atPath: url.path) else {
174 | completion(false)
175 | return
176 | }
177 |
178 | self.pullData { (data) in
179 | guard let data = data else {
180 | completion(false)
181 | return
182 | }
183 |
184 | guard let decodedCodable = self.decode(from: data) else {
185 | completion(false)
186 | return
187 | }
188 |
189 | if !self.isOpened {
190 | self.initialDataHash = data.hashValue
191 | }
192 |
193 | self.codable = decodedCodable
194 |
195 | completion(true)
196 |
197 | }
198 |
199 | }
200 |
201 | }
202 |
203 |
204 | /// Pulls the file-data from given url
205 | /// - Parameters:
206 | /// - completion: Returns boolean that indicates success or failure. Data is nil if fetch failed.
207 | func pullData(completion: @escaping (_ data: Data?) -> Void) {
208 |
209 | guard let url = self.url, FileManager.default.fileExists(atPath: url.path) else {
210 | completion(nil)
211 | return
212 | }
213 |
214 | serializationQueue.async {
215 |
216 | do {
217 | let data = try Data(contentsOf: url)
218 | completion(data)
219 | } catch {
220 | completion(nil)
221 | }
222 |
223 | }
224 |
225 | }
226 |
227 |
228 | /// Helper method that serializes the codable object to data
229 | /// - Returns: If success the codable as data. If failure nil.
230 | func prepareData(completion: @escaping (_ data: Data?) -> Void) {
231 |
232 | guard let nodeCodable = self.codable else {
233 | completion(nil)
234 | return
235 | }
236 |
237 | serializationQueue.async {
238 |
239 | do {
240 | let encoder = PropertyListEncoder()
241 | let data = try encoder.encode(nodeCodable)
242 | completion(data)
243 | return
244 | } catch {
245 | completion(nil)
246 | return
247 | }
248 |
249 | }
250 |
251 | }
252 |
253 |
254 | /// Writing Node to file
255 | /// - Parameter completion: Returns a boolean that indicates success or failure
256 | func push(completion: @escaping (_ success: Bool) -> Void) {
257 |
258 | serializationQueue.async {
259 |
260 | guard let url = self.url else {
261 | completion(false)
262 | return
263 | }
264 |
265 | self.prepareData { (data) in
266 | guard let data = data else {
267 | completion(false)
268 | return
269 | }
270 |
271 | do {
272 | try data.write(to: url)
273 | self.initialDataHash = data.hashValue
274 | } catch {
275 | Logger.main.error("Could not write data to file: \(error.localizedDescription)")
276 | completion(false)
277 | return
278 | }
279 |
280 | _ = self.moveFilesIfNeeded()
281 | completion(true)
282 |
283 | }
284 |
285 | }
286 |
287 | }
288 |
289 |
290 | /// Moves drawing to Node. Calls update to generate thumbnail
291 | /// - Parameter drawing: Given PKDrawing
292 | func setDrawing(drawing: PKDrawing) {
293 |
294 | codable?.drawing = drawing
295 | codable?.lastModified = NSDate().timeIntervalSince1970
296 |
297 | didUpdate()
298 | push()
299 |
300 | }
301 |
302 |
303 | /// Sends a message to each observer, that there happened changes inside this object.
304 | func didUpdate() {
305 |
306 | if !observersEnabled { return }
307 |
308 | DispatchQueue.main.async {
309 | self.observer?.didUpdate(node: self)
310 | self.observers.forEach({ $0.observer.didUpdate(node: self) })
311 | }
312 |
313 | }
314 |
315 |
316 |
317 | // MARK: - Observer methods
318 |
319 | /// Enable observer calls ;)
320 | func enableObservers() {
321 | observersEnabledValue = true
322 | }
323 |
324 |
325 | /// Suppresses the observer calls ;)
326 | func disableObservers() {
327 | observersEnabledValue = false
328 | }
329 |
330 |
331 | /// Adds a new observer to this class
332 | /// - Parameters:
333 | /// - observer: NodeObserver class that listens for a possible Node-update
334 | /// - reuseIdentifier: A unique string that is used to release the observer from the node.
335 | func addObserver(_ observer: NodeObserver, _ reuseIdentifier: String) {
336 | observers.append(NodeObserverReusable(reuseIdentifier: reuseIdentifier, observer: observer))
337 | }
338 |
339 |
340 | /// Removes an existing observer from this class
341 | func deleteObserver(_ observer: NodeObserver, _ reuseIdentifier: String) {
342 | observers.removeAll { (reusableObserver) -> Bool in
343 | return reusableObserver.reuseIdentifier == reuseIdentifier
344 | }
345 | }
346 |
347 |
348 | }
349 |
--------------------------------------------------------------------------------
/Jottre/Models/Node/NodeCollector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCollector.swift
3 | // Jottre
4 | //
5 | // Created by Anton Lorani on 16.01.21.
6 | //
7 |
8 | import UIKit
9 | import OSLog
10 | import Foundation
11 |
12 | protocol NodeCollectorObserver {
13 |
14 | func didInsertNode(nodeCollector: NodeCollector, at index: Int)
15 |
16 | func didDeleteNode(nodeCollector: NodeCollector, at index: Int)
17 |
18 | }
19 |
20 | class NodeCollector {
21 |
22 | // MARK: - Properties
23 |
24 | private var observers: [NodeCollectorObserver] = []
25 |
26 | private var backgroundFetchIsActiveValue: Bool = true
27 |
28 | var backgroundFetchIsActive: Bool {
29 | return backgroundFetchIsActiveValue
30 | }
31 |
32 | private var observersEnabledValue: Bool = true
33 |
34 | var observersEnabled: Bool {
35 | return observersEnabledValue
36 | }
37 |
38 | static func writePath() -> URL {
39 | return Settings.getLocalPath()
40 | }
41 |
42 | static func readPath() -> URL {
43 | return settings.getPath()
44 | }
45 |
46 | var nodes: [Node] = []
47 |
48 | var traitCollection: UITraitCollection = UITraitCollection() {
49 | didSet {
50 | update()
51 | }
52 | }
53 |
54 |
55 |
56 | // MARK: - Init
57 |
58 | /// Initializes the NodeCollector object and automatically pulls Nodes from the default container-path
59 | init() {
60 | initializeBackgroundFetch(interval: 1)
61 | }
62 |
63 |
64 |
65 | // MARK: - Methods
66 |
67 | /// Loads the nodes from default path
68 | /// - Parameter completion: Returns a boolean that indicates success or failure
69 | func pull(completion: ((Bool) -> Void)? = nil) {
70 | nodes = []
71 | let files = try! FileManager.default.contentsOfDirectory(atPath: NodeCollector.readPath().path)
72 | files.forEach { (name) in
73 | let url = NodeCollector.readPath().appendingPathComponent(name)
74 | self.pullNode(url: url)
75 | }
76 |
77 | completion?(true)
78 | }
79 |
80 |
81 | /// Pulls a specific node from url.
82 | /// This node will be added to the NodeCollector
83 | /// - Parameters:
84 | /// - url: Should point to a .jot file on the users file-system
85 | /// - completion: Returns a boolean that indicates success or failure
86 | func pullNode(url: URL, completion: ((Bool) -> Void)? = nil) {
87 |
88 | let node = Node(url: url)
89 | node.collector = self
90 | node.pull { (success) in
91 | if success {
92 | self.addNode(node)
93 | }
94 | completion?(success)
95 | }
96 |
97 | }
98 |
99 |
100 | /// Force pulls all the Nodes to the file-system
101 | /// - Parameter completion: Returns a boolean that indicates success or failure
102 | func push(completion: ((Bool) -> Void)? = nil) {
103 |
104 | nodes.forEach({ $0.push() })
105 | completion?(true)
106 |
107 | }
108 |
109 |
110 | /// Creates a new Node for given name
111 | /// - Parameter name: This will be the name and filename (without suffix .jot) of the Node
112 | /// - Parameter completion: Returns a boolean that indicates success or failure and the hopefully created node
113 | func createNode(name: String, completion: @escaping (_ node: Node?) -> Void) {
114 |
115 | let name = NodeCollector.computeCopyName(baseName: name, path: NodeCollector.writePath())
116 | let nodePath = NodeCollector.writePath().appendingPathComponent(name).appendingPathExtension("jot")
117 |
118 | let tmpNode = Node(url: nodePath)
119 | tmpNode.collector = self
120 | tmpNode.push { (success) in
121 | completion(success ? tmpNode : nil)
122 | }
123 |
124 | }
125 |
126 |
127 | /// Updates the meta-data for each Node in this object
128 | func update() {
129 | nodes.forEach({ $0.didUpdate() })
130 | }
131 |
132 |
133 |
134 | // MARK: - Observer methods
135 |
136 | /// Enable observer calls ;)
137 | func enableObservers() {
138 | observersEnabledValue = true
139 | }
140 |
141 |
142 | /// Suppresses the observer calls ;)
143 | func disableObservers() {
144 | observersEnabledValue = false
145 | }
146 |
147 |
148 | /// Adds a new observer to this class ;)
149 | func addObserver(_ observer: NodeCollectorObserver) {
150 | observers.append(observer)
151 | }
152 |
153 |
154 |
155 | // MARK: - BackgroundFetch methods
156 |
157 | /// Activates Background fetches
158 | func continueBackgroundFetch() {
159 | backgroundFetchIsActiveValue = true
160 | }
161 |
162 |
163 | /// Pauses the Background fetches (Process will be still running in the background)
164 | func pauseBackgroundFetch() {
165 | backgroundFetchIsActiveValue = false
166 | }
167 |
168 |
169 | // FIXME: - Can this method be written a bit better?
170 | /// Continuously fetches the newest version of the Node inside the NodeCollector
171 | /// - Parameter interval: Duration between each pull call
172 | private func initializeBackgroundFetch(interval: Int) {
173 |
174 | var downloadingFilesFromURL: [URL] = []
175 |
176 | DispatchQueue.main.async {
177 |
178 | Timer.scheduledTimer(withTimeInterval: TimeInterval(interval), repeats: true) { (timer) in
179 |
180 | if !self.backgroundFetchIsActive {
181 | return
182 | }
183 |
184 | let files = try! FileManager.default.contentsOfDirectory(atPath: NodeCollector.readPath().path)
185 | let fileURLs: [URL] = files.map({ NodeCollector.readPath().appendingPathComponent($0) })
186 |
187 | for url in fileURLs {
188 |
189 | if !url.isJot() && !url.isCloudAndJot() {
190 | continue
191 | }
192 |
193 | let tmpNode = Node(url: url)
194 | tmpNode.pull { (success) in
195 |
196 | if let targetNode = self.nodes.filter({ $0.url == tmpNode.url! }).first {
197 |
198 | if !Downloader.isCloudEnabled {
199 | return
200 | }
201 |
202 | /// - Grab the iCloud file status (Only available if file in cloud)
203 | if Downloader.getStatus(url: targetNode.url!) == URLUbiquitousItemDownloadingStatus.current {
204 | targetNode.pullData { (data) in
205 |
206 | guard let data = data else {
207 | return
208 | }
209 |
210 | if data.hashValue != targetNode.initialDataHash {
211 | targetNode.pullHandler { (success) in
212 | self.update()
213 | }
214 | }
215 |
216 | }
217 | return
218 | }
219 |
220 | /// - Checking if our targetURL has already downloading
221 | if downloadingFilesFromURL.contains(targetNode.url!) { return }
222 |
223 | /// - Downloading file from iCloud
224 | let downloader = Downloader(url: targetNode.url!)
225 | downloadingFilesFromURL.append(targetNode.url!)
226 | downloader.execute { (downloadSuccess) in
227 |
228 | if downloadSuccess {
229 |
230 | let index = downloadingFilesFromURL.firstIndex(of: targetNode.url!)
231 | downloadingFilesFromURL.remove(at: index!)
232 |
233 | targetNode.pullHandler { (success) in
234 | self.update()
235 | }
236 |
237 | }
238 |
239 | }
240 |
241 | } else {
242 | tmpNode.collector = self
243 | self.addNode(tmpNode)
244 | }
245 |
246 | }
247 |
248 | }
249 |
250 | /// - Checking if any inmemory-node has been removed from the directory
251 | for node in self.nodes {
252 | if !fileURLs.contains(node.url!) {
253 | guard let index = self.nodes.firstIndex(of: node) else { continue }
254 | self.removeNode(at: index)
255 | }
256 | }
257 |
258 | }
259 |
260 | }
261 |
262 | }
263 |
264 |
265 | /// Adds a node to the NodeCollector
266 | /// - Parameter node: Node that will be added
267 | func addNode(_ node: Node) {
268 |
269 | DispatchQueue.main.async {
270 |
271 | self.nodes.append(node)
272 | let index = self.nodes.count - 1
273 |
274 | if !self.observersEnabled { return }
275 | self.observers.forEach({ $0.didInsertNode(nodeCollector: self, at: index) })
276 |
277 | }
278 |
279 | }
280 |
281 |
282 | /// Removes a Node inside a NodeCollector at a certain index
283 | /// - Parameter index: Index of node in NodeCollector that will be removed
284 | func removeNode(at index: Int) {
285 |
286 | self.nodes.remove(at: index)
287 |
288 | if !self.observersEnabled { return }
289 | self.observers.forEach({ $0.didDeleteNode(nodeCollector: self, at: index) })
290 |
291 | }
292 |
293 |
294 |
295 | // MARK: - Static methods
296 | // FIXME: - This method is currently running on the main-thread. This needs to be changed.
297 |
298 | /// Generates a name and will modify it if this name already exists in a given directory
299 | /// - Complexity: O(n) where n is the number of files inside the root-folder
300 | /// - Parameter baseName: The target name
301 | /// - Parameter path: The path where this file is stored
302 | /// - Returns: The validated name (If name already exists this method adds the suffix ' copy' to the baseName)
303 | static func computeCopyName(baseName: String, path: URL) -> String {
304 | var newName: String = baseName
305 | var currentPath: URL = path.appendingPathComponent(newName).appendingPathExtension("jot")
306 |
307 | while true {
308 | if FileManager.default.fileExists(atPath: currentPath.path) {
309 | newName = "\(newName) " + NSLocalizedString("(copy)", comment: "")
310 | currentPath = currentPath.deletingLastPathComponent().appendingPathComponent(newName).appendingPathExtension("jot")
311 | continue
312 | }
313 | break
314 | }
315 |
316 | return newName
317 | }
318 |
319 | }
320 |
--------------------------------------------------------------------------------
/Jottre.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A61A616125EEB2B40030684C /* RedoButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61A616025EEB2B40030684C /* RedoButton.swift */; };
11 | A61A616425EEB34E0030684C /* UndoButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61A616325EEB34E0030684C /* UndoButton.swift */; };
12 | A61A616725EEB44E0030684C /* SpaceButtonBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61A616625EEB44E0030684C /* SpaceButtonBarItem.swift */; };
13 | A61BB9FA25D0604200E63CF6 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61BB9F925D0604200E63CF6 /* ShareButton.swift */; };
14 | A61BB9FE25D0608B00E63CF6 /* CustomButtonBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61BB9FD25D0608B00E63CF6 /* CustomButtonBarItem.swift */; };
15 | A6330A7F25D05F6700068DE5 /* AddButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6330A7E25D05F6700068DE5 /* AddButton.swift */; };
16 | A6350E5425C9CEFB0059E96E /* NodeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6350E5325C9CEFB0059E96E /* NodeExtensions.swift */; };
17 | A650EE6825C5819500830112 /* NodeCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A650EE6725C5819500830112 /* NodeCodable.swift */; };
18 | A650EE6B25C585D300830112 /* NodeDecoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A650EE6A25C585D300830112 /* NodeDecoderExtension.swift */; };
19 | A65F231D25B5C1B000A0E870 /* IconSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F231C25B5C1B000A0E870 /* IconSettingsCell.swift */; };
20 | A6A8803E25BAB41E0080DD9D /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6A8803D25BAB41E0080DD9D /* Downloader.swift */; };
21 | A6BB130725B45ECB008C5D8A /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BB130625B45ECB008C5D8A /* SettingsViewController.swift */; };
22 | A6BB130C25B46057008C5D8A /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BB130B25B46057008C5D8A /* Settings.swift */; };
23 | A6BB131025B46BA2008C5D8A /* SettingsNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BB130F25B46BA2008C5D8A /* SettingsNavigationViewController.swift */; };
24 | A6BB131325B46E2D008C5D8A /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BB131225B46E2D008C5D8A /* SettingsCell.swift */; };
25 | A6BB131B25B472F9008C5D8A /* AppearanceSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BB131A25B472F9008C5D8A /* AppearanceSettingsCell.swift */; };
26 | A6BB134125B49FDF008C5D8A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A6BB134325B49FDF008C5D8A /* Localizable.strings */; };
27 | A6BB134825B4A469008C5D8A /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BB134725B4A469008C5D8A /* SettingsButton.swift */; };
28 | A6BB134C25B4AEFC008C5D8A /* SettingsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BB134B25B4AEFC008C5D8A /* SettingsExtensions.swift */; };
29 | A6BB134F25B4B2D0008C5D8A /* CloudSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BB134E25B4B2D0008C5D8A /* CloudSettingsCell.swift */; };
30 | A6C0D70325B9DB9D00336230 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6C0D70225B9DB9D00336230 /* URL.swift */; };
31 | A6C0D70725B9F21400336230 /* PKDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6C0D70625B9F21400336230 /* PKDrawing.swift */; };
32 | A6C0D70B25B9F42400336230 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6C0D70A25B9F42400336230 /* LoadingView.swift */; };
33 | A6E3B7EC25B3260200C65157 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B7EB25B3260200C65157 /* AppDelegate.swift */; };
34 | A6E3B7EE25B3260200C65157 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B7ED25B3260200C65157 /* SceneDelegate.swift */; };
35 | A6E3B7F525B3260300C65157 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6E3B7F425B3260300C65157 /* Assets.xcassets */; };
36 | A6E3B7F825B3260300C65157 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A6E3B7F625B3260300C65157 /* LaunchScreen.storyboard */; };
37 | A6E3B82125B32AB400C65157 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B82025B32AB400C65157 /* Logger.swift */; };
38 | A6E3B82425B32ABE00C65157 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B82325B32ABE00C65157 /* String.swift */; };
39 | A6E3B82825B32AD300C65157 /* NavigationTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B82725B32AD300C65157 /* NavigationTextButton.swift */; };
40 | A6E3B82C25B32AEB00C65157 /* NavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B82B25B32AEB00C65157 /* NavigationViewController.swift */; };
41 | A6E3B82F25B32B0700C65157 /* InitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B82E25B32B0700C65157 /* InitialViewController.swift */; };
42 | A6E3B83425B32BBC00C65157 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B83325B32BBC00C65157 /* Node.swift */; };
43 | A6E3B83725B32BC500C65157 /* NodeCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B83625B32BC500C65157 /* NodeCollector.swift */; };
44 | A6E3B83A25B333EC00C65157 /* NodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B83925B333EC00C65157 /* NodeCell.swift */; };
45 | A6E3B83D25B3340300C65157 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B83C25B3340300C65157 /* UIView.swift */; };
46 | A6E3B84125B334AF00C65157 /* InitialExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B84025B334AF00C65157 /* InitialExtensions.swift */; };
47 | A6E3B84B25B347C100C65157 /* DrawViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B84A25B347C100C65157 /* DrawViewController.swift */; };
48 | A6E3B84E25B3495800C65157 /* DrawExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B84D25B3495800C65157 /* DrawExtensions.swift */; };
49 | A6E3B85225B34ACB00C65157 /* ThumbnailGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B85125B34ACB00C65157 /* ThumbnailGenerator.swift */; };
50 | A6E3B85E25B3673C00C65157 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B85D25B3673C00C65157 /* UIDevice.swift */; };
51 | A6E3B86525B372A800C65157 /* ExportActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B86425B372A800C65157 /* ExportActions.swift */; };
52 | A6E3B86825B374A200C65157 /* MenuActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E3B86725B374A200C65157 /* MenuActions.swift */; };
53 | A6F60F8525CAA2CC002CAAE4 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6F60F8425CAA2CC002CAAE4 /* Data.swift */; };
54 | /* End PBXBuildFile section */
55 |
56 | /* Begin PBXFileReference section */
57 | A61A616025EEB2B40030684C /* RedoButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedoButton.swift; sourceTree = ""; };
58 | A61A616325EEB34E0030684C /* UndoButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoButton.swift; sourceTree = ""; };
59 | A61A616625EEB44E0030684C /* SpaceButtonBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceButtonBarItem.swift; sourceTree = ""; };
60 | A61BB9F925D0604200E63CF6 /* ShareButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButton.swift; sourceTree = ""; };
61 | A61BB9FD25D0608B00E63CF6 /* CustomButtonBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButtonBarItem.swift; sourceTree = ""; };
62 | A6330A7E25D05F6700068DE5 /* AddButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddButton.swift; sourceTree = ""; };
63 | A6350E5325C9CEFB0059E96E /* NodeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeExtensions.swift; sourceTree = ""; };
64 | A650EE6725C5819500830112 /* NodeCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeCodable.swift; sourceTree = ""; };
65 | A650EE6A25C585D300830112 /* NodeDecoderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDecoderExtension.swift; sourceTree = ""; };
66 | A65F231C25B5C1B000A0E870 /* IconSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSettingsCell.swift; sourceTree = ""; };
67 | A6A8803D25BAB41E0080DD9D /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; };
68 | A6BB130625B45ECB008C5D8A /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; };
69 | A6BB130B25B46057008C5D8A /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; };
70 | A6BB130F25B46BA2008C5D8A /* SettingsNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigationViewController.swift; sourceTree = ""; };
71 | A6BB131225B46E2D008C5D8A /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = ""; };
72 | A6BB131A25B472F9008C5D8A /* AppearanceSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsCell.swift; sourceTree = ""; };
73 | A6BB134025B49F81008C5D8A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/LaunchScreen.strings; sourceTree = ""; };
74 | A6BB134225B49FDF008C5D8A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
75 | A6BB134525B49FE0008C5D8A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; };
76 | A6BB134725B4A469008C5D8A /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = ""; };
77 | A6BB134B25B4AEFC008C5D8A /* SettingsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExtensions.swift; sourceTree = ""; };
78 | A6BB134E25B4B2D0008C5D8A /* CloudSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudSettingsCell.swift; sourceTree = ""; };
79 | A6C0D70225B9DB9D00336230 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; };
80 | A6C0D70625B9F21400336230 /* PKDrawing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKDrawing.swift; sourceTree = ""; };
81 | A6C0D70A25B9F42400336230 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; };
82 | A6E3B7E825B3260200C65157 /* Jottre.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jottre.app; sourceTree = BUILT_PRODUCTS_DIR; };
83 | A6E3B7EB25B3260200C65157 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
84 | A6E3B7ED25B3260200C65157 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
85 | A6E3B7F425B3260300C65157 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
86 | A6E3B7F725B3260300C65157 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
87 | A6E3B7F925B3260300C65157 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
88 | A6E3B80025B3261800C65157 /* Jottre.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Jottre.entitlements; sourceTree = ""; };
89 | A6E3B82025B32AB400C65157 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; };
90 | A6E3B82325B32ABE00C65157 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; };
91 | A6E3B82725B32AD300C65157 /* NavigationTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTextButton.swift; sourceTree = ""; };
92 | A6E3B82B25B32AEB00C65157 /* NavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewController.swift; sourceTree = ""; };
93 | A6E3B82E25B32B0700C65157 /* InitialViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialViewController.swift; sourceTree = ""; };
94 | A6E3B83325B32BBC00C65157 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; };
95 | A6E3B83625B32BC500C65157 /* NodeCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeCollector.swift; sourceTree = ""; };
96 | A6E3B83925B333EC00C65157 /* NodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeCell.swift; sourceTree = ""; };
97 | A6E3B83C25B3340300C65157 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; };
98 | A6E3B84025B334AF00C65157 /* InitialExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialExtensions.swift; sourceTree = ""; };
99 | A6E3B84A25B347C100C65157 /* DrawViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawViewController.swift; sourceTree = ""; };
100 | A6E3B84D25B3495800C65157 /* DrawExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawExtensions.swift; sourceTree = ""; };
101 | A6E3B85125B34ACB00C65157 /* ThumbnailGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailGenerator.swift; sourceTree = ""; };
102 | A6E3B85D25B3673C00C65157 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIDevice.swift; path = Jottre/Helpers/UIDevice.swift; sourceTree = SOURCE_ROOT; };
103 | A6E3B86425B372A800C65157 /* ExportActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportActions.swift; sourceTree = ""; };
104 | A6E3B86725B374A200C65157 /* MenuActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuActions.swift; sourceTree = ""; };
105 | A6F60F8425CAA2CC002CAAE4 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; };
106 | /* End PBXFileReference section */
107 |
108 | /* Begin PBXFrameworksBuildPhase section */
109 | A6E3B7E525B3260200C65157 /* Frameworks */ = {
110 | isa = PBXFrameworksBuildPhase;
111 | buildActionMask = 2147483647;
112 | files = (
113 | );
114 | runOnlyForDeploymentPostprocessing = 0;
115 | };
116 | /* End PBXFrameworksBuildPhase section */
117 |
118 | /* Begin PBXGroup section */
119 | A61BBA0125D0634400E63CF6 /* BarButtonItems */ = {
120 | isa = PBXGroup;
121 | children = (
122 | A6E3B82725B32AD300C65157 /* NavigationTextButton.swift */,
123 | A61BB9FD25D0608B00E63CF6 /* CustomButtonBarItem.swift */,
124 | A61A616025EEB2B40030684C /* RedoButton.swift */,
125 | A61A616325EEB34E0030684C /* UndoButton.swift */,
126 | A61A616625EEB44E0030684C /* SpaceButtonBarItem.swift */,
127 | A6BB134725B4A469008C5D8A /* SettingsButton.swift */,
128 | A61BB9F925D0604200E63CF6 /* ShareButton.swift */,
129 | A6330A7E25D05F6700068DE5 /* AddButton.swift */,
130 | );
131 | path = BarButtonItems;
132 | sourceTree = "";
133 | };
134 | A6BB130525B45E52008C5D8A /* SettingsScene */ = {
135 | isa = PBXGroup;
136 | children = (
137 | A6BB130F25B46BA2008C5D8A /* SettingsNavigationViewController.swift */,
138 | A6BB130625B45ECB008C5D8A /* SettingsViewController.swift */,
139 | A6BB134B25B4AEFC008C5D8A /* SettingsExtensions.swift */,
140 | );
141 | path = SettingsScene;
142 | sourceTree = "";
143 | };
144 | A6BB130925B46047008C5D8A /* Node */ = {
145 | isa = PBXGroup;
146 | children = (
147 | A6E3B83325B32BBC00C65157 /* Node.swift */,
148 | A6350E5325C9CEFB0059E96E /* NodeExtensions.swift */,
149 | A650EE6A25C585D300830112 /* NodeDecoderExtension.swift */,
150 | A650EE6725C5819500830112 /* NodeCodable.swift */,
151 | A6E3B83625B32BC500C65157 /* NodeCollector.swift */,
152 | );
153 | path = Node;
154 | sourceTree = "";
155 | };
156 | A6BB130A25B4604C008C5D8A /* Settings */ = {
157 | isa = PBXGroup;
158 | children = (
159 | A6BB130B25B46057008C5D8A /* Settings.swift */,
160 | );
161 | path = Settings;
162 | sourceTree = "";
163 | };
164 | A6BB131525B472D9008C5D8A /* SettingsCell */ = {
165 | isa = PBXGroup;
166 | children = (
167 | A6BB131225B46E2D008C5D8A /* SettingsCell.swift */,
168 | A6BB131A25B472F9008C5D8A /* AppearanceSettingsCell.swift */,
169 | A6BB134E25B4B2D0008C5D8A /* CloudSettingsCell.swift */,
170 | A65F231C25B5C1B000A0E870 /* IconSettingsCell.swift */,
171 | );
172 | path = SettingsCell;
173 | sourceTree = "";
174 | };
175 | A6E3B7DF25B3260200C65157 = {
176 | isa = PBXGroup;
177 | children = (
178 | A6E3B7EA25B3260200C65157 /* Jottre */,
179 | A6E3B7E925B3260200C65157 /* Products */,
180 | );
181 | sourceTree = "";
182 | };
183 | A6E3B7E925B3260200C65157 /* Products */ = {
184 | isa = PBXGroup;
185 | children = (
186 | A6E3B7E825B3260200C65157 /* Jottre.app */,
187 | );
188 | name = Products;
189 | sourceTree = "";
190 | };
191 | A6E3B7EA25B3260200C65157 /* Jottre */ = {
192 | isa = PBXGroup;
193 | children = (
194 | A6E3B85025B34ABA00C65157 /* Helpers */,
195 | A6E3B83225B32BAF00C65157 /* Models */,
196 | A6E3B81325B3297200C65157 /* Extensions */,
197 | A6E3B80F25B3295200C65157 /* Views */,
198 | A6E3B80725B3287C00C65157 /* Controllers */,
199 | A6E3B80025B3261800C65157 /* Jottre.entitlements */,
200 | A6E3B7EB25B3260200C65157 /* AppDelegate.swift */,
201 | A6E3B7ED25B3260200C65157 /* SceneDelegate.swift */,
202 | A6E3B7F425B3260300C65157 /* Assets.xcassets */,
203 | A6E3B7F625B3260300C65157 /* LaunchScreen.storyboard */,
204 | A6E3B7F925B3260300C65157 /* Info.plist */,
205 | A6BB134325B49FDF008C5D8A /* Localizable.strings */,
206 | );
207 | path = Jottre;
208 | sourceTree = "";
209 | };
210 | A6E3B80725B3287C00C65157 /* Controllers */ = {
211 | isa = PBXGroup;
212 | children = (
213 | A6E3B82B25B32AEB00C65157 /* NavigationViewController.swift */,
214 | A6E3B83F25B3349900C65157 /* InitialScene */,
215 | A6E3B84925B347AE00C65157 /* DrawScene */,
216 | A6BB130525B45E52008C5D8A /* SettingsScene */,
217 | );
218 | path = Controllers;
219 | sourceTree = "";
220 | };
221 | A6E3B80F25B3295200C65157 /* Views */ = {
222 | isa = PBXGroup;
223 | children = (
224 | A61BBA0125D0634400E63CF6 /* BarButtonItems */,
225 | A6BB131525B472D9008C5D8A /* SettingsCell */,
226 | A6E3B83925B333EC00C65157 /* NodeCell.swift */,
227 | A6C0D70A25B9F42400336230 /* LoadingView.swift */,
228 | );
229 | path = Views;
230 | sourceTree = "";
231 | };
232 | A6E3B81325B3297200C65157 /* Extensions */ = {
233 | isa = PBXGroup;
234 | children = (
235 | A6E3B82025B32AB400C65157 /* Logger.swift */,
236 | A6E3B82325B32ABE00C65157 /* String.swift */,
237 | A6E3B83C25B3340300C65157 /* UIView.swift */,
238 | A6E3B85D25B3673C00C65157 /* UIDevice.swift */,
239 | A6C0D70225B9DB9D00336230 /* URL.swift */,
240 | A6C0D70625B9F21400336230 /* PKDrawing.swift */,
241 | A6F60F8425CAA2CC002CAAE4 /* Data.swift */,
242 | );
243 | path = Extensions;
244 | sourceTree = "";
245 | };
246 | A6E3B83225B32BAF00C65157 /* Models */ = {
247 | isa = PBXGroup;
248 | children = (
249 | A6BB130A25B4604C008C5D8A /* Settings */,
250 | A6BB130925B46047008C5D8A /* Node */,
251 | );
252 | path = Models;
253 | sourceTree = "";
254 | };
255 | A6E3B83F25B3349900C65157 /* InitialScene */ = {
256 | isa = PBXGroup;
257 | children = (
258 | A6E3B82E25B32B0700C65157 /* InitialViewController.swift */,
259 | A6E3B84025B334AF00C65157 /* InitialExtensions.swift */,
260 | A6E3B86725B374A200C65157 /* MenuActions.swift */,
261 | );
262 | path = InitialScene;
263 | sourceTree = "";
264 | };
265 | A6E3B84925B347AE00C65157 /* DrawScene */ = {
266 | isa = PBXGroup;
267 | children = (
268 | A6E3B84A25B347C100C65157 /* DrawViewController.swift */,
269 | A6E3B84D25B3495800C65157 /* DrawExtensions.swift */,
270 | A6E3B86425B372A800C65157 /* ExportActions.swift */,
271 | );
272 | path = DrawScene;
273 | sourceTree = "";
274 | };
275 | A6E3B85025B34ABA00C65157 /* Helpers */ = {
276 | isa = PBXGroup;
277 | children = (
278 | A6E3B85125B34ACB00C65157 /* ThumbnailGenerator.swift */,
279 | A6A8803D25BAB41E0080DD9D /* Downloader.swift */,
280 | );
281 | path = Helpers;
282 | sourceTree = "";
283 | };
284 | /* End PBXGroup section */
285 |
286 | /* Begin PBXNativeTarget section */
287 | A6E3B7E725B3260200C65157 /* Jottre */ = {
288 | isa = PBXNativeTarget;
289 | buildConfigurationList = A6E3B7FC25B3260300C65157 /* Build configuration list for PBXNativeTarget "Jottre" */;
290 | buildPhases = (
291 | A6E3B7E425B3260200C65157 /* Sources */,
292 | A6E3B7E525B3260200C65157 /* Frameworks */,
293 | A6E3B7E625B3260200C65157 /* Resources */,
294 | );
295 | buildRules = (
296 | );
297 | dependencies = (
298 | );
299 | name = Jottre;
300 | productName = Jottre;
301 | productReference = A6E3B7E825B3260200C65157 /* Jottre.app */;
302 | productType = "com.apple.product-type.application";
303 | };
304 | /* End PBXNativeTarget section */
305 |
306 | /* Begin PBXProject section */
307 | A6E3B7E025B3260200C65157 /* Project object */ = {
308 | isa = PBXProject;
309 | attributes = {
310 | LastSwiftUpdateCheck = 1230;
311 | LastUpgradeCheck = 1230;
312 | TargetAttributes = {
313 | A6E3B7E725B3260200C65157 = {
314 | CreatedOnToolsVersion = 12.3;
315 | };
316 | };
317 | };
318 | buildConfigurationList = A6E3B7E325B3260200C65157 /* Build configuration list for PBXProject "Jottre" */;
319 | compatibilityVersion = "Xcode 9.3";
320 | developmentRegion = en;
321 | hasScannedForEncodings = 0;
322 | knownRegions = (
323 | en,
324 | Base,
325 | de,
326 | );
327 | mainGroup = A6E3B7DF25B3260200C65157;
328 | productRefGroup = A6E3B7E925B3260200C65157 /* Products */;
329 | projectDirPath = "";
330 | projectRoot = "";
331 | targets = (
332 | A6E3B7E725B3260200C65157 /* Jottre */,
333 | );
334 | };
335 | /* End PBXProject section */
336 |
337 | /* Begin PBXResourcesBuildPhase section */
338 | A6E3B7E625B3260200C65157 /* Resources */ = {
339 | isa = PBXResourcesBuildPhase;
340 | buildActionMask = 2147483647;
341 | files = (
342 | A6E3B7F825B3260300C65157 /* LaunchScreen.storyboard in Resources */,
343 | A6BB134125B49FDF008C5D8A /* Localizable.strings in Resources */,
344 | A6E3B7F525B3260300C65157 /* Assets.xcassets in Resources */,
345 | );
346 | runOnlyForDeploymentPostprocessing = 0;
347 | };
348 | /* End PBXResourcesBuildPhase section */
349 |
350 | /* Begin PBXSourcesBuildPhase section */
351 | A6E3B7E425B3260200C65157 /* Sources */ = {
352 | isa = PBXSourcesBuildPhase;
353 | buildActionMask = 2147483647;
354 | files = (
355 | A6E3B86525B372A800C65157 /* ExportActions.swift in Sources */,
356 | A61A616725EEB44E0030684C /* SpaceButtonBarItem.swift in Sources */,
357 | A61A616125EEB2B40030684C /* RedoButton.swift in Sources */,
358 | A6E3B7EC25B3260200C65157 /* AppDelegate.swift in Sources */,
359 | A6E3B84125B334AF00C65157 /* InitialExtensions.swift in Sources */,
360 | A6330A7F25D05F6700068DE5 /* AddButton.swift in Sources */,
361 | A6E3B85225B34ACB00C65157 /* ThumbnailGenerator.swift in Sources */,
362 | A6BB134825B4A469008C5D8A /* SettingsButton.swift in Sources */,
363 | A6E3B82C25B32AEB00C65157 /* NavigationViewController.swift in Sources */,
364 | A6BB130725B45ECB008C5D8A /* SettingsViewController.swift in Sources */,
365 | A6E3B84E25B3495800C65157 /* DrawExtensions.swift in Sources */,
366 | A6E3B83D25B3340300C65157 /* UIView.swift in Sources */,
367 | A6350E5425C9CEFB0059E96E /* NodeExtensions.swift in Sources */,
368 | A6C0D70725B9F21400336230 /* PKDrawing.swift in Sources */,
369 | A6E3B84B25B347C100C65157 /* DrawViewController.swift in Sources */,
370 | A6E3B85E25B3673C00C65157 /* UIDevice.swift in Sources */,
371 | A6C0D70325B9DB9D00336230 /* URL.swift in Sources */,
372 | A650EE6B25C585D300830112 /* NodeDecoderExtension.swift in Sources */,
373 | A6BB131325B46E2D008C5D8A /* SettingsCell.swift in Sources */,
374 | A6E3B7EE25B3260200C65157 /* SceneDelegate.swift in Sources */,
375 | A6E3B83425B32BBC00C65157 /* Node.swift in Sources */,
376 | A61BB9FE25D0608B00E63CF6 /* CustomButtonBarItem.swift in Sources */,
377 | A61A616425EEB34E0030684C /* UndoButton.swift in Sources */,
378 | A6E3B82F25B32B0700C65157 /* InitialViewController.swift in Sources */,
379 | A6E3B83A25B333EC00C65157 /* NodeCell.swift in Sources */,
380 | A65F231D25B5C1B000A0E870 /* IconSettingsCell.swift in Sources */,
381 | A6F60F8525CAA2CC002CAAE4 /* Data.swift in Sources */,
382 | A6BB131B25B472F9008C5D8A /* AppearanceSettingsCell.swift in Sources */,
383 | A6E3B83725B32BC500C65157 /* NodeCollector.swift in Sources */,
384 | A6E3B82425B32ABE00C65157 /* String.swift in Sources */,
385 | A6BB130C25B46057008C5D8A /* Settings.swift in Sources */,
386 | A6BB134F25B4B2D0008C5D8A /* CloudSettingsCell.swift in Sources */,
387 | A6BB134C25B4AEFC008C5D8A /* SettingsExtensions.swift in Sources */,
388 | A6A8803E25BAB41E0080DD9D /* Downloader.swift in Sources */,
389 | A6BB131025B46BA2008C5D8A /* SettingsNavigationViewController.swift in Sources */,
390 | A6E3B82825B32AD300C65157 /* NavigationTextButton.swift in Sources */,
391 | A61BB9FA25D0604200E63CF6 /* ShareButton.swift in Sources */,
392 | A650EE6825C5819500830112 /* NodeCodable.swift in Sources */,
393 | A6E3B86825B374A200C65157 /* MenuActions.swift in Sources */,
394 | A6C0D70B25B9F42400336230 /* LoadingView.swift in Sources */,
395 | A6E3B82125B32AB400C65157 /* Logger.swift in Sources */,
396 | );
397 | runOnlyForDeploymentPostprocessing = 0;
398 | };
399 | /* End PBXSourcesBuildPhase section */
400 |
401 | /* Begin PBXVariantGroup section */
402 | A6BB134325B49FDF008C5D8A /* Localizable.strings */ = {
403 | isa = PBXVariantGroup;
404 | children = (
405 | A6BB134225B49FDF008C5D8A /* en */,
406 | A6BB134525B49FE0008C5D8A /* de */,
407 | );
408 | name = Localizable.strings;
409 | sourceTree = "";
410 | };
411 | A6E3B7F625B3260300C65157 /* LaunchScreen.storyboard */ = {
412 | isa = PBXVariantGroup;
413 | children = (
414 | A6E3B7F725B3260300C65157 /* Base */,
415 | A6BB134025B49F81008C5D8A /* de */,
416 | );
417 | name = LaunchScreen.storyboard;
418 | sourceTree = "";
419 | };
420 | /* End PBXVariantGroup section */
421 |
422 | /* Begin XCBuildConfiguration section */
423 | A6E3B7FA25B3260300C65157 /* Debug */ = {
424 | isa = XCBuildConfiguration;
425 | buildSettings = {
426 | ALWAYS_SEARCH_USER_PATHS = NO;
427 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
428 | CLANG_ANALYZER_NONNULL = YES;
429 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
430 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
431 | CLANG_CXX_LIBRARY = "libc++";
432 | CLANG_ENABLE_MODULES = YES;
433 | CLANG_ENABLE_OBJC_ARC = YES;
434 | CLANG_ENABLE_OBJC_WEAK = YES;
435 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
436 | CLANG_WARN_BOOL_CONVERSION = YES;
437 | CLANG_WARN_COMMA = YES;
438 | CLANG_WARN_CONSTANT_CONVERSION = YES;
439 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
440 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
441 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
442 | CLANG_WARN_EMPTY_BODY = YES;
443 | CLANG_WARN_ENUM_CONVERSION = YES;
444 | CLANG_WARN_INFINITE_RECURSION = YES;
445 | CLANG_WARN_INT_CONVERSION = YES;
446 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
447 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
448 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
449 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
450 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
451 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
452 | CLANG_WARN_STRICT_PROTOTYPES = YES;
453 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
454 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
455 | CLANG_WARN_UNREACHABLE_CODE = YES;
456 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
457 | COPY_PHASE_STRIP = NO;
458 | DEBUG_INFORMATION_FORMAT = dwarf;
459 | ENABLE_STRICT_OBJC_MSGSEND = YES;
460 | ENABLE_TESTABILITY = YES;
461 | GCC_C_LANGUAGE_STANDARD = gnu11;
462 | GCC_DYNAMIC_NO_PIC = NO;
463 | GCC_NO_COMMON_BLOCKS = YES;
464 | GCC_OPTIMIZATION_LEVEL = 0;
465 | GCC_PREPROCESSOR_DEFINITIONS = (
466 | "DEBUG=1",
467 | "$(inherited)",
468 | );
469 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
470 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
471 | GCC_WARN_UNDECLARED_SELECTOR = YES;
472 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
473 | GCC_WARN_UNUSED_FUNCTION = YES;
474 | GCC_WARN_UNUSED_VARIABLE = YES;
475 | IPHONEOS_DEPLOYMENT_TARGET = 14.3;
476 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
477 | MTL_FAST_MATH = YES;
478 | ONLY_ACTIVE_ARCH = YES;
479 | SDKROOT = iphoneos;
480 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
481 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
482 | };
483 | name = Debug;
484 | };
485 | A6E3B7FB25B3260300C65157 /* Release */ = {
486 | isa = XCBuildConfiguration;
487 | buildSettings = {
488 | ALWAYS_SEARCH_USER_PATHS = NO;
489 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
490 | CLANG_ANALYZER_NONNULL = YES;
491 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
492 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
493 | CLANG_CXX_LIBRARY = "libc++";
494 | CLANG_ENABLE_MODULES = YES;
495 | CLANG_ENABLE_OBJC_ARC = YES;
496 | CLANG_ENABLE_OBJC_WEAK = YES;
497 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
498 | CLANG_WARN_BOOL_CONVERSION = YES;
499 | CLANG_WARN_COMMA = YES;
500 | CLANG_WARN_CONSTANT_CONVERSION = YES;
501 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
502 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
503 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
504 | CLANG_WARN_EMPTY_BODY = YES;
505 | CLANG_WARN_ENUM_CONVERSION = YES;
506 | CLANG_WARN_INFINITE_RECURSION = YES;
507 | CLANG_WARN_INT_CONVERSION = YES;
508 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
509 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
510 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
511 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
512 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
513 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
514 | CLANG_WARN_STRICT_PROTOTYPES = YES;
515 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
516 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
517 | CLANG_WARN_UNREACHABLE_CODE = YES;
518 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
519 | COPY_PHASE_STRIP = NO;
520 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
521 | ENABLE_NS_ASSERTIONS = NO;
522 | ENABLE_STRICT_OBJC_MSGSEND = YES;
523 | GCC_C_LANGUAGE_STANDARD = gnu11;
524 | GCC_NO_COMMON_BLOCKS = YES;
525 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
526 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
527 | GCC_WARN_UNDECLARED_SELECTOR = YES;
528 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
529 | GCC_WARN_UNUSED_FUNCTION = YES;
530 | GCC_WARN_UNUSED_VARIABLE = YES;
531 | IPHONEOS_DEPLOYMENT_TARGET = 14.3;
532 | MTL_ENABLE_DEBUG_INFO = NO;
533 | MTL_FAST_MATH = YES;
534 | SDKROOT = iphoneos;
535 | SWIFT_COMPILATION_MODE = wholemodule;
536 | SWIFT_OPTIMIZATION_LEVEL = "-O";
537 | VALIDATE_PRODUCT = YES;
538 | };
539 | name = Release;
540 | };
541 | A6E3B7FD25B3260300C65157 /* Debug */ = {
542 | isa = XCBuildConfiguration;
543 | buildSettings = {
544 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
545 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
546 | CODE_SIGN_ENTITLEMENTS = Jottre/Jottre.entitlements;
547 | CODE_SIGN_STYLE = Automatic;
548 | CURRENT_PROJECT_VERSION = 20;
549 | DEVELOPMENT_TEAM = Y78RPE9KK3;
550 | INFOPLIST_FILE = Jottre/Info.plist;
551 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
552 | LD_RUNPATH_SEARCH_PATHS = (
553 | "$(inherited)",
554 | "@executable_path/Frameworks",
555 | );
556 | MARKETING_VERSION = 1.5;
557 | PRODUCT_BUNDLE_IDENTIFIER = com.antonlorani.jottre;
558 | PRODUCT_NAME = "$(TARGET_NAME)";
559 | SUPPORTS_MACCATALYST = YES;
560 | SWIFT_VERSION = 5.0;
561 | TARGETED_DEVICE_FAMILY = "1,2";
562 | };
563 | name = Debug;
564 | };
565 | A6E3B7FE25B3260300C65157 /* Release */ = {
566 | isa = XCBuildConfiguration;
567 | buildSettings = {
568 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
569 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
570 | CODE_SIGN_ENTITLEMENTS = Jottre/Jottre.entitlements;
571 | CODE_SIGN_STYLE = Automatic;
572 | CURRENT_PROJECT_VERSION = 20;
573 | DEVELOPMENT_TEAM = Y78RPE9KK3;
574 | INFOPLIST_FILE = Jottre/Info.plist;
575 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
576 | LD_RUNPATH_SEARCH_PATHS = (
577 | "$(inherited)",
578 | "@executable_path/Frameworks",
579 | );
580 | MARKETING_VERSION = 1.5;
581 | PRODUCT_BUNDLE_IDENTIFIER = com.antonlorani.jottre;
582 | PRODUCT_NAME = "$(TARGET_NAME)";
583 | SUPPORTS_MACCATALYST = YES;
584 | SWIFT_VERSION = 5.0;
585 | TARGETED_DEVICE_FAMILY = "1,2";
586 | };
587 | name = Release;
588 | };
589 | /* End XCBuildConfiguration section */
590 |
591 | /* Begin XCConfigurationList section */
592 | A6E3B7E325B3260200C65157 /* Build configuration list for PBXProject "Jottre" */ = {
593 | isa = XCConfigurationList;
594 | buildConfigurations = (
595 | A6E3B7FA25B3260300C65157 /* Debug */,
596 | A6E3B7FB25B3260300C65157 /* Release */,
597 | );
598 | defaultConfigurationIsVisible = 0;
599 | defaultConfigurationName = Release;
600 | };
601 | A6E3B7FC25B3260300C65157 /* Build configuration list for PBXNativeTarget "Jottre" */ = {
602 | isa = XCConfigurationList;
603 | buildConfigurations = (
604 | A6E3B7FD25B3260300C65157 /* Debug */,
605 | A6E3B7FE25B3260300C65157 /* Release */,
606 | );
607 | defaultConfigurationIsVisible = 0;
608 | defaultConfigurationName = Release;
609 | };
610 | /* End XCConfigurationList section */
611 | };
612 | rootObject = A6E3B7E025B3260200C65157 /* Project object */;
613 | }
614 |
--------------------------------------------------------------------------------