Skip to content

Commit 1721b1e

Browse files
committed
Add settings menu options for changing data source and resetting save location; refactor ContentView to use user-defined sheet ID
1 parent cf27617 commit 1721b1e

File tree

5 files changed

+216
-34
lines changed

5 files changed

+216
-34
lines changed

README.md

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Daily Word Pro is a sleek and minimalistic macOS menu bar app designed to improv
1212
- Click on the word to search for it on Google for further exploration.
1313
- **Beautiful UI**: Clean and minimal design with hover effects and customizable colors that adapt to macOS's light and dark modes.
1414
- **Custom Word Bank**: Integrates with your Google Sheet for easy customization of words.
15+
- **Settings Menu**: Configure your Google Sheet, reset save location, view about information, and exit the app.
1516

1617
---
1718

@@ -57,11 +58,19 @@ Daily Word Pro is a sleek and minimalistic macOS menu bar app designed to improv
5758

5859
3. **Configure Google Sheets API**:
5960
- Create a Google API key from the [Google Cloud Console](https://console.cloud.google.com/).
60-
- Update your API key and Google Sheet ID in `ContentView.swift`:
61-
```swift
62-
let sheetID = "YOUR_GOOGLE_SHEET_ID"
63-
let apiKey = "YOUR_GOOGLE_API_KEY"
64-
```
61+
- Update your API key and Google Sheet ID in `Config.plist`:
62+
```xml
63+
<?xml version="1.0" encoding="UTF-8"?>
64+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
65+
<plist version="1.0">
66+
<dict>
67+
<key>API_KEY</key>
68+
<string>YOUR_GOOGLE_API_KEY</string>
69+
<key>SHEET_ID</key>
70+
<string>YOUR_GOOGLE_SHEET_ID</string>
71+
</dict>
72+
</plist>
73+
```
6574

6675
4. **Run the App**:
6776
- Select "My Mac" as the target device in Xcode.
@@ -84,18 +93,36 @@ Daily Word Pro is a sleek and minimalistic macOS menu bar app designed to improv
8493
- Update your Google Sheet with new words, meanings, and examples.
8594
- The app will fetch the updated data automatically.
8695

96+
4. **Settings Menu**:
97+
- **Change Data Source**: Configure your Google Sheet link.
98+
- **Reset Save Location**: Reset the location where memorized words are saved.
99+
- **About**: View information about the app.
100+
- **Exit**: Close the app.
101+
87102
---
88103

89104
## Project Structure
90105

91106
```
92107
📦DailyWordPro
93108
┣ 📂DailyWordPro
94-
📜ContentView.swift # Main UI and logic for the app
95-
📜DailyWordProApp.swift # App entry point
96-
📂Assets.xcassets # App icons and color assets
97-
📂DailyWordPro.xcodeproj # Xcode project configuration
98-
📜README.md # Project documentation
109+
┃ ┣ 📜AboutView.swift # About view for the app
110+
┃ ┣ 📜Assets.xcassets # App icons and color assets
111+
┃ ┣ 📜Config.plist # Configuration file for API key and sheet ID
112+
┃ ┣ 📜ContentView.swift # Main UI and logic for the app
113+
┃ ┣ 📜CustomWindow.swift # Custom window implementation
114+
┃ ┣ 📜GoogleSheetResponse.swift # Model for Google Sheet response
115+
┃ ┣ 📜Info.plist # Info.plist for the app
116+
┃ ┣ 📜MemorizedWordsManager.swift # Manager for memorized words
117+
┃ ┣ 📜SettingsMenu.swift # Settings menu for the app
118+
┃ ┣ 📜SheetConfigView.swift # View for configuring Google Sheet
119+
┃ ┣ 📜VocabularyApp.entitlements # App entitlements
120+
┃ ┣ 📜VocabularyAppApp.swift # App entry point
121+
┃ ┗ 📜WordView.swift # View for displaying word details
122+
┣ 📂DailyWordPro.xcodeproj # Xcode project configuration
123+
┣ 📂VocabularyAppTests # Unit tests for the app
124+
┣ 📂VocabularyAppUITests # UI tests for the app
125+
┗ 📜README.md # Project documentation
99126
```
100127
101128
---

VocabularyApp/ContentView.swift

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,15 @@ struct ContentView: View {
66
@State private var meaning: String = "Loading..."
77
@State private var example: String = "Loading..."
88
@ObservedObject private var memorizedWordsManager = MemorizedWordsManager()
9+
@AppStorage("sheetID") private var userSheetID: String = ""
910
private var apiKey: String
10-
private var sheetID: String
11+
private var defaultSheetID: String
1112

1213
@State private var aboutWindow: NSWindow?
1314

14-
init() {
15-
if let path = Bundle.main.path(forResource: "Config", ofType: "plist"),
16-
let config = NSDictionary(contentsOfFile: path),
17-
let apiKey = config["API_KEY"] as? String,
18-
let sheetID = config["SHEET_ID"] as? String {
19-
self.apiKey = apiKey
20-
self.sheetID = sheetID
21-
} else {
22-
fatalError("API_KEY or SHEET_ID not found in Config.plist")
23-
}
15+
init(apiKey: String, defaultSheetID: String) {
16+
self.apiKey = apiKey
17+
self.defaultSheetID = defaultSheetID
2418
}
2519

2620
var body: some View {
@@ -124,6 +118,8 @@ struct ContentView: View {
124118
}
125119

126120
func loadRandomWord() {
121+
let sheetID = userSheetID.isEmpty ? defaultSheetID : userSheetID
122+
127123
let url = URL(string: "https://sheets.googleapis.com/v4/spreadsheets/\(sheetID)/values/Sheet1?key=\(apiKey)")!
128124

129125
var request = URLRequest(url: url)
@@ -151,7 +147,7 @@ struct ContentView: View {
151147
DispatchQueue.main.async {
152148
word = "No more words!"
153149
meaning = "You have memorized all words."
154-
example = ""
150+
example = "Please add more words to your Google Sheet."
155151
}
156152
}
157153
} catch {

VocabularyApp/SettingsMenu.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ struct SettingsMenu: View {
44
var resetSaveLocation: () -> Void
55
var showAbout: () -> Void
66
var exitApp: () -> Void
7+
@State private var showingSheetConfig = false
78

89
var body: some View {
910
Menu {
11+
Button("Change Data Source") {
12+
showingSheetConfig.toggle()
13+
}
1014
Button("Reset Save Location") {
1115
resetSaveLocation()
1216
}
@@ -23,5 +27,8 @@ struct SettingsMenu: View {
2327
.foregroundColor(.primary)
2428
}
2529
.buttonStyle(PlainButtonStyle())
30+
.sheet(isPresented: $showingSheetConfig) {
31+
SheetConfigView()
32+
}
2633
}
2734
}

VocabularyApp/SheetConfigView.swift

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import SwiftUI
2+
3+
struct SheetConfigView: View {
4+
@AppStorage("sheetID") private var userSheetID: String = ""
5+
@Environment(\.dismiss) var dismiss
6+
@State private var sheetLink: String = ""
7+
@State private var isValid: Bool = false
8+
@State private var errorMessage: String?
9+
10+
var body: some View {
11+
VStack(spacing: 16) {
12+
Text("Google Sheet Configuration")
13+
.font(.headline)
14+
.frame(maxWidth: .infinity, alignment: .leading)
15+
16+
Text("1. Create a copy of the template sheet")
17+
.font(.subheadline)
18+
.frame(maxWidth: .infinity, alignment: .leading)
19+
20+
Link("Open Template",
21+
destination: URL(string: "https://docs.google.com/spreadsheets/d/1U66wi1O42CeuC_7QuMTbF2hlewJinCAmgTOJpZFbV1k/copy")!)
22+
.frame(maxWidth: .infinity, alignment: .center)
23+
24+
Text("2. Change the sharing settings to 'Anyone with the link'")
25+
.font(.subheadline)
26+
.foregroundColor(.red)
27+
.frame(maxWidth: .infinity, alignment: .leading)
28+
.fixedSize(horizontal: false, vertical: true)
29+
30+
Text("3. Copy your Google Sheet link and paste below")
31+
.font(.subheadline)
32+
.frame(maxWidth: .infinity, alignment: .leading)
33+
34+
TextField("Google Sheet Link", text: $sheetLink)
35+
.textFieldStyle(.roundedBorder)
36+
.frame(width: 280)
37+
38+
if let errorMessage = errorMessage {
39+
Text(errorMessage)
40+
.foregroundColor(.red)
41+
.font(.caption)
42+
.frame(maxWidth: .infinity, alignment: .leading)
43+
}
44+
45+
HStack {
46+
Button("Save") {
47+
validateAndSave()
48+
}
49+
.disabled(sheetLink.isEmpty)
50+
51+
Button("Close") {
52+
dismiss()
53+
}
54+
.foregroundColor(.red)
55+
}
56+
57+
Text("Sheet format: word | meaning | example")
58+
.font(.caption)
59+
.foregroundColor(.secondary)
60+
.frame(maxWidth: .infinity, alignment: .center)
61+
}
62+
.padding()
63+
.frame(width: 320)
64+
}
65+
66+
private func validateAndSave() {
67+
guard let path = Bundle.main.path(forResource: "Config", ofType: "plist"),
68+
let config = NSDictionary(contentsOfFile: path),
69+
let apiKey = config["API_KEY"] as? String else {
70+
fatalError("API_KEY not found in Config.plist")
71+
}
72+
73+
guard let extractedSheetID = extractSheetID(from: sheetLink) else {
74+
errorMessage = "Invalid Google Sheet link. Please ensure the link is correct."
75+
return
76+
}
77+
78+
let url = URL(string: "https://sheets.googleapis.com/v4/spreadsheets/\(extractedSheetID)/values/Sheet1?key=\(apiKey)")!
79+
80+
var request = URLRequest(url: url)
81+
request.httpMethod = "GET"
82+
83+
URLSession.shared.dataTask(with: request) { data, response, error in
84+
guard let data = data, error == nil else {
85+
DispatchQueue.main.async {
86+
self.errorMessage = "Failed to fetch data. Please check your Google Sheet link."
87+
}
88+
return
89+
}
90+
91+
do {
92+
let decodedResponse = try JSONDecoder().decode(GoogleSheetResponse.self, from: data)
93+
let columns = decodedResponse.values.first ?? []
94+
95+
if columns.contains("Word") && columns.contains("Meaning") && columns.contains("Example") {
96+
DispatchQueue.main.async {
97+
self.userSheetID = extractedSheetID
98+
self.errorMessage = nil
99+
self.dismiss()
100+
if let appDelegate = NSApp.delegate as? AppDelegate {
101+
appDelegate.reloadContentView()
102+
appDelegate.contentView?.loadRandomWord()
103+
}
104+
}
105+
} else {
106+
DispatchQueue.main.async {
107+
self.errorMessage = "Invalid sheet format. Please ensure the sheet contains 'Word', 'Meaning', and 'Example' columns."
108+
}
109+
}
110+
} catch {
111+
DispatchQueue.main.async {
112+
self.errorMessage = "Error decoding data: \(error.localizedDescription)"
113+
}
114+
}
115+
}.resume()
116+
}
117+
118+
private func extractSheetID(from link: String) -> String? {
119+
let pattern = "https://docs.google.com/spreadsheets/d/([a-zA-Z0-9-_]+)"
120+
let regex = try? NSRegularExpression(pattern: pattern)
121+
let nsString = link as NSString
122+
let results = regex?.matches(in: link, range: NSRange(location: 0, length: nsString.length))
123+
let sheetID = results?.first.map { nsString.substring(with: $0.range(at: 1)) }
124+
return sheetID
125+
}
126+
}

VocabularyApp/VocabularyAppApp.swift

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,23 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2323
var popover: NSPopover?
2424
var eventMonitor: EventMonitor?
2525
var contentView: ContentView?
26+
private var apiKey: String
27+
private var defaultSheetID: String
28+
29+
override init() {
30+
if let path = Bundle.main.path(forResource: "Config", ofType: "plist"),
31+
let config = NSDictionary(contentsOfFile: path),
32+
let apiKey = config["API_KEY"] as? String,
33+
let sheetID = config["SHEET_ID"] as? String {
34+
self.apiKey = apiKey
35+
self.defaultSheetID = sheetID
36+
} else {
37+
fatalError("API_KEY or SHEET_ID not found in Config.plist")
38+
}
39+
}
2640

2741
func applicationDidFinishLaunching(_ notification: Notification) {
28-
contentView = ContentView()
29-
30-
popover = NSPopover()
31-
popover?.contentSize = NSSize(width: 360, height: 0)
32-
popover?.behavior = .transient
33-
34-
let hostingController = NSHostingController(rootView: contentView!)
35-
hostingController.view.setFrameSize(NSSize(width: 360, height: 1))
36-
37-
hostingController.view.autoresizingMask = [.height]
38-
39-
popover?.contentViewController = hostingController
42+
loadContentView()
4043

4144
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
4245
if let button = statusItem?.button {
@@ -54,6 +57,21 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5457
loadNewWordIfNeeded()
5558
}
5659

60+
func loadContentView() {
61+
contentView = ContentView(apiKey: apiKey, defaultSheetID: defaultSheetID)
62+
63+
popover = NSPopover()
64+
popover?.contentSize = NSSize(width: 360, height: 0)
65+
popover?.behavior = .transient
66+
67+
let hostingController = NSHostingController(rootView: contentView!)
68+
hostingController.view.setFrameSize(NSSize(width: 360, height: 1))
69+
70+
hostingController.view.autoresizingMask = [.height]
71+
72+
popover?.contentViewController = hostingController
73+
}
74+
5775
@objc func togglePopover(_ sender: Any?) {
5876
if let popover = popover {
5977
if popover.isShown {
@@ -92,6 +110,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
92110
userDefaults.set(Date(), forKey: lastLoadedDateKey)
93111
}
94112
}
113+
114+
func reloadContentView() {
115+
loadContentView()
116+
if let popover = popover, popover.isShown {
117+
popover.contentViewController = NSHostingController(rootView: contentView!)
118+
}
119+
contentView?.loadRandomWord()
120+
}
95121
}
96122

97123
class EventMonitor {

0 commit comments

Comments
 (0)