+
close
diff --git a/components/stats/YearInReview.vue b/components/stats/YearInReview.vue
new file mode 100644
index 00000000..4e04ec07
--- /dev/null
+++ b/components/stats/YearInReview.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
![]()
+
+
+
+
\ No newline at end of file
diff --git a/components/stats/YearInReviewBanner.vue b/components/stats/YearInReviewBanner.vue
new file mode 100644
index 00000000..dee1e155
--- /dev/null
+++ b/components/stats/YearInReviewBanner.vue
@@ -0,0 +1,134 @@
+
+
+
+
+ close
+
+
+
+
+
{{ yearInReviewYear }} Year in Review
+
+
{{ showYearInReview ? 'Hide Year in Review' : `See ${yearInReviewYear} Year in Review` }}
+
+
+
+
+
+
+
+
+
+ chevron_left
+ Previous
+
+
+
Share
+
+
+
Your Year in Review ({{ yearInReviewVariant + 1 }})
+
{{ yearInReviewVariant + 1 }}
+
+
+
+
+ Refresh
+ refresh
+
+
+
+ Next
+ chevron_right
+
+
+
+
+
+
+
+ Share
+
+
+
+
+
+
+
+
+ chevron_left
+ Previous
+
+
+
Share
+
+
+
Server Year in Review ({{ yearInReviewServerVariant + 1 }})
+
{{ yearInReviewServerVariant + 1 }}
+
+
+
+
+ Refresh
+ refresh
+
+
+
+ Next
+ chevron_right
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/stats/YearInReviewServer.vue b/components/stats/YearInReviewServer.vue
new file mode 100644
index 00000000..e6d443d0
--- /dev/null
+++ b/components/stats/YearInReviewServer.vue
@@ -0,0 +1,262 @@
+
+
+
+
+
+
![]()
+
+
+
+
\ No newline at end of file
diff --git a/components/stats/YearInReviewShort.vue b/components/stats/YearInReviewShort.vue
new file mode 100644
index 00000000..ae77e987
--- /dev/null
+++ b/components/stats/YearInReviewShort.vue
@@ -0,0 +1,192 @@
+
+
+
+
+
+
![]()
+
+
+
+
\ No newline at end of file
diff --git a/components/ui/Btn.vue b/components/ui/Btn.vue
index 12ccf24c..630ed540 100644
--- a/components/ui/Btn.vue
+++ b/components/ui/Btn.vue
@@ -30,6 +30,7 @@ export default {
default: ''
},
paddingX: Number,
+ paddingY: Number,
small: Boolean,
loading: Boolean,
disabled: Boolean
@@ -48,14 +49,17 @@ export default {
if (this.small) {
list.push('text-sm')
if (this.paddingX === undefined) list.push('px-4')
- list.push('py-1')
+ if (this.paddingY === undefined) list.push('py-1')
} else {
if (this.paddingX === undefined) list.push('px-8')
- list.push('py-2')
+ if (this.paddingY === undefined) list.push('py-2')
}
if (this.paddingX !== undefined) {
list.push(`px-${this.paddingX}`)
}
+ if (this.paddingY !== undefined) {
+ list.push(`py-${this.paddingY}`)
+ }
if (this.disabled) {
list.push('cursor-not-allowed')
}
diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift
index e1423b13..26a74dcc 100644
--- a/ios/App/App/AppDelegate.swift
+++ b/ios/App/App/AppDelegate.swift
@@ -4,17 +4,17 @@ import RealmSwift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
-
+
private let logger = AppLogger(category: "AppDelegate")
lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
var backgroundCompletionHandler: (() -> Void)?
-
+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
-
+
let configuration = Realm.Configuration(
- schemaVersion: 15,
+ schemaVersion: 16,
migrationBlock: { [weak self] migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
@@ -48,10 +48,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
newObject?["languageCode"] = "en-us"
}
}
+ if (oldSchemaVersion < 16) {
+ self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding chapterTrack setting")
+ migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
+ newObject?["chapterTrack"] = false
+ }
+ }
+
}
)
Realm.Configuration.defaultConfiguration = configuration
-
+
return true
}
@@ -93,7 +100,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
-
+
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
// Stores the completion handler for background downloads
// The identifier of this method can be ignored at this time as we only have one background url session
diff --git a/ios/App/App/plugins/AbsAudioPlayer.m b/ios/App/App/plugins/AbsAudioPlayer.m
index 49aaff7a..8170d059 100644
--- a/ios/App/App/plugins/AbsAudioPlayer.m
+++ b/ios/App/App/plugins/AbsAudioPlayer.m
@@ -15,6 +15,7 @@ CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer",
CAP_PLUGIN_METHOD(closePlayback, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
+ CAP_PLUGIN_METHOD(setChapterTrack, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift
index 5ebe376c..edc4e274 100644
--- a/ios/App/App/plugins/AbsAudioPlayer.swift
+++ b/ios/App/App/plugins/AbsAudioPlayer.swift
@@ -119,6 +119,17 @@ public class AbsAudioPlayer: CAPPlugin {
PlayerHandler.setPlaybackSpeed(speed: settings.playbackRate)
call.resolve()
}
+
+ @objc func setChapterTrack(_ call: CAPPluginCall) {
+ let chapterTrack = call.getBool("enabled", true)
+ logger.log(String(chapterTrack))
+ let settings = PlayerSettings.main()
+ try? settings.update {
+ settings.chapterTrack = chapterTrack
+ }
+ PlayerHandler.setChapterTrack()
+ call.resolve()
+ }
@objc func playPlayer(_ call: CAPPluginCall) {
PlayerHandler.paused = false
diff --git a/ios/App/Podfile b/ios/App/Podfile
index 531fb33f..b29cd4cb 100644
--- a/ios/App/Podfile
+++ b/ios/App/Podfile
@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
+ pod 'ByteowlsCapacitorFilesharer', :path => '../../node_modules/@byteowls/capacitor-filesharer'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock
index d07dbd1b..f26f837f 100644
--- a/ios/App/Podfile.lock
+++ b/ios/App/Podfile.lock
@@ -1,5 +1,7 @@
PODS:
- Alamofire (5.6.4)
+ - ByteowlsCapacitorFilesharer (5.0.0):
+ - Capacitor
- Capacitor (5.4.0):
- CapacitorCordova
- CapacitorApp (5.0.6):
@@ -29,6 +31,7 @@ PODS:
DEPENDENCIES:
- Alamofire (~> 5.5)
+ - "ByteowlsCapacitorFilesharer (from `../../node_modules/@byteowls/capacitor-filesharer`)"
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)"
@@ -49,6 +52,8 @@ SPEC REPOS:
- RealmSwift
EXTERNAL SOURCES:
+ ByteowlsCapacitorFilesharer:
+ :path: "../../node_modules/@byteowls/capacitor-filesharer"
Capacitor:
:path: "../../node_modules/@capacitor/ios"
CapacitorApp:
@@ -74,6 +79,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Alamofire: 4e95d97098eacb88856099c4fc79b526a299e48c
+ ByteowlsCapacitorFilesharer: f6a773825632d65d5404a34764c4a3fd857bb176
Capacitor: a5cd803e02b471591c81165f400ace01f40b11d3
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
CapacitorBrowser: 7a0fb6a1011abfaaf2dfedfd8248f942a8eda3d6
@@ -88,6 +94,6 @@ SPEC CHECKSUMS:
Realm: 3fd136cb4c83a927482a7f1612496d37beed3cf5
RealmSwift: 513d4dcbf5bfc4d573454088b592685fc48dd716
-PODFILE CHECKSUM: 7a8fc177ef0646dd60a1ee8aa387964975fcc1e3
+PODFILE CHECKSUM: 02e6ffe2f51a453ce222ee9af0e55e9448d8514c
COCOAPODS: 1.12.1
diff --git a/ios/App/Shared/models/PlaybackSession.swift b/ios/App/Shared/models/PlaybackSession.swift
index baa89553..ad6812ae 100644
--- a/ios/App/Shared/models/PlaybackSession.swift
+++ b/ios/App/Shared/models/PlaybackSession.swift
@@ -160,3 +160,11 @@ class PlaybackSession: Object, Codable, Deletable {
try container.encode(localMediaProgressId, forKey: .localMediaProgressId)
}
}
+
+extension PlaybackSession {
+ func getCurrentChapter() -> Chapter? {
+ return chapters.first { chapter in
+ chapter.start <= self.currentTime && chapter.end > self.currentTime
+ }
+ }
+}
diff --git a/ios/App/Shared/models/PlayerSettings.swift b/ios/App/Shared/models/PlayerSettings.swift
index 0a8b3788..c768c50c 100644
--- a/ios/App/Shared/models/PlayerSettings.swift
+++ b/ios/App/Shared/models/PlayerSettings.swift
@@ -12,6 +12,8 @@ class PlayerSettings: Object {
// The webapp has a persisted setting for playback speed, but it's not always available to the native code
// Lets track it natively as well, so we never have a situation where the UI and native player are out of sync
@Persisted var playbackRate: Float = 1.0
+ @Persisted var chapterTrack: Bool = true
+
// Singleton pattern for Realm objects
static func main() -> PlayerSettings {
diff --git a/ios/App/Shared/models/server/Chapter.swift b/ios/App/Shared/models/server/Chapter.swift
index 2e4d1e02..537ce6c7 100644
--- a/ios/App/Shared/models/server/Chapter.swift
+++ b/ios/App/Shared/models/server/Chapter.swift
@@ -39,3 +39,13 @@ class Chapter: EmbeddedObject, Codable {
try container.encode(title, forKey: .title)
}
}
+
+extension Chapter {
+ func getRelativeChapterCurrentTime(sessionCurrentTime: Double) -> Double {
+ return sessionCurrentTime - self.start
+ }
+
+ func getRelativeChapterEndTime() -> Double {
+ return self.end - self.start
+ }
+}
diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift
index 665d776c..8a66801c 100644
--- a/ios/App/Shared/player/AudioPlayer.swift
+++ b/ios/App/Shared/player/AudioPlayer.swift
@@ -224,6 +224,8 @@ class AudioPlayer: NSObject {
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
+ // Update the now playing and chapter info
+ self.updateNowPlaying()
}
}
}
@@ -443,6 +445,10 @@ class AudioPlayer: NSObject {
}
}
+ public func setChapterTrack() {
+ self.updateNowPlaying()
+ }
+
public func getCurrentTime() -> Double? {
guard let playbackSession = self.getPlaybackSession() else { return nil }
let currentTrackTime = self.audioPlayer.currentTime().seconds
@@ -583,12 +589,34 @@ class AudioPlayer: NSObject {
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { [weak self] event in
- self?.play(allowSeekBack: true)
+ guard let strongSelf = self else { return .commandFailed }
+ if strongSelf.isPlaying() {
+ strongSelf.pause()
+ } else {
+ strongSelf.play(allowSeekBack: true)
+ }
return .success
}
+
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget { [weak self] event in
- self?.pause()
+ guard let strongSelf = self else { return .commandFailed }
+ if strongSelf.isPlaying() {
+ strongSelf.pause()
+ } else {
+ strongSelf.play(allowSeekBack: true)
+ }
+ return .success
+ }
+
+ commandCenter.togglePlayPauseCommand.isEnabled = true
+ commandCenter.togglePlayPauseCommand.addTarget { [weak self] event in
+ guard let strongSelf = self else { return .commandFailed }
+ if strongSelf.isPlaying() {
+ strongSelf.pause()
+ } else {
+ strongSelf.play(allowSeekBack: true)
+ }
return .success
}
@@ -640,7 +668,14 @@ class AudioPlayer: NSObject {
return .noSuchContent
}
- self?.seek(event.positionTime, from: "remote")
+ // Adjust seek time if chapter track is being used
+ var seekTime = event.positionTime
+ if PlayerSettings.main().chapterTrack {
+ if let session = self?.getPlaybackSession(), let currentChapter = session.getCurrentChapter() {
+ seekTime += currentChapter.start
+ }
+ }
+ self?.seek(seekTime, from: "remote")
return .success
}
@@ -657,7 +692,16 @@ class AudioPlayer: NSObject {
}
private func updateNowPlaying() {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
- if let duration = self.getDuration(), let currentTime = self.getCurrentTime() {
+ if let session = self.getPlaybackSession(), let currentChapter = session.getCurrentChapter(), PlayerSettings.main().chapterTrack {
+ NowPlayingInfo.shared.update(
+ duration: currentChapter.getRelativeChapterEndTime(),
+ currentTime: currentChapter.getRelativeChapterCurrentTime(sessionCurrentTime: session.currentTime),
+ rate: rate,
+ chapterName: currentChapter.title,
+ chapterNumber: (session.chapters.firstIndex(of: currentChapter) ?? 0) + 1,
+ chapterCount: session.chapters.count
+ )
+ } else if let duration = self.getDuration(), let currentTime = self.getCurrentTime() {
NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: rate)
}
}
diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift
index 8b6541bc..718cdf95 100644
--- a/ios/App/Shared/player/PlayerHandler.swift
+++ b/ios/App/Shared/player/PlayerHandler.swift
@@ -61,6 +61,10 @@ class PlayerHandler {
self.player?.setPlaybackRate(speed)
}
+ public static func setChapterTrack() {
+ self.player?.setChapterTrack()
+ }
+
public static func getSleepTimeRemaining() -> Double? {
return self.player?.getSleepTimeRemaining()
}
diff --git a/ios/App/Shared/util/NowPlayingInfo.swift b/ios/App/Shared/util/NowPlayingInfo.swift
index 6393611c..86ca8ad4 100644
--- a/ios/App/Shared/util/NowPlayingInfo.swift
+++ b/ios/App/Shared/util/NowPlayingInfo.swift
@@ -53,7 +53,7 @@ class NowPlayingInfo {
self.setMetadata(artwork: artwork, metadata: metadata)
}
}
- public func update(duration: Double, currentTime: Double, rate: Float) {
+ public func update(duration: Double, currentTime: Double, rate: Float, chapterName: String? = nil, chapterNumber: Int? = nil, chapterCount: Int? = nil) {
// Update on the main to prevent access collisions
DispatchQueue.main.async { [weak self] in
if let self = self {
@@ -62,6 +62,18 @@ class NowPlayingInfo {
self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
self.nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
+
+ if let chapterName = chapterName, let chapterNumber = chapterNumber, let chapterCount = chapterCount {
+ self.nowPlayingInfo[MPMediaItemPropertyTitle] = chapterName
+ self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterNumber] = chapterNumber
+ self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterCount] = chapterCount
+ } else {
+ // Set the title back to the book title
+ self.nowPlayingInfo[MPMediaItemPropertyTitle] = self.nowPlayingInfo[MPMediaItemPropertyAlbumTitle]
+ self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterNumber] = nil
+ self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterCount] = nil
+ }
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo
}
}
@@ -89,7 +101,7 @@ class NowPlayingInfo {
nowPlayingInfo[MPMediaItemPropertyTitle] = metadata!.title
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
- nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
+ nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.title
}
private func shouldFetchCover(id: String) -> Bool {
diff --git a/package-lock.json b/package-lock.json
index 953ab15d..62dbdb29 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "audiobookshelf-app",
"version": "0.9.70-beta",
"dependencies": {
+ "@byteowls/capacitor-filesharer": "^5.0.0",
"@capacitor/android": "^5.0.0",
"@capacitor/app": "^5.0.6",
"@capacitor/browser": "^5.1.0",
@@ -1939,6 +1940,17 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@byteowls/capacitor-filesharer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@byteowls/capacitor-filesharer/-/capacitor-filesharer-5.0.0.tgz",
+ "integrity": "sha512-LtIMd8Ge94Kj9BQWF+A646drHAkRVC6ulTPZ1InkQtNH4VIy3WfYilN20VZM5KkOvrQ1lslXFIJwquwKptLgmw==",
+ "dependencies": {
+ "file-saver": "2.0.5"
+ },
+ "peerDependencies": {
+ "@capacitor/core": ">=5"
+ }
+ },
"node_modules/@capacitor/android": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.4.0.tgz",
@@ -9195,6 +9207,11 @@
"node": ">=8.9.0"
}
},
+ "node_modules/file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+ },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -21078,6 +21095,14 @@
"to-fast-properties": "^2.0.0"
}
},
+ "@byteowls/capacitor-filesharer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@byteowls/capacitor-filesharer/-/capacitor-filesharer-5.0.0.tgz",
+ "integrity": "sha512-LtIMd8Ge94Kj9BQWF+A646drHAkRVC6ulTPZ1InkQtNH4VIy3WfYilN20VZM5KkOvrQ1lslXFIJwquwKptLgmw==",
+ "requires": {
+ "file-saver": "2.0.5"
+ }
+ },
"@capacitor/android": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.4.0.tgz",
@@ -26353,6 +26378,11 @@
}
}
},
+ "file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+ },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -34083,4 +34113,4 @@
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
}
}
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 69ed6623..63387bc5 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"ionic:serve": "npm run start"
},
"dependencies": {
+ "@byteowls/capacitor-filesharer": "^5.0.0",
"@capacitor/android": "^5.0.0",
"@capacitor/app": "^5.0.6",
"@capacitor/browser": "^5.1.0",
@@ -47,4 +48,4 @@
"postcss": "^8.3.5",
"tailwindcss": "^3.3.2"
}
-}
\ No newline at end of file
+}
diff --git a/pages/item/_id/index.vue b/pages/item/_id/index.vue
index a1530f08..12809d13 100644
--- a/pages/item/_id/index.vue
+++ b/pages/item/_id/index.vue
@@ -54,7 +54,7 @@
{{ $strings.ButtonRead }} {{ ebookFormat }}
- {{ (downloadItem || startingDownload) ? 'downloading' : 'download' }}
+ {{ downloadItem || startingDownload ? 'downloading' : 'download' }}
more_vert
@@ -79,8 +79,8 @@
{{ podcastAuthor }}
- {{ author.name }}
- ,
+ {{ author.name }},
@@ -90,8 +90,8 @@
{{ $strings.LabelSeries }}
- {{ series.text }}
- ,
+ {{ series.text }},
@@ -101,16 +101,16 @@
{{ $strings.LabelNarrators }}
- {{ narrator }}
- ,
+ {{ narrator }},
{{ $strings.LabelGenres }}
- {{ genre }}
- ,
+ {{ genre }},
diff --git a/pages/stats.vue b/pages/stats.vue
index e9ff08dd..5c704b0b 100644
--- a/pages/stats.vue
+++ b/pages/stats.vue
@@ -1,5 +1,8 @@
+
+
+
{{ $strings.HeaderYourStats }}
@@ -50,6 +53,9 @@