CHResume/ResumeBuilder/ContentView.swift

287 lines
8.4 KiB
Swift
Raw Permalink Normal View History

2023-08-17 10:12:26 +02:00
import SwiftUI
2023-08-18 22:47:24 +02:00
import SFSafeSymbols
2023-08-21 09:16:45 +02:00
import UniformTypeIdentifiers
2023-08-17 10:12:26 +02:00
struct ContentView: View {
2023-08-18 22:47:24 +02:00
2023-08-18 22:56:11 +02:00
@Environment(\.colorScheme)
var defaultColorScheme: ColorScheme
2023-08-21 09:16:45 +02:00
@State
var content: [CVInfo]
2023-08-18 22:47:24 +02:00
let style: CVStyle
2023-08-18 22:56:11 +02:00
2023-08-20 13:11:13 +02:00
init(content: [CVInfo], style: CVStyle) {
2023-08-21 09:16:45 +02:00
self._content = .init(initialValue: content)
2023-08-18 22:56:11 +02:00
self.style = style
}
@State
var darkModeEnabled = true
@State
var didReadDarkMode = false
2023-08-20 13:11:13 +02:00
@State
var selectedLanguageIndex = 0
2023-08-23 16:39:42 +02:00
@State
private var showExportFormatPicker = false
2023-08-18 22:56:11 +02:00
var colorStyle: ColorScheme {
darkModeEnabled ? .dark : .light
}
2023-08-20 13:11:13 +02:00
var info: CVInfo {
content[selectedLanguageIndex]
}
2023-08-18 22:47:24 +02:00
2023-08-17 10:12:26 +02:00
var body: some View {
2023-08-18 22:47:24 +02:00
VStack(alignment: .leading) {
HStack {
2023-08-20 13:11:13 +02:00
Picker("", selection: $selectedLanguageIndex) {
ForEach(Array(content.enumerated()), id: \.element) { content in
Text(content.element.language)
.tag(content.offset)
}
2023-08-21 09:16:45 +02:00
}.frame(maxWidth: 100)
2023-08-23 16:39:42 +02:00
Button(action: {
showExportFormatPicker = true
}) {
Label("Export", systemSymbol: .squareAndArrowDown)
2023-08-21 09:16:45 +02:00
.padding(3)
2023-11-10 13:46:43 +01:00
.frame(maxHeight: 20)
2023-08-23 16:39:42 +02:00
}.confirmationDialog(
"Which format would you like?",
isPresented: $showExportFormatPicker
) {
Button("JSON data", action: exportJsonData)
Button("PDF", action: exportPdf)
Button("Image", action: exportImage)
Button("Cancel", role: .cancel, action: {})
} message: {
Text(" Choose the export format")
2023-08-21 09:16:45 +02:00
}
Button(action: importData) {
Label("Import", systemSymbol: .docBadgePlus)
.padding(8)
2023-11-10 13:46:43 +01:00
.frame(maxHeight: 20)
2023-08-18 22:47:24 +02:00
}
2023-08-20 13:11:13 +02:00
Spacer()
2023-08-18 22:56:11 +02:00
Toggle("Dark mode", isOn: $darkModeEnabled)
2023-08-20 13:11:13 +02:00
.toggleStyle(SwitchToggleStyle())
2023-08-18 22:47:24 +02:00
}
2023-08-23 16:17:34 +02:00
.padding([.top, .trailing])
.padding(.leading, 6)
.padding(.bottom, 4)
2023-08-18 22:47:24 +02:00
ScrollView(.vertical) {
CV(info: info, style: style)
}.frame(width: style.pageWidth)
}
2023-08-18 22:56:11 +02:00
.preferredColorScheme(colorStyle)
.onAppear {
guard !didReadDarkMode else {
return
}
darkModeEnabled = defaultColorScheme == .dark
didReadDarkMode = true
}
2023-08-18 22:47:24 +02:00
}
2023-08-21 09:16:45 +02:00
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
}
2023-08-23 16:39:42 +02:00
private func exportPdf() {
2023-08-18 22:47:24 +02:00
DispatchQueue.main.async {
2023-08-21 09:16:45 +02:00
guard let url = showPdfSavePanel() else {
print("No url to save PDF")
2023-08-18 22:47:24 +02:00
return
}
2023-08-23 16:39:42 +02:00
savePDF(to: url)
}
}
private func exportImage() {
DispatchQueue.main.async {
guard let url = showJpgSavePanel() else {
print("No image url")
return
}
self.saveImage(to: url)
2023-08-18 22:47:24 +02:00
}
}
2023-08-23 16:39:42 +02:00
private func exportJsonData() {
guard let url = showJsonSavePanel() else {
print("No url to save data")
return
}
saveJson(to: url)
}
2023-08-21 09:16:45 +02:00
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
}
2023-08-23 16:39:42 +02:00
private func showJsonSavePanel() -> URL? {
showSavePanel(
2023-08-21 09:16:45 +02:00
type: .json,
title: "Save JSON",
fileName: "CV.json",
message: "Choose a location to save a JSON file of the resume data")
}
2023-08-23 16:39:42 +02:00
private func showJpgSavePanel() -> URL? {
showSavePanel(
type: .jpeg,
title: "Save image",
fileName: "CV.jpg",
message: "Choose a location to save an image of the resume")
}
2023-08-21 09:16:45 +02:00
private func showPdfSavePanel() -> URL? {
2023-08-23 16:39:42 +02:00
showSavePanel(
2023-08-21 09:16:45 +02:00
type: .pdf,
title: "Save PDF",
fileName: "CV.pdf",
message: "Choose a location to save a PDF of the resume")
}
2023-08-23 16:39:42 +02:00
private func showSavePanel(type: UTType, title: String, fileName: String, message: String) -> URL? {
2023-08-18 22:47:24 +02:00
let savePanel = NSSavePanel()
2023-08-21 09:16:45 +02:00
savePanel.allowedContentTypes = [type]
2023-08-18 22:47:24 +02:00
savePanel.canCreateDirectories = true
savePanel.isExtensionHidden = false
savePanel.allowsOtherFileTypes = false
2023-08-21 09:16:45 +02:00
savePanel.title = title
savePanel.message = message
2023-08-18 22:47:24 +02:00
savePanel.nameFieldLabel = "File name:"
2023-08-21 09:16:45 +02:00
savePanel.nameFieldStringValue = fileName
2023-08-18 22:47:24 +02:00
let response = savePanel.runModal()
guard response == .OK else {
return nil
}
return savePanel.url
}
2023-08-20 13:11:13 +02:00
private var renderContent: some View {
2023-08-18 22:47:24 +02:00
CV(info: info, style: style)
.frame(width: style.pageWidth, height: style.pageHeight)
2023-09-19 14:50:20 +02:00
.preferredColorScheme(colorStyle)
2023-08-23 16:39:42 +02:00
}
@MainActor
private func saveImage(to url: URL) {
let renderer = ImageRenderer(content: renderContent)
renderer.scale = 3
guard let image = renderer.nsImage else {
print("No image from renderer")
return
}
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil)!
let bitmapRep = NSBitmapImageRep(cgImage: cgImage)
let data = bitmapRep.representation(using: .jpeg, properties: [:])!
do {
try data.write(to: url)
print("Data saved")
} catch {
print("Failed to save image: \(error)")
}
2023-08-18 22:47:24 +02:00
}
@MainActor
2023-08-23 16:39:42 +02:00
private func savePDF(to url: URL) {
2023-08-20 13:11:13 +02:00
let renderer = ImageRenderer(content: renderContent)
2023-08-18 22:47:24 +02:00
var didFinish = false
renderer.render { size, context in
var box = CGRect(x: 0, y: 0, width: size.width, height: size.height)
2023-08-23 16:39:42 +02:00
guard let pdf = CGContext(url as CFURL, mediaBox: &box, nil) else {
2023-08-18 22:47:24 +02:00
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 {
2023-08-23 16:39:42 +02:00
return
2023-08-17 10:12:26 +02:00
}
2023-08-18 22:47:24 +02:00
print("PDF created")
2023-08-23 16:39:42 +02:00
}
private func saveJson(to url: URL) {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data: Data
do {
data = try encoder.encode(info)
} catch {
print("Failed to encode data: \(error)")
return
}
do {
try data.write(to: url)
} catch {
print("Failed to write data: \(error)")
}
2023-08-17 10:12:26 +02:00
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
2023-08-20 13:11:13 +02:00
ContentView(content: [cvInfoEnglish, cvInfoGerman], style: cvStyle)
2023-08-18 22:47:24 +02:00
.frame(width: 600, height: 600 * sqrt(2))
2023-08-17 10:12:26 +02:00
}
}