This commit is contained in:
advplyr
2021-09-01 20:07:11 -05:00
commit 495af35043
106 changed files with 18798 additions and 0 deletions
@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.audiobookshelf.app">
<!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name="com.audiobookshelf.app.MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service
android:enabled="true"
android:name=".PlayerNotificationService">
</service>
</application>
</manifest>
@@ -0,0 +1,6 @@
{
"appId": "com.audiobookshelf.app",
"appName": "audiobookshelf-app",
"webDir": "dist",
"bundledWebRuntime": false
}
@@ -0,0 +1,10 @@
[
{
"pkg": "@capacitor/dialog",
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
},
{
"pkg": "@capacitor/toast",
"classpath": "com.capacitorjs.plugins.toast.ToastPlugin"
}
]
@@ -0,0 +1,38 @@
package com.audiobookshelf.app
import android.net.Uri
import com.getcapacitor.JSObject
class Audiobook {
var id:String = "audiobook"
var token:String = ""
var playlistUrl:String = ""
var title:String = "No Title"
var author:String = "Unknown"
var series:String = ""
var cover:String = ""
var playWhenReady:Boolean = false
var startTime:Long = 0
var duration:Long = 0
var hasPlayerLoaded:Boolean = false
val playlistUri:Uri
val coverUri:Uri
constructor(jsondata:JSObject) {
id = jsondata.getString("id", "audiobook").toString()
title = jsondata.getString("title", "No Title").toString()
token = jsondata.getString("token", "").toString()
author = jsondata.getString("author", "Unknown").toString()
series = jsondata.getString("series", "").toString()
cover = jsondata.getString("cover", "").toString()
playlistUrl = jsondata.getString("playlistUrl", "").toString()
playWhenReady = jsondata.getBoolean("playWhenReady", false) == true
startTime = jsondata.getString("startTime", "0")!!.toLong()
duration = jsondata.getString("duration", "0")!!.toLong()
playlistUri = Uri.parse(playlistUrl)
coverUri = Uri.parse(cover)
}
}
@@ -0,0 +1,65 @@
package com.audiobookshelf.app
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import com.example.myapp.MyNativeAudio
import com.getcapacitor.BridgeActivity
class MainActivity : BridgeActivity() {
private val tag = "MainActivity"
private var mBounded = false
lateinit var foregroundService : PlayerNotificationService
private lateinit var mConnection : ServiceConnection
lateinit var pluginCallback : () -> Unit
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
registerPlugin(MyNativeAudio::class.java)
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
mConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName) {
Log.w(tag, "Service Disconnected")
mBounded = false
}
override fun onServiceConnected(name: ComponentName, service: IBinder) {
Log.d(tag, "Service Connected")
mBounded = true
val mLocalBinder = service as PlayerNotificationService.LocalBinder
foregroundService = mLocalBinder.getService()
// Let MyNativeAudio know foreground service is ready and setup event listener
if (pluginCallback != null) {
pluginCallback()
}
}
}
val startIntent = Intent(this, PlayerNotificationService::class.java)
bindService(startIntent, mConnection as ServiceConnection, Context.BIND_AUTO_CREATE);
}
fun stopMyService() {
if (mBounded) {
mConnection?.let { unbindService(it) };
mBounded = false;
}
val stopIntent = Intent(this, PlayerNotificationService::class.java)
stopService(stopIntent)
}
}
@@ -0,0 +1,125 @@
package com.example.myapp
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import com.audiobookshelf.app.Audiobook
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.PlayerNotificationService
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
@CapacitorPlugin(name = "MyNativeAudio")
class MyNativeAudio : Plugin() {
private val tag = "MyNativeAudio"
lateinit var mainActivity:MainActivity
lateinit var playerNotificationService: PlayerNotificationService
override fun load() {
mainActivity = (activity as MainActivity)
var foregroundServiceReady : () -> Unit = {
playerNotificationService = mainActivity.foregroundService
playerNotificationService.setCustomObjectListener(object: PlayerNotificationService.MyCustomObjectListener {
override fun onPlayingUpdate(isPlaying:Boolean) {
emit("onPlayingUpdate", isPlaying)
}
override fun onMetadata(metadata:JSObject) {
notifyListeners("onMetadata", metadata)
}
})
}
mainActivity.pluginCallback = foregroundServiceReady
}
fun emit(evtName: String, value:Any) {
var ret:JSObject = JSObject()
ret.put("value", value)
notifyListeners(evtName, ret)
}
@PluginMethod
fun initPlayer(call: PluginCall) {
if (!PlayerNotificationService.isStarted) {
Log.w(tag, "Starting foreground service --")
Intent(mainActivity, PlayerNotificationService::class.java).also { intent ->
ContextCompat.startForegroundService(mainActivity, intent)
}
} else {
Log.w(tag, "Service already started --")
}
var audiobook:Audiobook = Audiobook(call.data)
Handler(Looper.getMainLooper()).post() {
playerNotificationService.initPlayer(audiobook)
call.resolve()
}
}
@PluginMethod
fun getCurrentTime(call: PluginCall) {
Handler(Looper.getMainLooper()).post() {
var currentTime = playerNotificationService.getCurrentTime()
Log.d(tag, "Get Current Time $currentTime")
val ret = JSObject()
ret.put("value", currentTime)
call.resolve(ret)
}
}
@PluginMethod
fun pausePlayer(call: PluginCall) {
Handler(Looper.getMainLooper()).post() {
playerNotificationService.pause()
call.resolve()
}
}
@PluginMethod
fun playPlayer(call: PluginCall) {
Handler(Looper.getMainLooper()).post() {
playerNotificationService.play()
call.resolve()
}
}
@PluginMethod
fun seekPlayer(call: PluginCall) {
var time:Long = call.getString("timeMs", "0")!!.toLong()
Handler(Looper.getMainLooper()).post() {
playerNotificationService.seekPlayer(time)
call.resolve()
}
}
@PluginMethod
fun seekForward10(call: PluginCall) {
Handler(Looper.getMainLooper()).post() {
playerNotificationService.seekForward10()
call.resolve()
}
}
@PluginMethod
fun seekBackward10(call: PluginCall) {
Handler(Looper.getMainLooper()).post() {
playerNotificationService.seekBackward10()
call.resolve()
}
}
@PluginMethod
fun terminateStream(call: PluginCall) {
Handler(Looper.getMainLooper()).post() {
playerNotificationService.terminateStream()
call.resolve()
}
}
}
@@ -0,0 +1,422 @@
package com.audiobookshelf.app
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.getcapacitor.JSObject
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.*
import kotlinx.coroutines.*
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
class PlayerNotificationService : Service() {
companion object {
var isStarted = false
}
interface MyCustomObjectListener {
fun onPlayingUpdate(isPlaying: Boolean)
fun onMetadata(metadata: JSObject)
}
private val tag = "PlayerService"
private lateinit var listener:MyCustomObjectListener
private lateinit var ctx:Context
private lateinit var mPlayer: SimpleExoPlayer
private lateinit var mediaSessionConnector: MediaSessionConnector
private lateinit var playerNotificationManager: PlayerNotificationManager
private lateinit var mediaSession: MediaSessionCompat
private val serviceJob = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
private val binder = LocalBinder()
private val glideOptions = RequestOptions()
.fallback(R.drawable.icon)
.diskCacheStrategy(DiskCacheStrategy.DATA)
private var notificationId = 10;
private var channelId = "audiobookshelf_channel"
private var channelName = "Audiobookshelf Channel"
private var currentAudiobook:Audiobook? = null
fun setCustomObjectListener(mylistener: MyCustomObjectListener) {
listener = mylistener
}
/*
Service related stuff
*/
override fun onBind(intent: Intent?): IBinder? {
Log.d(tag, "onBind")
return binder
}
inner class LocalBinder : Binder() {
// Return this instance of LocalService so clients can call public methods
fun getService(): PlayerNotificationService = this@PlayerNotificationService
}
fun stopService(context: Context) {
val stopIntent = Intent(context, PlayerNotificationService::class.java)
context.stopService(stopIntent)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(tag, "onStartCommand $startId")
isStarted = true
return START_STICKY
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId,
channelName, NotificationManager.IMPORTANCE_HIGH)
chan.lightColor = Color.DKGRAY
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
// detach player
override fun onDestroy() {
playerNotificationManager.setPlayer(null)
mPlayer.release()
mediaSession.release()
Log.d(tag, "onDestroy")
isStarted = false
super.onDestroy()
}
//removing service when user swipe out our app
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
Log.d(tag, "onTaskRemoved")
stopSelf()
}
override fun onCreate() {
super.onCreate()
ctx = this
channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId, channelName)
} else ""
mPlayer = SimpleExoPlayer.Builder(this).build()
setPlayerListeners()
val sessionActivityPendingIntent =
packageManager?.getLaunchIntentForPackage(packageName)?.let { sessionIntent ->
PendingIntent.getActivity(this, 0, sessionIntent, 0)
}
mediaSession = MediaSessionCompat(this, tag)
.apply {
setSessionActivity(sessionActivityPendingIntent)
isActive = true
}
Log.d(tag, "Media Session Set")
val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken)
val builder = PlayerNotificationManager.Builder(
ctx,
notificationId,
channelId)
builder.setMediaDescriptionAdapter(DescriptionAdapter(mediaController))
builder.setNotificationListener(object : PlayerNotificationManager.NotificationListener {
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
onGoing: Boolean) {
// Start foreground service
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
startForeground(notificationId, notification)
}
override fun onNotificationCancelled(
notificationId: Int,
dismissedByUser: Boolean
) {
if (dismissedByUser) {
Log.d(tag, "onNotificationCancelled dismissed by user")
stopSelf()
} else {
Log.d(tag, "onNotificationCancelled not dismissed by user")
}
}
})
playerNotificationManager = builder.build()
playerNotificationManager.setMediaSessionToken(mediaSession.sessionToken)
playerNotificationManager.setUsePlayPauseActions(true)
playerNotificationManager.setUseNextAction(false)
playerNotificationManager.setUsePreviousAction(false)
playerNotificationManager.setUseChronometer(false)
playerNotificationManager.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX)
// Unknown action
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
// Color is set based on the art - cannot override
// playerNotificationManager.setColor(Color.RED)
// playerNotificationManager.setColorized(true)
// Icon needs to be black and white
// playerNotificationManager.setSmallIcon(R.drawable.icon_32)
mediaSessionConnector = MediaSessionConnector(mediaSession)
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
return MediaDescriptionCompat.Builder()
.setMediaId(currentAudiobook!!.id)
.setTitle(currentAudiobook!!.title)
.setSubtitle(currentAudiobook!!.author)
.setMediaUri(currentAudiobook!!.playlistUri)
.setIconUri(currentAudiobook!!.coverUri)
.build()
}
}
mediaSessionConnector.setQueueNavigator(queueNavigator)
mediaSessionConnector.setPlayer(mPlayer)
//attach player to playerNotificationManager
playerNotificationManager.setPlayer(mPlayer)
}
private inner class DescriptionAdapter(private val controller: MediaControllerCompat) :
PlayerNotificationManager.MediaDescriptionAdapter {
var currentIconUri: Uri? = null
var currentBitmap: Bitmap? = null
override fun createCurrentContentIntent(player: Player): PendingIntent? =
controller.sessionActivity
override fun getCurrentContentText(player: Player) = controller.metadata.description.subtitle.toString()
override fun getCurrentContentTitle(player: Player) = controller.metadata.description.title.toString()
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
val albumArtUri = controller.metadata.description.iconUri
return if (currentIconUri != albumArtUri || currentBitmap == null) {
// Cache the bitmap for the current audiobook so that successive calls to
// `getCurrentLargeIcon` don't cause the bitmap to be recreated.
currentIconUri = albumArtUri
serviceScope.launch {
currentBitmap = albumArtUri?.let {
resolveUriAsBitmap(it)
}
currentBitmap?.let { callback.onBitmap(it) }
}
null
} else {
currentBitmap
}
}
private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {
return withContext(Dispatchers.IO) {
// Block on downloading artwork.
Glide.with(ctx).applyDefaultRequestOptions(glideOptions)
.asBitmap()
.load(uri)
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.get()
}
}
}
private fun setPlayerListeners() {
mPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
error.message?.let { Log.e(tag, it) }
error.localizedMessage?.let { Log.e(tag, it) }
}
override fun onEvents(player: Player, events: Player.Events) {
if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
Log.d(tag, "EVENT_TRACKS_CHANGED")
}
if (events.contains(Player.EVENT_TIMELINE_CHANGED)) {
Log.d(tag, "EVENT_TIMELINE_CHANGED")
}
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
Log.d(tag, "EVENT_POSITION_DISCONTINUITY")
}
if (events.contains(Player.EVENT_IS_LOADING_CHANGED)) {
Log.d(tag, "EVENT_IS_LOADING_CHANGED : " + mPlayer.isLoading.toString())
}
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
if (mPlayer.playbackState == Player.STATE_READY) {
Log.d(tag, "STATE_READY : " + mPlayer.duration.toString())
if (!currentAudiobook!!.hasPlayerLoaded && currentAudiobook!!.startTime > 0) {
Log.d(tag, "Should seek to ${currentAudiobook!!.startTime}")
mPlayer.seekTo(currentAudiobook!!.startTime)
}
currentAudiobook!!.hasPlayerLoaded = true
sendClientMetadata("ready")
}
if (mPlayer.playbackState == Player.STATE_BUFFERING) {
Log.d(tag, "STATE_BUFFERING : " + mPlayer.currentPosition.toString())
sendClientMetadata("buffering")
}
if (mPlayer.playbackState == Player.STATE_ENDED) {
Log.d(tag, "STATE_ENDED")
sendClientMetadata("ended")
}
if (mPlayer.playbackState == Player.STATE_IDLE) {
Log.d(tag, "STATE_IDLE")
sendClientMetadata("idle")
}
}
if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) {
Log.d(tag, "EVENT_MEDIA_METADATA_CHANGED")
}
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED")
}
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
Log.d(tag, "EVENT IS PLAYING CHANGED")
if (listener != null) listener.onPlayingUpdate(player.isPlaying)
}
}
})
}
/*
User callable methods
*/
// fun initPlayer(token: String, playlistUri: String, playWhenReady: Boolean, currentTime: Long, title: String, artist: String, albumArt: String) {
fun initPlayer(audiobook: Audiobook) {
currentAudiobook = audiobook
Log.d(tag, "Init Player Audiobook ${currentAudiobook!!.playlistUrl} | ${currentAudiobook!!.title} | ${currentAudiobook!!.author}")
if (mPlayer.isPlaying) {
Log.d(tag, "Init Player audiobook already playing")
}
val metadata = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, currentAudiobook!!.title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, currentAudiobook!!.title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, currentAudiobook!!.author)
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, currentAudiobook!!.author)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentAudiobook!!.author)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, currentAudiobook!!.series)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, currentAudiobook!!.cover)
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, currentAudiobook!!.cover)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, currentAudiobook!!.id)
.build()
mediaSession.setMetadata(metadata)
var mediaMetadata = MediaMetadata.Builder().build()
var mediaItem = MediaItem.Builder().setUri(currentAudiobook!!.playlistUri).setMediaMetadata(mediaMetadata).build()
var dataSourceFactory = DefaultHttpDataSource.Factory()
dataSourceFactory.setUserAgent(channelId)
dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${currentAudiobook!!.token}"))
var mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
mPlayer.setMediaSource(mediaSource, true)
mPlayer.prepare()
mPlayer.playWhenReady = currentAudiobook!!.playWhenReady
}
fun getCurrentTime() : Long {
return mPlayer.currentPosition
}
fun play() {
if (mPlayer.isPlaying) {
Log.d(tag, "Already playing")
return
}
mPlayer.play()
}
fun pause() {
mPlayer.pause()
}
fun seekPlayer(time: Long) {
mPlayer.seekTo(time)
}
fun seekForward10() {
mPlayer.seekTo(mPlayer.currentPosition + 10000)
}
fun seekBackward10() {
mPlayer.seekTo(mPlayer.currentPosition - 10000)
}
fun terminateStream() {
if (mPlayer.playbackState == Player.STATE_READY) {
mPlayer.clearMediaItems()
}
}
fun sendClientMetadata(stateName: String) {
var metadata = JSObject()
var duration = mPlayer.duration
if (duration < 0) duration = 0
metadata.put("duration", duration)
metadata.put("currentTime", mPlayer.currentPosition)
metadata.put("stateName", stateName)
if (listener != null) listener.onMetadata(metadata)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">AudioBookshelf</string>
<string name="title_activity_main">AudioBookshelf</string>
<string name="package_name">com.audiobookshelf.app</string>
<string name="custom_url_scheme">com.audiobookshelf.app</string>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:background">@drawable/splash</item>
</style>
</resources>
+15
View File
@@ -0,0 +1,15 @@
<?xml version='1.0' encoding='utf-8'?>
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" />
<feature name="File">
<param name="android-package" value="org.apache.cordova.file.FileUtils"/>
<param name="onload" value="true"/>
</feature>
<feature name="Media">
<param name="android-package" value="org.apache.cordova.media.AudioHandler"/>
</feature>
</widget>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}