From 857d273a9d8921172103445a214224ce84a5be32 Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Tue, 11 Jun 2024 17:51:45 -1000 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=A7=99=E2=80=8D=E2=99=82=EF=B8=8F?= =?UTF-8?q?=20Introduction=20flow=20to=20help=20users=20get=20started=20(#?= =?UTF-8?q?90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Thoughts.xcodeproj/project.pbxproj | 108 +++++++++++- .../AccentColor.colorset/Contents.json | 27 +++ Thoughts/Extensions/AnyTransition.swift | 41 +++++ Thoughts/Extensions/EdgeInsets.swift | 33 ++++ Thoughts/Model/ApplicationModel.swift | 39 ++++- Thoughts/Model/ThoughtsError.swift | 1 + Thoughts/Modifiers/PreviewModifier.swift | 50 ++++++ Thoughts/Modifiers/SelectionModifier.swift | 53 ++++++ .../Views/{ => Compose}/ComposeView.swift | 8 - .../Views/{ => Compose}/ComposeWindow.swift | 0 .../Views/{ => Compose}/ContentView.swift | 5 +- .../Views/Introduction/FinderPreview.swift | 73 ++++++++ .../Views/Introduction/IntroductionView.swift | 162 ++++++++++++++++++ .../Views/Introduction/KeyboardPreview.swift | 89 ++++++++++ .../Views/Introduction/LocationPreview.swift | 45 +++++ .../Views/Introduction/MarketingView.swift | 114 ++++++++++++ Thoughts/Views/Introduction/MenuPreview.swift | 123 +++++++++++++ .../Introduction/NSIntroductionWindow.swift | 34 ++++ Thoughts/Views/Introduction/Page.swift | 33 ++++ Thoughts/Views/Introduction/Pager.swift | 53 ++++++ .../Views/Introduction/SymbolHeader.swift | 39 +++++ Thoughts/Views/{ => Menu}/MainMenu.swift | 10 ++ .../Views/{ => Settings}/SettingsView.swift | 2 +- .../Views/{ => Settings}/SettingsWindow.swift | 0 24 files changed, 1118 insertions(+), 24 deletions(-) create mode 100644 Thoughts/Extensions/AnyTransition.swift create mode 100644 Thoughts/Extensions/EdgeInsets.swift create mode 100644 Thoughts/Modifiers/PreviewModifier.swift create mode 100644 Thoughts/Modifiers/SelectionModifier.swift rename Thoughts/Views/{ => Compose}/ComposeView.swift (92%) rename Thoughts/Views/{ => Compose}/ComposeWindow.swift (100%) rename Thoughts/Views/{ => Compose}/ContentView.swift (91%) create mode 100644 Thoughts/Views/Introduction/FinderPreview.swift create mode 100644 Thoughts/Views/Introduction/IntroductionView.swift create mode 100644 Thoughts/Views/Introduction/KeyboardPreview.swift create mode 100644 Thoughts/Views/Introduction/LocationPreview.swift create mode 100644 Thoughts/Views/Introduction/MarketingView.swift create mode 100644 Thoughts/Views/Introduction/MenuPreview.swift create mode 100644 Thoughts/Views/Introduction/NSIntroductionWindow.swift create mode 100644 Thoughts/Views/Introduction/Page.swift create mode 100644 Thoughts/Views/Introduction/Pager.swift create mode 100644 Thoughts/Views/Introduction/SymbolHeader.swift rename Thoughts/Views/{ => Menu}/MainMenu.swift (86%) rename Thoughts/Views/{ => Settings}/SettingsView.swift (97%) rename Thoughts/Views/{ => Settings}/SettingsWindow.swift (100%) diff --git a/Thoughts.xcodeproj/project.pbxproj b/Thoughts.xcodeproj/project.pbxproj index 0f6adbf..b262416 100644 --- a/Thoughts.xcodeproj/project.pbxproj +++ b/Thoughts.xcodeproj/project.pbxproj @@ -11,12 +11,22 @@ D80E08A12B675AAC0023DD4F /* ComposeWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80E08A02B675AAC0023DD4F /* ComposeWindow.swift */; }; D80E08A62B6761370023DD4F /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80E08A52B6761370023DD4F /* Document.swift */; }; D828CFF82BEECDDF00AC8E8B /* highlightedtexteditor-license in Resources */ = {isa = PBXBuildFile; fileRef = D86C389D2BEECDAA00248CD0 /* highlightedtexteditor-license */; }; + D82ED7912C1924B100BC0BA8 /* PreviewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82ED7902C1924B100BC0BA8 /* PreviewModifier.swift */; }; + D82ED7932C1935E700BC0BA8 /* EdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82ED7922C1935E700BC0BA8 /* EdgeInsets.swift */; }; + D82ED7962C19378500BC0BA8 /* SelectionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82ED7952C19378500BC0BA8 /* SelectionModifier.swift */; }; + D83943E02C18FDDF00EB902E /* KeyboardPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83943DF2C18FDDF00EB902E /* KeyboardPreview.swift */; }; + D83943E62C191ED900EB902E /* SymbolHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83943E52C191ED900EB902E /* SymbolHeader.swift */; }; + D83943E82C191F3900EB902E /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83943E72C191F3900EB902E /* Page.swift */; }; + D83943EA2C191FA900EB902E /* Pager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83943E92C191FA900EB902E /* Pager.swift */; }; + D83943EC2C1920F200EB902E /* LocationPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83943EB2C1920F200EB902E /* LocationPreview.swift */; }; + D83943EE2C1921F100EB902E /* FinderPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83943ED2C1921F100EB902E /* FinderPreview.swift */; }; D83C2F882B857B8C00CE60F7 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83C2F872B857B8C00CE60F7 /* URL.swift */; }; D83F58092C0968F300EF69D3 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83F58082C0968F300EF69D3 /* Trie.swift */; }; D83F580B2C096EA300EF69D3 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83F580A2C096EA300EF69D3 /* UTType.swift */; }; D83F580D2C096F5A00EF69D3 /* TrieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83F580C2C096F5A00EF69D3 /* TrieTests.swift */; }; D84010C52BD74C760049715E /* yams-license in Resources */ = {isa = PBXBuildFile; fileRef = D84010C42BD74C760049715E /* yams-license */; }; D840967F2BD73CBE00926C85 /* LocationDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = D840967E2BD73CBE00926C85 /* LocationDetails.swift */; }; + D847E9332C1105A800FD6B63 /* AnyTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D847E9322C1105A800FD6B63 /* AnyTransition.swift */; }; D850C2D42B69D85400338546 /* KeyedDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D850C2D32B69D85400338546 /* KeyedDefaults.swift */; }; D850C2D62B69D8D500338546 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D850C2D52B69D8D500338546 /* ContentView.swift */; }; D852AF022B6027CA00B77A3D /* ThoughtsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852AF012B6027CA00B77A3D /* ThoughtsApp.swift */; }; @@ -47,9 +57,13 @@ D89224A42BD7368F00DA0932 /* TimeZoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89224A32BD7368F00DA0932 /* TimeZoneTests.swift */; }; D89224A72BD736B900DA0932 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89224A62BD736B900DA0932 /* Date.swift */; }; D89224A92BD736DF00DA0932 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89224A82BD736DF00DA0932 /* TimeZone.swift */; }; + D8B1466D2C1290200063E81F /* MenuPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B1466C2C1290200063E81F /* MenuPreview.swift */; }; D8BA4D492BE59E16001A2A26 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = D8BA4D482BE59E16001A2A26 /* HighlightedTextEditor */; }; D8BA4D4D2BE5A9CB001A2A26 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BA4D4C2BE5A9CB001A2A26 /* SettingsWindow.swift */; }; D8C283E92BD9954F00161C82 /* HashRainbow in Frameworks */ = {isa = PBXBuildFile; productRef = D8C283E82BD9954F00161C82 /* HashRainbow */; }; + D8CEE50A2C0A64FF00D2025A /* IntroductionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CEE5092C0A64FF00D2025A /* IntroductionView.swift */; }; + D8CEE50C2C0A66FD00D2025A /* NSIntroductionWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CEE50B2C0A66FD00D2025A /* NSIntroductionWindow.swift */; }; + D8CEE50E2C0A676900D2025A /* MarketingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CEE50D2C0A676900D2025A /* MarketingView.swift */; }; D8DFFCC82BD82DE900C9532A /* CLAuthorizationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */; }; D8E27EA42BD9EA53001EEE98 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E27EA32BD9EA53001EEE98 /* String.swift */; }; D8E27EA62BD9EB61001EEE98 /* Licensable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E27EA52BD9EB61001EEE98 /* Licensable.swift */; }; @@ -81,12 +95,22 @@ D80E089E2B674E900023DD4F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; D80E08A02B675AAC0023DD4F /* ComposeWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeWindow.swift; sourceTree = ""; }; D80E08A52B6761370023DD4F /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; + D82ED7902C1924B100BC0BA8 /* PreviewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewModifier.swift; sourceTree = ""; }; + D82ED7922C1935E700BC0BA8 /* EdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeInsets.swift; sourceTree = ""; }; + D82ED7952C19378500BC0BA8 /* SelectionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionModifier.swift; sourceTree = ""; }; + D83943DF2C18FDDF00EB902E /* KeyboardPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPreview.swift; sourceTree = ""; }; + D83943E52C191ED900EB902E /* SymbolHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolHeader.swift; sourceTree = ""; }; + D83943E72C191F3900EB902E /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; }; + D83943E92C191FA900EB902E /* Pager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pager.swift; sourceTree = ""; }; + D83943EB2C1920F200EB902E /* LocationPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPreview.swift; sourceTree = ""; }; + D83943ED2C1921F100EB902E /* FinderPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinderPreview.swift; sourceTree = ""; }; D83C2F872B857B8C00CE60F7 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; D83F58082C0968F300EF69D3 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = ""; }; D83F580A2C096EA300EF69D3 /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = ""; }; D83F580C2C096F5A00EF69D3 /* TrieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrieTests.swift; sourceTree = ""; }; D84010C42BD74C760049715E /* yams-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "yams-license"; sourceTree = ""; }; D840967E2BD73CBE00926C85 /* LocationDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDetails.swift; sourceTree = ""; }; + D847E9322C1105A800FD6B63 /* AnyTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyTransition.swift; sourceTree = ""; }; D850C2D32B69D85400338546 /* KeyedDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedDefaults.swift; sourceTree = ""; }; D850C2D52B69D8D500338546 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; D852AEFE2B6027CA00B77A3D /* Thoughts.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Thoughts.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -118,7 +142,11 @@ D89224A32BD7368F00DA0932 /* TimeZoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneTests.swift; sourceTree = ""; }; D89224A62BD736B900DA0932 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; D89224A82BD736DF00DA0932 /* TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = ""; }; + D8B1466C2C1290200063E81F /* MenuPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuPreview.swift; sourceTree = ""; }; D8BA4D4C2BE5A9CB001A2A26 /* SettingsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = ""; }; + D8CEE5092C0A64FF00D2025A /* IntroductionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionView.swift; sourceTree = ""; }; + D8CEE50B2C0A66FD00D2025A /* NSIntroductionWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSIntroductionWindow.swift; sourceTree = ""; }; + D8CEE50D2C0A676900D2025A /* MarketingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketingView.swift; sourceTree = ""; }; D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLAuthorizationStatus.swift; sourceTree = ""; }; D8E27EA32BD9EA53001EEE98 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; D8E27EA52BD9EB61001EEE98 /* Licensable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Licensable.swift; sourceTree = ""; }; @@ -178,8 +206,10 @@ D80E08A22B675AD40023DD4F /* Extensions */ = { isa = PBXGroup; children = ( + D847E9322C1105A800FD6B63 /* AnyTransition.swift */, D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */, D86255902C0926B40044BD37 /* Date.swift */, + D82ED7922C1935E700BC0BA8 /* EdgeInsets.swift */, D862558E2C09262E0044BD37 /* FileManager.swift */, D850C2D32B69D85400338546 /* KeyedDefaults.swift */, D8E27EA52BD9EB61001EEE98 /* Licensable.swift */, @@ -192,6 +222,59 @@ path = Extensions; sourceTree = ""; }; + D82ED7942C19376000BC0BA8 /* Modifiers */ = { + isa = PBXGroup; + children = ( + D82ED7902C1924B100BC0BA8 /* PreviewModifier.swift */, + D82ED7952C19378500BC0BA8 /* SelectionModifier.swift */, + ); + path = Modifiers; + sourceTree = ""; + }; + D83943E12C1910AA00EB902E /* Introduction */ = { + isa = PBXGroup; + children = ( + D83943ED2C1921F100EB902E /* FinderPreview.swift */, + D8CEE5092C0A64FF00D2025A /* IntroductionView.swift */, + D83943DF2C18FDDF00EB902E /* KeyboardPreview.swift */, + D83943EB2C1920F200EB902E /* LocationPreview.swift */, + D8CEE50D2C0A676900D2025A /* MarketingView.swift */, + D8B1466C2C1290200063E81F /* MenuPreview.swift */, + D8CEE50B2C0A66FD00D2025A /* NSIntroductionWindow.swift */, + D83943E72C191F3900EB902E /* Page.swift */, + D83943E92C191FA900EB902E /* Pager.swift */, + D83943E52C191ED900EB902E /* SymbolHeader.swift */, + ); + path = Introduction; + sourceTree = ""; + }; + D83943E22C19189D00EB902E /* Compose */ = { + isa = PBXGroup; + children = ( + D852AF652B604B1200B77A3D /* ComposeView.swift */, + D80E08A02B675AAC0023DD4F /* ComposeWindow.swift */, + D850C2D52B69D8D500338546 /* ContentView.swift */, + ); + path = Compose; + sourceTree = ""; + }; + D83943E32C1918B300EB902E /* Settings */ = { + isa = PBXGroup; + children = ( + D8F06D502B6731CE0039AEF4 /* SettingsView.swift */, + D8BA4D4C2BE5A9CB001A2A26 /* SettingsWindow.swift */, + ); + path = Settings; + sourceTree = ""; + }; + D83943E42C19199C00EB902E /* Menu */ = { + isa = PBXGroup; + children = ( + D852AF682B604B3E00B77A3D /* MainMenu.swift */, + ); + path = Menu; + sourceTree = ""; + }; D83F58072C0968EA00EF69D3 /* Utilities */ = { isa = PBXGroup; children = ( @@ -234,6 +317,7 @@ D80E08A22B675AD40023DD4F /* Extensions */, D8F06D522B6733440039AEF4 /* Licenses */, D80E089F2B67572E0023DD4F /* Model */, + D82ED7942C19376000BC0BA8 /* Modifiers */, D852AF072B6027CC00B77A3D /* Preview Content */, D83F58072C0968EA00EF69D3 /* Utilities */, D8F06D4F2B6731C00039AEF4 /* Views */, @@ -288,12 +372,10 @@ D8F06D4F2B6731C00039AEF4 /* Views */ = { isa = PBXGroup; children = ( - D852AF652B604B1200B77A3D /* ComposeView.swift */, - D80E08A02B675AAC0023DD4F /* ComposeWindow.swift */, - D850C2D52B69D8D500338546 /* ContentView.swift */, - D852AF682B604B3E00B77A3D /* MainMenu.swift */, - D8F06D502B6731CE0039AEF4 /* SettingsView.swift */, - D8BA4D4C2BE5A9CB001A2A26 /* SettingsWindow.swift */, + D83943E22C19189D00EB902E /* Compose */, + D83943E12C1910AA00EB902E /* Introduction */, + D83943E42C19199C00EB902E /* Menu */, + D83943E32C1918B300EB902E /* Settings */, ); path = Views; sourceTree = ""; @@ -468,31 +550,45 @@ buildActionMask = 2147483647; files = ( D852AF642B604AF500B77A3D /* ApplicationModel.swift in Sources */, + D8CEE50A2C0A64FF00D2025A /* IntroductionView.swift in Sources */, + D8CEE50C2C0A66FD00D2025A /* NSIntroductionWindow.swift in Sources */, D850C2D62B69D8D500338546 /* ContentView.swift in Sources */, D840967F2BD73CBE00926C85 /* LocationDetails.swift in Sources */, + D8CEE50E2C0A676900D2025A /* MarketingView.swift in Sources */, D8DFFCC82BD82DE900C9532A /* CLAuthorizationStatus.swift in Sources */, + D83943EC2C1920F200EB902E /* LocationPreview.swift in Sources */, D852AF692B604B3E00B77A3D /* MainMenu.swift in Sources */, D83C2F882B857B8C00CE60F7 /* URL.swift in Sources */, D86255912C0926B40044BD37 /* Date.swift in Sources */, D89224A02BD735DA00DA0932 /* RegionalDate.swift in Sources */, + D83943EA2C191FA900EB902E /* Pager.swift in Sources */, D876ABDB2BEC842800667F8E /* Sequence.swift in Sources */, D861B4072C0991CC00BA9B80 /* ThoughtsCommands.swift in Sources */, D83F580B2C096EA300EF69D3 /* UTType.swift in Sources */, + D83943E82C191F3900EB902E /* Page.swift in Sources */, D862558D2C0925B40044BD37 /* Details.swift in Sources */, D86255842C091CF80044BD37 /* Library.swift in Sources */, D86255862C09248D0044BD37 /* DirectoryScanner.swift in Sources */, + D82ED7932C1935E700BC0BA8 /* EdgeInsets.swift in Sources */, + D83943E02C18FDDF00EB902E /* KeyboardPreview.swift in Sources */, D892249E2BD735BE00DA0932 /* ThoughtsError.swift in Sources */, D850C2D42B69D85400338546 /* KeyedDefaults.swift in Sources */, D8E27EA62BD9EB61001EEE98 /* Licensable.swift in Sources */, D83F58092C0968F300EF69D3 /* Trie.swift in Sources */, D852AF662B604B1200B77A3D /* ComposeView.swift in Sources */, + D82ED7962C19378500BC0BA8 /* SelectionModifier.swift in Sources */, D862558F2C09262E0044BD37 /* FileManager.swift in Sources */, D8BA4D4D2BE5A9CB001A2A26 /* SettingsWindow.swift in Sources */, D8F06D512B6731CE0039AEF4 /* SettingsView.swift in Sources */, D8E27EA42BD9EA53001EEE98 /* String.swift in Sources */, D80E08A62B6761370023DD4F /* Document.swift in Sources */, + D8B1466D2C1290200063E81F /* MenuPreview.swift in Sources */, D852AF022B6027CA00B77A3D /* ThoughtsApp.swift in Sources */, + D82ED7912C1924B100BC0BA8 /* PreviewModifier.swift in Sources */, + D83943EE2C1921F100EB902E /* FinderPreview.swift in Sources */, + D83943E62C191ED900EB902E /* SymbolHeader.swift in Sources */, D80E08A12B675AAC0023DD4F /* ComposeWindow.swift in Sources */, + D847E9332C1105A800FD6B63 /* AnyTransition.swift in Sources */, D89224A22BD7365C00DA0932 /* TimeZone.swift in Sources */, D892249C2BD735A100DA0932 /* Metadata.swift in Sources */, ); diff --git a/Thoughts/Assets.xcassets/AccentColor.colorset/Contents.json b/Thoughts/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..8f98b1d 100644 --- a/Thoughts/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Thoughts/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,33 @@ { "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" } ], diff --git a/Thoughts/Extensions/AnyTransition.swift b/Thoughts/Extensions/AnyTransition.swift new file mode 100644 index 0000000..0a05791 --- /dev/null +++ b/Thoughts/Extensions/AnyTransition.swift @@ -0,0 +1,41 @@ +// Copyright (c) 2024 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 Interact + +extension AnyTransition { + + static var push: AnyTransition { + AnyTransition.asymmetric( + insertion: .move(edge: .trailing), + removal: .move(edge: .leading) + ) + } + + static var pop: AnyTransition { + AnyTransition.asymmetric( + insertion: .move(edge: .leading), + removal: .move(edge: .trailing) + ) + } + +} diff --git a/Thoughts/Extensions/EdgeInsets.swift b/Thoughts/Extensions/EdgeInsets.swift new file mode 100644 index 0000000..acbb459 --- /dev/null +++ b/Thoughts/Extensions/EdgeInsets.swift @@ -0,0 +1,33 @@ +// Copyright (c) 2024 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 + +extension EdgeInsets { + + init(horizontal: CGFloat, vertical: CGFloat) { + self.init(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } + + init(size: CGFloat) { + self.init(top: size, leading: size, bottom: size, trailing: size) + } + +} diff --git a/Thoughts/Model/ApplicationModel.swift b/Thoughts/Model/ApplicationModel.swift index 310af27..85a7f81 100644 --- a/Thoughts/Model/ApplicationModel.swift +++ b/Thoughts/Model/ApplicationModel.swift @@ -31,8 +31,11 @@ class ApplicationModel: NSObject { enum SettingsKey: String { case rootURL case shouldSaveLocation + case introductionVersion } + static let introductionVersion = 1 + @MainActor var tags: Trie { return library?.tags ?? Trie() } @@ -61,6 +64,12 @@ class ApplicationModel: NSObject { } } + @MainActor var introductionVersion: Int { + didSet { + keyedDefaults.set(introductionVersion, forKey: .introductionVersion) + } + } + @MainActor var document = Document() { didSet { documentChanges.send(document) @@ -74,6 +83,10 @@ class ApplicationModel: NSObject { } } + @MainActor var didShowIntroduction: Bool { + return introductionVersion == Self.introductionVersion + } + let toggleFocusPublisher = PassthroughSubject() private var cancellables = Set() @@ -90,6 +103,7 @@ class ApplicationModel: NSObject { @MainActor override init() { rootURL = try? keyedDefaults.securityScopedURL(forKey: .rootURL) shouldSaveLocation = keyedDefaults.bool(forKey: .shouldSaveLocation, default: false) + introductionVersion = keyedDefaults.integer(forKey: .introductionVersion, default: 0) super.init() rootURLChanges.send(rootURL) locationManager.delegate = self @@ -124,13 +138,21 @@ class ApplicationModel: NSObject { reloadLibrary() - // Show the compose (configure) window if the root URL is empty. - if rootURL == nil { - new() + if !didShowIntroduction { + showIntroduction() } } + @MainActor func showIntroduction() { + let window = NSIntroductionWindow(applicationModel: self) + window.center() + window.makeKeyAndOrderFront(nil) + } + @MainActor func new() { + guard didShowIntroduction else { + return + } if useDemoData { let location = LocationDetails(latitude: 34.2133, longitude: 135.5853, @@ -143,7 +165,7 @@ class ApplicationModel: NSObject { It’s for recording _ephemeral_ notes. For when you just want to get something down and out of your head, happy in the knowledge that it’s recorded _somewhere_. - Thoughts doesn’t offer any viewing functionality--it’s all about file-and-forget. It saves notes in **Markdown** and **Frontmatter** so it pairs perfectly with tools like [Obsidian](https://obsidian.md) and static site builders like [Jekyll](https://jekyllrb.com), [Hugo](https://gohugo.io), and [InContext](https://incontext.app). + Thoughts doesn’t offer any viewing functionality---it’s all about file-and-forget. It saves notes in **Markdown** and **Frontmatter** so it pairs perfectly with tools like [Obsidian](https://obsidian.md) and static site builders like [Jekyll](https://jekyllrb.com), [Hugo](https://gohugo.io), and [InContext](https://incontext.app). """ document.tags = ["software", "apple", "mac", "markdown", "journaling"] document.location = location @@ -159,7 +181,7 @@ class ApplicationModel: NSObject { toggleFocusPublisher.send(()) } - @MainActor func updateUserLocation() { + @MainActor func updateUserLocation(completion: (() -> Void)? = nil) { requestUserLocation { result in switch result { case .success(let location): @@ -167,6 +189,7 @@ class ApplicationModel: NSObject { case .failure(let error): print("Failed to fetch location with error '\(error)'.") } + completion?() } } @@ -182,7 +205,7 @@ class ApplicationModel: NSObject { library?.start() } - @MainActor func setRootURL() { + @MainActor func setRootURL() -> Bool { dispatchPrecondition(condition: .onQueue(.main)) let openPanel = NSOpenPanel() openPanel.canChooseFiles = false @@ -190,10 +213,11 @@ class ApplicationModel: NSObject { openPanel.canCreateDirectories = true guard openPanel.runModal() == NSApplication.ModalResponse.OK, let url = openPanel.url else { - return + return false } rootURL = url document = Document() + return true } } @@ -206,6 +230,7 @@ extension ApplicationModel: CLLocationManagerDelegate { return } guard shouldSaveLocation else { + completion(.failure(ThoughtsError.userLocationDisabled)) return } locationRequests.append(completion) diff --git a/Thoughts/Model/ThoughtsError.swift b/Thoughts/Model/ThoughtsError.swift index 3aac6ca..c36ba47 100644 --- a/Thoughts/Model/ThoughtsError.swift +++ b/Thoughts/Model/ThoughtsError.swift @@ -25,4 +25,5 @@ enum ThoughtsError: Error { case accessError case encodingError case locationServicesDisabled + case userLocationDisabled } diff --git a/Thoughts/Modifiers/PreviewModifier.swift b/Thoughts/Modifiers/PreviewModifier.swift new file mode 100644 index 0000000..71fe1bf --- /dev/null +++ b/Thoughts/Modifiers/PreviewModifier.swift @@ -0,0 +1,50 @@ +// Copyright (c) 2024 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 + +struct PreviewModifier: ViewModifier { + + struct LayoutMetrics { + static let minimumHeight = 160.0 + static let cornerRadius = 16.0 + } + + func body(content: Content) -> some View { + VStack { + content + .font(.body) + .frame(maxWidth: .infinity) + .frame(minHeight: LayoutMetrics.minimumHeight) + .background(Color(NSColor.textBackgroundColor)) + .cornerRadius(LayoutMetrics.cornerRadius) + Spacer() + } + } + +} + +extension View { + + func preview() -> some View { + return modifier(PreviewModifier()) + } + +} diff --git a/Thoughts/Modifiers/SelectionModifier.swift b/Thoughts/Modifiers/SelectionModifier.swift new file mode 100644 index 0000000..5f3eeb0 --- /dev/null +++ b/Thoughts/Modifiers/SelectionModifier.swift @@ -0,0 +1,53 @@ +// Copyright (c) 2024 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 + +struct SelectionModifier: ViewModifier { + + let insets: EdgeInsets + let cornerRadius: CGFloat + let isSelected: Bool + + func body(content: Content) -> some View { + if isSelected { + content + .foregroundStyle(.primary, .primary, .primary) + .padding(insets) + .background(RoundedRectangle(cornerRadius: cornerRadius) + .fill(.secondary) + .foregroundStyle(.tint)) + } else { + content + .padding(insets) + .background(RoundedRectangle(cornerRadius: cornerRadius) + .fill(.clear)) + } + } + +} + +extension View { + + func selection(insets: EdgeInsets, cornerRadius: CGFloat, isSelected: Bool = false) -> some View { + return modifier(SelectionModifier(insets: insets, cornerRadius: cornerRadius, isSelected: isSelected)) + } + +} diff --git a/Thoughts/Views/ComposeView.swift b/Thoughts/Views/Compose/ComposeView.swift similarity index 92% rename from Thoughts/Views/ComposeView.swift rename to Thoughts/Views/Compose/ComposeView.swift index c7851d9..67cf56f 100644 --- a/Thoughts/Views/ComposeView.swift +++ b/Thoughts/Views/Compose/ComposeView.swift @@ -80,14 +80,6 @@ struct ComposeView: View { .background(.background) .navigationTitle(applicationModel.document.date.formatted(date: .complete, time: .standard)) .navigationSubtitle(applicationModel.document.location?.summary ?? "") - .toolbar { - if applicationModel.shouldSaveLocation && applicationModel.document.location == nil { - ToolbarItem(placement: .navigation) { - ProgressView() - .controlSize(.small) - } - } - } .onOpenURL { url in switch url { case .compose: diff --git a/Thoughts/Views/ComposeWindow.swift b/Thoughts/Views/Compose/ComposeWindow.swift similarity index 100% rename from Thoughts/Views/ComposeWindow.swift rename to Thoughts/Views/Compose/ComposeWindow.swift diff --git a/Thoughts/Views/ContentView.swift b/Thoughts/Views/Compose/ContentView.swift similarity index 91% rename from Thoughts/Views/ContentView.swift rename to Thoughts/Views/Compose/ContentView.swift index bdfbe9d..0fd0bd6 100644 --- a/Thoughts/Views/ContentView.swift +++ b/Thoughts/Views/Compose/ContentView.swift @@ -56,7 +56,7 @@ struct ContentView: View { } description: { Text("Select a folder to store your notes.") Button { - applicationModel.setRootURL() + _ = applicationModel.setRootURL() } label: { Text("Set Notes Folder") } @@ -70,7 +70,8 @@ struct ContentView: View { } label: { let hasLocation = applicationModel.document.location != nil Label("Use Location", systemImage: systemImage) - .foregroundColor(hasLocation ? .purple : nil) + .foregroundColor(hasLocation ? .accent : nil) + .symbolEffect(.pulse, isActive: applicationModel.shouldSaveLocation && applicationModel.document.location == nil) } .disabled(applicationModel.rootURL == nil) } diff --git a/Thoughts/Views/Introduction/FinderPreview.swift b/Thoughts/Views/Introduction/FinderPreview.swift new file mode 100644 index 0000000..b48bed3 --- /dev/null +++ b/Thoughts/Views/Introduction/FinderPreview.swift @@ -0,0 +1,73 @@ +// Copyright (c) 2024 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 Interact + +struct FinderPreview: View { + + struct LayoutMetrics { + + static let iconSize = 16.0 + + } + + struct FinderRow: View { + + let title: String + let isSelected: Bool + + init(_ title: String, isSelected: Bool = false) { + self.title = title + self.isSelected = isSelected + } + + var body: some View { + HStack(spacing: 4) { + Image(nsImage: NSWorkspace.shared.icon(for: .markdown)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: LayoutMetrics.iconSize, height: LayoutMetrics.iconSize) + Text(title) + Spacer() + } + .selection(insets: EdgeInsets(horizontal: 2, vertical: 2), cornerRadius: 4.0, isSelected: isSelected) + } + + } + + var body: some View { + VStack(spacing: 1) { + FinderRow("2024-05-31-06-28-55.md") + FinderRow("2024-05-31-06-29-11.md") + FinderRow("2024-05-31-06-57-27.md") + FinderRow("2024-06-06-01-08-33.md", isSelected: true) + } + .frame(maxWidth: 320) + .padding() + .preview() + } + +} + +#Preview { + FinderPreview() +} diff --git a/Thoughts/Views/Introduction/IntroductionView.swift b/Thoughts/Views/Introduction/IntroductionView.swift new file mode 100644 index 0000000..aa5da88 --- /dev/null +++ b/Thoughts/Views/Introduction/IntroductionView.swift @@ -0,0 +1,162 @@ +// Copyright (c) 2024 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 Interact + +struct IntroductionView: View { + + @Environment(\.closeWindow) private var closeWindow + + let applicationModel: ApplicationModel + + enum PageIdentifier: Identifiable { + + var id: Self { + return self + } + + case welcome + case folder + case location + case open + case keyboard + } + + @State var page: PageIdentifier = .welcome + + var body: some View { + VStack(spacing: 0) { + VStack { + Pager($page) { page in + switch page { + case .welcome: + Page { + MarketingView("Welcome to Thoughts") { + Text("Thoughts works with your workflows to help you quickly get your ideas into your existing systems and tools.") + Text("Pair it with apps like [Obsidian](https://obsidian.md) and [Typora](https://typora.io) to organize and take your notes further.") + } header: { + Image("Icon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 72, height: 72) + } footer: { + Text("Thoughts is not affiliated with Obsidian or Typora.") + } + } actions: { + Button("Continue") { + withAnimation { + self.page = .folder + } + } + .keyboardShortcut(.defaultAction) + } + case .folder: + Page { + MarketingView("Capture Your Ideas", systemImage: "tray.and.arrow.down") { + Text("Thoughts stores all your notes in a folder of your choosing so you can easily integrate it with your exiting workflows.") + FinderPreview() + } + } actions: { + Button("Set Destination Folder") { + if applicationModel.setRootURL() { + withAnimation { + self.page = .location + } + } + } + .keyboardShortcut(.defaultAction) + } + case .location: + Page { + MarketingView("Remember Your Location", systemImage: "location") { + Text("Thoughts can store your location in Frontmatter so you never forget where you were when you had that important idea.") + LocationPreview() + } footer: { + Text("Thoughts never collects or stores your data. See our [Privacy Policy](https://thoughts.jbmorley.co.uk/#privacy-policy).") + } + } actions: { + Button("Skip") { + withAnimation { + self.page = .open + } + } + .keyboardShortcut(.cancelAction) + Button("Allow Location Access") { + applicationModel.shouldSaveLocation = true + applicationModel.updateUserLocation { + DispatchQueue.main.async { + withAnimation { + self.page = .open + } + } + } + } + .keyboardShortcut(.defaultAction) + } + case .open: + Page { + MarketingView("Always Available", systemImage: "menubar.arrow.up.rectangle") { + Text("Thoughts lives in the Menu Bar, waiting for your notes.") + Text("Open at login to ensure you never miss something.") + MenuPreview() + } footer: { + Text("If you can't see the Thoughts icon in the Menu Bar, it might be hiding behind the notch. 👀") + } + } actions: { + Button("Skip") { + withAnimation { + self.page = .keyboard + } + } + .keyboardShortcut(.cancelAction) + Button("Open at Login") { + Application.shared.openAtLogin = true + applicationModel.introductionVersion = ApplicationModel.introductionVersion + withAnimation { + self.page = .keyboard + } + } + .keyboardShortcut(.defaultAction) + } + case .keyboard: + Page { + MarketingView("Keyboard First", systemImage: "keyboard") { + Text("Use global shortcuts to write and edit notes without taking your hands off the keyboard.") + KeyboardPreview() + } + } actions: { + Button("Start Writing") { + withAnimation { + closeWindow() + applicationModel.new() + } + } + .keyboardShortcut(.defaultAction) + } + } + } + } + .frame(width: 600, height: 600) + } + } + +} diff --git a/Thoughts/Views/Introduction/KeyboardPreview.swift b/Thoughts/Views/Introduction/KeyboardPreview.swift new file mode 100644 index 0000000..d838df7 --- /dev/null +++ b/Thoughts/Views/Introduction/KeyboardPreview.swift @@ -0,0 +1,89 @@ +// Copyright (c) 2024 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 + +struct KeyboardPreview: View { + + struct Key: View { + + struct LayoutMetrics { + static let unitWidth = 56.0 + static let unitHeight = 56.0 + static let cornerRadius = 4.0 + static let padding = 6.0 + } + + let legend: String + let text: String? + let width: CGFloat + + init(_ legend: String, text: String? = nil, width: CGFloat = 1.0) { + self.legend = legend + self.text = text + self.width = width + } + + var body: some View { + VStack(alignment: .trailing) { + if let text { + HStack { + Spacer() + Text(legend) + .font(.title2) + } + Spacer() + HStack { + Spacer() + Text(text) + .font(.footnote) + } + } else { + Text(legend) + .font(.title2) + } + } + .padding(LayoutMetrics.padding) + .frame(width: LayoutMetrics.unitWidth * width, + height: LayoutMetrics.unitHeight) + .background(RoundedRectangle(cornerRadius: LayoutMetrics.cornerRadius) + .fill(.secondary) + .foregroundStyle(.tint)) + } + + } + + var body: some View { + HStack { + Key("⌃", text: "control") + Key("⌥", text: "option") + Key("⌘", text: "command", width: 1.25) + Text("+") + .font(.title) + Key("T") + } + .preview() + } + +} + +#Preview { + KeyboardPreview() +} diff --git a/Thoughts/Views/Introduction/LocationPreview.swift b/Thoughts/Views/Introduction/LocationPreview.swift new file mode 100644 index 0000000..96b0fae --- /dev/null +++ b/Thoughts/Views/Introduction/LocationPreview.swift @@ -0,0 +1,45 @@ +// Copyright (c) 2024 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 Interact + +struct LocationPreview: View { + + var body: some View { + HStack { + Text(""" +--- +location: + latitude: 2.15908069616624e+1 + longitude: -1.58103050228585e+2 + name: "Island Vintage Coffee" + locality: "Hale'iwa" +--- +""") + .multilineTextAlignment(.leading) + } + .textSelection(.enabled) + .monospaced() + .preview() + } + +} diff --git a/Thoughts/Views/Introduction/MarketingView.swift b/Thoughts/Views/Introduction/MarketingView.swift new file mode 100644 index 0000000..c1abc9d --- /dev/null +++ b/Thoughts/Views/Introduction/MarketingView.swift @@ -0,0 +1,114 @@ +// Copyright (c) 2024 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 + +struct MarketingView: View { + + let title: String + let subtitle: String? + + let content: Content + let header: Header + let footer: Footer + + init(_ title: String, + subtitle: String? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header, + @ViewBuilder footer: () -> Footer) { + self.title = title + self.subtitle = subtitle + self.content = content() + self.header = header() + self.footer = footer() + } + + var body: some View { + VStack(spacing: 16) { + header + Text(title) + .font(.largeTitle) + .fontWeight(.bold) + if let subtitle { + Text(subtitle) + .foregroundStyle(.secondary) + } + content + .font(.title3) + .textSelection(.enabled) + .multilineTextAlignment(.center) + .frame(maxWidth: 420) + .controlSize(.large) + Spacer() + footer + .foregroundStyle(.secondary) + .font(.footnote) + .textSelection(.enabled) + } + } + +} + +extension MarketingView where Footer == EmptyView { + + init(_ title: String, + subtitle: String? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) { + self.title = title + self.subtitle = subtitle + self.content = content() + self.header = header() + self.footer = EmptyView() + } + +} + +extension MarketingView where Header == SymbolHeader { + + init(_ title: String, + subtitle: String? = nil, + systemImage: String, + @ViewBuilder content: () -> Content, + @ViewBuilder footer: () -> Footer) { + self.title = title + self.subtitle = subtitle + self.content = content() + self.header = SymbolHeader(systemImage: systemImage) + self.footer = footer() + } + +} + +extension MarketingView where Header == SymbolHeader, Footer == EmptyView { + + init(_ title: String, + subtitle: String? = nil, + systemImage: String, + @ViewBuilder content: () -> Content) { + self.title = title + self.subtitle = subtitle + self.content = content() + self.header = SymbolHeader(systemImage: systemImage) + self.footer = EmptyView() + } + +} diff --git a/Thoughts/Views/Introduction/MenuPreview.swift b/Thoughts/Views/Introduction/MenuPreview.swift new file mode 100644 index 0000000..db82e22 --- /dev/null +++ b/Thoughts/Views/Introduction/MenuPreview.swift @@ -0,0 +1,123 @@ +// Copyright (c) 2024 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 + +struct MenuPreview: View { + + struct MenuItem: View { + + let label: String + let shortcut: String? + let isSelected: Bool + + init(_ label: String, shortcut: String? = nil, isSelected: Bool = false) { + self.label = label + self.shortcut = shortcut + self.isSelected = isSelected + } + + var body: some View { + HStack { + Text(label) + Spacer() + if let shortcut { + Text(shortcut) + .foregroundStyle(.secondary) + } + } + .selection(insets: EdgeInsets(horizontal: 8, vertical: 2), + cornerRadius: 4.0, + isSelected: isSelected) + } + + } + + var time: String { + let formatter = DateFormatter() + formatter.dateFormat = "EEE MMM d h:mma" + return formatter.string(from: Date()) + } + + var body: some View { + + Grid(horizontalSpacing: 0, verticalSpacing: 0) { + GridRow { + HStack { + Spacer() + } + HStack { + Image(systemName: "text.justify.left") + .shadow(radius: 1) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Rectangle() + .fill(.tertiary) + .clipShape(RoundedRectangle(cornerRadius: 4))) + Spacer() + } + .padding(.horizontal, 4) + HStack { + Spacer() + Text(time) + .foregroundStyle(.tertiary) + } + .padding(.horizontal, 4) + } + GridRow { + Divider() + .padding(.vertical, 4) + .gridCellColumns(3) + } + GridRow { + HStack { + Spacer() + } + HStack { + VStack(spacing: 2) { + MenuItem("New…", shortcut: "⌃⌥⌘T", isSelected: true) + Divider() + MenuItem("About…") + MenuItem("Settings…", shortcut: "⌘,") + Divider() + MenuItem("Quit", shortcut: "⌘Q") + } + .frame(maxWidth: 300) + .padding(6) + .cornerRadius(8) + .overlay(RoundedRectangle(cornerRadius: 8) + .stroke() + .foregroundStyle(.tertiary)) + } + .padding(.bottom) + HStack { + Spacer() + } + } + } + .padding(6) + .preview() + } + +} + +#Preview { + MenuPreview() +} diff --git a/Thoughts/Views/Introduction/NSIntroductionWindow.swift b/Thoughts/Views/Introduction/NSIntroductionWindow.swift new file mode 100644 index 0000000..1240a38 --- /dev/null +++ b/Thoughts/Views/Introduction/NSIntroductionWindow.swift @@ -0,0 +1,34 @@ +// Copyright (c) 2024 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 + +class NSIntroductionWindow: NSWindow { + + convenience init(applicationModel: ApplicationModel) { + self.init(contentViewController: NSHostingController(rootView: IntroductionView(applicationModel: applicationModel))) + self.title = "Welcome to Thoughts" + self.titleVisibility = .hidden + self.titlebarAppearsTransparent = true + self.styleMask.remove([.miniaturizable, .closable, .resizable, .borderless, .fullSizeContentView]) + self.isMovableByWindowBackground = true + } + +} diff --git a/Thoughts/Views/Introduction/Page.swift b/Thoughts/Views/Introduction/Page.swift new file mode 100644 index 0000000..35ff0ea --- /dev/null +++ b/Thoughts/Views/Introduction/Page.swift @@ -0,0 +1,33 @@ +// Copyright (c) 2024 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 + +struct Page { + + let content: AnyView + let actions: AnyView + + init(@ViewBuilder content: () -> some View, @ViewBuilder actions: () -> some View) { + self.content = AnyView(content()) + self.actions = AnyView(actions()) + } + +} diff --git a/Thoughts/Views/Introduction/Pager.swift b/Thoughts/Views/Introduction/Pager.swift new file mode 100644 index 0000000..48c0514 --- /dev/null +++ b/Thoughts/Views/Introduction/Pager.swift @@ -0,0 +1,53 @@ +// Copyright (c) 2024 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 + +struct Pager: View { + + @Binding var item: Item + + let content: (Item) -> Page + + init(_ item: Binding, content: @escaping (Item) -> Page) { + self._item = item + self.content = content + } + + var body: some View { + VStack(spacing: 0) { + content(item).content + .id(item) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding() + .transition(.push) + HStack { + Spacer() + content(item).actions + .transition(.blurReplace()) + } + .id(item) + .controlSize(.large) + .padding() + .frame(maxWidth: .infinity) + } + } + +} diff --git a/Thoughts/Views/Introduction/SymbolHeader.swift b/Thoughts/Views/Introduction/SymbolHeader.swift new file mode 100644 index 0000000..a5128e6 --- /dev/null +++ b/Thoughts/Views/Introduction/SymbolHeader.swift @@ -0,0 +1,39 @@ +// Copyright (c) 2024 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 + +struct SymbolHeader: View { + + struct LayoutMetrics { + static let size = 72.0 + } + + let systemImage: String + + var body: some View { + Image(systemName: systemImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: LayoutMetrics.size, height: LayoutMetrics.size) + .foregroundStyle(.tint) + } + +} diff --git a/Thoughts/Views/MainMenu.swift b/Thoughts/Views/Menu/MainMenu.swift similarity index 86% rename from Thoughts/Views/MainMenu.swift rename to Thoughts/Views/Menu/MainMenu.swift index c5c12d1..b1abcc7 100644 --- a/Thoughts/Views/MainMenu.swift +++ b/Thoughts/Views/Menu/MainMenu.swift @@ -33,6 +33,7 @@ struct MainMenu: View { applicationModel.new() } .keyboardShortcut("t", modifiers: [.command, .option, .control]) + .disabled(!applicationModel.didShowIntroduction) Divider() Button { openURL(.about) @@ -45,7 +46,16 @@ struct MainMenu: View { Text("Settings...") } .keyboardShortcut(",") + .disabled(!applicationModel.didShowIntroduction) Divider() +#if DEBUG + Button { + applicationModel.showIntroduction() + } label: { + Text("Introduction...") + } + Divider() +#endif Button { NSApplication.shared.terminate(nil) } label: { diff --git a/Thoughts/Views/SettingsView.swift b/Thoughts/Views/Settings/SettingsView.swift similarity index 97% rename from Thoughts/Views/SettingsView.swift rename to Thoughts/Views/Settings/SettingsView.swift index bbd755c..6c0b6ba 100644 --- a/Thoughts/Views/SettingsView.swift +++ b/Thoughts/Views/Settings/SettingsView.swift @@ -38,7 +38,7 @@ struct SettingsView: View { Section { Toggle("Open at Login", isOn: $application.openAtLogin) Button("Set Notes Folder") { - applicationModel.setRootURL() + _ = applicationModel.setRootURL() } } #if DEBUG diff --git a/Thoughts/Views/SettingsWindow.swift b/Thoughts/Views/Settings/SettingsWindow.swift similarity index 100% rename from Thoughts/Views/SettingsWindow.swift rename to Thoughts/Views/Settings/SettingsWindow.swift