Browse Source

๐ŸŽ‰ Rewrite txtodo in new cross-platform structure

main
FIGBERT 1 year ago
commit
a3456c1b67
Signed by: FIGBERT GPG Key ID: 67F1598D607A844B
  1. 10
      .gitignore
  2. 44
      README.md
  3. 95
      Shared/AddNoteView.swift
  4. 145
      Shared/AddTask.swift
  5. 11
      Shared/Assets.xcassets/AccentColor.colorset/Contents.json
  6. 148
      Shared/Assets.xcassets/AppIcon.appiconset/Contents.json
  7. 6
      Shared/Assets.xcassets/Contents.json
  8. 102
      Shared/ContentView.swift
  9. 15
      Shared/Data Models/Task+CoreDataClass.swift
  10. 32
      Shared/Data Models/Task+CoreDataProperties.swift
  11. 16
      Shared/Data Models/txtodo.xcdatamodeld/txtodo.xcdatamodel/contents
  12. 31
      Shared/DevicePaddingModifiers.swift
  13. 18
      Shared/FrameModifier.swift
  14. 78
      Shared/NoteView.swift
  15. 29
      Shared/SectionLabel.swift
  16. 89
      Shared/Settings Views/NotificationSection.swift
  17. BIN
      Shared/en.lproj/Localizable.strings
  18. BIN
      Shared/he.lproj/Localizable.strings
  19. 62
      Shared/txtodoApp.swift
  20. 36
      iOS/AboutSheet.swift
  21. 25
      iOS/HomeHeaderView.swift
  22. 55
      iOS/Info.plist
  23. 55
      iOS/MenuView.swift
  24. 28
      iOS/NoteSheet.swift
  25. 28
      iOS/SettingsSheet.swift
  26. 143
      iOS/TaskView.swift
  27. 26
      macOS/HomeHeaderView.swift
  28. 28
      macOS/Info.plist
  29. 38
      macOS/SettingsView.swift
  30. 156
      macOS/TaskView.swift
  31. 22
      macOS/macOS.entitlements
  32. 16
      txtodo.entitlements
  33. 611
      txtodo.xcodeproj/project.pbxproj
  34. 7
      txtodo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  35. 8
      txtodo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

10
.gitignore

@ -0,0 +1,10 @@
# File generated based on GitHub gitignore Swift.gitignore recommendations
xcuserdata/
*.ipa
*.dSYM.zip
*.dSYM
*.hmap
timeline.xctimeline
playground.xcworkspace
.build/
.DS_Store

44
README.md

