diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..330d167 --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ diff --git a/Luto.xcodeproj/project.pbxproj b/Luto.xcodeproj/project.pbxproj index e9835c1..d530874 100644 --- a/Luto.xcodeproj/project.pbxproj +++ b/Luto.xcodeproj/project.pbxproj @@ -7,13 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 680A62122A5F475D004C21A4 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680A62112A5F475D004C21A4 /* ViewExtensions.swift */; }; 682D06A62A5487D200EA4745 /* LutoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06A52A5487D200EA4745 /* LutoApp.swift */; }; - 682D06A82A5487D200EA4745 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06A72A5487D200EA4745 /* ContentView.swift */; }; + 682D06A82A5487D200EA4745 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06A72A5487D200EA4745 /* MainView.swift */; }; 682D06AA2A5487D500EA4745 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 682D06A92A5487D500EA4745 /* Assets.xcassets */; }; 682D06AD2A5487D500EA4745 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 682D06AC2A5487D500EA4745 /* Preview Assets.xcassets */; }; 682D06B82A5487D600EA4745 /* LutoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06B72A5487D600EA4745 /* LutoTests.swift */; }; 682D06C22A5487D600EA4745 /* LutoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06C12A5487D600EA4745 /* LutoUITests.swift */; }; 682D06C42A5487D600EA4745 /* LutoUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06C32A5487D600EA4745 /* LutoUITestsLaunchTests.swift */; }; + 6838051A2A57356700CEF29C /* TextFieldExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683805192A57356700CEF29C /* TextFieldExtensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,9 +36,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 680A62112A5F475D004C21A4 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = ""; }; 682D06A22A5487D200EA4745 /* Luto.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Luto.app; sourceTree = BUILT_PRODUCTS_DIR; }; 682D06A52A5487D200EA4745 /* LutoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LutoApp.swift; sourceTree = ""; }; - 682D06A72A5487D200EA4745 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 682D06A72A5487D200EA4745 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 682D06A92A5487D500EA4745 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 682D06AC2A5487D500EA4745 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 682D06AE2A5487D500EA4745 /* Luto.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Luto.entitlements; sourceTree = ""; }; @@ -45,6 +48,7 @@ 682D06BD2A5487D600EA4745 /* LutoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LutoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 682D06C12A5487D600EA4745 /* LutoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LutoUITests.swift; sourceTree = ""; }; 682D06C32A5487D600EA4745 /* LutoUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LutoUITestsLaunchTests.swift; sourceTree = ""; }; + 683805192A57356700CEF29C /* TextFieldExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldExtensions.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,6 +76,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 680A620F2A5F4739004C21A4 /* Utils */ = { + isa = PBXGroup; + children = ( + 683805192A57356700CEF29C /* TextFieldExtensions.swift */, + 680A62112A5F475D004C21A4 /* ViewExtensions.swift */, + ); + path = Utils; + sourceTree = ""; + }; 682D06992A5487D200EA4745 = { isa = PBXGroup; children = ( @@ -95,8 +108,8 @@ 682D06A42A5487D200EA4745 /* Luto */ = { isa = PBXGroup; children = ( + 683805182A57354900CEF29C /* UI */, 682D06A52A5487D200EA4745 /* LutoApp.swift */, - 682D06A72A5487D200EA4745 /* ContentView.swift */, 682D06A92A5487D500EA4745 /* Assets.xcassets */, 682D06AE2A5487D500EA4745 /* Luto.entitlements */, 682D06AB2A5487D500EA4745 /* Preview Content */, @@ -129,6 +142,15 @@ path = LutoUITests; sourceTree = ""; }; + 683805182A57354900CEF29C /* UI */ = { + isa = PBXGroup; + children = ( + 682D06A72A5487D200EA4745 /* MainView.swift */, + 680A620F2A5F4739004C21A4 /* Utils */, + ); + path = UI; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -259,7 +281,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 682D06A82A5487D200EA4745 /* ContentView.swift in Sources */, + 682D06A82A5487D200EA4745 /* MainView.swift in Sources */, + 6838051A2A57356700CEF29C /* TextFieldExtensions.swift in Sources */, + 680A62122A5F475D004C21A4 /* ViewExtensions.swift in Sources */, 682D06A62A5487D200EA4745 /* LutoApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Luto/Assets.xcassets/AccentColor.colorset/Contents.json b/Luto/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..e8393cf 100644 --- a/Luto/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Luto/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0", + "green" : "205", + "red" : "255" + } + }, "idiom" : "universal" } ], diff --git a/Luto/ContentView.swift b/Luto/ContentView.swift deleted file mode 100644 index 7b8c149..0000000 --- a/Luto/ContentView.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ContentView.swift -// Luto -// -// Created by Pierre Boulc'h on 04/07/2023. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") - } - .padding() - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/Luto/LutoApp.swift b/Luto/LutoApp.swift index 98283c6..6c85115 100644 --- a/Luto/LutoApp.swift +++ b/Luto/LutoApp.swift @@ -9,9 +9,51 @@ import SwiftUI @main struct LutoApp: App { + + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + var body: some Scene { - WindowGroup { - ContentView() + WindowGroup(id: "MainWindow") { + MainView(taskTitle: "", taskDescription: "", listTask: []) } } } + +class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { + + private var statusItem: NSStatusItem! + private var popover: NSPopover! + + @MainActor func applicationDidFinishLaunching(_ notification: Notification) { + + if let window = NSApplication.shared.windows.first { + window.close() + } + + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + + if let statusButton = statusItem.button { + statusButton.image = NSImage(systemSymbolName: "brain", accessibilityDescription: "Chart Line") + statusButton.action = #selector(togglePopover) + } + + self.popover = NSPopover() + self.popover.contentSize = NSSize(width: 500, height: 500) + self.popover.behavior = .transient + self.popover.contentViewController = NSHostingController(rootView: MainView(taskTitle: "", taskDescription: "", listTask: []) +) + } + + @objc func togglePopover() { + + if let button = statusItem.button { + if popover.isShown { + self.popover.performClose(nil) + } else { + popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) + } + } + + } + +} diff --git a/Luto/UI/MainView.swift b/Luto/UI/MainView.swift new file mode 100644 index 0000000..084c537 --- /dev/null +++ b/Luto/UI/MainView.swift @@ -0,0 +1,94 @@ +// +// ContentView.swift +// Luto +// +// Created by Pierre Boulc'h on 04/07/2023. +// + +import SwiftUI +import AppKit + +struct MainView: View { + + @State var taskTitle = "" + @State var taskDescription = "" + @State var listTask: [String] = [] + + @State var showAdditionnalFields = false + @FocusState private var titleFieldInFocus: Bool + @FocusState private var descriptionFieldInFocus: Bool + + + var body: some View { + VStack { + Text("Mes tâches") + LazyVStack(alignment: .leading) { + ForEach(Array(listTask.enumerated()), id: \.offset) { index, task in + HStack { + Text("\(task)") + .padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 8)) + Spacer() + Button(action: { + removeTask(index: index) + }, label: { + Image(systemName: "trash") + }).buttonStyle(PlainButtonStyle()).padding(12) + .foregroundColor(.accentColor) + }.border(width: 5, edges: [.leading], color: .accentColor) + } + } + Spacer() + HStack { + TextField("Titre de la tâche ...", text: $taskTitle).focused($titleFieldInFocus).onChange(of: titleFieldInFocus) { isFocused in + if !isFocused { + withAnimation { + showAdditionnalFields = true + } + } + }.onSubmit { + addTask() + } + Button(action: { + addTask() + }, label: { + Image(systemName: "plus") + }).buttonStyle(PlainButtonStyle()).padding(12) + .background(Color.accentColor) + .foregroundColor(.black) + .cornerRadius(24) + }.textFieldStyle(OvalTextFieldStyle()) + if showAdditionnalFields { + TextField("Description", text: $taskDescription).focused($descriptionFieldInFocus) + } + } + .padding() + .frame(width: 500, height: 500) + } + + func addTask() { + if !taskTitle.isEmpty { + withAnimation { + listTask.append(taskTitle) + } + taskTitle = "" + } + } + + func removeTask(index: Int) { + _ = withAnimation { + listTask.remove(at: index) + } + } + + init(taskTitle: String = "", taskDescription: String = "", listTask: [String]) { + self.taskTitle = taskTitle + self.taskDescription = taskDescription + self.listTask = listTask + } +} + +struct MainView_Previews: PreviewProvider { + static var previews: some View { + MainView(taskTitle: "", taskDescription: "", listTask: []) + } +} diff --git a/Luto/UI/Utils/TextFieldExtensions.swift b/Luto/UI/Utils/TextFieldExtensions.swift new file mode 100644 index 0000000..7795469 --- /dev/null +++ b/Luto/UI/Utils/TextFieldExtensions.swift @@ -0,0 +1,32 @@ +// +// TextFieldExtensions.swift +// Luto +// +// Created by Pierre Boulc'h on 06/07/2023. +// + +import Foundation +import SwiftUI + +struct OvalTextFieldStyle: TextFieldStyle { + func _body(configuration: TextField) -> some View { + configuration + .textFieldStyle(.plain) + .colorMultiply(.gray) + .padding(10) + .tint(.gray) + .foregroundColor(.black) + .background(.white) + .cornerRadius(20) + .shadow(color: .accentColor, radius: 2) + + } +} + +extension NSTextField { + open override var focusRingType: NSFocusRingType { + get { .none } + set { } + } +} + diff --git a/Luto/UI/Utils/ViewExtensions.swift b/Luto/UI/Utils/ViewExtensions.swift new file mode 100644 index 0000000..c9822aa --- /dev/null +++ b/Luto/UI/Utils/ViewExtensions.swift @@ -0,0 +1,31 @@ +// +// ViewExtensions.swift +// Luto +// +// Created by Pierre Boulc'h on 12/07/2023. +// + +import Foundation +import SwiftUI + +extension View { + func border(width: CGFloat, edges: [Edge], color: Color) -> some View { + overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color)) + } +} + +struct EdgeBorder: Shape { + var width: CGFloat + var edges: [Edge] + + func path(in rect: CGRect) -> Path { + edges.map { edge -> Path in + switch edge { + case .top: return Path(.init(x: rect.minX, y: rect.minY, width: rect.width, height: width)) + case .bottom: return Path(.init(x: rect.minX, y: rect.maxY - width, width: rect.width, height: width)) + case .leading: return Path(.init(x: rect.minX, y: rect.minY, width: width, height: rect.height)) + case .trailing: return Path(.init(x: rect.maxX - width, y: rect.minY, width: width, height: rect.height)) + } + }.reduce(into: Path()) { $0.addPath($1) } + } +}