├── 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 | 
3 |
4 | # Graphnote
5 |
6 | "Augment your memory"
7 |
8 | Graphnote Client for iOS and macOS
9 |
10 | 
11 |
12 | 
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 | 
39 |
40 | #### Push queue flow
41 | 
42 |
43 | #### Pull queue flow
44 | 
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 |
--------------------------------------------------------------------------------