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 = {
|
||||
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;
|
||||
};
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,11 +40,18 @@
|
||||
<key>UIColorName</key>
|
||||
<string>LaunchBackground</string>
|
||||
</dict>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<key>UISupportedInterfaceOrientations~iphone</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Reference in New Issue
Block a user