From 911fc0c8f850edb3ec9f4a8a1bfe8f803658bc70 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Sun, 20 Aug 2023 13:11:13 +0200 Subject: [PATCH] Add multi-language option --- ResumeBuilder/CV.swift | 12 +-- ResumeBuilder/ContentView.swift | 30 ++++-- ResumeBuilder/Data.swift | 102 ++++++++++++++++-- ResumeBuilder/Data/CVInfo.swift | 26 ++++- .../Main Elements/TitledIconSection.swift | 2 +- ResumeBuilder/Main Elements/TopView.swift | 3 +- ResumeBuilder/ResumeBuilderApp.swift | 4 +- 7 files changed, 152 insertions(+), 27 deletions(-) diff --git a/ResumeBuilder/CV.swift b/ResumeBuilder/CV.swift index abfed59..96f7122 100644 --- a/ResumeBuilder/CV.swift +++ b/ResumeBuilder/CV.swift @@ -11,7 +11,7 @@ struct CV: View { } var body: some View { - VStack(alignment: .leading) { + VStack { TopView(info: info.top, style: style.header) .frame(height: style.header.height) Rectangle() @@ -53,13 +53,7 @@ struct CV: View { } } Spacer(minLength: 0) - HStack { - VStack(alignment: .leading) { - ForEach(info.footer) { text in - Text(text) - } - } - } + Text(info.footer) .font(.footnote) .foregroundColor(.secondary) } @@ -70,7 +64,7 @@ struct CV: View { struct CV_Previews: PreviewProvider { static var previews: some View { - CV(info: cvInfo, style: cvStyle) + CV(info: cvInfoEnglish, style: cvStyle) .previewLayout(.fixed(width: 600, height: 600 * sqrt(2))) } } diff --git a/ResumeBuilder/ContentView.swift b/ResumeBuilder/ContentView.swift index 1d475aa..96f9e69 100644 --- a/ResumeBuilder/ContentView.swift +++ b/ResumeBuilder/ContentView.swift @@ -6,12 +6,12 @@ struct ContentView: View { @Environment(\.colorScheme) var defaultColorScheme: ColorScheme - let info: CVInfo + let content: [CVInfo] let style: CVStyle - init(info: CVInfo, style: CVStyle) { - self.info = info + init(content: [CVInfo], style: CVStyle) { + self.content = content self.style = style } @@ -21,19 +21,35 @@ struct ContentView: View { @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(width: 100) Button(action: createAndSavePDF) { - Label("Save", systemSymbol: .squareAndArrowUp) + Label("Export PDF", systemSymbol: .squareAndArrowDown) } .padding() + Spacer() Toggle("Dark mode", isOn: $darkModeEnabled) + .toggleStyle(SwitchToggleStyle()) } + .padding(.horizontal) ScrollView(.vertical) { CV(info: info, style: style) }.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) .frame(width: style.pageWidth, height: style.pageHeight) } @@ -97,7 +113,7 @@ struct ContentView: View { @MainActor private func renderPDF() -> URL? { let pdfURL = URL.documentsDirectory.appending(path: "cv.pdf") - let renderer = ImageRenderer(content: content) + let renderer = ImageRenderer(content: renderContent) var didFinish = false renderer.render { size, context in @@ -128,7 +144,7 @@ struct ContentView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView(info: cvInfo, style: cvStyle) + ContentView(content: [cvInfoEnglish, cvInfoGerman], style: cvStyle) .frame(width: 600, height: 600 * sqrt(2)) } } diff --git a/ResumeBuilder/Data.swift b/ResumeBuilder/Data.swift index fb7f7d7..d2c521e 100644 --- a/ResumeBuilder/Data.swift +++ b/ResumeBuilder/Data.swift @@ -20,12 +20,13 @@ let cvStyle = CVStyle( iconSize: 20, rowSpacing: 3, verticalTagSpacing: 3, - horizontalGap: 5, + horizontalGap: 3, tagBackground: .gray.opacity(0.1), tagRounding: 8) ) -let cvInfo = CVInfo( +let cvInfoEnglish = CVInfo( + language: "English", top: TopInfo( imageName: "Cover", 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 usually work on various creative projects, including woodworking, electronics, sewing, and programming. I also love being active in nature." ]), - footer: [ - "Design by Christoph Hagen, 2023.", - "Please use the information in this document responsibly. Consider the environmental impact before printing." - ]) + footer: "Design by Christoph Hagen, 2023.") + +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.") diff --git a/ResumeBuilder/Data/CVInfo.swift b/ResumeBuilder/Data/CVInfo.swift index 0ae0f73..5fc824b 100644 --- a/ResumeBuilder/Data/CVInfo.swift +++ b/ResumeBuilder/Data/CVInfo.swift @@ -9,6 +9,8 @@ struct Titled { struct CVInfo { + let language: String + let top: TopInfo let work: Titled @@ -21,5 +23,27 @@ struct CVInfo { let about: Titled - 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) + } } diff --git a/ResumeBuilder/Main Elements/TitledIconSection.swift b/ResumeBuilder/Main Elements/TitledIconSection.swift index d9fbc6f..3bbfdf2 100644 --- a/ResumeBuilder/Main Elements/TitledIconSection.swift +++ b/ResumeBuilder/Main Elements/TitledIconSection.swift @@ -29,7 +29,7 @@ struct TitledIconSection: View { TitledSection(title: content.title, spacing: titleSpacing) { VStack(alignment: .leading) { ForEach(content.items) { item in - HStack(alignment: .firstTextBaseline) { + HStack(alignment: .firstTextBaseline, spacing: 5) { Image(systemSymbol: item.systemSymbol) .frame( width: style.iconSize, diff --git a/ResumeBuilder/Main Elements/TopView.swift b/ResumeBuilder/Main Elements/TopView.swift index cb206b0..85a6f36 100644 --- a/ResumeBuilder/Main Elements/TopView.swift +++ b/ResumeBuilder/Main Elements/TopView.swift @@ -24,6 +24,7 @@ struct TopView: View { RightImageLabel(info.place, systemSymbol: .house) .padding(.leading, -4) RightImageLabel(info.ageText, systemSymbol: .hourglass) + Spacer() }.font(.subheadline) } .frame(width: sideWidth) @@ -63,7 +64,7 @@ struct TopView_Previews: PreviewProvider { TopView(info: .init( imageName: "Cover", 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", ageText: "Age 32", web: "christophhagen.de", diff --git a/ResumeBuilder/ResumeBuilderApp.swift b/ResumeBuilder/ResumeBuilderApp.swift index 1bdeef5..424c53b 100644 --- a/ResumeBuilder/ResumeBuilderApp.swift +++ b/ResumeBuilder/ResumeBuilderApp.swift @@ -4,7 +4,7 @@ import SwiftUI struct ResumeBuilderApp: App { var body: some Scene { WindowGroup { - ContentView(info: cvInfo, style: cvStyle) - } + ContentView(content: [cvInfoEnglish, cvInfoGerman], style: cvStyle) + }.windowResizability(.contentSize) } }