mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Enlarge Mac UI density for readable desktop layouts.
Bump typography, controls, path-map nodes, and window size so macOS no longer feels phone-scaled. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b6fa222659
commit
cc4415a8a6
6 changed files with 376 additions and 98 deletions
|
|
@ -8,6 +8,10 @@ struct MaturitaApp: App {
|
|||
WindowGroup {
|
||||
NativeRoot(vm: vm)
|
||||
}
|
||||
#if os(macOS)
|
||||
.defaultSize(width: 1280, height: 840)
|
||||
.windowResizability(.contentMinSize)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25,6 +29,9 @@ struct NativeRoot: View {
|
|||
.tint(vm.palette.tint)
|
||||
.preferredColorScheme(vm.mode == .dark ? .dark : .light)
|
||||
.onAppear { vm.checkUpdate(false) }
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 980, minHeight: 640)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var showsUpdateAccessory: Bool {
|
||||
|
|
@ -68,11 +75,18 @@ private struct UpdateAccessory: View {
|
|||
var body: some View {
|
||||
HStack {
|
||||
Text(bannerText)
|
||||
#if os(macOS)
|
||||
.font(.body)
|
||||
#else
|
||||
.font(.subheadline)
|
||||
#endif
|
||||
Spacer()
|
||||
if vm.update.canInstall {
|
||||
Button(vm.tr("update_install")) { vm.installUpdate() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
#if os(macOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,17 @@
|
|||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
enum MacChrome {
|
||||
static let contentMaxWidth: CGFloat = 1080
|
||||
static let exerciseMaxWidth: CGFloat = 900
|
||||
static let cardPadding: CGFloat = 24
|
||||
static let listRowMinHeight: CGFloat = 52
|
||||
static let homeTileMin: CGFloat = 260
|
||||
static let homeTileHeight: CGFloat = 176
|
||||
static let pathNode: CGFloat = 84
|
||||
}
|
||||
#endif
|
||||
|
||||
struct FlowLayout: Layout {
|
||||
var spacing: CGFloat = 6
|
||||
|
||||
|
|
@ -48,6 +60,9 @@ struct PrimaryButton: View {
|
|||
Text(label)
|
||||
.font(.body.weight(.semibold))
|
||||
.frame(maxWidth: .infinity)
|
||||
#if os(macOS)
|
||||
.padding(.vertical, 4)
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
|
@ -66,6 +81,9 @@ struct PillButton: View {
|
|||
var body: some View {
|
||||
Button(label, action: action)
|
||||
.buttonStyle(.bordered)
|
||||
#if os(macOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +92,11 @@ struct GlassCard<Content: View>: View {
|
|||
|
||||
var body: some View {
|
||||
content()
|
||||
#if os(macOS)
|
||||
.padding(MacChrome.cardPadding)
|
||||
#else
|
||||
.padding(16)
|
||||
#endif
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.softSurface(cornerRadius: 22)
|
||||
}
|
||||
|
|
@ -107,7 +129,11 @@ struct FeedbackLine: View {
|
|||
} icon: {
|
||||
Image(systemName: kind == "ok" ? "checkmark.circle.fill" : kind == "warn" ? "exclamationmark.triangle.fill" : "xmark.circle.fill")
|
||||
}
|
||||
#if os(macOS)
|
||||
.font(.body.weight(.semibold))
|
||||
#else
|
||||
.font(.subheadline.weight(.semibold))
|
||||
#endif
|
||||
.foregroundStyle(kind == "ok" ? palette.success : kind == "warn" ? palette.warning : palette.error)
|
||||
}
|
||||
}
|
||||
|
|
@ -121,7 +147,11 @@ struct Meaning: View {
|
|||
var body: some View {
|
||||
if visible && !text.isEmpty {
|
||||
Text(text)
|
||||
#if os(macOS)
|
||||
.font(.body)
|
||||
#else
|
||||
.font(.footnote)
|
||||
#endif
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
|
|
@ -139,13 +169,20 @@ struct WordField: View {
|
|||
TextField(placeholder, text: $value)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
if let mark {
|
||||
Image(systemName: mark ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||
.foregroundStyle(mark ? (palette?.success ?? .green) : (palette?.error ?? .red))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
#if os(macOS)
|
||||
.padding(.vertical, 14)
|
||||
#else
|
||||
.padding(.vertical, 10)
|
||||
#endif
|
||||
.background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
|
|
@ -169,20 +206,35 @@ struct SegChip: View {
|
|||
Button(label, action: action)
|
||||
.buttonStyle(.bordered)
|
||||
.tint(selected ? .accentColor : .secondary)
|
||||
#if os(macOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct Hint: View {
|
||||
let text: String
|
||||
var body: some View {
|
||||
Text(text).font(.footnote).foregroundStyle(.secondary).padding(.bottom, 6)
|
||||
Text(text)
|
||||
#if os(macOS)
|
||||
.font(.body)
|
||||
#else
|
||||
.font(.footnote)
|
||||
#endif
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
}
|
||||
|
||||
struct Prompt: View {
|
||||
let text: String
|
||||
var body: some View {
|
||||
Text(text).font(.body.weight(.medium))
|
||||
Text(text)
|
||||
#if os(macOS)
|
||||
.font(.title3.weight(.medium))
|
||||
#else
|
||||
.font(.body.weight(.medium))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +246,11 @@ struct WordChip: View {
|
|||
Button(text, action: action)
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.primary)
|
||||
#if os(macOS)
|
||||
.controlSize(.large)
|
||||
#else
|
||||
.controlSize(.small)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,8 +261,13 @@ struct WordSlot<Content: View>: View {
|
|||
|
||||
var body: some View {
|
||||
content()
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: .infinity, minHeight: 56, alignment: .leading)
|
||||
.padding(12)
|
||||
#else
|
||||
.frame(maxWidth: .infinity, minHeight: 44, alignment: .leading)
|
||||
.padding(8)
|
||||
#endif
|
||||
.background(Color(.tertiarySystemFill), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
|
|
@ -224,18 +285,29 @@ struct OptionChip: View {
|
|||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: symbol)
|
||||
.font(.title3)
|
||||
.foregroundStyle(symbolColor)
|
||||
Text(text)
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
#if os(macOS)
|
||||
.padding(.vertical, 8)
|
||||
#else
|
||||
.padding(.vertical, 4)
|
||||
#endif
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(tint)
|
||||
#if os(macOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
|
||||
|
|
@ -296,8 +368,8 @@ extension View {
|
|||
/// Full-width, leading-aligned content for exercise screens.
|
||||
func exerciseContent() -> some View {
|
||||
#if os(macOS)
|
||||
frame(maxWidth: 720, alignment: .leading)
|
||||
.padding(20)
|
||||
frame(maxWidth: MacChrome.exerciseMaxWidth, alignment: .leading)
|
||||
.padding(28)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#else
|
||||
frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
|
@ -311,6 +383,9 @@ extension View {
|
|||
func appListStyle() -> some View {
|
||||
#if os(macOS)
|
||||
listStyle(.inset)
|
||||
.environment(\.defaultMinListRowHeight, MacChrome.listRowMinHeight)
|
||||
.scrollContentBackground(.hidden)
|
||||
.padding(.horizontal, 8)
|
||||
#else
|
||||
self
|
||||
#endif
|
||||
|
|
@ -345,8 +420,39 @@ struct BottomAction<Extra: View>: View {
|
|||
PrimaryButton(label: label, action: action)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
#if os(macOS)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 10)
|
||||
.frame(maxWidth: MacChrome.exerciseMaxWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
#else
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 4)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// Primary + secondary text sized for desktop lists.
|
||||
struct ListRowLabel: View {
|
||||
let title: String
|
||||
var subtitle: String? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
if let subtitle, !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
#if os(macOS)
|
||||
.font(.body)
|
||||
#else
|
||||
.font(.caption)
|
||||
#endif
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -384,7 +384,14 @@ private struct AssignEx: View {
|
|||
SegChip(label: groups[i], selected: active == i) { active = i }
|
||||
}
|
||||
}
|
||||
Text(vm.tr("wordbank")).font(.caption).foregroundStyle(.secondary).padding(.top, 10)
|
||||
Text(vm.tr("wordbank"))
|
||||
#if os(macOS)
|
||||
.font(.body)
|
||||
#else
|
||||
.font(.caption)
|
||||
#endif
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 10)
|
||||
FlowLayout(spacing: 6) {
|
||||
ForEach(items.indices, id: \.self) { i in
|
||||
if loc[i] == -1 {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ private struct Serpentine {
|
|||
}
|
||||
|
||||
/// Keep enough room under/above each node for a two-line caption.
|
||||
#if os(macOS)
|
||||
private let labelBand: CGFloat = 56
|
||||
#else
|
||||
private let labelBand: CGFloat = 44
|
||||
#endif
|
||||
|
||||
private func serpentine(
|
||||
count n: Int,
|
||||
|
|
@ -138,7 +142,7 @@ struct PathMap: View {
|
|||
nodeSize: nodeSize,
|
||||
lift: branch == nil ? 0 : 56
|
||||
)
|
||||
let labelWidth = min(118, max(72, layout.spacing - 16))
|
||||
let labelWidth = min(140, max(88, layout.spacing - 16))
|
||||
let canvasH = layout.height + labelBand + 24
|
||||
ScrollView([.horizontal, .vertical]) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
|
|
@ -151,7 +155,11 @@ struct PathMap: View {
|
|||
nodeView(node, index: i)
|
||||
.position(x: pt.x, y: pt.y)
|
||||
Text(node.label)
|
||||
#if os(macOS)
|
||||
.font(.callout.weight(.semibold))
|
||||
#else
|
||||
.font(.caption2.weight(.semibold))
|
||||
#endif
|
||||
.foregroundStyle(node.locked ? palette.overlay : palette.text)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
|
|
@ -167,19 +175,23 @@ struct PathMap: View {
|
|||
|
||||
if let branch, branch.index < layout.points.count {
|
||||
let base = layout.points[branch.index]
|
||||
let tip = NodePos(x: base.x + 52, y: base.y - 110, row: base.row)
|
||||
let tip = NodePos(x: base.x + 64, y: base.y - 128, row: base.row)
|
||||
Path { p in
|
||||
p.move(to: CGPoint(x: base.x, y: base.y - nodeSize / 2))
|
||||
p.addQuadCurve(
|
||||
to: CGPoint(x: tip.x, y: tip.y + nodeSize / 2),
|
||||
control: CGPoint(x: base.x + 12, y: tip.y + 24)
|
||||
control: CGPoint(x: base.x + 14, y: tip.y + 28)
|
||||
)
|
||||
}
|
||||
.stroke(palette.rail, style: StrokeStyle(lineWidth: 8, lineCap: .round))
|
||||
.stroke(palette.rail, style: StrokeStyle(lineWidth: 10, lineCap: .round))
|
||||
nodeView(branch.node, index: nil)
|
||||
.position(x: tip.x, y: tip.y)
|
||||
Text(branch.node.label)
|
||||
#if os(macOS)
|
||||
.font(.callout.weight(.semibold))
|
||||
#else
|
||||
.font(.caption2.weight(.semibold))
|
||||
#endif
|
||||
.foregroundStyle(palette.text)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
|
|
@ -195,7 +207,11 @@ struct PathMap: View {
|
|||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(minHeight: 520)
|
||||
#else
|
||||
.frame(minHeight: 420)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -240,25 +256,60 @@ struct PathMap: View {
|
|||
}
|
||||
if node.finish {
|
||||
VStack(spacing: 2) {
|
||||
if let index { Text("\(index + 1)").font(.headline.weight(.black)) }
|
||||
if let index {
|
||||
Text("\(index + 1)")
|
||||
#if os(macOS)
|
||||
.font(.title2.weight(.black))
|
||||
#else
|
||||
.font(.headline.weight(.black))
|
||||
#endif
|
||||
}
|
||||
FinishFlagBadge(palette: palette)
|
||||
}
|
||||
.foregroundStyle(palette.overlay)
|
||||
} else if node.locked {
|
||||
VStack(spacing: 2) {
|
||||
if let index { Text("\(index + 1)").font(.headline.weight(.black)) }
|
||||
Image(systemName: "lock.fill").font(.caption2.weight(.bold))
|
||||
if let index {
|
||||
Text("\(index + 1)")
|
||||
#if os(macOS)
|
||||
.font(.title2.weight(.black))
|
||||
#else
|
||||
.font(.headline.weight(.black))
|
||||
#endif
|
||||
}
|
||||
Image(systemName: "lock.fill")
|
||||
#if os(macOS)
|
||||
.font(.caption.weight(.bold))
|
||||
#else
|
||||
.font(.caption2.weight(.bold))
|
||||
#endif
|
||||
}
|
||||
.foregroundStyle(palette.overlay)
|
||||
} else if node.done {
|
||||
VStack(spacing: 2) {
|
||||
if let index { Text("\(index + 1)").font(.headline.weight(.black)) }
|
||||
Image(systemName: "checkmark").font(.caption2.weight(.bold))
|
||||
if let index {
|
||||
Text("\(index + 1)")
|
||||
#if os(macOS)
|
||||
.font(.title2.weight(.black))
|
||||
#else
|
||||
.font(.headline.weight(.black))
|
||||
#endif
|
||||
}
|
||||
Image(systemName: "checkmark")
|
||||
#if os(macOS)
|
||||
.font(.caption.weight(.bold))
|
||||
#else
|
||||
.font(.caption2.weight(.bold))
|
||||
#endif
|
||||
}
|
||||
.foregroundStyle(palette.onAccent)
|
||||
} else if let index {
|
||||
Text("\(index + 1)")
|
||||
#if os(macOS)
|
||||
.font(.title2.weight(.black))
|
||||
#else
|
||||
.font(.headline.weight(.black))
|
||||
#endif
|
||||
.foregroundStyle(node.current ? palette.onAccent : palette.text)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,15 +37,15 @@ struct PracticeHome: View {
|
|||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(vm.tr("subjects_title"))
|
||||
.font(.title2.weight(.semibold))
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 220), spacing: 16)], spacing: 16) {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: MacChrome.homeTileMin), spacing: 18)], spacing: 18) {
|
||||
ForEach(Array(vm.content.subjects.enumerated()), id: \.offset) { _, s in
|
||||
subjectTile(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(28)
|
||||
.frame(maxWidth: 920, alignment: .leading)
|
||||
.padding(32)
|
||||
.frame(maxWidth: MacChrome.contentMaxWidth, alignment: .leading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.navigationTitle("Maturita")
|
||||
|
|
@ -70,28 +70,28 @@ struct PracticeHome: View {
|
|||
}
|
||||
|
||||
private func subjectTileBody(_ s: J, open: Bool, progress: Double, part: ProgressSum) -> some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
SubjectIcon(icon: s.str("icon"), open: open, size: 36)
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SubjectIcon(icon: s.str("icon"), open: open, size: 44)
|
||||
Text(vm.tr(s.str("key")))
|
||||
.font(.headline)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(open ? .primary : .secondary)
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer(minLength: 0)
|
||||
if open && part.totalEx > 0 {
|
||||
ProgressView(value: progress)
|
||||
Text(vm.fmt("stats_ex_fmt", part.doneEx, part.totalEx))
|
||||
.font(.caption)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.monospacedDigit()
|
||||
} else if !open {
|
||||
Text(vm.tr("stats_locked"))
|
||||
.font(.caption)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(18)
|
||||
.frame(maxWidth: .infinity, minHeight: 148, alignment: .topLeading)
|
||||
.softSurface(cornerRadius: 20)
|
||||
.padding(22)
|
||||
.frame(maxWidth: .infinity, minHeight: MacChrome.homeTileHeight, alignment: .topLeading)
|
||||
.softSurface(cornerRadius: 22)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
@ -193,10 +193,11 @@ struct ChangelogCard: View {
|
|||
Button(vm.tr("changelog_dismiss")) { vm.dismissChangelog() }
|
||||
#if os(macOS)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
#else
|
||||
.buttonStyle(.glassProminent)
|
||||
#endif
|
||||
.controlSize(.small)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -233,20 +234,43 @@ struct SettingsScreen: View {
|
|||
Text(vm.tr("mode_light")).tag(ColorMode.light)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
#if os(macOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
}
|
||||
Section(vm.tr("theme")) {
|
||||
ForEach(Array(ThemeId.allCases), id: \.self) { id in
|
||||
Button {
|
||||
vm.setTheme(id)
|
||||
} label: {
|
||||
HStack {
|
||||
Circle().fill(themePalette(id, vm.mode).tint).frame(width: 14, height: 14)
|
||||
Text(themeNames[id.rawValue]).foregroundStyle(.primary)
|
||||
HStack(spacing: 12) {
|
||||
Circle().fill(themePalette(id, vm.mode).tint)
|
||||
.frame(width: {
|
||||
#if os(macOS)
|
||||
18
|
||||
#else
|
||||
14
|
||||
#endif
|
||||
}(), height: {
|
||||
#if os(macOS)
|
||||
18
|
||||
#else
|
||||
14
|
||||
#endif
|
||||
}())
|
||||
Text(themeNames[id.rawValue])
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
.foregroundStyle(.primary)
|
||||
Spacer()
|
||||
if vm.themeId == id {
|
||||
Image(systemName: "checkmark").fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.padding(.vertical, 4)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -256,12 +280,25 @@ struct SettingsScreen: View {
|
|||
Text("English").tag(UiLang.en)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
#if os(macOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
}
|
||||
Section(vm.tr("updates")) {
|
||||
Text(updateText).foregroundStyle(.secondary)
|
||||
Text(updateText)
|
||||
#if os(macOS)
|
||||
.font(.body)
|
||||
#endif
|
||||
.foregroundStyle(.secondary)
|
||||
Button(vm.tr("update_check")) { vm.checkUpdate(true) }
|
||||
#if os(macOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
if vm.update.canInstall {
|
||||
Button(vm.tr("update_install")) { vm.installUpdate() }
|
||||
#if os(macOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
}
|
||||
LabeledContent("Build", value: "\(AppConfig.versionName) · \(String(AppConfig.commit.prefix(7)))")
|
||||
}
|
||||
|
|
@ -316,9 +353,16 @@ struct StatsScreen: View {
|
|||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(vm.tr(s.str("key")))
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
Spacer()
|
||||
Text(open ? vm.fmt("stats_ex_fmt", part.doneEx, part.totalEx) : vm.tr("stats_locked"))
|
||||
#if os(macOS)
|
||||
.font(.body)
|
||||
#else
|
||||
.font(.caption)
|
||||
#endif
|
||||
.foregroundStyle(.secondary)
|
||||
.monospacedDigit()
|
||||
}
|
||||
|
|
@ -327,7 +371,13 @@ struct StatsScreen: View {
|
|||
}
|
||||
}
|
||||
} icon: {
|
||||
SubjectIcon(icon: s.str("icon"), open: open)
|
||||
SubjectIcon(icon: s.str("icon"), open: open, size: {
|
||||
#if os(macOS)
|
||||
32
|
||||
#else
|
||||
26
|
||||
#endif
|
||||
}())
|
||||
}
|
||||
.foregroundStyle(open ? .primary : .secondary)
|
||||
}
|
||||
|
|
@ -339,8 +389,20 @@ struct StatsScreen: View {
|
|||
|
||||
private func stat(_ label: String, _ value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(label).font(.caption).foregroundStyle(.secondary)
|
||||
Text(value).font(.title3.bold()).monospacedDigit()
|
||||
Text(label)
|
||||
#if os(macOS)
|
||||
.font(.subheadline)
|
||||
#else
|
||||
.font(.caption)
|
||||
#endif
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
#if os(macOS)
|
||||
.font(.title2.bold())
|
||||
#else
|
||||
.font(.title3.bold())
|
||||
#endif
|
||||
.monospacedDigit()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
|
@ -393,10 +455,7 @@ struct SearchScreen: View {
|
|||
#endif
|
||||
if !hit.locked { vm.openFromSearch(hit.target) }
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(hit.title)
|
||||
Text(hit.sub).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
ListRowLabel(title: hit.title, subtitle: hit.sub)
|
||||
}
|
||||
.disabled(hit.locked)
|
||||
.foregroundStyle(hit.locked ? .secondary : .primary)
|
||||
|
|
@ -542,14 +601,14 @@ struct RoadmapScreen: View {
|
|||
},
|
||||
palette: vm.palette,
|
||||
litUntil: lit,
|
||||
nodeSize: 68,
|
||||
mx: 88,
|
||||
my: 120,
|
||||
spac: 168,
|
||||
gap: 240,
|
||||
wave: 16
|
||||
nodeSize: MacChrome.pathNode,
|
||||
mx: 100,
|
||||
my: 140,
|
||||
spac: 196,
|
||||
gap: 280,
|
||||
wave: 18
|
||||
)
|
||||
.padding(20)
|
||||
.padding(28)
|
||||
.navigationTitle(vm.tr("roadmap_title"))
|
||||
.navigationSubtitle(vm.tr("roadmap_sub"))
|
||||
}
|
||||
|
|
@ -628,15 +687,15 @@ struct UnitMapScreen: View {
|
|||
},
|
||||
palette: vm.palette,
|
||||
litUntil: lit,
|
||||
nodeSize: 64,
|
||||
mx: 88,
|
||||
my: 118,
|
||||
spac: 164,
|
||||
gap: 230,
|
||||
wave: 14,
|
||||
nodeSize: MacChrome.pathNode,
|
||||
mx: 100,
|
||||
my: 136,
|
||||
spac: 190,
|
||||
gap: 268,
|
||||
wave: 16,
|
||||
branch: branch
|
||||
)
|
||||
.padding(20)
|
||||
.padding(28)
|
||||
.navigationTitle(u.str("title"))
|
||||
.navigationSubtitle(vm.tr(u.str("sub")))
|
||||
}
|
||||
|
|
@ -674,17 +733,11 @@ struct NetYearsScreen: View {
|
|||
var body: some View {
|
||||
List {
|
||||
NavigationLink(value: Route.netMap) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(vm.tr("net_year1"))
|
||||
Text(vm.tr("net_sub")).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
ListRowLabel(title: vm.tr("net_year1"), subtitle: vm.tr("net_sub"))
|
||||
}
|
||||
ForEach(["net_year2", "net_year3", "net_year4"], id: \.self) { key in
|
||||
Label {
|
||||
VStack(alignment: .leading) {
|
||||
Text(vm.tr(key))
|
||||
Text(vm.tr("net_year_locked_sub")).font(.caption)
|
||||
}
|
||||
ListRowLabel(title: vm.tr(key), subtitle: vm.tr("net_year_locked_sub"))
|
||||
} icon: {
|
||||
Image(systemName: "lock.fill")
|
||||
}
|
||||
|
|
@ -734,14 +787,14 @@ struct LessonListScreen: View {
|
|||
},
|
||||
palette: vm.palette,
|
||||
litUntil: CGFloat(next ?? rows.count),
|
||||
nodeSize: 64,
|
||||
mx: 86,
|
||||
my: 116,
|
||||
spac: 160,
|
||||
gap: 236,
|
||||
wave: 14
|
||||
nodeSize: MacChrome.pathNode,
|
||||
mx: 98,
|
||||
my: 134,
|
||||
spac: 186,
|
||||
gap: 272,
|
||||
wave: 16
|
||||
)
|
||||
.padding(20)
|
||||
.padding(28)
|
||||
.navigationTitle(title)
|
||||
.navigationSubtitle(subtitle)
|
||||
}
|
||||
|
|
@ -833,25 +886,39 @@ struct SlidesScreen: View {
|
|||
|
||||
private func slidePage(_ slide: J) -> some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text(slide.str("kicker"))
|
||||
#if os(macOS)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
#else
|
||||
.font(.caption.weight(.semibold))
|
||||
#endif
|
||||
.foregroundStyle(.secondary)
|
||||
Text(slide.str("title")).font(.title2.bold())
|
||||
Text(slide.str("title"))
|
||||
#if os(macOS)
|
||||
.font(.title.bold())
|
||||
#else
|
||||
.font(.title2.bold())
|
||||
#endif
|
||||
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)
|
||||
Text(line)
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.padding(.bottom, 24)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 720, alignment: .leading)
|
||||
.frame(maxWidth: MacChrome.exerciseMaxWidth, alignment: .leading)
|
||||
.padding(12)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -862,13 +929,34 @@ struct CzechMapScreen: View {
|
|||
|
||||
var body: some View {
|
||||
List {
|
||||
Label(vm.tr("Literatura"), systemImage: "lock.fill")
|
||||
Label {
|
||||
Text(vm.tr("Literatura"))
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
} icon: {
|
||||
Image(systemName: "lock.fill")
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
NavigationLink(value: Route.mluvnice) {
|
||||
Label(vm.tr("Mluvnice"), systemImage: "textformat")
|
||||
Label {
|
||||
Text(vm.tr("Mluvnice"))
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
} icon: {
|
||||
Image(systemName: "textformat")
|
||||
}
|
||||
}
|
||||
NavigationLink(value: Route.readingList) {
|
||||
Label(vm.tr("Maturitní četba"), systemImage: "books.vertical")
|
||||
Label {
|
||||
Text(vm.tr("Maturitní četba"))
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
} icon: {
|
||||
Image(systemName: "books.vertical")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(vm.tr("Český jazyk a literatura"))
|
||||
|
|
@ -885,15 +973,18 @@ struct BookListScreen: View {
|
|||
ForEach(Array(vm.content.books.enumerated()), id: \.offset) { _, b in
|
||||
NavigationLink(value: Route.book(b.str("id"))) {
|
||||
Label {
|
||||
VStack(alignment: .leading) {
|
||||
Text(b.str("title"))
|
||||
ListRowLabel(
|
||||
title: b.str("title"),
|
||||
subtitle: {
|
||||
let genre = b.str("genre")
|
||||
Text(genre.isEmpty ? vm.tr(b.str("subKey")) : genre)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
return genre.isEmpty ? vm.tr(b.str("subKey")) : genre
|
||||
}()
|
||||
)
|
||||
} icon: {
|
||||
Image(systemName: "book.fill")
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -916,24 +1007,24 @@ struct BookScreen: View {
|
|||
if hasQuiz {
|
||||
NavigationLink(value: Route.bookQuiz(id)) {
|
||||
Label {
|
||||
VStack(alignment: .leading) {
|
||||
Text(vm.tr(b.str("quizTitle")))
|
||||
Text(vm.tr(b.str("quizSub"))).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
ListRowLabel(title: vm.tr(b.str("quizTitle")), subtitle: vm.tr(b.str("quizSub")))
|
||||
} icon: {
|
||||
Image(systemName: "questionmark.circle")
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasPlot {
|
||||
NavigationLink(value: Route.bookPlot(id)) {
|
||||
Label {
|
||||
VStack(alignment: .leading) {
|
||||
Text(vm.tr(b.str("plotTitle")))
|
||||
Text(vm.tr(b.str("plotSub"))).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
ListRowLabel(title: vm.tr(b.str("plotTitle")), subtitle: vm.tr(b.str("plotSub")))
|
||||
} icon: {
|
||||
Image(systemName: "list.number")
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -941,6 +1032,10 @@ struct BookScreen: View {
|
|||
Section(note.str("title")) {
|
||||
ForEach(Array(note.strs("lines").enumerated()), id: \.offset) { _, line in
|
||||
Text(line)
|
||||
#if os(macOS)
|
||||
.font(.title3)
|
||||
.padding(.vertical, 4)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ struct MaturitaApp: App {
|
|||
WindowGroup {
|
||||
MacRoot(vm: vm)
|
||||
}
|
||||
.defaultSize(width: 1100, height: 720)
|
||||
.defaultSize(width: 1280, height: 840)
|
||||
.windowResizability(.contentMinSize)
|
||||
.windowToolbarStyle(.unified)
|
||||
.windowStyle(.automatic)
|
||||
.commands {
|
||||
|
|
@ -37,7 +38,7 @@ struct MaturitaApp: App {
|
|||
Settings {
|
||||
SettingsScreen(vm: vm)
|
||||
.formStyle(.grouped)
|
||||
.frame(minWidth: 420, minHeight: 460)
|
||||
.frame(minWidth: 480, minHeight: 520)
|
||||
.tint(vm.palette.tint)
|
||||
.preferredColorScheme(vm.mode == .dark ? .dark : .light)
|
||||
}
|
||||
|
|
@ -58,7 +59,7 @@ struct MacRoot: View {
|
|||
}
|
||||
}
|
||||
.onAppear { vm.checkUpdate(false) }
|
||||
.frame(minWidth: 880, minHeight: 540)
|
||||
.frame(minWidth: 980, minHeight: 640)
|
||||
}
|
||||
|
||||
private var showsUpdateAccessory: Bool {
|
||||
|
|
@ -108,6 +109,7 @@ private struct MacWindowToolbar: ToolbarContent {
|
|||
} label: {
|
||||
Label(vm.tr("search"), systemImage: "magnifyingglass")
|
||||
}
|
||||
.controlSize(.large)
|
||||
.help(vm.tr("search"))
|
||||
|
||||
Button {
|
||||
|
|
@ -115,6 +117,7 @@ private struct MacWindowToolbar: ToolbarContent {
|
|||
} label: {
|
||||
Label(vm.tr("stats"), systemImage: "chart.bar.fill")
|
||||
}
|
||||
.controlSize(.large)
|
||||
.help(vm.tr("stats"))
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +129,7 @@ private struct MacWindowToolbar: ToolbarContent {
|
|||
} label: {
|
||||
Label(vm.tr("settings"), systemImage: "gearshape")
|
||||
}
|
||||
.controlSize(.large)
|
||||
.help(vm.tr("settings"))
|
||||
}
|
||||
}
|
||||
|
|
@ -137,19 +141,20 @@ private struct UpdateBanner: View {
|
|||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "arrow.down.app.fill")
|
||||
.font(.title3)
|
||||
Text(bannerText)
|
||||
.font(.subheadline)
|
||||
.font(.body)
|
||||
Spacer()
|
||||
if vm.update.canInstall {
|
||||
Button(vm.tr("update_install")) { vm.installUpdate() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
.controlSize(.large)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 14)
|
||||
.softSurface(cornerRadius: 16)
|
||||
.frame(maxWidth: 560)
|
||||
.frame(maxWidth: 640)
|
||||
}
|
||||
|
||||
private var bannerText: String {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue