diff --git a/ResumeBuilder.xcodeproj/project.pbxproj b/ResumeBuilder.xcodeproj/project.pbxproj index 43e585a..c2d3de4 100644 --- a/ResumeBuilder.xcodeproj/project.pbxproj +++ b/ResumeBuilder.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ E267D1BA2A8F9D9C0069112B /* SkillStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1B92A8F9D9C0069112B /* SkillStyle.swift */; }; E267D1BC2A8FFF300069112B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1BB2A8FFF300069112B /* TagView.swift */; }; E267D1C02A9009780069112B /* CVLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1BF2A9009780069112B /* CVLanguage.swift */; }; + E267D1C22A9287900069112B /* Titled.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1C12A9287900069112B /* Titled.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -71,6 +72,7 @@ E267D1B92A8F9D9C0069112B /* SkillStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkillStyle.swift; sourceTree = ""; }; E267D1BB2A8FFF300069112B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; E267D1BF2A9009780069112B /* CVLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVLanguage.swift; sourceTree = ""; }; + E267D1C12A9287900069112B /* Titled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Titled.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -138,6 +140,7 @@ E267D1A12A8E45AE0069112B /* SkillsSet.swift */, E267D19D2A8E45540069112B /* TopInfo.swift */, E267D1AE2A8F69830069112B /* Publication.swift */, + E267D1C12A9287900069112B /* Titled.swift */, ); path = Data; sourceTree = ""; @@ -283,6 +286,7 @@ E267D1A72A8ECC170069112B /* ContentView.swift in Sources */, E267D18E2A8E1BEE0069112B /* RightImageLabel.swift in Sources */, E267D1962A8E3E760069112B /* TitledTextSection.swift in Sources */, + E267D1C22A9287900069112B /* Titled.swift in Sources */, E267D1B82A8F9A2A0069112B /* HeaderStyle.swift in Sources */, E267D18A2A8E140E0069112B /* LeftImageLabel.swift in Sources */, E267D19A2A8E44F40069112B /* TitledIconSection.swift in Sources */, diff --git a/ResumeBuilder/ContentView.swift b/ResumeBuilder/ContentView.swift index 96f9e69..497a171 100644 --- a/ResumeBuilder/ContentView.swift +++ b/ResumeBuilder/ContentView.swift @@ -1,17 +1,19 @@ import SwiftUI import SFSafeSymbols +import UniformTypeIdentifiers struct ContentView: View { @Environment(\.colorScheme) var defaultColorScheme: ColorScheme - let content: [CVInfo] + @State + var content: [CVInfo] let style: CVStyle init(content: [CVInfo], style: CVStyle) { - self.content = content + self._content = .init(initialValue: content) self.style = style } @@ -40,16 +42,24 @@ struct ContentView: View { Text(content.element.language) .tag(content.offset) } - }.frame(width: 100) + }.frame(maxWidth: 100) Button(action: createAndSavePDF) { - Label("Export PDF", systemSymbol: .squareAndArrowDown) + Label("Save PDF", systemSymbol: .squareAndArrowDown) + .padding(3) + } + Button(action: exportData) { + Label("Export", systemSymbol: .squareAndArrowUp) + .padding(3) + } + Button(action: importData) { + Label("Import", systemSymbol: .docBadgePlus) + .padding(8) } - .padding() Spacer() Toggle("Dark mode", isOn: $darkModeEnabled) .toggleStyle(SwitchToggleStyle()) } - .padding(.horizontal) + .padding() ScrollView(.vertical) { CV(info: info, style: style) }.frame(width: style.pageWidth) @@ -64,28 +74,111 @@ struct ContentView: View { } } + private func exportData() { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let data: Data + do { + data = try encoder.encode(info) + } catch { + print("Failed to encode data: \(error)") + return + } + guard let url = showDataSavePanel() else { + print("No url to save data") + return + } + do { + try data.write(to: url) + } catch { + print("Failed to write data: \(error)") + } + } + + private func importData() { + guard let url = showOpenFilePanel() else { + print("No url to import") + return + } + let data: Data + do { + data = try Data(contentsOf: url) + } catch { + print("Failed to open file: \(error)") + return + } + let newData: CVInfo + do { + newData = try JSONDecoder().decode(CVInfo.self, from: data) + } catch { + print("Failed to decode data: \(error)") + return + } + guard let index = content.firstIndex(where: { $0.language == newData.language }) else { + content.append(newData) + selectedLanguageIndex = content.count - 1 + return + } + content[index] = newData + selectedLanguageIndex = index + } + private func createAndSavePDF() { DispatchQueue.main.async { guard let pdfURL = renderPDF() else { return } - guard let url = showSavePanel() else { + guard let url = showPdfSavePanel() else { + print("No url to save PDF") return } writePDF(at: pdfURL, to: url) } } - private func showSavePanel() -> URL? { + private func showOpenFilePanel() -> URL? { + let panel = NSOpenPanel() + panel.allowedContentTypes = [.json] + panel.canCreateDirectories = true + panel.isExtensionHidden = false + panel.allowsOtherFileTypes = false + panel.title = "Load JSON" + panel.message = "Choose a JSON file of resume data to import" + panel.nameFieldLabel = "File name:" + panel.nameFieldStringValue = "CV.json" + let response = panel.runModal() + guard response == .OK else { + return nil + } + return panel.url + } + + private func showDataSavePanel() -> URL? { + showPanel( + type: .json, + title: "Save JSON", + fileName: "CV.json", + message: "Choose a location to save a JSON file of the resume data") + } + + private func showPdfSavePanel() -> URL? { + showPanel( + type: .pdf, + title: "Save PDF", + fileName: "CV.pdf", + message: "Choose a location to save a PDF of the resume") + } + + private func showPanel(type: UTType, title: String, fileName: String, message: String) -> URL? { let savePanel = NSSavePanel() - savePanel.allowedContentTypes = [.pdf] + savePanel.allowedContentTypes = [type] savePanel.canCreateDirectories = true savePanel.isExtensionHidden = false savePanel.allowsOtherFileTypes = false - savePanel.title = "Save PDF" - savePanel.message = "Choose a location to save a PDF of the resume" + savePanel.title = title + savePanel.message = message savePanel.nameFieldLabel = "File name:" - savePanel.nameFieldStringValue = "CV.pdf" + savePanel.nameFieldStringValue = fileName let response = savePanel.runModal() guard response == .OK else { diff --git a/ResumeBuilder/Data/CVInfo.swift b/ResumeBuilder/Data/CVInfo.swift index 5fc824b..000439c 100644 --- a/ResumeBuilder/Data/CVInfo.swift +++ b/ResumeBuilder/Data/CVInfo.swift @@ -1,12 +1,5 @@ import Foundation -struct Titled { - - let title: String - - let items: [Content] -} - struct CVInfo { let language: String @@ -47,3 +40,7 @@ extension CVInfo: Hashable { hasher.combine(language) } } + +extension CVInfo: Codable { + +} diff --git a/ResumeBuilder/Data/CareerStation.swift b/ResumeBuilder/Data/CareerStation.swift index e551d2f..a3291b5 100644 --- a/ResumeBuilder/Data/CareerStation.swift +++ b/ResumeBuilder/Data/CareerStation.swift @@ -16,3 +16,7 @@ struct CareerStation: Identifiable { title + time + location } } + +extension CareerStation: Codable { + +} diff --git a/ResumeBuilder/Data/Publication.swift b/ResumeBuilder/Data/Publication.swift index 9e635e6..7849347 100644 --- a/ResumeBuilder/Data/Publication.swift +++ b/ResumeBuilder/Data/Publication.swift @@ -10,3 +10,7 @@ struct Publication: Identifiable { title + venue } } + +extension Publication: Codable { + +} diff --git a/ResumeBuilder/Data/SkillsSet.swift b/ResumeBuilder/Data/SkillsSet.swift index 16b9838..e7209a5 100644 --- a/ResumeBuilder/Data/SkillsSet.swift +++ b/ResumeBuilder/Data/SkillsSet.swift @@ -11,3 +11,26 @@ struct SkillsSet: Identifiable { entries.joined() } } + +extension SkillsSet: Decodable { + + private enum CodingKeys: String, CodingKey { + case systemSymbol + case entries + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let rawSymbol = try container.decode(String.self, forKey: .systemSymbol) + self.systemSymbol = .init(rawValue: rawSymbol) + self.entries = try container.decode([String].self, forKey: .entries) + } +} + +extension SkillsSet: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(systemSymbol.rawValue, forKey: .systemSymbol) + try container.encode(entries, forKey: .entries) + } +} diff --git a/ResumeBuilder/Data/Titled.swift b/ResumeBuilder/Data/Titled.swift new file mode 100644 index 0000000..e3a6529 --- /dev/null +++ b/ResumeBuilder/Data/Titled.swift @@ -0,0 +1,12 @@ +import Foundation + +struct Titled { + + let title: String + + let items: [Content] +} + +extension Titled: Codable where Content: Codable { + +} diff --git a/ResumeBuilder/Data/TopInfo.swift b/ResumeBuilder/Data/TopInfo.swift index ededd7a..e0f58eb 100644 --- a/ResumeBuilder/Data/TopInfo.swift +++ b/ResumeBuilder/Data/TopInfo.swift @@ -20,3 +20,7 @@ struct TopInfo { let github: String } + +extension TopInfo: Codable { + +}