@ -0,0 +1,44 @@
# txtodo
pronounced "text to do," **txtodo** is a minimalist open-source todo list app made by [figbert][0] and inspired by [jeff huang][1]. it lists your immediate, short-term tasks to help you get things done without overthinking it. plus, it's lightweight, open-source, and built with swiftui.
after reading a [post][2] by jeff huang on [hacker news][3], i started thinking about how i managed my own daily tasks. i wanted to make a solution that could train me into being highly productive.
that solution is **txtodo**. it manages immediate, short-term tasks to help you get things done. welcome to productivity. welcome to txtodo.
download it on [the app store](https://apps.apple.com/us/app/txtodo/id1504609185) and give it a try!
## milestones
- [x] launch alpha on testflight
- [x] launch wider beta testing
- [x] submit to apple for approval
- [x] go live on the app store! :tada:
## short term goals
- [x] implement a "floating" task system for long term goals
- [ ] add i18n
- [x] hebrew
- [ ] arabic
- [ ] chinese
- [ ] spanish
- [ ] japanese
## long term goals
- [x] add e2e encrypted cloud storage
- [x] create macos companion app
- [x] monetize in a not-evil way
# links
1. [txtodo homepage](https://txtodo.app/)
2. [figbert homepage](https://figbert.com/)
3. [txtodo repository](https://github.com/therealFIGBERT/txtodo)
4. [txtodo homepage repository](https://github.com/therealFIGBERT/txtodo.app)
5. [txtodo on the App Store](https://apps.apple.com/us/app/txtodo/id1504609185)
# license
copyright ยฉ 2020 figbert industries. all rights reserved.
[0]: https://figbert.com/
[1]: https://jeffhuang.com/productivity_text_file/
[2]: https://news.ycombinator.com/item?id=22276184
[3]: https://news.ycombinator.com/

95
Shared/AddNoteView.swift

@ -0,0 +1,95 @@
//
// AddNoteView.swift
// txtodo
//
// Created by FIGBERT on 8/6/20.
//
import SwiftUI
struct AddNoteView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@ObservedObject var task: Task
@State private var config = AddNoteViewConfig()
var body: some View {
Group {
if !config.addingNote {
HStack {
Image(systemName: "plus.square")
Spacer()
Text("create a note")
Spacer()
Image(systemName: "plus.square")
}
.onTapGesture {
#if os(iOS)
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
self.config.addingNote = true
generator.impactOccurred()
#else
self.config.addingNote = true
#endif
}
} else {
HStack {
Image(systemName: "multiply.square")
.onTapGesture {
#if os(iOS)
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
config.clear()
generator.impactOccurred()
#else
config.clear()
#endif
}
Spacer()
TextField("tap", text: $config.newNoteText) {
addTask()
}
.multilineTextAlignment(.center)
Spacer()
Image(systemName: "plus.square")
.onTapGesture {
addTask(forceKeyboardClose: true)
}
}
}
}
.font(.system(size: 18, weight: .light, design: .rounded))
.foregroundColor(Color.secondary.opacity(0.5))
.multilineTextAlignment(.center)
}
func addTask(forceKeyboardClose: Bool = false) {
#if os(iOS)
if forceKeyboardClose { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
#endif
guard self.config.newNoteText != "" else {return}
#if os(iOS)
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
#endif
self.managedObjectContext.performAndWait {
self.task.notes.append(self.config.newNoteText)
try? self.managedObjectContext.save()
}
#if os(iOS)
generator.impactOccurred()
#endif
self.config.clear()
}
}
struct AddNoteViewConfig {
var addingNote: Bool = false
var newNoteText: String = ""
mutating func clear() {
addingNote = false
newNoteText = ""
}
}

145
Shared/AddTask.swift

@ -0,0 +1,145 @@
//
// AddTask.swift
// txtodo
//
// Created by FIGBERT on 8/3/20.
//
import SwiftUI
struct AddTaskController: View {
@Environment(\.managedObjectContext) var managedObjectContext
@State private var config = AddTaskControllerConfig()
let lessThanThreeFloatingTasks: Bool
var body: some View {
HStack {
if config.showingDaily {
AddTaskView(showingOtherAddTaskView: $config.showingFloating, isAddingDailyTask: true)
.environment(\.managedObjectContext, self.managedObjectContext)
}
if config.showingDaily && lessThanThreeFloatingTasks && config.showingFloating {
Spacer()
}
if lessThanThreeFloatingTasks && config.showingFloating {
AddTaskView(showingOtherAddTaskView: $config.showingDaily, isAddingDailyTask: false)
.environment(\.managedObjectContext, self.managedObjectContext)
}
}
.horizontalPaddingOnMacOS()
}
}
struct AddTaskView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@State private var config = AddTaskViewConfig()
@Binding var showingOtherAddTaskView: Bool
let isAddingDailyTask: Bool
var body: some View {
Group {
if !config.addingTask {
HStack {
Image(systemName: "plus.square")
Spacer()
Text(isAddingDailyTask ? NSLocalizedString("daily", comment: "") : NSLocalizedString("floating", comment: ""))
Spacer()
Image(systemName: "plus.square")
}
.onTapGesture {
#if os(iOS)
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
self.config.addingTask = true
self.showingOtherAddTaskView = false
generator.impactOccurred()
#else
self.config.addingTask = true
self.showingOtherAddTaskView = false
#endif
}
} else {
HStack {
Image(systemName: "multiply.square")
.onTapGesture {
#if os(iOS)
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
config.clear()
self.showingOtherAddTaskView = true
generator.impactOccurred()
#else
config.clear()
self.showingOtherAddTaskView = true
#endif
}
Spacer()
TextField("tap", text: $config.newTaskText) {
addTask()
}
.multilineTextAlignment(.center)
Picker(
selection: $config.newTaskPriority,
label: Text("task priority"),
content: {
Text("!").tag(1)
Text("!!").tag(2)
Text("!!!").tag(3)
})
.pickerStyle(SegmentedPickerStyle())
.labelsHidden()
Spacer()
Image(systemName: "plus.square")
.onTapGesture {
addTask(forceKeyboardClose: true)
}
}
}
}
.font(.system(size: 18, weight: .light, design: .rounded))
.foregroundColor(Color.secondary.opacity(0.5))
.multilineTextAlignment(.center)
}
func addTask(forceKeyboardClose: Bool = false) {
#if os(iOS)
if forceKeyboardClose { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
#endif
guard self.config.newTaskText != "" else {return}
#if os(iOS)
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
#endif
let newTask = Task(context: self.managedObjectContext)
newTask.id = UUID()
newTask.date = Date()
newTask.notes = [String]()
newTask.daily = self.isAddingDailyTask
newTask.name = self.config.newTaskText
newTask.priority = Int16(self.config.newTaskPriority)
try? self.managedObjectContext.save()
#if os(iOS)
generator.impactOccurred()
#endif
self.config.clear()
self.showingOtherAddTaskView = true
}
}
struct AddTaskControllerConfig {
var showingDaily: Bool = true
var showingFloating: Bool = true
}
struct AddTaskViewConfig {
var addingTask: Bool = false
var newTaskText: String = ""
var newTaskPriority: Int = 1
mutating func clear() {
addingTask = false
newTaskText = ""
newTaskPriority = 1
}
}

11
Shared/Assets.xcassets/AccentColor.colorset/Contents.json

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

148
Shared/Assets.xcassets/AppIcon.appiconset/Contents.json

@ -0,0 +1,148 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

6
Shared/Assets.xcassets/Contents.json

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

102
Shared/ContentView.swift

@ -0,0 +1,102 @@
//
// ContentView.swift
// Shared
//
// Created by FIGBERT on 7/27/20.
//
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(
entity: Task.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Task.completed, ascending: true),
NSSortDescriptor(keyPath: \Task.priority, ascending: false),
NSSortDescriptor(keyPath: \Task.name, ascending: true)
],
predicate: NSPredicate(format: "daily == %d", false)
) var floatingTasks: FetchedResults<Task>
@FetchRequest(
entity: Task.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Task.completed, ascending: true),
NSSortDescriptor(keyPath: \Task.priority, ascending: false),
NSSortDescriptor(keyPath: \Task.name, ascending: true)
],
predicate: NSPredicate(
format: "daily == %d AND date < %@",
argumentArray: [
true,
Calendar.current.startOfDay(
for: Calendar.current.date(
byAdding: .day,
value: 1,
to: Date()
)!
)
]
)
) var dailyTasks: FetchedResults<Task>
let currentDay = Calendar.current.component(.day, from: Date.init())
var body: some View {
ZStack(alignment: .bottomTrailing) {
#if os(iOS)
Color.gray.opacity(0.25)
.edgesIgnoringSafeArea(.all)
#endif
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 10) {
HomeHeaderView().padding(.top)
if floatingTasks.count > 0 {
SectionLabel(text: "floating")
ForEach(self.floatingTasks, id: \.id) { task in
TaskView(task: task, priority: Int(task.priority))
.environment(\.managedObjectContext, self.managedObjectContext)
.onAppear(perform: {
if task.completed && Calendar.current.component(.day, from: task.date) < self.currentDay {
self.managedObjectContext.performAndWait {
self.managedObjectContext.delete(task)
try? self.managedObjectContext.save()
}
}
})
}
}
if dailyTasks.count > 0 {
SectionLabel(text: "daily")
ForEach(self.dailyTasks, id: \.id) { task in
TaskView(task: task, priority: Int(task.priority))
.environment(\.managedObjectContext, self.managedObjectContext)
.onAppear(perform: {
if Calendar.current.component(.day, from: task.date) < self.currentDay {
self.managedObjectContext.performAndWait {
self.managedObjectContext.delete(task)
try? self.managedObjectContext.save()
}
}
})
}
}
AddTaskController(lessThanThreeFloatingTasks: floatingTasks.count < 3)
.environment(\.managedObjectContext, self.managedObjectContext)
Spacer()
}
.horizontalPaddingOnIOS()
.animation(.easeIn(duration: 0.15))
}
#if os(iOS)
MenuView()
#endif
}
.modifier(FrameModifier())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

15
Shared/Data Models/Task+CoreDataClass.swift

@ -0,0 +1,15 @@
//
// Task+CoreDataClass.swift
// txtodo
//
// Created by FIGBERT on 7/27/20.
//
//
import Foundation
import CoreData
@objc(Task)
public class Task: NSManagedObject {
}

32
Shared/Data Models/Task+CoreDataProperties.swift

@ -0,0 +1,32 @@
//
// Task+CoreDataProperties.swift
// txtodo
//
// Created by FIGBERT on 7/31/20.
//
//
import Foundation
import CoreData
extension Task {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Task> {
return NSFetchRequest<Task>(entityName: "Task")
}
@NSManaged public var completed: Bool
@NSManaged public var daily: Bool
@NSManaged public var date: Date
@NSManaged public var id: UUID
@NSManaged public var name: String
@NSManaged public var notes: [String]
@NSManaged public var priority: Int16
@NSManaged public var hasBeenDelayed: Bool
}
extension Task : Identifiable {
}

16
Shared/Data Models/txtodo.xcdatamodeld/txtodo.xcdatamodel/contents

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17175" systemVersion="20A5323l" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<entity name="Task" representedClassName="Task" syncable="YES">
<attribute name="completed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="daily" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="date" attributeType="Date" defaultDateTimeInterval="599601600" usesScalarValueType="NO"/>
<attribute name="hasBeenDelayed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="notes" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName"/>
<attribute name="priority" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="YES"/>
</entity>
<elements>
<element name="Task" positionX="-63" positionY="-18" width="128" height="149"/>
</elements>
</model>

31
Shared/DevicePaddingModifiers.swift

@ -0,0 +1,31 @@
//
// DevicePaddingModifiers.swift
// txtodo
//
// Created by FIGBERT on 8/11/20.
//
import SwiftUI
struct HorizontalPadding: ViewModifier {
func body(content: Content) -> some View {
return content.padding(.horizontal)
}
}
extension View {
func horizontalPaddingOnIOS() -> some View {
#if os(iOS)
return self.modifier(HorizontalPadding())
#else
return self
#endif
}
func horizontalPaddingOnMacOS() -> some View {
#if os(macOS)
return self.modifier(HorizontalPadding())
#else
return self
#endif
}
}

18
Shared/FrameModifier.swift

@ -0,0 +1,18 @@
//
// FrameModifier.swift
// txtodo
//
// Created by FIGBERT on 8/6/20.
//
import SwiftUI
struct FrameModifier: ViewModifier {
func body(content: Content) -> some View {
#if os(macOS)
return content.frame(width: 325, height: 400)
#else
return content
#endif
}
}

78
Shared/NoteView.swift

@ -0,0 +1,78 @@
//
// NoteView.swift
// txtodo
//
// Created by FIGBERT on 8/11/20.
//
import SwiftUI
struct NoteView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@ObservedObject var task: Task
@State var note: String
@State private var config = NoteViewConfig()
var body: some View {
let noteIntermediary = Binding<String>(
get: { self.note },
set: { value in
self.config.editingCache = value
}
)
return HStack {
Image(systemName: "minus")
.padding(.trailing)
if !config.editing {
Text(note)
.onTapGesture(count: 2) {
self.config.editing = true
}
} else {
TextField("edit note", text: noteIntermediary) {
if let index = self.task.notes.firstIndex(of: self.note) {
self.managedObjectContext.performAndWait {
self.task.notes[index] = self.config.editingCache
try? self.managedObjectContext.save()
}
}
self.config.editing = false
}
}
Spacer()
if config.showingDelete {
Image(systemName: "trash.circle.fill")
.font(.system(size: 25))
.foregroundColor(.red)
.onTapGesture {
if let index = self.task.notes.firstIndex(of: self.note) {
self.managedObjectContext.performAndWait {
self.task.notes.remove(at: index)
try? self.managedObjectContext.save()
}
}
}
}
}
.offset(x: config.offset)
.gesture(
DragGesture()
.onChanged({ value in
config.offset = value.translation.width
})
.onEnded({ value in
if -config.offset > 15 {
config.showingDelete.toggle()
}
config.offset = 0
})
)
}
}
struct NoteViewConfig {
var editing: Bool = false
var showingDelete: Bool = false
var offset: CGFloat = 0
var editingCache = ""
}

