Skip to content

Commit

Permalink
feat: πŸ§™β€β™‚οΈ Introduction flow to help users get started (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbmorley authored Jun 12, 2024
1 parent d22f84b commit 857d273
Show file tree
Hide file tree
Showing 24 changed files with 1,118 additions and 24 deletions.
108 changes: 102 additions & 6 deletions Thoughts.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions Thoughts/Assets.xcassets/AccentColor.colorset/Contents.json
Original file line number Diff line number Diff line change
@@ -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"
}
],
Expand Down
41 changes: 41 additions & 0 deletions Thoughts/Extensions/AnyTransition.swift
Original file line number Diff line number Diff line change
@@ -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)
)
}

}
33 changes: 33 additions & 0 deletions Thoughts/Extensions/EdgeInsets.swift
Original file line number Diff line number Diff line change
@@ -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)
}

}
39 changes: 32 additions & 7 deletions Thoughts/Model/ApplicationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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)
Expand All @@ -74,6 +83,10 @@ class ApplicationModel: NSObject {
}
}

@MainActor var didShowIntroduction: Bool {
return introductionVersion == Self.introductionVersion
}

let toggleFocusPublisher = PassthroughSubject<Void, Never>()

private var cancellables = Set<AnyCancellable>()
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -159,14 +181,15 @@ class ApplicationModel: NSObject {
toggleFocusPublisher.send(())
}

@MainActor func updateUserLocation() {
@MainActor func updateUserLocation(completion: (() -> Void)? = nil) {
requestUserLocation { result in
switch result {
case .success(let location):
self.document.location = location
case .failure(let error):
print("Failed to fetch location with error '\(error)'.")
}
completion?()
}
}

Expand All @@ -182,18 +205,19 @@ class ApplicationModel: NSObject {
library?.start()
}

@MainActor func setRootURL() {
@MainActor func setRootURL() -> 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
return false
}
rootURL = url
document = Document()
return true
}

}
Expand All @@ -206,6 +230,7 @@ extension ApplicationModel: CLLocationManagerDelegate {
return
}
guard shouldSaveLocation else {
completion(.failure(ThoughtsError.userLocationDisabled))
return
}
locationRequests.append(completion)
Expand Down
1 change: 1 addition & 0 deletions Thoughts/Model/ThoughtsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ enum ThoughtsError: Error {
case accessError
case encodingError
case locationServicesDisabled
case userLocationDisabled
}
50 changes: 50 additions & 0 deletions Thoughts/Modifiers/PreviewModifier.swift
Original file line number Diff line number Diff line change
@@ -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())
}

}
53 changes: 53 additions & 0 deletions Thoughts/Modifiers/SelectionModifier.swift
Original file line number Diff line number Diff line change
@@ -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))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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)
}
Expand Down
Loading

0 comments on commit 857d273

Please sign in to comment.