├── Resources ├── appstore.png ├── graphnote.png ├── graphnote_icon.png ├── graphnote_icon_128.png ├── graphnote_screenshot.png ├── graphnote_screenshot_dark.png ├── graphnote_screenshot_light.png ├── graphnote_screenshot_link_content.png ├── graphnote_screenshot_link_content_light.png └── graphnote_screenshot_link_content_preview_light.png ├── Shared ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 16.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 256.png │ │ ├── 29.png │ │ ├── 32.png │ │ ├── 40.png │ │ ├── 50.png │ │ ├── 512.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 64.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 1024.png │ │ └── Contents.json │ ├── GraphnoteIcon.imageset │ │ ├── 1024.png │ │ ├── 1024 1.png │ │ ├── 1024 2.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── AppGlobalUIState.swift ├── TreeDocumentIdentifier.swift ├── GlobalDimension.swift ├── Components │ ├── DocumentView │ │ ├── BlockSpacer.swift │ │ ├── BlockType.swift │ │ ├── BulletView.swift │ │ ├── HeadingView.swift │ │ ├── PromptView.swift │ │ ├── ContentLinkModalVM.swift │ │ ├── DocumentViewVM.swift │ │ ├── ContentLinkModal.swift │ │ ├── DocumentPreviewView.swift │ │ ├── BlockViewVM.swift │ │ ├── BlockView.swift │ │ └── DocumentView.swift │ ├── TreeView │ │ ├── TreeTagView.swift │ │ ├── TreeDocView.swift │ │ ├── CheckmarkView.swift │ │ ├── TreeViewItem.swift │ │ ├── TreeViewAddView.swift │ │ ├── MenuCardView.swift │ │ ├── ArrowView.swift │ │ ├── TreeBulletView.swift │ │ ├── TreeViewSubline.swift │ │ ├── TreeViewLine.swift │ │ ├── TreeView.swift │ │ └── TreeViewLabel.swift │ ├── LoadingSpinnerView.swift │ ├── LabelField │ │ ├── EditIconView.swift │ │ ├── XMarkIconView.swift │ │ ├── CheckmarkIconView.swift │ │ └── LabelField.swift │ ├── Sidebar │ │ ├── GearIconVIew.swift │ │ └── SidebarView.swift │ ├── PromptMenu.swift │ ├── Toolbar │ │ └── ToolbarView.swift │ ├── Labels │ │ ├── LabelContextMenu.swift │ │ ├── LabelColors.swift │ │ ├── LabelPalette.swift │ │ ├── LabelColor.swift │ │ └── LabelView.swift │ ├── WorkspaceMenu.swift │ ├── SyncStatusIconView.swift │ ├── LoadingView.swift │ ├── AddLabelView.swift │ ├── AddLabelField.swift │ ├── SplitView │ │ └── SplitView.swift │ └── SignInView.swift ├── Model │ ├── LabelLink.swift │ ├── User.swift │ ├── Workspace.swift │ ├── Label.swift │ ├── Document.swift │ └── Block.swift ├── Spacing.swift ├── ColorPalette.swift ├── Services │ ├── SyncService │ │ ├── SyncMessage.swift │ │ ├── SyncServiceDBApplyQueue.swift │ │ ├── SyncServiceDBPullQueue.swift │ │ └── SyncServiceDBPushQueue.swift │ ├── LabelService.swift │ └── UserService.swift ├── GNColor.swift ├── Repositories │ ├── UserBuilder.swift │ ├── LabelLinkRepo.swift │ ├── LabelRepo.swift │ └── UserRepo.swift ├── DB Model │ ├── SyncMessageIDEntity.swift │ ├── SyncMessageEntity.swift │ ├── UserEntity.swift │ ├── LabelLinkEntity.swift │ ├── WorkspaceEntity.swift │ ├── DocumentEntity.swift │ ├── BlockEntity.swift │ ├── DataController.swift │ ├── LabelEntity.swift │ └── Graphnote.xcdatamodeld │ │ └── Graphnote.xcdatamodel │ │ └── contents ├── HorizontalFlexView.swift ├── LinkedListView.swift ├── Containers │ ├── BlockViewContainerVM.swift │ ├── DocumentContainer.swift │ ├── DocumentContainerVM.swift │ └── BlockViewContainer.swift ├── SettingsView.swift └── ContentViewVM.swift ├── .gitignore ├── Docs └── Diagrams │ ├── PullQueue │ ├── PullQueue.drawio.png │ └── PullQueue.drawio │ ├── PushQueue │ ├── PushQueue.drawio.png │ └── PushQueue.drawio │ └── AppStartup │ └── AppStartupDiagram.drawio.png ├── iOS ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── iOS.entitlements ├── Graphnote--iOS--Info.plist ├── GlobalDimensionIOS.swift ├── AppIOS.swift └── MobileUtils.swift ├── Graphnote.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── Graphnote (iOS).xcscheme │ └── Graphnote (macOS).xcscheme ├── macOS ├── macOS.entitlements ├── Graphnote--macOS--Info.plist └── App │ ├── AppMacOS.swift │ └── GlobalDimensionMacOS.swift └── README.md /Resources/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/appstore.png -------------------------------------------------------------------------------- /Resources/graphnote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/graphnote.png -------------------------------------------------------------------------------- /Resources/graphnote_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/graphnote_icon.png -------------------------------------------------------------------------------- /Resources/graphnote_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/graphnote_icon_128.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Resources/graphnote_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/graphnote_screenshot.png -------------------------------------------------------------------------------- /Resources/graphnote_screenshot_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/graphnote_screenshot_dark.png -------------------------------------------------------------------------------- /Resources/graphnote_screenshot_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/graphnote_screenshot_light.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Graphnote.xcodeproj/project.xcworkspace/xcuserdata/ 3 | Graphnote.xcodeproj/xcuserdata/ 4 | GoogleService-Info.plist 5 | -------------------------------------------------------------------------------- /Docs/Diagrams/PullQueue/PullQueue.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Docs/Diagrams/PullQueue/PullQueue.drawio.png -------------------------------------------------------------------------------- /Docs/Diagrams/PushQueue/PushQueue.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Docs/Diagrams/PushQueue/PushQueue.drawio.png -------------------------------------------------------------------------------- /iOS/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Resources/graphnote_screenshot_link_content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/graphnote_screenshot_link_content.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Docs/Diagrams/AppStartup/AppStartupDiagram.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Docs/Diagrams/AppStartup/AppStartupDiagram.drawio.png -------------------------------------------------------------------------------- /Resources/graphnote_screenshot_link_content_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/graphnote_screenshot_link_content_light.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/GraphnoteIcon.imageset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/GraphnoteIcon.imageset/1024.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/GraphnoteIcon.imageset/1024 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/GraphnoteIcon.imageset/1024 1.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/GraphnoteIcon.imageset/1024 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Shared/Assets.xcassets/GraphnoteIcon.imageset/1024 2.png -------------------------------------------------------------------------------- /Resources/graphnote_screenshot_link_content_preview_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphnote-app/graphnote/HEAD/Resources/graphnote_screenshot_link_content_preview_light.png -------------------------------------------------------------------------------- /Graphnote.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Shared/AppGlobalUIState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppGlobalUIState.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/2/23. 6 | // 7 | 8 | enum AppGlobalUIState { 9 | case loading 10 | case doc 11 | case settings 12 | case signIn 13 | } 14 | -------------------------------------------------------------------------------- /Shared/TreeDocumentIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeDocumentIdentifier.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 2/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TreeDocumentIdentifier: Equatable { 11 | let label: UUID 12 | let document: UUID 13 | let workspace: UUID 14 | } 15 | -------------------------------------------------------------------------------- /Graphnote.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Shared/GlobalDimension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalDimension.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/29/23. 6 | // 7 | 8 | import Foundation 9 | 10 | #if os(macOS) 11 | typealias GlobalDimension = GlobalDimensionMacOS 12 | #else 13 | typealias GlobalDimension = GlobalDimensionIOS 14 | #endif 15 | -------------------------------------------------------------------------------- /iOS/iOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.applesignin 6 | 7 | Default 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/BlockSpacer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockSpacer.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 3/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BlockSpacer: View { 11 | var body: some View { 12 | Spacer() 13 | .frame(height: Spacing.spacing4.rawValue) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/BlockType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockType.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 3/12/23. 6 | // 7 | 8 | enum BlockType: String, Codable { 9 | case body 10 | case heading1 11 | case heading2 12 | case heading3 13 | case heading4 14 | case bullet 15 | case contentLink 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Shared/Model/LabelLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelLink.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/27/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct LabelLink: Codable { 11 | let id: UUID 12 | let label: UUID 13 | let document: UUID 14 | let workspace: UUID 15 | let createdAt: Date 16 | let modifiedAt: Date 17 | } 18 | -------------------------------------------------------------------------------- /Shared/Model/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct User: Codable, Equatable { 11 | public let id: String 12 | public let email: String 13 | public let givenName: String? 14 | public let familyName: String? 15 | public let createdAt: Date 16 | public let modifiedAt: Date 17 | } 18 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/TreeTagView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeTagView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 1/22/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TreeTagView: View { 11 | var body: some View { 12 | Image(systemName: "tag") 13 | } 14 | } 15 | 16 | struct FileIconView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | TreeTagView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/TreeDocView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeDocView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 3/8/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TreeDocView: View { 11 | var body: some View { 12 | Image(systemName: "doc.plaintext") 13 | } 14 | } 15 | 16 | struct TreeDocView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | TreeDocView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/CheckmarkView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckmarkView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 1/26/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CheckmarkView: View { 11 | var body: some View { 12 | Image(systemName: "checkmark") 13 | } 14 | } 15 | 16 | struct CheckmarkView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | CheckmarkView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/TreeViewItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeViewItem.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/21/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct TreeViewItem { 12 | let id: UUID 13 | let workspace: UUID 14 | let title: String 15 | let color: Color 16 | let subItems: [TreeViewSubItem]? 17 | } 18 | 19 | struct TreeViewSubItem { 20 | let id: UUID 21 | let title: String 22 | } 23 | -------------------------------------------------------------------------------- /Shared/Spacing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spacings.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 2/23/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | enum Spacing: CGFloat { 12 | case spacing0 = 0 13 | case spacing1 = 4 14 | case spacing2 = 8 15 | case spacing3 = 12 16 | case spacing4 = 16 17 | case spacing5 = 20 18 | case spacing6 = 24 19 | case spacing7 = 32 20 | case spacing8 = 40 21 | case spacing9 = 56 22 | } 23 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/TreeViewAddView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeViewAddView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 1/26/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TreeViewAddView: View { 11 | var body: some View { 12 | Image(systemName: "plus") 13 | .padding(4) 14 | } 15 | } 16 | 17 | struct TreeViewAddView_Previews: PreviewProvider { 18 | static var previews: some View { 19 | TreeViewAddView() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/GraphnoteIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "1024 2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "1024 1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "1024.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Shared/Model/Workspace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Workspace.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Workspace: Equatable, Codable { 11 | static func == (lhs: Workspace, rhs: Workspace) -> Bool { 12 | lhs.id == rhs.id 13 | } 14 | 15 | let id: UUID 16 | let title: String 17 | let createdAt: Date 18 | let modifiedAt: Date 19 | let user: String 20 | let labels: [Label] 21 | let documents: [Document] 22 | } 23 | -------------------------------------------------------------------------------- /macOS/macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.applesignin 6 | 7 | Default 8 | 9 | com.apple.security.app-sandbox 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /iOS/Graphnote--iOS--Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsLocalNetworking 8 | 9 | 10 | UIApplicationSceneManifest 11 | 12 | UIApplicationSupportsMultipleScenes 13 | 14 | UISceneConfigurations 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /macOS/Graphnote--macOS--Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsLocalNetworking 8 | 9 | 10 | UIApplicationSceneManifest 11 | 12 | UIApplicationSupportsMultipleScenes 13 | 14 | UISceneConfigurations 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Shared/Components/LoadingSpinnerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingSpinnerView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/2/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LoadingSpinnerView: View { 11 | let width = Spacing.spacing8.rawValue 12 | 13 | var body: some View { 14 | Image(systemName: "gear") 15 | .resizable() 16 | .frame(width: width, height: width) 17 | } 18 | } 19 | 20 | struct LoadingScreenView_Previews: PreviewProvider { 21 | static var previews: some View { 22 | LoadingSpinnerView() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/MenuCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuCardView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MenuCardView: View { 11 | let diameter = Spacing.spacing4.rawValue 12 | 13 | var body: some View { 14 | Image(systemName: "menucard") 15 | .resizable() 16 | .frame(width: diameter, height: diameter) 17 | } 18 | } 19 | 20 | struct MenuCardView_Previews: PreviewProvider { 21 | static var previews: some View { 22 | MenuCardView() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Shared/Components/LabelField/EditIconView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddIconView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AddIconView: View { 11 | var body: some View { 12 | Image(systemName: "tag") 13 | .renderingMode(.template) 14 | .resizable() 15 | .frame(width: GlobalDimensionIOS.iconSmall, height: GlobalDimensionIOS.iconSmall) 16 | } 17 | } 18 | 19 | struct AddIconView_Previews: PreviewProvider { 20 | static var previews: some View { 21 | AddIconView() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Shared/Components/Sidebar/GearIconVIew.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GearIconVIew.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GearIconVIew: View { 11 | let action: () -> Void 12 | 13 | var body: some View { 14 | Button { 15 | action() 16 | } label: { 17 | Image(systemName: "gear") 18 | } 19 | .buttonStyle(.plain) 20 | } 21 | } 22 | 23 | struct GearIconVIew_Previews: PreviewProvider { 24 | static var previews: some View { 25 | GearIconVIew() { 26 | 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Shared/Components/LabelField/XMarkIconView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XMarkIconView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct XMarkIconView: View { 11 | var body: some View { 12 | Image(systemName: "xmark") 13 | .renderingMode(.template) 14 | .resizable() 15 | .foregroundColor(Color.red) 16 | .frame(width: GlobalDimension.iconSmall, height: GlobalDimension.iconSmall) 17 | } 18 | } 19 | 20 | struct XMarkIconView_Previews: PreviewProvider { 21 | static var previews: some View { 22 | XMarkIconView() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/ArrowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrowView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 2/19/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ArrowView: View { 11 | let color: Color 12 | let down: Bool 13 | 14 | var body: some View { 15 | Image(systemName: down ? "arrowtriangle.down.fill" : "arrowtriangle.right.fill") 16 | .renderingMode(.template) 17 | .padding() 18 | .foregroundColor(color) 19 | } 20 | } 21 | 22 | struct ArrowView_Previews: PreviewProvider { 23 | static var previews: some View { 24 | ArrowView(color: .blue, down: false) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/TreeBulletView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeBulletView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 1/22/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | fileprivate enum Dimensions: CGFloat { 11 | case bulletDiameter = 6 12 | } 13 | 14 | struct TreeBulletView: View { 15 | var body: some View { 16 | Circle() 17 | .frame(width: Dimensions.bulletDiameter.rawValue, height: Dimensions.bulletDiameter.rawValue) 18 | .foregroundColor(Color.gray) 19 | } 20 | } 21 | 22 | struct TreeBulletView_Previews: PreviewProvider { 23 | static var previews: some View { 24 | TreeBulletView() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Shared/Components/LabelField/CheckmarkIconView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckmarkIconView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CheckmarkIconView: View { 11 | var body: some View { 12 | Image(systemName: "checkmark") 13 | .renderingMode(.template) 14 | .resizable() 15 | .foregroundColor(Color.green) 16 | .frame(width: GlobalDimension.iconSmall, height: GlobalDimension.iconSmall) 17 | } 18 | } 19 | 20 | struct CheckmarkIconView_Previews: PreviewProvider { 21 | static var previews: some View { 22 | CheckmarkIconView() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Shared/ColorPalette.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorPalette.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 2/23/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ColorPalette { 11 | #if os(macOS) 12 | static let darkBG1 = Color(NSColor.controlBackgroundColor) 13 | static let lightSidebarMobile = Color(NSColor.clear) 14 | #else 15 | static let darkBG1 = Color(red: 30 / 255.0, green: 30 / 255.0, blue: 30 / 255.0) 16 | static let lightSidebarMobile = Color(red: 235.0 / 255.0, green: 235.0 / 255.0, blue: 235.0 / 255.0) 17 | static let darkSidebarMobile = Color.black 18 | #endif 19 | 20 | static let lightBG1 = Color.white 21 | 22 | static let primaryText = Color.primary 23 | } 24 | -------------------------------------------------------------------------------- /Shared/Components/PromptMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PromptMenu.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/23/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PromptMenu: View { 11 | let onAddContentLink: () -> Void 12 | 13 | var body: some View { 14 | List() { 15 | Button("Add content link") { 16 | onAddContentLink() 17 | } 18 | .buttonStyle(.borderless) 19 | } 20 | .shadow(radius: Spacing.spacing6.rawValue) 21 | .frame(width: GlobalDimension.treeWidth, height: 400) 22 | .zIndex(2) 23 | } 24 | } 25 | 26 | struct PromptMenu_Previews: PreviewProvider { 27 | static var previews: some View { 28 | PromptMenu { 29 | 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOS/GlobalDimensionIOS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalDimension.swift 3 | // Graphnote (iOS) 4 | // 5 | // Created by Hayden Pennington on 3/29/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GlobalDimensionIOS { 11 | static let treeWidth: CGFloat = 350 12 | static let toolbarWidth = Spacing.spacing4.rawValue + (Spacing.spacing3.rawValue * 2) 13 | static let maxDocumentContentWidth = 800.0 14 | // static let minDocumentContentWidth = 200.0 15 | static let minDocumentContentHeight = 300.00 16 | static let iconSmall = 16.0 17 | static let modalWidth = 300.0 18 | static let modalHeight = 450.0 19 | static let minDocumentPreviewContentWidth = 250.0 20 | static let maxDocumentPreviewContentWidth = 450.0 21 | static let labelModalHeight = 500.0 22 | static let labelModalWidth = 400.0 23 | } 24 | -------------------------------------------------------------------------------- /Shared/Services/SyncService/SyncMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncMessageType.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/6/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SyncMessageType: String, Codable { 11 | case user 12 | case workspace 13 | case document 14 | case label 15 | case labelLink 16 | case block 17 | } 18 | 19 | enum SyncMessageAction: String, Codable { 20 | case create 21 | case read 22 | case update 23 | case delete 24 | } 25 | 26 | struct SyncMessage: Codable { 27 | let id: UUID 28 | let user: String 29 | let timestamp: Date 30 | let type: SyncMessageType 31 | let action: SyncMessageAction 32 | let isSynced: Bool 33 | let isApplied: Bool 34 | let contents: String 35 | } 36 | 37 | struct SyncMessageIDsResult: Codable { 38 | let lastSyncTime: Double 39 | let ids: [String] 40 | } 41 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0x81", 10 | "red" : "0x5E" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "0.506", 28 | "red" : "0.369" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/AppIOS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppIOS.swift 3 | // Graphnote (iOS) 4 | // 5 | // Created by Hayden Pennington on 3/29/23. 6 | // 7 | 8 | import SwiftUI 9 | import FirebaseCore 10 | 11 | @main 12 | struct AppIOS: App { 13 | @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate 14 | @Environment(\.colorScheme) private var colorScheme 15 | 16 | var body: some Scene { 17 | WindowGroup { 18 | ContentView() 19 | .autocorrectionDisabled() 20 | .keyboardType(.alphabet) 21 | .scrollDismissesKeyboard(.interactively) 22 | } 23 | } 24 | } 25 | 26 | class AppDelegate: NSObject, UIApplicationDelegate { 27 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 28 | FirebaseApp.configure() 29 | return true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Shared/GNColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GNColor.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/29/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GNColorComponents { 11 | let red: CGFloat 12 | let green: CGFloat 13 | let blue: CGFloat 14 | } 15 | 16 | #if os(macOS) 17 | import Cocoa 18 | typealias GNColor = NSColor 19 | 20 | extension GNColor { 21 | func getColorComponents() -> GNColorComponents { 22 | return GNColorComponents(red: self.redComponent, green: self.greenComponent, blue: self.blueComponent) 23 | } 24 | } 25 | 26 | #else 27 | import UIKit 28 | typealias GNColor = UIColor 29 | 30 | extension GNColor { 31 | func getColorComponents() -> GNColorComponents { 32 | let components = self.cgColor.components! 33 | return GNColorComponents(red: components[0], green: components[1], blue: components[2]) 34 | } 35 | } 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /Shared/Components/Toolbar/ToolbarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToolbarView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ToolbarView: View { 11 | @Environment(\.colorScheme) private var colorScheme 12 | 13 | let content: () -> any View 14 | let action: () -> Void 15 | 16 | var body: some View { 17 | VStack { 18 | Spacer() 19 | AnyView(content()) 20 | .padding(Spacing.spacing3.rawValue) 21 | .onTapGesture(perform: action) 22 | } 23 | .background(colorScheme == .dark ? ColorPalette.darkBG1 : ColorPalette.lightBG1) 24 | } 25 | } 26 | 27 | struct ToolbarView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | ToolbarView { 30 | MenuCardView() 31 | } action: { 32 | 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /macOS/App/AppMacOS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppMacOS.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import SwiftUI 9 | import FirebaseCore 10 | 11 | @main 12 | struct AppMacOS: App { 13 | @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate 14 | 15 | var body: some Scene { 16 | WindowGroup { 17 | ContentView() 18 | .frame(minWidth: GlobalDimension.windowMinimumWidth, minHeight: GlobalDimension.windowMinimumHeight) 19 | } 20 | .defaultSize(width: GlobalDimension.windowDefaultWidth, height: GlobalDimension.windowDefaultHeight) 21 | .windowToolbarStyle(.unifiedCompact) 22 | .windowStyle(.hiddenTitleBar) 23 | } 24 | } 25 | 26 | class AppDelegate: NSObject, NSApplicationDelegate { 27 | func applicationDidFinishLaunching(_ notification: Notification) { 28 | FirebaseApp.configure() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/TreeViewSubline.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeViewSubline.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TreeViewSubline: View { 11 | let title: String 12 | let selected: Bool 13 | 14 | var body: some View { 15 | HStack { 16 | TreeBulletView() 17 | HStack { 18 | Text(title) 19 | Spacer() 20 | }.frame(width: 130) 21 | } 22 | .padding(Spacing.spacing2.rawValue) 23 | .background(selected ? Color.gray.opacity(0.25) : .clear) 24 | .cornerRadius(Spacing.spacing2.rawValue) 25 | .contentShape(RoundedRectangle(cornerRadius: Spacing.spacing2.rawValue)) 26 | 27 | } 28 | } 29 | 30 | struct TreeViewSubLine_Previews: PreviewProvider { 31 | static var previews: some View { 32 | TreeViewSubline(title: "Test preview", selected: false) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Shared/Repositories/UserBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserBuilder.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserBuilder { 11 | private static let moc = DataController.shared.container.viewContext 12 | 13 | static func create(user: User) -> Bool { 14 | let userEntity = UserEntity(entity: UserEntity.entity(), insertInto: moc) 15 | userEntity.id = user.id 16 | userEntity.email = user.email 17 | userEntity.familyName = user.familyName 18 | userEntity.givenName = user.givenName 19 | userEntity.createdAt = user.createdAt 20 | userEntity.modifiedAt = user.modifiedAt 21 | 22 | do { 23 | try moc.save() 24 | return true 25 | } catch let error { 26 | print(error) 27 | #if DEBUG 28 | fatalError() 29 | #endif 30 | return false 31 | } 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /macOS/App/GlobalDimensionMacOS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalDimensionMacOS.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GlobalDimensionMacOS { 11 | static let treeWidth: CGFloat = 250 12 | static let toolbarWidth = Spacing.spacing4.rawValue + (Spacing.spacing3.rawValue * 2) 13 | static let maxDocumentContentWidth = 800.0 14 | static let minDocumentContentWidth = 400.0 15 | static let minDocumentContentHeight = 300.0 16 | static let minDocumentPreviewContentWidth = 450.0 17 | static let maxDocumentPreviewContentWidth = 250.0 18 | static let windowMinimumWidth = 850.0 19 | static let windowMinimumHeight = 550.0 20 | static let windowDefaultWidth = 1038.0 21 | static let windowDefaultHeight = 600.0 22 | static let iconSmall = 16.0 23 | static let modalWidth = 550.0 24 | static let modalHeight = 525.0 25 | static let labelModalWidth = 500.0 26 | static let labelModalHeight = 400.0 27 | } 28 | -------------------------------------------------------------------------------- /Shared/Components/Labels/LabelContextMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelContextMenu.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LabelContextMenu: View { 11 | let rename: () -> Void 12 | let delete: () -> Void 13 | 14 | var body: some View { 15 | Group { 16 | Menu("Set Color") { 17 | ForEach(LabelPalette.allCases(), id: \.self) { color in 18 | Button(color.rawValue) { 19 | 20 | } 21 | } 22 | } 23 | Button("Rename") { 24 | rename() 25 | } 26 | Button("Drop") { 27 | delete() 28 | } 29 | } 30 | } 31 | } 32 | 33 | struct LabelContextMenu_Previews: PreviewProvider { 34 | static var previews: some View { 35 | LabelContextMenu { 36 | 37 | } delete: { 38 | 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Shared/Components/Labels/LabelColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelColors.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/20/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct LabelColor { 12 | static let primary = Color(red: 94.0 / 255.0, green: 129.0 / 255.0, blue: 255.0 / 255.0) 13 | static let purple = Color(red: 225.0 / 255.0, green: 115.0 / 255.0, blue: 226.0 / 255.0) 14 | static let pink = Color(red: 255.0 / 255.0, green: 117.0 / 255.0, blue: 179.0 / 255.0) 15 | static let orangeDark = Color(red: 255.0 / 255.0, green: 152.0 / 255.0, blue: 129.0 / 255.0) 16 | static let orangeLight = Color(red: 255.0 / 255.0, green: 201.0 / 255.0, blue: 98.0 / 255.0) 17 | static let yellow = Color(red: 249.0 / 255.0, green: 248.0 / 255.0, blue: 113.0 / 255.0) 18 | 19 | static let allColors: [SwiftUI.Color] = [ 20 | LabelColor.orangeLight, 21 | LabelColor.primary, 22 | LabelColor.pink, 23 | LabelColor.orangeDark, 24 | LabelColor.yellow, 25 | LabelColor.purple 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/BulletView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BulletView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 3/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | fileprivate let bulletFontSize: CGFloat = 18.0 11 | 12 | struct BulletView: View { 13 | let text: String 14 | 15 | var body: some View { 16 | HStack(alignment: .center, spacing: Spacing.spacing0.rawValue) { 17 | TreeBulletView() 18 | .padding(Spacing.spacing3.rawValue) 19 | Spacer() 20 | .frame(width: Spacing.spacing3.rawValue) 21 | Text(text) 22 | .font(.system(size: bulletFontSize)) 23 | } 24 | } 25 | } 26 | 27 | struct BulletView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | VStack(alignment: .leading) { 30 | BulletView(text: "Bullet point number one") 31 | BulletView(text: "Bullet point number two") 32 | BulletView(text: "Bullet point number three") 33 | BulletView(text: "Bullet point number four") 34 | }.padding() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Shared/Components/Labels/LabelPalette.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelPalette.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/27/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum LabelPalette: String, Codable { 11 | case primary, purple, red, lightPurple, pink, limeGreen, darkOrange, lightOrange 12 | 13 | func getColor() -> Color { 14 | switch self { 15 | case .primary: 16 | return LabelColor.primary 17 | case .purple: 18 | return LabelColor.purple 19 | case .red: 20 | return LabelColor.red 21 | case .lightPurple: 22 | return LabelColor.lightPurple 23 | case .pink: 24 | return LabelColor.pink 25 | case .limeGreen: 26 | return LabelColor.limeGreen 27 | case .darkOrange: 28 | return LabelColor.darkOrange 29 | case .lightOrange: 30 | return LabelColor.lightOrange 31 | } 32 | } 33 | 34 | static func allCases() -> [LabelPalette] { 35 | return [primary, purple, red, lightPurple, pink, limeGreen, darkOrange, lightOrange] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Shared/Components/WorkspaceMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkspaceMenu.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WorkspaceMenu: View { 11 | @Binding var selectedIndex: Int 12 | 13 | let workspaceTitles: [String] 14 | 15 | var body: some View { 16 | if workspaceTitles.count > selectedIndex { 17 | 18 | Menu(workspaceTitles[selectedIndex]) { 19 | ForEach(0.. NSFetchRequest { 12 | return NSFetchRequest(entityName: "SyncMessageIDEntity") 13 | } 14 | 15 | @NSManaged public var id: UUID 16 | @NSManaged public var isSynced: Bool 17 | @NSManaged public var isApplied: Bool 18 | } 19 | 20 | extension SyncMessageIDEntity { 21 | static public func getEntity(id: UUID, moc: NSManagedObjectContext) throws -> SyncMessageIDEntity? { 22 | do { 23 | let fetchRequest = SyncMessageIDEntity.fetchRequest() 24 | fetchRequest.predicate = NSPredicate(format: "id == %@", id.uuidString) 25 | guard let entity = try moc.fetch(fetchRequest).first else { 26 | return nil 27 | } 28 | 29 | return entity 30 | 31 | } catch let error { 32 | print(error) 33 | throw error 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/HeadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeadingView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 3/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HeadingView: View { 11 | 12 | let size: HeadingSize 13 | let text: String 14 | let textDidChange: (_ text: String) -> Void 15 | 16 | enum HeadingSize: Int { 17 | case heading1 = 40 18 | case heading2 = 32 19 | case heading3 = 24 20 | case heading4 = 18 21 | } 22 | 23 | init(size: HeadingSize, text: String, textDidChange: @escaping (_: String) -> Void) { 24 | self.size = size 25 | self.text = text 26 | self.textDidChange = textDidChange 27 | self.content = text 28 | } 29 | 30 | @State private var content: String 31 | 32 | var body: some View { 33 | TextField("", text: $content, axis: .vertical) 34 | .textFieldStyle(.plain) 35 | .font(.system(size: CGFloat(size.rawValue))) 36 | .onAppear { 37 | content = text 38 | } 39 | .onChange(of: content) { newValue in 40 | textDidChange(newValue) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Shared/Repositories/LabelLinkRepo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelLinkRepo.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/28/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | import SwiftUI 11 | 12 | struct LabelLinkRepo { 13 | let user: User 14 | 15 | private let moc = DataController.shared.container.viewContext 16 | 17 | func readAll(document: Document) throws -> [LabelLink]? { 18 | do { 19 | let fetchRequest = LabelLinkEntity.fetchRequest() 20 | fetchRequest.predicate = NSPredicate(format: "document == %@", document.id.uuidString) 21 | let labelLinkEntities = try moc.fetch(fetchRequest) 22 | return labelLinkEntities.map { labelLinkEntity in 23 | return LabelLink( 24 | id: labelLinkEntity.id, 25 | label: labelLinkEntity.label, 26 | document: labelLinkEntity.document, 27 | workspace: labelLinkEntity.workspace, 28 | createdAt: labelLinkEntity.createdAt, 29 | modifiedAt: labelLinkEntity.modifiedAt 30 | ) 31 | } 32 | } catch let error { 33 | print(error) 34 | throw error 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/PromptView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PromptView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 5/7/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PromptView: View { 11 | let id: UUID 12 | let type: BlockType 13 | let block: Block 14 | let selected: Bool 15 | 16 | var font: Font { 17 | switch type { 18 | default: 19 | return .custom("", size: PromptFontDimensions.bodyFontSize, relativeTo: .body) 20 | } 21 | } 22 | 23 | var body: some View { 24 | Text(block.content) 25 | .font(font) 26 | .lineSpacing(Spacing.spacing2.rawValue) 27 | .disableAutocorrection(true) 28 | .textFieldStyle(.plain) 29 | .multilineTextAlignment(.leading) 30 | .padding([.top, .bottom], Spacing.spacing2.rawValue) 31 | .overlay { 32 | if selected { 33 | ZStack { 34 | RoundedRectangle(cornerRadius: 8) 35 | .stroke(LabelPalette.primary.getColor()) 36 | RoundedRectangle(cornerRadius: 8) 37 | .fill(LabelPalette.primary.getColor().opacity(0.125)) 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Shared/DB Model/SyncMessageEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SynMessageEntity.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/6/23. 6 | // 7 | 8 | import CoreData 9 | 10 | public class SyncMessageEntity: NSManagedObject { 11 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 12 | return NSFetchRequest(entityName: "SyncMessageEntity") 13 | } 14 | 15 | @NSManaged public var id: UUID 16 | @NSManaged public var user: String 17 | @NSManaged public var type: String 18 | @NSManaged public var action: String 19 | @NSManaged public var timestamp: Date 20 | @NSManaged public var contents: Data 21 | @NSManaged public var isSynced: Bool 22 | @NSManaged public var isApplied: Bool 23 | } 24 | 25 | extension SyncMessageEntity { 26 | static public func getEntity(id: UUID, moc: NSManagedObjectContext) throws -> SyncMessageEntity? { 27 | do { 28 | let fetchRequest = SyncMessageEntity.fetchRequest() 29 | fetchRequest.predicate = NSPredicate(format: "id == %@", id.uuidString) 30 | guard let entity = try moc.fetch(fetchRequest).first else { 31 | return nil 32 | } 33 | 34 | return entity 35 | 36 | } catch let error { 37 | print(error) 38 | throw error 39 | } 40 | } 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /Shared/DB Model/UserEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserEntity.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import CoreData 9 | 10 | public class UserEntity: NSManagedObject { 11 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 12 | return NSFetchRequest(entityName: "UserEntity") 13 | } 14 | 15 | @NSManaged public var id: String 16 | @NSManaged public var email: String 17 | @NSManaged public var givenName: String? 18 | @NSManaged public var familyName: String? 19 | @NSManaged public var createdAt: Date 20 | @NSManaged public var modifiedAt: Date 21 | } 22 | 23 | extension UserEntity : Comparable { 24 | public static func < (lhs: UserEntity, rhs: UserEntity) -> Bool { 25 | lhs.createdAt < rhs.createdAt 26 | } 27 | 28 | static public func getEntity(id: String, moc: NSManagedObjectContext) throws -> UserEntity? { 29 | do { 30 | let fetchRequest = UserEntity.fetchRequest() 31 | fetchRequest.predicate = NSPredicate(format: "id == %@", id) 32 | guard let entity = try moc.fetch(fetchRequest).first else { 33 | return nil 34 | } 35 | 36 | return entity 37 | 38 | } catch let error { 39 | print(error) 40 | throw error 41 | } 42 | } 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /Shared/HorizontalFlexView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalFlexView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/30/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HorizontalFlexView: View { 11 | let content: () -> any View 12 | 13 | var body: some View { 14 | HStack { 15 | Spacer() 16 | #if os(macOS) 17 | AnyView(content()) 18 | .frame(minWidth: GlobalDimension.minDocumentContentWidth) 19 | .frame(maxWidth: GlobalDimension.maxDocumentContentWidth) 20 | #else 21 | AnyView(content()) 22 | .frame(maxWidth: GlobalDimension.maxDocumentContentWidth) 23 | #endif 24 | Spacer() 25 | } 26 | .padding([.top, .bottom]) 27 | } 28 | } 29 | 30 | struct HorizontalFlexPreviewView: View { 31 | let content: () -> any View 32 | 33 | var body: some View { 34 | HStack { 35 | Spacer() 36 | #if os(macOS) 37 | AnyView(content()) 38 | .frame(maxWidth: .infinity) 39 | // .frame(minWidth: GlobalDimension.minDocumentPreviewContentWidth) 40 | // .frame(maxWidth: GlobalDimensio/n.maxDocumentPreviewContentWidth) 41 | #else 42 | AnyView(content()) 43 | #endif 44 | Spacer() 45 | } 46 | .padding([.top, .bottom]) 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Shared/Model/Label.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Label.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct Label: Equatable, Hashable, Codable { 12 | static public func == (lhs: Label, rhs: Label) -> Bool { 13 | return lhs.id == rhs.id || lhs.title == rhs.title 14 | } 15 | 16 | let id: UUID 17 | let title: String 18 | let color: LabelPalette 19 | let workspace: UUID 20 | let user: String 21 | let createdAt: Date 22 | let modifiedAt: Date 23 | 24 | func hash(into hasher: inout Hasher) { 25 | hasher.combine(id) 26 | hasher.combine(workspace) 27 | hasher.combine(title) 28 | } 29 | 30 | init(id: UUID, title: String, color: LabelPalette, workspace: UUID, user: String, createdAt: Date, modifiedAt: Date) { 31 | self.id = id 32 | self.title = title 33 | self.color = color 34 | self.workspace = workspace 35 | self.user = user 36 | self.createdAt = createdAt 37 | self.modifiedAt = modifiedAt 38 | } 39 | 40 | init(from: LabelEntity) throws { 41 | self.id = from.id 42 | self.title = from.title 43 | self.color = LabelPalette(rawValue: from.color)! 44 | self.workspace = from.workspace!.id 45 | self.user = from.user!.id 46 | self.createdAt = from.createdAt 47 | self.modifiedAt = from.modifiedAt 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Shared/DB Model/LabelLinkEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelLinkEntity.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | public class LabelLinkEntity: NSManagedObject { 12 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 13 | return NSFetchRequest(entityName: "LabelLinkEntity") 14 | } 15 | 16 | @NSManaged public var id: UUID 17 | @NSManaged public var document: UUID 18 | @NSManaged public var workspace: UUID 19 | @NSManaged public var label: UUID 20 | @NSManaged public var createdAt: Date 21 | @NSManaged public var modifiedAt: Date 22 | } 23 | 24 | extension LabelLinkEntity : Comparable { 25 | public static func < (lhs: LabelLinkEntity, rhs: LabelLinkEntity) -> Bool { 26 | lhs.createdAt < rhs.createdAt 27 | } 28 | 29 | static public func getEntity(id: UUID, moc: NSManagedObjectContext) throws -> LabelLinkEntity? { 30 | do { 31 | let fetchRequest = LabelLinkEntity.fetchRequest() 32 | fetchRequest.predicate = NSPredicate(format: "id == %@", id.uuidString) 33 | guard let entity = try moc.fetch(fetchRequest).first else { 34 | return nil 35 | } 36 | 37 | return entity 38 | 39 | } catch let error { 40 | print(error) 41 | throw error 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Icon](https://raw.githubusercontent.com/graphnote-io/graphnote/master/Resources/graphnote_icon_128.png) 3 | 4 | # Graphnote 5 | 6 | "Augment your memory" 7 | 8 | Graphnote Client for iOS and macOS 9 | 10 | ![Screenshot](https://raw.githubusercontent.com/graphnote-io/graphnote/master/Resources/graphnote_screenshot_dark.png) 11 | 12 | ![Screenshot](https://raw.githubusercontent.com/graphnote-io/graphnote/master/Resources/graphnote_screenshot_light.png) 13 | 14 | ### Sign up for the beta! 15 | [Beta signup on website](https://graphnote.app) 16 | 17 | ### Roadmap 🗺️ 18 | 19 | - [x] Workspaces 20 | - [x] Documents 21 | - [x] Blocks 22 | - [x] Text 23 | - [ ] Image 24 | - [ ] Table 25 | - [ ] Todo 26 | - [ ] Bullet Point 27 | - [x] Content link 28 | - [ ] Basic link 29 | - [x] Offline Support 30 | - [ ] Undo / Redo 31 | - [ ] Search / Filter 32 | - [ ] Apple Pencil Support 33 | - [ ] Handwritten to print conversion 34 | - [ ] Vector sketches 35 | 36 | ### Engineering 37 | #### App startup flow 38 | ![AppStartup](https://raw.githubusercontent.com/graphnote-io/graphnote/master/Docs/Diagrams/AppStartup/AppStartupDiagram.drawio.png) 39 | 40 | #### Push queue flow 41 | ![PushQueue](https://raw.githubusercontent.com/graphnote-io/graphnote/master/Docs/Diagrams/PushQueue/PushQueue.drawio.png) 42 | 43 | #### Pull queue flow 44 | ![PullQueue](https://raw.githubusercontent.com/graphnote-io/graphnote/master/Docs/Diagrams/PullQueue/PullQueue.drawio.png) 45 | 46 | #### Apply queue flow (coming soon) -------------------------------------------------------------------------------- /Shared/DB Model/WorkspaceEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkspaceEntity.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 2/13/22. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | public class WorkspaceEntity: NSManagedObject { 12 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 13 | return NSFetchRequest(entityName: "WorkspaceEntity") 14 | } 15 | 16 | @NSManaged public var id: UUID 17 | @NSManaged public var title: String 18 | @NSManaged public var documents: NSSet 19 | @NSManaged public var createdAt: Date 20 | @NSManaged public var modifiedAt: Date 21 | @NSManaged public var user: UserEntity? 22 | @NSManaged public var labels: NSSet 23 | } 24 | 25 | extension WorkspaceEntity : Comparable { 26 | public static func < (lhs: WorkspaceEntity, rhs: WorkspaceEntity) -> Bool { 27 | lhs.createdAt < rhs.createdAt 28 | } 29 | 30 | static public func getEntity(id: UUID, moc: NSManagedObjectContext) throws -> WorkspaceEntity? { 31 | do { 32 | let fetchRequest = WorkspaceEntity.fetchRequest() 33 | fetchRequest.predicate = NSPredicate(format: "id == %@", id.uuidString) 34 | guard let entity = try moc.fetch(fetchRequest).first else { 35 | return nil 36 | } 37 | 38 | return entity 39 | 40 | } catch let error { 41 | print(error) 42 | throw error 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Shared/Components/Labels/LabelColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelColor.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/20/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // primary: 5E81FF 12 | // purple: #8B5EFF 13 | // red: #FF5E81 14 | // lightPurple: #FF5ED2 15 | // pink: #FF7581 16 | // limeGreen: #5EFF8B 17 | // darkOrange: #FF9881 18 | // lightOrange: FFC962 19 | 20 | struct LabelColor { 21 | static let primary = Color(red: 94.0 / 255.0, green: 129.0 / 255.0, blue: 255.0 / 255.0) 22 | static let purple = Color(red: 139.0 / 255.0, green: 94.0 / 255.0, blue: 255.0 / 255.0) 23 | static let red = Color(red: 255.0 / 255.0, green: 94.0 / 255.0, blue: 129.0 / 255.0) 24 | static let lightPurple = Color(red: 255.0 / 255.0, green: 94.0 / 255.0, blue: 210.0 / 255.0) 25 | static let pink = Color(red: 255.0 / 255.0, green: 117.0 / 255.0, blue: 179.0 / 255.0) 26 | static let limeGreen = Color(red: 94.0 / 255.0, green: 255.0 / 255.0, blue: 139.0 / 255.0) 27 | static let darkOrange = Color(red: 255.0 / 255.0, green: 152.0 / 255.0, blue: 129.0 / 255.0) 28 | static let lightOrange = Color(red: 255.0 / 255.0, green: 201.0 / 255.0, blue: 98.0 / 255.0) 29 | 30 | static let allColors: [SwiftUI.Color] = [ 31 | LabelColor.primary, 32 | LabelColor.purple, 33 | LabelColor.red, 34 | LabelColor.lightPurple, 35 | LabelColor.pink, 36 | LabelColor.limeGreen, 37 | LabelColor.darkOrange, 38 | LabelColor.lightOrange, 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /Shared/DB Model/DocumentEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentEntity.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 2/13/22. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | public class DocumentEntity: NSManagedObject { 12 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 13 | return NSFetchRequest(entityName: "DocumentEntity") 14 | } 15 | 16 | @NSManaged public var id: UUID 17 | @NSManaged public var title: String 18 | @NSManaged public var focused: UUID? 19 | @NSManaged public var workspace: WorkspaceEntity? 20 | @NSManaged public var createdAt: Date 21 | @NSManaged public var modifiedAt: Date 22 | @NSManaged public var user: UserEntity? 23 | } 24 | 25 | extension DocumentEntity : Comparable { 26 | public static func < (lhs: DocumentEntity, rhs: DocumentEntity) -> Bool { 27 | lhs.createdAt < rhs.createdAt 28 | } 29 | 30 | static public func getEntity(id: UUID, moc: NSManagedObjectContext) throws -> DocumentEntity? { 31 | do { 32 | let fetchRequest = DocumentEntity.fetchRequest() 33 | fetchRequest.predicate = NSPredicate(format: "id == %@", id.uuidString) 34 | guard let entity = try moc.fetch(fetchRequest).first else { 35 | return nil 36 | } 37 | 38 | return entity 39 | 40 | } catch let error { 41 | print(error) 42 | throw error 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Shared/DB Model/BlockEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockEntity.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 3/11/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | public class BlockEntity: NSManagedObject { 12 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 13 | return NSFetchRequest(entityName: "BlockEntity") 14 | } 15 | 16 | @NSManaged public var id: UUID 17 | @NSManaged public var type: String 18 | @NSManaged public var content: String 19 | @NSManaged public var prev: UUID? 20 | @NSManaged public var next: UUID? 21 | @NSManaged public var graveyard: Bool 22 | @NSManaged public var createdAt: Date 23 | @NSManaged public var modifiedAt: Date 24 | @NSManaged public var document: DocumentEntity? 25 | } 26 | 27 | extension BlockEntity : Comparable { 28 | public static func < (lhs: BlockEntity, rhs: BlockEntity) -> Bool { 29 | lhs.createdAt < rhs.createdAt 30 | } 31 | 32 | static public func getEntity(id: UUID, moc: NSManagedObjectContext) throws -> BlockEntity? { 33 | do { 34 | let fetchRequest = BlockEntity.fetchRequest() 35 | fetchRequest.predicate = NSPredicate(format: "id == %@", id.uuidString) 36 | guard let entity = try moc.fetch(fetchRequest).first else { 37 | return nil 38 | } 39 | 40 | return entity 41 | 42 | } catch let error { 43 | print(error) 44 | throw error 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/TreeViewLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeViewLine.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TreeViewLine: View { 11 | let id: UUID 12 | let color: Color 13 | let toggle: Bool 14 | let label: String 15 | let isAll: Bool 16 | 17 | init(id: UUID, color: Color, toggle: Bool, label: String, isAll: Bool = false) { 18 | self.id = id 19 | self.color = color 20 | self.toggle = toggle 21 | self.label = label 22 | self.isAll = isAll 23 | } 24 | 25 | var body: some View { 26 | HStack { 27 | ArrowView(color: color, down: toggle) 28 | .frame(width: TreeViewLabelDimensions.arrowWidthHeight.rawValue, height: TreeViewLabelDimensions.arrowWidthHeight.rawValue) 29 | if isAll { 30 | TreeDocView() 31 | .foregroundColor(color) 32 | .padding(TreeViewLabelDimensions.rowPadding.rawValue) 33 | } else { 34 | TreeTagView() 35 | .foregroundColor(color) 36 | .padding(TreeViewLabelDimensions.rowPadding.rawValue) 37 | } 38 | 39 | Text(label) 40 | .bold() 41 | .padding(TreeViewLabelDimensions.rowPadding.rawValue) 42 | } 43 | .contentShape(Rectangle()) 44 | } 45 | } 46 | 47 | struct TreeViewLine_Previews: PreviewProvider { 48 | static var previews: some View { 49 | TreeViewLine(id: UUID(), color: .red, toggle: false, label: "Preview") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Shared/LinkedListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkedListView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/28/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NodeView: View, Hashable { 11 | let id: UUID 12 | let type: String 13 | let graveyard: Bool 14 | let content: String 15 | let prev: UUID? 16 | let next: UUID? 17 | 18 | var body: some View { 19 | VStack(alignment: .leading) { 20 | Text("prev: \(prev?.uuidString ?? "")") 21 | Text(type) 22 | Text("id: \(id.uuidString)") 23 | Text(content) 24 | Text("next: \(next?.uuidString ?? "")") 25 | } 26 | .frame(width: 300, alignment: .leading) 27 | .padding() 28 | .background(graveyard ? Color.gray : content == "SENTINEL BLOCK" ? Color.green : Color.accentColor) 29 | .cornerRadius(Spacing.spacing3.rawValue) 30 | } 31 | } 32 | 33 | struct LinkedListView: View { 34 | let nodes: [NodeView] 35 | 36 | var body: some View { 37 | ScrollView(.vertical) { 38 | VStack { 39 | ForEach(nodes, id: \.self) { node in 40 | node 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | struct LinkedListView_Previews: PreviewProvider { 48 | static var previews: some View { 49 | LinkedListView(nodes: [ 50 | NodeView(id: UUID(), type: "prompt", graveyard: false, content: "Testing prompt", prev: nil, next: nil), 51 | NodeView(id: UUID(), type: "body", graveyard: true, content: "Testing body", prev: nil, next: nil) 52 | ]) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Shared/Model/Document.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Document.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum DocumentError: Error { 11 | case noWorkspace 12 | } 13 | 14 | public struct Document: Equatable, Codable, Hashable { 15 | public let id: UUID 16 | public let title: String 17 | public let focused: UUID? 18 | public let createdAt: Date 19 | public let modifiedAt: Date 20 | public let workspace: UUID 21 | 22 | public func hash(into hasher: inout Hasher) { 23 | hasher.combine(id) 24 | hasher.combine(title) 25 | hasher.combine(workspace) 26 | } 27 | 28 | public init(id: UUID, title: String, focused: UUID?, createdAt: Date, modifiedAt: Date, workspace: UUID) { 29 | self.id = id 30 | self.title = title 31 | self.focused = focused 32 | self.createdAt = createdAt 33 | self.modifiedAt = modifiedAt 34 | self.workspace = workspace 35 | } 36 | 37 | public init(from: DocumentEntity) throws { 38 | if let workspace = from.workspace { 39 | self.id = from.id 40 | self.title = from.title 41 | self.focused = from.focused 42 | self.createdAt = from.createdAt 43 | self.modifiedAt = from.modifiedAt 44 | self.workspace = workspace.id 45 | } else { 46 | throw DocumentError.noWorkspace 47 | } 48 | } 49 | } 50 | 51 | public struct DocumentUpdate: Codable { 52 | public let id: UUID 53 | public let workspace: UUID 54 | public let content: Dictionary 55 | } 56 | -------------------------------------------------------------------------------- /Shared/Containers/BlockViewContainerVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockViewContainerVM.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/17/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class BlockViewContainerVM: ObservableObject { 11 | func insertBlock(user: User, workspace: Workspace, document: Document, promptText: String, prev: UUID?, next: UUID?) -> Block? { 12 | do { 13 | let now = Date.now 14 | 15 | let type: BlockType = .body 16 | let block = Block(id: UUID(), type: type, content: promptText, prev: prev, next: next, graveyard: false, createdAt: now, modifiedAt: now, document: document) 17 | 18 | return try DataService.shared.createBlock(user: user, workspace: workspace, document: document, block: block, prev: prev, next: next) 19 | } catch let error { 20 | print(error) 21 | #if DEBUG 22 | fatalError() 23 | #endif 24 | return nil 25 | } 26 | } 27 | 28 | func updateBlock(_ block: Block, user: User, workspace: Workspace, document: Document, prev: UUID? = nil, next: UUID? = nil, type: BlockType? = nil, text: String? = nil) { 29 | let next = next ?? block.next 30 | let prev = prev ?? block.prev 31 | let type = type ?? block.type 32 | let text = text ?? block.content 33 | let updatedBlock = Block(id: block.id, type: type, content: text, prev: prev, next: next, graveyard: block.graveyard, createdAt: block.createdAt, modifiedAt: .now, document: document) 34 | DataService.shared.updateBlock(user: user, workspace: workspace, document: document, block: updatedBlock) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/TreeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TreeView: View { 11 | @Binding var selectedSubItem: TreeDocumentIdentifier? 12 | let allID: UUID 13 | var items: [TreeViewItem] 14 | let refresh: () -> Void 15 | 16 | var body: some View { 17 | ScrollView { 18 | HStack(spacing: Spacing.spacing0.rawValue) { 19 | VStack(alignment: .leading) { 20 | ForEach(items, id: \.id) { item in 21 | TreeViewLabel(id: UUID(), label: .constant(item.title), color: item.color, isAll: allID == item.id) { 22 | if let subItems = item.subItems { 23 | return ForEach(subItems, id: \.id) { subItem in 24 | TreeViewSubline(title: subItem.title, selected: selectedSubItem?.document == subItem.id && selectedSubItem?.label == item.id) 25 | .onTapGesture { 26 | selectedSubItem = TreeDocumentIdentifier(label: item.id, document: subItem.id, workspace: item.workspace) 27 | } 28 | } 29 | } else { 30 | return EmptyView() 31 | } 32 | } 33 | } 34 | } 35 | Spacer() 36 | } 37 | } 38 | .padding([.top, .bottom]) 39 | .refreshable { 40 | refresh() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Shared/Components/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 2/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LoadingView: View { 11 | @State private var opacity = 0.2 12 | @State private var rotationAngle = 0.0 13 | 14 | var body: some View { 15 | GeometryReader { geometry in 16 | HStack { 17 | VStack { 18 | LoadingSpinnerView() 19 | .rotationEffect(Angle(radians: rotationAngle)) 20 | .opacity(opacity) 21 | Text("Loading...") 22 | .font(.largeTitle) 23 | .opacity(opacity) 24 | .padding() 25 | .task { 26 | withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { 27 | opacity += 0.8 28 | } 29 | } 30 | 31 | 32 | } 33 | }.frame(width: geometry.size.width, height: geometry.size.height) 34 | .onAppear { 35 | withAnimation(.linear(duration: 1)) { 36 | rotationAngle += 2 37 | } 38 | Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in 39 | withAnimation(.linear(duration: 1)) { 40 | rotationAngle += 2 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | struct LoadingView_Previews: PreviewProvider { 49 | static var previews: some View { 50 | LoadingView() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Shared/Components/TreeView/TreeViewLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeViewLabel.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 1/22/22. 6 | // 7 | 8 | import SwiftUI 9 | import CoreData 10 | import Combine 11 | 12 | enum TreeViewLabelDimensions: CGFloat { 13 | case arrowWidthHeight = 16 14 | case rowPadding = 4 15 | } 16 | 17 | enum FocusField: Hashable { 18 | case field 19 | } 20 | 21 | struct TreeViewLabel: View, Identifiable { 22 | @Environment(\.colorScheme) private var colorScheme 23 | 24 | @State private var toggle = false 25 | @State private var editable = false 26 | @FocusState private var focusedField: FocusField? 27 | 28 | let id: UUID 29 | @Binding var label: String 30 | let color: Color 31 | let isAll: Bool 32 | let content: () -> any View 33 | 34 | var body: some View { 35 | VStack(alignment: .leading) { 36 | TreeViewLine(id: UUID(), color: color, toggle: toggle, label: label, isAll: isAll) 37 | .padding(TreeViewLabelDimensions.rowPadding.rawValue) 38 | .frame(minWidth: 200, alignment: .leading) 39 | .contentShape(RoundedRectangle(cornerRadius: Spacing.spacing2.rawValue)) 40 | // .contextMenu { 41 | // Button { 42 | // editable = true 43 | // } label: { 44 | // Text("Rename label") 45 | // } 46 | // } 47 | .onTapGesture { 48 | toggle.toggle() 49 | } 50 | 51 | if toggle { 52 | VStack(alignment: .leading) { 53 | AnyView(content()) 54 | } 55 | .padding([.leading], 40) 56 | } 57 | } 58 | 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Shared/Services/LabelService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelService.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/3/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LabelService { 11 | let user: User 12 | let workspace: Workspace 13 | 14 | func addLabel(title: String, color: LabelPalette, document: Document) throws -> Bool { 15 | let labelRepo = LabelRepo(user: user, workspace: workspace) 16 | let documentRepo = DocumentRepo(user: user, workspace: workspace) 17 | 18 | var label = Label(id: UUID(), title: title, color: color, workspace: workspace.id, user: user.id, createdAt: Date.now, modifiedAt: Date.now) 19 | 20 | let existingLabelUUID = try? labelRepo.exists(label: label) 21 | 22 | if let existingLabelUUID { 23 | do { 24 | if let existingLabel = try labelRepo.read(id: existingLabelUUID) { 25 | label = existingLabel 26 | } 27 | } catch let error { 28 | print(error) 29 | throw error 30 | } 31 | 32 | let labelAttachmentExists = try documentRepo.attachExists(label: label, document: document) 33 | if labelAttachmentExists { 34 | print("Attachment exists and label exists") 35 | return false 36 | } else { 37 | try DataService.shared.attachLabel(user: user, label: label, document: document, workspace: workspace) 38 | return true 39 | } 40 | 41 | } else { 42 | try DataService.shared.createLabel(user: user, label: label, workspace: workspace) 43 | try DataService.shared.attachLabel(user: user, label: label, document: document, workspace: workspace) 44 | return true 45 | } 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Shared/Model/Block.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Block.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum BlockError: Error { 11 | case documentParseFailed 12 | case noDocument 13 | } 14 | 15 | struct Block: Codable, Hashable { 16 | let id: UUID 17 | let type: BlockType 18 | let content: String 19 | let prev: UUID? 20 | let next: UUID? 21 | let graveyard: Bool 22 | let createdAt: Date 23 | let modifiedAt: Date 24 | let document: Document 25 | 26 | init(id: UUID, type: BlockType, content: String, prev: UUID?, next: UUID?, graveyard: Bool, createdAt: Date, modifiedAt: Date, document: Document) { 27 | self.id = id 28 | self.type = type 29 | self.content = content 30 | self.prev = prev 31 | self.next = next 32 | self.graveyard = graveyard 33 | self.createdAt = createdAt 34 | self.modifiedAt = modifiedAt 35 | self.document = document 36 | } 37 | 38 | init(from: BlockEntity) throws { 39 | do { 40 | if let document = from.document { 41 | self.id = from.id 42 | self.type = BlockType(rawValue: from.type)! 43 | self.content = from.content 44 | self.prev = from.prev 45 | self.next = from.next 46 | self.graveyard = from.graveyard 47 | self.createdAt = from.createdAt 48 | self.modifiedAt = from.modifiedAt 49 | self.document = try Document(from: document) 50 | } else { 51 | throw BlockError.noDocument 52 | } 53 | } catch let error { 54 | #if DEBUG 55 | fatalError(error.localizedDescription) 56 | #endif 57 | throw BlockError.documentParseFailed 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Shared/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsView: View { 11 | let user: User 12 | 13 | var content: some View { 14 | VStack(alignment: .leading) { 15 | Spacer() 16 | .frame(height: Spacing.spacing4.rawValue) 17 | HStack { 18 | Text("Account") 19 | .font(.title) 20 | .bold() 21 | Spacer() 22 | 23 | } 24 | Button { 25 | let now = Date.now 26 | let workspace = Workspace( 27 | id: UUID(), 28 | title: "New workspace", 29 | createdAt: now, 30 | modifiedAt: now, 31 | user: user.id, 32 | labels: [], 33 | documents: [] 34 | ) 35 | do { 36 | try DataService.shared.createWorkspace(user: user, workspace: workspace) 37 | } catch let error { 38 | print(error) 39 | } 40 | 41 | } label: { 42 | HStack { 43 | Image(systemName: "plus") 44 | Text("New Workspace") 45 | } 46 | .frame(height: Spacing.spacing7.rawValue) 47 | } 48 | .buttonStyle(.plain) 49 | .padding(Spacing.spacing3.rawValue) 50 | Spacer() 51 | } 52 | } 53 | 54 | var body: some View { 55 | GeometryReader { geometry in 56 | HorizontalFlexView { 57 | content 58 | } 59 | .frame(minHeight: geometry.size.height) 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/ContentLinkModalVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentLinkModalVM.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/23/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class ContentLinkModalVM: ObservableObject { 11 | @Published var blocks: [Block] = [] 12 | @Published var labels: [Label] = [] 13 | @Published var title = "" 14 | 15 | // TODO: - Investigate this? 16 | private func blockSort(_ a: Block, _ b: Block) -> Bool { 17 | return true 18 | } 19 | 20 | func fetchDocument(user: User, workspace: Workspace, document: Document) { 21 | let repo = DocumentRepo(user: user, workspace: workspace) 22 | do { 23 | if let blocks = try repo.readBlocks(document: document) { 24 | self.blocks = blocks.sorted(by: { a, b in 25 | blockSort(a, b) 26 | }) 27 | } 28 | 29 | if let labels = repo.readLabels(document: document) { 30 | self.labels = labels 31 | } 32 | 33 | self.title = document.title 34 | 35 | } catch let error { 36 | print(error) 37 | } 38 | } 39 | 40 | func createLink(user: User, workspace: Workspace, document: Document, content: UUID, prev: UUID?, next: UUID?) { 41 | let now = Date.now 42 | // - TODO: BLOCK PREV NEXT 43 | let link = Block(id: UUID(), type: .contentLink, content: content.uuidString, prev: prev, next: next, graveyard: false, createdAt: now, modifiedAt: now, document: document) 44 | do { 45 | _ = try DataService.shared.createBlock(user: user, workspace: workspace, document: document, block: link, prev: prev, next: next) 46 | } catch let error { 47 | #if DEBUG 48 | fatalError() 49 | #else 50 | print(error) 51 | #endif 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Shared/DB Model/DataController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataController.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 1/25/22. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | class DataController: ObservableObject { 12 | let container = NSPersistentContainer(name: "Graphnote") 13 | 14 | @Published private(set) var loaded = false 15 | 16 | static let shared = DataController() 17 | 18 | private init() { 19 | // container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy 20 | 21 | self.load() 22 | } 23 | 24 | private func load() { 25 | container.loadPersistentStores { description, error in 26 | if let error = error { 27 | print("Core Data failed to load: \(error.localizedDescription)") 28 | self.loaded = false 29 | } 30 | 31 | self.loaded = true 32 | } 33 | } 34 | 35 | func dropDatabase() { 36 | softCleanDatabase() 37 | } 38 | 39 | func dropTable(fetchRequest: NSFetchRequest) { 40 | let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) 41 | try! self.container.viewContext.execute(deleteRequest) 42 | try! self.container.viewContext.save() 43 | } 44 | 45 | func softCleanDatabase() { 46 | // dropTable(fetchRequest: UserEntity.fetchRequest()) 47 | dropTable(fetchRequest: WorkspaceEntity.fetchRequest()) 48 | dropTable(fetchRequest: DocumentEntity.fetchRequest()) 49 | dropTable(fetchRequest: BlockEntity.fetchRequest()) 50 | dropTable(fetchRequest: LabelEntity.fetchRequest()) 51 | dropTable(fetchRequest: LabelLinkEntity.fetchRequest()) 52 | dropTable(fetchRequest: SyncMessageEntity.fetchRequest()) 53 | dropTable(fetchRequest: SyncMessageIDEntity.fetchRequest()) 54 | dropTable(fetchRequest: LastSyncTimeEntity.fetchRequest()) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /iOS/MobileUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MobileUtils.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 1/23/22. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | struct MobileUtils { 12 | static func resignKeyboard() { 13 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 14 | } 15 | 16 | /// Excellent way to find the orientation in SwiftUI from Apple forums https://developer.apple.com/forums/thread/126878 17 | final class OrientationInfo: ObservableObject { 18 | enum Orientation { 19 | case portrait 20 | case landscape 21 | } 22 | 23 | @Published var orientation: Orientation 24 | 25 | private var _observer: NSObjectProtocol? 26 | 27 | init() { 28 | // fairly arbitrary starting value for 'flat' orientations 29 | if UIDevice.current.orientation.isLandscape { 30 | self.orientation = .landscape 31 | } 32 | else { 33 | self.orientation = .portrait 34 | } 35 | 36 | // unowned self because we unregister before self becomes invalid 37 | _observer = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { [unowned self] note in 38 | guard let device = note.object as? UIDevice else { 39 | return 40 | } 41 | if device.orientation.isPortrait { 42 | self.orientation = .portrait 43 | } 44 | else if device.orientation.isLandscape { 45 | self.orientation = .landscape 46 | } 47 | } 48 | 49 | } 50 | 51 | deinit { 52 | if let observer = _observer { 53 | NotificationCenter.default.removeObserver(observer) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Shared/Services/UserService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserService.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/15/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class UserService { 11 | func fetchUser(id: String, callback: @escaping (_ user: User?, _ error: SyncServiceError?) -> Void) { 12 | var request = URLRequest(url: baseURL.appendingPathComponent("user") 13 | .appending(queryItems: [.init(name: "id", value: id)])) 14 | request.httpMethod = "GET" 15 | print("SyncService fetchUser fetching: \(id)") 16 | 17 | let task = URLSession.shared.dataTask(with: request) { data, response, error in 18 | if let error { 19 | print(error) 20 | callback(nil, SyncServiceError.postFailed) 21 | return 22 | } 23 | 24 | if let response = response as? HTTPURLResponse { 25 | print(response.statusCode) 26 | print(response) 27 | switch response.statusCode { 28 | case 200: 29 | if let data { 30 | let decoder = JSONDecoder() 31 | decoder.dateDecodingStrategy = .millisecondsSince1970 32 | 33 | do { 34 | let user = try decoder.decode(User.self, from: data) 35 | callback(user, nil) 36 | } catch let error { 37 | print(error) 38 | callback(nil, SyncServiceError.decoderFailed) 39 | } 40 | 41 | } 42 | case 404: 43 | callback(nil, SyncServiceError.userNotFound) 44 | default: 45 | print("Response failed with statusCode: \(response.statusCode)") 46 | callback(nil, SyncServiceError.unknown) 47 | } 48 | 49 | } 50 | } 51 | 52 | task.resume() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Shared/DB Model/LabelEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelEntity.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | class LabelEntity: NSManagedObject { 12 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 13 | return NSFetchRequest(entityName: "LabelEntity") 14 | } 15 | 16 | @NSManaged var id: UUID 17 | @NSManaged var title: String 18 | @NSManaged var color: String 19 | @NSManaged var workspace: WorkspaceEntity? 20 | @NSManaged var user: UserEntity? 21 | @NSManaged var createdAt: Date 22 | @NSManaged var modifiedAt: Date 23 | } 24 | 25 | extension LabelEntity : Comparable { 26 | static func < (lhs: LabelEntity, rhs: LabelEntity) -> Bool { 27 | lhs.createdAt < rhs.createdAt 28 | } 29 | 30 | static func getEntity(id: UUID, moc: NSManagedObjectContext) throws -> LabelEntity? { 31 | do { 32 | let fetchRequest = LabelEntity.fetchRequest() 33 | fetchRequest.predicate = NSPredicate(format: "id == %@", id.uuidString) 34 | guard let entity = try moc.fetch(fetchRequest).first else { 35 | return nil 36 | } 37 | 38 | return entity 39 | 40 | } catch let error { 41 | print(error) 42 | throw error 43 | } 44 | } 45 | 46 | static func getEntity(title: String, workspace: Workspace, moc: NSManagedObjectContext) throws -> LabelEntity? { 47 | do { 48 | let fetchRequest = LabelEntity.fetchRequest() 49 | fetchRequest.predicate = NSPredicate(format: "title == %@ && workspace.id == %@", title, workspace.id.uuidString) 50 | guard let entity = try moc.fetch(fetchRequest).first else { 51 | return nil 52 | } 53 | 54 | return entity 55 | 56 | } catch let error { 57 | print(error) 58 | throw error 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Shared/Components/Sidebar/SidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SidebarView: View { 11 | @Binding var items: [TreeViewItem] 12 | @Binding var settingsOpen: Bool 13 | let workspaceTitles: [String] 14 | @Binding var selectedWorkspaceTitleIndex: Int 15 | @Binding var selectedSubItem: TreeDocumentIdentifier? 16 | let allID: UUID 17 | let newDocument: () -> Void 18 | let refresh: () -> Void 19 | 20 | var body: some View { 21 | VStack(alignment: .leading) { 22 | Spacer() 23 | .frame(maxHeight: Spacing.spacing7.rawValue) 24 | VStack(alignment: .leading) { 25 | TreeView(selectedSubItem: $selectedSubItem, allID: allID, items: items) { 26 | refresh() 27 | } 28 | 29 | } 30 | .padding(Spacing.spacing3.rawValue) 31 | Spacer() 32 | Group { 33 | Button { 34 | newDocument() 35 | } label: { 36 | HStack { 37 | Image(systemName: "plus") 38 | Text("New Document") 39 | } 40 | .frame(height: Spacing.spacing7.rawValue) 41 | } 42 | .buttonStyle(.plain) 43 | .padding(Spacing.spacing3.rawValue) 44 | HStack { 45 | GearIconVIew { 46 | settingsOpen = true 47 | } 48 | .frame(height: Spacing.spacing7.rawValue) 49 | WorkspaceMenu(selectedIndex: $selectedWorkspaceTitleIndex, workspaceTitles: workspaceTitles) 50 | .id("\(selectedWorkspaceTitleIndex):\(workspaceTitles)".hashValue) 51 | .frame(height: Spacing.spacing7.rawValue) 52 | } 53 | .padding(Spacing.spacing3.rawValue) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Shared/Services/SyncService/SyncServiceDBApplyQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncServiceDBApplyQueue.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/7/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class SyncServiceDBApplyQueue { 11 | let user: User 12 | 13 | var queue = [SyncMessage]() 14 | 15 | private lazy var syncMessageRepo = { 16 | return SyncMessageRepo(user: user) 17 | }() 18 | 19 | init(user: User) { 20 | self.user = user 21 | self.queue = [] 22 | 23 | // Read from the database to the queue 24 | fetchQueue() 25 | } 26 | 27 | func add(message: SyncMessage) -> Bool { 28 | do { 29 | // try syncMessageRepo.create(id: message.id) 30 | // Add to runtime queue 31 | self.queue.append(message) 32 | return true 33 | } catch let error { 34 | print(error) 35 | return false 36 | } 37 | } 38 | 39 | func remove(id: UUID) -> Bool { 40 | // remove from DB 41 | do { 42 | try syncMessageRepo.updateToIsApplied(id: id) 43 | // Remove from runtime queue if successful 44 | self.queue = self.queue.filter { 45 | $0.id != id 46 | } 47 | return true 48 | } catch let error { 49 | print(error) 50 | return false 51 | } 52 | } 53 | 54 | func peek(offset: Int = 0) -> SyncMessage? { 55 | if count > offset { 56 | return self.queue[offset] 57 | } else { 58 | return nil 59 | } 60 | } 61 | 62 | func element() -> SyncMessage { 63 | return queue.first! 64 | } 65 | 66 | var count: Int { 67 | return self.queue.count 68 | } 69 | 70 | func has(message: SyncMessage) -> Bool { 71 | return queue.contains(where: { filterMessage in 72 | return filterMessage.id == message.id 73 | }) 74 | } 75 | 76 | func fetchQueue() { 77 | if let queue = try? syncMessageRepo.readAllWhere(isApplied: false) { 78 | self.queue = queue 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Shared/Services/SyncService/SyncServiceDBPullQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncServiceDBPullQueue.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/15/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class SyncServiceDBPullQueue { 11 | let user: User 12 | 13 | private var queue = [UUID]() 14 | 15 | private lazy var syncMessageRepo = { 16 | return SyncMessageRepo(user: user) 17 | }() 18 | 19 | init(user: User) { 20 | self.user = user 21 | self.queue = [] 22 | 23 | // Read from the database to the queue 24 | fetchQueue() 25 | } 26 | 27 | func add(id: UUID) -> Bool { 28 | // Add to DB 29 | do { 30 | try syncMessageRepo.create(id: id) 31 | // Add to runtime queue if successful 32 | self.queue.append(id) 33 | return true 34 | } catch let error { 35 | print(error) 36 | return false 37 | } 38 | } 39 | 40 | func remove(id: UUID) -> Bool { 41 | // remove from DB 42 | do { 43 | try syncMessageRepo.setSyncedOnMessageID(id: id) 44 | // Remove from runtime queue if successful 45 | self.queue = self.queue.filter { 46 | $0 != id 47 | } 48 | return true 49 | } catch let error { 50 | print(error) 51 | return false 52 | } 53 | } 54 | 55 | func peek(offset: Int = 0) -> UUID? { 56 | if queue.count > offset { 57 | return queue[offset] 58 | } else { 59 | return nil 60 | } 61 | } 62 | 63 | func element() -> UUID { 64 | return queue.first! 65 | } 66 | 67 | var count: Int { 68 | return self.queue.count 69 | } 70 | 71 | func has(id: UUID) -> Bool { 72 | return queue.contains(where: { filteredId in 73 | return filteredId == id 74 | }) 75 | } 76 | 77 | func fetchQueue() { 78 | if let queue = syncMessageRepo.readAllIDsNotSynced() { 79 | self.queue = queue 80 | } else { 81 | print("No queue") 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/DocumentViewVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentViewVM.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/17/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class DocumentViewVM: ObservableObject { 11 | private func getLastIndex(user: User, workspace: Workspace, document: Document) -> Block? { 12 | return DataService.shared.getLastBlock(user: user, workspace: workspace, document: document) 13 | } 14 | 15 | @Published var documents: [Document] = [] 16 | 17 | func fetchDocuments(user: User, workspace: Workspace) { 18 | do { 19 | let repo = DocumentRepo(user: user, workspace: workspace) 20 | let docs = try repo.readAll() 21 | documents = docs 22 | } catch let error { 23 | print(error) 24 | } 25 | } 26 | 27 | func createLink(user: User, workspace: Workspace, document: Document, content: UUID, prev: UUID?, next: UUID?) { 28 | let now = Date.now 29 | // - TODO: BLOCK PREV NEXT 30 | let link = Block(id: UUID(), type: .contentLink, content: content.uuidString, prev: prev, next: next, graveyard: false, createdAt: now, modifiedAt: now, document: document) 31 | do { 32 | _ = try DataService.shared.createBlock(user: user, workspace: workspace, document: document, block: link, prev: prev, next: next) 33 | } catch let error { 34 | #if DEBUG 35 | fatalError() 36 | #else 37 | print(error) 38 | #endif 39 | } 40 | } 41 | 42 | func updateBlock(_ block: Block, user: User, workspace: Workspace, document: Document, prev: UUID? = nil, next: UUID? = nil, type: BlockType? = nil, text: String? = nil) { 43 | let next = next ?? block.next 44 | let prev = prev ?? block.prev 45 | let type = type ?? block.type 46 | let text = text ?? block.content 47 | let updatedBlock = Block(id: block.id, type: type, content: text, prev: prev, next: next, graveyard: block.graveyard, createdAt: block.createdAt, modifiedAt: .now, document: document) 48 | DataService.shared.updateBlock(user: user, workspace: workspace, document: document, block: updatedBlock) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Shared/Containers/DocumentContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentContainer.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/29/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DocumentContainer: View { 11 | @Environment(\.colorScheme) private var colorScheme 12 | 13 | let user: User 14 | let workspace: Workspace 15 | let document: Document 16 | let onRefresh: () -> Void 17 | 18 | @StateObject private var vm = DocumentContainerVM() 19 | 20 | private let labelLinkCreatedNotification = Notification.Name(SyncServiceNotification.labelLinkCreated.rawValue) 21 | 22 | init(user: User, workspace: Workspace, document: Document, onRefresh: @escaping () -> Void) { 23 | self.user = user 24 | self.workspace = workspace 25 | self.document = document 26 | self.onRefresh = onRefresh 27 | } 28 | 29 | var body: some View { 30 | DocumentView(title: $vm.title, labels: vm.labels, allLabels: vm.allLabels, blocks: vm.blocks, user: user, workspace: workspace, document: document) { 31 | vm.fetch(user: user, workspace: workspace, document: document) 32 | } fetchBlocks: { 33 | vm.fetchBlocks(user: user, workspace: workspace, document: document) 34 | } onRefresh: { 35 | vm.fetch(user: user, workspace: workspace, document: document) 36 | } save: { title in 37 | vm.save(title: title, force: true) 38 | vm.fetchBlocks(user: user, workspace: workspace, document: document) 39 | } 40 | .id(document.id) 41 | .background(colorScheme == .dark ? ColorPalette.darkBG1 : ColorPalette.lightBG1) 42 | .onAppear { 43 | vm.fetch(user: user, workspace: workspace, document: document) 44 | } 45 | .onReceive(NotificationCenter.default.publisher(for: labelLinkCreatedNotification)) { notification in 46 | vm.fetch(user: user, workspace: workspace, document: document) 47 | } 48 | .onChange(of: document) { _ in 49 | vm.fetch(user: user, workspace: workspace, document: document) 50 | } 51 | .onDisappear { 52 | vm.save() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/ContentLinkModal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentLinkModal.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/23/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentLinkModal: View { 11 | @Environment(\.colorScheme) private var colorScheme 12 | 13 | let user: User 14 | let workspace: Workspace 15 | let document: Document 16 | let documents: [Document] 17 | @Binding var selectedLink: UUID? 18 | @Binding var open: Bool 19 | let createLink: () -> Void 20 | 21 | @State private var _document: Document? 22 | 23 | @StateObject private var vm = ContentLinkModalVM() 24 | 25 | var body: some View { 26 | VStack(alignment: .leading) { 27 | Menu(_document?.title ?? "") { 28 | ForEach(documents, id: \.id) { document in 29 | Button(document.title) { 30 | self._document = document 31 | vm.fetchDocument(user: user, workspace: workspace, document: document) 32 | } 33 | .buttonStyle(.borderless) 34 | } 35 | } 36 | .padding() 37 | 38 | List { 39 | DocumentPreviewView(title: vm.title, labels: vm.labels, blocks: vm.blocks, user: user, workspace: workspace, document: document, selectedLink: $selectedLink) 40 | .frame(maxWidth: .infinity) 41 | } 42 | .scrollContentBackground(.hidden) 43 | .background(colorScheme == .dark ? ColorPalette.darkBG1 : ColorPalette.lightBG1) 44 | 45 | HStack { 46 | Button("Cancel") { 47 | selectedLink = nil 48 | 49 | open = false 50 | } 51 | Spacer() 52 | Button("Create link") { 53 | if let selectedLink { 54 | // vm.createLink(user: user, workspace: workspace, document: document, content: selectedLink, prev: nil, next: nil) 55 | createLink() 56 | open = false 57 | } 58 | } 59 | .buttonStyle(.borderedProminent) 60 | .disabled(selectedLink == nil) 61 | } 62 | .padding() 63 | } 64 | .frame(width: GlobalDimension.modalWidth, height: GlobalDimension.modalHeight) 65 | .background(colorScheme == .dark ? ColorPalette.darkBG1 : ColorPalette.lightBG1) 66 | .cornerRadius(8) 67 | .shadow(radius: 20) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Shared/Components/AddLabelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddLabelView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/2/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AddLabelView: View { 11 | let user: User 12 | let workspace: Workspace 13 | let allLabels: [Label] 14 | let save: (_ labels: [Label]) -> Void 15 | let close: () -> Void 16 | 17 | @State private var isEmpty = true 18 | @State private var color = LabelPalette.allCases().randomElement()! 19 | @State private var title = "" 20 | 21 | @State private var labels: [Label] = [] 22 | 23 | var body: some View { 24 | VStack { 25 | #if os(macOS) 26 | VStack(alignment: .leading) { 27 | AddLabelField(user: user, workspace: workspace, labels: labels, allLabels: allLabels) { label in 28 | labels.append(label) 29 | } 30 | .padding([.top, .bottom]) 31 | .textFieldStyle(.roundedBorder) 32 | // .frame(width: 220) 33 | } 34 | .padding() 35 | .cornerRadius(24) 36 | .onChange(of: labels, perform: { newValue in 37 | if newValue.count == .zero { 38 | isEmpty = true 39 | } else { 40 | isEmpty = false 41 | } 42 | }) 43 | #else 44 | 45 | AddLabelField(user: user, workspace: workspace, labels: labels, allLabels: allLabels) { label in 46 | labels.append(label) 47 | } 48 | .padding([.top, .bottom]) 49 | .textFieldStyle(.roundedBorder) 50 | .padding() 51 | .cornerRadius(24) 52 | .onChange(of: labels, perform: { newValue in 53 | if newValue.count == .zero { 54 | isEmpty = true 55 | } else { 56 | isEmpty = false 57 | } 58 | }) 59 | #endif 60 | Spacer() 61 | HStack { 62 | Button("Close") { 63 | close() 64 | } 65 | .foregroundColor(.red) 66 | .buttonStyle(.borderless) 67 | .padding() 68 | Spacer() 69 | Button("Add") { 70 | save(labels) 71 | } 72 | .disabled(isEmpty) 73 | .foregroundColor(isEmpty ? Color.gray : Color.accentColor) 74 | .buttonStyle(.borderless) 75 | .padding() 76 | }.padding() 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Shared/Services/SyncService/SyncServiceDBPushQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncServiceDBPushQueue.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/7/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class SyncServiceDBPushQueue { 11 | let user: User 12 | 13 | private var queue = [SyncMessage]() 14 | 15 | private lazy var syncMessageRepo = { 16 | return SyncMessageRepo(user: user) 17 | }() 18 | 19 | init(user: User) { 20 | self.user = user 21 | self.queue = [] 22 | 23 | // Read from the database to the queue 24 | fetchQueue() 25 | } 26 | 27 | func add(message: SyncMessage) -> Bool { 28 | // Add to DB 29 | do { 30 | print("Adding: \(message.id)") 31 | print("Type: \(message.type)") 32 | print("Action: \(message.action)") 33 | try syncMessageRepo.create(message: message) 34 | 35 | // Add to runtime queue if successful 36 | if message.isApplied == false { 37 | self.queue.append(message) 38 | } 39 | return true 40 | } catch let error { 41 | print(error) 42 | #if DEBUG 43 | fatalError() 44 | #endif 45 | return false 46 | } 47 | } 48 | 49 | func remove(id: UUID) -> Bool { 50 | // remove from DB 51 | do { 52 | try syncMessageRepo.updateToIsSynced(id: id) 53 | // Remove from runtime queue if successful 54 | self.queue = self.queue.filter { 55 | $0.id != id 56 | } 57 | return true 58 | } catch let error { 59 | print(error) 60 | return false 61 | } 62 | } 63 | 64 | func peek(offset: Int = 0) -> SyncMessage? { 65 | if count > offset { 66 | return self.queue[offset] 67 | } else { 68 | return nil 69 | } 70 | } 71 | 72 | func element() -> SyncMessage { 73 | return queue.first! 74 | } 75 | 76 | var count: Int { 77 | return self.queue.count 78 | } 79 | 80 | func has(message: SyncMessage) -> Bool { 81 | return queue.contains(where: { filterMessage in 82 | return filterMessage.id == message.id 83 | }) 84 | } 85 | 86 | func fetchQueue() { 87 | do { 88 | let queue = try syncMessageRepo.readAllWhere(isSynced: false) 89 | self.queue = queue 90 | 91 | } catch let error { 92 | print(error) 93 | #if DEBUG 94 | fatalError() 95 | #endif 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Shared/Components/AddLabelField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddLabelField.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/24/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AddLabelField: View { 11 | let user: User 12 | let workspace: Workspace 13 | var labels: [Label] 14 | let allLabels: [Label] 15 | let addLabel: (Label) -> Void 16 | 17 | private let pad = 8.0 18 | @State private var addLabelText = "" 19 | @State private var isSuggestionsPopoverPresented = false 20 | 21 | var body: some View { 22 | TextField("Add label", text: $addLabelText) 23 | .padding() 24 | .frame(maxWidth: .infinity, alignment: .leading) 25 | .foregroundColor(.gray) 26 | .onChange(of: addLabelText) { newValue in 27 | if allLabels.filter({$0.title.contains(addLabelText)}).first != nil { 28 | isSuggestionsPopoverPresented = true 29 | } 30 | } 31 | .onSubmit { 32 | if !addLabelText.isEmpty { 33 | let now = Date.now 34 | addLabel(Label(id: UUID(), title: addLabelText, color: LabelPalette.allCases().randomElement()!, workspace: workspace.id, user: user.id, createdAt: now, modifiedAt: now)) 35 | addLabelText = "" 36 | } 37 | } 38 | .popover(isPresented: $isSuggestionsPopoverPresented, arrowEdge: .bottom) { 39 | let suggestionLabels = allLabels.filter {$0.title.contains(addLabelText)} 40 | 41 | ScrollView(.horizontal) { 42 | HStack { 43 | ForEach(suggestionLabels, id: \.id) { suggestion in 44 | LabelView(label: suggestion) { _ in 45 | 46 | } 47 | .padding() 48 | .onTapGesture { 49 | addLabel(suggestion) 50 | addLabelText = "" 51 | isSuggestionsPopoverPresented = false 52 | } 53 | .fixedSize() 54 | } 55 | } 56 | } 57 | } 58 | GeometryReader { proxy in 59 | ScrollView(.horizontal) { 60 | HStack { 61 | ForEach(labels, id: \.id) { label in 62 | LabelView(label: label) { newValue in 63 | 64 | } 65 | } 66 | } 67 | .cornerRadius(pad) 68 | Spacer() 69 | } 70 | .frame(width: proxy.size.width - pad * 2, height: Spacing.spacing6.rawValue) 71 | .padding(pad) 72 | } 73 | .foregroundColor(.black) 74 | 75 | Spacer() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/DocumentPreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentPreviewView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 4/23/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// 11 | /// The main document editor for the application. 12 | /// 13 | /// # Overview 14 | /// This component is responsible for viewing and editing documents, and everything that comes with docs. 15 | /// 16 | /// Use the DocumentView as such: 17 | /// 18 | /// ```swift 19 | /// DocumentView( 20 | /// title: $vm.title, 21 | /// labels: $vm.labels, 22 | /// blocks: $vm.blocks, 23 | /// user: $vm.user, 24 | /// workspace: $vm.workspace, 25 | /// document: $vm.document 26 | /// ) 27 | /// ``` 28 | /// 29 | struct DocumentPreviewView: View { 30 | @Environment(\.colorScheme) private var colorScheme 31 | 32 | /// A binding to the title label 33 | let title: String 34 | /// The binding to an array of label model objects. 35 | let labels: [Label] 36 | /// All the labels in the workspace for suggestions. 37 | let blocks: [Block] 38 | 39 | /// The logged in user. 40 | let user: User 41 | /// The current selected workspace. 42 | let workspace: Workspace 43 | /// The current selected document within the workspace. 44 | let document: Document 45 | /// The selected block when creating a content link. 46 | @Binding var selectedLink: UUID? 47 | 48 | @StateObject private var vm = DocumentViewVM() 49 | @State private var promptMenuOpen = false 50 | @State private var linkMenuOpen = false 51 | @State private var focused: FocusedPrompt = FocusedPrompt(uuid: nil, text: "") 52 | 53 | private let pad: Double = 30 54 | 55 | var content: some View { 56 | Group { 57 | VStack(alignment: .leading, spacing: pad) { 58 | // HStack() { 59 | // VStack(alignment: .leading) { 60 | // TextField("", text: .constant(title)) 61 | // .font(.largeTitle) 62 | // .textFieldStyle(.plain) 63 | // Spacer() 64 | // .frame(height: 20) 65 | // LabelField(fetch: fetch, labels: labels, allLabels: [], user: user, workspace: workspace, document: document) 66 | // } 67 | // .foregroundColor(.primary) 68 | // } 69 | HStack() { 70 | BlockViewContainer(user: user, workspace: workspace, document: document, blocks: blocks, promptMenuOpen: $promptMenuOpen, editable: false, selectedLink: $selectedLink, focused: $focused) { 71 | } 72 | 73 | Spacer() 74 | } 75 | Spacer() 76 | } 77 | 78 | } 79 | .padding(.trailing, GlobalDimension.toolbarWidth) 80 | } 81 | 82 | var body: some View { 83 | HorizontalFlexPreviewView { 84 | #if os(macOS) 85 | content 86 | .padding(GlobalDimension.toolbarWidth + Spacing.spacing1.rawValue) 87 | #else 88 | content 89 | .padding(.top, Spacing.spacing8.rawValue) 90 | #endif 91 | } 92 | .onAppear { 93 | vm.fetchDocuments(user: user, workspace: workspace) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Graphnote.xcodeproj/xcshareddata/xcschemes/Graphnote (iOS).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 66 | 68 | 74 | 75 | 76 | 77 | 79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/BlockViewVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockViewVM.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class BlockViewVM: ObservableObject { 11 | private let saveInterval = 2.0 12 | private var timer: Timer? = nil 13 | 14 | @Published var content: String = "INIT" { 15 | didSet { 16 | prevContent = oldValue 17 | } 18 | } 19 | 20 | var prevContent: String = "INIT" 21 | 22 | let user: User 23 | let workspace: Workspace 24 | let document: Document 25 | let block: Block 26 | 27 | init(text: String, user: User, workspace: Workspace, document: Document, block: Block) { 28 | self.content = text 29 | self.user = user 30 | self.workspace = workspace 31 | self.document = document 32 | self.block = block 33 | self.timer?.invalidate() 34 | self.timer = nil 35 | self.timer = Timer.scheduledTimer(timeInterval: saveInterval, target: self, selector: #selector(save), userInfo: nil, repeats: true) 36 | } 37 | 38 | deinit { 39 | timer?.invalidate() 40 | timer = nil 41 | } 42 | 43 | @objc 44 | func save() { 45 | if content != prevContent && content != block.content { 46 | 47 | // if let localBlock = DataService.shared.readBlock(user: user, workspace: workspace, document: document, block: block.id) { 48 | let contentCapture = content 49 | fetch() 50 | // print("BLOCK: \(localBlock)") 51 | DataService.shared.updateBlock(user: user, workspace: workspace, document: document, block: block, content: contentCapture) 52 | prevContent = contentCapture 53 | fetch() 54 | // } 55 | 56 | } 57 | } 58 | 59 | func deleteBlock(block: Block, user: User, workspace: Workspace) throws { 60 | DataService.shared.deleteBlock(user: user, workspace: workspace, block: block) 61 | do { 62 | // let documentRepo = DocumentRepo(user: user, workspace: workspace) 63 | // 64 | // let updateBlock = Block(id: block.id, 65 | // type: block.type, 66 | // content: block.content, 67 | // prev: block.prev, 68 | // next: block.next, 69 | // graveyard: true, 70 | // createdAt: block.createdAt, 71 | // modifiedAt: .now, 72 | // document: document) 73 | // documentRepo.update(block: updateBlock) 74 | 75 | } catch let error { 76 | print(error) 77 | } 78 | } 79 | 80 | func readBlock(id: UUID, user: User, workspace: Workspace) -> Block? { 81 | do { 82 | let documentRepo = DocumentRepo(user: user, workspace: workspace) 83 | return try documentRepo.readBlock(document: document, block: id) 84 | } catch let error { 85 | print(error) 86 | return nil 87 | } 88 | } 89 | 90 | func fetch() { 91 | if let block = DataService.shared.readBlock(user: user, workspace: workspace, document: document, block: block.id) { 92 | if block.content != content { 93 | content = block.content 94 | } 95 | } 96 | 97 | } 98 | 99 | func getBlockText(id: UUID) -> String? { 100 | let block = DataService.shared.readBlock(user: user, workspace: workspace, document: document, block: id) 101 | return block?.content 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Shared/Repositories/LabelRepo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelRepo.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | import SwiftUI 11 | 12 | enum LabelRepoError: Error { 13 | case workspaceFailed 14 | case userFailed 15 | } 16 | 17 | struct LabelRepo { 18 | let user: User 19 | let workspace: Workspace 20 | 21 | private let moc = DataController.shared.container.viewContext 22 | 23 | func create(label: Label) throws -> LabelEntity? { 24 | guard let workspaceEntity = try? getWorkspaceEntity(workspace: workspace) else { 25 | print("Failed to get workspaceEntity: \(workspace)") 26 | throw LabelRepoError.workspaceFailed 27 | } 28 | 29 | if let labelEntity = try? LabelEntity.getEntity(title: label.title, workspace: workspace, moc: moc) { 30 | return labelEntity 31 | } else { 32 | do { 33 | guard let userEntity = try? UserEntity.getEntity(id: label.user, moc: moc) else { 34 | throw LabelRepoError.userFailed 35 | } 36 | 37 | let labelEntity = LabelEntity(entity: LabelEntity.entity(), insertInto: moc) 38 | labelEntity.title = label.title 39 | labelEntity.id = label.id 40 | labelEntity.modifiedAt = label.modifiedAt 41 | labelEntity.createdAt = label.createdAt 42 | labelEntity.workspace = workspaceEntity 43 | labelEntity.color = label.color.rawValue 44 | labelEntity.user = userEntity 45 | 46 | try moc.save() 47 | return labelEntity 48 | 49 | } catch let error { 50 | print(error) 51 | return nil 52 | } 53 | 54 | } 55 | } 56 | 57 | func read(id: UUID) throws -> Label? { 58 | do { 59 | guard let labelEntity = try LabelEntity.getEntity(id: id, moc: moc) else { 60 | return nil 61 | } 62 | 63 | if let user = labelEntity.user { 64 | let label = try Label(from: labelEntity) 65 | return label 66 | 67 | } else { 68 | return nil 69 | } 70 | 71 | 72 | } catch let error { 73 | print(error) 74 | throw error 75 | } 76 | } 77 | 78 | func exists(label: Label) throws -> UUID? { 79 | do { 80 | let fetchRequest = LabelEntity.fetchRequest() 81 | fetchRequest.predicate = NSPredicate(format: "title == %@ || id == %@", label.title, label.id.uuidString) 82 | let result = try moc.fetch(fetchRequest) 83 | 84 | if let first = result.first { 85 | return first.id 86 | } 87 | 88 | return nil 89 | 90 | } catch let error { 91 | print(error) 92 | throw error 93 | } 94 | } 95 | 96 | private func getWorkspaceEntity(workspace: Workspace) throws -> WorkspaceEntity? { 97 | do { 98 | let fetchRequest = WorkspaceEntity.fetchRequest() 99 | fetchRequest.predicate = NSPredicate(format: "id == %@", workspace.id.uuidString) 100 | guard let user = try moc.fetch(fetchRequest).first else { 101 | return nil 102 | } 103 | 104 | return user 105 | 106 | } catch let error { 107 | print(error) 108 | throw error 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /Shared/Components/LabelField/LabelField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelField.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum LabelNotification: String { 11 | case newLabel 12 | } 13 | 14 | struct LabelField: View { 15 | @State private var editing = false 16 | @State private var selectedLabel: Label? 17 | @State private var showAddSheet = false 18 | @State private var labelExistsAlertOpen = false 19 | 20 | private let labelService: LabelService 21 | 22 | let fetch: () -> Void 23 | let labels: [Label] 24 | let allLabels: [Label] 25 | let user: User 26 | let workspace: Workspace 27 | let document: Document 28 | 29 | init(fetch: @escaping () -> Void, labels: [Label], allLabels: [Label], user: User, workspace: Workspace, document: Document) { 30 | self.labelService = LabelService(user: user, workspace: workspace) 31 | self.fetch = fetch 32 | self.labels = labels 33 | self.allLabels = allLabels 34 | self.user = user 35 | self.workspace = workspace 36 | self.document = document 37 | } 38 | 39 | func newLabelNotification() { 40 | NotificationCenter.default.post(name: Notification.Name(LabelNotification.newLabel.rawValue), object: nil) 41 | } 42 | 43 | var body: some View { 44 | HStack { 45 | ScrollView(.horizontal, showsIndicators: false) { 46 | HStack { 47 | ForEach(labels, id: \.self) { label in 48 | LabelView(label: label) { newName in 49 | 50 | } 51 | } 52 | } 53 | } 54 | Spacer() 55 | .frame(width: Spacing.spacing1.rawValue) 56 | AddIconView() 57 | .sheet(isPresented: $showAddSheet, content: { 58 | AddLabelView(user: user, workspace: workspace, allLabels: allLabels) { labels in 59 | do { 60 | for label in labels { 61 | let title = label.title 62 | let color = label.color 63 | let added = try labelService.addLabel(title: title, color: color, document: document) 64 | if added { 65 | newLabelNotification() 66 | fetch() 67 | self.showAddSheet = false 68 | 69 | } else { 70 | self.showAddSheet = false 71 | DispatchQueue.main.async { 72 | labelExistsAlertOpen = true 73 | } 74 | } 75 | } 76 | 77 | 78 | } catch let error { 79 | print(error) 80 | return 81 | } 82 | } close: { 83 | self.showAddSheet = false 84 | } 85 | .id(document.id) 86 | .presentationDetents([.medium, .large]) 87 | .frame(width: GlobalDimension.labelModalWidth, height: GlobalDimension.labelModalHeight) 88 | }) 89 | .alert("Label already exists!", isPresented: $labelExistsAlertOpen, actions: { 90 | }) 91 | .onTapGesture { 92 | showAddSheet = true 93 | } 94 | Spacer(minLength: Spacing.spacing4.rawValue) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Shared/Components/Labels/LabelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/20/23. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | 11 | struct LabelView: View { 12 | let label: Label 13 | let rename: (_ name: String) -> Void 14 | 15 | enum FocusedField { 16 | case title 17 | } 18 | 19 | @State private var editing = false 20 | @State private var content: String = "" 21 | @FocusState private var focusedField: FocusedField? 22 | @FocusState private var isFocused: Bool 23 | 24 | init(label: Label, rename: @escaping (_: String) -> Void) { 25 | self.label = label 26 | self.rename = rename 27 | 28 | // Set the content to label.title initially and when we get updates from init 29 | self.content = label.title 30 | } 31 | 32 | private let height = 24.0 33 | private let minWidth = 60.0 34 | #if os(macOS) 35 | private let font = Font.title3 36 | #else 37 | private let font = Font.body 38 | #endif 39 | 40 | var editingTextField: some View { 41 | TextField("", text: $content) 42 | .font(font) 43 | .foregroundColor(Color.black) 44 | .lineLimit(1) 45 | .bold() 46 | .padding([.leading, .trailing], Spacing.spacing3.rawValue) 47 | .padding([.top, .bottom], Spacing.spacing0.rawValue) 48 | .frame(minWidth: minWidth) 49 | .frame(height: height) 50 | .background(RoundedRectangle(cornerRadius: height).fill(label.color.getColor())) 51 | .contentShape(RoundedRectangle(cornerRadius: height)) 52 | .focused($focusedField, equals: .title) 53 | .focused($isFocused) 54 | .onChange(of: isFocused, perform: { newValue in 55 | if newValue == false { 56 | editing = newValue 57 | } 58 | }) 59 | .onSubmit { 60 | editing = false 61 | } 62 | } 63 | 64 | var body: some View { 65 | if editing { 66 | #if os(macOS) 67 | Form { 68 | editingTextField 69 | .onExitCommand { 70 | editing = false 71 | } 72 | } 73 | #else 74 | editingTextField 75 | #endif 76 | 77 | } else { 78 | Text(label.title) 79 | .font(font) 80 | .foregroundColor(Color.black) 81 | .lineLimit(1) 82 | .bold() 83 | .padding([.leading, .trailing], Spacing.spacing3.rawValue) 84 | .padding([.top, .bottom], Spacing.spacing0.rawValue) 85 | .frame(minWidth: minWidth) 86 | .frame(height: height) 87 | .background( 88 | RoundedRectangle(cornerRadius: height) 89 | .fill(label.color.getColor()) 90 | ) 91 | .contextMenu { 92 | LabelContextMenu { 93 | editing = true 94 | focusedField = .title 95 | content = label.title 96 | } delete: { 97 | 98 | } 99 | 100 | } 101 | .contentShape(RoundedRectangle(cornerRadius: height)) 102 | } 103 | } 104 | } 105 | 106 | //struct Label_Previews: PreviewProvider { 107 | // static var previews: some View { 108 | // LabelView(label: Label(id: UUID(), title: "Test", colorRed: 0.12, colorGreen: 0.5, colorBlue: 0.8, createdAt: .now, modifiedAt: .now)) { newLabel in 109 | // 110 | // } 111 | // } 112 | //} 113 | -------------------------------------------------------------------------------- /Graphnote.xcodeproj/xcshareddata/xcschemes/Graphnote (macOS).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 76 | 78 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /Graphnote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "abseil-cpp-swiftpm", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git", 7 | "state" : { 8 | "revision" : "583de9bd60f66b40e78d08599cc92036c2e7e4e1", 9 | "version" : "0.20220203.2" 10 | } 11 | }, 12 | { 13 | "identity" : "boringssl-swiftpm", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/firebase/boringssl-SwiftPM.git", 16 | "state" : { 17 | "revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab", 18 | "version" : "0.9.1" 19 | } 20 | }, 21 | { 22 | "identity" : "firebase-ios-sdk", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/firebase/firebase-ios-sdk", 25 | "state" : { 26 | "revision" : "7e80c25b51c2ffa238879b07fbfc5baa54bb3050", 27 | "version" : "9.6.0" 28 | } 29 | }, 30 | { 31 | "identity" : "googleappmeasurement", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/google/GoogleAppMeasurement.git", 34 | "state" : { 35 | "revision" : "c1cfde8067668027b23a42c29d11c246152fe046", 36 | "version" : "9.6.0" 37 | } 38 | }, 39 | { 40 | "identity" : "googledatatransport", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/google/GoogleDataTransport.git", 43 | "state" : { 44 | "revision" : "cc7265b8e3906304e6e81f32c1662a94bbae2357", 45 | "version" : "9.2.2" 46 | } 47 | }, 48 | { 49 | "identity" : "googleutilities", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/google/GoogleUtilities.git", 52 | "state" : { 53 | "revision" : "871d43135925cde39ef7421d8723ce47edfdcc39", 54 | "version" : "7.11.1" 55 | } 56 | }, 57 | { 58 | "identity" : "grpc-ios", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/grpc/grpc-ios.git", 61 | "state" : { 62 | "revision" : "8440b914756e0d26d4f4d054a1c1581daedfc5b6", 63 | "version" : "1.44.3-grpc" 64 | } 65 | }, 66 | { 67 | "identity" : "gtm-session-fetcher", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/google/gtm-session-fetcher.git", 70 | "state" : { 71 | "revision" : "5ccda3981422a84186387dbb763ba739178b529c", 72 | "version" : "2.3.0" 73 | } 74 | }, 75 | { 76 | "identity" : "leveldb", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/firebase/leveldb.git", 79 | "state" : { 80 | "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", 81 | "version" : "1.22.2" 82 | } 83 | }, 84 | { 85 | "identity" : "nanopb", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/firebase/nanopb.git", 88 | "state" : { 89 | "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", 90 | "version" : "2.30909.0" 91 | } 92 | }, 93 | { 94 | "identity" : "promises", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/google/promises.git", 97 | "state" : { 98 | "revision" : "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a", 99 | "version" : "2.2.0" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-protobuf", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/apple/swift-protobuf.git", 106 | "state" : { 107 | "revision" : "0af9125c4eae12a4973fb66574c53a54962a9e1e", 108 | "version" : "1.21.0" 109 | } 110 | } 111 | ], 112 | "version" : 2 113 | } 114 | -------------------------------------------------------------------------------- /Shared/Components/SplitView/SplitView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SplitView: View { 11 | @Binding var sidebarOpen: Bool 12 | let syncStatus: SyncServiceStatus 13 | 14 | let sidebar: () -> any View 15 | let detail: () -> any View 16 | let retrySync: () -> Void 17 | 18 | 19 | var drag: some Gesture { 20 | DragGesture() 21 | .onEnded { _ in 22 | withAnimation { 23 | self.sidebarOpen.toggle() 24 | } 25 | } 26 | } 27 | 28 | var content: some View { 29 | GeometryReader { geometry in 30 | HStack(spacing: Spacing.spacing0.rawValue) { 31 | #if os(macOS) 32 | if sidebarOpen { 33 | AnyView(sidebar()) 34 | .frame(width: !sidebarOpen ? .zero : nil) 35 | } 36 | ToolbarView { 37 | MenuCardView() 38 | } action: { 39 | // withAnimation { 40 | self.sidebarOpen.toggle() 41 | // } 42 | 43 | } 44 | 45 | #else 46 | 47 | let portrait = MobileUtils.OrientationInfo().orientation == .portrait 48 | let width = geometry.size.width 49 | let sidebarSizeMultiplier = portrait && width < 500 ? 0.85 : 0.35 50 | let finalWidth = width * sidebarSizeMultiplier 51 | if sidebarOpen { 52 | ZStack { 53 | ColorPalette.lightSidebarMobile 54 | .ignoresSafeArea() 55 | AnyView(sidebar()) 56 | .frame(maxWidth: GlobalDimension.treeWidth) 57 | .frame(width: !sidebarOpen ? .zero : finalWidth < GlobalDimension.treeWidth ? finalWidth : GlobalDimension.treeWidth) 58 | } 59 | .frame(maxWidth: GlobalDimension.treeWidth) 60 | .frame(width: !sidebarOpen ? .zero : finalWidth < GlobalDimension.treeWidth ? finalWidth : GlobalDimension.treeWidth) 61 | } 62 | 63 | ToolbarView { 64 | MenuCardView() 65 | } action: { 66 | withAnimation { 67 | self.sidebarOpen.toggle() 68 | } 69 | } 70 | .gesture(drag) 71 | .onTapGesture { 72 | if sidebarOpen { 73 | withAnimation { 74 | self.sidebarOpen.toggle() 75 | } 76 | } 77 | } 78 | #endif 79 | 80 | AnyView(detail()) 81 | .overlay(alignment: .trailing) { 82 | VStack(spacing: .zero) { 83 | Spacer() 84 | SyncStatusIconView(status: syncStatus) 85 | .padding(Spacing.spacing3.rawValue) 86 | .onTapGesture(perform: retrySync) 87 | 88 | } 89 | 90 | } 91 | } 92 | } 93 | } 94 | 95 | var body: some View { 96 | #if os(macOS) 97 | content 98 | .edgesIgnoringSafeArea(.all) 99 | #else 100 | content 101 | #endif 102 | } 103 | } 104 | 105 | struct SplitView_Previews: PreviewProvider { 106 | static var previews: some View { 107 | SplitView(sidebarOpen: .constant(true), syncStatus: .success) { 108 | EmptyView() 109 | } detail: { 110 | EmptyView() 111 | } retrySync: { 112 | 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Shared/Repositories/UserRepo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRepo.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/26/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | struct UserRepo { 12 | 13 | private let moc = DataController.shared.container.viewContext 14 | 15 | // func create(workspace: Workspace, for user: User) -> Bool { 16 | // do { 17 | // guard let userEntity = try UserEntity.getEntity(id: user.id, moc: moc) else { 18 | // return false 19 | // } 20 | // 21 | // let workspaceEntity = WorkspaceEntity(entity: WorkspaceEntity.entity(), insertInto: moc) 22 | // workspaceEntity.id = workspace.id 23 | // workspaceEntity.user = userEntity 24 | // workspaceEntity.createdAt = workspace.createdAt 25 | // workspaceEntity.modifiedAt = workspace.modifiedAt 26 | // workspaceEntity.title = workspace.title 27 | // workspaceEntity.labels = NSSet(array: workspace.labels.map { 28 | // let labelEntity = LabelEntity(entity: LabelEntity.entity(), insertInto: moc) 29 | // labelEntity.id = $0.id 30 | // labelEntity.title = $0.title 31 | // labelEntity.createdAt = $0.createdAt 32 | // labelEntity.modifiedAt = $0.modifiedAt 33 | // labelEntity.color = $0.color.rawValue 34 | // labelEntity.workspace = workspaceEntity 35 | // return labelEntity 36 | // }) 37 | // 38 | // try moc.save() 39 | // 40 | // return true 41 | // 42 | // } catch let error { 43 | // print(error) 44 | // return false 45 | // } 46 | // } 47 | 48 | func read(id: String) -> User? { 49 | do { 50 | guard let userEntity = try getEntity(id: id) else { 51 | return nil 52 | } 53 | 54 | let user = User( 55 | id: userEntity.id, 56 | email: userEntity.email, 57 | givenName: userEntity.givenName, 58 | familyName: userEntity.familyName, 59 | createdAt: userEntity.createdAt, 60 | modifiedAt: userEntity.modifiedAt 61 | ) 62 | return user 63 | 64 | } catch let error { 65 | print(error) 66 | return nil 67 | } 68 | } 69 | 70 | func readAll() throws -> [User]? { 71 | let entities = try self.getEntities() 72 | 73 | return entities.map { 74 | User( 75 | id: $0.id, 76 | email: $0.email, 77 | givenName: $0.givenName, 78 | familyName: $0.familyName, 79 | createdAt: $0.createdAt, 80 | modifiedAt: $0.modifiedAt 81 | ) 82 | } 83 | } 84 | 85 | func delete(user: User) throws { 86 | do { 87 | let fetchRequest = UserEntity.fetchRequest() 88 | fetchRequest.predicate = NSPredicate(format: "id == %@", user.id) 89 | let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest as! NSFetchRequest) 90 | 91 | try moc.execute(deleteRequest) 92 | 93 | try? moc.save() 94 | } catch let error { 95 | print(error) 96 | throw error 97 | } 98 | } 99 | 100 | private func getEntity(id: String) throws -> UserEntity? { 101 | do { 102 | let fetchRequest = UserEntity.fetchRequest() 103 | fetchRequest.predicate = NSPredicate(format: "id == %@", id) 104 | guard let user = try moc.fetch(fetchRequest).first else { 105 | return nil 106 | } 107 | 108 | return user 109 | 110 | } catch let error { 111 | print(error) 112 | throw error 113 | } 114 | } 115 | 116 | private func getEntities() throws -> [UserEntity] { 117 | do { 118 | 119 | let fetchRequest = UserEntity.fetchRequest() 120 | let users = try moc.fetch(fetchRequest) 121 | return users 122 | 123 | } catch let error { 124 | print(error) 125 | throw error 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Shared/Containers/DocumentContainerVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentContainerVM.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/29/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class DocumentContainerVM: ObservableObject { 11 | private var previousTitle: String? = nil 12 | private var previousId: UUID? = nil 13 | private let saveInterval = 2.0 14 | private var timer: Timer? = nil 15 | private var saving = false 16 | 17 | init() { 18 | self.timer = Timer.scheduledTimer(timeInterval: saveInterval, target: self, selector: #selector(_saveHandler), userInfo: nil, repeats: true) 19 | } 20 | 21 | deinit { 22 | timer?.invalidate() 23 | timer = nil 24 | } 25 | 26 | @objc 27 | func _saveHandler() { 28 | if !saving { 29 | self.save() 30 | } 31 | } 32 | 33 | func save(title: String? = nil, force: Bool = false) { 34 | if let title { 35 | if self.title != title { 36 | self.title = title 37 | } 38 | } 39 | 40 | if !saving { 41 | self.saving = true 42 | 43 | if (self.title != self.previousTitle && previousId == self.document?.id) || force { 44 | if let document = self.document, let workspace = self.workspace, let user = self.user { 45 | DataService.shared.updateDocumentTitle(user: user, workspace: workspace, document: document, title: self.title) 46 | } 47 | self.previousId = document?.id 48 | } 49 | 50 | self.saving = false 51 | } 52 | } 53 | 54 | @Published var title = "" { 55 | didSet { 56 | self.previousTitle = title 57 | } 58 | } 59 | @Published var labels = [Label]() 60 | @Published var blocks = [Block]() 61 | @Published var allLabels = [Label]() 62 | 63 | var document: Document? = nil { 64 | didSet { 65 | if document?.title != oldValue?.title || document?.id != oldValue?.id { 66 | self.previousId = oldValue?.id 67 | if let document { 68 | self.previousTitle = self.title 69 | self.title = document.title 70 | } 71 | } 72 | } 73 | } 74 | 75 | var workspace: Workspace? = nil 76 | var user: User? = nil 77 | 78 | func fetchBlocks(user: User, workspace: Workspace, document: Document) { 79 | let documentRepo = DocumentRepo(user: user, workspace: workspace) 80 | 81 | do { 82 | if let blocks = try documentRepo.readBlocks(document: document) { 83 | self.blocks = blocks 84 | } 85 | } catch let error { 86 | print(error) 87 | return 88 | } 89 | } 90 | 91 | func fetch(user: User, workspace: Workspace, document: Document) { 92 | let documentRepo = DocumentRepo(user: user, workspace: workspace) 93 | 94 | if let labels = documentRepo.readLabels(document: document) { 95 | if labels != self.labels { 96 | self.labels = labels 97 | } 98 | } 99 | 100 | do { 101 | 102 | let allLabels = try WorkspaceRepo(user: user).readAllLabels(workspace: workspace) 103 | if allLabels != self.allLabels { 104 | self.allLabels = allLabels 105 | } 106 | 107 | if let blocks = try documentRepo.readBlocks(document: document) { 108 | if blocks != self.blocks { 109 | self.blocks = blocks 110 | } 111 | } 112 | } catch let error { 113 | print(error) 114 | return 115 | } 116 | 117 | self.user = user 118 | self.workspace = workspace 119 | 120 | do { 121 | if let doc = try documentRepo.read(id: document.id) { 122 | self.document = doc 123 | } 124 | } catch let error { 125 | print(error) 126 | return 127 | } 128 | 129 | // if document.title != self.title { 130 | // self.title = document.title 131 | // } 132 | 133 | // if self.previousId == nil { 134 | // self.previousId = document.id 135 | // } 136 | 137 | // if self.previousTitle == nil { 138 | // self.previousTitle = document.title 139 | // } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Shared/Containers/BlockViewContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockViewContainer.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/17/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FocusedPrompt { 11 | let uuid: UUID? 12 | let text: String 13 | } 14 | 15 | struct BlockViewContainer: View { 16 | let user: User 17 | let workspace: Workspace 18 | let document: Document 19 | let blocks: [Block] 20 | @Binding var promptMenuOpen: Bool 21 | let editable: Bool 22 | @Binding var selectedLink: UUID? 23 | @Binding var focused: FocusedPrompt 24 | let action: () -> Void 25 | 26 | @StateObject private var vm = BlockViewContainerVM() 27 | @State private var isKeyDown = false 28 | @State private var id: UUID? = nil 29 | @State private var prevContent = "INIT" 30 | 31 | private let blockCreatedNotification = Notification.Name(DataServiceNotification.blockCreated.rawValue) 32 | private let blockDeletedNotification = Notification.Name(DataServiceNotification.blockDeleted.rawValue) 33 | private let blockUpdatedNotification = Notification.Name(SyncServiceNotification.blockUpdated.rawValue) 34 | private let blockDeletedSyncServiceNotification = Notification.Name(SyncServiceNotification.blockDeleted.rawValue) 35 | 36 | var body: some View { 37 | VStack(alignment: .leading, spacing: .zero) { 38 | ForEach(blocks, id: \.id) { block in 39 | if block.graveyard { 40 | EmptyView() 41 | .frame(width: .zero, height: .zero) 42 | .id(id) 43 | } else { 44 | BlockView(user: user, 45 | workspace: workspace, 46 | document: document, 47 | block: block, 48 | editable: block.type == .contentLink ? false : editable, 49 | focused: $focused, 50 | selectedLink: $selectedLink, 51 | promptMenuOpen: $promptMenuOpen, 52 | selectedContentId: $selectedLink, 53 | fetch: action 54 | ) { (id, text) in 55 | 56 | guard let index = blocks.firstIndex(where: { $0.id == id }) else { 57 | return 58 | } 59 | 60 | if index == 0 && blocks.isEmpty { 61 | if let newBlock = vm.insertBlock(user: user, workspace: workspace, document: document, promptText: "", prev: block.id, next: nil) { 62 | vm.updateBlock(block, user: user, workspace: workspace, document: document, next: newBlock.id, text: text) 63 | DataService.shared.updateDocumentFocused(user: user, workspace: workspace, document: document, focused: newBlock.id) 64 | focused = FocusedPrompt(uuid: newBlock.id, text: newBlock.content) 65 | 66 | } 67 | } else { 68 | if let newBlock = vm.insertBlock(user: user, workspace: workspace, document: document, promptText: "", prev: block.id, next: block.next) { 69 | vm.updateBlock(block, user: user, workspace: workspace, document: document, next: newBlock.id, text: text) 70 | DataService.shared.updateDocumentFocused(user: user, workspace: workspace, document: document, focused: newBlock.id) 71 | focused = FocusedPrompt(uuid: newBlock.id, text: newBlock.content) 72 | } 73 | } 74 | 75 | action() 76 | } 77 | .id(block.id) 78 | } 79 | } 80 | } 81 | .onAppear { 82 | focused = FocusedPrompt(uuid: document.focused, text: "") 83 | } 84 | .onReceive(NotificationCenter.default.publisher(for: blockCreatedNotification)) { notification in 85 | action() 86 | } 87 | .onReceive(NotificationCenter.default.publisher(for: blockDeletedNotification)) { notification in 88 | action() 89 | } 90 | .onReceive(NotificationCenter.default.publisher(for: blockUpdatedNotification)) { notification in 91 | action() 92 | } 93 | .onReceive(NotificationCenter.default.publisher(for: blockDeletedSyncServiceNotification)) { notification in 94 | action() 95 | } 96 | .fixedSize(horizontal: false, vertical: true) 97 | .submitScope() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "114.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "120.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "180.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "20.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "20x20" 74 | }, 75 | { 76 | "filename" : "40.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "20x20" 80 | }, 81 | { 82 | "filename" : "29.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "29x29" 86 | }, 87 | { 88 | "filename" : "58.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "29x29" 92 | }, 93 | { 94 | "filename" : "40.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "40x40" 98 | }, 99 | { 100 | "filename" : "80.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "40x40" 104 | }, 105 | { 106 | "filename" : "50.png", 107 | "idiom" : "ipad", 108 | "scale" : "1x", 109 | "size" : "50x50" 110 | }, 111 | { 112 | "filename" : "100.png", 113 | "idiom" : "ipad", 114 | "scale" : "2x", 115 | "size" : "50x50" 116 | }, 117 | { 118 | "filename" : "72.png", 119 | "idiom" : "ipad", 120 | "scale" : "1x", 121 | "size" : "72x72" 122 | }, 123 | { 124 | "filename" : "144.png", 125 | "idiom" : "ipad", 126 | "scale" : "2x", 127 | "size" : "72x72" 128 | }, 129 | { 130 | "filename" : "76.png", 131 | "idiom" : "ipad", 132 | "scale" : "1x", 133 | "size" : "76x76" 134 | }, 135 | { 136 | "filename" : "152.png", 137 | "idiom" : "ipad", 138 | "scale" : "2x", 139 | "size" : "76x76" 140 | }, 141 | { 142 | "filename" : "167.png", 143 | "idiom" : "ipad", 144 | "scale" : "2x", 145 | "size" : "83.5x83.5" 146 | }, 147 | { 148 | "filename" : "1024.png", 149 | "idiom" : "ios-marketing", 150 | "scale" : "1x", 151 | "size" : "1024x1024" 152 | }, 153 | { 154 | "filename" : "16.png", 155 | "idiom" : "mac", 156 | "scale" : "1x", 157 | "size" : "16x16" 158 | }, 159 | { 160 | "filename" : "32.png", 161 | "idiom" : "mac", 162 | "scale" : "2x", 163 | "size" : "16x16" 164 | }, 165 | { 166 | "filename" : "32.png", 167 | "idiom" : "mac", 168 | "scale" : "1x", 169 | "size" : "32x32" 170 | }, 171 | { 172 | "filename" : "64.png", 173 | "idiom" : "mac", 174 | "scale" : "2x", 175 | "size" : "32x32" 176 | }, 177 | { 178 | "filename" : "128.png", 179 | "idiom" : "mac", 180 | "scale" : "1x", 181 | "size" : "128x128" 182 | }, 183 | { 184 | "filename" : "256.png", 185 | "idiom" : "mac", 186 | "scale" : "2x", 187 | "size" : "128x128" 188 | }, 189 | { 190 | "filename" : "256.png", 191 | "idiom" : "mac", 192 | "scale" : "1x", 193 | "size" : "256x256" 194 | }, 195 | { 196 | "filename" : "512.png", 197 | "idiom" : "mac", 198 | "scale" : "2x", 199 | "size" : "256x256" 200 | }, 201 | { 202 | "filename" : "512.png", 203 | "idiom" : "mac", 204 | "scale" : "1x", 205 | "size" : "512x512" 206 | }, 207 | { 208 | "filename" : "1024.png", 209 | "idiom" : "mac", 210 | "scale" : "2x", 211 | "size" : "512x512" 212 | } 213 | ], 214 | "info" : { 215 | "author" : "xcode", 216 | "version" : 1 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Docs/Diagrams/PullQueue/PullQueue.drawio: -------------------------------------------------------------------------------- 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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Docs/Diagrams/PushQueue/PushQueue.drawio: -------------------------------------------------------------------------------- 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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/BlockView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 2/23/23. 6 | // 7 | 8 | import SwiftUI 9 | enum BlockFocusedField { 10 | case prompt 11 | case body 12 | } 13 | 14 | struct BlockView: View { 15 | let user: User 16 | let workspace: Workspace 17 | let document: Document 18 | let block: Block 19 | let editable: Bool 20 | @Binding var focused: FocusedPrompt 21 | @Binding var selectedLink: UUID? 22 | @Binding var promptMenuOpen: Bool 23 | @Binding var selectedContentId: UUID? 24 | let fetch: () -> Void 25 | let onEnter: (_ id: UUID, _ text: String) -> Void 26 | 27 | @StateObject private var vm: BlockViewVM 28 | 29 | init(user: User, 30 | workspace: Workspace, 31 | document: Document, 32 | block: Block, 33 | editable: Bool, 34 | focused: Binding, 35 | selectedLink: Binding, 36 | promptMenuOpen: Binding, 37 | selectedContentId: Binding, 38 | fetch: @escaping () -> Void, 39 | onEnter: @escaping (UUID, String) -> Void 40 | ) { 41 | self.user = user 42 | self.workspace = workspace 43 | self.document = document 44 | self.block = block 45 | self.editable = editable 46 | self._focused = focused 47 | self._selectedLink = selectedLink 48 | self._promptMenuOpen = promptMenuOpen 49 | self._selectedContentId = selectedContentId 50 | self.fetch = fetch 51 | self.onEnter = onEnter 52 | 53 | self._vm = StateObject(wrappedValue: BlockViewVM(text: block.content, user: user, workspace: workspace, document: document, block: block)) 54 | } 55 | 56 | private let blockUpdatedNotification = Notification.Name(SyncServiceNotification.blockUpdated.rawValue) 57 | private let blockCreatedNotification = Notification.Name(SyncServiceNotification.blockCreated.rawValue) 58 | 59 | @FocusState private var focusedField: BlockFocusedField? { 60 | didSet { 61 | focusFieldPromptFlag = focusedField == .prompt ? true : false 62 | } 63 | } 64 | 65 | @State private var focusFieldPromptFlag = false 66 | @State private var linkContent: String = "" 67 | 68 | @ViewBuilder var bodyType: some View { 69 | if editable { 70 | PromptField(id: block.id, type: .body, text: $vm.content, focused: $focused, promptMenuOpen: $promptMenuOpen) { (id, text) in 71 | self.onEnter(id, text) 72 | } onBackspaceRemove: { 73 | do { 74 | if let prev = block.prev { 75 | if let prevBlock = vm.readBlock(id: prev, user: user, workspace: workspace) { 76 | if prevBlock.graveyard == false { 77 | self.focused = FocusedPrompt(uuid: prevBlock.id, text: prevBlock.content) 78 | 79 | try vm.deleteBlock(block: block, user: user, workspace: workspace) 80 | fetch() 81 | } 82 | } 83 | 84 | } else if let next = block.next { 85 | if let nextBlock = vm.readBlock(id: next, user: user, workspace: workspace) { 86 | if nextBlock.graveyard == false { 87 | self.focused = FocusedPrompt(uuid: nextBlock.id, text: nextBlock.content) 88 | 89 | try vm.deleteBlock(block: block, user: user, workspace: workspace) 90 | fetch() 91 | } 92 | } 93 | } 94 | } catch let error { 95 | print(error) 96 | } 97 | } 98 | .id(block.id) 99 | .onAppear { 100 | vm.content = block.content 101 | } 102 | .onChange(of: block.content) { newValue in 103 | if newValue != vm.content { 104 | vm.content = newValue 105 | vm.prevContent = newValue 106 | } 107 | } 108 | 109 | } else { 110 | PromptView(id: block.id, type: block.type, block: block, selected: block.id == selectedContentId) 111 | .onTapGesture { 112 | if block.id != selectedContentId { 113 | selectedContentId = block.id 114 | } else { 115 | selectedContentId = nil 116 | } 117 | } 118 | } 119 | } 120 | 121 | var font: Font { 122 | switch block.type { 123 | default: 124 | return .custom("", size: PromptFontDimensions.bodyFontSize, relativeTo: .body) 125 | } 126 | } 127 | 128 | var contentLinkType: some View { 129 | Text(linkContent) 130 | .font(font) 131 | .lineSpacing(Spacing.spacing2.rawValue) 132 | .onAppear { 133 | if let content = vm.readBlock(id: UUID(uuidString: block.content)!, user: user, workspace: workspace)?.content { 134 | linkContent = content 135 | } 136 | } 137 | .padding(Spacing.spacing4.rawValue) 138 | .overlay { 139 | ZStack { 140 | RoundedRectangle(cornerRadius: 8) 141 | .stroke(LabelPalette.primary.getColor()) 142 | RoundedRectangle(cornerRadius: 8) 143 | .fill(LabelPalette.primary.getColor().opacity(0.125)) 144 | } 145 | } 146 | .padding([.top, .bottom], Spacing.spacing2.rawValue) 147 | 148 | } 149 | 150 | var body: some View { 151 | switch block.type { 152 | case .body, .heading1, .heading2, .heading3, .heading4: 153 | bodyType 154 | case .contentLink: 155 | contentLinkType 156 | default: 157 | fatalError() 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Shared/Components/DocumentView/DocumentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentView.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// 11 | /// The main document editor for the application. 12 | /// 13 | /// # Overview 14 | /// This component is responsible for viewing and editing documents, and everything that comes with docs. 15 | /// 16 | /// Use the DocumentView as such: 17 | /// 18 | /// ```swift 19 | /// DocumentView( 20 | /// title: $vm.title, 21 | /// labels: $vm.labels, 22 | /// blocks: $vm.blocks, 23 | /// user: $vm.user, 24 | /// workspace: $vm.workspace, 25 | /// document: $vm.document 26 | /// ) { 27 | /// 28 | /// } fetchBlocks: { 29 | /// 30 | /// } onRefresh: { 31 | /// 32 | /// } 33 | /// ``` 34 | /// 35 | struct DocumentView: View { 36 | @Environment(\.colorScheme) private var colorScheme 37 | 38 | /// A binding to the title label 39 | @Binding var title: String 40 | /// The binding to an array of label model objects. 41 | let labels: [Label] 42 | /// All labels for suggestions 43 | let allLabels: [Label] 44 | /// The binding to an array of block model objects. 45 | let blocks: [Block] 46 | /// The logged in user. 47 | let user: User 48 | /// The current selected workspace. 49 | let workspace: Workspace 50 | /// The current selected document within the workspace. 51 | let document: Document 52 | /// The fetch method is called whenver the entire document contents need updating. 53 | let fetch: () -> Void 54 | /// This method is for when only the blocks within the documnet need updating (no title, etc). 55 | let fetchBlocks: () -> Void 56 | /// Called when "pull to refresh" is invoked to refresh the whole documnet. 57 | let onRefresh: () -> Void 58 | /// Call to save the document state (title, etc) whenever the focused state becomes false on title. 59 | let save: (String) -> Void 60 | 61 | @StateObject private var vm = DocumentViewVM() 62 | @FocusState private var isFocused: Bool 63 | @State private var focused: FocusedPrompt = FocusedPrompt(uuid: nil, text: "") 64 | @State private var promptMenuOpen = false 65 | @State private var linkMenuOpen = false 66 | @State private var selectedLink: UUID? = nil 67 | 68 | 69 | private let pad: Double = 30 70 | 71 | var content: some View { 72 | Group { 73 | if DEBUG_VISUAL_LINKED_LIST { 74 | HStack { 75 | LinkedListView(nodes: blocks.map { 76 | return NodeView(id: $0.id, type: $0.type.rawValue, graveyard: $0.graveyard, content: $0.content, prev: $0.prev, next: $0.next) 77 | }) 78 | LinkedListView(nodes: blocks.filter({$0.graveyard == false}).map { 79 | return NodeView(id: $0.id, type: $0.type.rawValue, graveyard: $0.graveyard, content: $0.content, prev: $0.prev, next: $0.next) 80 | }) 81 | } 82 | } 83 | VStack(alignment: .center, spacing: pad) { 84 | HStack() { 85 | VStack(alignment: .leading) { 86 | TextField("", text: $title) 87 | .font(.largeTitle) 88 | .textFieldStyle(.plain) 89 | .focused($isFocused) 90 | .onChange(of: isFocused) { [title] newValue in 91 | if newValue == false { 92 | save(title) 93 | // self.title = title 94 | } 95 | } 96 | .onSubmit { 97 | focused = FocusedPrompt(uuid: blocks.first?.id, text: blocks.first?.content ?? "") 98 | isFocused = false 99 | } 100 | 101 | Spacer() 102 | .frame(height: 20) 103 | LabelField(fetch: fetch, labels: labels, allLabels: allLabels, user: user, workspace: workspace, document: document) 104 | } 105 | .foregroundColor(.primary) 106 | } 107 | HStack() { 108 | BlockViewContainer(user: user, workspace: workspace, document: document, blocks: blocks, promptMenuOpen: $promptMenuOpen, editable: true, selectedLink: .constant(nil), focused: $focused) { 109 | fetch() 110 | } 111 | 112 | Spacer() 113 | } 114 | Spacer() 115 | } 116 | 117 | } 118 | .padding(.trailing, GlobalDimension.toolbarWidth) 119 | } 120 | 121 | var body: some View { 122 | ScrollView { 123 | HorizontalFlexView { 124 | #if os(macOS) 125 | content 126 | .padding(GlobalDimension.toolbarWidth + Spacing.spacing1.rawValue) 127 | #else 128 | content 129 | .padding(.top, Spacing.spacing8.rawValue) 130 | #endif 131 | } 132 | .frame(minHeight: GlobalDimension.minDocumentContentHeight) 133 | .background(colorScheme == .dark ? ColorPalette.darkBG1 : ColorPalette.lightBG1) 134 | } 135 | .onAppear { 136 | vm.fetchDocuments(user: user, workspace: workspace) 137 | save(title) 138 | onRefresh() 139 | } 140 | .onTapGesture { 141 | promptMenuOpen = false 142 | linkMenuOpen = false 143 | } 144 | .overlay { 145 | if promptMenuOpen == true { 146 | PromptMenu { 147 | promptMenuOpen = false 148 | linkMenuOpen = true 149 | } 150 | } else if linkMenuOpen == true { 151 | ContentLinkModal(user: user, workspace: workspace, document: document, documents: vm.documents, selectedLink: $selectedLink, open: $linkMenuOpen) { 152 | do { 153 | let docRepo = DocumentRepo(user: user, workspace: workspace) 154 | if let insertId = focused.uuid, let selectedLink { 155 | if let insertBlock = try docRepo.readBlock(document: document, block: insertId) { 156 | let updatedBlock = Block(id: insertBlock.id, type: .contentLink, content: selectedLink.uuidString, prev: insertBlock.prev, next: insertBlock.next, graveyard: false, createdAt: insertBlock.createdAt, modifiedAt: .now, document: document) 157 | vm.updateBlock(updatedBlock, user: user, workspace: workspace, document: document) 158 | onRefresh() 159 | } 160 | } 161 | } catch let error { 162 | print(error) 163 | } 164 | } 165 | } 166 | } 167 | .onChange(of: linkMenuOpen, perform: { newValue in 168 | // if newValue == false { 169 | // vm.clearPrompt(user: user, workspace: workspace, document: document) 170 | // } 171 | }) 172 | .refreshable { 173 | onRefresh() 174 | } 175 | } 176 | } 177 | 178 | -------------------------------------------------------------------------------- /Shared/Components/SignInView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInView.swift 3 | // Graphnote 4 | // 5 | // Created by Hayden Pennington on 4/2/23. 6 | // 7 | 8 | import SwiftUI 9 | import AuthenticationServices 10 | import CryptoKit 11 | 12 | enum SignInError: LocalizedError { 13 | case signInNoUser 14 | case createUserFailed 15 | case fetchFailed 16 | case unknown 17 | 18 | var errorDescription: String? { 19 | switch self { 20 | case .signInNoUser: 21 | return "There was an issue signing you in.. Please make sure you are connected to the internet. Contact us for support if the issue continues!" 22 | case .createUserFailed: 23 | return "There was an issue signing you in.. (Failed to create user)" 24 | case .fetchFailed: 25 | return "There was an issue signing you in.. (Failed to fetch user)" 26 | case .unknown: 27 | return "There was an issue signing you in.. (Failed for unknown reason)" 28 | } 29 | } 30 | } 31 | 32 | struct SignInView: View { 33 | @Environment(\.colorScheme) private var colorScheme 34 | 35 | let callback: (_ isSignUp: Bool, _ user: User?, _ success: Bool) -> Void 36 | 37 | @State private var welcomeOpacity = 0.0 38 | @State private var getStartedOpacity = 0.0 39 | @State private var signInButtonOpacity = 0.0 40 | @State private var imageSizeScaler = 0.25 41 | @State private var isFailureAlertOpen = false 42 | @State private var isFailureAlertMessage: SignInError? = nil 43 | @State private var currentNonce: String? = nil 44 | 45 | private let duration = 2.0 46 | private let imageWidth = 140.0 47 | private let authService = AuthService() 48 | 49 | init(callback: @escaping (_ isSignUp: Bool, _ user: User?, _ success: Bool) -> Void) { 50 | self.callback = callback 51 | } 52 | 53 | private func randomNonceString(length: Int = 32) -> String { 54 | precondition(length > 0) 55 | var randomBytes = [UInt8](repeating: 0, count: length) 56 | let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes) 57 | if errorCode != errSecSuccess { 58 | fatalError( 59 | "Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)" 60 | ) 61 | } 62 | 63 | let charset: [Character] = 64 | Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") 65 | 66 | let nonce = randomBytes.map { byte in 67 | // Pick a random character from the set, wrapping around if needed. 68 | charset[Int(byte) % charset.count] 69 | } 70 | 71 | return String(nonce) 72 | } 73 | 74 | private func sha256(_ input: String) -> String { 75 | let inputData = Data(input.utf8) 76 | let hashedData = SHA256.hash(data: inputData) 77 | let hashString = hashedData.compactMap { 78 | String(format: "%02x", $0) 79 | }.joined() 80 | 81 | return hashString 82 | } 83 | 84 | var body: some View { 85 | GeometryReader { geometry in 86 | VStack(spacing: Spacing.spacing6.rawValue) { 87 | Group { 88 | Spacer() 89 | Spacer() 90 | } 91 | VStack { 92 | Image("GraphnoteIcon") 93 | .resizable() 94 | .opacity(imageSizeScaler) 95 | .frame(width: imageWidth * imageSizeScaler, height: imageWidth * imageSizeScaler) 96 | }.frame(width: imageWidth, height: imageWidth) 97 | Text("\"Augment your memory\"") 98 | .font(.title3) 99 | .opacity(welcomeOpacity) 100 | Group { 101 | Spacer() 102 | Spacer() 103 | } 104 | Text("Welcome to Graphnote") 105 | .font(.system(size: Spacing.spacing7.rawValue)) 106 | .foregroundColor(LabelColor.primary) 107 | .opacity(welcomeOpacity) 108 | Text("Thanks for using Graphnote. Built in Tennessee with Love.") 109 | .font(.body) 110 | .multilineTextAlignment(.center) 111 | .opacity(welcomeOpacity) 112 | Spacer() 113 | .frame(height: Spacing.spacing6.rawValue) 114 | Text("Sign in to get started.") 115 | .font(.title2) 116 | .opacity(getStartedOpacity) 117 | SignInWithAppleButton { request in 118 | request.requestedScopes = [.fullName, .email] 119 | currentNonce = randomNonceString() 120 | guard let nonce = currentNonce else { 121 | return 122 | } 123 | 124 | request.nonce = sha256(nonce) 125 | } onCompletion: { result in 126 | switch result { 127 | case .success(let authorization): 128 | guard let nonce = currentNonce else { 129 | #if DEBUG 130 | fatalError() 131 | #endif 132 | return 133 | } 134 | 135 | authService.process(authorization: authorization, currentNonce: nonce, callback: { isSignUp, user, error in 136 | if let error { 137 | switch error { 138 | case .createUserFailed: 139 | isFailureAlertMessage = SignInError.createUserFailed 140 | case .fetchFailed: 141 | isFailureAlertMessage = SignInError.fetchFailed 142 | case .userNotFound: 143 | isFailureAlertMessage = SignInError.signInNoUser 144 | case .unknown: 145 | isFailureAlertMessage = SignInError.unknown 146 | } 147 | print(error) 148 | 149 | isFailureAlertOpen = true 150 | callback(isSignUp, nil, false) 151 | } 152 | 153 | callback(isSignUp, user, true) 154 | 155 | }) 156 | 157 | case .failure(let error): 158 | print(error) 159 | callback(false, nil, false) 160 | } 161 | } 162 | .frame(width: 220, height: 40) 163 | .opacity(signInButtonOpacity) 164 | Spacer() 165 | } 166 | .frame(width: geometry.size.width, height: geometry.size.height) 167 | .ignoresSafeArea() 168 | .background(colorScheme == .dark ? ColorPalette.darkBG1 : ColorPalette.lightBG1) 169 | } 170 | .alert(isPresented: $isFailureAlertOpen, error: isFailureAlertMessage, actions: { 171 | Text("OK") 172 | .onTapGesture { 173 | isFailureAlertMessage = nil 174 | } 175 | }) 176 | .onAppear { 177 | withAnimation(.easeInOut(duration: 1)) { 178 | imageSizeScaler = 1.0 179 | } 180 | 181 | withAnimation(.easeOut(duration: duration).delay(1)) { 182 | welcomeOpacity = 1.0 183 | } 184 | 185 | withAnimation(.easeOut(duration: duration).delay(1 + duration / 2)) { 186 | getStartedOpacity = 1.0 187 | signInButtonOpacity = 1.0 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Shared/DB Model/Graphnote.xcdatamodeld/Graphnote.xcdatamodel/contents: -------------------------------------------------------------------------------- 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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /Shared/ContentViewVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentViewVM.swift 3 | // Graphnote (macOS) 4 | // 5 | // Created by Hayden Pennington on 3/19/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class ContentViewVM: NSObject, ObservableObject { 12 | let ALL_ID = UUID() 13 | 14 | @Published var treeItems: [TreeViewItem] = [] 15 | @Published var selectedDocument: Document? = nil { 16 | didSet { 17 | print("SELECTED DOCUMENT DID SET") 18 | } 19 | } 20 | @Published var workspaces: [Workspace]? = nil 21 | 22 | @Published var selectedWorkspace: Workspace? = nil { 23 | didSet { 24 | if selectedWorkspace != oldValue { 25 | if let user = user { 26 | if let workspace = selectedWorkspace { 27 | let documentRepo = DocumentRepo(user: user, workspace: workspace) 28 | if let documents = try? documentRepo.readAll(), let document = documents.first { 29 | selectedDocument = document 30 | if let selectedDocument, let selectedWorkspace { 31 | selectedSubItem = TreeDocumentIdentifier(label: ALL_ID, document: selectedDocument.id, workspace: selectedWorkspace.id) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | @Published var selectedWorkspaceIndex: Int = 0 { 41 | didSet { 42 | if selectedWorkspaceIndex != oldValue { 43 | if let workspaces { 44 | if workspaces.count > selectedWorkspaceIndex { 45 | selectedWorkspace = workspaces[selectedWorkspaceIndex] 46 | fetch() 47 | } 48 | } 49 | } 50 | } 51 | } 52 | @Published var selectedSubItem: TreeDocumentIdentifier? = nil { 53 | didSet { 54 | if selectedSubItem != nil { 55 | if let selectedSubItem, let user, let selectedWorkspace { 56 | let workspaceRepo = WorkspaceRepo(user: user) 57 | if let document = try? workspaceRepo.read(document: selectedSubItem.document, workspace: selectedWorkspace.id) { 58 | self.selectedDocument = document 59 | } 60 | 61 | } 62 | } 63 | } 64 | } 65 | 66 | @Published var user: User? = nil 67 | 68 | @objc 69 | func methodOfReceivedNotification(notification: NSNotification) { 70 | fetch() 71 | } 72 | 73 | func initializeUser() { 74 | if let user = try? UserRepo().readAll()?.first { 75 | self.user = user 76 | } 77 | } 78 | 79 | func initializeUserWorkspaces() { 80 | if let user { 81 | let workspaceRepo = WorkspaceRepo(user: user) 82 | 83 | if let workspaces = try? workspaceRepo.readAll(), let workspace = workspaces.first { 84 | self.selectedWorkspace = selectedWorkspace ?? workspace 85 | self.selectedWorkspaceIndex = selectedWorkspace != nil ? selectedWorkspaceIndex : 0 86 | self.workspaces = self.workspaces ?? workspaces 87 | 88 | NotificationCenter.default.addObserver(self, selector: #selector(self.methodOfReceivedNotification(notification:)), name: Notification.Name(LabelNotification.newLabel.rawValue), object: nil) 89 | } 90 | } 91 | } 92 | 93 | private func updateCurrentWorkspace() { 94 | if let selectedWorkspace, let user { 95 | let workspaceRepo = WorkspaceRepo(user: user) 96 | let updatedWorkpace = try? workspaceRepo.read(workspace: selectedWorkspace.id) 97 | self.selectedWorkspace = updatedWorkpace 98 | } 99 | } 100 | 101 | private func updateCurrentDocument() { 102 | if let selectedDocument, let selectedWorkspace, let user { 103 | let workspaceRepo = WorkspaceRepo(user: user) 104 | let updatedDocument = try? workspaceRepo.read(document: selectedDocument.id, workspace: selectedWorkspace.id) 105 | self.selectedDocument = updatedDocument 106 | } 107 | } 108 | 109 | func addDocument(_ document: Document) -> Bool { 110 | if let user, let selectedWorkspace { 111 | do { 112 | try DataService.shared.createDocument(user: user, document: document) 113 | let now = Date.now 114 | let prompt = Block(id: UUID(), type: .body, content: "", prev: nil, next: nil, graveyard: false, createdAt: now, modifiedAt: now, document: document) 115 | if try DataService.shared.createBlock(user: user, workspace: selectedWorkspace, document: document, block: prompt, prev: nil, next: nil) == nil { 116 | return false 117 | } 118 | 119 | DataService.shared.updateDocumentFocused(user: user, workspace: selectedWorkspace, document: document, focused: prompt.id) 120 | 121 | if let selected = selectedSubItem { 122 | selectedSubItem = TreeDocumentIdentifier(label: selected.label, document: document.id, workspace: selectedWorkspace.id) 123 | } 124 | return true 125 | } catch let error { 126 | print(error) 127 | return false 128 | } 129 | 130 | } else { 131 | return false 132 | } 133 | } 134 | 135 | func fetchDocument() { 136 | if selectedDocument != nil { 137 | updateCurrentDocument() 138 | } 139 | } 140 | 141 | func fetch() { 142 | if let user { 143 | 144 | let workspaceRepo = WorkspaceRepo(user: user) 145 | 146 | if let workspaces = try? workspaceRepo.readAll() { 147 | self.workspaces = workspaces 148 | if workspaces.count > selectedWorkspaceIndex { 149 | let workspace = workspaces[selectedWorkspaceIndex] 150 | self.workspaces = workspaces 151 | if selectedWorkspace != nil { 152 | updateCurrentWorkspace() 153 | } 154 | 155 | 156 | let curWorkspace = self.selectedWorkspace ?? workspace 157 | 158 | self.selectedWorkspace = curWorkspace 159 | 160 | treeItems = curWorkspace.labels.map({ label in 161 | 162 | let workspaceRepo = WorkspaceRepo(user: user) 163 | let labelLinks = try? workspaceRepo.readLabelLinks(workspace: curWorkspace).filter { 164 | return $0.label == label.id 165 | } 166 | 167 | let subItems = labelLinks?.compactMap { labelLink in 168 | do { 169 | if let document = try workspaceRepo.read(document: labelLink.document, workspace: labelLink.workspace) { 170 | return TreeViewSubItem(id: document.id, title: document.title) 171 | } 172 | } catch let error { 173 | print(error) 174 | } 175 | 176 | return nil 177 | } 178 | 179 | return TreeViewItem(id: label.id, workspace: workspace.id, title: label.title, color: label.color.getColor(), subItems: subItems) 180 | }).sorted(by: { lhs, rhs in 181 | lhs.title < rhs.title 182 | }) 183 | 184 | treeItems.append( 185 | TreeViewItem(id: ALL_ID, workspace: workspace.id, title: "ALL", color: Color.gray, subItems: curWorkspace.documents.map { 186 | TreeViewSubItem(id: $0.id, title: $0.title) 187 | }) 188 | ) 189 | } 190 | } else { 191 | if let selectedWorkspace { 192 | treeItems.append( 193 | TreeViewItem(id: ALL_ID, workspace: selectedWorkspace.id, title: "ALL", color: Color.gray, subItems: nil) 194 | ) 195 | } 196 | } 197 | } else { 198 | if let selectedWorkspace { 199 | treeItems.append( 200 | TreeViewItem(id: ALL_ID, workspace: selectedWorkspace.id, title: "ALL", color: Color.gray, subItems: nil) 201 | ) 202 | } 203 | 204 | } 205 | } 206 | } 207 | --------------------------------------------------------------------------------