Skip to content

Commit 0b9219e

Browse files
committed
added saving of QR at application level and text limit
1 parent 4e8c769 commit 0b9219e

File tree

4 files changed

+162
-57
lines changed

4 files changed

+162
-57
lines changed

QRCodeGenerator/QRCodeGenerator/ContentView.swift

Lines changed: 157 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,27 @@ struct ContentView: View {
1212
// QR text + image
1313
@State private var qrInputtext = ""
1414
@State private var image: NSImage?
15+
private let maxQRBytes = 1273 // QR v40-H byte capacity (byte mode)
16+
@State private var limitWarning: String? = nil
17+
@State private var previousValidText: String = ""
1518

16-
// File management
19+
// File management (app-managed root in Application Support)
1720
@State private var rootFolderURL: URL?
1821
@State private var tree: FileNode?
1922
@State private var selectedFileURL: URL?
2023
@State private var statusMessage: String = ""
24+
// Name sheet (for Save As / Rename without NSSavePanel)
25+
@State private var showNameSheet: Bool = false
26+
@State private var nameSheetTitle: String = ""
27+
@State private var nameField: String = ""
28+
private enum NameAction { case saveAs, rename }
29+
@State private var pendingAction: NameAction? = nil
2130

2231
var body: some View {
2332
HStack(spacing: 0) {
2433
// Sidebar: folder tree and controls
2534
VStack(alignment: .leading, spacing: 8) {
2635
HStack {
27-
Button("Choose Root Folder") { chooseRootFolder() }
2836
Button("Refresh") { refreshTree() }
2937
.disabled(rootFolderURL == nil)
3038
}
@@ -46,16 +54,16 @@ struct ContentView: View {
4654
selectedFileURL = node.url
4755
if let txt = FileScanner.readText(from: node.url) {
4856
qrInputtext = txt
49-
image = QRCodeGenerator.getQRImageUsingNew(qrcode: qrInputtext)
57+
enforceLimitAndUpdate()
5058
}
5159
}
5260
}
5361
}
5462
}
5563
} else {
5664
VStack(alignment: .leading, spacing: 8) {
57-
Text("No root folder selected.")
58-
Text("Click ‘Choose Root Folder’ to browse and manage QR texts.")
65+
Text("Preparing app library…")
66+
Text("Your QR files are saved in the app’s Application Support folder.")
5967
.foregroundColor(.secondary)
6068
}
6169
.padding()
@@ -77,8 +85,13 @@ struct ContentView: View {
7785
.frame(minHeight: 30, maxHeight: 80)
7886
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2)))
7987
.onChange(of: qrInputtext) { _ in
80-
image = QRCodeGenerator.getQRImageUsingNew(qrcode: qrInputtext)
88+
enforceLimitAndUpdate()
8189
}
90+
if let warning = limitWarning {
91+
Text(warning)
92+
.font(.caption)
93+
.foregroundColor(.red)
94+
}
8295
}
8396
.padding([.top, .horizontal, .bottom])
8497

@@ -102,48 +115,45 @@ struct ContentView: View {
102115
}
103116
}
104117
.onAppear {
118+
// Ensure app root exists and load tree
119+
self.rootFolderURL = ensureAppRoot()
120+
refreshTree()
105121
// Initial render if any prefilled text
106122
image = QRCodeGenerator.getQRImageUsingNew(qrcode: qrInputtext)
123+
previousValidText = qrInputtext
124+
}
125+
.sheet(isPresented: $showNameSheet) {
126+
VStack(alignment: .leading, spacing: 12) {
127+
Text(nameSheetTitle).font(.headline)
128+
TextField("Filename", text: $nameField)
129+
.textFieldStyle(RoundedBorderTextFieldStyle())
130+
.frame(minWidth: 360)
131+
Text("Allowed extensions: \(Array(FileScanner.allowedExtensions).sorted().joined(separator: ", "))")
132+
.font(.caption)
133+
.foregroundColor(.secondary)
134+
HStack {
135+
Spacer()
136+
Button("Cancel") { showNameSheet = false }
137+
Button("Save") { performNameAction() }.keyboardShortcut(.defaultAction)
138+
}
139+
}
140+
.padding(20)
107141
}
108142
}
109143

110144
// MARK: - Actions
111145

112-
private func chooseRootFolder() {
113-
let panel = NSOpenPanel()
114-
panel.canChooseDirectories = true
115-
panel.canChooseFiles = false
116-
panel.allowsMultipleSelection = false
117-
panel.prompt = "Choose"
118-
if panel.runModal() == .OK, let url = panel.url {
119-
rootFolderURL = url
120-
refreshTree()
121-
status("Root set to \(url.lastPathComponent)")
122-
}
123-
}
124-
125146
private func refreshTree() {
126147
guard let root = rootFolderURL else { return }
127148
tree = FileScanner.buildTree(at: root)
128149
}
129150

