diff --git a/macos/Thoughts-iOS/Assets.xcassets/AccentColor.colorset/Contents.json b/macos/Thoughts-iOS/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..8f98b1d --- /dev/null +++ b/macos/Thoughts-iOS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.945", + "green" : "0.757", + "red" : "0.333" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.898", + "green" : "0.557", + "red" : "0.251" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Thoughts-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Thoughts-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..b3f06f6 --- /dev/null +++ b/macos/Thoughts-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,36 @@ +{ + "images" : [ + { + "filename" : "icon_1024x1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Thoughts-iOS/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png b/macos/Thoughts-iOS/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png new file mode 100644 index 0000000..83346b2 Binary files /dev/null and b/macos/Thoughts-iOS/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png differ diff --git a/macos/Thoughts-iOS/Assets.xcassets/Contents.json b/macos/Thoughts-iOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/macos/Thoughts-iOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Thoughts-iOS/ThoughtsApp.swift b/macos/Thoughts-iOS/ThoughtsApp.swift new file mode 100644 index 0000000..4d71b5d --- /dev/null +++ b/macos/Thoughts-iOS/ThoughtsApp.swift @@ -0,0 +1,106 @@ +// Copyright (c) 2021-2026 Jason Morley +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +import ThoughtsCore + +@main +struct ThoughtsApp: App { + + enum SheetType: Identifiable { + + var id: Self { + return self + } + + case introduction + } + + class ModelDelegate: NSObject, ApplicationModelDelegate { + + func showIntroduction(applicationModel: ApplicationModel) { + } + + func showUpdateAlert(applicationModel: ApplicationModel) { + } + + func showThought(applicationModel: ApplicationModel) { + } + + } + + @Environment(\.scenePhase) private var scenePhase + + @State private var sheet: SheetType? + + var applicationModel: ApplicationModel + var modelDelegate: ModelDelegate + + init() { + let applicationModel = ApplicationModel() + let modelDelegate = ModelDelegate() + applicationModel.delegate = modelDelegate + self.applicationModel = applicationModel + self.modelDelegate = modelDelegate + self.applicationModel.start() + } + + var body: some Scene { + WindowGroup { + NavigationView { + ContentView(applicationModel: applicationModel) + .navigationBarTitleDisplayMode(.inline) + .sheet(item: $sheet) { sheet in + switch sheet { + case .introduction: + NavigationView { + IntroductionView(applicationModel: applicationModel) + } + } + } + } + .environment(applicationModel) + } + .onChange(of: applicationModel.didShowIntroduction, initial: true) { _, didShowIntroduction in + guard !didShowIntroduction else { + return + } + sheet = .introduction + } + .onChange(of: scenePhase) { _, scenePhase in + switch scenePhase { + case .background, .inactive: + applicationModel.lastBackgroundDate = min(Date(), applicationModel.lastBackgroundDate) + case .active: + let backgroundDuration = applicationModel.lastBackgroundDate.timeIntervalSinceNow * -1 + print("Opened after \(backgroundDuration) seconds.") + guard backgroundDuration > 60 * 5 else { + break + } + print("Creating new thought...") + applicationModel.lastBackgroundDate = Date.distantFuture + applicationModel.new() + @unknown default: + break + } + } + } +} diff --git a/macos/Thoughts-iOS/Views/IntroductionView.swift b/macos/Thoughts-iOS/Views/IntroductionView.swift new file mode 100644 index 0000000..7a29806 --- /dev/null +++ b/macos/Thoughts-iOS/Views/IntroductionView.swift @@ -0,0 +1,40 @@ +// Copyright (c) 2021-2026 Jason Morley +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI +import ThoughtsCore + +struct IntroductionView: View { + + @Environment(\.dismiss) private var dismiss + + let applicationModel: ApplicationModel + + var body: some View { + Button("Done") { + applicationModel.introductionVersion = ApplicationModel.introductionVersion + applicationModel.new() + dismiss() + } + .navigationTitle("Introduction") + .interactiveDismissDisabled() + } + +} diff --git a/macos/Thoughts.xcodeproj/project.pbxproj b/macos/Thoughts.xcodeproj/project.pbxproj index f49c3b6..eeddd26 100644 --- a/macos/Thoughts.xcodeproj/project.pbxproj +++ b/macos/Thoughts.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ D804FB112C081A2F004F8666 /* FrontmatterSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D804FB102C081A2F004F8666 /* FrontmatterSwift */; }; + D818BD022FD163FE0028C860 /* ThoughtsCore in Frameworks */ = {isa = PBXBuildFile; productRef = D818BD012FD163FE0028C860 /* ThoughtsCore */; }; D8296CEE2E29EBBF005088B6 /* ThoughtsCore in Frameworks */ = {isa = PBXBuildFile; productRef = D8296CED2E29EBBF005088B6 /* ThoughtsCore */; }; D87707B52C01592400362786 /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = D87707B42C01592400362786 /* HotKey */; }; D8C283E92BD9954F00161C82 /* HashRainbow in Frameworks */ = {isa = PBXBuildFile; productRef = D8C283E82BD9954F00161C82 /* HashRainbow */; }; @@ -32,6 +33,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + D818BCF62FD1616E0028C860 /* Thoughts-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Thoughts-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D8296CEB2E29EB48005088B6 /* ThoughtsCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ThoughtsCore; sourceTree = ""; }; D852AEFE2B6027CA00B77A3D /* Thoughts.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Thoughts.app; sourceTree = BUILT_PRODUCTS_DIR; }; D852AF0F2B6027CD00B77A3D /* ThoughtsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ThoughtsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -52,9 +54,18 @@ D8184A6F2E1D278900D46CE7 /* ThoughtsUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ThoughtsUITests; sourceTree = ""; }; D8184A782E1D278C00D46CE7 /* ThoughtsTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ThoughtsTests; sourceTree = ""; }; D8184ABB2E1D27A200D46CE7 /* Thoughts */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (D8184AED2E1D27A200D46CE7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Thoughts; sourceTree = ""; }; + D818BCF72FD1616E0028C860 /* Thoughts-iOS */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "Thoughts-iOS"; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + D818BCF32FD1616E0028C860 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D818BD022FD163FE0028C860 /* ThoughtsCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D852AEFB2B6027CA00B77A3D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -91,6 +102,7 @@ D8184ABB2E1D27A200D46CE7 /* Thoughts */, D8184A782E1D278C00D46CE7 /* ThoughtsTests */, D8184A6F2E1D278900D46CE7 /* ThoughtsUITests */, + D818BCF72FD1616E0028C860 /* Thoughts-iOS */, D852AEFF2B6027CA00B77A3D /* Products */, D8C283EA2BD9955D00161C82 /* Frameworks */, ); @@ -102,6 +114,7 @@ D852AEFE2B6027CA00B77A3D /* Thoughts.app */, D852AF0F2B6027CD00B77A3D /* ThoughtsTests.xctest */, D852AF192B6027CD00B77A3D /* ThoughtsUITests.xctest */, + D818BCF62FD1616E0028C860 /* Thoughts-iOS.app */, ); name = Products; sourceTree = ""; @@ -116,6 +129,29 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + D818BCF52FD1616E0028C860 /* Thoughts-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D818BCFE2FD1616F0028C860 /* Build configuration list for PBXNativeTarget "Thoughts-iOS" */; + buildPhases = ( + D818BCF22FD1616E0028C860 /* Sources */, + D818BCF32FD1616E0028C860 /* Frameworks */, + D818BCF42FD1616E0028C860 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + D818BCF72FD1616E0028C860 /* Thoughts-iOS */, + ); + name = "Thoughts-iOS"; + packageProductDependencies = ( + D818BD012FD163FE0028C860 /* ThoughtsCore */, + ); + productName = "Thoughts-iOS"; + productReference = D818BCF62FD1616E0028C860 /* Thoughts-iOS.app */; + productType = "com.apple.product-type.application"; + }; D852AEFD2B6027CA00B77A3D /* Thoughts */ = { isa = PBXNativeTarget; buildConfigurationList = D852AF232B6027CD00B77A3D /* Build configuration list for PBXNativeTarget "Thoughts" */; @@ -192,9 +228,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1520; + LastSwiftUpdateCheck = 2650; LastUpgradeCheck = 2650; TargetAttributes = { + D818BCF52FD1616E0028C860 = { + CreatedOnToolsVersion = 26.5; + }; D852AEFD2B6027CA00B77A3D = { CreatedOnToolsVersion = 15.2; }; @@ -230,11 +269,19 @@ D852AEFD2B6027CA00B77A3D /* Thoughts */, D852AF0E2B6027CD00B77A3D /* ThoughtsTests */, D852AF182B6027CD00B77A3D /* ThoughtsUITests */, + D818BCF52FD1616E0028C860 /* Thoughts-iOS */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + D818BCF42FD1616E0028C860 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D852AEFC2B6027CA00B77A3D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -259,6 +306,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + D818BCF22FD1616E0028C860 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D852AEFA2B6027CA00B77A3D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -296,6 +350,87 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + D818BCFF2FD1616F0028C860 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = QS82QFHKWB; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Thoughts; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Thoughts needs access to your location to save it in notes metadata."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = uk.co.jbmorley.thoughts.apps.appstore; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + D818BD002FD1616F0028C860 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = QS82QFHKWB; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Thoughts; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Thoughts needs access to your location to save it in notes metadata."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = uk.co.jbmorley.thoughts.apps.appstore; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; D852AF212B6027CD00B77A3D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -565,6 +700,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + D818BCFE2FD1616F0028C860 /* Build configuration list for PBXNativeTarget "Thoughts-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D818BCFF2FD1616F0028C860 /* Debug */, + D818BD002FD1616F0028C860 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D852AEF92B6027CA00B77A3D /* Build configuration list for PBXProject "Thoughts" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -638,6 +782,10 @@ isa = XCSwiftPackageProductDependency; productName = FrontmatterSwift; }; + D818BD012FD163FE0028C860 /* ThoughtsCore */ = { + isa = XCSwiftPackageProductDependency; + productName = ThoughtsCore; + }; D8296CED2E29EBBF005088B6 /* ThoughtsCore */ = { isa = XCSwiftPackageProductDependency; productName = ThoughtsCore; diff --git a/macos/Thoughts/ThoughtsApp.swift b/macos/Thoughts/ThoughtsApp.swift index 1748ddb..637099d 100644 --- a/macos/Thoughts/ThoughtsApp.swift +++ b/macos/Thoughts/ThoughtsApp.swift @@ -53,21 +53,6 @@ struct ThoughtsApp: App { applicationModel.suppressUpdateCheck = suppressionState == .on } - func setRootURL(applicationModel: ApplicationModel) -> Bool { - dispatchPrecondition(condition: .onQueue(.main)) - let openPanel = NSOpenPanel() - openPanel.canChooseFiles = false - openPanel.canChooseDirectories = true - openPanel.canCreateDirectories = true - guard openPanel.runModal() == NSApplication.ModalResponse.OK, - let url = openPanel.url else { - return false - } - applicationModel.rootURL = url - applicationModel.document = Document() - return true - } - func showThought(applicationModel: ApplicationModel) { NSWorkspace.shared.open(.compose) } diff --git a/macos/Thoughts/Views/Introduction/IntroductionView.swift b/macos/Thoughts/Views/Introduction/IntroductionView.swift index 37523e1..aa2ff3c 100644 --- a/macos/Thoughts/Views/Introduction/IntroductionView.swift +++ b/macos/Thoughts/Views/Introduction/IntroductionView.swift @@ -78,11 +78,9 @@ struct IntroductionView: View { FinderPreview() } } actions: { - Button("Set Destination Folder") { - if applicationModel.setRootURL() { - withAnimation { - self.page = .location - } + SetNotesFolderButton { + withAnimation { + self.page = .location } } .keyboardShortcut(.defaultAction) diff --git a/macos/ThoughtsCore/Sources/ThoughtsCore/Extensions/FileManager.swift b/macos/ThoughtsCore/Sources/ThoughtsCore/Extensions/FileManager.swift index ad45d23..a1b1cab 100644 --- a/macos/ThoughtsCore/Sources/ThoughtsCore/Extensions/FileManager.swift +++ b/macos/ThoughtsCore/Sources/ThoughtsCore/Extensions/FileManager.swift @@ -89,4 +89,8 @@ extension FileManager { return files } + public func fileExists(at url: URL) -> Bool { + return fileExists(atPath: url.path) + } + } diff --git a/macos/ThoughtsCore/Sources/ThoughtsCore/Extensions/Sequence.swift b/macos/ThoughtsCore/Sources/ThoughtsCore/Extensions/Sequence.swift index 46b3b8e..468922d 100644 --- a/macos/ThoughtsCore/Sources/ThoughtsCore/Extensions/Sequence.swift +++ b/macos/ThoughtsCore/Sources/ThoughtsCore/Extensions/Sequence.swift @@ -46,7 +46,7 @@ let secondaryBackground = NSColor.windowBackgroundColor let lighterColor = NSColor.lightGray let textColor = NSColor.labelColor #else -let defaultEditorFont = UIFont.preferredFont(forTextStyle: .body) +let defaultEditorFont = UIFont.monospacedSystemFont(ofSize: UIFont.systemFontSize, weight: .regular) let codeFont = UIFont.monospacedSystemFont(ofSize: UIFont.systemFontSize, weight: .thin) let headingTraits: UIFontDescriptor.SymbolicTraits = [.traitBold, .traitExpanded] let boldTraits: UIFontDescriptor.SymbolicTraits = [.traitBold] diff --git a/macos/ThoughtsCore/Sources/ThoughtsCore/Model/ApplicationModel.swift b/macos/ThoughtsCore/Sources/ThoughtsCore/Model/ApplicationModel.swift index e059f67..1c3bab1 100644 --- a/macos/ThoughtsCore/Sources/ThoughtsCore/Model/ApplicationModel.swift +++ b/macos/ThoughtsCore/Sources/ThoughtsCore/Model/ApplicationModel.swift @@ -33,11 +33,16 @@ public protocol ApplicationModelDelegate: AnyObject { func showIntroduction(applicationModel: ApplicationModel) func showUpdateAlert(applicationModel: ApplicationModel) - func setRootURL(applicationModel: ApplicationModel) -> Bool func showThought(applicationModel: ApplicationModel) } +extension URL { + + static let rootBookmarkURL: URL = URL.applicationSupportDirectory.appendingPathComponent("RootBookmark") + +} + @Observable public class ApplicationModel: NSObject, @unchecked Sendable { @@ -46,6 +51,8 @@ public class ApplicationModel: NSObject, @unchecked Sendable { case shouldSaveLocation case introductionVersion case suppressUpdateCheck + case lastBackgroundDate + case lastDocumentDate } public static let introductionVersion = 1 @@ -56,14 +63,26 @@ public class ApplicationModel: NSObject, @unchecked Sendable { @MainActor public var rootURL: URL? { didSet { - rootURLChanges.send(rootURL) - reloadLibrary() - do { + if let rootURL { + guard rootURL.startAccessingSecurityScopedResource() else { + return + } + rootURLChanges.send(rootURL) + reloadLibrary() + do { #if os(macOS) - try keyedDefaults.set(securityScopedURL: rootURL, forKey: .rootURL) + try keyedDefaults.set(securityScopedURL: rootURL, forKey: .rootURL) +#else + try URL.writeBookmarkData(try rootURL.bookmarkData(options: .suitableForBookmarkFile, + includingResourceValuesForKeys: nil, + relativeTo: nil), + to: .rootBookmarkURL) #endif - } catch { - print("Failed to save bookmark data with error \(error).") + } catch { + print("Failed to save bookmark data with error \(error).") + } + } else { + // TODO: Remove the default. } } } @@ -92,8 +111,21 @@ public class ApplicationModel: NSObject, @unchecked Sendable { } } + @MainActor public var lastBackgroundDate: Date { + didSet { + keyedDefaults.set(lastBackgroundDate, forKey: .lastBackgroundDate) + } + } + + @MainActor public var lastDocumentDate: Date? { + didSet { + keyedDefaults.set(lastDocumentDate, forKey: .lastDocumentDate) + } + } + @MainActor public var document = Document() { didSet { + lastDocumentDate = document.date documentChanges.send(document) } } @@ -133,17 +165,54 @@ public class ApplicationModel: NSObject, @unchecked Sendable { private let storeUpdateChecker = StoreUpdateChecker() @MainActor public override init() { + + // Ensure the Application Support directory exists. + let fileManager = FileManager.default + if !fileManager.fileExists(at: .applicationSupportDirectory) { + try! FileManager.default.createDirectory(at: .applicationSupportDirectory, withIntermediateDirectories: true) + } + + // Load the root URL. #if os(macOS) - rootURL = try? keyedDefaults.securityScopedURL(forKey: .rootURL) + _rootURL = try? keyedDefaults.securityScopedURL(forKey: .rootURL) +#else + if fileManager.fileExists(at: .rootBookmarkURL), + let data = try? URL.bookmarkData(withContentsOf: .rootBookmarkURL) { + var isStale = true + _rootURL = try? URL(resolvingBookmarkData: data, bookmarkDataIsStale: &isStale) + _ = _rootURL?.startAccessingSecurityScopedResource() + } #endif + + // Load the other settings. shouldSaveLocation = keyedDefaults.bool(forKey: .shouldSaveLocation, default: false) introductionVersion = keyedDefaults.integer(forKey: .introductionVersion, default: 0) suppressUpdateCheck = keyedDefaults.bool(forKey: .suppressUpdateCheck, default: false) + lastBackgroundDate = keyedDefaults.object(forKey: .lastBackgroundDate) as? Date ?? Date.distantFuture + lastDocumentDate = keyedDefaults.object(forKey: .lastDocumentDate) as? Date + super.init() rootURLChanges.send(rootURL) locationManager.delegate = self locationManager.pausesLocationUpdatesAutomatically = false storeUpdateChecker.delegate = self + + // Reload the last document if necessary. + if let lastDocumentDate, + let fileURL = rootURL?.appendingPathComponent(Self.filename(for: lastDocumentDate)), + let document = Document(contentsOf: fileURL) + { + self.document = document + } else { + new() + } + } + + public static func filename(for date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd-HH-mm-ss" + formatter.timeZone = .gmt + return formatter.string(from: date).appending(".md") } @MainActor public func start() { @@ -164,7 +233,8 @@ public class ApplicationModel: NSObject, @unchecked Sendable { // Write the changes to disk. do { - try document.sync(to: rootURL) + let fileURL = rootURL.appendingPathComponent(Self.filename(for: document.date)) + try document.sync(to: fileURL) } catch { print("Failed to save file with error '\(error)'.") } @@ -249,10 +319,6 @@ public class ApplicationModel: NSObject, @unchecked Sendable { library?.start() } - @MainActor public func setRootURL() -> Bool { - return delegate?.setRootURL(applicationModel: self) ?? false - } - } extension ApplicationModel: CLLocationManagerDelegate { diff --git a/macos/ThoughtsCore/Sources/ThoughtsCore/Model/Document.swift b/macos/ThoughtsCore/Sources/ThoughtsCore/Model/Document.swift index 6c1df31..61d1009 100644 --- a/macos/ThoughtsCore/Sources/ThoughtsCore/Model/Document.swift +++ b/macos/ThoughtsCore/Sources/ThoughtsCore/Model/Document.swift @@ -23,8 +23,11 @@ import Foundation import Yams +import FrontmatterSwift + public struct Document { + // TODO: Store the timezone. public var date: Date public var content: String public var tags: [String] @@ -34,6 +37,39 @@ public struct Document { return content.isEmpty } + public init?(contentsOf url: URL) { + + guard FileManager.default.fileExists(at: url) else { + return nil + } + + var options = DecodeOptions() + options.detectDates = false + guard let frontmatterDocument = try? FrontmatterDocument(contentsOf: url, options: options) else { + return nil + } + + // TODO: Location should be optional. + // TODO: We should preserve other metadata. + // TODO: Custom date handling might not be necessary with `FrontmatterDocument`. + guard let tags = frontmatterDocument.metadata["tags"] as? [String], + let location = frontmatterDocument.metadata["location"] as? [String: Any], + let locality = location["locality"] as? String, + let name = location["name"] as? String, + let longitude = location["longitude"] as? CLLocationDegrees, + let latitude = location["latitude"] as? CLLocationDegrees, + let dateString = frontmatterDocument.metadata["date"] as? String, + let date = try? RegionalDate(string: dateString) + else { + return nil + } + + self.location = LocationDetails(latitude: latitude, longitude: longitude, name: name, locality: locality) + self.date = date.date + self.tags = tags + self.content = frontmatterDocument.content + } + public init(date: Date = Date()) { self.date = date self.content = "" @@ -54,12 +90,7 @@ public struct Document { } } - public func sync(to rootURL: URL) throws { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd-HH-mm-ss" - formatter.timeZone = .gmt - let filename = formatter.string(from: date) - let url = rootURL.appendingPathComponent(filename).appendingPathExtension("md") + public func sync(to url: URL) throws { if isEmpty { let fileManager = FileManager.default if fileManager.fileExists(atPath: url.path) { diff --git a/macos/ThoughtsCore/Sources/ThoughtsCore/Model/RegionalDate.swift b/macos/ThoughtsCore/Sources/ThoughtsCore/Model/RegionalDate.swift index f417109..55a421d 100644 --- a/macos/ThoughtsCore/Sources/ThoughtsCore/Model/RegionalDate.swift +++ b/macos/ThoughtsCore/Sources/ThoughtsCore/Model/RegionalDate.swift @@ -43,10 +43,7 @@ public struct RegionalDate: Codable, Equatable { self.timeZone = timeZone } - public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let string = try container.decode(String.self) - + public init(string: String) throws { let dateFormatter = ISO8601DateFormatter() guard let date = dateFormatter.date(from: string) else { throw ThoughtsError.encodingError @@ -55,6 +52,12 @@ public struct RegionalDate: Codable, Equatable { self.timeZone = TimeZone(iso8601: string) ?? .gmt } + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + try self.init(string: string) + } + public func encode(to encoder: any Encoder) throws { let dateFormatter = ISO8601DateFormatter() dateFormatter.timeZone = timeZone diff --git a/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/ComposeView.swift b/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/ComposeView.swift index 5afe940..0719978 100644 --- a/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/ComposeView.swift +++ b/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/ComposeView.swift @@ -53,8 +53,8 @@ struct ComposeView: View { @Bindable var applicationModel = applicationModel VStack(spacing: 0) { HighlightedTextEditor(text: $applicationModel.document.content, highlightRules: .thoughtsMarkdown) - .frame(minWidth: 400) - .edgesIgnoringSafeArea(.all) +// .frame(minWidth: 400) +// .edgesIgnoringSafeArea(.all) .focused($focus, equals: .text) Divider() TagField("Add tags...", tokens: $applicationModel.document.tags) { candidate, tags in @@ -79,7 +79,7 @@ struct ComposeView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.background) .navigationTitle(applicationModel.document.date.formatted(date: .complete, time: .standard)) - .navigationSubtitle(applicationModel.document.location?.summary ?? "") + .navigationSubtitle(applicationModel.document.location?.summary ?? "-") .onOpenURL { url in switch url { case .compose: diff --git a/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/ContentView.swift b/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/ContentView.swift index e7b025c..c53afde 100644 --- a/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/ContentView.swift +++ b/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/ContentView.swift @@ -48,11 +48,7 @@ public struct ContentView: View { Label("No Folder Set", systemImage: "folder") } description: { Text("Select a folder to store your notes.") - Button { - _ = applicationModel.setRootURL() - } label: { - Text("Set Notes Folder") - } + SetNotesFolderButton() } } } @@ -68,6 +64,21 @@ public struct ContentView: View { } .disabled(applicationModel.rootURL == nil) } +#if os(iOS) + ToolbarItem { + Button { + applicationModel.new() + } label: { + Label("New", systemImage: "document.badge.plus") + } + } + ToolbarItem(placement: .navigationBarLeading) { + Button { + } label: { + Label("Settings", systemImage: "gear") + } + } +#endif } } diff --git a/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/SetNotesFolderButton.swift b/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/SetNotesFolderButton.swift new file mode 100644 index 0000000..c5bd46b --- /dev/null +++ b/macos/ThoughtsCore/Sources/ThoughtsCore/Views/Compose/SetNotesFolderButton.swift @@ -0,0 +1,48 @@ +// Copyright (c) 2021-2026 Jason Morley +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +public struct SetNotesFolderButton: View { + + @Environment(ApplicationModel.self) private var applicationModel + + @State var showFileImporter = false + + let completion: () -> Void + + public init(completion: @escaping () -> Void = {}) { + self.completion = completion + } + + public var body: some View { + Button("Set Notes Folder") { + showFileImporter = true + } + .fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.folder]) { result in + guard case .success(let url) = result else { + return + } + applicationModel.rootURL = url + completion() + } + } + +} diff --git a/macos/dependencies/FrontmatterSwift b/macos/dependencies/FrontmatterSwift index 0d5d493..6e5b062 160000 --- a/macos/dependencies/FrontmatterSwift +++ b/macos/dependencies/FrontmatterSwift @@ -1 +1 @@ -Subproject commit 0d5d49396109b2a4f0f5c52aec993ef50b611ded +Subproject commit 6e5b06291bee4a0b8ffdd2b992479ee8bbbc7fde