├── .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 |
5 | 6 |
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 |
27 | 28 |
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 | --------------------------------------------------------------------------------