29
Shared/SectionLabel.swift

@ -0,0 +1,29 @@
//
// SectionLabel.swift
// txtodo
//
// Created by FIGBERT on 7/28/20.
//
import SwiftUI
struct SectionLabel: View {
let text: String
var body: some View {
HStack {
Text(NSLocalizedString(text, comment: ""))
.font(.system(size: 10, weight: .bold))
.foregroundColor(.gray)
.multilineTextAlignment(.leading)
Spacer()
}
.horizontalPaddingOnMacOS()
}
}
struct SectionLabel_Previews: PreviewProvider {
static var previews: some View {
SectionLabel(text: "example")
}
}

89
Shared/Settings Views/NotificationSection.swift

@ -0,0 +1,89 @@
//
// NotificationSection.swift
// txtodo
//
// Created by FIGBERT on 8/6/20.
//
import SwiftUI
import UserNotifications
struct NotificationSection: View {
@AppStorage("notifications") var notificationsEnabled: Bool = false { willSet {
if !newValue {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
} else {
enableNotifications()
}
} }
@AppStorage("notificationID") var notificationID: String = "unset"
@AppStorage("notificationTime") var notificationTime: Int = 0 { willSet { enableNotifications() } }
var body: some View {
Section {
Toggle(isOn: $notificationsEnabled) {
#if os(iOS)
Label(notificationsEnabled ? "notifications enabled" : "notifications disabled", systemImage: "app.badge")
#else
Text("txtodo notifications \(notificationsEnabled ? "enabled" : "disabled")")
#endif
}
HStack {
Label("time scheduled", systemImage: "clock")
Spacer()
Picker(
selection: $notificationTime,
label: Text("notification time"),
content: {
Text("8:30").tag(0)
Text("9:30").tag(1)
Text("10:30").tag(2)
})
.labelsHidden()
.disabled(!notificationsEnabled)
.pickerStyle(SegmentedPickerStyle())
}
}
}
func enableNotifications() {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { success, error in
if success {
let content = UNMutableNotificationContent()
content.title = String(format: NSLocalizedString("open txtodo", comment: ""))
content.body = String(format: NSLocalizedString("take some time to plan your day", comment: ""))
content.sound = UNNotificationSound.default
var time = DateComponents()
if self.notificationTime == 0 {
time.hour = 8
} else if self.notificationTime == 1 {
time.hour = 9
} else {
time.hour = 10
}
time.minute = 30
let trigger = UNCalendarNotificationTrigger(
dateMatching: time,
repeats: true
)
let id = UUID().uuidString
self.notificationID = id
let request = UNNotificationRequest(
identifier: id,
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request)
} else if let error = error {
print(error.localizedDescription)
}
}
}
}
struct NotificationSection_Previews: PreviewProvider {
static var previews: some View {
NotificationSection()
}
}

