diff --git a/ABS Client/ABS Client-macOS.entitlements b/ABS Client/ABS Client-macOS.entitlements
new file mode 100644
index 0000000..38da9a9
--- /dev/null
+++ b/ABS Client/ABS Client-macOS.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+ com.apple.security.files.user-selected.read-write
+
+
+
diff --git a/ABS Client/Audiobookshelf swift.xcodeproj/project.pbxproj b/ABS Client/Audiobookshelf swift.xcodeproj/project.pbxproj
index 8c41f64..1a9ed49 100644
--- a/ABS Client/Audiobookshelf swift.xcodeproj/project.pbxproj
+++ b/ABS Client/Audiobookshelf swift.xcodeproj/project.pbxproj
@@ -254,22 +254,23 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
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_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = PP34X97WS3;
- ENABLE_APP_SANDBOX = NO;
+ "ENABLE_APP_SANDBOX[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = 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=iphonesimulator*]" = NO;
"INFOPLIST_FILE[sdk=iphoneos*]" = "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;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -283,6 +284,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
+ MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.local.ABS-Client";
PRODUCT_NAME = "ABS Client";
@@ -296,7 +298,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
@@ -305,22 +307,23 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
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_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = PP34X97WS3;
- ENABLE_APP_SANDBOX = NO;
+ "ENABLE_APP_SANDBOX[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = 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=iphonesimulator*]" = NO;
"INFOPLIST_FILE[sdk=iphoneos*]" = "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;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -334,6 +337,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
+ MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.local.ABS-Client";
PRODUCT_NAME = "ABS Client";
@@ -347,7 +351,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
diff --git a/ABS Client/Audiobookshelf swift/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/ABS Client/Audiobookshelf swift/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
index 4f335a0..7e205d8 100644
Binary files a/ABS Client/Audiobookshelf swift/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png and b/ABS Client/Audiobookshelf swift/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ
diff --git a/ABS Client/Audiobookshelf swift/Audiobookshelf_swiftApp.swift b/ABS Client/Audiobookshelf swift/Audiobookshelf_swiftApp.swift
index 1981542..31d7728 100644
--- a/ABS Client/Audiobookshelf swift/Audiobookshelf_swiftApp.swift
+++ b/ABS Client/Audiobookshelf swift/Audiobookshelf_swiftApp.swift
@@ -35,13 +35,9 @@ struct Audiobookshelf_swiftApp: App {
#if os(iOS)
private func configureAudioSession() {
- do {
- let session = AVAudioSession.sharedInstance()
- try session.setCategory(.playback, mode: .default, options: [])
- try session.setActive(true)
- } catch {
- // non-fatal
- }
+ // Nur die Kategorie registrieren — setActive(true) passiert erst in play(),
+ // damit beim App-Start keine laufende Fremd-Wiedergabe unterbrochen wird.
+ try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
}
#endif
}
diff --git a/ABS Client/Audiobookshelf swift/Services/PlayerEngine.swift b/ABS Client/Audiobookshelf swift/Services/PlayerEngine.swift
index 73e65dd..f636d90 100644
--- a/ABS Client/Audiobookshelf swift/Services/PlayerEngine.swift
+++ b/ABS Client/Audiobookshelf swift/Services/PlayerEngine.swift
@@ -11,6 +11,12 @@ import AppKit
private typealias PlayerArtworkImage = NSImage
#endif
+enum SleepTimerMode: Equatable, Hashable {
+ case off
+ case minutes(Int)
+ case endOfBook
+}
+
@Observable
@MainActor
final class PlayerEngine {
@@ -21,6 +27,11 @@ final class PlayerEngine {
var isReady: Bool = false
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 trackDurations: [Double] = []
private var trackPlayerItems: [AVPlayerItem] = []
@@ -28,6 +39,7 @@ final class PlayerEngine {
private var timeObserver: Any?
private var endObservers: [NSObjectProtocol] = []
private var isSeeking: Bool = false
+ private var sleepTickTask: Task?
var itemId: String?
@@ -127,12 +139,16 @@ final class PlayerEngine {
player?.play()
player?.rate = rate
isPlaying = true
+ if case .minutes = sleepTimer, sleepRemainingSeconds > 0, sleepTickTask == nil {
+ startSleepTickTask()
+ }
updateNowPlayingInfo()
}
func pause() {
player?.pause()
isPlaying = false
+ cancelSleepTickTask()
updateNowPlayingInfo()
}
@@ -143,6 +159,9 @@ final class PlayerEngine {
func setRate(_ newRate: Float) {
rate = newRate
if isPlaying { player?.rate = newRate }
+ if case .endOfBook = sleepTimer {
+ sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
+ }
updateNowPlayingInfo()
}
@@ -221,9 +240,13 @@ final class PlayerEngine {
let wasPlaying = isPlaying
isPlaying = player.timeControlStatus == .playing
if wasPlaying != isPlaying { updateNowPlayingInfo() }
+ updateEndOfBookSleep()
}
func teardown() {
+ cancelSleepTickTask()
+ sleepTimer = .off
+ sleepRemainingSeconds = 0
if let token = timeObserver { player?.removeTimeObserver(token) }
timeObserver = nil
for obs in endObservers { NotificationCenter.default.removeObserver(obs) }
@@ -248,6 +271,57 @@ final class PlayerEngine {
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
private func configureRemoteCommandsIfNeeded() {
diff --git a/ABS Client/Audiobookshelf swift/Views/LibraryGridView.swift b/ABS Client/Audiobookshelf swift/Views/LibraryGridView.swift
index 9b714a0..6a52659 100644
--- a/ABS Client/Audiobookshelf swift/Views/LibraryGridView.swift
+++ b/ABS Client/Audiobookshelf swift/Views/LibraryGridView.swift
@@ -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
}
}
diff --git a/ABS Client/Audiobookshelf swift/Views/LibraryItemCell.swift b/ABS Client/Audiobookshelf swift/Views/LibraryItemCell.swift
index f9881dd..58cdbb6 100644
--- a/ABS Client/Audiobookshelf swift/Views/LibraryItemCell.swift
+++ b/ABS Client/Audiobookshelf swift/Views/LibraryItemCell.swift
@@ -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
diff --git a/ABS Client/Audiobookshelf swift/Views/PlayerBar.swift b/ABS Client/Audiobookshelf swift/Views/PlayerBar.swift
index f0c71d9..5619672 100644
--- a/ABS Client/Audiobookshelf swift/Views/PlayerBar.swift
+++ b/ABS Client/Audiobookshelf swift/Views/PlayerBar.swift
@@ -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
diff --git a/ABS Client/Info-iOS.plist b/ABS Client/Info-iOS.plist
index 0461174..abec678 100644
--- a/ABS Client/Info-iOS.plist
+++ b/ABS Client/Info-iOS.plist
@@ -40,11 +40,18 @@
UIColorName
LaunchBackground
- UISupportedInterfaceOrientations
+ UISupportedInterfaceOrientations~iphone
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
diff --git a/Exports/Mac/ABS Client.app/Contents/Info.plist b/Exports/Mac/ABS Client.app/Contents/Info.plist
new file mode 100644
index 0000000..333bdb6
--- /dev/null
+++ b/Exports/Mac/ABS Client.app/Contents/Info.plist
@@ -0,0 +1,56 @@
+
+
+
+
+ BuildMachineOSBuild
+ 25F71
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ ABS Client
+ CFBundleExecutable
+ ABS Client
+ CFBundleIconFile
+ AppIcon
+ CFBundleIconName
+ AppIcon
+ CFBundleIdentifier
+ com.local.ABS-Client
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ABS Client
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSupportedPlatforms
+
+ MacOSX
+
+ CFBundleVersion
+ 1
+ DTCompiler
+ com.apple.compilers.llvm.clang.1_0
+ DTPlatformBuild
+ 25F70
+ DTPlatformName
+ macosx
+ DTPlatformVersion
+ 26.5
+ DTSDKBuild
+ 25F70
+ DTSDKName
+ macosx26.5
+ DTXcode
+ 2650
+ DTXcodeBuild
+ 17F42
+ LSApplicationCategoryType
+ public.app-category.books
+ LSMinimumSystemVersion
+ 26.0
+ NSAccentColorName
+ AccentColor
+
+
diff --git a/Exports/Mac/ABS Client.app/Contents/MacOS/ABS Client b/Exports/Mac/ABS Client.app/Contents/MacOS/ABS Client
new file mode 100755
index 0000000..e074d25
Binary files /dev/null and b/Exports/Mac/ABS Client.app/Contents/MacOS/ABS Client differ
diff --git a/Exports/Mac/ABS Client.app/Contents/PkgInfo b/Exports/Mac/ABS Client.app/Contents/PkgInfo
new file mode 100644
index 0000000..bd04210
--- /dev/null
+++ b/Exports/Mac/ABS Client.app/Contents/PkgInfo
@@ -0,0 +1 @@
+APPL????
\ No newline at end of file
diff --git a/Exports/Mac/ABS Client.app/Contents/Resources/AppIcon.icns b/Exports/Mac/ABS Client.app/Contents/Resources/AppIcon.icns
new file mode 100644
index 0000000..3e9d861
Binary files /dev/null and b/Exports/Mac/ABS Client.app/Contents/Resources/AppIcon.icns differ
diff --git a/Exports/Mac/ABS Client.app/Contents/Resources/Assets.car b/Exports/Mac/ABS Client.app/Contents/Resources/Assets.car
new file mode 100644
index 0000000..00e8537
Binary files /dev/null and b/Exports/Mac/ABS Client.app/Contents/Resources/Assets.car differ
diff --git a/Exports/Mac/ABS Client.app/Contents/_CodeSignature/CodeResources b/Exports/Mac/ABS Client.app/Contents/_CodeSignature/CodeResources
new file mode 100644
index 0000000..c4003e9
--- /dev/null
+++ b/Exports/Mac/ABS Client.app/Contents/_CodeSignature/CodeResources
@@ -0,0 +1,139 @@
+
+
+
+
+ files
+
+ Resources/AppIcon.icns
+
+ PIslMkIS+dR91jAoGiXhG5ZQxNw=
+
+ Resources/Assets.car
+
+ q6mmhkyHvvRXVcw9rIIm/MbqTqI=
+
+
+ files2
+
+ Resources/AppIcon.icns
+
+ hash2
+
+ UmgGZpb6aN5Xnz7XswrsNpMxnVcQINgSUpIhHWS1GBM=
+
+
+ Resources/Assets.car
+
+ hash2
+
+ lfMPkDEQ0MA2q3+uXr1255juM39IkgqKOiK8Kede+X0=
+
+
+
+ rules
+
+ ^Resources/
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^version.plist$
+
+
+ rules2
+
+ .*\.dSYM($|/)
+
+ weight
+ 11
+
+ ^(.*/)?\.DS_Store$
+
+ omit
+
+ weight
+ 2000
+
+ ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/
+
+ nested
+
+ weight
+ 10
+
+ ^.*
+
+ ^Info\.plist$
+
+ omit
+
+ weight
+ 20
+
+ ^PkgInfo$
+
+ omit
+
+ weight
+ 20
+
+ ^Resources/
+
+ weight
+ 20
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^[^/]+$
+
+ nested
+
+ weight
+ 10
+
+ ^embedded\.provisionprofile$
+
+ weight
+ 20
+
+ ^version\.plist$
+
+ weight
+ 20
+
+
+
+
diff --git a/Exports/Mac/ABS-Client.dmg b/Exports/Mac/ABS-Client.dmg
index a5b77b8..dc1f5a8 100644
Binary files a/Exports/Mac/ABS-Client.dmg and b/Exports/Mac/ABS-Client.dmg differ
diff --git a/Exports/Mac/dmgmaker.sh b/Exports/Mac/dmgmaker.sh
index 9ec663c..97e4aa7 100644
--- a/Exports/Mac/dmgmaker.sh
+++ b/Exports/Mac/dmgmaker.sh
@@ -1,9 +1,2 @@
-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
-≈
+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'
+