CHResume/ResumeBuilder/ContentView.swift
2023-08-23 16:17:34 +02:00

246 lines
7.2 KiB
Swift

import SwiftUI
import SFSafeSymbols
import UniformTypeIdentifiers
struct ContentView: View {
@Environment(\.colorScheme)
var defaultColorScheme: ColorScheme
@State
var content: [CVInfo]
let style: CVStyle
init(content: [CVInfo], style: CVStyle) {
self._content = .init(initialValue: content)
self.style = style
}
@State
var darkModeEnabled = true
@State
var didReadDarkMode = false
@State
var selectedLanguageIndex = 0
var colorStyle: ColorScheme {
darkModeEnabled ? .dark : .light
}
var info: CVInfo {
content[selectedLanguageIndex]
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Picker("", selection: $selectedLanguageIndex) {
ForEach(Array(content.enumerated()), id: \.element) { content in
Text(content.element.language)
.tag(content.offset)
}
}.frame(maxWidth: 100)
Button(action: createAndSavePDF) {
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)
}
Spacer()
Toggle("Dark mode", isOn: $darkModeEnabled)
.toggleStyle(SwitchToggleStyle())
}
.padding([.top, .trailing])
.padding(.leading, 6)
.padding(.bottom, 4)
ScrollView(.vertical) {
CV(info: info, style: style)
}.frame(width: style.pageWidth)
}
.preferredColorScheme(colorStyle)
.onAppear {
guard !didReadDarkMode else {
return
}
darkModeEnabled = defaultColorScheme == .dark
didReadDarkMode = true
}
}
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 = showPdfSavePanel() else {
print("No url to save PDF")
return
}
writePDF(at: pdfURL, to: 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 = [type]
savePanel.canCreateDirectories = true
savePanel.isExtensionHidden = false
savePanel.allowsOtherFileTypes = false
savePanel.title = title
savePanel.message = message
savePanel.nameFieldLabel = "File name:"
savePanel.nameFieldStringValue = fileName
let response = savePanel.runModal()
guard response == .OK else {
return nil
}
return savePanel.url
}
private func writePDF(at source: URL, to destination: URL) {
do {
if FileManager.default.fileExists(atPath: destination.path) {
try FileManager.default.removeItem(at: destination)
}
try FileManager.default.copyItem(at: source, to: destination)
} catch {
print("Failed to save pdf: \(error)")
}
}
private var renderContent: some View {
CV(info: info, style: style)
.frame(width: style.pageWidth, height: style.pageHeight)
}
@MainActor
private func renderPDF() -> URL? {
let pdfURL = URL.documentsDirectory.appending(path: "cv.pdf")
let renderer = ImageRenderer(content: renderContent)
var didFinish = false
renderer.render { size, context in
var box = CGRect(x: 0, y: 0, width: size.width, height: size.height)
guard let pdf = CGContext(pdfURL as CFURL, mediaBox: &box, nil) else {
print("Failed to create CGContext")
return
}
let options: [CFString: Any] = [
kCGPDFContextMediaBox: CGRect(origin: .zero, size: size)
]
pdf.beginPDFPage(options as CFDictionary)
context(pdf)
pdf.endPDFPage()
pdf.closePDF()
didFinish = true
}
guard didFinish else {
return nil
}
print("PDF created")
return pdfURL
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(content: [cvInfoEnglish, cvInfoGerman], style: cvStyle)
.frame(width: 600, height: 600 * sqrt(2))
}
}