BIN
Shared/en.lproj/Localizable.strings

Binary file not shown.

BIN
Shared/he.lproj/Localizable.strings

Binary file not shown.

62
Shared/txtodoApp.swift

@ -0,0 +1,62 @@
//
// txtodoApp.swift
// Shared
//
// Created by FIGBERT on 7/27/20.
//
import SwiftUI
import CoreData
import UserNotifications
@main
struct txtodoApp: App {
@Environment(\.scenePhase) private var scenePhase
@SceneBuilder var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, self.persistentContainer.viewContext)
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
case .inactive:
saveContext()
case .background:
saveContext()
@unknown default:
saveContext()
}
}
#if os(macOS)
Settings {
SettingsView()
}
#endif
}
var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "txtodo")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
func saveContext() {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

36
iOS/AboutSheet.swift

@ -0,0 +1,36 @@
//
// AboutSheet.swift
// txtodo (iOS)
//
// Created by FIGBERT on 8/6/20.
//
import SwiftUI
struct AboutSheet: View {
var body: some View {
VStack() {
Text("about")
.underline()
.padding(.vertical)
Text("aboutOne")
.multilineTextAlignment(.center)
Text("aboutTwo")
.multilineTextAlignment(.center)
.padding(.vertical)
Text("aboutThree")
.multilineTextAlignment(.center)
Link(destination: URL(string: "https://txtodo.app/")!) { Text("view site") }
.padding(.vertical)
Link(destination: URL(string: "https://jeffhuang.com/productivity_text_file/")!) { Text("view inspo") }
Spacer()
}
.padding()
}
}
struct AboutSheet_Previews: PreviewProvider {
static var previews: some View {
AboutSheet()
}
}

25
iOS/HomeHeaderView.swift

@ -0,0 +1,25 @@
//
// HomeHeaderView.swift
// txtodo (iOS)
//
// Created by FIGBERT on 8/9/20.
//
import SwiftUI
struct HomeHeaderView: View {
var body: some View {
HStack {
Spacer()
Text("title")
.underline()
Spacer()
}
}
}
struct HomeHeaderView_Previews: PreviewProvider {
static var previews: some View {
HomeHeaderView()
}
}

55
iOS/Info.plist

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>3.0.0</string>
<key>CFBundleVersion</key>
<string>3.0.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchScreen</key>
<dict/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

55
iOS/MenuView.swift

@ -0,0 +1,55 @@
//
// MenuView.swift
// txtodo (iOS)
//
// Created by FIGBERT on 8/6/20.
//
import SwiftUI
struct MenuView: View {
@State private var config = MenuViewConfig()
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 18) {
if config.active {
Label("settings", systemImage: "gear")
.onTapGesture {
config.showSettings = true
}
.sheet(isPresented: $config.showSettings) {
SettingsSheet()
}
Label("about", systemImage: "book")
.onTapGesture {
config.showAbout = true
}
.sheet(isPresented: $config.showAbout) {
AboutSheet()
}
}
Image(systemName: config.active ? "chevron.up" : "line.horizontal.3")
.font(.system(size: 30, weight: .ultraLight))
.onTapGesture {
config.active.toggle()
}
}
Spacer()
}
.font(.system(size: 18, weight: .light))
.padding()
}
}
struct MenuViewConfig {
var active: Bool = false
var showSettings: Bool = false
var showAbout: Bool = false
}
struct MenuView_Previews: PreviewProvider {
static var previews: some View {
MenuView()
}
}

