Skip to content

Commit c606997

Browse files
committed
Add custom window controller, settings menu, and about view; implement memorized words management with user-defined save location
1 parent 02b7658 commit c606997

File tree

19 files changed

+296
-168
lines changed

19 files changed

+296
-168
lines changed

VocabularyApp.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@
388388
buildSettings = {
389389
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
390390
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
391+
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
391392
CODE_SIGN_ENTITLEMENTS = VocabularyApp/VocabularyApp.entitlements;
392393
CODE_SIGN_STYLE = Automatic;
393394
CURRENT_PROJECT_VERSION = 1;
@@ -396,6 +397,8 @@
396397
ENABLE_HARDENED_RUNTIME = YES;
397398
ENABLE_PREVIEWS = YES;
398399
GENERATE_INFOPLIST_FILE = YES;
400+
INFOPLIST_KEY_CFBundleDisplayName = DailyWordPro;
401+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
399402
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
400403
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
401404
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -427,6 +430,7 @@
427430
buildSettings = {
428431
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
429432
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
433+
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
430434
CODE_SIGN_ENTITLEMENTS = VocabularyApp/VocabularyApp.entitlements;
431435
CODE_SIGN_STYLE = Automatic;
432436
CURRENT_PROJECT_VERSION = 1;
@@ -435,6 +439,8 @@
435439
ENABLE_HARDENED_RUNTIME = YES;
436440
ENABLE_PREVIEWS = YES;
437441
GENERATE_INFOPLIST_FILE = YES;
442+
INFOPLIST_KEY_CFBundleDisplayName = DailyWordPro;
443+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
438444
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
439445
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
440446
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;

VocabularyApp/AboutView.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import SwiftUI
2+
import AppKit
3+
4+
struct AboutView: View {
5+
var body: some View {
6+
VStack(spacing: 16) {
7+
Image("AppIcon")
8+
.resizable()
9+
.scaledToFit()
10+
.frame(width: 64, height: 64)
11+
.cornerRadius(16)
12+
Text("DailyWordPro")
13+
.font(.title)
14+
.fontWeight(.bold)
15+
Text("Version 1.0.0")
16+
.font(.subheadline)
17+
.foregroundColor(.secondary)
18+
Divider()
19+
Text("DailyWordPro is a sleek and minimalistic macOS menu bar app designed to help you expand your vocabulary, one word at a time.")
20+
.multilineTextAlignment(.center)
21+
.padding(.horizontal)
22+
Divider()
23+
VStack(spacing: 8) {
24+
Text("Developed by Parth Desai")
25+
.font(.headline)
26+
27+
Link("Visit Website", destination: URL(string: "https://parthdesai.site")!)
28+
.foregroundColor(.blue)
29+
30+
Link("Support Email", destination: URL(string: "mailto:[email protected]")!)
31+
.foregroundColor(.blue)
32+
33+
Link("Follow on X", destination: URL(string: "https://twitter.com/pycoder2000")!)
34+
.foregroundColor(.blue)
35+
}
36+
37+
Text("© 2025 Parth Desai")
38+
.font(.footnote)
39+
.foregroundColor(.secondary)
40+
}
41+
.padding()
42+
.frame(width: 360, height: 400)
43+
}
44+
}
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Lines changed: 1 addition & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1 @@
1-
{
2-
"images" : [
3-
{
4-
"idiom" : "universal",
5-
"platform" : "ios",
6-
"size" : "1024x1024"
7-
},
8-
{
9-
"appearances" : [
10-
{
11-
"appearance" : "luminosity",
12-
"value" : "dark"
13-
}
14-
],
15-
"idiom" : "universal",
16-
"platform" : "ios",
17-
"size" : "1024x1024"
18-
},
19-
{
20-
"appearances" : [
21-
{
22-
"appearance" : "luminosity",
23-
"value" : "tinted"
24-
}
25-
],
26-
"idiom" : "universal",
27-
"platform" : "ios",
28-
"size" : "1024x1024"
29-
},
30-
{
31-
"idiom" : "mac",
32-
"scale" : "1x",
33-
"size" : "16x16"
34-
},
35-
{
36-
"idiom" : "mac",
37-
"scale" : "2x",
38-
"size" : "16x16"
39-
},
40-
{
41-
"idiom" : "mac",
42-
"scale" : "1x",
43-
"size" : "32x32"
44-
},
45-
{
46-
"idiom" : "mac",
47-
"scale" : "2x",
48-
"size" : "32x32"
49-
},
50-
{
51-
"idiom" : "mac",
52-
"scale" : "1x",
53-
"size" : "128x128"
54-
},
55-
{
56-
"idiom" : "mac",
57-
"scale" : "2x",
58-
"size" : "128x128"
59-
},
60-
{
61-
"idiom" : "mac",
62-
"scale" : "1x",
63-
"size" : "256x256"
64-
},
65-
{
66-
"idiom" : "mac",
67-
"scale" : "2x",
68-
"size" : "256x256"
69-
},
70-
{
71-
"idiom" : "mac",
72-
"scale" : "1x",
73-
"size" : "512x512"
74-
},
75-
{
76-
"idiom" : "mac",
77-
"scale" : "2x",
78-
"size" : "512x512"
79-
}
80-
],
81-
"info" : {
82-
"author" : "xcode",
83-
"version" : 1
84-
}
85-
}
1+
{"images":[{"size":"1024x1024","filename":"1024-mac.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]}
Loading
Loading
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"images": [
3+
{
4+
"filename": "128-mac.png",
5+
"idiom": "universal",
6+
"scale": "1x"
7+
},
8+
{
9+
"filename": "256-mac.png",
10+
"idiom": "universal",
11+
"scale": "2x"
12+
}
13+
],
14+
"info": {
15+
"author": "xcode",
16+
"version": 1
17+
}
18+
}

VocabularyApp/ContentView.swift

Lines changed: 37 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
1-
//
2-
// ContentView.swift
3-
// VocabularyApp
4-
//
5-
// Created by Parth Desai on 1/10/25.
6-
//
7-
81
import SwiftUI
92
import AppKit
10-
import UniformTypeIdentifiers
113

124
struct ContentView: View {
135
@State private var word: String = "Loading..."
146
@State private var meaning: String = "Loading..."
157
@State private var example: String = "Loading..."
16-
@State private var memorizedWords: [String] = []
8+
@ObservedObject private var memorizedWordsManager = MemorizedWordsManager()
179
private var apiKey: String
1810
private var sheetID: String
1911

12+
@State private var aboutWindow: NSWindow?
13+
2014
init() {
2115
if let path = Bundle.main.path(forResource: "Config", ofType: "plist"),
2216
let config = NSDictionary(contentsOfFile: path),
@@ -31,7 +25,6 @@ struct ContentView: View {
3125

3226
var body: some View {
3327
VStack(spacing: 16) {
34-
3528
Text(word)
3629
.font(.system(size: 28, weight: .bold))
3730
.foregroundColor(.primary)
@@ -50,7 +43,6 @@ struct ContentView: View {
5043
}
5144
.frame(maxWidth: .infinity, alignment: .center)
5245

53-
5446
VStack(alignment: .leading, spacing: 8) {
5547
Text("Meaning:")
5648
.font(.headline)
@@ -67,7 +59,6 @@ struct ContentView: View {
6759
.padding(.horizontal)
6860
.frame(maxWidth: .infinity, alignment: .leading)
6961

70-
7162
VStack(alignment: .leading, spacing: 8) {
7263
Text("Example:")
7364
.font(.headline)
@@ -87,10 +78,10 @@ struct ContentView: View {
8778

8879
Divider()
8980

90-
9181
HStack {
9282
Button("Memorized it") {
93-
markAsMemorized()
83+
memorizedWordsManager.markAsMemorized(word: word)
84+
loadRandomWord()
9485
}
9586
.buttonStyle(.borderedProminent)
9687
.keyboardShortcut("m", modifiers: [])
@@ -108,8 +99,21 @@ struct ContentView: View {
10899
.background(Color(NSColor.windowBackgroundColor))
109100
.cornerRadius(12)
110101
.shadow(radius: 8)
102+
.overlay(
103+
VStack {
104+
HStack {
105+
Spacer()
106+
SettingsMenu(
107+
resetSaveLocation: memorizedWordsManager.resetSaveLocation,
108+
showAbout: showAbout,
109+
exitApp: exitApp
110+
)
111+
}
112+
Spacer()
113+
}
114+
.padding()
115+
)
111116
.onAppear {
112-
loadMemorizedWords()
113117
loadRandomWord()
114118
}
115119
}
@@ -135,8 +139,7 @@ struct ContentView: View {
135139
let decodedResponse = try JSONDecoder().decode(GoogleSheetResponse.self, from: data)
136140
let rows = decodedResponse.values.dropFirst()
137141

138-
139-
let unmemorizedRows = rows.filter { !memorizedWords.contains($0[0]) }
142+
let unmemorizedRows = rows.filter { !memorizedWordsManager.memorizedWords.contains($0[0]) }
140143

141144
if let randomRow = unmemorizedRows.randomElement() {
142145
DispatchQueue.main.async {
@@ -157,72 +160,24 @@ struct ContentView: View {
157160
}.resume()
158161
}
159162

160-
func markAsMemorized() {
161-
guard !word.isEmpty, !memorizedWords.contains(word) else { return }
162-
163-
if UserDefaults.standard.string(forKey: "memorizedWordsPath") == nil {
164-
promptUserForSaveLocation { selectedURL in
165-
if let selectedURL = selectedURL {
166-
UserDefaults.standard.set(selectedURL.path, forKey: "memorizedWordsPath")
167-
saveMemorizedWords()
168-
loadRandomWord()
169-
} else {
170-
print("User canceled the save location selection.")
171-
}
172-
}
173-
} else {
174-
memorizedWords.append(word)
175-
saveMemorizedWords()
176-
loadRandomWord()
177-
}
178-
}
179-
180-
181-
func saveMemorizedWords() {
182-
let fileURL = getFileURL()
183-
do {
184-
let data = try JSONEncoder().encode(memorizedWords)
185-
try data.write(to: fileURL)
186-
print("Memorized words saved to file.")
187-
} catch {
188-
print("Failed to save memorized words: \(error.localizedDescription)")
163+
func showAbout() {
164+
if aboutWindow == nil {
165+
aboutWindow = NSWindow(
166+
contentRect: NSRect(x: 0, y: 0, width: 360, height: 400),
167+
styleMask: [.titled, .closable, .resizable],
168+
backing: .buffered,
169+
defer: false
170+
)
171+
aboutWindow?.center()
172+
aboutWindow?.title = "About DailyWordPro"
173+
aboutWindow?.contentView = NSHostingView(rootView: AboutView())
174+
aboutWindow?.isReleasedWhenClosed = false
189175
}
176+
aboutWindow?.makeKeyAndOrderFront(nil)
177+
NSApp.activate(ignoringOtherApps: true)
190178
}
191179

192-
func loadMemorizedWords() {
193-
let fileURL = getFileURL()
194-
do {
195-
let data = try Data(contentsOf: fileURL)
196-
memorizedWords = try JSONDecoder().decode([String].self, from: data)
197-
print("Loaded memorized words: \(memorizedWords)")
198-
} catch {
199-
print("No existing file found, starting fresh.")
200-
}
201-
}
202-
203-
func getFileURL() -> URL {
204-
if let savedPath = UserDefaults.standard.string(forKey: "memorizedWordsPath"),
205-
!savedPath.isEmpty {
206-
return URL(fileURLWithPath: savedPath)
207-
} else {
208-
let fileManager = FileManager.default
209-
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
210-
return documentsURL.appendingPathComponent("memorized_words.json")
211-
}
212-
}
213-
214-
func promptUserForSaveLocation(completion: @escaping (URL?) -> Void) {
215-
let savePanel = NSSavePanel()
216-
savePanel.title = "Select Location for Memorized Words"
217-
savePanel.allowedContentTypes = [.json] // Use UTType.json
218-
savePanel.nameFieldStringValue = "memorized_words.json"
219-
220-
savePanel.begin { response in
221-
if response == .OK, let selectedURL = savePanel.url {
222-
completion(selectedURL)
223-
} else {
224-
completion(nil)
225-
}
226-
}
180+
func exitApp() {
181+
NSApp.terminate(nil)
227182
}
228-
}
183+
}

VocabularyApp/CustomWindow.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,5 @@ class CustomWindowController: NSWindowController, NSWindowDelegate {
1919
}
2020

2121
func windowWillClose(_ notification: Notification) {
22-
NSApp.terminate(nil)
2322
}
2423
}

VocabularyApp/Info.plist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2+
<plist version="1.0">
3+
<dict>
4+
<key>CFBundleIconName</key>
5+
<string>AppIcon</string>
6+
<key>LSUIElement</key>
7+
<true/>
8+
<key>XSAppIconAssets</key>
9+
<string>Assets.xcassets/AppIcon.appiconset</string>
10+
</dict>
11+
</plist>

0 commit comments

Comments
 (0)