diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 04a1ec3..b66b0ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -215,9 +215,62 @@ jobs: name: maturita-ios path: dist-ios/maturita-ios.ipa + macos-swift: + name: macOS Swift app + runs-on: macos-26 + steps: + - uses: actions/checkout@v4 + + - name: Import Developer ID certificate + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + if [ -z "${MACOS_CERTIFICATE:-}" ]; then + echo "No Developer ID certificate; the bundle will be ad-hoc signed." + exit 0 + fi + cert="$RUNNER_TEMP/certificate.p12" + keychain="$RUNNER_TEMP/signing.keychain-db" + pass="$(openssl rand -base64 32)" + echo "$MACOS_CERTIFICATE" | base64 --decode > "$cert" + security create-keychain -p "$pass" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$pass" "$keychain" + security import "$cert" -k "$keychain" \ + -P "$MACOS_CERTIFICATE_PASSWORD" \ + -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple: -s \ + -k "$pass" "$keychain" + security list-keychains -d user -s "$keychain" \ + $(security list-keychains -d user | tr -d '"') + rm -f "$cert" + + - name: Build Swift .app zip + env: + COMMIT: ${{ github.sha }} + DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer + MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + run: | + chmod +x scripts/bundle-macos-swift.sh + ./scripts/bundle-macos-swift.sh + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: maturita-macos-swift + path: dist/maturita-macos-swift-*.zip + publish: name: Publish to builds branch - needs: [linux, windows, macos, android, ios] + needs: [linux, windows, macos, android, ios, macos-swift] # One broken platform must not freeze the others: publish whatever was # built and keep the previous package for anything that failed. if: ${{ !cancelled() && contains(needs.*.result, 'success') }} @@ -254,6 +307,7 @@ jobs: # here, so a platform that failed this run is not offered as new. [ -f fresh/maturita-android.apk ] && echo "${{ github.sha }}" > staged/VERSION-android [ -f fresh/maturita-ios.ipa ] && echo "${{ github.sha }}" > staged/VERSION-ios + ls fresh/maturita-macos-swift-*.zip >/dev/null 2>&1 && echo "${{ github.sha }}" > staged/VERSION-macos-swift ls fresh staged # The branch is rewritten from scratch every time rather than added to, diff --git a/.gitignore b/.gitignore index b65f39d..62da60d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,5 +41,10 @@ ios/build/ ios/Payload/ ios/**/xcuserdata/ +# native macOS Swift +macos/DerivedData/ +macos/build/ +macos/**/xcuserdata/ + # Xcode workspace user state *.xcworkspace/xcuserdata/ diff --git a/Makefile b/Makefile index aee3d04..806d15b 100644 --- a/Makefile +++ b/Makefile @@ -124,4 +124,8 @@ ios: chmod +x scripts/bundle-ios.sh ./scripts/bundle-ios.sh -.PHONY: all clean run bundle installer-zips android ios +macos: + chmod +x scripts/bundle-macos-swift.sh + ./scripts/bundle-macos-swift.sh + +.PHONY: all clean run bundle installer-zips android ios macos diff --git a/Maturita.xcworkspace/contents.xcworkspacedata b/Maturita.xcworkspace/contents.xcworkspacedata index 0cf4a51..ce429e1 100644 --- a/Maturita.xcworkspace/contents.xcworkspacedata +++ b/Maturita.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/README.md b/README.md index e6b2fd8..3049df0 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ spolehlivější instalátor výše, nebo stažení přímo z té stránky větv - [maturita-linux-x86_64.AppImage](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-linux-x86_64.AppImage) - [maturita-android.apk](https://github.com/pepaskopec-blip/maturita.c/raw/refs/heads/builds/maturita-android.apk) - [maturita-ios.ipa](https://github.com/pepaskopec-blip/maturita.c/raw/refs/heads/builds/maturita-ios.ipa) +- [maturita-macos-swift-arm64.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-macos-swift-arm64.zip) Postup a nastavení se ukládají vedle `.app` / složky / AppImage do `progress/`. Na Androidu a iOS do úložiště aplikace (stejné statistiky, témata a jazyk). @@ -141,6 +142,7 @@ make bundle # dist/maturita.exe jde spustit i z Průzkumník src/ C zdroje a maturita.h android/ Jetpack Compose přehrávač (stejný obsah) ios/ SwiftUI přehrávač (stejný obsah) +macos/ nativní SwiftUI aplikace pro Mac (stejný obsah) data/ style.css, share/, podpisy, Windows .rc assets/ zdrojová ikona a screenshoty installers/ instalátory, které stáhnou aktuální build @@ -175,9 +177,17 @@ open Maturita.xcworkspace # v kořeni repozitáře # nebo: make ios # unsigned IPA v dist-ios/maturita-ios.ipa ``` -Workspace odkazuje na `ios/Maturita.xcodeproj`. Projekt nepřesouvejte – build -čte `../src` a `../scripts` a v kořeni by `Maturita/` kolidovalo se -zkompilovaným binárem `maturita`. +Nativní macOS ze zdroje (Xcode 26, macOS 26+): + +```bash +open Maturita.xcworkspace # scheme Maturita z macos/Maturita.xcodeproj +# nebo: make macos # zip v dist/maturita-macos-swift-arm64.zip +``` + +Workspace odkazuje na `ios/Maturita.xcodeproj` a `macos/Maturita.xcodeproj`. +Projekty nepřesouvejte – build čte `../src` a `../scripts` a v kořeni by +`Maturita/` kolidovalo se zkompilovaným binárem `maturita`. Nativní Mac +aplikace sdílí cvičení s iOS přehrávačem; GTK verze (`make bundle`) zůstává. Licence: [GPL-3.0](LICENSE). @@ -230,6 +240,7 @@ above, or download from that branch page: - [maturita-linux-x86_64.AppImage](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-linux-x86_64.AppImage) - [maturita-android.apk](https://github.com/pepaskopec-blip/maturita.c/raw/refs/heads/builds/maturita-android.apk) - [maturita-ios.ipa](https://github.com/pepaskopec-blip/maturita.c/raw/refs/heads/builds/maturita-ios.ipa) +- [maturita-macos-swift-arm64.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-macos-swift-arm64.zip) Progress and settings live in `progress/` next to the `.app` / folder / AppImage. On Android and iOS they stay in app storage (same stats, themes, language). @@ -319,6 +330,7 @@ make bundle # dist/maturita.exe can be double-clicked src/ C sources and maturita.h android/ Jetpack Compose player (same content) ios/ SwiftUI player (same content) +macos/ native SwiftUI Mac app (same content) data/ style.css, share/, signing files, Windows .rc assets/ source icon and screenshots installers/ fetch-the-latest installers @@ -353,8 +365,17 @@ open Maturita.xcworkspace # at the repo root # or: make ios # unsigned IPA at dist-ios/maturita-ios.ipa ``` -The workspace points at `ios/Maturita.xcodeproj`. Do not move the project – -the build reads `../src` and `../scripts`, and at the root `Maturita/` would -collide with the compiled `maturita` binary. +Native macOS from source (Xcode 26, macOS 26+): + +```bash +open Maturita.xcworkspace # scheme Maturita from macos/Maturita.xcodeproj +# or: make macos # zip at dist/maturita-macos-swift-arm64.zip +``` + +The workspace points at `ios/Maturita.xcodeproj` and `macos/Maturita.xcodeproj`. +Do not move the projects – the build reads `../src` and `../scripts`, and at +the root `Maturita/` would collide with the compiled `maturita` binary. The +native Mac app shares exercises with the iOS player; the GTK build +(`make bundle`) stays. License: [GPL-3.0](LICENSE). diff --git a/ios/Maturita/MaturitaApp.swift b/ios/Maturita/MaturitaApp.swift index abb4510..357863d 100644 --- a/ios/Maturita/MaturitaApp.swift +++ b/ios/Maturita/MaturitaApp.swift @@ -85,93 +85,3 @@ private struct UpdateAccessory: View { return vm.tr(vm.update.messageKey) } } - -struct RouteDestination: View { - @ObservedObject var vm: AppModel - let route: Route - - var body: some View { - switch route { - case .welcome, .subjects: - PracticeHome(vm: vm) - case .stats: - StatsScreen(vm: vm) - case .roadmap: - RoadmapScreen(vm: vm) - case .unitMap(let id): - UnitMapScreen(vm: vm, unitId: id) - case .germanEx(let unit, let ex): - GermanExercise(vm: vm, unitId: unit, ex: ex) - case .vocab(let id): - VocabExercise(vm: vm, unitId: id) - case .netYears: - NetYearsScreen(vm: vm) - case .netMap: - LessonListScreen( - title: vm.tr("net_year1"), - subtitle: vm.tr("net_sub"), - rows: vm.content.netLessons.map { l in - let id = l.int("id") - return LessonRow( - id: "n\(id)", - title: vm.tr(l.str("titleKey")), - done: vm.progress.netDone(id), - destination: .netLesson(id) - ) - } - ) - case .netLesson(let id): - if let l = vm.content.netLesson(id) { - SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .netEx(id)) - } - case .netEx(let id): - NetQuizScreen(vm: vm, id: id) - case .hwMap: - LessonListScreen( - title: vm.tr("Technické vybavení"), - subtitle: vm.tr("hw_sub"), - rows: vm.content.hw.map { l in - let id = l.int("id") - return LessonRow( - id: "h\(id)", - title: vm.tr(l.str("titleKey")), - done: vm.progress.hwDone(id), - destination: .hwLesson(id) - ) - } - ) - case .hwLesson(let id): - if let l = vm.content.hwLesson(id) { - SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .hwEx(id)) - } - case .hwEx(let id): - HwQuizScreen(vm: vm, id: id) - case .czechMap: - CzechMapScreen(vm: vm) - case .mluvnice: - LessonListScreen( - title: vm.tr("Mluvnice"), - subtitle: vm.tr("mluv_sub"), - rows: vm.content.mluvnice.map { m in - let n = m.int("id") - return LessonRow( - id: "m\(n)", - title: m.str("name"), - done: vm.progress.mluvDone(n), - destination: .mluvEx(n) - ) - } - ) - case .mluvEx(let n): - MluvExercise(vm: vm, n: n) - case .readingList: - BookListScreen(vm: vm) - case .book(let id): - BookScreen(vm: vm, id: id) - case .bookQuiz(let id): - LitQuizScreen(vm: vm, id: id) - case .bookPlot(let id): - PlotScreen(vm: vm, id: id) - } - } -} diff --git a/ios/Maturita/UI/Components.swift b/ios/Maturita/UI/Components.swift index 221f0da..592cca6 100644 --- a/ios/Maturita/UI/Components.swift +++ b/ios/Maturita/UI/Components.swift @@ -49,7 +49,11 @@ struct PrimaryButton: View { .font(.body.weight(.semibold)) .frame(maxWidth: .infinity) } + #if os(iOS) .buttonStyle(.glassProminent) + #else + .buttonStyle(.borderedProminent) + #endif .controlSize(.large) } } @@ -250,17 +254,39 @@ extension View { /// Detail screens with their own bottom action hide the tab bar so the /// primary button is never covered by it. + @ViewBuilder func detailScreen(_ title: String, subtitle: String? = nil) -> some View { + #if os(iOS) navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .navSubtitle(subtitle) .toolbar(.hidden, for: .tabBar) + #else + navigationTitle(title) + .navSubtitle(subtitle) + #endif + } + + @ViewBuilder + func sheetDetents() -> some View { + #if os(iOS) + presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + #else + self + #endif } /// Full-width, leading-aligned content for exercise screens. func exerciseContent() -> some View { + #if os(macOS) + frame(maxWidth: 720, alignment: .leading) + .padding(20) + .frame(maxWidth: .infinity, alignment: .leading) + #else frame(maxWidth: .infinity, alignment: .leading) .padding() + #endif } } diff --git a/ios/Maturita/UI/ExercisesMore.swift b/ios/Maturita/UI/ExercisesMore.swift index 752dac6..23e637d 100644 --- a/ios/Maturita/UI/ExercisesMore.swift +++ b/ios/Maturita/UI/ExercisesMore.swift @@ -304,7 +304,7 @@ private struct ComboLine: View { if let word, idx < picks.count { picks[idx] = word } open = false } - .presentationDetents([.medium, .large]) + .sheetDetents() } if revealed, let meaning, !meaning.isEmpty { Meaning(text: vm.tr(meaning), palette: p, visible: true) @@ -334,8 +334,7 @@ struct WordPicker: View { } } } - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.visible) + .sheetDetents() } } @@ -781,7 +780,7 @@ private struct PrefixMenu: View { if let word { value = word } open = false } - .presentationDetents([.medium, .large]) + .sheetDetents() } } } diff --git a/ios/Maturita/UI/Screens.swift b/ios/Maturita/UI/Screens.swift index 2bbda04..72b16c5 100644 --- a/ios/Maturita/UI/Screens.swift +++ b/ios/Maturita/UI/Screens.swift @@ -20,7 +20,7 @@ struct PracticeHome: View { VStack(alignment: .leading, spacing: 10) { Text(vm.tr("welcome_title")) .font(.title.bold()) - Text(vm.tr("welcome_body_ios")) + Text(vm.tr(Self.welcomeBodyKey)) .font(.subheadline) .foregroundStyle(.secondary) ProgressView(value: pct) @@ -50,7 +50,17 @@ struct PracticeHome: View { } } .navigationTitle("Maturita") + #if os(iOS) .navigationBarTitleDisplayMode(.large) + #endif + } + + private static var welcomeBodyKey: String { + #if os(macOS) + "welcome_body_macos" + #else + "welcome_body_ios" + #endif } private func subjectLabel(_ s: J) -> some View { @@ -458,32 +468,20 @@ struct SlidesScreen: View { @State private var idx = 0 var body: some View { - TabView(selection: $idx) { - ForEach(slides.indices, id: \.self) { i in - let slide = slides[i] - ScrollView { - VStack(alignment: .leading, spacing: 12) { - Text(slide.str("kicker")) - .font(.caption.weight(.semibold)) - .foregroundStyle(.secondary) - Text(slide.str("title")).font(.title2.bold()) - if let tip = slide.strOrNull("tip") { - GlassCard { - Label(tip, systemImage: "lightbulb.fill") - } - } - ForEach(Array(slide.strs("lines").enumerated()), id: \.offset) { _, line in - Text(line).padding(.vertical, 4) - } - } - .padding() - .padding(.bottom, 24) - .frame(maxWidth: .infinity, alignment: .leading) + Group { + #if os(iOS) + TabView(selection: $idx) { + ForEach(slides.indices, id: \.self) { i in + slidePage(slides[i]).tag(i) } - .tag(i) } + .tabViewStyle(.page(indexDisplayMode: .never)) + #else + if slides.indices.contains(idx) { + slidePage(slides[idx]) + } + #endif } - .tabViewStyle(.page(indexDisplayMode: .never)) .detailScreen(title, subtitle: subtitle) .safeAreaInset(edge: .bottom) { VStack(spacing: 10) { @@ -503,7 +501,11 @@ struct SlidesScreen: View { Image(systemName: "chevron.left") .frame(minWidth: 24) } + #if os(iOS) .buttonStyle(.glass) + #else + .buttonStyle(.bordered) + #endif .controlSize(.large) } if idx < slides.count - 1 { @@ -523,6 +525,31 @@ struct SlidesScreen: View { .background(.bar) } } + + private func slidePage(_ slide: J) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + Text(slide.str("kicker")) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Text(slide.str("title")).font(.title2.bold()) + if let tip = slide.strOrNull("tip") { + GlassCard { + Label(tip, systemImage: "lightbulb.fill") + } + } + ForEach(Array(slide.strs("lines").enumerated()), id: \.offset) { _, line in + Text(line).padding(.vertical, 4) + } + } + .padding() + .padding(.bottom, 24) + .frame(maxWidth: .infinity, alignment: .leading) + #if os(macOS) + .frame(maxWidth: 720, alignment: .leading) + #endif + } + } } struct CzechMapScreen: View { @@ -607,3 +634,93 @@ struct BookScreen: View { } } } + +struct RouteDestination: View { + @ObservedObject var vm: AppModel + let route: Route + + var body: some View { + switch route { + case .welcome, .subjects: + PracticeHome(vm: vm) + case .stats: + StatsScreen(vm: vm) + case .roadmap: + RoadmapScreen(vm: vm) + case .unitMap(let id): + UnitMapScreen(vm: vm, unitId: id) + case .germanEx(let unit, let ex): + GermanExercise(vm: vm, unitId: unit, ex: ex) + case .vocab(let id): + VocabExercise(vm: vm, unitId: id) + case .netYears: + NetYearsScreen(vm: vm) + case .netMap: + LessonListScreen( + title: vm.tr("net_year1"), + subtitle: vm.tr("net_sub"), + rows: vm.content.netLessons.map { l in + let id = l.int("id") + return LessonRow( + id: "n\(id)", + title: vm.tr(l.str("titleKey")), + done: vm.progress.netDone(id), + destination: .netLesson(id) + ) + } + ) + case .netLesson(let id): + if let l = vm.content.netLesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .netEx(id)) + } + case .netEx(let id): + NetQuizScreen(vm: vm, id: id) + case .hwMap: + LessonListScreen( + title: vm.tr("Technické vybavení"), + subtitle: vm.tr("hw_sub"), + rows: vm.content.hw.map { l in + let id = l.int("id") + return LessonRow( + id: "h\(id)", + title: vm.tr(l.str("titleKey")), + done: vm.progress.hwDone(id), + destination: .hwLesson(id) + ) + } + ) + case .hwLesson(let id): + if let l = vm.content.hwLesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .hwEx(id)) + } + case .hwEx(let id): + HwQuizScreen(vm: vm, id: id) + case .czechMap: + CzechMapScreen(vm: vm) + case .mluvnice: + LessonListScreen( + title: vm.tr("Mluvnice"), + subtitle: vm.tr("mluv_sub"), + rows: vm.content.mluvnice.map { m in + let n = m.int("id") + return LessonRow( + id: "m\(n)", + title: m.str("name"), + done: vm.progress.mluvDone(n), + destination: .mluvEx(n) + ) + } + ) + case .mluvEx(let n): + MluvExercise(vm: vm, n: n) + case .readingList: + BookListScreen(vm: vm) + case .book(let id): + BookScreen(vm: vm, id: id) + case .bookQuiz(let id): + LitQuizScreen(vm: vm, id: id) + case .bookPlot(let id): + PlotScreen(vm: vm, id: id) + } + } +} diff --git a/macos/Maturita.xcodeproj/project.pbxproj b/macos/Maturita.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ec37b15 --- /dev/null +++ b/macos/Maturita.xcodeproj/project.pbxproj @@ -0,0 +1,429 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + B101000000000000000001 /* MaturitaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000001 /* MaturitaApp.swift */; }; + B101000000000000000002 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000002 /* Platform.swift */; }; + B101000000000000000003 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000003 /* Updater.swift */; }; + B101000000000000000004 /* AppModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000004 /* AppModel.swift */; }; + B101000000000000000005 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000005 /* Route.swift */; }; + B101000000000000000006 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000006 /* JSON.swift */; }; + B101000000000000000007 /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000007 /* Content.swift */; }; + B101000000000000000008 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000008 /* Theme.swift */; }; + B101000000000000000009 /* Answers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000009 /* Answers.swift */; }; + B101000000000000000010 /* ProgressStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000010 /* ProgressStore.swift */; }; + B101000000000000000011 /* Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000011 /* Components.swift */; }; + B101000000000000000012 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000012 /* Icons.swift */; }; + B101000000000000000013 /* Screens.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000013 /* Screens.swift */; }; + B101000000000000000014 /* Exercises.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000014 /* Exercises.swift */; }; + B101000000000000000015 /* ExercisesMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000015 /* ExercisesMore.swift */; }; + B101000000000000000016 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B201000000000000000016 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + B201000000000000000001 /* MaturitaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaturitaApp.swift; sourceTree = ""; }; + B201000000000000000002 /* Platform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = ""; }; + B201000000000000000003 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; }; + B201000000000000000004 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppModel.swift; path = ../ios/Maturita/AppModel.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000005 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Route.swift; path = ../ios/Maturita/Route.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000006 /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = JSON.swift; path = ../ios/Maturita/Data/JSON.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000007 /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Content.swift; path = ../ios/Maturita/Data/Content.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000008 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Theme.swift; path = ../ios/Maturita/Data/Theme.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000009 /* Answers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Answers.swift; path = ../ios/Maturita/Data/Answers.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000010 /* ProgressStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ProgressStore.swift; path = ../ios/Maturita/Data/ProgressStore.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000011 /* Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Components.swift; path = ../ios/Maturita/UI/Components.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000012 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Icons.swift; path = ../ios/Maturita/UI/Icons.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000013 /* Screens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Screens.swift; path = ../ios/Maturita/UI/Screens.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000014 /* Exercises.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Exercises.swift; path = ../ios/Maturita/UI/Exercises.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000015 /* ExercisesMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExercisesMore.swift; path = ../ios/Maturita/UI/ExercisesMore.swift; sourceTree = SOURCE_ROOT; }; + B201000000000000000016 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B201000000000000000017 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B201000000000000000018 /* Maturita.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Maturita.entitlements; sourceTree = ""; }; + B301000000000000000001 /* Maturita.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Maturita.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B401000000000000000001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B501000000000000000001 = { + isa = PBXGroup; + children = ( + B501000000000000000002 /* Maturita */, + B501000000000000000006 /* Products */, + ); + sourceTree = ""; + }; + B501000000000000000002 /* Maturita */ = { + isa = PBXGroup; + children = ( + B201000000000000000001 /* MaturitaApp.swift */, + B201000000000000000002 /* Platform.swift */, + B201000000000000000004 /* AppModel.swift */, + B201000000000000000005 /* Route.swift */, + B501000000000000000003 /* Data */, + B501000000000000000004 /* Update */, + B501000000000000000005 /* UI */, + B201000000000000000016 /* Assets.xcassets */, + B201000000000000000017 /* Info.plist */, + B201000000000000000018 /* Maturita.entitlements */, + ); + path = Maturita; + sourceTree = ""; + }; + B501000000000000000003 /* Data */ = { + isa = PBXGroup; + children = ( + B201000000000000000006 /* JSON.swift */, + B201000000000000000007 /* Content.swift */, + B201000000000000000008 /* Theme.swift */, + B201000000000000000009 /* Answers.swift */, + B201000000000000000010 /* ProgressStore.swift */, + ); + name = Data; + sourceTree = ""; + }; + B501000000000000000004 /* Update */ = { + isa = PBXGroup; + children = ( + B201000000000000000003 /* Updater.swift */, + ); + path = Update; + sourceTree = ""; + }; + B501000000000000000005 /* UI */ = { + isa = PBXGroup; + children = ( + B201000000000000000011 /* Components.swift */, + B201000000000000000012 /* Icons.swift */, + B201000000000000000013 /* Screens.swift */, + B201000000000000000014 /* Exercises.swift */, + B201000000000000000015 /* ExercisesMore.swift */, + ); + name = UI; + sourceTree = ""; + }; + B501000000000000000006 /* Products */ = { + isa = PBXGroup; + children = ( + B301000000000000000001 /* Maturita.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B601000000000000000001 /* Maturita */ = { + isa = PBXNativeTarget; + buildConfigurationList = B801000000000000000002 /* Build configuration list for PBXNativeTarget "Maturita" */; + buildPhases = ( + B701000000000000000001 /* Sources */, + B401000000000000000001 /* Frameworks */, + B701000000000000000002 /* Resources */, + B701000000000000000004 /* Extract content */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Maturita; + productName = Maturita; + productReference = B301000000000000000001 /* Maturita.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B001000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 2700; + TargetAttributes = { + B601000000000000000001 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = B801000000000000000001 /* Build configuration list for PBXProject "Maturita" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = cs; + hasScannedForEncodings = 0; + knownRegions = ( + en, + cs, + Base, + ); + mainGroup = B501000000000000000001; + productRefGroup = B501000000000000000006 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B601000000000000000001 /* Maturita */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B701000000000000000002 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B101000000000000000016 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + B701000000000000000004 /* Extract content */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Extract content"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/content.json", + "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/content.json", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Pull the exercises out of the C sources and drop the JSON into the app\n# bundle. Nothing generated is kept in the repository, so a fresh clone opens\n# and runs in Xcode without extra steps.\nset -eu\nextract=\"${SRCROOT}/../scripts/extract-android-content.py\"\nif [ ! -f \"$extract\" ]; then\n echo \"error: ${extract} not found. Keep macos/ inside the maturita.c checkout.\" >&2\n exit 1\nfi\nout=\"${DERIVED_FILE_DIR}/content.json\"\nmkdir -p \"${DERIVED_FILE_DIR}\"\npython3 \"$extract\" --out \"$out\"\nmkdir -p \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\"\ncp \"$out\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/content.json\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B701000000000000000001 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B101000000000000000001 /* MaturitaApp.swift in Sources */, + B101000000000000000002 /* Platform.swift in Sources */, + B101000000000000000003 /* Updater.swift in Sources */, + B101000000000000000004 /* AppModel.swift in Sources */, + B101000000000000000005 /* Route.swift in Sources */, + B101000000000000000006 /* JSON.swift in Sources */, + B101000000000000000007 /* Content.swift in Sources */, + B101000000000000000008 /* Theme.swift in Sources */, + B101000000000000000009 /* Answers.swift in Sources */, + B101000000000000000010 /* ProgressStore.swift in Sources */, + B101000000000000000011 /* Components.swift in Sources */, + B101000000000000000012 /* Icons.swift in Sources */, + B101000000000000000013 /* Screens.swift in Sources */, + B101000000000000000014 /* Exercises.swift in Sources */, + B101000000000000000015 /* ExercisesMore.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + B901000000000000000001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + B901000000000000000002 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + B901000000000000000003 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Maturita/Maturita.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COMMIT = dev; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = C2U9S337H8; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Maturita/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = maturita.c; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + INFOPLIST_KEY_NSHighResolutionCapable = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "GPL-3.0"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = "macos-1"; + PRODUCT_BUNDLE_IDENTIFIER = com.bukovinafilip.maturita.macos; + PRODUCT_NAME = Maturita; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + B901000000000000000004 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Maturita/Maturita.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COMMIT = dev; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = C2U9S337H8; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Maturita/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = maturita.c; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + INFOPLIST_KEY_NSHighResolutionCapable = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "GPL-3.0"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = "macos-1"; + PRODUCT_BUNDLE_IDENTIFIER = com.bukovinafilip.maturita.macos; + PRODUCT_NAME = Maturita; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B801000000000000000001 /* Build configuration list for PBXProject "Maturita" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B901000000000000000001 /* Debug */, + B901000000000000000002 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B801000000000000000002 /* Build configuration list for PBXNativeTarget "Maturita" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B901000000000000000003 /* Debug */, + B901000000000000000004 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B001000000000000000001 /* Project object */; +} diff --git a/macos/Maturita.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/macos/Maturita.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/macos/Maturita.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Maturita.xcodeproj/xcshareddata/xcschemes/Maturita.xcscheme b/macos/Maturita.xcodeproj/xcshareddata/xcschemes/Maturita.xcscheme new file mode 100644 index 0000000..ef75b56 --- /dev/null +++ b/macos/Maturita.xcodeproj/xcshareddata/xcschemes/Maturita.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Maturita/Assets.xcassets/AccentColor.colorset/Contents.json b/macos/Maturita/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/macos/Maturita/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..700d5cf --- /dev/null +++ b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, + { "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, + { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, + { "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, + { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, + { "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, + { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, + { "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, + { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, + { "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..e82185d Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000..6775872 Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000..0aa6855 Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000..1e26937 Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000..6775872 Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000..6e40e8f Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000..1e26937 Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000..d3c7bc7 Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000..6e40e8f Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000..80afb9d Binary files /dev/null and b/macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/macos/Maturita/Assets.xcassets/Contents.json b/macos/Maturita/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/macos/Maturita/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Maturita/Info.plist b/macos/Maturita/Info.plist new file mode 100644 index 0000000..531c44a --- /dev/null +++ b/macos/Maturita/Info.plist @@ -0,0 +1,12 @@ + + + + + GITCommit + $(COMMIT) + UPDATE_BRANCH + builds + UPDATE_REPO + pepaskopec-blip/maturita.c + + diff --git a/macos/Maturita/Maturita.entitlements b/macos/Maturita/Maturita.entitlements new file mode 100644 index 0000000..08ba3a3 --- /dev/null +++ b/macos/Maturita/Maturita.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/macos/Maturita/MaturitaApp.swift b/macos/Maturita/MaturitaApp.swift new file mode 100644 index 0000000..3257b41 --- /dev/null +++ b/macos/Maturita/MaturitaApp.swift @@ -0,0 +1,171 @@ +import SwiftUI + +@main +struct MaturitaApp: App { + @StateObject private var vm = AppModel() + + var body: some Scene { + WindowGroup { + MacRoot(vm: vm) + } + .defaultSize(width: 1100, height: 720) + .commands { + CommandGroup(replacing: .newItem) {} + CommandMenu(vm.tr("search_placeholder")) { + Button(vm.tr("search_placeholder")) { + vm.tab = .search + } + .keyboardShortcut("k", modifiers: [.command]) + } + CommandMenu(vm.tr("subjects_title")) { + Button(vm.tr("search_home")) { + vm.go(.subjects) + } + .keyboardShortcut("1", modifiers: [.command]) + Button(vm.tr("stats")) { + vm.tab = .progress + } + .keyboardShortcut("2", modifiers: [.command]) + Button(vm.tr("settings")) { + vm.tab = .settings + } + .keyboardShortcut("3", modifiers: [.command]) + } + } + + Settings { + SettingsScreen(vm: vm) + .frame(minWidth: 420, minHeight: 460) + .tint(vm.palette.tint) + .preferredColorScheme(vm.mode == .dark ? .dark : .light) + } + } +} + +struct MacRoot: View { + @ObservedObject var vm: AppModel + + var body: some View { + NavigationSplitView { + MacSidebar(vm: vm) + .navigationSplitViewColumnWidth(min: 200, ideal: 240, max: 320) + } detail: { + switch vm.tab { + case .practice: + NavigationStack(path: $vm.practicePath) { + PracticeHome(vm: vm) + .navigationDestination(for: Route.self) { route in + RouteDestination(vm: vm, route: route) + } + } + case .progress: + NavigationStack { + StatsScreen(vm: vm) + } + case .search: + NavigationStack { + SearchScreen(vm: vm) + } + case .settings: + NavigationStack { + SettingsScreen(vm: vm) + } + } + } + .navigationSplitViewStyle(.balanced) + .tint(vm.palette.tint) + .preferredColorScheme(vm.mode == .dark ? .dark : .light) + .overlay(alignment: .top) { + if showsUpdateAccessory { + UpdateBanner(vm: vm) + .padding(12) + } + } + .onAppear { vm.checkUpdate(false) } + .frame(minWidth: 880, minHeight: 540) + } + + private var showsUpdateAccessory: Bool { + vm.update.canInstall || vm.update.status == "downloading" || vm.update.status == "staged" + } +} + +private struct MacSidebar: View { + @ObservedObject var vm: AppModel + + var body: some View { + List(selection: $vm.tab) { + Section { + Label(vm.tr("search_home"), systemImage: "square.grid.2x2.fill") + .tag(AppTab.practice) + Label(vm.tr("stats"), systemImage: "chart.bar.fill") + .tag(AppTab.progress) + Label(vm.tr("search_placeholder"), systemImage: "magnifyingglass") + .tag(AppTab.search) + Label(vm.tr("settings"), systemImage: "gearshape.fill") + .tag(AppTab.settings) + } + + Section(vm.tr("subjects_title")) { + ForEach(Array(vm.content.subjects.enumerated()), id: \.offset) { _, s in + let open = s.bool("open") + let dest = s.strOrNull("target").flatMap(routeFromPage) + if open, let dest, dest != .subjects, dest != .welcome { + Button { + vm.openFromSearch(dest.page) + } label: { + subjectLabel(s) + } + .buttonStyle(.plain) + } else { + subjectLabel(s) + .foregroundStyle(.secondary) + } + } + } + } + .listStyle(.sidebar) + .navigationTitle("maturita.c") + } + + private func subjectLabel(_ s: J) -> some View { + Label { + VStack(alignment: .leading, spacing: 2) { + Text(vm.tr(s.str("key"))) + if !s.bool("open") { + Text(vm.tr("stats_locked")).font(.caption).foregroundStyle(.secondary) + } + } + } icon: { + SubjectIcon(icon: s.str("icon"), open: s.bool("open")) + } + } +} + +private struct UpdateBanner: View { + @ObservedObject var vm: AppModel + + var body: some View { + HStack(spacing: 12) { + Image(systemName: "arrow.down.app.fill") + Text(bannerText) + .font(.subheadline) + Spacer() + if vm.update.canInstall { + Button(vm.tr("update_install")) { vm.installUpdate() } + .buttonStyle(.borderedProminent) + } + } + .padding(12) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .shadow(color: .black.opacity(0.12), radius: 8, y: 2) + .frame(maxWidth: 640) + } + + private var bannerText: String { + if let arg = vm.update.messageArg { + return vm.fmt(vm.update.messageKey, arg) + } + return vm.tr(vm.update.messageKey) + } +} diff --git a/macos/Maturita/Platform.swift b/macos/Maturita/Platform.swift new file mode 100644 index 0000000..cc9c1e2 --- /dev/null +++ b/macos/Maturita/Platform.swift @@ -0,0 +1,38 @@ +#if os(macOS) +import AppKit +import SwiftUI + +extension NSColor { + static var tertiarySystemFill: NSColor { + NSColor.quaternaryLabelColor.withAlphaComponent(0.16) + } + + static var secondarySystemGroupedBackground: NSColor { + .controlBackgroundColor + } + + static var separator: NSColor { .separatorColor } +} + +enum TextInputAutocapitalization { + case never +} + +enum UIKeyboardType { + case decimalPad +} + +enum MacNavBarTitleMode { + case large + case inline +} + +extension View { + func textInputAutocapitalization(_ style: TextInputAutocapitalization) -> some View { self } + + func keyboardType(_ type: UIKeyboardType) -> some View { self } + + func navigationBarTitleDisplayMode(_ mode: MacNavBarTitleMode) -> some View { self } +} + +#endif diff --git a/macos/Maturita/Update/Updater.swift b/macos/Maturita/Update/Updater.swift new file mode 100644 index 0000000..d03948f --- /dev/null +++ b/macos/Maturita/Update/Updater.swift @@ -0,0 +1,233 @@ +import AppKit +import Foundation + +struct UpdateState { + var status = "idle" + var remote: String? + var messageKey = "update_current" + var messageArg: String? = String(AppConfig.commit.prefix(7)) + var canInstall = false +} + +final class Updater { + private let repo = AppConfig.updateRepo + private let branch = AppConfig.updateBranch + + func check(interactive: Bool, cb: @escaping (UpdateState) -> Void) { + if AppConfig.commit == "dev" { + emit(cb, UpdateState(status: "dev", messageKey: "update_err_devbuild", messageArg: nil)) + return + } + emit(cb, UpdateState(status: "checking", messageKey: "update_checking", messageArg: nil)) + DispatchQueue.global(qos: .utility).async { + do { + let atom = try self.get("https://github.com/\(self.repo)/commits/\(self.branch).atom") + let remote: String + if let tip = self.atomTip(atom), + let marker = try? self.get("https://raw.githubusercontent.com/\(self.repo)/\(tip)/VERSION-macos-swift") + .trimmingCharacters(in: .whitespacesAndNewlines), + marker.count >= 7 { + remote = marker + } else if let sha = self.atomMainSha(atom) { + remote = sha + } else if let tip = self.atomTip(atom) { + remote = try self.get("https://raw.githubusercontent.com/\(self.repo)/\(tip)/VERSION") + .trimmingCharacters(in: .whitespacesAndNewlines) + } else { + throw URLError(.cannotParseResponse) + } + let local = AppConfig.commit + if self.sameCommit(remote, local) { + self.emit(cb, UpdateState(status: "ok", remote: remote, messageKey: "update_uptodate", messageArg: String(remote.prefix(7)))) + } else { + self.emit(cb, UpdateState(status: "available", remote: remote, messageKey: "update_available", messageArg: String(remote.prefix(7)), canInstall: true)) + } + } catch { + self.emit(cb, UpdateState(status: "error", messageKey: "update_err_network", messageArg: nil)) + } + } + } + + func install(cb: @escaping (UpdateState) -> Void) { + emit(cb, UpdateState(status: "downloading", messageKey: "update_downloading", messageArg: nil)) + DispatchQueue.global(qos: .utility).async { + do { + let atom = try self.get("https://github.com/\(self.repo)/commits/\(self.branch).atom") + let tip = self.atomTip(atom) ?? "" + guard !tip.isEmpty else { throw URLError(.cannotParseResponse) } + let zipName = Self.assetName() + let url = "https://raw.githubusercontent.com/\(self.repo)/\(tip)/\(zipName)" + let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first + ?? URL(fileURLWithPath: NSTemporaryDirectory()) + let staging = cache.appendingPathComponent("maturita-update-\(ProcessInfo.processInfo.processIdentifier)") + try? FileManager.default.removeItem(at: staging) + try FileManager.default.createDirectory(at: staging, withIntermediateDirectories: true) + let archive = staging.appendingPathComponent(zipName) + try self.download(url, to: archive) + let unpacked = staging.appendingPathComponent("new") + try FileManager.default.createDirectory(at: unpacked, withIntermediateDirectories: true) + try self.run("/usr/bin/ditto", ["-x", "-k", archive.path, unpacked.path]) + guard let source = self.findApp(in: unpacked) else { + throw URLError(.cannotParseResponse) + } + guard let target = Self.runningAppURL() else { + throw URLError(.cannotOpenFile) + } + let script = self.writeSwapScript() + let proc = Process() + proc.executableURL = URL(fileURLWithPath: "/bin/sh") + proc.arguments = [script.path, "\(ProcessInfo.processInfo.processIdentifier)", target.path, source.path, staging.path] + try proc.run() + self.emit(cb, UpdateState(status: "staged", remote: tip, messageKey: "update_staged", messageArg: String(tip.prefix(7)))) + DispatchQueue.main.async { + NSApp.terminate(nil) + } + } catch { + self.emit(cb, UpdateState(status: "error", messageKey: "update_err_download", messageArg: nil)) + } + } + } + + private static func assetName() -> String { + #if arch(arm64) + return "maturita-macos-swift-arm64.zip" + #else + return "maturita-macos-swift-x86_64.zip" + #endif + } + + private static func runningAppURL() -> URL? { + let bundle = Bundle.main.bundleURL + return bundle.pathExtension == "app" ? bundle : nil + } + + private func findApp(in root: URL) -> URL? { + let keys: [URLResourceKey] = [.isDirectoryKey] + guard let en = FileManager.default.enumerator(at: root, includingPropertiesForKeys: keys) else { return nil } + for case let url as URL in en where url.pathExtension == "app" { + return url + } + return nil + } + + private func writeSwapScript() -> URL { + let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("maturita-swift-update.sh") + let body = """ + #!/bin/sh + if [ -z "$MATURITA_UPDATE_DETACHED" ]; then + MATURITA_UPDATE_DETACHED=1 + export MATURITA_UPDATE_DETACHED + nohup /bin/sh "$0" "$@" >/dev/null 2>&1 & + exit 0 + fi + pid=$1; target=$2; source=$3; staging=$4 + n=0 + while kill -0 "$pid" 2>/dev/null && [ $n -lt 600 ]; do + sleep 0.2; n=$((n+1)) + done + place=$target + if /usr/bin/ditto "$source" "$target" 2>/dev/null; then + xattr -cr "$target" 2>/dev/null + else + alt="$HOME/Applications/$(basename "$target")" + mkdir -p "$HOME/Applications" + if /usr/bin/ditto "$source" "$alt" 2>/dev/null; then + xattr -cr "$alt" 2>/dev/null + place=$alt + fi + fi + open -n "$place" + rm -rf "$staging" + rm -f "$0" + """ + try? body.write(to: url, atomically: true, encoding: .utf8) + try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path) + return url + } + + private func emit(_ cb: @escaping (UpdateState) -> Void, _ state: UpdateState) { + DispatchQueue.main.async { cb(state) } + } + + private func atomTip(_ atom: String) -> String? { + if let m = atom.range(of: #"Commit/([0-9a-f]{40})"#, options: [.regularExpression, .caseInsensitive]) { + return String(atom[m]).split(separator: "/").last.map(String.init) + } + if let m = atom.range(of: #"/commit/([0-9a-f]{7,40})"#, options: [.regularExpression, .caseInsensitive]) { + return String(atom[m]).split(separator: "/").last.map(String.init) + } + return nil + } + + private func atomMainSha(_ atom: String) -> String? { + guard let regex = try? NSRegularExpression(pattern: #"Build from ([0-9a-f]{7,40})"#, options: .caseInsensitive), + let match = regex.firstMatch(in: atom, range: NSRange(atom.startIndex..., in: atom)), + let range = Range(match.range(at: 1), in: atom) + else { return nil } + return String(atom[range]) + } + + private func sameCommit(_ a: String, _ b: String) -> Bool { + let n = min(a.count, b.count) + return n >= 7 && a.prefix(n).lowercased() == b.prefix(n).lowercased() + } + + private func get(_ url: String) throws -> String { + guard let u = URL(string: url) else { throw URLError(.badURL) } + var req = URLRequest(url: u, timeoutInterval: 15) + req.setValue("maturita.c-macos", forHTTPHeaderField: "User-Agent") + req.setValue("*/*", forHTTPHeaderField: "Accept") + let sem = DispatchSemaphore(value: 0) + var result: Result = .failure(URLError(.unknown)) + URLSession.shared.dataTask(with: req) { data, resp, err in + if let err { result = .failure(err) } + else if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) { + result = .failure(URLError(.badServerResponse)) + } else if let data, let text = String(data: data, encoding: .utf8) { + result = .success(text) + } else { + result = .failure(URLError(.cannotDecodeContentData)) + } + sem.signal() + }.resume() + _ = sem.wait(timeout: .now() + 20) + return try result.get() + } + + private func download(_ url: String, to dest: URL) throws { + guard let u = URL(string: url) else { throw URLError(.badURL) } + let sem = DispatchSemaphore(value: 0) + var result: Result = .failure(URLError(.unknown)) + let task = URLSession.shared.downloadTask(with: u) { file, resp, err in + if let err { result = .failure(err) } + else if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) { + result = .failure(URLError(.badServerResponse)) + } else if let file { + do { + try? FileManager.default.removeItem(at: dest) + try FileManager.default.moveItem(at: file, to: dest) + result = .success(()) + } catch { + result = .failure(error) + } + } else { + result = .failure(URLError(.cannotDecodeContentData)) + } + sem.signal() + } + task.resume() + _ = sem.wait(timeout: .now() + 1800) + try result.get() + } + + private func run(_ launch: String, _ args: [String]) throws { + let proc = Process() + proc.executableURL = URL(fileURLWithPath: launch) + proc.arguments = args + try proc.run() + proc.waitUntilExit() + if proc.terminationStatus != 0 { + throw URLError(.cannotParseResponse) + } + } +} diff --git a/scripts/bundle-macos-swift.sh b/scripts/bundle-macos-swift.sh new file mode 100755 index 0000000..f672efb --- /dev/null +++ b/scripts/bundle-macos-swift.sh @@ -0,0 +1,128 @@ +#!/bin/sh +# Build the native Swift macOS .app and zip it for the builds branch. + +set -eu + +root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +cd "$root" + +export DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}" +if [ -z "${COMMIT:-}" ]; then + if [ ! -e "$root/.git" ] || + ! COMMIT="$(git -C "$root" rev-parse --verify HEAD 2>/dev/null)"; then + echo "error: cannot determine the source commit; set COMMIT when bundling without Git metadata" >&2 + exit 1 + fi +fi +export COMMIT + +arch=$(uname -m) +case "$arch" in + arm64|aarch64) arch_tag=arm64 ;; + x86_64) arch_tag=x86_64 ;; + *) + echo "Unsupported architecture: $arch" >&2 + exit 1 + ;; +esac + +dd="$root/macos/DerivedData" +out_zip="maturita-macos-swift-${arch_tag}.zip" +mkdir -p "$dd" "$root/dist" + +xcodebuild \ + -project macos/Maturita.xcodeproj \ + -scheme Maturita \ + -configuration Release \ + -destination "generic/platform=macOS" \ + -derivedDataPath "$dd" \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + COMMIT="$COMMIT" + +app=$(find "$dd" -name 'Maturita.app' -path '*/Release/*' | head -n 1) +if [ -z "$app" ]; then + echo "Maturita.app not found" >&2 + exit 1 +fi + +dest="$root/dist/Maturita.app" +rm -rf "$dest" +cp -R "$app" "$dest" + +ENTITLEMENTS="$root/macos/Maturita/Maturita.entitlements" + +pick_sign_identity() { + if [ -n "${MACOS_SIGN_IDENTITY:-}" ]; then + printf '%s\n' "$MACOS_SIGN_IDENTITY" + return + fi + found="$(security find-identity -v -p codesigning 2>/dev/null \ + | awk -F'"' '/Developer ID Application/ { print $2; exit }')" + if [ -n "$found" ]; then + printf '%s\n' "$found" + return + fi + printf '%s\n' "-" +} + +IDENTITY="$(pick_sign_identity)" +echo "==> Signing Maturita.app ($IDENTITY)" +sign_args="--force --sign $IDENTITY" +if [ "$IDENTITY" != "-" ]; then + sign_args="$sign_args --options runtime --timestamp" +fi +if [ -f "$ENTITLEMENTS" ]; then + # shellcheck disable=SC2086 + codesign $sign_args --entitlements "$ENTITLEMENTS" "$dest" +else + # shellcheck disable=SC2086 + codesign $sign_args "$dest" +fi +codesign --verify --strict --verbose=2 "$dest" + +can_notarize() { + [ "$IDENTITY" != "-" ] || return 1 + if [ -n "${APPLE_API_KEY:-}" ] && [ -n "${APPLE_API_KEY_ID:-}" ] && + [ -n "${APPLE_API_ISSUER:-}" ]; then + return 0 + fi + if [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] && + [ -n "${APPLE_TEAM_ID:-}" ]; then + return 0 + fi + return 1 +} + +if can_notarize; then + echo "==> Notarizing Maturita.app" + zip="$root/dist/.notarize-$out_zip" + rm -f "$zip" + ditto -c -k --keepParent "$dest" "$zip" + if [ -n "${APPLE_API_KEY:-}" ]; then + keyfile="$(mktemp)" + printf '%s\n' "$APPLE_API_KEY" > "$keyfile" + xcrun notarytool submit "$zip" --wait \ + --key "$keyfile" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER" + rm -f "$keyfile" + else + xcrun notarytool submit "$zip" --wait \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" + fi + rm -f "$zip" + xcrun stapler staple "$dest" +else + if [ "$IDENTITY" != "-" ] && [ -n "${GITHUB_ACTIONS:-}${CI:-}" ]; then + echo "error: Developer ID is present but notarization secrets are missing." >&2 + exit 1 + fi +fi + +rm -f "$root/dist/$out_zip" +( + cd "$root/dist" + zip -qry "$out_zip" Maturita.app +) +echo "wrote $root/dist/$out_zip" diff --git a/scripts/extract-android-content.py b/scripts/extract-android-content.py index 12f11e9..433f5ed 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -10,8 +10,9 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "src" -# Default target is the Android asset; Xcode passes `--out` and copies the -# file into the app bundle itself, so nothing generated lives under ios/. +# Default target is the Android asset; Xcode (iOS / native macOS) passes +# `--out` and copies the file into the app bundle itself, so nothing +# generated lives under ios/ or macos/. OUTS = [ ROOT / "android" / "app" / "src" / "main" / "assets" / "content.json", ] diff --git a/src/i18n.c b/src/i18n.c index 5701117..7921bd9 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -66,6 +66,11 @@ const TrEntry tr_ui[] = { "stejný obsah, rozhraní podle iOS.", "Native exam practice on iPhone. Czech and English, " "the same content, adapted to iOS."}, + {"welcome_body_macos", + "Nativní procvičování na Macu. Česky i anglicky, " + "stejný obsah, rozhraní podle macOS.", + "Native exam practice on the Mac. Czech and English, " + "the same content, adapted to macOS."}, {"update_ios_sideload", "Nový build je na GitHubu. Nainstalujte ho znovu přes Xcode nebo AltStore (%s).", "A newer build is on GitHub. Reinstall it with Xcode or AltStore (%s)."},