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-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-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-08-23 16:39:42 +02:00
|
|
|
.preferredColorScheme(.dark)
|
|
|
|
}
|
|
|
|
|
|
|
|
@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
|
|
|
}
|
|
|
|
}
|