diff --git a/android/app/build.gradle b/android/app/build.gradle index d7252823..179ad30e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.audiobookshelf.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 38 - versionName "0.9.19-beta" + versionCode 39 + versionName "0.9.20-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a5c482ba..030ce910 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -20,6 +21,14 @@ android:networkSecurityConfig="@xml/network_security_config" android:requestLegacyExternalStorage="true"> + + + + + + + + + + - - - diff --git a/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt b/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt index 0832b189..f924e78a 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt @@ -66,7 +66,7 @@ class Audiobook { // return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) return Uri.parse(localCoverUrl) } - if (book.cover == "") return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) + if (book.cover == "" || serverUrl == "" || token == "") return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) return Uri.parse("$serverUrl${book.cover}?token=$token&ts=${book.lastUpdate}") } @@ -74,29 +74,6 @@ class Audiobook { return duration.toLong() * 1000L } - fun toMediaItem():MediaBrowserCompat.MediaItem { - var builder = MediaDescriptionCompat.Builder() - .setMediaId(id) - .setTitle(book.title) - .setSubtitle(book.authorFL) - .setMediaUri(null) - .setIconUri(getCover()) - - val extras = Bundle() - if (isDownloaded) { - extras.putLong( - MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS, - MediaDescriptionCompat.STATUS_DOWNLOADED) - } -// extras.putInt( -// MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, -// MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED) - builder.setExtras(extras) - - var mediaDescription = builder.build() - return MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) - } - fun toMediaMetadata():MediaMetadataCompat { return MediaMetadataCompat.Builder().apply { putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) diff --git a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt index ca0e8c56..edce7676 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt @@ -2,15 +2,10 @@ package com.audiobookshelf.app import android.app.Activity import android.content.Context -import android.os.Bundle import android.os.Handler import android.os.Looper -import android.support.v4.media.MediaBrowserCompat -import android.support.v4.media.MediaDescriptionCompat import android.util.Log -import androidx.media.MediaBrowserServiceCompat -import androidx.media.utils.MediaConstants import com.getcapacitor.JSArray import com.getcapacitor.JSObject import com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlite @@ -40,17 +35,22 @@ class AudiobookManager { fun init() { var sharedPreferences = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE) - serverUrl = sharedPreferences.getString("serverUrl", null).toString() + serverUrl = sharedPreferences.getString("serverUrl", "").toString() Log.d(tag, "SHARED PREF SERVERURL $serverUrl") - token = sharedPreferences.getString("token", null).toString() + token = sharedPreferences.getString("token", "").toString() Log.d(tag, "SHARED PREF TOKEN $token") } fun loadAudiobooks(cb: (() -> Unit)) { + Log.d(tag, "LOAD AUDIBOOOSK $serverUrl | $token") if (serverUrl == "" || token == "") { Log.d(tag, "No Server or Token set") cb() return + } else if (!serverUrl.startsWith("http")) { + Log.e(tag, "Invalid server url $serverUrl") + cb() + return } var url = "$serverUrl/api/library/main/audiobooks" @@ -98,78 +98,6 @@ class AudiobookManager { }) } - fun fetchAudiobooks(result: MediaBrowserServiceCompat.Result>) { - var url = "$serverUrl/api/library/main/audiobooks" - val request = Request.Builder() - .url(url).addHeader("Authorization", "Bearer $token") - .build() - - client.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - Log.d(tag, "FAILURE TO CONNECT") - e.printStackTrace() - } - - override fun onResponse(call: Call, response: Response) { - response.use { - if (!response.isSuccessful) throw IOException("Unexpected code $response") - - var bodyString = response.body!!.string() - var json = JSArray(bodyString) - var totalBooks = json.length() - 1 - for (i in 0..totalBooks) { - var abobj = json.get(i) - var jsobj = JSObject(abobj.toString()) - jsobj.put("isDownloaded", false) - var audiobook = Audiobook(jsobj, serverUrl, token) - - if (audiobook.isMissing || audiobook.isInvalid) { - Log.d(tag, "Audiobook ${audiobook.book.title} is missing or invalid") - } else if (audiobook.numTracks <= 0) { - Log.d(tag, "Audiobook ${audiobook.book.title} has audio tracks") - } else { - var audiobookExists = audiobooks.find { it.id == audiobook.id } - if (audiobookExists == null) { - audiobooks.add(audiobook) - } else { - Log.d(tag, "Audiobook already there from downloaded") - } - } - } - Log.d(tag, "${audiobooks.size} Audiobooks Loaded") - - val mediaItems: MutableList = mutableListOf() - audiobooks.forEach { - var builder = MediaDescriptionCompat.Builder() - .setMediaId(it.id) - .setTitle(it.book.title) - .setSubtitle(it.book.authorFL) - .setMediaUri(null) - .setIconUri(it.getCover()) - - val extras = Bundle() - if (it.isDownloaded) { - extras.putLong( - MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS, - MediaDescriptionCompat.STATUS_DOWNLOADED) - } -// extras.putInt( -// MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, -// MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED) - builder.setExtras(extras) - - var mediaDescription = builder.build() - var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) - mediaItems.add(newMediaItem) - - } - Log.d(tag, "AudiobookManager: Sending ${mediaItems.size} Audiobooks") - result.sendResult(mediaItems) - } - } - }) - } - fun load() { isLoading = true hasLoaded = true @@ -261,4 +189,58 @@ class AudiobookManager { var audiobookStreamData = AudiobookStreamData(abStreamDataObj) return audiobookStreamData } + + fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int { + val lhsLength = lhs.length + 1 + val rhsLength = rhs.length + 1 + + var cost = Array(lhsLength) { it } + var newCost = Array(lhsLength) { 0 } + + for (i in 1..rhsLength-1) { + newCost[0] = i + + for (j in 1..lhsLength-1) { + val match = if(lhs[j - 1] == rhs[i - 1]) 0 else 1 + + val costReplace = cost[j - 1] + match + val costInsert = cost[j] + 1 + val costDelete = newCost[j - 1] + 1 + + newCost[j] = Math.min(Math.min(costInsert, costDelete), costReplace) + } + + val swap = cost + cost = newCost + newCost = swap + } + + return cost[lhsLength - 1] + } + + fun searchForAudiobook(query:String):Audiobook? { + var closestDistance = 99 + var closestMatch:Audiobook? = null + audiobooks.forEach { + var dist = levenshtein(it.book.title, query) + Log.d(tag, "LEVENSHTEIN $dist") + if (dist < closestDistance) { + closestDistance = dist + closestMatch = it + } + } + if (closestMatch != null) { + Log.d(tag, "Closest Search is ${closestMatch?.book?.title} with distance $closestDistance") + if (closestDistance < 2) { + return closestMatch + } + return null + } + return null + } + + fun getFirstAudiobook():Audiobook? { + if (audiobooks.isEmpty()) return null + return audiobooks[0] + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt index c7cdad2e..c59fef38 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt @@ -1,18 +1,13 @@ package com.audiobookshelf.app -import android.app.Activity import android.app.DownloadManager +import android.app.SearchManager import android.content.* import android.os.* import android.util.Log import com.anggrayudi.storage.SimpleStorage import com.anggrayudi.storage.SimpleStorageHelper import com.getcapacitor.BridgeActivity -import com.getcapacitor.JSObject -import com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlite -import okhttp3.OkHttpClient -import okhttp3.Request -import java.net.URL class MainActivity : BridgeActivity() { diff --git a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt index 15fb4024..245674f7 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt @@ -1,5 +1,6 @@ package com.audiobookshelf.app +import android.annotation.SuppressLint import android.app.* import android.content.Context import android.content.Intent @@ -12,7 +13,9 @@ 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.support.v4.media.session.PlaybackStateCompat import android.util.Log +import android.view.KeyEvent import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.media.MediaBrowserServiceCompat @@ -31,11 +34,9 @@ import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.upstream.* import kotlinx.coroutines.* -import android.view.KeyEvent +import okhttp3.OkHttpClient import java.util.* import kotlin.concurrent.schedule -import android.annotation.SuppressLint -import okhttp3.OkHttpClient const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px @@ -49,8 +50,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { interface MyCustomObjectListener { fun onPlayingUpdate(isPlaying: Boolean) fun onMetadata(metadata: JSObject) - fun onPrepare(audiobookId:String, playWhenReady:Boolean) - fun onSleepTimerEnded(currentPosition:Long) + fun onPrepare(audiobookId: String, playWhenReady: Boolean) + fun onSleepTimerEnded(currentPosition: Long) } @@ -63,6 +64,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { private lateinit var mediaSessionConnector: MediaSessionConnector private lateinit var playerNotificationManager: PlayerNotificationManager private lateinit var mediaSession: MediaSessionCompat + private lateinit var transportControls:MediaControllerCompat.TransportControls private val serviceJob = SupervisorJob() private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob) @@ -77,8 +79,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { private var currentAudiobookStreamData:AudiobookStreamData? = null -// private var audiobooks = mutableListOf() - private var mediaButtonClickCount: Int = 0 var mediaButtonClickTimeout: Long = 1000 //ms var seekAmount: Long = 20000 //ms @@ -127,7 +127,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } override fun onStart(intent: Intent?, startId: Int) { - Log.d(tag, "onStart $startId" ) + Log.d(tag, "onStart $startId") } @RequiresApi(Build.VERSION_CODES.O) @@ -141,6 +141,57 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { return channelId } + private fun playAudiobookFromMediaBrowser(audiobook: Audiobook, playWhenReady: Boolean) { + if (!audiobook.isDownloaded) { + var streamListener = object : AudiobookManager.OnStreamData { + override fun onStreamReady(asd: AudiobookStreamData) { + Log.d(tag, "Stream Ready ${asd.playlistUrl}") + asd.playWhenReady = playWhenReady + initPlayer(asd) + } + } + audiobookManager.openStream(audiobook, streamListener) + } else { + var asd = audiobookManager.initLocalPlay(audiobook) + asd.playWhenReady = playWhenReady + initPlayer(asd) + } + } + + private fun playFirstAudiobook(playWhenReady: Boolean) { + var firstAudiobook = audiobookManager.getFirstAudiobook() + if (firstAudiobook != null) { + playAudiobookFromMediaBrowser(firstAudiobook, playWhenReady) + } + } + + private fun openFromMediaId(mediaId: String, playWhenReady: Boolean) { + var audiobook = audiobookManager.audiobooks.find { it.id == mediaId } + if (audiobook == null) { + Log.e(tag, "Audiobook NOT FOUND") + return + } + + playAudiobookFromMediaBrowser(audiobook, playWhenReady) + } + + private fun openFromSearch(query: String?, playWhenReady: Boolean) { + if (query?.isNullOrEmpty() == true) { + Log.d(tag, "Empty search query play first audiobook") + playFirstAudiobook(playWhenReady) + return + } + + var audiobook = audiobookManager.searchForAudiobook(query) + if (audiobook == null) { + Log.e(tag, "No Audiobook found for search $query") + pause() + return + } + + playAudiobookFromMediaBrowser(audiobook, playWhenReady) + } + // detach player override fun onDestroy() { playerNotificationManager.setPlayer(null) @@ -193,6 +244,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { isActive = true } + + Log.d(tag, "Media Session Set") val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken) @@ -247,6 +300,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // Unknown action playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE) + transportControls = mediaController.transportControls + // Color is set based on the art - cannot override // playerNotificationManager.setColor(Color.RED) // playerNotificationManager.setColorized(true) @@ -267,114 +322,112 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } -// val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer { -// override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean { -// Log.d(tag, "ON COMMAND $command") -// return false -// } -// -// override fun getSupportedPrepareActions(): Long { -// return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or -// PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or -// PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or -// PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH -// } -// -// override fun onPrepare(playWhenReady: Boolean) { -// Log.d(tag, "ON PREPARE $playWhenReady") -// var audiobook = audiobookManager.audiobooks[0] -// if (audiobook == null) { -// Log.e(tag, "Audiobook NOT FOUND") -// return -// } -// -// var streamListener = object : AudiobookManager.OnStreamData { -// override fun onStreamReady(asd: AudiobookStreamData) { -// Log.d(tag, "Stream Ready ${asd.playlistUrl}") -// initPlayer(asd) -// } -// } -// audiobookManager.openStream(audiobook, streamListener) -// } -// -// override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { -// Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady") -// var audiobook = audiobookManager.audiobooks.find { it.id == mediaId } -// if (audiobook == null) { -// Log.e(tag, "Audiobook NOT FOUND") -// return -// } -// -// var streamListener = object : AudiobookManager.OnStreamData { -// override fun onStreamReady(asd: AudiobookStreamData) { -// Log.d(tag, "Stream Ready ${asd.playlistUrl}") -// initPlayer(asd) -// } -// } -// audiobookManager.openStream(audiobook, streamListener) -// } -// -// override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) { -// Log.d(tag, "ON PREPARE FROM SEARCH $query") -// } -// -// override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) { -// Log.d(tag, "ON PREPARE FROM URI $uri") -// } -// -// } + val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer { + override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean { + Log.d(tag, "ON COMMAND $command") + return false + } + + override fun getSupportedPrepareActions(): Long { + return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or + PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or + PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or + PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + } + + override fun onPrepare(playWhenReady: Boolean) { + Log.d(tag, "ON PREPARE $playWhenReady") + playFirstAudiobook(playWhenReady) + } + + override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { + Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady") + openFromMediaId(mediaId, playWhenReady) + } + + override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) { + Log.d(tag, "ON PREPARE FROM SEARCH $query") + openFromSearch(query, playWhenReady) + } + + override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) { + Log.d(tag, "ON PREPARE FROM URI $uri") + } + } + + mediaSessionConnector.setEnabledPlaybackActions( + PlaybackStateCompat.ACTION_PLAY_PAUSE + or PlaybackStateCompat.ACTION_PLAY + or PlaybackStateCompat.ACTION_PAUSE + or PlaybackStateCompat.ACTION_SEEK_TO + or PlaybackStateCompat.ACTION_FAST_FORWARD + or PlaybackStateCompat.ACTION_REWIND + or PlaybackStateCompat.ACTION_STOP + ) mediaSessionConnector.setQueueNavigator(queueNavigator) -// mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer) + mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer) mediaSessionConnector.setPlayer(mPlayer) //attach player to playerNotificationManager playerNotificationManager.setPlayer(mPlayer) - mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS) mediaSession.setCallback(object : MediaSessionCompat.Callback() { override fun onPrepare() { Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT") - super.onPrepare() + playFirstAudiobook(true) } + override fun onPlay() { Log.d(tag, "ON PLAY MEDIA SESSION COMPAT") play() } + + override fun onPrepareFromSearch(query: String?, extras: Bundle?) { + Log.d(tag, "ON PREPARE FROM SEARCH $query") + super.onPrepareFromSearch(query, extras) + } + + override fun onPlayFromSearch(query: String?, extras: Bundle?) { + Log.d(tag, "ON PLAY FROM SEARCH $query") + openFromSearch(query, true) + } + override fun onPause() { Log.d(tag, "ON PLAY MEDIA SESSION COMPAT") pause() } + override fun onStop() { pause() } + override fun onSkipToPrevious() { seekBackward(seekAmount) } + override fun onSkipToNext() { seekForward(seekAmount) } + + override fun onFastForward() { + seekForward(seekAmount) + } + + override fun onRewind() { + seekForward(seekAmount) + } + + override fun onSeekTo(pos: Long) { + seekPlayer(pos) + } + override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId") - - var audiobook = audiobookManager.audiobooks.find { it.id == mediaId } - if (audiobook == null) { - Log.e(tag, "Audiobook NOT FOUND") + if (mediaId.isNullOrEmpty()) { + playFirstAudiobook(true) return } - - if (!audiobook.isDownloaded) { - - var streamListener = object : AudiobookManager.OnStreamData { - override fun onStreamReady(asd: AudiobookStreamData) { - Log.d(tag, "Stream Ready ${asd.playlistUrl}") - initPlayer(asd) - } - } - audiobookManager.openStream(audiobook, streamListener) - } else { - var asd = audiobookManager.initLocalPlay(audiobook) - initPlayer(asd) - } + openFromMediaId(mediaId, true) } override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { @@ -390,7 +443,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { if (keyEvent?.getAction() == KeyEvent.ACTION_UP) { when (keyEvent?.getKeyCode()) { KeyEvent.KEYCODE_HEADSETHOOK -> { - if(0 == mediaButtonClickCount) { + if (0 == mediaButtonClickCount) { if (mPlayer.isPlaying) pause() else @@ -399,11 +452,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { handleMediaButtonClickCount() } KeyEvent.KEYCODE_MEDIA_PLAY -> { - if(0 == mediaButtonClickCount) play() + if (0 == mediaButtonClickCount) play() handleMediaButtonClickCount() } KeyEvent.KEYCODE_MEDIA_PAUSE -> { - if(0 == mediaButtonClickCount) pause() + if (0 == mediaButtonClickCount) pause() handleMediaButtonClickCount() } KeyEvent.KEYCODE_MEDIA_NEXT -> { @@ -415,6 +468,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { KeyEvent.KEYCODE_MEDIA_STOP -> { terminateStream() } + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { + Log.d(tag, "PLAY PAUSE TEST") + transportControls.playFromSearch("Brave New World", Bundle()) + +// if (mPlayer.isPlaying) { +// if (0 == mediaButtonClickCount) pause() +// handleMediaButtonClickCount() +// } else { +// if (0 == mediaButtonClickCount) play() +// handleMediaButtonClickCount() +// } + } else -> { Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}") return false @@ -526,8 +591,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { if (lastPauseTime == 0L) { sendClientMetadata("ready_no_sync") lastPauseTime = -1; - } - else sendClientMetadata("ready") + } else sendClientMetadata("ready") } if (mPlayer.playbackState == Player.STATE_BUFFERING) { Log.d(tag, "STATE_BUFFERING : " + mPlayer.currentPosition.toString()) @@ -565,8 +629,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } } - } - else lastPauseTime = System.currentTimeMillis() + } else lastPauseTime = System.currentTimeMillis() listener?.onPlayingUpdate(player.isPlaying) } } @@ -676,6 +739,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } fun pause() { + mPlayer.pause() } @@ -683,15 +747,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { mPlayer.seekTo(time) } - fun seekForward(amount:Long) { + fun seekForward(amount: Long) { mPlayer.seekTo(mPlayer.currentPosition + amount) } - fun seekBackward(amount:Long) { + fun seekBackward(amount: Long) { mPlayer.seekTo(mPlayer.currentPosition - amount) } - fun setPlaybackSpeed(speed:Float) { + fun setPlaybackSpeed(speed: Float) { mPlayer.setPlaybackSpeed(speed) } @@ -718,10 +782,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // MEDIA BROWSER STUFF (ANDROID AUTO) // private val AUTO_MEDIA_ROOT = "/" + private val ALL_ROOT = "__ALL__" private lateinit var browseTree:BrowseTree - private fun isValid(packageName:String, uid:Int) : Boolean { + private fun isValid(packageName: String, uid: Int) : Boolean { Log.d(tag, "Check package $packageName is valid with uid $uid") return true } @@ -734,16 +799,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // No further calls will be made to other media browsing methods. null } else { - - val maximumRootChildLimit = rootHints?.getInt( - MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, - /* defaultValue= */ 4) +// +// val maximumRootChildLimit = rootHints?.getInt( +// MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, +// /* defaultValue= */ 4) // val supportedRootChildFlags = rootHints.getInt( // MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, // /* defaultValue= */ android.media.browse.MediaBrowser.MediaItem.FLAG_BROWSABLE) - val extras = Bundle() extras.putBoolean( MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true) @@ -761,9 +825,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onLoadChildren(parentMediaId: String, result: Result>) { val mediaItems: MutableList = mutableListOf() Log.d(tag, "ON LOAD CHILDREN $parentMediaId") + var flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE if (!audiobookManager.hasLoaded) { - Log.d(tag, "audiobook manager loading") result.detach() audiobookManager.load() audiobookManager.loadAudiobooks() { @@ -772,7 +836,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { Log.d(tag, "LOADED AUDIOBOOKS") browseTree = BrowseTree(this, audiobookManager.audiobooks, null) val children = browseTree[parentMediaId]?.map { item -> - MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) + MediaBrowserCompat.MediaItem(item.description, flag) } if (children != null) { Log.d(tag, "BROWSE TREE CHILDREN ${children.size}") @@ -784,8 +848,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { Log.d(tag, "AUDIOBOOKS LOADING") result.detach() return - } else { - Log.d(tag, "ABs are loaded") } if (audiobookManager.audiobooks.size == 0) { @@ -794,9 +856,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { return } - - var flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE - val children = browseTree[parentMediaId]?.map { item -> MediaBrowserCompat.MediaItem(item.description, flag) } @@ -805,21 +864,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } result.sendResult(children as MutableList?) -// audiobookManager.audiobooks.forEach { -// var builder = MediaDescriptionCompat.Builder() -// .setMediaId(it.id) -// .setTitle(it.book.title) -// .setSubtitle(it.book.authorFL) -// .setMediaUri(null) -// .setIconUri(it.getCover(audiobookManager.serverUrl, audiobookManager.token)) -// -// -// -// var mediaDescription = builder.build() -// var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) -// mediaItems.add(newMediaItem) -// } - // Check if this is the root menu: if (AUTO_MEDIA_ROOT == parentMediaId) { // build the MediaItem objects for the top level, @@ -833,11 +877,53 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // result.sendResult(mediaItems) } + override fun onSearch(query: String, extras: Bundle?, result: Result>) { + val mediaItems: MutableList = mutableListOf() + + if (!audiobookManager.hasLoaded) { + result.detach() + audiobookManager.load() + audiobookManager.loadAudiobooks() { + audiobookManager.isLoading = false + + Log.d(tag, "LOADED AUDIOBOOKS") + browseTree = BrowseTree(this, audiobookManager.audiobooks, null) + val children = browseTree[ALL_ROOT]?.map { item -> + MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + } + if (children != null) { + Log.d(tag, "BROWSE TREE CHILDREN ${children.size}") + } + result.sendResult(children as MutableList?) + } + return + } else if (audiobookManager.isLoading) { + Log.d(tag, "AUDIOBOOKS LOADING") + result.detach() + return + } + + if (audiobookManager.audiobooks.size == 0) { + Log.d(tag, "AudiobookManager: Sending no items") + result.sendResult(mediaItems) + return + } + + + val children = browseTree[ALL_ROOT]?.map { item -> + MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + } + if (children != null) { + Log.d(tag, "NO CHILDREN ON SEARCH ${children.size}") + } + result.sendResult(children as MutableList?) + } + // // SLEEP TIMER STUFF // - fun setSleepTimer(time:Long, isChapterTime:Boolean) : Boolean { + fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean { Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime") sleepTimerTask?.cancel() sleepChapterTime = 0L @@ -852,7 +938,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } sleepChapterTime = time - sleepTimerTask = Timer("SleepTimer",false).schedule(0L, 1000L) { + sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) { Handler(Looper.getMainLooper()).post() { if (mPlayer.isPlaying && mPlayer.currentPosition > sleepChapterTime) { Log.d(tag, "Sleep Timer Pausing Player on Chapter") @@ -864,7 +950,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } } else { - sleepTimerTask = Timer("SleepTimer",false).schedule(time) { + sleepTimerTask = Timer("SleepTimer", false).schedule(time) { Log.d(tag, "Sleep Timer Done") Handler(Looper.getMainLooper()).post() { if (mPlayer.isPlaying) { diff --git a/android/app/src/main/res/xml/automotive_app_desc.xml b/android/app/src/main/res/xml/automotive_app_desc.xml index b4a189f6..e9cf4c14 100644 --- a/android/app/src/main/res/xml/automotive_app_desc.xml +++ b/android/app/src/main/res/xml/automotive_app_desc.xml @@ -1,3 +1,5 @@ + + diff --git a/assets/app.css b/assets/app.css index b3286f79..5184f350 100644 --- a/assets/app.css +++ b/assets/app.css @@ -18,4 +18,28 @@ .box-shadow-book { box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166; +} + +.bookshelfRow { + background-image: url(/wood_panels.jpg); +} +.bookshelfDivider { + background: rgb(149, 119, 90); + background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); + box-shadow: 2px 10px 8px #1111117e; +} + +/* +Bookshelf Label +*/ +.categoryPlacard { + background-image: url(https://image.freepik.com/free-photo/brown-wooden-textured-flooring-background_53876-128537.jpg); + letter-spacing: 1px; +} +.shinyBlack { + background-color: #2d3436; + background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%); + border-color: rgba(255, 244, 182, 0.6); + border-style: solid; + color: #fce3a6; } \ No newline at end of file diff --git a/assets/fonts.css b/assets/fonts.css index de51c84a..0096d9e2 100644 --- a/assets/fonts.css +++ b/assets/fonts.css @@ -1,12 +1,18 @@ -/* fallback */ + @font-face { font-family: 'Material Icons'; font-style: normal; font-weight: 400; - src: url(/material-icons.woff2) format('woff2'); + src: url(/fonts/MaterialIcons.woff2) format('woff2'); +} +@font-face { + font-family: 'Material Icons Outlined'; + font-style: normal; + font-weight: 400; + src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2'); } -.material-icons { +/* .material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; @@ -27,6 +33,9 @@ .material-icons.text-lg { font-size: 1.25rem; } +.material-icons.text-2xl { + font-size: 1.5rem; +} .material-icons.text-3xl { font-size: 1.875rem; } @@ -38,6 +47,42 @@ } .material-icons.text-base { font-size: 1rem; +} */ + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} +.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) { + font-size: 1.5rem; +} + +.material-icons-outlined { + font-family: 'Material Icons Outlined'; + font-weight: normal; + font-style: normal; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} +.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) { + font-size: 1.5rem; } @font-face { @@ -45,7 +90,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/GentiumBookBasic.woff2) format('woff2'); + src: url(/fonts/GentiumBookBasic.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -54,6 +99,6 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/GentiumBookBasic.woff2) format('woff2'); + src: url(/fonts/GentiumBookBasic.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } \ No newline at end of file diff --git a/components/app/Appbar.vue b/components/app/Appbar.vue index 471b29b2..f1ffef53 100644 --- a/components/app/Appbar.vue +++ b/components/app/Appbar.vue @@ -1,13 +1,13 @@ @@ -54,6 +49,9 @@ export default { } }, computed: { + socketConnected() { + return this.$store.state.socketConnected + }, currentLibrary() { return this.$store.getters['libraries/getCurrentLibrary'] }, @@ -61,7 +59,7 @@ export default { return this.currentLibrary ? this.currentLibrary.name : 'Main' }, showBack() { - return this.$route.name !== 'index' + return this.$route.name !== 'index' && !this.$route.name.startsWith('bookshelf') }, user() { return this.$store.state.user.user @@ -81,6 +79,9 @@ export default { } }, methods: { + clickShowSideDrawer() { + this.$store.commit('setShowSideDrawer', true) + }, clickShowLibraryModal() { this.$store.commit('libraries/setShowModal', true) }, @@ -88,7 +89,7 @@ export default { if (this.$route.name === 'audiobook-id-edit') { this.$router.push(`/audiobook/${this.$route.params.id}`) } else { - this.$router.push('/') + this.$router.push('/bookshelf') } }, logout() { diff --git a/components/app/SideDrawer.vue b/components/app/SideDrawer.vue new file mode 100644 index 00000000..1341411b --- /dev/null +++ b/components/app/SideDrawer.vue @@ -0,0 +1,116 @@ + + + \ No newline at end of file diff --git a/components/bookshelf/GroupShelf.vue b/components/bookshelf/GroupShelf.vue new file mode 100644 index 00000000..e0eeff5f --- /dev/null +++ b/components/bookshelf/GroupShelf.vue @@ -0,0 +1,35 @@ + + + diff --git a/components/bookshelf/LibraryShelf.vue b/components/bookshelf/LibraryShelf.vue new file mode 100644 index 00000000..807eda58 --- /dev/null +++ b/components/bookshelf/LibraryShelf.vue @@ -0,0 +1,28 @@ + + + diff --git a/components/bookshelf/Shelf.vue b/components/bookshelf/Shelf.vue new file mode 100644 index 00000000..d112903d --- /dev/null +++ b/components/bookshelf/Shelf.vue @@ -0,0 +1,34 @@ + + + diff --git a/components/cards/CollectionCard.vue b/components/cards/CollectionCard.vue new file mode 100644 index 00000000..8626d71d --- /dev/null +++ b/components/cards/CollectionCard.vue @@ -0,0 +1,86 @@ + + + \ No newline at end of file diff --git a/components/cards/CollectionCover.vue b/components/cards/CollectionCover.vue new file mode 100644 index 00000000..7ecb98cf --- /dev/null +++ b/components/cards/CollectionCover.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/components/cards/SeriesCard.vue b/components/cards/SeriesCard.vue new file mode 100644 index 00000000..020af63c --- /dev/null +++ b/components/cards/SeriesCard.vue @@ -0,0 +1,113 @@ + + + \ No newline at end of file diff --git a/components/cards/SeriesCover.vue b/components/cards/SeriesCover.vue new file mode 100644 index 00000000..f6277d27 --- /dev/null +++ b/components/cards/SeriesCover.vue @@ -0,0 +1,171 @@ + + + \ No newline at end of file diff --git a/components/home/BookshelfNavBar.vue b/components/home/BookshelfNavBar.vue new file mode 100644 index 00000000..54e1f94c --- /dev/null +++ b/components/home/BookshelfNavBar.vue @@ -0,0 +1,42 @@ + + + + + \ No newline at end of file diff --git a/components/home/BookshelfToolbar.vue b/components/home/BookshelfToolbar.vue new file mode 100644 index 00000000..66ae754a --- /dev/null +++ b/components/home/BookshelfToolbar.vue @@ -0,0 +1,138 @@ + + + + + \ No newline at end of file diff --git a/components/modals/OrderModal.vue b/components/modals/OrderModal.vue index c3a3c5f7..2d564eba 100644 --- a/components/modals/OrderModal.vue +++ b/components/modals/OrderModal.vue @@ -7,8 +7,8 @@
{{ item.text }}
- - {{ descending ? 'expand_more' : 'expand_less' }} + + {{ descending ? 'south' : 'north' }} diff --git a/components/tables/CollectionBooksTable.vue b/components/tables/CollectionBooksTable.vue new file mode 100644 index 00000000..0b123fa9 --- /dev/null +++ b/components/tables/CollectionBooksTable.vue @@ -0,0 +1,80 @@ + + + + + \ No newline at end of file diff --git a/components/tables/collection/BookTableRow.vue b/components/tables/collection/BookTableRow.vue new file mode 100644 index 00000000..ab83d5dd --- /dev/null +++ b/components/tables/collection/BookTableRow.vue @@ -0,0 +1,122 @@ + + + \ No newline at end of file diff --git a/layouts/default.vue b/layouts/default.vue index 9114d512..d4a0a99c 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -4,10 +4,10 @@
- - + + @@ -24,12 +24,24 @@ export default { data() { return {} }, + watch: { + networkConnected: { + handler(newVal) { + if (newVal) { + this.attemptConnection() + } + } + } + }, computed: { playerIsOpen() { return this.$store.getters['playerIsOpen'] }, routeName() { return this.$route.name + }, + networkConnected() { + return this.$store.state.networkConnected } }, methods: { @@ -152,9 +164,6 @@ export default { var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId) if (downloadObj) { - if (this.$refs.downloadsModal) { - this.$refs.downloadsModal.updateDownloadProgress({ audiobookId, progress }) - } this.$toast.update(downloadObj.toastId, { content: `${progress}% Downloading ${downloadObj.audiobook.book.title}` }) } }, @@ -331,20 +340,6 @@ export default { // this.checkLoadCurrent() // this.$store.dispatch('audiobooks/setNativeAudiobooks') // }, - async deleteDownload(download) { - console.log('Delete download', download.filename) - - if (this.$store.state.playingDownload && this.$store.state.playingDownload.id === download.id) { - console.warn('Deleting download when currently playing download - terminate play') - if (this.$refs.streamContainer) { - this.$refs.streamContainer.cancelStream() - } - } - if (download.contentUrl) { - await StorageManager.delete(download) - } - this.$store.commit('downloads/removeDownload', download) - }, async initMediaStore() { // Request and setup listeners for media files on native AudioDownloader.addListener('onDownloadComplete', (data) => { @@ -420,9 +415,33 @@ export default { }, showSuccessToast(message) { this.$toast.success(message) + }, + async attemptConnection() { + if (!this.$server) return + if (!this.networkConnected) { + console.warn('No network connection') + return + } + + var localServerUrl = await this.$localStore.getServerUrl() + var localUserToken = await this.$localStore.getToken() + if (localServerUrl) { + // Server and Token are stored + if (localUserToken) { + var isSocketAlreadyEstablished = this.$server.socket + var success = await this.$server.connect(localServerUrl, localUserToken) + if (!success && !this.$server.url) { + // Bad URL + } else if (!success) { + // Failed to connect + } else if (isSocketAlreadyEstablished) { + // No need to wait for connect event + } + } + } } }, - mounted() { + async mounted() { if (!this.$server) return console.error('No Server') // console.log(`Default Mounted set SOCKET listeners ${this.$server.connected}`) @@ -435,28 +454,33 @@ export default { if (this.$store.state.isFirstLoad) { this.$store.commit('setIsFirstLoad', false) - this.setupNetworkListener() + await this.setupNetworkListener() + this.attemptConnection() this.checkForUpdate() this.initMediaStore() } - MyNativeAudio.addListener('onPrepareMedia', (data) => { - var audiobookId = data.audiobookId - var playWhenReady = data.playWhenReady + if (!this.$server.connected) { + } - var audiobook = this.$store.getters['audiobooks/getAudiobook'](audiobookId) + // Old bad attempt at AA + // MyNativeAudio.addListener('onPrepareMedia', (data) => { + // var audiobookId = data.audiobookId + // var playWhenReady = data.playWhenReady - var download = this.$store.getters['downloads/getDownloadIfReady'](audiobookId) - this.$store.commit('setPlayOnLoad', playWhenReady) - if (!download) { - // Stream - this.$store.commit('setStreamAudiobook', audiobook) - this.$server.socket.emit('open_stream', audiobook.id) - } else { - // Local - this.$store.commit('setPlayingDownload', download) - } - }) + // var audiobook = this.$store.getters['audiobooks/getAudiobook'](audiobookId) + + // var download = this.$store.getters['downloads/getDownloadIfReady'](audiobookId) + // this.$store.commit('setPlayOnLoad', playWhenReady) + // if (!download) { + // // Stream + // this.$store.commit('setStreamAudiobook', audiobook) + // this.$server.socket.emit('open_stream', audiobook.id) + // } else { + // // Local + // this.$store.commit('setPlayingDownload', download) + // } + // }) }, beforeDestroy() { if (!this.$server) { diff --git a/package.json b/package.json index 2f2e5666..a97bc131 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-app", - "version": "v0.9.19-beta", + "version": "v0.9.20-beta", "author": "advplyr", "scripts": { "dev": "nuxt --hostname localhost --port 1337", diff --git a/pages/account.vue b/pages/account.vue index d1a08d46..e026f5d3 100644 --- a/pages/account.vue +++ b/pages/account.vue @@ -26,7 +26,7 @@ Open app store -

UA: {{ updateAvailability }} | Avail: {{ availableVersion }} | Curr: {{ currentVersion }} | ImmedAllowed: {{ immediateUpdateAllowed }}

+

UA: {{ updateAvailability }} | Avail: {{ availableVersion }} | Curr: {{ currentVersion }} | ImmedAllowed: {{ immediateUpdateAllowed }}

@@ -34,11 +34,18 @@ import { AppUpdate } from '@robingenz/capacitor-app-update' export default { + asyncData({ redirect, store }) { + if (!store.state.socketConnected) { + return redirect('/connect') + } + return {} + }, data() { return {} }, computed: { username() { + if (!this.user) return '' return this.user.username }, user() { diff --git a/pages/bookshelf.vue b/pages/bookshelf.vue new file mode 100644 index 00000000..f2346c52 --- /dev/null +++ b/pages/bookshelf.vue @@ -0,0 +1,102 @@ + + + + + \ No newline at end of file diff --git a/pages/bookshelf/collections.vue b/pages/bookshelf/collections.vue new file mode 100644 index 00000000..817a8f46 --- /dev/null +++ b/pages/bookshelf/collections.vue @@ -0,0 +1,51 @@ + + + \ No newline at end of file diff --git a/pages/bookshelf/index.vue b/pages/bookshelf/index.vue new file mode 100644 index 00000000..a5e028c8 --- /dev/null +++ b/pages/bookshelf/index.vue @@ -0,0 +1,93 @@ + + + \ No newline at end of file diff --git a/pages/bookshelf/library.vue b/pages/bookshelf/library.vue new file mode 100644 index 00000000..4887a016 --- /dev/null +++ b/pages/bookshelf/library.vue @@ -0,0 +1,48 @@ + + + \ No newline at end of file diff --git a/pages/bookshelf/series.vue b/pages/bookshelf/series.vue new file mode 100644 index 00000000..d747a956 --- /dev/null +++ b/pages/bookshelf/series.vue @@ -0,0 +1,104 @@ + + + \ No newline at end of file diff --git a/pages/collection/_id.vue b/pages/collection/_id.vue new file mode 100644 index 00000000..f63ebdee --- /dev/null +++ b/pages/collection/_id.vue @@ -0,0 +1,117 @@ + + + \ No newline at end of file diff --git a/pages/config.vue b/pages/config.vue new file mode 100644 index 00000000..4607a27b --- /dev/null +++ b/pages/config.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/pages/connect.vue b/pages/connect.vue index f592f95f..a1d30109 100644 --- a/pages/connect.vue +++ b/pages/connect.vue @@ -133,7 +133,7 @@ export default { if (this.$route.query && this.$route.query.redirect) { this.$router.replace(this.$route.query.redirect) } else { - this.$router.replace('/') + this.$router.replace('/bookshelf') } }, socketConnected() { diff --git a/pages/downloads.vue b/pages/downloads.vue new file mode 100644 index 00000000..a70842a4 --- /dev/null +++ b/pages/downloads.vue @@ -0,0 +1,239 @@ + + + \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue index a59e5fd7..63f010bf 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -27,6 +27,9 @@