Version 1.1 App Store
This commit is contained in:
12
ABS Client/ABS Client-macOS.entitlements
Normal file
12
ABS Client/ABS Client-macOS.entitlements
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>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -254,22 +254,23 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "ABS Client-macOS.entitlements";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
"ENABLE_APP_SANDBOX[sdk=macosx*]" = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "ABS Client";
|
|
||||||
INFOPLIST_KEY_CFBundleName = "ABS Client";
|
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
|
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
|
||||||
"GENERATE_INFOPLIST_FILE[sdk=iphoneos*]" = NO;
|
"GENERATE_INFOPLIST_FILE[sdk=iphoneos*]" = NO;
|
||||||
"GENERATE_INFOPLIST_FILE[sdk=iphonesimulator*]" = NO;
|
"GENERATE_INFOPLIST_FILE[sdk=iphonesimulator*]" = NO;
|
||||||
"INFOPLIST_FILE[sdk=iphoneos*]" = "Info-iOS.plist";
|
"INFOPLIST_FILE[sdk=iphoneos*]" = "Info-iOS.plist";
|
||||||
"INFOPLIST_FILE[sdk=iphonesimulator*]" = "Info-iOS.plist";
|
"INFOPLIST_FILE[sdk=iphonesimulator*]" = "Info-iOS.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "ABS Client";
|
||||||
|
INFOPLIST_KEY_CFBundleName = "ABS Client";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -283,6 +284,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.local.ABS-Client";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.local.ABS-Client";
|
||||||
PRODUCT_NAME = "ABS Client";
|
PRODUCT_NAME = "ABS Client";
|
||||||
@@ -296,7 +298,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -305,22 +307,23 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "ABS Client-macOS.entitlements";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
"ENABLE_APP_SANDBOX[sdk=macosx*]" = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "ABS Client";
|
|
||||||
INFOPLIST_KEY_CFBundleName = "ABS Client";
|
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
|
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
|
||||||
"GENERATE_INFOPLIST_FILE[sdk=iphoneos*]" = NO;
|
"GENERATE_INFOPLIST_FILE[sdk=iphoneos*]" = NO;
|
||||||
"GENERATE_INFOPLIST_FILE[sdk=iphonesimulator*]" = NO;
|
"GENERATE_INFOPLIST_FILE[sdk=iphonesimulator*]" = NO;
|
||||||
"INFOPLIST_FILE[sdk=iphoneos*]" = "Info-iOS.plist";
|
"INFOPLIST_FILE[sdk=iphoneos*]" = "Info-iOS.plist";
|
||||||
"INFOPLIST_FILE[sdk=iphonesimulator*]" = "Info-iOS.plist";
|
"INFOPLIST_FILE[sdk=iphonesimulator*]" = "Info-iOS.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "ABS Client";
|
||||||
|
INFOPLIST_KEY_CFBundleName = "ABS Client";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -334,6 +337,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.local.ABS-Client";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.local.ABS-Client";
|
||||||
PRODUCT_NAME = "ABS Client";
|
PRODUCT_NAME = "ABS Client";
|
||||||
@@ -347,7 +351,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 895 KiB After Width: | Height: | Size: 632 KiB |
@@ -35,13 +35,9 @@ struct Audiobookshelf_swiftApp: App {
|
|||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
private func configureAudioSession() {
|
private func configureAudioSession() {
|
||||||
do {
|
// Nur die Kategorie registrieren — setActive(true) passiert erst in play(),
|
||||||
let session = AVAudioSession.sharedInstance()
|
// damit beim App-Start keine laufende Fremd-Wiedergabe unterbrochen wird.
|
||||||
try session.setCategory(.playback, mode: .default, options: [])
|
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
|
||||||
try session.setActive(true)
|
|
||||||
} catch {
|
|
||||||
// non-fatal
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ import AppKit
|
|||||||
private typealias PlayerArtworkImage = NSImage
|
private typealias PlayerArtworkImage = NSImage
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
enum SleepTimerMode: Equatable, Hashable {
|
||||||
|
case off
|
||||||
|
case minutes(Int)
|
||||||
|
case endOfBook
|
||||||
|
}
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
@MainActor
|
@MainActor
|
||||||
final class PlayerEngine {
|
final class PlayerEngine {
|
||||||
@@ -21,6 +27,11 @@ final class PlayerEngine {
|
|||||||
var isReady: Bool = false
|
var isReady: Bool = false
|
||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
|
|
||||||
|
var sleepTimer: SleepTimerMode = .off
|
||||||
|
/// Verbleibende Wallclock-Sekunden bis der Sleep-Timer auslöst (0 wenn off).
|
||||||
|
/// Pausiert mit der Wiedergabe; bei `.endOfBook` rate-skaliert aus der Restspielzeit.
|
||||||
|
var sleepRemainingSeconds: Double = 0
|
||||||
|
|
||||||
private var player: AVQueuePlayer?
|
private var player: AVQueuePlayer?
|
||||||
private var trackDurations: [Double] = []
|
private var trackDurations: [Double] = []
|
||||||
private var trackPlayerItems: [AVPlayerItem] = []
|
private var trackPlayerItems: [AVPlayerItem] = []
|
||||||
@@ -28,6 +39,7 @@ final class PlayerEngine {
|
|||||||
private var timeObserver: Any?
|
private var timeObserver: Any?
|
||||||
private var endObservers: [NSObjectProtocol] = []
|
private var endObservers: [NSObjectProtocol] = []
|
||||||
private var isSeeking: Bool = false
|
private var isSeeking: Bool = false
|
||||||
|
private var sleepTickTask: Task<Void, Never>?
|
||||||
|
|
||||||
var itemId: String?
|
var itemId: String?
|
||||||
|
|
||||||
@@ -127,12 +139,16 @@ final class PlayerEngine {
|
|||||||
player?.play()
|
player?.play()
|
||||||
player?.rate = rate
|
player?.rate = rate
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
|
if case .minutes = sleepTimer, sleepRemainingSeconds > 0, sleepTickTask == nil {
|
||||||
|
startSleepTickTask()
|
||||||
|
}
|
||||||
updateNowPlayingInfo()
|
updateNowPlayingInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
func pause() {
|
func pause() {
|
||||||
player?.pause()
|
player?.pause()
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
|
cancelSleepTickTask()
|
||||||
updateNowPlayingInfo()
|
updateNowPlayingInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +159,9 @@ final class PlayerEngine {
|
|||||||
func setRate(_ newRate: Float) {
|
func setRate(_ newRate: Float) {
|
||||||
rate = newRate
|
rate = newRate
|
||||||
if isPlaying { player?.rate = newRate }
|
if isPlaying { player?.rate = newRate }
|
||||||
|
if case .endOfBook = sleepTimer {
|
||||||
|
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
|
||||||
|
}
|
||||||
updateNowPlayingInfo()
|
updateNowPlayingInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,9 +240,13 @@ final class PlayerEngine {
|
|||||||
let wasPlaying = isPlaying
|
let wasPlaying = isPlaying
|
||||||
isPlaying = player.timeControlStatus == .playing
|
isPlaying = player.timeControlStatus == .playing
|
||||||
if wasPlaying != isPlaying { updateNowPlayingInfo() }
|
if wasPlaying != isPlaying { updateNowPlayingInfo() }
|
||||||
|
updateEndOfBookSleep()
|
||||||
}
|
}
|
||||||
|
|
||||||
func teardown() {
|
func teardown() {
|
||||||
|
cancelSleepTickTask()
|
||||||
|
sleepTimer = .off
|
||||||
|
sleepRemainingSeconds = 0
|
||||||
if let token = timeObserver { player?.removeTimeObserver(token) }
|
if let token = timeObserver { player?.removeTimeObserver(token) }
|
||||||
timeObserver = nil
|
timeObserver = nil
|
||||||
for obs in endObservers { NotificationCenter.default.removeObserver(obs) }
|
for obs in endObservers { NotificationCenter.default.removeObserver(obs) }
|
||||||
@@ -248,6 +271,57 @@ final class PlayerEngine {
|
|||||||
clearNowPlayingInfo()
|
clearNowPlayingInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Sleep timer
|
||||||
|
|
||||||
|
func setSleepTimer(_ mode: SleepTimerMode) {
|
||||||
|
cancelSleepTickTask()
|
||||||
|
sleepTimer = mode
|
||||||
|
switch mode {
|
||||||
|
case .off:
|
||||||
|
sleepRemainingSeconds = 0
|
||||||
|
case .minutes(let m):
|
||||||
|
sleepRemainingSeconds = Double(m * 60)
|
||||||
|
if isPlaying { startSleepTickTask() }
|
||||||
|
case .endOfBook:
|
||||||
|
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startSleepTickTask() {
|
||||||
|
cancelSleepTickTask()
|
||||||
|
sleepTickTask = Task { @MainActor [weak self] in
|
||||||
|
while !Task.isCancelled {
|
||||||
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||||
|
guard !Task.isCancelled, let self else { return }
|
||||||
|
self.sleepRemainingSeconds = max(0, self.sleepRemainingSeconds - 0.5)
|
||||||
|
if self.sleepRemainingSeconds <= 0 {
|
||||||
|
self.sleepTimer = .off
|
||||||
|
self.pause()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelSleepTickTask() {
|
||||||
|
sleepTickTask?.cancel()
|
||||||
|
sleepTickTask = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateEndOfBookSleep() {
|
||||||
|
guard case .endOfBook = sleepTimer else { return }
|
||||||
|
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
|
||||||
|
if sleepRemainingSeconds <= 0 {
|
||||||
|
sleepTimer = .off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func wallclockRemainingUntilEndOfBook() -> Double {
|
||||||
|
let playback = max(0, totalDuration - absoluteCurrentTime)
|
||||||
|
let r = max(0.1, Double(rate))
|
||||||
|
return playback / r
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Now-playing / remote commands
|
// MARK: - Now-playing / remote commands
|
||||||
|
|
||||||
private func configureRemoteCommandsIfNeeded() {
|
private func configureRemoteCommandsIfNeeded() {
|
||||||
|
|||||||
@@ -5,9 +5,22 @@ struct LibraryGridView: View {
|
|||||||
var onRefresh: (() async -> Void)? = nil
|
var onRefresh: (() async -> Void)? = nil
|
||||||
var onSelect: (LibraryItem) -> Void
|
var onSelect: (LibraryItem) -> Void
|
||||||
|
|
||||||
|
@AppStorage("libraryCoverSize") private var coverSize: Double = Self.defaultCoverSize
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
zoomBar
|
||||||
|
gridContent
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
gridContent
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var gridContent: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVGrid(columns: gridColumns, spacing: 8) {
|
LazyVGrid(columns: gridColumns, spacing: gridSpacing) {
|
||||||
ForEach(items) { item in
|
ForEach(items) { item in
|
||||||
LibraryItemCell(item: item)
|
LibraryItemCell(item: item)
|
||||||
.onTapGesture { onSelect(item) }
|
.onTapGesture { onSelect(item) }
|
||||||
@@ -25,14 +38,59 @@ struct LibraryGridView: View {
|
|||||||
#endif
|
#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] {
|
private var gridColumns: [GridItem] {
|
||||||
|
[GridItem(.adaptive(minimum: CGFloat(coverSize)), spacing: gridSpacing)]
|
||||||
|
}
|
||||||
|
|
||||||
|
private var gridSpacing: CGFloat {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
// 3 equal columns — compact spacing for full height utilization
|
return 8
|
||||||
[GridItem(.flexible(), spacing: 8),
|
|
||||||
GridItem(.flexible(), spacing: 8),
|
|
||||||
GridItem(.flexible())]
|
|
||||||
#else
|
#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
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,17 +45,44 @@ struct LibraryItemCell: View {
|
|||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
private var iOSCover: some View {
|
private var iOSCover: some View {
|
||||||
Color(.systemGray6) // neutral bg for PNG transparent areas
|
coverContainer(background: AnyShapeStyle(Color(.systemGray6)))
|
||||||
.frame(maxWidth: .infinity) // explicitly fill the column width
|
}
|
||||||
|
#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)
|
.aspectRatio(1, contentMode: .fit)
|
||||||
.overlay {
|
.overlay {
|
||||||
if let url = app.client.coverURL(itemId: item.id) {
|
if let url = app.client.coverURL(itemId: item.id) {
|
||||||
AsyncImage(url: url) { image in
|
AsyncImage(url: url) { phase in
|
||||||
image.resizable().scaledToFill()
|
switch phase {
|
||||||
} placeholder: {
|
case .success(let img):
|
||||||
ProgressView().tint(.accentColor)
|
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 {
|
} else {
|
||||||
Image(systemName: "book.closed")
|
Image(systemName: "book.closed")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -63,34 +90,6 @@ struct LibraryItemCell: View {
|
|||||||
}
|
}
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.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
|
// MARK: - Download badge
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ struct PlayerBar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrubber
|
scrubber
|
||||||
HStack(spacing: 24) {
|
HStack(spacing: 20) {
|
||||||
Button { app.skip(by: -Double(skipSeconds)) } label: {
|
Button { app.skip(by: -Double(skipSeconds)) } label: {
|
||||||
Image(systemName: skipBackImage).font(.system(size: 22))
|
Image(systemName: skipBackImage).font(.system(size: 22))
|
||||||
}
|
}
|
||||||
@@ -77,6 +77,8 @@ struct PlayerBar: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
sleepMenu
|
||||||
|
|
||||||
rateMenu
|
rateMenu
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -113,6 +115,8 @@ struct PlayerBar: View {
|
|||||||
|
|
||||||
rateMenu
|
rateMenu
|
||||||
|
|
||||||
|
sleepMenu
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
statusIndicator
|
statusIndicator
|
||||||
@@ -218,7 +222,71 @@ struct PlayerBar: View {
|
|||||||
.font(.caption2.monospacedDigit())
|
.font(.caption2.monospacedDigit())
|
||||||
.foregroundStyle(.secondary)
|
.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 {
|
private var rateMenu: some View {
|
||||||
|
|||||||
@@ -40,11 +40,18 @@
|
|||||||
<key>UIColorName</key>
|
<key>UIColorName</key>
|
||||||
<string>LaunchBackground</string>
|
<string>LaunchBackground</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations~iphone</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
56
Exports/Mac/ABS Client.app/Contents/Info.plist
Normal file
56
Exports/Mac/ABS Client.app/Contents/Info.plist
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?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>BuildMachineOSBuild</key>
|
||||||
|
<string>25F71</string>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>ABS Client</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>ABS Client</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
|
<key>CFBundleIconName</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.local.ABS-Client</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>ABS Client</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
|
<array>
|
||||||
|
<string>MacOSX</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>DTCompiler</key>
|
||||||
|
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||||
|
<key>DTPlatformBuild</key>
|
||||||
|
<string>25F70</string>
|
||||||
|
<key>DTPlatformName</key>
|
||||||
|
<string>macosx</string>
|
||||||
|
<key>DTPlatformVersion</key>
|
||||||
|
<string>26.5</string>
|
||||||
|
<key>DTSDKBuild</key>
|
||||||
|
<string>25F70</string>
|
||||||
|
<key>DTSDKName</key>
|
||||||
|
<string>macosx26.5</string>
|
||||||
|
<key>DTXcode</key>
|
||||||
|
<string>2650</string>
|
||||||
|
<key>DTXcodeBuild</key>
|
||||||
|
<string>17F42</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.books</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>26.0</string>
|
||||||
|
<key>NSAccentColorName</key>
|
||||||
|
<string>AccentColor</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Exports/Mac/ABS Client.app/Contents/MacOS/ABS Client
Executable file
BIN
Exports/Mac/ABS Client.app/Contents/MacOS/ABS Client
Executable file
Binary file not shown.
1
Exports/Mac/ABS Client.app/Contents/PkgInfo
Normal file
1
Exports/Mac/ABS Client.app/Contents/PkgInfo
Normal file
@@ -0,0 +1 @@
|
|||||||
|
APPL????
|
||||||
BIN
Exports/Mac/ABS Client.app/Contents/Resources/AppIcon.icns
Normal file
BIN
Exports/Mac/ABS Client.app/Contents/Resources/AppIcon.icns
Normal file
Binary file not shown.
BIN
Exports/Mac/ABS Client.app/Contents/Resources/Assets.car
Normal file
BIN
Exports/Mac/ABS Client.app/Contents/Resources/Assets.car
Normal file
Binary file not shown.
139
Exports/Mac/ABS Client.app/Contents/_CodeSignature/CodeResources
Normal file
139
Exports/Mac/ABS Client.app/Contents/_CodeSignature/CodeResources
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<?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>files</key>
|
||||||
|
<dict>
|
||||||
|
<key>Resources/AppIcon.icns</key>
|
||||||
|
<data>
|
||||||
|
PIslMkIS+dR91jAoGiXhG5ZQxNw=
|
||||||
|
</data>
|
||||||
|
<key>Resources/Assets.car</key>
|
||||||
|
<data>
|
||||||
|
q6mmhkyHvvRXVcw9rIIm/MbqTqI=
|
||||||
|
</data>
|
||||||
|
</dict>
|
||||||
|
<key>files2</key>
|
||||||
|
<dict>
|
||||||
|
<key>Resources/AppIcon.icns</key>
|
||||||
|
<dict>
|
||||||
|
<key>hash2</key>
|
||||||
|
<data>
|
||||||
|
UmgGZpb6aN5Xnz7XswrsNpMxnVcQINgSUpIhHWS1GBM=
|
||||||
|
</data>
|
||||||
|
</dict>
|
||||||
|
<key>Resources/Assets.car</key>
|
||||||
|
<dict>
|
||||||
|
<key>hash2</key>
|
||||||
|
<data>
|
||||||
|
lfMPkDEQ0MA2q3+uXr1255juM39IkgqKOiK8Kede+X0=
|
||||||
|
</data>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>rules</key>
|
||||||
|
<dict>
|
||||||
|
<key>^Resources/</key>
|
||||||
|
<true/>
|
||||||
|
<key>^Resources/.*\.lproj/</key>
|
||||||
|
<dict>
|
||||||
|
<key>optional</key>
|
||||||
|
<true/>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>1000</real>
|
||||||
|
</dict>
|
||||||
|
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||||
|
<dict>
|
||||||
|
<key>omit</key>
|
||||||
|
<true/>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>1100</real>
|
||||||
|
</dict>
|
||||||
|
<key>^Resources/Base\.lproj/</key>
|
||||||
|
<dict>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>1010</real>
|
||||||
|
</dict>
|
||||||
|
<key>^version.plist$</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>rules2</key>
|
||||||
|
<dict>
|
||||||
|
<key>.*\.dSYM($|/)</key>
|
||||||
|
<dict>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>11</real>
|
||||||
|
</dict>
|
||||||
|
<key>^(.*/)?\.DS_Store$</key>
|
||||||
|
<dict>
|
||||||
|
<key>omit</key>
|
||||||
|
<true/>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>2000</real>
|
||||||
|
</dict>
|
||||||
|
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
||||||
|
<dict>
|
||||||
|
<key>nested</key>
|
||||||
|
<true/>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>10</real>
|
||||||
|
</dict>
|
||||||
|
<key>^.*</key>
|
||||||
|
<true/>
|
||||||
|
<key>^Info\.plist$</key>
|
||||||
|
<dict>
|
||||||
|
<key>omit</key>
|
||||||
|
<true/>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>20</real>
|
||||||
|
</dict>
|
||||||
|
<key>^PkgInfo$</key>
|
||||||
|
<dict>
|
||||||
|
<key>omit</key>
|
||||||
|
<true/>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>20</real>
|
||||||
|
</dict>
|
||||||
|
<key>^Resources/</key>
|
||||||
|
<dict>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>20</real>
|
||||||
|
</dict>
|
||||||
|
<key>^Resources/.*\.lproj/</key>
|
||||||
|
<dict>
|
||||||
|
<key>optional</key>
|
||||||
|
<true/>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>1000</real>
|
||||||
|
</dict>
|
||||||
|
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||||
|
<dict>
|
||||||
|
<key>omit</key>
|
||||||
|
<true/>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>1100</real>
|
||||||
|
</dict>
|
||||||
|
<key>^Resources/Base\.lproj/</key>
|
||||||
|
<dict>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>1010</real>
|
||||||
|
</dict>
|
||||||
|
<key>^[^/]+$</key>
|
||||||
|
<dict>
|
||||||
|
<key>nested</key>
|
||||||
|
<true/>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>10</real>
|
||||||
|
</dict>
|
||||||
|
<key>^embedded\.provisionprofile$</key>
|
||||||
|
<dict>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>20</real>
|
||||||
|
</dict>
|
||||||
|
<key>^version\.plist$</key>
|
||||||
|
<dict>
|
||||||
|
<key>weight</key>
|
||||||
|
<real>20</real>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Binary file not shown.
@@ -1,9 +1,2 @@
|
|||||||
create-dmg \
|
create-dmg --volname "ABS Client" --window-size 600 400 --icon-size 100 --icon "ABS Client.app" 150 200 --app-drop-link 450 200 ~/ABS-Client/Exports/Mac/ABS-Client.dmg 'ABS Client.app'
|
||||||
--volname "ABS-Client" \
|
|
||||||
--window-size 600 400 \
|
|
||||||
--icon-size 100 \
|
|
||||||
--icon "ABS-Client.app" 150 200 \
|
|
||||||
--app-drop-link 450 200 \
|
|
||||||
~/ABS-Client/Exports/Mac/ABS-Client.dmg \
|
|
||||||
ABS Client.app
|
|
||||||
≈
|
|
||||||
|
|||||||
Reference in New Issue
Block a user