Add a native SwiftUI macOS app that shares the iOS player.
The GTK Mac build stays; CI now also publishes maturita-macos-swift. Co-authored-by: Cursor <cursoragent@cursor.com>
56
.github/workflows/build.yml
vendored
|
|
@ -215,9 +215,62 @@ jobs:
|
||||||
name: maturita-ios
|
name: maturita-ios
|
||||||
path: dist-ios/maturita-ios.ipa
|
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:
|
publish:
|
||||||
name: Publish to builds branch
|
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
|
# One broken platform must not freeze the others: publish whatever was
|
||||||
# built and keep the previous package for anything that failed.
|
# built and keep the previous package for anything that failed.
|
||||||
if: ${{ !cancelled() && contains(needs.*.result, 'success') }}
|
if: ${{ !cancelled() && contains(needs.*.result, 'success') }}
|
||||||
|
|
@ -254,6 +307,7 @@ jobs:
|
||||||
# here, so a platform that failed this run is not offered as new.
|
# 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-android.apk ] && echo "${{ github.sha }}" > staged/VERSION-android
|
||||||
[ -f fresh/maturita-ios.ipa ] && echo "${{ github.sha }}" > staged/VERSION-ios
|
[ -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
|
ls fresh staged
|
||||||
|
|
||||||
# The branch is rewritten from scratch every time rather than added to,
|
# The branch is rewritten from scratch every time rather than added to,
|
||||||
|
|
|
||||||
5
.gitignore
vendored
|
|
@ -41,5 +41,10 @@ ios/build/
|
||||||
ios/Payload/
|
ios/Payload/
|
||||||
ios/**/xcuserdata/
|
ios/**/xcuserdata/
|
||||||
|
|
||||||
|
# native macOS Swift
|
||||||
|
macos/DerivedData/
|
||||||
|
macos/build/
|
||||||
|
macos/**/xcuserdata/
|
||||||
|
|
||||||
# Xcode workspace user state
|
# Xcode workspace user state
|
||||||
*.xcworkspace/xcuserdata/
|
*.xcworkspace/xcuserdata/
|
||||||
|
|
|
||||||
6
Makefile
|
|
@ -124,4 +124,8 @@ ios:
|
||||||
chmod +x scripts/bundle-ios.sh
|
chmod +x scripts/bundle-ios.sh
|
||||||
./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
|
||||||
|
|
|
||||||
3
Maturita.xcworkspace/contents.xcworkspacedata
generated
|
|
@ -4,4 +4,7 @@
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:ios/Maturita.xcodeproj">
|
location = "group:ios/Maturita.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:macos/Maturita.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|
|
||||||
33
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-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-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-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/`.
|
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).
|
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
|
src/ C zdroje a maturita.h
|
||||||
android/ Jetpack Compose přehrávač (stejný obsah)
|
android/ Jetpack Compose přehrávač (stejný obsah)
|
||||||
ios/ SwiftUI 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
|
data/ style.css, share/, podpisy, Windows .rc
|
||||||
assets/ zdrojová ikona a screenshoty
|
assets/ zdrojová ikona a screenshoty
|
||||||
installers/ instalátory, které stáhnou aktuální build
|
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
|
# nebo: make ios # unsigned IPA v dist-ios/maturita-ios.ipa
|
||||||
```
|
```
|
||||||
|
|
||||||
Workspace odkazuje na `ios/Maturita.xcodeproj`. Projekt nepřesouvejte – build
|
Nativní macOS ze zdroje (Xcode 26, macOS 26+):
|
||||||
čte `../src` a `../scripts` a v kořeni by `Maturita/` kolidovalo se
|
|
||||||
zkompilovaným binárem `maturita`.
|
```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).
|
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-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-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-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 /
|
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).
|
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
|
src/ C sources and maturita.h
|
||||||
android/ Jetpack Compose player (same content)
|
android/ Jetpack Compose player (same content)
|
||||||
ios/ SwiftUI player (same content)
|
ios/ SwiftUI player (same content)
|
||||||
|
macos/ native SwiftUI Mac app (same content)
|
||||||
data/ style.css, share/, signing files, Windows .rc
|
data/ style.css, share/, signing files, Windows .rc
|
||||||
assets/ source icon and screenshots
|
assets/ source icon and screenshots
|
||||||
installers/ fetch-the-latest installers
|
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
|
# or: make ios # unsigned IPA at dist-ios/maturita-ios.ipa
|
||||||
```
|
```
|
||||||
|
|
||||||
The workspace points at `ios/Maturita.xcodeproj`. Do not move the project –
|
Native macOS from source (Xcode 26, macOS 26+):
|
||||||
the build reads `../src` and `../scripts`, and at the root `Maturita/` would
|
|
||||||
collide with the compiled `maturita` binary.
|
```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).
|
License: [GPL-3.0](LICENSE).
|
||||||
|
|
|
||||||
|
|
@ -85,93 +85,3 @@ private struct UpdateAccessory: View {
|
||||||
return vm.tr(vm.update.messageKey)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,11 @@ struct PrimaryButton: View {
|
||||||
.font(.body.weight(.semibold))
|
.font(.body.weight(.semibold))
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
.buttonStyle(.glassProminent)
|
.buttonStyle(.glassProminent)
|
||||||
|
#else
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
#endif
|
||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,17 +254,39 @@ extension View {
|
||||||
|
|
||||||
/// Detail screens with their own bottom action hide the tab bar so the
|
/// Detail screens with their own bottom action hide the tab bar so the
|
||||||
/// primary button is never covered by it.
|
/// primary button is never covered by it.
|
||||||
|
@ViewBuilder
|
||||||
func detailScreen(_ title: String, subtitle: String? = nil) -> some View {
|
func detailScreen(_ title: String, subtitle: String? = nil) -> some View {
|
||||||
|
#if os(iOS)
|
||||||
navigationTitle(title)
|
navigationTitle(title)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navSubtitle(subtitle)
|
.navSubtitle(subtitle)
|
||||||
.toolbar(.hidden, for: .tabBar)
|
.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.
|
/// Full-width, leading-aligned content for exercise screens.
|
||||||
func exerciseContent() -> some View {
|
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)
|
frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding()
|
.padding()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ private struct ComboLine: View {
|
||||||
if let word, idx < picks.count { picks[idx] = word }
|
if let word, idx < picks.count { picks[idx] = word }
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
.presentationDetents([.medium, .large])
|
.sheetDetents()
|
||||||
}
|
}
|
||||||
if revealed, let meaning, !meaning.isEmpty {
|
if revealed, let meaning, !meaning.isEmpty {
|
||||||
Meaning(text: vm.tr(meaning), palette: p, visible: true)
|
Meaning(text: vm.tr(meaning), palette: p, visible: true)
|
||||||
|
|
@ -334,8 +334,7 @@ struct WordPicker: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.presentationDetents([.medium, .large])
|
.sheetDetents()
|
||||||
.presentationDragIndicator(.visible)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -781,7 +780,7 @@ private struct PrefixMenu: View {
|
||||||
if let word { value = word }
|
if let word { value = word }
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
.presentationDetents([.medium, .large])
|
.sheetDetents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ struct PracticeHome: View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text(vm.tr("welcome_title"))
|
Text(vm.tr("welcome_title"))
|
||||||
.font(.title.bold())
|
.font(.title.bold())
|
||||||
Text(vm.tr("welcome_body_ios"))
|
Text(vm.tr(Self.welcomeBodyKey))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
ProgressView(value: pct)
|
ProgressView(value: pct)
|
||||||
|
|
@ -50,7 +50,17 @@ struct PracticeHome: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Maturita")
|
.navigationTitle("Maturita")
|
||||||
|
#if os(iOS)
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.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 {
|
private func subjectLabel(_ s: J) -> some View {
|
||||||
|
|
@ -458,32 +468,20 @@ struct SlidesScreen: View {
|
||||||
@State private var idx = 0
|
@State private var idx = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $idx) {
|
Group {
|
||||||
ForEach(slides.indices, id: \.self) { i in
|
#if os(iOS)
|
||||||
let slide = slides[i]
|
TabView(selection: $idx) {
|
||||||
ScrollView {
|
ForEach(slides.indices, id: \.self) { i in
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
slidePage(slides[i]).tag(i)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
.tag(i)
|
|
||||||
}
|
}
|
||||||
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
|
#else
|
||||||
|
if slides.indices.contains(idx) {
|
||||||
|
slidePage(slides[idx])
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
|
||||||
.detailScreen(title, subtitle: subtitle)
|
.detailScreen(title, subtitle: subtitle)
|
||||||
.safeAreaInset(edge: .bottom) {
|
.safeAreaInset(edge: .bottom) {
|
||||||
VStack(spacing: 10) {
|
VStack(spacing: 10) {
|
||||||
|
|
@ -503,7 +501,11 @@ struct SlidesScreen: View {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
.frame(minWidth: 24)
|
.frame(minWidth: 24)
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
.buttonStyle(.glass)
|
.buttonStyle(.glass)
|
||||||
|
#else
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
#endif
|
||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
}
|
}
|
||||||
if idx < slides.count - 1 {
|
if idx < slides.count - 1 {
|
||||||
|
|
@ -523,6 +525,31 @@ struct SlidesScreen: View {
|
||||||
.background(.bar)
|
.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 {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
429
macos/Maturita.xcodeproj/project.pbxproj
Normal file
|
|
@ -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 = "<group>"; };
|
||||||
|
B201000000000000000002 /* Platform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = "<group>"; };
|
||||||
|
B201000000000000000003 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = "<group>"; };
|
||||||
|
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 = "<group>"; };
|
||||||
|
B201000000000000000017 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
B201000000000000000018 /* Maturita.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Maturita.entitlements; sourceTree = "<group>"; };
|
||||||
|
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 = "<group>";
|
||||||
|
};
|
||||||
|
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 = "<group>";
|
||||||
|
};
|
||||||
|
B501000000000000000003 /* Data */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B201000000000000000006 /* JSON.swift */,
|
||||||
|
B201000000000000000007 /* Content.swift */,
|
||||||
|
B201000000000000000008 /* Theme.swift */,
|
||||||
|
B201000000000000000009 /* Answers.swift */,
|
||||||
|
B201000000000000000010 /* ProgressStore.swift */,
|
||||||
|
);
|
||||||
|
name = Data;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
B501000000000000000004 /* Update */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B201000000000000000003 /* Updater.swift */,
|
||||||
|
);
|
||||||
|
path = Update;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
B501000000000000000005 /* UI */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B201000000000000000011 /* Components.swift */,
|
||||||
|
B201000000000000000012 /* Icons.swift */,
|
||||||
|
B201000000000000000013 /* Screens.swift */,
|
||||||
|
B201000000000000000014 /* Exercises.swift */,
|
||||||
|
B201000000000000000015 /* ExercisesMore.swift */,
|
||||||
|
);
|
||||||
|
name = UI;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
B501000000000000000006 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B301000000000000000001 /* Maturita.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* 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 */;
|
||||||
|
}
|
||||||
7
macos/Maturita.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2700"
|
||||||
|
version = "1.8">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "B601000000000000000001"
|
||||||
|
BuildableName = "Maturita.app"
|
||||||
|
BlueprintName = "Maturita"
|
||||||
|
ReferencedContainer = "container:Maturita.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeFlags = "YES"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "NO"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "B601000000000000000001"
|
||||||
|
BuildableName = "Maturita.app"
|
||||||
|
BlueprintName = "Maturita"
|
||||||
|
ReferencedContainer = "container:Maturita.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeFlags = "YES"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "B601000000000000000001"
|
||||||
|
BuildableName = "Maturita.app"
|
||||||
|
BlueprintName = "Maturita"
|
||||||
|
ReferencedContainer = "container:Maturita.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 46 KiB |
BIN
macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 153 KiB |
BIN
macos/Maturita/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 365 KiB |
6
macos/Maturita/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
12
macos/Maturita/Info.plist
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>GITCommit</key>
|
||||||
|
<string>$(COMMIT)</string>
|
||||||
|
<key>UPDATE_BRANCH</key>
|
||||||
|
<string>builds</string>
|
||||||
|
<key>UPDATE_REPO</key>
|
||||||
|
<string>pepaskopec-blip/maturita.c</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
10
macos/Maturita/Maturita.entitlements
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<false/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
171
macos/Maturita/MaturitaApp.swift
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
macos/Maturita/Platform.swift
Normal file
|
|
@ -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
|
||||||
233
macos/Maturita/Update/Updater.swift
Normal file
|
|
@ -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<String, Error> = .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<Void, Error> = .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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
scripts/bundle-macos-swift.sh
Executable file
|
|
@ -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"
|
||||||
|
|
@ -10,8 +10,9 @@ from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
SRC = ROOT / "src"
|
SRC = ROOT / "src"
|
||||||
# Default target is the Android asset; Xcode passes `--out` and copies the
|
# Default target is the Android asset; Xcode (iOS / native macOS) passes
|
||||||
# file into the app bundle itself, so nothing generated lives under ios/.
|
# `--out` and copies the file into the app bundle itself, so nothing
|
||||||
|
# generated lives under ios/ or macos/.
|
||||||
OUTS = [
|
OUTS = [
|
||||||
ROOT / "android" / "app" / "src" / "main" / "assets" / "content.json",
|
ROOT / "android" / "app" / "src" / "main" / "assets" / "content.json",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,11 @@ const TrEntry tr_ui[] = {
|
||||||
"stejný obsah, rozhraní podle iOS.",
|
"stejný obsah, rozhraní podle iOS.",
|
||||||
"Native exam practice on iPhone. Czech and English, "
|
"Native exam practice on iPhone. Czech and English, "
|
||||||
"the same content, adapted to iOS."},
|
"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",
|
{"update_ios_sideload",
|
||||||
"Nový build je na GitHubu. Nainstalujte ho znovu přes Xcode nebo AltStore (%s).",
|
"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)."},
|
"A newer build is on GitHub. Reinstall it with Xcode or AltStore (%s)."},
|
||||||
|
|
|
||||||