mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-28 13:58:23 +02:00
Merge branch 'master' into jramer/master
This commit is contained in:
commit
018a988124
43 changed files with 19408 additions and 198 deletions
81
.github/testing-page-template.html
vendored
Normal file
81
.github/testing-page-template.html
vendored
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=en>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv=content-type content="text/html; charset=utf-8" />
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
|
<title>Audiobookshelf :: Testers</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: rgb(55, 56, 56);
|
||||||
|
font-family: Sans;
|
||||||
|
margin: 40px auto;
|
||||||
|
max-width: 500px;
|
||||||
|
color: #ddd;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#apk {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: #222;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<a href="https://audiobookshelf.org">
|
||||||
|
<img src=logo.png />
|
||||||
|
</a>
|
||||||
|
<h1>Audiobookshelf :: Testers</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Here you can download the latest version of Audiobookshelf for Android for <b>testing</b>.
|
||||||
|
The app is automatically built for every new patch added to the project.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id=apk>
|
||||||
|
<div>Built on</div>
|
||||||
|
<div>__DATE__</div>
|
||||||
|
|
||||||
|
<div>Git commit</div>
|
||||||
|
<a href="https://github.com/advplyr/audiobookshelf-app/commit/__COMMIT__">__COMMIT__</a>
|
||||||
|
|
||||||
|
<div>Download</div>
|
||||||
|
<a href="__APK__">__APK__</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Using this requires some technical knowledge to install the APK manually.
|
||||||
|
Regular users should use the releases published on the <a href="https://play.google.com/store/apps/details?id=com.audiobookshelf.app">Play Store</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<p>Report bugs, request features, provide feedback, and contribute on <a href="https://github.com/advplyr/audiobookshelf-app/issues">GitHub</a></p>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/advplyr/audiobookshelf-app/issues">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width=32 height=32 viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
47
.github/workflows/build-apk.yml
vendored
Normal file
47
.github/workflows/build-apk.yml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
name: Build APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- 'ios/**'
|
||||||
|
- 'readme.md'
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- 'ios/**'
|
||||||
|
- 'readme.md'
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: use Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: build Nuxt project
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: copy to Android project
|
||||||
|
run: npx cap sync
|
||||||
|
|
||||||
|
- name: build Android app
|
||||||
|
run: ./android/gradlew assembleDebug -p android --no-daemon
|
||||||
|
|
||||||
|
- name: rename apk
|
||||||
|
working-directory: android/app/build/outputs/apk/debug/
|
||||||
|
run: |
|
||||||
|
build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)"
|
||||||
|
name="audiobookshelf-${build}.apk"
|
||||||
|
mv -v app-debug.apk "${name}"
|
||||||
|
|
||||||
|
- name: upload app
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: audiobookshelf-apk
|
||||||
|
path: android/app/build/outputs/apk/debug/*.apk
|
76
.github/workflows/deploy-apk.yml
vendored
Normal file
76
.github/workflows/deploy-apk.yml
vendored
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
name: Publish Test App
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- 'ios/**'
|
||||||
|
- 'readme.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: use Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: build Nuxt project
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: copy to Android project
|
||||||
|
run: npx cap sync
|
||||||
|
|
||||||
|
- name: build Android app
|
||||||
|
run: ./android/gradlew assembleDebug -p android --no-daemon
|
||||||
|
|
||||||
|
- name: rename apk
|
||||||
|
working-directory: android/app/build/outputs/apk/debug/
|
||||||
|
run: |
|
||||||
|
build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)"
|
||||||
|
name="audiobookshelf-${build}.apk"
|
||||||
|
mv -v app-debug.apk "${name}"
|
||||||
|
|
||||||
|
- name: prepare test page ressources
|
||||||
|
run: |
|
||||||
|
mkdir ghpages
|
||||||
|
cp android/app/build/outputs/apk/debug/*apk ghpages/
|
||||||
|
cp static/Logo.png ghpages/logo.png
|
||||||
|
cp .github/testing-page-template.html ghpages/index.html
|
||||||
|
|
||||||
|
- name: build test page
|
||||||
|
working-directory: ghpages
|
||||||
|
run: |
|
||||||
|
sed -i "s/__DATE__/$(date)/g" index.html
|
||||||
|
sed -i "s/__COMMIT__/$(git rev-parse --short HEAD)/g" index.html
|
||||||
|
sed -i "s/__APK__/$(ls *apk)/g" index.html
|
||||||
|
|
||||||
|
- name: upload test page artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: ./ghpages
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v1
|
|
@ -51,7 +51,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
var mediaItemsUpToDate = 0
|
var mediaItemsUpToDate = 0
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
val foldersFound = df.search(false, DocumentFileType.FOLDER)
|
val foldersFound = df.search(true, DocumentFileType.FOLDER)
|
||||||
|
|
||||||
// Match folders found with local library items already saved in db
|
// Match folders found with local library items already saved in db
|
||||||
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
||||||
|
@ -72,11 +72,16 @@ class FolderScanner(var ctx: Context) {
|
||||||
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
||||||
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
|
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
|
||||||
|
|
||||||
when (scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)) {
|
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
||||||
ItemScanResult.REMOVED -> mediaItemsRemoved++
|
|
||||||
ItemScanResult.UPDATED -> mediaItemsUpdated++
|
// Do not scan folders that have no media items and not an existing item already
|
||||||
ItemScanResult.ADDED -> mediaItemsAdded++
|
if (existingItem != null || filesInFolder.isNotEmpty()) {
|
||||||
else -> mediaItemsUpToDate++
|
when (scanLibraryItemFolder(itemFolder, filesInFolder, localFolder, existingItem, forceAudioProbe)) {
|
||||||
|
ItemScanResult.REMOVED -> mediaItemsRemoved++
|
||||||
|
ItemScanResult.UPDATED -> mediaItemsUpdated++
|
||||||
|
ItemScanResult.ADDED -> mediaItemsAdded++
|
||||||
|
else -> mediaItemsUpToDate++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +96,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
private fun scanLibraryItemFolder(itemFolder:DocumentFile, filesInFolder:List<DocumentFile>, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
||||||
val itemFolderName = itemFolder.name ?: ""
|
val itemFolderName = itemFolder.name ?: ""
|
||||||
val itemId = getLocalLibraryItemId(itemFolder.id)
|
val itemId = getLocalLibraryItemId(itemFolder.id)
|
||||||
|
|
||||||
|
@ -106,8 +111,6 @@ class FolderScanner(var ctx: Context) {
|
||||||
var coverContentUrl:String? = null
|
var coverContentUrl:String? = null
|
||||||
var coverAbsolutePath:String? = null
|
var coverAbsolutePath:String? = null
|
||||||
|
|
||||||
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
|
||||||
|
|
||||||
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
||||||
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,11 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
||||||
val tag = "DownloadItemManager"
|
val tag = "DownloadItemManager"
|
||||||
private val maxSimultaneousDownloads = 1
|
private val maxSimultaneousDownloads = 3
|
||||||
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
enum class DownloadCheckStatus {
|
enum class DownloadCheckStatus {
|
||||||
|
@ -188,7 +189,8 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
||||||
|
|
||||||
// Rename to fix appended .mp3 on m4b/m4a files
|
// Rename to fix appended .mp3 on m4b/m4a files
|
||||||
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
|
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
|
||||||
if (resultDocFile.name?.endsWith(".m4b.mp3") == true || resultDocFile.name?.endsWith(".m4a.mp3") == true) {
|
val docNameLowerCase = resultDocFile.name?.lowercase(Locale.getDefault()) ?: ""
|
||||||
|
if (docNameLowerCase.endsWith(".m4b.mp3")|| docNameLowerCase.endsWith(".m4a.mp3")) {
|
||||||
resultDocFile.renameTo(downloadItemPart.filename)
|
resultDocFile.renameTo(downloadItemPart.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ data class DownloadItem(
|
||||||
for (it in downloadItemParts) {
|
for (it in downloadItemParts) {
|
||||||
if (!it.completed && it.downloadId == null) {
|
if (!it.completed && it.downloadId == null) {
|
||||||
itemParts.add(it)
|
itemParts.add(it)
|
||||||
if (itemParts.size > limit) break
|
if (itemParts.size >= limit) break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
0
android/gradlew
vendored
Normal file → Executable file
0
android/gradlew
vendored
Normal file → Executable file
|
@ -48,25 +48,6 @@
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
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 */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<div v-if="user && currentLibrary">
|
<div v-if="user && currentLibrary">
|
||||||
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
||||||
<p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
<p class="text-smleading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,9 @@ export default {
|
||||||
bookmarks() {
|
bookmarks() {
|
||||||
if (!this.serverLibraryItemId) return []
|
if (!this.serverLibraryItemId) return []
|
||||||
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
||||||
|
},
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -188,6 +191,7 @@ export default {
|
||||||
const libraryItemId = payload.libraryItemId
|
const libraryItemId = payload.libraryItemId
|
||||||
const episodeId = payload.episodeId
|
const episodeId = payload.episodeId
|
||||||
const startTime = payload.startTime
|
const startTime = payload.startTime
|
||||||
|
const startWhenReady = !payload.paused
|
||||||
|
|
||||||
// When playing local library item and can also play this item from the server
|
// When playing local library item and can also play this item from the server
|
||||||
// then store the server library item id so it can be used if a cast is made
|
// then store the server library item id so it can be used if a cast is made
|
||||||
|
@ -223,7 +227,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Called playLibraryItem', libraryItemId)
|
console.log('Called playLibraryItem', libraryItemId)
|
||||||
const preparePayload = { libraryItemId, episodeId, playWhenReady: true, playbackRate }
|
const preparePayload = { libraryItemId, episodeId, playWhenReady: startWhenReady, playbackRate }
|
||||||
if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime
|
if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime
|
||||||
AbsAudioPlayer.prepareLibraryItem(preparePayload)
|
AbsAudioPlayer.prepareLibraryItem(preparePayload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
@ -268,9 +272,13 @@ export default {
|
||||||
this.notifyOnReady()
|
this.notifyOnReady()
|
||||||
},
|
},
|
||||||
notifyOnReady() {
|
notifyOnReady() {
|
||||||
|
// TODO: iOS opens last active playback session on app launch. Should be consistent with Android
|
||||||
|
if (!this.isIos) return
|
||||||
|
|
||||||
// If settings aren't loaded yet, native player will receive incorrect settings
|
// If settings aren't loaded yet, native player will receive incorrect settings
|
||||||
console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady)
|
console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady)
|
||||||
if (this.settingsLoaded && this.isReady) {
|
if (this.settingsLoaded && this.isReady && this.$store.state.isFirstAudioLoad) {
|
||||||
|
this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch
|
||||||
AbsAudioPlayer.onReady()
|
AbsAudioPlayer.onReady()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!altViewEnabled" class="absolute text-center categoryPlacard font-book transform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px">
|
<div v-if="!altViewEnabled" class="absolute text-center categoryPlacardtransform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px">
|
||||||
<div class="w-full h-full flex items-center justify-center rounded-sm border shinyBlack">
|
<div class="w-full h-full flex items-center justify-center rounded-sm border shinyBlack">
|
||||||
<p class="transform text-xs">{{ label }}</p>
|
<p class="transform text-xs">{{ label }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||||
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||||
|
@ -26,11 +26,11 @@
|
||||||
<!-- Placeholder Cover Title & Author -->
|
<!-- Placeholder Cover Title & Author -->
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(240, width) + 'px' }">
|
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-mdtext-center" :style="{ width: Math.min(240, width) + 'px' }">
|
||||||
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
||||||
</div>
|
</div>
|
||||||
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-mdtext-center" :style="{ width: Math.min(160, width) + 'px' }">
|
||||||
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<div v-if="isAltViewEnabled && isCategorized" class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-if="isAltViewEnabled && isCategorized" class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(240, width) + 'px' }">
|
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-mdtext-center" :style="{ width: Math.min(240, width) + 'px' }">
|
||||||
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: height + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
<div class="relative rounded-sm overflow-hidden" :style="{ height: height + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
||||||
<div class="w-full h-full relative bg-bg">
|
<div class="w-full h-full relative" :class="{ 'bg-bg': !noBg }">
|
||||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && hasCover ? 'object-contain' : 'object-fill'" />
|
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="(showCoverBg && hasCover) || noBg ? 'object-contain' : 'object-fill'" />
|
||||||
|
|
||||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,17 +18,17 @@
|
||||||
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
||||||
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
<p class="text-centertext-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book truncate leading-none origin-center" style="color: rgb(247 223 187); font-size: 0.8rem" :style="{ transform: `scale(${sizeMultiplier})` }">{{ titleCleaned }}</p>
|
<p class="text-centertruncate leading-none origin-center" style="color: rgb(247 223 187); font-size: 0.8rem" :style="{ transform: `scale(${sizeMultiplier})` }">{{ titleCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
<p class="text-center font-book truncate leading-none origin-center" style="color: rgb(247 223 187); opacity: 0.75; font-size: 0.6rem" :style="{ transform: `scale(${sizeMultiplier})` }">{{ authorCleaned }}</p>
|
<p class="text-centertruncate leading-none origin-center" style="color: rgb(247 223 187); opacity: 0.75; font-size: 0.6rem" :style="{ transform: `scale(${sizeMultiplier})` }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -48,7 +48,8 @@ export default {
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio: Number,
|
bookCoverAspectRatio: Number,
|
||||||
downloadCover: String,
|
downloadCover: String,
|
||||||
raw: Boolean
|
raw: Boolean,
|
||||||
|
noBg: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -156,7 +157,7 @@ export default {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.imageReady = true
|
this.imageReady = true
|
||||||
})
|
})
|
||||||
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
|
if (!this.noBg && this.$refs.cover && this.cover !== this.placeholderUrl) {
|
||||||
var { naturalWidth, naturalHeight } = this.$refs.cover
|
var { naturalWidth, naturalHeight } = this.$refs.cover
|
||||||
var aspectRatio = naturalHeight / naturalWidth
|
var aspectRatio = naturalHeight / naturalWidth
|
||||||
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
|
||||||
<p class="font-book text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
<p class="text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -134,7 +134,7 @@ export default {
|
||||||
|
|
||||||
var innerP = document.createElement('p')
|
var innerP = document.createElement('p')
|
||||||
innerP.textContent = this.name
|
innerP.textContent = this.name
|
||||||
innerP.className = 'text-sm font-book text-white'
|
innerP.className = 'text-smtext-white'
|
||||||
imgdiv.appendChild(innerP)
|
imgdiv.appendChild(innerP)
|
||||||
|
|
||||||
return imgdiv
|
return imgdiv
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
||||||
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
<p class="text-centertext-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<nuxt-link to="/bookshelf/series" v-if="selectedSeriesName" class="pt-1">
|
<nuxt-link to="/bookshelf/series" v-if="selectedSeriesName" class="pt-1">
|
||||||
<span class="material-icons">arrow_back</span>
|
<span class="material-icons">arrow_back</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
|
<p v-show="!selectedSeriesName" class="pt-1">{{ totalEntities }} {{ entityTitle }}</p>
|
||||||
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
|
<p v-show="selectedSeriesName" class="ml-2pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="changeView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
|
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="changeView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
|
||||||
<template v-if="page === 'library'">
|
<template v-if="page === 'library'">
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-2" style="max-height: 75%" @click.stop>
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<slot :name="item.value" :item="item" :selected="item.value === selected">
|
<slot :name="item.value" :item="item" :selected="item.value === selected">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-96 my-6 mx-auto">
|
<div class="w-96 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
|
<h1 class="text-2xl mb-4">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
|
||||||
<div class="relative w-96 h-72">
|
<div class="relative w-96 h-72">
|
||||||
<div class="absolute top-0 left-0">
|
<div class="absolute top-0 left-0">
|
||||||
<template v-for="lbl in yAxisLabels">
|
<template v-for="lbl in yAxisLabels">
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<div class="absolute -bottom-2 left-0 flex ml-6">
|
<div class="absolute -bottom-2 left-0 flex ml-6">
|
||||||
<template v-for="dayObj in last7Days">
|
<template v-for="dayObj in last7Days">
|
||||||
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
||||||
<p class="text-sm font-book">{{ dayObj.dayOfWeek.slice(0, 3) }}</p>
|
<p class="text-sm">{{ dayObj.dayOfWeek.slice(0, 3) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,7 +44,6 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickedIt() {
|
clickedIt() {
|
||||||
if (this.isIos) return // TODO: Implement on iOS
|
|
||||||
this.$router.push('/downloading')
|
this.$router.push('/downloading')
|
||||||
},
|
},
|
||||||
onItemDownloadComplete(data) {
|
onItemDownloadComplete(data) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
|
|
||||||
let configuration = Realm.Configuration(
|
let configuration = Realm.Configuration(
|
||||||
schemaVersion: 6,
|
schemaVersion: 7,
|
||||||
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
||||||
if (oldSchemaVersion < 1) {
|
if (oldSchemaVersion < 1) {
|
||||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
|
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
|
||||||
|
|
|
@ -53,6 +53,11 @@
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
|
@ -33,6 +33,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
||||||
let realm = try Realm()
|
let realm = try Realm()
|
||||||
try realm.write {
|
try realm.write {
|
||||||
|
downloadItemPart.bytesDownloaded = downloadItemPart.fileSize
|
||||||
downloadItemPart.progress = 100
|
downloadItemPart.progress = 100
|
||||||
downloadItemPart.completed = true
|
downloadItemPart.completed = true
|
||||||
}
|
}
|
||||||
|
@ -72,9 +73,11 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
||||||
// Calculate the download percentage
|
// Calculate the download percentage
|
||||||
let percentDownloaded = (Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100
|
let percentDownloaded = (Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100
|
||||||
|
|
||||||
// Only update the progress if we received accurate progress data
|
// Only update the progress if we received accurate progress data
|
||||||
if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 {
|
if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 {
|
||||||
try Realm().write {
|
try Realm().write {
|
||||||
|
downloadItemPart.bytesDownloaded = Double(totalBytesWritten)
|
||||||
downloadItemPart.progress = percentDownloaded
|
downloadItemPart.progress = percentDownloaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,6 +112,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
// Call the progress handler
|
// Call the progress handler
|
||||||
do {
|
do {
|
||||||
try progressHandler(downloadItem, part)
|
try progressHandler(downloadItem, part)
|
||||||
|
try? self.notifyListeners("onDownloadItemPartUpdate", data: part.asDictionary())
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Error while processing progress")
|
logger.error("Error while processing progress")
|
||||||
debugPrint(error)
|
debugPrint(error)
|
||||||
|
@ -158,10 +162,9 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit status for active downloads
|
// Check for items done downloading
|
||||||
if let activeDownloads = fetchActiveDownloads() {
|
if let activeDownloads = fetchActiveDownloads() {
|
||||||
for item in activeDownloads.values {
|
for item in activeDownloads.values {
|
||||||
try? self.notifyListeners("onItemDownloadUpdate", data: item.asDictionary())
|
|
||||||
if item.isDoneDownloading() { handleDoneDownloadItem(item) }
|
if item.isDoneDownloading() { handleDoneDownloadItem(item) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,19 +280,22 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
let downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!)
|
let downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!)
|
||||||
var tasks = [DownloadItemPartTask]()
|
var tasks = [DownloadItemPartTask]()
|
||||||
for (i, track) in tracks.enumerated() {
|
for (i, track) in tracks.enumerated() {
|
||||||
let task = try startLibraryItemTrackDownload(item: item, position: i, track: track, episode: episode)
|
let task = try startLibraryItemTrackDownload(downloadItemId: downloadItem.id!, item: item, position: i, track: track, episode: episode)
|
||||||
downloadItem.downloadItemParts.append(task.part)
|
downloadItem.downloadItemParts.append(task.part)
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also download the cover
|
// Also download the cover
|
||||||
if item.media?.coverPath != nil && !(item.media?.coverPath!.isEmpty ?? true) {
|
if item.media?.coverPath != nil && !(item.media?.coverPath!.isEmpty ?? true) {
|
||||||
if let task = try? startLibraryItemCoverDownload(item: item) {
|
if let task = try? startLibraryItemCoverDownload(downloadItemId: downloadItem.id!, item: item) {
|
||||||
downloadItem.downloadItemParts.append(task.part)
|
downloadItem.downloadItemParts.append(task.part)
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify client of download item
|
||||||
|
try? self.notifyListeners("onDownloadItem", data: downloadItem.asDictionary())
|
||||||
|
|
||||||
// Persist in the database before status start coming in
|
// Persist in the database before status start coming in
|
||||||
try Database.shared.saveDownloadItem(downloadItem)
|
try Database.shared.saveDownloadItem(downloadItem)
|
||||||
|
|
||||||
|
@ -299,7 +305,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
|
private func startLibraryItemTrackDownload(downloadItemId: String, item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
|
||||||
logger.log("TRACK \(track.contentUrl!)")
|
logger.log("TRACK \(track.contentUrl!)")
|
||||||
|
|
||||||
// If we don't name metadata, then we can't proceed
|
// If we don't name metadata, then we can't proceed
|
||||||
|
@ -312,7 +318,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
let localUrl = "\(itemDirectory)/\(filename)"
|
let localUrl = "\(itemDirectory)/\(filename)"
|
||||||
|
|
||||||
let task = session.downloadTask(with: serverUrl)
|
let task = session.downloadTask(with: serverUrl)
|
||||||
let part = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode)
|
let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode, size: track.metadata?.size ?? 0)
|
||||||
|
|
||||||
// Store the id on the task so the download item can be pulled from the database later
|
// Store the id on the task so the download item can be pulled from the database later
|
||||||
task.taskDescription = part.id
|
task.taskDescription = part.id
|
||||||
|
@ -320,13 +326,18 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
return DownloadItemPartTask(part: part, task: task)
|
return DownloadItemPartTask(part: part, task: task)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startLibraryItemCoverDownload(item: LibraryItem) throws -> DownloadItemPartTask {
|
private func startLibraryItemCoverDownload(downloadItemId: String, item: LibraryItem) throws -> DownloadItemPartTask {
|
||||||
let filename = "cover.jpg"
|
let filename = "cover.jpg"
|
||||||
let serverPath = "/api/items/\(item.id)/cover"
|
let serverPath = "/api/items/\(item.id)/cover"
|
||||||
let itemDirectory = try createLibraryItemFileDirectory(item: item)
|
let itemDirectory = try createLibraryItemFileDirectory(item: item)
|
||||||
let localUrl = "\(itemDirectory)/\(filename)"
|
let localUrl = "\(itemDirectory)/\(filename)"
|
||||||
|
|
||||||
let part = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil)
|
// Find library file to get cover size
|
||||||
|
let coverLibraryFile = item.libraryFiles.first(where: {
|
||||||
|
$0.metadata?.path == item.media?.coverPath
|
||||||
|
})
|
||||||
|
|
||||||
|
let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil, size: coverLibraryFile?.metadata?.size ?? 0)
|
||||||
let task = session.downloadTask(with: part.downloadURL!)
|
let task = session.downloadTask(with: part.downloadURL!)
|
||||||
|
|
||||||
// Store the id on the task so the download item can be pulled from the database later
|
// Store the id on the task so the download item can be pulled from the database later
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Foundation
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
|
|
||||||
class DownloadItem: Object, Codable {
|
class DownloadItem: Object, Codable {
|
||||||
@Persisted(primaryKey: true) var id: String?
|
@Persisted(primaryKey: true) var id:String?
|
||||||
@Persisted(indexed: true) var libraryItemId: String?
|
@Persisted(indexed: true) var libraryItemId: String?
|
||||||
@Persisted var episodeId: String?
|
@Persisted var episodeId: String?
|
||||||
@Persisted var userMediaProgress: MediaProgress?
|
@Persisted var userMediaProgress: MediaProgress?
|
||||||
|
|
|
@ -10,7 +10,9 @@ import RealmSwift
|
||||||
|
|
||||||
class DownloadItemPart: Object, Codable {
|
class DownloadItemPart: Object, Codable {
|
||||||
@Persisted(primaryKey: true) var id = ""
|
@Persisted(primaryKey: true) var id = ""
|
||||||
|
@Persisted var downloadItemId: String?
|
||||||
@Persisted var filename: String?
|
@Persisted var filename: String?
|
||||||
|
@Persisted var fileSize: Double = 0
|
||||||
@Persisted var itemTitle: String?
|
@Persisted var itemTitle: String?
|
||||||
@Persisted var serverPath: String?
|
@Persisted var serverPath: String?
|
||||||
@Persisted var audioTrack: AudioTrack?
|
@Persisted var audioTrack: AudioTrack?
|
||||||
|
@ -21,9 +23,10 @@ class DownloadItemPart: Object, Codable {
|
||||||
@Persisted var uri: String?
|
@Persisted var uri: String?
|
||||||
@Persisted var destinationUri: String?
|
@Persisted var destinationUri: String?
|
||||||
@Persisted var progress: Double = 0
|
@Persisted var progress: Double = 0
|
||||||
|
@Persisted var bytesDownloaded: Double = 0
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case id, filename, itemTitle, completed, moved, failed, progress
|
case id, downloadItemId, filename, fileSize, itemTitle, completed, moved, failed, progress, bytesDownloaded
|
||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
|
@ -33,32 +36,40 @@ class DownloadItemPart: Object, Codable {
|
||||||
required init(from decoder: Decoder) throws {
|
required init(from decoder: Decoder) throws {
|
||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
id = try values.decode(String.self, forKey: .id)
|
id = try values.decode(String.self, forKey: .id)
|
||||||
|
downloadItemId = try? values.decode(String.self, forKey: .downloadItemId)
|
||||||
filename = try? values.decode(String.self, forKey: .filename)
|
filename = try? values.decode(String.self, forKey: .filename)
|
||||||
|
fileSize = try values.decode(Double.self, forKey: .fileSize)
|
||||||
itemTitle = try? values.decode(String.self, forKey: .itemTitle)
|
itemTitle = try? values.decode(String.self, forKey: .itemTitle)
|
||||||
completed = try values.decode(Bool.self, forKey: .completed)
|
completed = try values.decode(Bool.self, forKey: .completed)
|
||||||
moved = try values.decode(Bool.self, forKey: .moved)
|
moved = try values.decode(Bool.self, forKey: .moved)
|
||||||
failed = try values.decode(Bool.self, forKey: .failed)
|
failed = try values.decode(Bool.self, forKey: .failed)
|
||||||
progress = try values.decode(Double.self, forKey: .progress)
|
progress = try values.decode(Double.self, forKey: .progress)
|
||||||
|
bytesDownloaded = try values.decode(Double.self, forKey: .bytesDownloaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encode(id, forKey: .id)
|
try container.encode(id, forKey: .id)
|
||||||
|
try container.encode(downloadItemId, forKey: .downloadItemId)
|
||||||
try container.encode(filename, forKey: .filename)
|
try container.encode(filename, forKey: .filename)
|
||||||
|
try container.encode(fileSize, forKey: .fileSize)
|
||||||
try container.encode(itemTitle, forKey: .itemTitle)
|
try container.encode(itemTitle, forKey: .itemTitle)
|
||||||
try container.encode(completed, forKey: .completed)
|
try container.encode(completed, forKey: .completed)
|
||||||
try container.encode(moved, forKey: .moved)
|
try container.encode(moved, forKey: .moved)
|
||||||
try container.encode(failed, forKey: .failed)
|
try container.encode(failed, forKey: .failed)
|
||||||
try container.encode(progress, forKey: .progress)
|
try container.encode(progress, forKey: .progress)
|
||||||
|
try container.encode(bytesDownloaded, forKey: .bytesDownloaded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DownloadItemPart {
|
extension DownloadItemPart {
|
||||||
convenience init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) {
|
convenience init(downloadItemId: String, filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?, size: Double) {
|
||||||
self.init()
|
self.init()
|
||||||
|
|
||||||
self.id = destination.toBase64()
|
self.id = destination.toBase64()
|
||||||
|
self.downloadItemId = downloadItemId
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
self.fileSize = size
|
||||||
self.itemTitle = itemTitle
|
self.itemTitle = itemTitle
|
||||||
self.serverPath = serverPath
|
self.serverPath = serverPath
|
||||||
self.audioTrack = AudioTrack.detachCopy(of: audioTrack)
|
self.audioTrack = AudioTrack.detachCopy(of: audioTrack)
|
||||||
|
|
|
@ -13,9 +13,10 @@ class FileMetadata: EmbeddedObject, Codable {
|
||||||
@Persisted var ext: String = ""
|
@Persisted var ext: String = ""
|
||||||
@Persisted var path: String = ""
|
@Persisted var path: String = ""
|
||||||
@Persisted var relPath: String = ""
|
@Persisted var relPath: String = ""
|
||||||
|
@Persisted var size:Double = 0
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case filename, ext, path, relPath
|
case filename, ext, path, relPath, size
|
||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
|
@ -29,6 +30,7 @@ class FileMetadata: EmbeddedObject, Codable {
|
||||||
ext = try values.decode(String.self, forKey: .ext)
|
ext = try values.decode(String.self, forKey: .ext)
|
||||||
path = try values.decode(String.self, forKey: .path)
|
path = try values.decode(String.self, forKey: .path)
|
||||||
relPath = try values.decode(String.self, forKey: .relPath)
|
relPath = try values.decode(String.self, forKey: .relPath)
|
||||||
|
size = try values.decode(Double.self, forKey: .size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
|
@ -37,5 +39,6 @@ class FileMetadata: EmbeddedObject, Codable {
|
||||||
try container.encode(ext, forKey: .ext)
|
try container.encode(ext, forKey: .ext)
|
||||||
try container.encode(path, forKey: .path)
|
try container.encode(path, forKey: .path)
|
||||||
try container.encode(relPath, forKey: .relPath)
|
try container.encode(relPath, forKey: .relPath)
|
||||||
|
try container.encode(size, forKey: .size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
<app-appbar />
|
<app-appbar />
|
||||||
<div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''">
|
<div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''">
|
||||||
<Nuxt />
|
<Nuxt />
|
||||||
|
|
||||||
|
<div v-if="attemptingConnection" class="absolute top-0 left-0 z-50 w-full h-full flex items-center justify-center">
|
||||||
|
<ui-loading-indicator text="Connecting to server..." />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-audio-player-container ref="streamContainer" />
|
<app-audio-player-container ref="streamContainer" />
|
||||||
<modals-libraries-modal />
|
<modals-libraries-modal />
|
||||||
|
|
19051
package-lock.json
generated
19051
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full p-4">
|
<div class="w-full h-full p-4">
|
||||||
<ui-text-input-with-label :value="serverConnConfigName" label="Connection Config Name" disabled class="my-2" />
|
<ui-text-input-with-label :value="serverAddress" label="Host" disabled class="my-2" />
|
||||||
|
|
||||||
<ui-text-input-with-label :value="username" label="Username" disabled class="my-2" />
|
<ui-text-input-with-label :value="username" label="Username" disabled class="my-2" />
|
||||||
|
|
||||||
<ui-btn color="primary flex items-center justify-between gap-2 ml-auto text-base mt-8" @click="logout">Logout<span class="material-icons" style="font-size: 1.1rem">logout</span></ui-btn>
|
<ui-btn color="primary flex items-center justify-between gap-2 ml-auto text-base mt-8" @click="logout">Logout<span class="material-icons" style="font-size: 1.1rem">logout</span></ui-btn>
|
||||||
|
|
||||||
<div class="flex justify-end items-center m-4 gap-3 right-0 bottom-0 absolute">
|
<div class="flex justify-end items-center m-4 gap-3 right-0 bottom-0 absolute">
|
||||||
<p class="text-sm font-book text-yellow-400 text-right">Report bugs, request features, provide feedback, and contribute on <a class="underline" href="https://github.com/advplyr/audiobookshelf-app" target="_blank">GitHub</a></p>
|
<p class="text-smtext-yellow-400 text-right">Report bugs, request features, provide feedback, and contribute on <a class="underline" href="https://github.com/advplyr/audiobookshelf-app" target="_blank">GitHub</a></p>
|
||||||
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
|
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="24" height="24" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
@ -41,9 +41,6 @@ export default {
|
||||||
serverConnectionConfig() {
|
serverConnectionConfig() {
|
||||||
return this.$store.state.user.serverConnectionConfig || {}
|
return this.$store.state.user.serverConnectionConfig || {}
|
||||||
},
|
},
|
||||||
serverConnConfigName() {
|
|
||||||
return this.serverConnectionConfig.name
|
|
||||||
},
|
|
||||||
serverAddress() {
|
serverAddress() {
|
||||||
return this.serverConnectionConfig.address
|
return this.serverConnectionConfig.address
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
<home-bookshelf-nav-bar />
|
<home-bookshelf-nav-bar />
|
||||||
<home-bookshelf-toolbar v-show="!hideToolbar" />
|
<home-bookshelf-toolbar v-show="!hideToolbar" />
|
||||||
<div id="bookshelf-wrapper" class="main-content overflow-y-auto overflow-x-hidden relative" :class="hideToolbar ? 'no-toolbar' : ''">
|
<div id="bookshelf-wrapper" class="main-content overflow-y-auto overflow-x-hidden relative" :class="hideToolbar ? 'no-toolbar' : ''">
|
||||||
<nuxt-child />
|
<nuxt-child :loading.sync="loading" />
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="absolute top-0 left-0 z-50 w-full h-full flex items-center justify-center">
|
||||||
|
<ui-loading-indicator text="Loading..." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -11,7 +14,9 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hideToolbar() {
|
hideToolbar() {
|
||||||
|
|
|
@ -26,18 +26,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
|
||||||
<ui-loading-indicator text="Loading Library..." />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
props: {
|
||||||
|
loading: Boolean
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
shelves: [],
|
shelves: [],
|
||||||
loading: false,
|
|
||||||
isFirstNetworkConnection: true,
|
isFirstNetworkConnection: true,
|
||||||
lastServerFetch: 0,
|
lastServerFetch: 0,
|
||||||
lastServerFetchLibraryId: null,
|
lastServerFetchLibraryId: null,
|
||||||
|
@ -77,6 +76,9 @@ export default {
|
||||||
networkConnected() {
|
networkConnected() {
|
||||||
return this.$store.state.networkConnected
|
return this.$store.state.networkConnected
|
||||||
},
|
},
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
|
},
|
||||||
currentLibraryName() {
|
currentLibraryName() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
},
|
},
|
||||||
|
@ -94,6 +96,14 @@ export default {
|
||||||
},
|
},
|
||||||
localMediaProgress() {
|
localMediaProgress() {
|
||||||
return this.$store.state.globals.localMediaProgress
|
return this.$store.state.globals.localMediaProgress
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
get() {
|
||||||
|
return this.loading
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:loading', val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -210,7 +220,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true
|
this.isLoading = true
|
||||||
this.shelves = []
|
this.shelves = []
|
||||||
|
|
||||||
if (this.user && this.currentLibraryId && this.networkConnected) {
|
if (this.user && this.currentLibraryId && this.networkConnected) {
|
||||||
|
@ -226,7 +236,7 @@ export default {
|
||||||
this.shelves = localCategories
|
this.shelves = localCategories
|
||||||
this.lastServerFetch = 0
|
this.lastServerFetch = 0
|
||||||
this.lastLocalFetch = Date.now()
|
this.lastLocalFetch = Date.now()
|
||||||
this.loading = false
|
this.isLoading = false
|
||||||
console.log('[categories] Local shelves set from failure', this.shelves.length, this.lastLocalFetch)
|
console.log('[categories] Local shelves set from failure', this.shelves.length, this.lastLocalFetch)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -247,6 +257,11 @@ export default {
|
||||||
return cat
|
return cat
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: iOS has its own implementation of this. Android & iOS should be consistent here.
|
||||||
|
if (!this.isIos) {
|
||||||
|
this.openMediaPlayerWithMostRecentListening()
|
||||||
|
}
|
||||||
|
|
||||||
// Only add the local shelf with the same media type
|
// Only add the local shelf with the same media type
|
||||||
const localShelves = localCategories.filter((cat) => cat.type === this.currentLibraryMediaType && !cat.localOnly)
|
const localShelves = localCategories.filter((cat) => cat.type === this.currentLibraryMediaType && !cat.localOnly)
|
||||||
this.shelves.push(...localShelves)
|
this.shelves.push(...localShelves)
|
||||||
|
@ -259,7 +274,41 @@ export default {
|
||||||
console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch)
|
console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false
|
this.isLoading = false
|
||||||
|
},
|
||||||
|
openMediaPlayerWithMostRecentListening() {
|
||||||
|
// If we don't already have a player open
|
||||||
|
// Try opening the first book from continue-listening without playing it
|
||||||
|
if (this.$store.state.playerLibraryItemId || !this.$store.state.isFirstAudioLoad) return
|
||||||
|
this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch
|
||||||
|
|
||||||
|
const continueListeningShelf = this.shelves.find((cat) => cat.id === 'continue-listening')
|
||||||
|
const mostRecentEntity = continueListeningShelf?.entities?.[0]
|
||||||
|
if (mostRecentEntity) {
|
||||||
|
const playObject = {
|
||||||
|
libraryItemId: mostRecentEntity.id,
|
||||||
|
episodeId: mostRecentEntity.recentEpisode?.id || null,
|
||||||
|
paused: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is a local copy
|
||||||
|
if (mostRecentEntity.localLibraryItem) {
|
||||||
|
if (mostRecentEntity.recentEpisode) {
|
||||||
|
// Check if the podcast episode has a local copy
|
||||||
|
const localEpisode = mostRecentEntity.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId === mostRecentEntity.recentEpisode.id)
|
||||||
|
if (localEpisode) {
|
||||||
|
playObject.libraryItemId = mostRecentEntity.localLibraryItem.id
|
||||||
|
playObject.episodeId = localEpisode.id
|
||||||
|
playObject.serverLibraryItemId = mostRecentEntity.id
|
||||||
|
playObject.serverEpisodeId = mostRecentEntity.recentEpisode.id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playObject.libraryItemId = mostRecentEntity.localLibraryItem.id
|
||||||
|
playObject.serverLibraryItemId = mostRecentEntity.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$eventBus.$emit('play-item', playObject)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
libraryChanged() {
|
libraryChanged() {
|
||||||
if (this.currentLibraryId) {
|
if (this.currentLibraryId) {
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="absolute top-0 left-0 w-full p-6 flex items-center flex-col justify-center z-0 short:hidden">
|
<div class="absolute top-0 left-0 w-full p-6 flex items-center flex-col justify-center z-0 short:hidden">
|
||||||
<img src="/Logo.png" class="h-20 w-20 mb-2" />
|
<img src="/Logo.png" class="h-20 w-20 mb-2" />
|
||||||
<h1 class="text-2xl font-book">audiobookshelf</h1>
|
<h1 class="text-2xl">audiobookshelf</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="hidden absolute short:block top-1.5 left-12 p-2 font-book text-xl">audiobookshelf</p>
|
<p class="hidden absolute short:block top-1.5 left-12 p-2text-xl">audiobookshelf</p>
|
||||||
|
|
||||||
<!-- <p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> -->
|
<!-- <p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> -->
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-3 py-4 overflow-y-auto relative bg-bg">
|
<div class="w-full h-full px-3 py-4 overflow-y-auto overflow-x-hidden relative bg-bg">
|
||||||
<div class="fixed top-0 left-0 w-full h-full pointer-events-none p-px z-10">
|
<div class="fixed top-0 left-0 w-full h-full pointer-events-none p-px z-10">
|
||||||
<div class="w-full h-full" :style="{ backgroundColor: coverRgb }" />
|
<div class="w-full h-full" :style="{ backgroundColor: coverRgb }" />
|
||||||
<div class="w-full h-full absolute top-0 left-0" style="background: linear-gradient(169deg, rgba(0, 0, 0, 0.4) 0%, rgba(55, 56, 56, 1) 80%)" />
|
<div class="w-full h-full absolute top-0 left-0" style="background: linear-gradient(169deg, rgba(0, 0, 0, 0.4) 0%, rgba(55, 56, 56, 1) 80%)" />
|
||||||
|
@ -7,8 +7,15 @@
|
||||||
|
|
||||||
<div class="z-10 relative">
|
<div class="z-10 relative">
|
||||||
<div class="w-full flex justify-center relative mb-4">
|
<div class="w-full flex justify-center relative mb-4">
|
||||||
|
<div style="width: 0; transform: translateX(-50vw); overflow: visible">
|
||||||
|
<div style="width: 150vw; overflow: hidden">
|
||||||
|
<div id="coverBg" style="filter: blur(5vw)">
|
||||||
|
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="relative" @click="showFullscreenCover = true">
|
<div class="relative" @click="showFullscreenCover = true">
|
||||||
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
|
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg @imageLoaded="coverImageLoaded" />
|
||||||
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * progressPercent + 'px' }"></div>
|
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * progressPercent + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,7 +127,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full py-4">
|
<div class="w-full py-4">
|
||||||
<p class="text-sm">{{ description }}</p>
|
<p class="text-sm text-justify" style="hyphens: auto;">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" :local-library-item-id="localLibraryItemId" :episodes="episodes" :local-episodes="localLibraryItemEpisodes" :is-local="isLocal" />
|
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" :local-library-item-id="localLibraryItemId" :episodes="episodes" :local-episodes="localLibraryItemEpisodes" :is-local="isLocal" />
|
||||||
|
@ -368,19 +375,24 @@ export default {
|
||||||
if (!this.isIos) {
|
if (!this.isIos) {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'History',
|
text: 'History',
|
||||||
value: 'history'
|
value: 'history',
|
||||||
|
icon: 'history'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push({
|
if (!this.userIsFinished) {
|
||||||
text: this.userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished',
|
items.push({
|
||||||
value: 'markFinished'
|
text: 'Mark as Finished',
|
||||||
})
|
value: 'markFinished',
|
||||||
|
icon: 'beenhere'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (this.progressPercent > 0 && !this.userIsFinished) {
|
if (this.progressPercent > 0) {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'Discard Progress',
|
text: 'Discard Progress',
|
||||||
value: 'discardProgress'
|
value: 'discardProgress',
|
||||||
|
icon: 'backspace'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -388,20 +400,23 @@ export default {
|
||||||
if (this.localLibraryItemId) {
|
if (this.localLibraryItemId) {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'Manage Local Files',
|
text: 'Manage Local Files',
|
||||||
value: 'manageLocal'
|
value: 'manageLocal',
|
||||||
|
icon: 'folder'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isPodcast && this.serverLibraryItemId) {
|
if (!this.isPodcast && this.serverLibraryItemId) {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'Add to Playlist',
|
text: 'Add to Playlist',
|
||||||
value: 'playlist'
|
value: 'playlist',
|
||||||
|
icon: 'playlist_add'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
text: 'More Info',
|
text: 'More Info',
|
||||||
value: 'details'
|
value: 'details',
|
||||||
|
icon: 'info'
|
||||||
})
|
})
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
@ -722,4 +737,8 @@ export default {
|
||||||
width: calc(100% - 64px);
|
width: calc(100% - 64px);
|
||||||
max-width: calc(100% - 64px);
|
max-width: calc(100% - 64px);
|
||||||
}
|
}
|
||||||
|
#coverBg > div {
|
||||||
|
width: 150vw !important;
|
||||||
|
max-width: 150vw !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full py-6 px-2">
|
<div class="w-full h-full py-6 px-2">
|
||||||
<div v-if="localLibraryItem" class="w-full h-full" :class="orderChanged ? 'pb-20' : ''">
|
<div v-if="localLibraryItem" class="w-full h-full">
|
||||||
<div class="px-2 flex items-center mb-2">
|
<div class="px-2 flex items-center mb-2">
|
||||||
<p class="text-base font-book font-semibold">{{ mediaMetadata.title }}</p>
|
<p class="text-basefont-semibold">{{ mediaMetadata.title }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<button v-if="audioTracks.length && !isPodcast" class="shadow-sm text-accent flex items-center justify-center rounded-full mx-2" @click.stop="play">
|
<button v-if="audioTracks.length && !isPodcast" class="shadow-sm text-accent flex items-center justify-center rounded-full mx-2" @click.stop="play">
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
<div v-if="isScanning" class="w-full text-center p-4">
|
<div v-if="isScanning" class="w-full text-center p-4">
|
||||||
<p>Scanning...</p>
|
<p>Scanning...</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full max-w-full media-item-container overflow-y-auto overflow-x-hidden relative">
|
<div v-else class="w-full max-w-full media-item-container overflow-y-auto overflow-x-hidden relative" :class="{ 'media-order-changed': orderChanged }">
|
||||||
<div v-if="!isPodcast" class="w-full">
|
<div v-if="!isPodcast" class="w-full">
|
||||||
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
|
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
|
||||||
|
|
||||||
|
@ -395,6 +395,18 @@ export default {
|
||||||
height: calc(100vh - 200px);
|
height: calc(100vh - 200px);
|
||||||
max-height: calc(100vh - 200px);
|
max-height: calc(100vh - 200px);
|
||||||
}
|
}
|
||||||
|
.media-item-container.media-order-changed {
|
||||||
|
height: calc(100vh - 280px);
|
||||||
|
max-height: calc(100vh - 280px);
|
||||||
|
}
|
||||||
|
.playerOpen .media-item-container {
|
||||||
|
height: calc(100vh - 300px);
|
||||||
|
max-height: calc(100vh - 300px);
|
||||||
|
}
|
||||||
|
.playerOpen .media-item-container.media-order-changed {
|
||||||
|
height: calc(100vh - 380px);
|
||||||
|
max-height: calc(100vh - 380px);
|
||||||
|
}
|
||||||
.sortable-ghost {
|
.sortable-ghost {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,21 +8,21 @@
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
<div class="px-3">
|
<div class="px-3">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Items Finished</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">Items Finished</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Days Listened</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">Days Listened</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Minutes Listening</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">Minutes Listening</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,21 +30,20 @@
|
||||||
<stats-daily-listening-chart :listening-stats="listeningStats" class="lg:scale-100 transform scale-90 px-0" />
|
<stats-daily-listening-chart :listening-stats="listeningStats" class="lg:scale-100 transform scale-90 px-0" />
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<div class="flex mb-4 items-center">
|
<div class="flex mb-4 items-center">
|
||||||
<h1 class="text-2xl font-book">Recent Sessions</h1>
|
<h1 class="text-2xl">Recent Sessions</h1>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!mostRecentListeningSessions.length">No Listening Sessions</p>
|
<p v-if="!mostRecentListeningSessions.length">No Listening Sessions</p>
|
||||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||||
<div :key="item.id" class="w-full py-0.5">
|
<div :key="item.id" class="w-full py-0.5">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
<p class="text-sm text-white text-opacity-70 w-8 min-w-8">{{ index + 1 }}. </p>
|
||||||
<div class="w-56">
|
<div class="w-56">
|
||||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
<p class="text-sm text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="w-16 min-w-16 text-right">
|
||||||
<div class="w-18 text-right">
|
<p class="text-xs font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
||||||
<p class="text-sm font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Binary file not shown.
|
@ -130,8 +130,8 @@ export const mutations = {
|
||||||
downloadItem.downloadItemParts = downloadItem.downloadItemParts.map(dip => {
|
downloadItem.downloadItemParts = downloadItem.downloadItemParts.map(dip => {
|
||||||
let newDip = dip.id == downloadItemPart.id ? downloadItemPart : dip
|
let newDip = dip.id == downloadItemPart.id ? downloadItemPart : dip
|
||||||
|
|
||||||
totalBytes += newDip.fileSize
|
totalBytes += newDip.completed ? Number(newDip.bytesDownloaded) : Number(newDip.fileSize)
|
||||||
totalBytesDownloaded += newDip.bytesDownloaded
|
totalBytesDownloaded += Number(newDip.bytesDownloaded)
|
||||||
|
|
||||||
return newDip
|
return newDip
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,7 @@ export const state = () => ({
|
||||||
networkConnectionType: null,
|
networkConnectionType: null,
|
||||||
isNetworkUnmetered: true,
|
isNetworkUnmetered: true,
|
||||||
isFirstLoad: true,
|
isFirstLoad: true,
|
||||||
|
isFirstAudioLoad: true,
|
||||||
hasStoragePermission: false,
|
hasStoragePermission: false,
|
||||||
selectedLibraryItem: null,
|
selectedLibraryItem: null,
|
||||||
showReader: false,
|
showReader: false,
|
||||||
|
@ -120,6 +121,9 @@ export const mutations = {
|
||||||
setIsFirstLoad(state, val) {
|
setIsFirstLoad(state, val) {
|
||||||
state.isFirstLoad = val
|
state.isFirstLoad = val
|
||||||
},
|
},
|
||||||
|
setIsFirstAudioLoad(state, val) {
|
||||||
|
state.isFirstAudioLoad = val
|
||||||
|
},
|
||||||
setSocketConnected(state, val) {
|
setSocketConnected(state, val) {
|
||||||
state.socketConnected = val
|
state.socketConnected = val
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const getters = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
||||||
if (!state.user.bookmarks) return []
|
if (!state?.user?.bookmarks) return []
|
||||||
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId)
|
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId)
|
||||||
},
|
},
|
||||||
getUserSetting: (state) => (key) => {
|
getUserSetting: (state) => (key) => {
|
||||||
|
|
|
@ -33,8 +33,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Source Sans Pro', ...defaultTheme.fontFamily.sans],
|
sans: ['Source Sans Pro', ...defaultTheme.fontFamily.sans],
|
||||||
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono],
|
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono]
|
||||||
book: ['Gentium Book Basic', 'serif']
|
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
xxs: '0.625rem'
|
xxs: '0.625rem'
|
||||||
|
@ -46,7 +45,8 @@ module.exports = {
|
||||||
'4': '1rem',
|
'4': '1rem',
|
||||||
'8': '2rem',
|
'8': '2rem',
|
||||||
'10': '2.5rem',
|
'10': '2.5rem',
|
||||||
'12': '3rem'
|
'12': '3rem',
|
||||||
|
'16': '4rem'
|
||||||
},
|
},
|
||||||
minHeight: {
|
minHeight: {
|
||||||
'12': '3rem'
|
'12': '3rem'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue