Files
ABS-Client/ABS Client/Audiobookshelf swift/Views/LibraryGridView.swift
Scarriffle a17a61ad9a Home screen, library-wide search, splash with orbital particles, macOS download fix
- Add Netflix-style Home with recent audiobooks/podcasts, list/grid toggle shared with Library
- Add searchable libraries — audiobooks filter on title/author; podcasts also index and search episodes across the whole library
- Add unified context menu (download / progress remove) across Home, Library, and PodcastDetail
- Rework cover badges: listened checkmark top-right, download indicator bottom-right; hide progress bar when finished
- Add AirPlay picker in PlayerBar
- Replace splash with four-phase animation: orbital particles → attract → flash → fade; cross-platform iOS+macOS, runtime AppIcon lookup
- Fix macOS parallel downloads: align connection settings with iOS, raise timeoutIntervalForRequest to 300s
- Fix progress deletion using server-side progress UUID (DELETE /api/me/progress/:id)
- Bump CFBundleVersion to 8

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 08:54:43 +02:00

102 lines
2.6 KiB
Swift

import SwiftUI
struct LibraryGridView: View {
let items: [LibraryItem]
var onRefresh: (() async -> Void)? = nil
var dimDownloading: Bool = false
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: gridSpacing) {
// Identify by syncKey, not by `id`: a synthetic episode item
// shares the podcast container's `id`, so the default
// `Identifiable`-based ForEach would collide if both appear in
// a single search result list.
ForEach(items, id: \.syncKey) { item in
LibraryItemCell(item: item, dimDownloading: dimDownloading)
.onTapGesture { onSelect(item) }
}
}
#if os(iOS)
.padding(.horizontal, 8)
.padding(.vertical, 4)
#else
.padding(20)
#endif
}
#if os(iOS)
.refreshable { await onRefresh?() }
#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)
return 8
#else
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
}
}