28
iOS/NoteSheet.swift

@ -0,0 +1,28 @@
//
// NoteSheet.swift
// txtodo
//
// Created by FIGBERT on 8/5/20.
//
import SwiftUI
struct NoteSheet: View {
@Environment(\.managedObjectContext) var managedObjectContext
@ObservedObject var task: Task
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10) {
Text(task.name)
.underline()
ForEach(task.notes, id: \.self) { note in
NoteView(task: task, note: note)
.environment(\.managedObjectContext, self.managedObjectContext)
}
AddNoteView(task: task)
}
}
.padding()
}
}

28
iOS/SettingsSheet.swift

@ -0,0 +1,28 @@
//
// SettingsSheet.swift
// txtodo (iOS)
//
// Created by FIGBERT on 8/6/20.
//
import SwiftUI
struct SettingsSheet: View {
var body: some View {
VStack {
Text("settings")
.underline()
.padding()
Form {
NotificationSection()
}
.listStyle(GroupedListStyle())
}
}
}
struct SettingsSheet_Previews: PreviewProvider {
static var previews: some View {
SettingsSheet()
}
}

143
iOS/TaskView.swift

@ -0,0 +1,143 @@
//
// TaskView.swift
// txtodo
//
// Created by FIGBERT on 7/27/20.
//
import SwiftUI
struct TaskView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@Environment(\.layoutDirection) var direction
@ObservedObject var task: Task
@State var priority: Int
@State private var config = TaskViewConfig()
var body: some View {
let priorityIntermediary = Binding<Int>(
get: { self.priority },
set: { value in
self.priority = value
self.managedObjectContext.performAndWait {
self.task.priority = Int16(value)
try? self.managedObjectContext.save()
}
self.config.editingPriority = false
}
)
return HStack {
if task.daily && !task.hasBeenDelayed && !task.completed && config.showingDelay {
Image(systemName: "calendar.circle.fill")
.font(.system(size: 25))
.foregroundColor(.blue)
.onTapGesture {
self.managedObjectContext.performAndWait {
self.task.date = Calendar.current.date(byAdding: .day, value: 1, to: task.date) ?? Date()
self.task.hasBeenDelayed = true
try? self.managedObjectContext.save()
}
config.showingDelay = false
}
}
Image(systemName: task.completed ? "checkmark.square" : "square")
.onTapGesture {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
self.managedObjectContext.performAndWait {
self.task.completed.toggle()
if !self.task.daily {
self.task.date = Date.init()
}
try? self.managedObjectContext.save()
}
generator.impactOccurred()
}
Spacer()
if !config.editingText {
Text(task.name)
.strikethrough(task.completed)
.onTapGesture(count: 2) {
if !self.task.completed {
self.config.editingText = true
}
}
.onTapGesture {
self.config.showingNotes = true
}
} else {
TextField("edit task", text: $task.name) {
self.config.editingText = false
self.managedObjectContext.performAndWait {
try? self.managedObjectContext.save()
}
}
}
Spacer()
if !config.editingPriority {
HStack(alignment: .center, spacing: 2) {
ForEach(1 ..< Int(task.priority + 1), id: \.self) {_ in
Text("!")
}
}
.font(.system(size: 10, weight: .light))
.onTapGesture(count: 2) {
if !self.task.completed {
self.config.editingPriority = true
}
}
} else {
Picker(
selection: priorityIntermediary,
label: Text("task priority"),
content: {
Text("!").tag(1)
Text("!!").tag(2)
Text("!!!").tag(3)
})
.pickerStyle(SegmentedPickerStyle())
}
if config.showingDelete {
Image(systemName: "trash.circle.fill")
.font(.system(size: 25))
.foregroundColor(.red)
.onTapGesture {
self.managedObjectContext.performAndWait {
self.managedObjectContext.delete(self.task)
try? self.managedObjectContext.save()
}
}
}
}
.font(.system(size: 18, weight: .light))
.foregroundColor(task.completed ? .secondary : .primary)
.multilineTextAlignment(.center)
.offset(x: config.offset)
.gesture(
DragGesture()
.onChanged({ value in
config.offset = direction == .leftToRight ? value.translation.width : -value.translation.width
})
.onEnded({ value in
if task.daily && config.offset > 15 {
config.showingDelay.toggle()
} else if -config.offset > 15 {
config.showingDelete.toggle()
}
config.offset = 0
})
)
.sheet(isPresented: $config.showingNotes, content: {
NoteSheet(task: self.task)
})
}
}
struct TaskViewConfig {
var editingText: Bool = false
var editingPriority: Bool = false
var showingNotes: Bool = false
var showingDelete: Bool = false
var showingDelay: Bool = false
var offset: CGFloat = 0
}