130151
private func saveAs() {
131-
guard let root = rootFolderURL else { return }
132-
// Ask for filename
133-
let savePanel = NSSavePanel()
134-
savePanel.directoryURL = root
135-
savePanel.allowedFileTypes = ["qr", "txt", "qrtext"]
136-
savePanel.nameFieldStringValue = suggestFileName()
137-
if savePanel.runModal() == .OK, let url = savePanel.url {
138-
do {
139-
try FileScanner.writeText(qrInputtext, to: url)
140-
selectedFileURL = url
141-
refreshTree()
142-
status("Saved \(url.lastPathComponent)")
143-
} catch {
144-
status("Save failed: \(error.localizedDescription)")
145-
}
146-
}
152+
// Prompt for filename within app library
153+
nameSheetTitle = "Save As"
154+
nameField = suggestFileName() + ".txt"
155+
pendingAction = .saveAs
156+
showNameSheet = true
147157
}
148158

149159
private func saveCurrent() {
@@ -169,22 +179,10 @@ struct ContentView: View {
169179

170180
private func renameCurrent() {
171181
guard let currentURL = selectedFileURL else { return }
172-
let panel = NSSavePanel()
173-
panel.directoryURL = currentURL.deletingLastPathComponent()
174-
panel.nameFieldStringValue = currentURL.lastPathComponent
175-
panel.allowedFileTypes = Array(FileScanner.allowedExtensions)
176-
panel.prompt = "Rename"
177-
if panel.runModal() == .OK, let newURL = panel.url {
178-
guard newURL != currentURL else { return }
179-
do {
180-
try FileManager.default.moveItem(at: currentURL, to: newURL)
181-
selectedFileURL = newURL
182-
refreshTree()
183-
status("Renamed to \(newURL.lastPathComponent)")
184-
} catch {
185-
status("Rename failed: \(error.localizedDescription)")
186-
}
187-
}
182+
nameSheetTitle = "Rename"
183+
nameField = currentURL.lastPathComponent
184+
pendingAction = .rename
185+
showNameSheet = true
188186
}
189187

190188
private func status(_ message: String) {
@@ -202,6 +200,114 @@ struct ContentView: View {
202200
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
203201
return formatter.string(from: Date())
204202
}
203+
204+
// MARK: - App folder helpers
205+
206+
private func ensureAppRoot() -> URL? {
207+
let fm = FileManager.default
208+
let base = (try? fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true))
209+
let bundleID = Bundle.main.bundleIdentifier ?? "TextToQR"
210+
guard let baseURL = base else { return nil }
211+
let appRoot = baseURL.appendingPathComponent(bundleID, isDirectory: true)
212+
.appendingPathComponent("QRCodes", isDirectory: true)
213+
do {
214+
try fm.createDirectory(at: appRoot, withIntermediateDirectories: true)
215+
return appRoot
216+
} catch {
217+
status("Could not create app folder: \(error.localizedDescription)")
218+
return nil
219+
}
220+
}
221+
222+
private func forceIntoRoot(_ url: URL, root: URL) -> URL {
223+
// If user navigates outside the app root in the save panel, force saving under root with chosen filename
224+
let standardized = url.standardizedFileURL
225+
if standardized.path.hasPrefix(root.standardizedFileURL.path) {
226+
return standardized
227+
}
228+
return root.appendingPathComponent(url.lastPathComponent)
229+
}
230+
231+
// MARK: - Input limit enforcement
232+
233+
private func asciiByteCount(_ text: String) -> Int? {
234+
text.data(using: .ascii)?.count
235+
}
236+
237+
private func truncateToMaxASCIIBytes(_ text: String, max: Int) -> String {
238+
var result = ""
239+
var count = 0
240+
for ch in text {
241+
let s = String(ch)
242+
guard let bytes = s.data(using: .ascii) else { break }
243+
if count + bytes.count > max { break }
244+
result.append(ch)
245+
count += bytes.count
246+
}
247+
return result
248+
}
249+
250+
private func enforceLimitAndUpdate() {
251+
// Reject non-ASCII and enforce max byte length for current QR encoding settings
252+
guard let byteCount = asciiByteCount(qrInputtext) else {
253+
limitWarning = "Only ASCII characters are supported."
254+
// revert to last valid
255+
qrInputtext = previousValidText
256+
return
257+
}
258+
if byteCount > maxQRBytes {
259+
// Truncate and warn
260+
let truncated = truncateToMaxASCIIBytes(qrInputtext, max: maxQRBytes)
261+
qrInputtext = truncated
262+
limitWarning = "Exceeded max length of \(maxQRBytes) bytes. Extra text truncated."
263+
} else {
264+
limitWarning = nil
265+
previousValidText = qrInputtext
266+
}
267+
image = QRCodeGenerator.getQRImageUsingNew(qrcode: qrInputtext)
268+
}
269+
270+
private func sanitizeFilename(_ name: String) -> String {
271+
var base = name.trimmingCharacters(in: .whitespacesAndNewlines)
272+
if base.isEmpty { base = suggestFileName() }
273+
// Replace illegal chars
274+
base = base.replacingOccurrences(of: "\\s+", with: "_", options: .regularExpression)
275+
.replacingOccurrences(of: "[\\\\/:*?\"<>|]", with: "-", options: .regularExpression)
276+
return base
277+
}
278+
279+
private func ensureAllowedExtension(for filename: String) -> String {
280+
let url = URL(fileURLWithPath: filename)
281+
let ext = url.pathExtension.lowercased()
282+
if FileScanner.allowedExtensions.contains(ext) { return filename }
283+
// default to .txt
284+
return url.deletingPathExtension().lastPathComponent + ".txt"
285+
}
286+
287+
private func performNameAction() {
288+
guard let root = rootFolderURL, let action = pendingAction else { showNameSheet = false; return }
289+
let cleaned = ensureAllowedExtension(for: sanitizeFilename(nameField))
290+
let dest = root.appendingPathComponent(cleaned)
291+
do {
292+
switch action {
293+
case .saveAs:
294+
try FileScanner.writeText(qrInputtext, to: dest)
295+
selectedFileURL = dest
296+
status("Saved \(dest.lastPathComponent)")
297+
case .rename:
298+
if let current = selectedFileURL, current != dest {
299+
try FileManager.default.moveItem(at: current, to: dest)
300+
selectedFileURL = dest
301+
status("Renamed to \(dest.lastPathComponent)")
302+
}
303+
}
304+
refreshTree()
305+
} catch {
306+
status("Operation failed: \(error.localizedDescription)")
307+
}
308+
showNameSheet = false
309+
pendingAction = nil
310+
}
205311
}
206312

207313
struct ContentView_Previews: PreviewProvider {

QRCodeGenerator/QRCodeGenerator/QRCodeGenerator.entitlements

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,5 @@
44
<dict>
55
<key>com.apple.security.app-sandbox</key>
66
<true/>
7-
<key>com.apple.security.files.user-selected.read-write</key>
8-
<true/>
97
</dict>
108
</plist>

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ TextToQR renders a QR code for any text you type. It uses the `CIQRCodeGenerator
1414

1515
## Features
1616

17-
- Folder-based management: choose a root folder; browse subfolders/files
17+
- App-managed storage: files saved under Application Support in the app container (no folder selection)
18+
- Folder-based management within the app’s library; browse subfolders/files
1819
- Load/save QR texts as files (`.txt`, `.qr`, `.qrtext`)
1920
- Edit and generate QR without saving; choose to Save or Save As later
2021
- Live preview as you type (no Render button)
@@ -25,8 +26,8 @@ TextToQR renders a QR code for any text you type. It uses the `CIQRCodeGenerator
2526

2627
- Open `QRCodeGenerator/QRCodeGenerator.xcodeproj` in Xcode (macOS app target).
2728
- Build and run.
28-
- Click "Choose Root Folder" to point the app at a directory containing your team's QR text files (supports subfolders).
29-
- Start typing to generate a QR without saving; click Save or Save As to persist.
29+
- The app stores QR texts under `Application Support/<bundle id>/QRCodes`.
30+
- Start typing to generate a QR without saving; click Save or Save As to persist within the app library.
3031
- Or select a file to load and regenerate its QR; edit and Save to update that file.
3132

3233
See `docs/DEVELOPMENT.md` for detailed setup instructions.

docs/DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ See `docs/RELEASE.md` for a lightweight release process.
3838

3939
## Sandbox / Permissions
4040

41-
- The app uses App Sandbox with `com.apple.security.files.user-selected.read-write` so users can choose a folder and save QR text files there.
41+
- The app uses App Sandbox and saves QR text files inside the app container: `Application Support/<bundle id>/QRCodes`. Users do not select arbitrary folders.

0 commit comments

Comments
 (0)