Version 1.1 App Store

This commit is contained in:
Scarriffle
2026-05-25 10:21:11 +02:00
parent 7ca511d37f
commit 15d8e71d09
17 changed files with 478 additions and 71 deletions

View File

@@ -5,9 +5,22 @@ struct LibraryGridView: View {
var onRefresh: (() async -> Void)? = nil
var onSelect: (LibraryItem) -> Void
@AppStorage("libraryCoverSize") private var coverSize: Double = Self.defaultCoverSize
var body: some View {
#if os(macOS)
VStack(spacing: 0) {
zoomBar
gridContent
}
#else
gridContent
#endif
}
private var gridContent: some View {
ScrollView {
LazyVGrid(columns: gridColumns, spacing: 8) {
LazyVGrid(columns: gridColumns, spacing: gridSpacing) {
ForEach(items) { item in
LibraryItemCell(item: item)
.onTapGesture { onSelect(item) }
@@ -25,14 +38,59 @@ struct LibraryGridView: View {
#endif
}
#if os(macOS)
private var zoomBar: some View {
HStack(spacing: 8) {
Spacer()
Image(systemName: "rectangle.grid.3x2")
.font(.caption2)
.foregroundStyle(.secondary)
Slider(value: $coverSize, in: Self.minCoverSize...Self.maxCoverSize)
.frame(maxWidth: 220)
Image(systemName: "square")
.font(.caption2)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
}
#endif
private var gridColumns: [GridItem] {
[GridItem(.adaptive(minimum: CGFloat(coverSize)), spacing: gridSpacing)]
}
private var gridSpacing: CGFloat {
#if os(iOS)
// 3 equal columns compact spacing for full height utilization
[GridItem(.flexible(), spacing: 8),
GridItem(.flexible(), spacing: 8),
GridItem(.flexible())]
return 8
#else
[GridItem(.adaptive(minimum: 180), spacing: 20)]
return 20
#endif
}
// MARK: - Cover-Größen-Defaults (plattformabhängig)
private static var defaultCoverSize: Double {
#if os(iOS)
return 110
#else
return 180
#endif
}
private static var minCoverSize: Double {
#if os(iOS)
return 80
#else
return 120
#endif
}
private static var maxCoverSize: Double {
#if os(iOS)
return 200
#else
return 320
#endif
}
}

View File

@@ -45,17 +45,44 @@ struct LibraryItemCell: View {
#if os(iOS)
private var iOSCover: some View {
Color(.systemGray6) // neutral bg for PNG transparent areas
.frame(maxWidth: .infinity) // explicitly fill the column width
coverContainer(background: AnyShapeStyle(Color(.systemGray6)))
}
#endif
#if os(macOS)
private var macosCover: some View {
coverContainer(background: AnyShapeStyle(.quaternary))
}
#endif
/// Quadratischer Cover-Container: füllt die Spaltenbreite, behält 1:1-Aspektverhältnis
/// und zeigt das Bild **ohne Verzerrung** (`.scaledToFit`). Nicht-quadratische Cover
/// bekommen Letterbox-/Pillarbox-Ränder im neutralen Hintergrund.
private func coverContainer(background: AnyShapeStyle) -> some View {
Rectangle()
.fill(background)
.frame(maxWidth: .infinity)
.aspectRatio(1, contentMode: .fit)
.overlay {
if let url = app.client.coverURL(itemId: item.id) {
AsyncImage(url: url) { image in
image.resizable().scaledToFill()
} placeholder: {
ProgressView().tint(.accentColor)
AsyncImage(url: url) { phase in
switch phase {
case .success(let img):
img.resizable().scaledToFit()
case .empty:
ProgressView()
#if os(macOS)
.controlSize(.small)
#else
.tint(.accentColor)
#endif
case .failure:
Image(systemName: "book.closed")
.foregroundStyle(.secondary)
@unknown default:
EmptyView()
}
}
.clipped() // clip image overflow before rounding
} else {
Image(systemName: "book.closed")
.foregroundStyle(.secondary)
@@ -63,34 +90,6 @@ struct LibraryItemCell: View {
}
.clipShape(RoundedRectangle(cornerRadius: 8))
}
#endif
#if os(macOS)
private var macosCover: some View {
Group {
if let url = app.client.coverURL(itemId: item.id) {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
Rectangle().fill(.quaternary)
.overlay(ProgressView().controlSize(.small))
case .success(let img):
img.resizable().aspectRatio(contentMode: .fill)
case .failure:
Rectangle().fill(.quaternary)
.overlay(Image(systemName: "book.closed").foregroundStyle(.secondary))
@unknown default:
Rectangle().fill(.quaternary)
}
}
} else {
Rectangle().fill(.quaternary)
}
}
.frame(width: 180, height: 180)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
#endif
// MARK: - Download badge

View File

@@ -62,7 +62,7 @@ struct PlayerBar: View {
}
scrubber
HStack(spacing: 24) {
HStack(spacing: 20) {
Button { app.skip(by: -Double(skipSeconds)) } label: {
Image(systemName: skipBackImage).font(.system(size: 22))
}
@@ -77,6 +77,8 @@ struct PlayerBar: View {
Spacer()
sleepMenu
rateMenu
Button {
@@ -113,6 +115,8 @@ struct PlayerBar: View {
rateMenu
sleepMenu
Spacer(minLength: 0)
statusIndicator
@@ -218,9 +222,73 @@ struct PlayerBar: View {
.font(.caption2.monospacedDigit())
.foregroundStyle(.secondary)
}
if app.player.sleepTimer != .off {
HStack(spacing: 4) {
Image(systemName: "moon.zzz.fill")
.font(.caption2)
Text("\(formatTime(app.player.sleepRemainingSeconds)) · endet \(formatWallTime(sleepEndsAt))")
.font(.caption2.monospacedDigit())
}
.foregroundStyle(.tint)
.frame(maxWidth: .infinity, alignment: .center)
}
}
}
private var sleepMenu: some View {
Menu {
sleepOption(title: "Aus", mode: .off)
Divider()
sleepOption(title: "10 Minuten", mode: .minutes(10))
sleepOption(title: "20 Minuten", mode: .minutes(20))
sleepOption(title: "30 Minuten", mode: .minutes(30))
sleepOption(title: "1 Stunde", mode: .minutes(60))
sleepOption(title: endOfPlaybackLabel, mode: .endOfBook)
} label: {
Image(systemName: app.player.sleepTimer == .off ? "moon.zzz" : "moon.zzz.fill")
#if os(iOS)
.font(.system(size: 22))
#else
.font(.system(size: 16))
#endif
.foregroundStyle(app.player.sleepTimer == .off ? Color.secondary : Color.accentColor)
}
#if os(macOS)
.menuStyle(.borderlessButton)
.fixedSize()
#endif
.menuIndicator(.hidden)
.help("Sleep-Timer")
.disabled(!app.player.isReady)
}
@ViewBuilder
private func sleepOption(title: String, mode: SleepTimerMode) -> some View {
Button {
app.player.setSleepTimer(mode)
} label: {
if app.player.sleepTimer == mode {
Label(title, systemImage: "checkmark")
} else {
Text(title)
}
}
}
private var sleepEndsAt: Date {
Date().addingTimeInterval(app.player.sleepRemainingSeconds)
}
private var endOfPlaybackLabel: String {
app.currentItem?.isPodcast == true
? "Bis Ende der Folge"
: "Bis Ende des Hörbuchs"
}
private func formatWallTime(_ date: Date) -> String {
date.formatted(.dateTime.hour().minute())
}
private var rateMenu: some View {
Menu {
ForEach([0.75, 1.0, 1.25, 1.5, 1.75, 2.0], id: \.self) { r in