26
macOS/HomeHeaderView.swift

@ -0,0 +1,26 @@
//
// HomeHeaderView.swift
// txtodo
//
// Created by FIGBERT on 8/8/20.
//
import SwiftUI
struct HomeHeaderView: View {
var body: some View {
Text({ () -> String in
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter.string(from: Date())
}())
.font(.system(size: 20, weight: .bold))
.padding(.horizontal)
}
}
struct HomeHeaderView_Previews: PreviewProvider {
static var previews: some View {
HomeHeaderView()
}
}

28
macOS/Info.plist

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>3.0.0</string>
<key>CFBundleVersion</key>
<string>3.0.0</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
</dict>
</plist>

38
macOS/SettingsView.swift

@ -0,0 +1,38 @@
//
// SettingsView.swift
// txtodo (macOS)
//
// Created by FIGBERT on 8/6/20.
//
import SwiftUI
struct SettingsView: View {
var body: some View {
TabView {
VStack {
Text("txtodo by figbert - v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String)")
Link(destination: URL(string: "https://txtodo.app/")!) { Text("view site") }
.padding()
Link(destination: URL(string: "https://jeffhuang.com/productivity_text_file/")!) { Text("view inspo") }
}
.tabItem {
Text("about")
Image(systemName: "book")
}
Form { NotificationSection() }
.padding()
.tabItem {
Text("notifications")
Image(systemName: "app.badge")
}
}
.frame(width: 300, height: 150)
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
}
}

