mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-04 19:14:51 +02:00
Add:iOS audio player
This commit is contained in:
parent
4496b1170c
commit
4bf75c606a
24 changed files with 467 additions and 129 deletions
|
@ -13,6 +13,7 @@ dependencies {
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-dialog')
|
implementation project(':capacitor-dialog')
|
||||||
implementation project(':capacitor-network')
|
implementation project(':capacitor-network')
|
||||||
|
implementation project(':capacitor-status-bar')
|
||||||
implementation project(':capacitor-storage')
|
implementation project(':capacitor-storage')
|
||||||
implementation project(':robingenz-capacitor-app-update')
|
implementation project(':robingenz-capacitor-app-update')
|
||||||
implementation project(':capacitor-data-storage-sqlite')
|
implementation project(':capacitor-data-storage-sqlite')
|
||||||
|
|
|
@ -15,6 +15,10 @@
|
||||||
"pkg": "@capacitor/network",
|
"pkg": "@capacitor/network",
|
||||||
"classpath": "com.capacitorjs.plugins.network.NetworkPlugin"
|
"classpath": "com.capacitorjs.plugins.network.NetworkPlugin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pkg": "@capacitor/status-bar",
|
||||||
|
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pkg": "@capacitor/storage",
|
"pkg": "@capacitor/storage",
|
||||||
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
|
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
|
||||||
|
|
|
@ -14,6 +14,9 @@ project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/d
|
||||||
include ':capacitor-network'
|
include ':capacitor-network'
|
||||||
project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
|
project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
|
||||||
|
|
||||||
|
include ':capacitor-status-bar'
|
||||||
|
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||||
|
|
||||||
include ':capacitor-storage'
|
include ':capacitor-storage'
|
||||||
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
|
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,30 @@
|
||||||
@import "./fonts.css";
|
@import "./fonts.css";
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-wrapper {
|
||||||
|
height: calc(100vh - env(safe-area-inset-top));
|
||||||
|
min-height: calc(100vh - env(safe-area-inset-top));
|
||||||
|
max-height: calc(100vh - env(safe-area-inset-top));
|
||||||
|
margin-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
height: calc(100% - 64px);
|
||||||
|
min-height: calc(100% - 64px);
|
||||||
|
max-height: calc(100% - 64px);
|
||||||
|
}
|
||||||
|
#content.playerOpen {
|
||||||
|
height: calc(100% - 164px);
|
||||||
|
min-height: calc(100% - 164px);
|
||||||
|
max-height: calc(100% - 164px);
|
||||||
|
}
|
||||||
|
|
||||||
#bookshelf {
|
#bookshelf {
|
||||||
min-height: calc(100vh - 48px);
|
height: calc(100% - 48px);
|
||||||
|
min-height: calc(100% - 48px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-shadow-sm {
|
.box-shadow-sm {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="fixed top-0 bottom-0 left-0 right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
|
<div class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
|
||||||
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
|
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
|
||||||
<div class="top-2 left-4 absolute cursor-pointer">
|
<div class="top-2 left-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
||||||
|
@ -524,6 +524,7 @@ export default {
|
||||||
this.streamId = stream ? stream.id : null
|
this.streamId = stream ? stream.id : null
|
||||||
this.audiobookId = audiobookStreamData.audiobookId
|
this.audiobookId = audiobookStreamData.audiobookId
|
||||||
this.initObject = { ...audiobookStreamData }
|
this.initObject = { ...audiobookStreamData }
|
||||||
|
console.log('[AudioPlayer] Set Audio Player', !!stream)
|
||||||
|
|
||||||
var init = true
|
var init = true
|
||||||
if (!!stream) {
|
if (!!stream) {
|
||||||
|
@ -612,6 +613,7 @@ export default {
|
||||||
var data = await MyNativeAudio.getCurrentTime()
|
var data = await MyNativeAudio.getCurrentTime()
|
||||||
this.currentTime = Number((data.value / 1000).toFixed(2))
|
this.currentTime = Number((data.value / 1000).toFixed(2))
|
||||||
this.bufferedTime = Number((data.bufferedTime / 1000).toFixed(2))
|
this.bufferedTime = Number((data.bufferedTime / 1000).toFixed(2))
|
||||||
|
console.log('[AudioPlayer] Got Current Time', this.currentTime)
|
||||||
this.timeupdate()
|
this.timeupdate()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
|
|
|
@ -378,7 +378,7 @@ export default {
|
||||||
console.log('[StreamContainer] Stream Open: ' + this.title)
|
console.log('[StreamContainer] Stream Open: ' + this.title)
|
||||||
|
|
||||||
if (!this.$refs.audioPlayer) {
|
if (!this.$refs.audioPlayer) {
|
||||||
console.error('No Audio Player Mini')
|
console.error('[StreamContainer] No Audio Player Mini')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,7 +406,12 @@ export default {
|
||||||
audiobookId: this.audiobookId,
|
audiobookId: this.audiobookId,
|
||||||
tracks: this.tracksForCast
|
tracks: this.tracksForCast
|
||||||
}
|
}
|
||||||
|
console.log('[StreamContainer] Set Audio Player', JSON.stringify(audiobookStreamData))
|
||||||
|
if (!this.$refs.audioPlayer) {
|
||||||
|
console.error('[StreamContainer] Invalid no audio player')
|
||||||
|
} else {
|
||||||
|
console.log('[StreamContainer] Has Audio Player Ref')
|
||||||
|
}
|
||||||
this.$refs.audioPlayer.set(audiobookStreamData, stream, !this.stream)
|
this.$refs.audioPlayer.set(audiobookStreamData, stream, !this.stream)
|
||||||
|
|
||||||
this.stream = stream
|
this.stream = stream
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="fixed top-0 left-0 right-0 bottom-0 w-full h-full z-50 overflow-hidden pointer-events-none">
|
<div class="fixed top-0 left-0 right-0 layout-wrapper w-full z-50 overflow-hidden pointer-events-none">
|
||||||
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
|
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
|
||||||
<div class="absolute top-0 right-0 w-64 h-full bg-primary transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
|
<div class="absolute top-0 right-0 w-64 h-full bg-primary transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
|
||||||
<div class="px-6 mb-4">
|
<div class="px-6 mb-4">
|
||||||
<p v-if="socketConnected" class="text-base">
|
<p v-if="socketConnected" class="text-base">
|
||||||
Welcome, <strong>{{ username }}</strong>
|
Welcome,
|
||||||
|
<strong>{{ username }}</strong>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-y-auto">
|
<div class="w-full overflow-y-auto">
|
||||||
|
|
|
@ -90,8 +90,9 @@ export default {
|
||||||
return this.isCoverSquareAspectRatio ? 1 : 1.6
|
return this.isCoverSquareAspectRatio ? 1 : 1.6
|
||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
// var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
|
||||||
var coverSize = 100
|
var coverSize = 100
|
||||||
|
if (window.innerWidth <= 375) coverSize = 90
|
||||||
|
|
||||||
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
||||||
return coverSize
|
return coverSize
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-20 right-0 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-full" style="top: 72px">
|
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-20 right-0 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-full" style="top: 72px">
|
||||||
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
||||||
<p class="text-xs">
|
<p class="text-xs">
|
||||||
<strong>{{ key }}</strong
|
<strong>{{ key }}</strong>
|
||||||
>: {{ comicMetadata[key] }}
|
: {{ comicMetadata[key] }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -210,10 +210,10 @@ export default {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#comic-reader {
|
#comic-reader {
|
||||||
height: calc(100vh - 32px);
|
height: calc(100% - 32px);
|
||||||
}
|
}
|
||||||
.pagemenu {
|
.pagemenu {
|
||||||
max-height: calc(100vh - 80px);
|
max-height: calc(100% - 80px);
|
||||||
}
|
}
|
||||||
.comicimg {
|
.comicimg {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -357,6 +357,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
@ -378,6 +379,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
|
<string>fetch</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
|
|
|
@ -6,8 +6,11 @@ CAP_PLUGIN(MyNativeAudio, "MyNativeAudio",
|
||||||
CAP_PLUGIN_METHOD(initPlayer, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(initPlayer, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(seekForward10, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(seekBackward10, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(seekPlayer, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(seekPlayer, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(terminateStream, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(terminateStream, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(getStreamSyncData, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,8 +1,26 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Capacitor
|
import Capacitor
|
||||||
|
import MediaPlayer
|
||||||
import AVKit
|
import AVKit
|
||||||
|
|
||||||
|
|
||||||
|
extension UIImageView {
|
||||||
|
public func imageFromUrl(urlString: String) {
|
||||||
|
if let url = NSURL(string: urlString) {
|
||||||
|
let request = NSURLRequest(url: url as URL)
|
||||||
|
NSURLConnection.sendAsynchronousRequest(request as URLRequest, queue: OperationQueue.main) {
|
||||||
|
(response: URLResponse?, data: Data?, error: Error?) -> Void in
|
||||||
|
if let imageData = data as Data? {
|
||||||
|
self.image = UIImage(data: imageData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Audiobook {
|
struct Audiobook {
|
||||||
|
var streamId = ""
|
||||||
|
var audiobookId = ""
|
||||||
var title = "No Title"
|
var title = "No Title"
|
||||||
var author = "Unknown"
|
var author = "Unknown"
|
||||||
var playWhenReady = false
|
var playWhenReady = false
|
||||||
|
@ -16,18 +34,43 @@ struct Audiobook {
|
||||||
|
|
||||||
@objc(MyNativeAudio)
|
@objc(MyNativeAudio)
|
||||||
public class MyNativeAudio: CAPPlugin {
|
public class MyNativeAudio: CAPPlugin {
|
||||||
|
|
||||||
var avPlayer: AVPlayer!
|
var avPlayer: AVPlayer!
|
||||||
var currentCall: CAPPluginCall?
|
var currentCall: CAPPluginCall?
|
||||||
var audioPlayer: AVPlayer!
|
var audioPlayer: AVPlayer!
|
||||||
var audiobook: Audiobook?
|
var audiobook: Audiobook?
|
||||||
|
|
||||||
|
enum PlayerState {
|
||||||
|
case stopped
|
||||||
|
case playing
|
||||||
|
case paused
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playerState: PlayerState = .stopped
|
||||||
|
|
||||||
|
// Key-value observing context
|
||||||
|
private var playerItemContext = 0
|
||||||
|
|
||||||
override public func load() {
|
override public func load() {
|
||||||
|
NSLog("Load MyNativeAudio")
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(stop),
|
NotificationCenter.default.addObserver(self, selector: #selector(stop),
|
||||||
name:Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
|
name:Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self,
|
||||||
|
selector: #selector(appDidEnterBackground),
|
||||||
|
name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self,
|
||||||
|
selector: #selector(appWillEnterForeground),
|
||||||
|
name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||||
|
|
||||||
|
setupRemoteTransportControls()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func initPlayer(_ call: CAPPluginCall) {
|
@objc func initPlayer(_ call: CAPPluginCall) {
|
||||||
|
NSLog("Init Player")
|
||||||
audiobook = Audiobook(
|
audiobook = Audiobook(
|
||||||
|
streamId: call.getString("id") ?? "",
|
||||||
|
audiobookId: call.getString("audiobookId") ?? "",
|
||||||
title: call.getString("title") ?? "No Title",
|
title: call.getString("title") ?? "No Title",
|
||||||
author: call.getString("author") ?? "Unknown",
|
author: call.getString("author") ?? "Unknown",
|
||||||
playWhenReady: call.getBool("playWhenReady", false),
|
playWhenReady: call.getBool("playWhenReady", false),
|
||||||
|
@ -50,51 +93,85 @@ public class MyNativeAudio: CAPPlugin {
|
||||||
url: url!,
|
url: url!,
|
||||||
options: ["AVURLAssetHTTPHeaderFieldsKey": headers]
|
options: ["AVURLAssetHTTPHeaderFieldsKey": headers]
|
||||||
)
|
)
|
||||||
let playerItem = AVPlayerItem(asset: asset)
|
|
||||||
self.audioPlayer = AVPlayer(playerItem: playerItem)
|
|
||||||
// self.audioPlayer = AVPlayer(url: url)
|
|
||||||
|
|
||||||
self.audioPlayer.play()
|
print("Playing audiobook url \(String(describing: url))")
|
||||||
|
|
||||||
|
// For play in background
|
||||||
|
do {
|
||||||
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowAirPlay])
|
||||||
|
NSLog("[TEST] Playback OK")
|
||||||
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
NSLog("[TEST] Session is Active")
|
||||||
|
} catch {
|
||||||
|
NSLog("[TEST] Failed to set BG Data")
|
||||||
|
print(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func seekForward10() {
|
let playerItem = AVPlayerItem(asset: asset)
|
||||||
|
|
||||||
|
// Register as an observer of the player item's status property
|
||||||
|
playerItem.addObserver(self,
|
||||||
|
forKeyPath: #keyPath(AVPlayerItem.status),
|
||||||
|
options: [.old, .new],
|
||||||
|
context: &playerItemContext)
|
||||||
|
|
||||||
|
self.audioPlayer = AVPlayer(playerItem: playerItem)
|
||||||
|
let time = self.audioPlayer.currentItem?.currentTime()
|
||||||
|
|
||||||
|
print("Audio Player Initialized \(String(describing: time))")
|
||||||
|
|
||||||
|
call.resolve(["success": true])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func seekForward(_ call: CAPPluginCall) {
|
||||||
|
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||||
|
|
||||||
let duration = self.audioPlayer.currentItem?.duration.seconds ?? 0
|
let duration = self.audioPlayer.currentItem?.duration.seconds ?? 0
|
||||||
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||||
var destinationTime = currentTime + 10
|
var destinationTime = currentTime + amount
|
||||||
if (destinationTime > duration) { destinationTime = duration }
|
if (destinationTime > duration) { destinationTime = duration }
|
||||||
|
|
||||||
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
|
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
|
||||||
self.audioPlayer.seek(to: time)
|
self.audioPlayer.seek(to: time)
|
||||||
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func seekBackward10() {
|
@objc func seekBackward(_ call: CAPPluginCall) {
|
||||||
|
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||||
|
|
||||||
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||||
var destinationTime = currentTime - 10
|
var destinationTime = currentTime - amount
|
||||||
if (destinationTime < 0) { destinationTime = 0 }
|
if (destinationTime < 0) { destinationTime = 0 }
|
||||||
|
|
||||||
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
|
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
|
||||||
self.audioPlayer.seek(to: time)
|
self.audioPlayer.seek(to: time)
|
||||||
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func seekPlayer(_ call: CAPPluginCall) {
|
@objc func seekPlayer(_ call: CAPPluginCall) {
|
||||||
var seekTime = call.getInt("timeMs") ?? 0
|
var seekTime = (Int(call.getString("timeMs", "0")) ?? 0) / 1000
|
||||||
seekTime /= 1000
|
NSLog("Seek Player \(seekTime)")
|
||||||
|
|
||||||
if (seekTime < 0) { seekTime = 0 }
|
if (seekTime < 0) { seekTime = 0 }
|
||||||
|
|
||||||
let time = CMTime(seconds:Double(seekTime),preferredTimescale: 1000)
|
let time = CMTime(seconds:Double(seekTime),preferredTimescale: 1000)
|
||||||
self.audioPlayer.seek(to: time)
|
self.audioPlayer.seek(to: time)
|
||||||
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func pausePlayer() {
|
@objc func pausePlayer(_ call: CAPPluginCall) {
|
||||||
self.audioPlayer.pause()
|
pause()
|
||||||
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func playPlayer() {
|
@objc func playPlayer(_ call: CAPPluginCall) {
|
||||||
self.audioPlayer.play()
|
play()
|
||||||
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func terminateStream() {
|
@objc func terminateStream(_ call: CAPPluginCall) {
|
||||||
self.audioPlayer.pause()
|
pause()
|
||||||
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func stop() {
|
@objc func stop() {
|
||||||
|
@ -103,4 +180,190 @@ public class MyNativeAudio: CAPPlugin {
|
||||||
call.resolve([ "result": true])
|
call.resolve([ "result": true])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
||||||
|
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||||
|
let buffTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||||
|
NSLog("AVPlayer getCurrentTime \(currTime)")
|
||||||
|
call.resolve([ "value": currTime * 1000, "bufferedTime": buffTime * 1000 ])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getStreamSyncData(_ call: CAPPluginCall) {
|
||||||
|
let streamId = audiobook?.streamId ?? ""
|
||||||
|
call.resolve([ "isPlaying": false, "lastPauseTime": 0, "id": streamId ])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
||||||
|
let speed = call.getFloat("speed") ?? 0
|
||||||
|
NSLog("[TEST] Set Playback Speed \(speed)")
|
||||||
|
audioPlayer.rate = speed
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
func play() {
|
||||||
|
audioPlayer.play()
|
||||||
|
self.notifyListeners("onPlayingUpdate", data: [
|
||||||
|
"value": true
|
||||||
|
])
|
||||||
|
|
||||||
|
playerState = .playing
|
||||||
|
setupNowPlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pause() {
|
||||||
|
audioPlayer.pause()
|
||||||
|
self.notifyListeners("onPlayingUpdate", data: [
|
||||||
|
"value": false
|
||||||
|
])
|
||||||
|
|
||||||
|
playerState = .paused
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentTime() -> Double {
|
||||||
|
return self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func duration() -> Double {
|
||||||
|
return self.audioPlayer.currentItem?.duration.seconds ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func playbackRate() -> Float {
|
||||||
|
return self.audioPlayer.rate
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMetadata() {
|
||||||
|
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||||
|
let duration = self.audioPlayer.currentItem?.duration.seconds ?? 0
|
||||||
|
self.notifyListeners("onMetadata", data: [
|
||||||
|
"duration": duration * 1000,
|
||||||
|
"currentTime": currTime * 1000,
|
||||||
|
"stateName": "unknown"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override func observeValue(forKeyPath keyPath: String?,
|
||||||
|
of object: Any?,
|
||||||
|
change: [NSKeyValueChangeKey : Any]?,
|
||||||
|
context: UnsafeMutableRawPointer?) {
|
||||||
|
|
||||||
|
// Only handle observations for the playerItemContext
|
||||||
|
guard context == &playerItemContext else {
|
||||||
|
super.observeValue(forKeyPath: keyPath,
|
||||||
|
of: object,
|
||||||
|
change: change,
|
||||||
|
context: context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyPath == #keyPath(AVPlayerItem.status) {
|
||||||
|
let status: AVPlayerItem.Status
|
||||||
|
if let statusNumber = change?[.newKey] as? NSNumber {
|
||||||
|
status = AVPlayerItem.Status(rawValue: statusNumber.intValue)!
|
||||||
|
print("AVPlayer Status Change \(String(status.rawValue))")
|
||||||
|
} else {
|
||||||
|
status = .unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch over status value
|
||||||
|
switch status {
|
||||||
|
case .readyToPlay:
|
||||||
|
// Player item is ready to play.
|
||||||
|
NSLog("AVPlayer ready to play")
|
||||||
|
setNowPlayingMetadata()
|
||||||
|
sendMetadata()
|
||||||
|
if (audiobook?.playWhenReady == true) {
|
||||||
|
NSLog("AVPlayer playWhenReady == true")
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .failed:
|
||||||
|
// Player item failed. See error.
|
||||||
|
break
|
||||||
|
case .unknown:
|
||||||
|
// Player item is not yet ready
|
||||||
|
break
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func appDidEnterBackground() {
|
||||||
|
setupNowPlaying()
|
||||||
|
NSLog("[TEST] App Enter Backround")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func appWillEnterForeground() {
|
||||||
|
|
||||||
|
NSLog("[TEST] App Will Enter Foreground")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRemoteTransportControls() {
|
||||||
|
// Get the shared MPRemoteCommandCenter
|
||||||
|
let commandCenter = MPRemoteCommandCenter.shared()
|
||||||
|
|
||||||
|
// Add handler for Play Command
|
||||||
|
commandCenter.playCommand.addTarget { [unowned self] event in
|
||||||
|
NSLog("[TEST] Play Command \(playbackRate())")
|
||||||
|
if playbackRate() == 0.0 {
|
||||||
|
play()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
return .commandFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add handler for Pause Command
|
||||||
|
commandCenter.pauseCommand.addTarget { [unowned self] event in
|
||||||
|
NSLog("[TEST] Pause Command \(playbackRate())")
|
||||||
|
if playbackRate() == 1.0 {
|
||||||
|
pause()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
return .commandFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNowPlayingMetadata() {
|
||||||
|
|
||||||
|
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
|
||||||
|
var nowPlayingInfo = [String: Any]()
|
||||||
|
|
||||||
|
NSLog("%@", "**** Set track metadata: title \(audiobook?.title ?? "")")
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = audiobook?.playlistUrl ?? ""
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = "hls"
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyTitle] = audiobook?.title ?? ""
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyArtist] = audiobook?.author ?? ""
|
||||||
|
|
||||||
|
if (audiobook?.cover != nil) {
|
||||||
|
let myImageView = UIImageView()
|
||||||
|
myImageView.imageFromUrl(urlString: audiobook?.cover ?? "")
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = myImageView.image
|
||||||
|
}
|
||||||
|
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = audiobook?.author ?? ""
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = audiobook?.title ?? ""
|
||||||
|
|
||||||
|
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupNowPlaying() {
|
||||||
|
|
||||||
|
if (playerState != .playing) {
|
||||||
|
NSLog("[TEST] Not current playing so not updating now playing info")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
|
||||||
|
var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]()
|
||||||
|
|
||||||
|
NSLog("%@", "**** Set playback info: rate \(playbackRate()), position \(currentTime()), duration \(duration())")
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration()
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime()
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = playbackRate()
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
|
||||||
|
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,14 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
||||||
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
||||||
|
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
||||||
|
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||||
|
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
||||||
|
pod 'RobingenzCapacitorAppUpdate', :path => '../../node_modules/@robingenz/capacitor-app-update'
|
||||||
|
pod 'CapacitorDataStorageSqlite', :path => '../../node_modules/capacitor-data-storage-sqlite'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full min-h-screen h-full bg-bg text-white">
|
<div class="w-full layout-wrapper bg-bg text-white">
|
||||||
<Nuxt />
|
<Nuxt />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full min-h-screen h-full bg-bg text-white">
|
<div class="w-full layout-wrapper bg-bg text-white">
|
||||||
<app-appbar />
|
<app-appbar />
|
||||||
<div id="content" class="overflow-hidden" :class="playerIsOpen ? 'playerOpen' : ''">
|
<div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''">
|
||||||
<Nuxt />
|
<Nuxt />
|
||||||
</div>
|
</div>
|
||||||
<app-audio-player-container ref="streamContainer" />
|
<app-audio-player-container ref="streamContainer" />
|
||||||
|
@ -16,7 +16,6 @@ import { Capacitor } from '@capacitor/core'
|
||||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||||
import AudioDownloader from '@/plugins/audio-downloader'
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
import StorageManager from '@/plugins/storage-manager'
|
import StorageManager from '@/plugins/storage-manager'
|
||||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
@ -414,12 +413,3 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
#content {
|
|
||||||
height: calc(100vh - 64px);
|
|
||||||
}
|
|
||||||
#content.playerOpen {
|
|
||||||
height: calc(100vh - 164px);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -20,7 +20,7 @@ export default {
|
||||||
},
|
},
|
||||||
meta: [
|
meta: [
|
||||||
{ charset: 'utf-8' },
|
{ charset: 'utf-8' },
|
||||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
{ name: 'viewport', content: 'viewport-fit=cover, width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1' },
|
||||||
{ hid: 'description', name: 'description', content: '' },
|
{ hid: 'description', name: 'description', content: '' },
|
||||||
{ name: 'format-detection', content: 'telephone=no' }
|
{ name: 'format-detection', content: 'telephone=no' }
|
||||||
],
|
],
|
||||||
|
|
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.33-beta",
|
"version": "0.9.35-beta",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1076,6 +1076,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/network/-/network-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/network/-/network-1.0.3.tgz",
|
||||||
"integrity": "sha512-DgRusTC0UkTJE9IQIAMgqBnRnTaj8nFeGH7dwRldfVBZAtHBTkU8wCK/tU1oWtaY2Wam+iyVKXUAhYDO7yeD9Q=="
|
"integrity": "sha512-DgRusTC0UkTJE9IQIAMgqBnRnTaj8nFeGH7dwRldfVBZAtHBTkU8wCK/tU1oWtaY2Wam+iyVKXUAhYDO7yeD9Q=="
|
||||||
},
|
},
|
||||||
|
"@capacitor/status-bar": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-5MGWFq76iiKvHpbZ/Xc0Zig3WZyzWZ62wvC4qxak8OuVHBNG4fA1p/XXY9teQPaU3SupEJHnLkw6Gn1LuDp+ew=="
|
||||||
|
},
|
||||||
"@capacitor/storage": {
|
"@capacitor/storage": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/storage/-/storage-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/storage/-/storage-1.1.0.tgz",
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"@capacitor/dialog": "^1.0.3",
|
"@capacitor/dialog": "^1.0.3",
|
||||||
"@capacitor/ios": "^3.2.2",
|
"@capacitor/ios": "^3.2.2",
|
||||||
"@capacitor/network": "^1.0.3",
|
"@capacitor/network": "^1.0.3",
|
||||||
|
"@capacitor/status-bar": "^1.0.6",
|
||||||
"@capacitor/storage": "^1.1.0",
|
"@capacitor/storage": "^1.1.0",
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@robingenz/capacitor-app-update": "^1.0.0",
|
"@robingenz/capacitor-app-update": "^1.0.0",
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
||||||
<p class="text-sm text-gray-400">by {{ author }}</p>
|
<p class="text-sm text-gray-400">by {{ author }}</p>
|
||||||
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
||||||
{{ $elapsedPretty(duration) }}<span class="px-4">{{ $bytesPretty(size) }}</span>
|
{{ $elapsedPretty(duration) }}
|
||||||
|
<span class="px-4">{{ $bytesPretty(size) }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
|
<div v-if="progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
|
||||||
|
@ -35,7 +36,7 @@
|
||||||
<span class="material-icons">auto_stories</span>
|
<span class="material-icons">auto_stories</span>
|
||||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-if="isConnected && showPlay" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
<ui-btn v-if="isConnected && showPlay && !isIos" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
||||||
<span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
|
<span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,6 +85,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
|
},
|
||||||
isConnected() {
|
isConnected() {
|
||||||
return this.$store.state.socketConnected
|
return this.$store.state.socketConnected
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,11 +27,13 @@ export default {
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.main-content {
|
.main-content {
|
||||||
|
height: calc(100% - 72px);
|
||||||
max-height: calc(100% - 72px);
|
max-height: calc(100% - 72px);
|
||||||
min-height: calc(100% - 72px);
|
min-height: calc(100% - 72px);
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
.main-content.home-page {
|
.main-content.home-page {
|
||||||
|
height: calc(100% - 36px);
|
||||||
max-height: calc(100% - 36px);
|
max-height: calc(100% - 36px);
|
||||||
min-height: calc(100% - 36px);
|
min-height: calc(100% - 36px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full min-h-full relative">
|
||||||
<template v-for="(shelf, index) in shelves">
|
<template v-for="(shelf, index) in shelves">
|
||||||
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
|
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -7,16 +7,21 @@
|
||||||
<div v-if="!shelves.length" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
<div v-if="!shelves.length" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-4 text-center text-xl">
|
<p class="mb-4 text-center text-xl">
|
||||||
Bookshelf empty<span v-show="isSocketConnected">
|
Bookshelf empty
|
||||||
for library <strong>{{ currentLibraryName }}</strong></span
|
<span v-show="isSocketConnected">
|
||||||
>
|
for library
|
||||||
|
<strong>{{ currentLibraryName }}</strong>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="w-full" v-if="!isSocketConnected">
|
<div class="w-full" v-if="!isSocketConnected">
|
||||||
<div class="flex justify-center items-center mb-3">
|
<div class="flex justify-center items-center mb-3">
|
||||||
<span class="material-icons text-error text-lg">cloud_off</span>
|
<span class="material-icons text-error text-lg">cloud_off</span>
|
||||||
<p class="pl-2 text-error text-sm">Audiobookshelf server not connected.</p>
|
<p class="pl-2 text-error text-sm">Audiobookshelf server not connected.</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="px-4 text-center text-error absolute bottom-12 left-0 right-0 mx-auto"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p>
|
<p class="px-4 text-center text-error absolute bottom-12 left-0 right-0 mx-auto">
|
||||||
|
<strong>Important!</strong> This app requires that you are running
|
||||||
|
<u>your own server</u> and does not provide any content.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<ui-btn v-if="!isSocketConnected" small @click="$router.push('/connect')" class="w-32">Connect</ui-btn>
|
<ui-btn v-if="!isSocketConnected" small @click="$router.push('/connect')" class="w-32">Connect</ui-btn>
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
<div class="w-full h-full py-6">
|
<div class="w-full h-full py-6">
|
||||||
<h1 class="text-2xl px-4">Downloads</h1>
|
<h1 class="text-2xl px-4">Downloads</h1>
|
||||||
|
|
||||||
|
<template v-if="isIos"></template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<div class="w-full px-2 py-2" :class="hasStoragePermission ? '' : 'text-error'">
|
<div class="w-full px-2 py-2" :class="hasStoragePermission ? '' : 'text-error'">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="material-icons" @click="changeDownloadFolderClick">{{ hasStoragePermission ? 'folder' : 'error' }}</span>
|
<span class="material-icons" @click="changeDownloadFolderClick">{{ hasStoragePermission ? 'folder' : 'error' }}</span>
|
||||||
|
@ -87,6 +91,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -105,6 +110,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
|
},
|
||||||
isSocketConnected() {
|
isSocketConnected() {
|
||||||
return this.$store.state.socketConnected
|
return this.$store.state.socketConnected
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { App } from '@capacitor/app'
|
import { App } from '@capacitor/app'
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
|
import { StatusBar, Style } from '@capacitor/status-bar';
|
||||||
import { formatDistance, format } from 'date-fns'
|
import { formatDistance, format } from 'date-fns'
|
||||||
|
|
||||||
|
const setStatusBarStyleDark = async () => {
|
||||||
|
await StatusBar.setStyle({ style: Style.Dark })
|
||||||
|
}
|
||||||
|
setStatusBarStyleDark()
|
||||||
|
|
||||||
App.addListener('backButton', async ({ canGoBack }) => {
|
App.addListener('backButton', async ({ canGoBack }) => {
|
||||||
if (!canGoBack) {
|
if (!canGoBack) {
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue