Add multi-language option
This commit is contained in:
parent
55de8ada91
commit
911fc0c8f8
@ -11,7 +11,7 @@ struct CV: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack {
|
||||||
TopView(info: info.top, style: style.header)
|
TopView(info: info.top, style: style.header)
|
||||||
.frame(height: style.header.height)
|
.frame(height: style.header.height)
|
||||||
Rectangle()
|
Rectangle()
|
||||||
@ -53,13 +53,7 @@ struct CV: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
HStack {
|
Text(info.footer)
|
||||||
VStack(alignment: .leading) {
|
|
||||||
ForEach(info.footer) { text in
|
|
||||||
Text(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@ -70,7 +64,7 @@ struct CV: View {
|
|||||||
|
|
||||||
struct CV_Previews: PreviewProvider {
|
struct CV_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
CV(info: cvInfo, style: cvStyle)
|
CV(info: cvInfoEnglish, style: cvStyle)
|
||||||
.previewLayout(.fixed(width: 600, height: 600 * sqrt(2)))
|
.previewLayout(.fixed(width: 600, height: 600 * sqrt(2)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,12 @@ struct ContentView: View {
|
|||||||
@Environment(\.colorScheme)
|
@Environment(\.colorScheme)
|
||||||
var defaultColorScheme: ColorScheme
|
var defaultColorScheme: ColorScheme
|
||||||
|
|
||||||
let info: CVInfo
|
let content: [CVInfo]
|
||||||
|
|
||||||
let style: CVStyle
|
let style: CVStyle
|
||||||
|
|
||||||
init(info: CVInfo, style: CVStyle) {
|
init(content: [CVInfo], style: CVStyle) {
|
||||||
self.info = info
|
self.content = content
|
||||||
self.style = style
|
self.style = style
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,19 +21,35 @@ struct ContentView: View {
|
|||||||
@State
|
@State
|
||||||
var didReadDarkMode = false
|
var didReadDarkMode = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var selectedLanguageIndex = 0
|
||||||
|
|
||||||
var colorStyle: ColorScheme {
|
var colorStyle: ColorScheme {
|
||||||
darkModeEnabled ? .dark : .light
|
darkModeEnabled ? .dark : .light
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var info: CVInfo {
|
||||||
|
content[selectedLanguageIndex]
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
|
Picker("", selection: $selectedLanguageIndex) {
|
||||||
|
ForEach(Array(content.enumerated()), id: \.element) { content in
|
||||||
|
Text(content.element.language)
|
||||||
|
.tag(content.offset)
|
||||||
|
}
|
||||||
|
}.frame(width: 100)
|
||||||
Button(action: createAndSavePDF) {
|
Button(action: createAndSavePDF) {
|
||||||
Label("Save", systemSymbol: .squareAndArrowUp)
|
Label("Export PDF", systemSymbol: .squareAndArrowDown)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
Spacer()
|
||||||
Toggle("Dark mode", isOn: $darkModeEnabled)
|
Toggle("Dark mode", isOn: $darkModeEnabled)
|
||||||
|
.toggleStyle(SwitchToggleStyle())
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
CV(info: info, style: style)
|
CV(info: info, style: style)
|
||||||
}.frame(width: style.pageWidth)
|
}.frame(width: style.pageWidth)
|
||||||
@ -89,7 +105,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var content: some View {
|
private var renderContent: some View {
|
||||||
CV(info: info, style: style)
|
CV(info: info, style: style)
|
||||||
.frame(width: style.pageWidth, height: style.pageHeight)
|
.frame(width: style.pageWidth, height: style.pageHeight)
|
||||||
}
|
}
|
||||||
@ -97,7 +113,7 @@ struct ContentView: View {
|
|||||||
@MainActor
|
@MainActor
|
||||||
private func renderPDF() -> URL? {
|
private func renderPDF() -> URL? {
|
||||||
let pdfURL = URL.documentsDirectory.appending(path: "cv.pdf")
|
let pdfURL = URL.documentsDirectory.appending(path: "cv.pdf")
|
||||||
let renderer = ImageRenderer(content: content)
|
let renderer = ImageRenderer(content: renderContent)
|
||||||
|
|
||||||
var didFinish = false
|
var didFinish = false
|
||||||
renderer.render { size, context in
|
renderer.render { size, context in
|
||||||
@ -128,7 +144,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView(info: cvInfo, style: cvStyle)
|
ContentView(content: [cvInfoEnglish, cvInfoGerman], style: cvStyle)
|
||||||
.frame(width: 600, height: 600 * sqrt(2))
|
.frame(width: 600, height: 600 * sqrt(2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,13 @@ let cvStyle = CVStyle(
|
|||||||
iconSize: 20,
|
iconSize: 20,
|
||||||
rowSpacing: 3,
|
rowSpacing: 3,
|
||||||
verticalTagSpacing: 3,
|
verticalTagSpacing: 3,
|
||||||
horizontalGap: 5,
|
horizontalGap: 3,
|
||||||
tagBackground: .gray.opacity(0.1),
|
tagBackground: .gray.opacity(0.1),
|
||||||
tagRounding: 8)
|
tagRounding: 8)
|
||||||
)
|
)
|
||||||
|
|
||||||
let cvInfo = CVInfo(
|
let cvInfoEnglish = CVInfo(
|
||||||
|
language: "English",
|
||||||
top: TopInfo(
|
top: TopInfo(
|
||||||
imageName: "Cover",
|
imageName: "Cover",
|
||||||
name: "Christoph Hagen",
|
name: "Christoph Hagen",
|
||||||
@ -114,7 +115,96 @@ let cvInfo = CVInfo(
|
|||||||
"I'm interested in acquiring knowledge and new skills, developing cutting-edge technologies, and finding efficient solutions.",
|
"I'm interested in acquiring knowledge and new skills, developing cutting-edge technologies, and finding efficient solutions.",
|
||||||
"I usually work on various creative projects, including woodworking, electronics, sewing, and programming. I also love being active in nature."
|
"I usually work on various creative projects, including woodworking, electronics, sewing, and programming. I also love being active in nature."
|
||||||
]),
|
]),
|
||||||
footer: [
|
footer: "Design by Christoph Hagen, 2023.")
|
||||||
"Design by Christoph Hagen, 2023.",
|
|
||||||
"Please use the information in this document responsibly. Consider the environmental impact before printing."
|
let cvInfoGerman = CVInfo(
|
||||||
])
|
language: "German",
|
||||||
|
top: TopInfo(
|
||||||
|
imageName: "Cover",
|
||||||
|
name: "Christoph Hagen",
|
||||||
|
tagLine: "Problemlöser und kreativer Kopf mit einer Vorliebe für interdisziplinäre Arbeit.",
|
||||||
|
place: "Würzburg",
|
||||||
|
ageText: "32 Jahre",
|
||||||
|
web: "christophhagen.de",
|
||||||
|
email: "jobs@christophhagen.de",
|
||||||
|
phone: "Auf Anfrage",
|
||||||
|
github: "github.com/christophhagen"),
|
||||||
|
work: .init(title: "Berufserfahrung", items: [
|
||||||
|
CareerStation(
|
||||||
|
time: "Jul 2020 - Jul 2023",
|
||||||
|
location: "Braunschweig",
|
||||||
|
title: "Deutsches Zentrum für Luft- und Raumfahrt",
|
||||||
|
subtitle: "Systemingenieur",
|
||||||
|
text: "Verantwortlich für die Flugzeugsysteme und Avionik, Sicherheitsanalysen, und Software einer hochfliegenden Solardrohne."),
|
||||||
|
CareerStation(
|
||||||
|
time: "Mär 2018 - Dez 2019",
|
||||||
|
location: "Würzburg",
|
||||||
|
title: "Julius-Maximilians-Universität",
|
||||||
|
subtitle: "Wissenschaftlicher Mitarbeiter",
|
||||||
|
text: "Forschung an Privatsphäre- und IT-Sicherheitstechnologien in der Gruppe Secure Software Systems."),
|
||||||
|
CareerStation(
|
||||||
|
time: "Jul 2017 - Okt 2017",
|
||||||
|
location: "Tokio, Japan",
|
||||||
|
title: "National Institute of Informatics",
|
||||||
|
subtitle: "Research Intern (Intelligent Robotics)",
|
||||||
|
text: "Thema: Concept Acquisition through interactions between Humans and Robots"),
|
||||||
|
CareerStation(
|
||||||
|
time: "Sep 2014 - Nov 2016",
|
||||||
|
location: "Würzburg",
|
||||||
|
title: "Julius-Maximilians-Universität",
|
||||||
|
subtitle: "Research & Teaching Assistant",
|
||||||
|
text: "Leitung von Übungen und Robotikworkshops, Entwurf einer modularen Verbindung für einen Roboterarm.")
|
||||||
|
]),
|
||||||
|
education: .init(title: "Bildung", items: [
|
||||||
|
CareerStation(
|
||||||
|
time: "Okt 2015 - Sep 2017",
|
||||||
|
location: "Kiruna, Schweden",
|
||||||
|
title: "Luleå University of Technology",
|
||||||
|
subtitle: "M. Sc. in Space Technology",
|
||||||
|
text: "Erasmus Mundus Double Degree Master mit Kursen in Robotik, Satellitenentwicklung und -kontrolle, Atmosphären- und Weltraumphsyik."),
|
||||||
|
CareerStation(
|
||||||
|
time: "Okt 2015 - Sep 2017",
|
||||||
|
location: "Espoo, Finnland",
|
||||||
|
title: "Aalto University of Electrical Engineering",
|
||||||
|
subtitle: "M. Sc. in Space Robotics and Automation",
|
||||||
|
text: "Abschlussarbeit: A Bluetooth based intra-satellite communication system"),
|
||||||
|
CareerStation(
|
||||||
|
time: "Okt 2013 - Aug 2015",
|
||||||
|
location: "Würzburg",
|
||||||
|
title: "Julius-Maximilians-Universität",
|
||||||
|
subtitle: "B. Sc. in Luft- und Raumfahrtinformatik",
|
||||||
|
text: "Mobile Robotik, Satellitensysteme, Echtzeitbetriebssysteme, Mathematik and Physik.")
|
||||||
|
]),
|
||||||
|
publications: .init(title: "Publikationen", items: [
|
||||||
|
Publication(
|
||||||
|
venue: "33rd Anual INCOSE International Symposium 2023",
|
||||||
|
title: "Model Based Verification and Validation Planning for a Solar Powered High-Altitude Platform"),
|
||||||
|
Publication(
|
||||||
|
venue: "ACM Transactions on Privacy and Security 2022",
|
||||||
|
title: "Contact Discovery in Mobile Messengers: Low-cost Attacks, Quantitative Analyses, and Efficient Mitigations"),
|
||||||
|
Publication(
|
||||||
|
venue: "Network and Distributed Systems Symposium 2021",
|
||||||
|
title: "All the Numbers are US: Large-scale Abuse of Contact Discovery in Mobile Messengers")
|
||||||
|
]),
|
||||||
|
skills: .init(title: "Fähigkeiten", items: [
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .characterBubble,
|
||||||
|
entries: ["Deutsch", "Englisch"]),
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .keyboard,
|
||||||
|
entries: ["Swift", "C", "C++", "Python"]),
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .display2,
|
||||||
|
entries: ["iOS", "Embedded", "macOS", "Linux"]),
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .theatermaskAndPaintbrush,
|
||||||
|
entries: ["UI Design", "CAD", "Holzverarbeitung", "Elektronik", "Foto/Videobearbeitung"]),
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .personFillCheckmark,
|
||||||
|
entries: ["Problemlösung", "Analytisches Denken", "Entscheidungsfindung", "Optimierung"])
|
||||||
|
]),
|
||||||
|
about: .init(title: "Über mich", items: [
|
||||||
|
"Ich eigne mir gerne neues Wissen und Fähigkeiten an, mag die Arbeit an Zukunftstechnologien, und schätze effizente Lösungen für Probleme.",
|
||||||
|
"Ich arbeite oft an verschiedenen Kreativprojekten, unter anderem Möbelbau, Elektronik, Software, oder Näharbeiten. Außerdem bin ich sehr gerne sportlich in der Natur aktiv."
|
||||||
|
]),
|
||||||
|
footer: "Design by Christoph Hagen, 2023.")
|
||||||
|
@ -9,6 +9,8 @@ struct Titled<Content> {
|
|||||||
|
|
||||||
struct CVInfo {
|
struct CVInfo {
|
||||||
|
|
||||||
|
let language: String
|
||||||
|
|
||||||
let top: TopInfo
|
let top: TopInfo
|
||||||
|
|
||||||
let work: Titled<CareerStation>
|
let work: Titled<CareerStation>
|
||||||
@ -21,5 +23,27 @@ struct CVInfo {
|
|||||||
|
|
||||||
let about: Titled<String>
|
let about: Titled<String>
|
||||||
|
|
||||||
let footer: [String]
|
let footer: String
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CVInfo: Identifiable {
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CVInfo: Equatable {
|
||||||
|
|
||||||
|
static func == (lhs: CVInfo, rhs: CVInfo) -> Bool {
|
||||||
|
lhs.language == rhs.language
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CVInfo: Hashable {
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(language)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ struct TitledIconSection: View {
|
|||||||
TitledSection(title: content.title, spacing: titleSpacing) {
|
TitledSection(title: content.title, spacing: titleSpacing) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
ForEach(content.items) { item in
|
ForEach(content.items) { item in
|
||||||
HStack(alignment: .firstTextBaseline) {
|
HStack(alignment: .firstTextBaseline, spacing: 5) {
|
||||||
Image(systemSymbol: item.systemSymbol)
|
Image(systemSymbol: item.systemSymbol)
|
||||||
.frame(
|
.frame(
|
||||||
width: style.iconSize,
|
width: style.iconSize,
|
||||||
|
@ -24,6 +24,7 @@ struct TopView: View {
|
|||||||
RightImageLabel(info.place, systemSymbol: .house)
|
RightImageLabel(info.place, systemSymbol: .house)
|
||||||
.padding(.leading, -4)
|
.padding(.leading, -4)
|
||||||
RightImageLabel(info.ageText, systemSymbol: .hourglass)
|
RightImageLabel(info.ageText, systemSymbol: .hourglass)
|
||||||
|
Spacer()
|
||||||
}.font(.subheadline)
|
}.font(.subheadline)
|
||||||
}
|
}
|
||||||
.frame(width: sideWidth)
|
.frame(width: sideWidth)
|
||||||
@ -63,7 +64,7 @@ struct TopView_Previews: PreviewProvider {
|
|||||||
TopView(info: .init(
|
TopView(info: .init(
|
||||||
imageName: "Cover",
|
imageName: "Cover",
|
||||||
name: "Christoph Hagen",
|
name: "Christoph Hagen",
|
||||||
tagLine: "Problem solver and creative mind with a favour for interdisciplinary work.",
|
tagLine: "Problem solver with a favour for interdisciplinary work.",
|
||||||
place: "Würzburg, Germany",
|
place: "Würzburg, Germany",
|
||||||
ageText: "Age 32",
|
ageText: "Age 32",
|
||||||
web: "christophhagen.de",
|
web: "christophhagen.de",
|
||||||
|
@ -4,7 +4,7 @@ import SwiftUI
|
|||||||
struct ResumeBuilderApp: App {
|
struct ResumeBuilderApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView(info: cvInfo, style: cvStyle)
|
ContentView(content: [cvInfoEnglish, cvInfoGerman], style: cvStyle)
|
||||||
}
|
}.windowResizability(.contentSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user