156
macOS/TaskView.swift

@ -0,0 +1,156 @@
//
// TaskView.swift
// txtodo
//
// Created by FIGBERT on 7/27/20.
//
import SwiftUI
struct TaskView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@Environment(\.layoutDirection) var direction
@ObservedObject var task: Task
@State var priority: Int
@State private var config = TaskViewConfig()
var body: some View {
let priorityIntermediary = Binding<Int>(
get: { self.priority },
set: { value in
self.priority = value
self.managedObjectContext.performAndWait {
self.task.priority = Int16(value)
try? self.managedObjectContext.save()
}
self.config.editingPriority = false
}
)
return VStack {
HStack {
if task.daily && !task.hasBeenDelayed && !task.completed && config.showingDelay {
Image(systemName: "calendar.circle.fill")
.font(.system(size: 25))
.foregroundColor(.blue)
.onTapGesture {
self.managedObjectContext.performAndWait {
self.task.date = Calendar.current.date(byAdding: .day, value: 1, to: task.date) ?? Date()
self.task.hasBeenDelayed = true
try? self.managedObjectContext.save()
}
config.showingDelay = false
}
}
Image(systemName: task.completed ? "checkmark.square" : "square")
.onTapGesture {
self.managedObjectContext.performAndWait {
self.task.completed.toggle()
if !self.task.daily {
self.task.date = Date.init()
}
try? self.managedObjectContext.save()
}
self.config.showingNotes = false
}
Spacer()
if !config.editingText {
Text(task.name)
.strikethrough(task.completed)
.onTapGesture(count: 2) {
if !self.task.completed {
self.config.editingText = true
}
}
.onTapGesture {
self.config.showingNotes.toggle()
}
} else {
TextField("edit task", text: $task.name) {
self.config.editingText = false
self.managedObjectContext.performAndWait {
try? self.managedObjectContext.save()
}
}
}
Spacer()
if !config.editingPriority {
HStack(alignment: .center, spacing: 2) {
ForEach(1 ..< Int(task.priority + 1), id: \.self) {_ in
Text("!")
}
}
.font(.system(size: 10, weight: .light))
.onTapGesture(count: 2) {
if !self.task.completed {
self.config.editingPriority = true
}
}
} else {
Picker(
selection: priorityIntermediary,
label: Text("task priority"),
content: {
Text("!").tag(1)
Text("!!").tag(2)
Text("!!!").tag(3)
})
.pickerStyle(SegmentedPickerStyle())
}
if config.showingDelete {
Image(systemName: "trash.circle.fill")
.font(.system(size: 25))
.foregroundColor(.red)
.onTapGesture {
self.managedObjectContext.performAndWait {
self.managedObjectContext.delete(self.task)
try? self.managedObjectContext.save()
}
}
}
}
.padding(.bottom, 5)
.foregroundColor(task.completed ? .secondary : .primary)
.multilineTextAlignment(.center)
.offset(x: config.offset)
.gesture(
DragGesture()
.onChanged({ value in
config.offset = direction == .leftToRight ? value.translation.width : -value.translation.width
})
.onEnded({ value in
if task.daily && config.offset > 15 {
config.showingDelay.toggle()
} else if -config.offset > 15 {
config.showingDelete.toggle()
}
config.offset = 0
})
)
if config.showingNotes {
VStack(spacing: 10) {
ForEach(task.notes, id: \.self) { note in
NoteView(task: task, note: