Compare commits

...
Author SHA1 Message Date
advplyr 8bc5932889 Version bump v0.9.76-beta 2024-08-13 10:26:08 -05:00
advplyrandGitHub 3cd597d71e Merge pull request #1286 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-13 10:19:04 -05:00
Fredrik LindqvistandHosted Weblate 48d5ad462e Translated using Weblate (Swedish)
Currently translated at 81.1% (249 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2024-08-13 17:18:39 +02:00
advplyr 40cab87640 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-08-13 10:18:28 -05:00
advplyr f405947ad8 Add:Random library sort #1285 2024-08-13 10:18:23 -05:00
advplyr fd550c15a5 iOS version bump 0.9.75 2024-08-12 17:38:44 -05:00
advplyr d18537cf04 Version bump v0.9.75-beta & targetSdkVersion bump to 34 2024-08-12 17:24:05 -05:00
advplyrandGitHub f01f0229a8 Merge pull request #1278 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-12 16:40:47 -05:00
CharlieandHosted Weblate b369eb2d40 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-12 23:34:46 +02:00
CharlieandHosted Weblate f08c65c5bf Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-12 23:34:46 +02:00
tonttulaandHosted Weblate 1043f4ecd7 Translated using Weblate (Finnish)
Currently translated at 72.3% (222 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-08-12 23:34:46 +02:00
SunSpringandHosted Weblate 4f17a32373 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-08-12 23:34:46 +02:00
CharlieandHosted Weblate 2203aae6e6 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-12 23:34:46 +02:00
advplyrandGitHub 7b60c7b6c5 Merge pull request #1279 from nichwall/release_close_issue
Add: workflow to automatically close issues on release
2024-08-12 16:34:41 -05:00
Nicholas Wallace be98e06efc Add: workflow to automatically close issues on release 2024-08-06 21:02:27 -07:00
advplyrandGitHub 45239bca34 Merge pull request #1258 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-04 10:09:21 -05:00
burghy86andHosted Weblate 2493ce7ed7 Translated using Weblate (Italian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-07-26 04:45:02 +00:00
advplyr 2b1e53b371 Remove references to matrix server 2024-07-23 16:22:24 -05:00
weblate.user.1274andHosted Weblate d643a1769f Translated using Weblate (Norwegian Bokmål)
Currently translated at 84.0% (258 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-07-23 23:19:53 +02:00
MarioandHosted Weblate a63b7a188c Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-07-23 23:19:53 +02:00
gallegonovatoandHosted Weblate f9c0162b8a Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-07-23 23:19:52 +02:00
Petteri HjortandHosted Weblate cda3cb3512 Translated using Weblate (Finnish)
Currently translated at 49.8% (153 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-07-23 23:19:52 +02:00
Marcin MartelaandHosted Weblate 766f6938a7 Translated using Weblate (Polish)
Currently translated at 96.0% (295 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-07-23 23:19:51 +02:00
tonttulaandHosted Weblate 8e1ccb517b Translated using Weblate (Finnish)
Currently translated at 48.2% (148 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-07-23 23:19:51 +02:00
MarioandHosted Weblate 540aab6f83 Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-07-23 23:19:50 +02:00
CharlieandHosted Weblate d1969d7897 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-07-23 23:19:50 +02:00
AhetekandHosted Weblate b69e8fd649 Translated using Weblate (Polish)
Currently translated at 93.4% (287 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-07-23 23:19:49 +02:00
Lode SmetsandHosted Weblate 2e95cf908d Translated using Weblate (Dutch)
Currently translated at 64.8% (199 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2024-07-23 23:19:49 +02:00
MarioandHosted Weblate b589bae2b0 Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-07-23 23:19:48 +02:00
AhetekandHosted Weblate e7e4891d82 Translated using Weblate (Polish)
Currently translated at 93.4% (287 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-07-23 23:19:48 +02:00
advplyr 03a46decba Fix:Server connect form press enter to submit #1158 2024-07-08 17:21:31 -05:00
advplyrandGitHub d80482f443 Merge pull request #1250 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-28 15:50:19 -05:00
J. LavoieandHosted Weblate 735cce7614 Translated using Weblate (Finnish)
Currently translated at 39.0% (120 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-06-27 04:45:17 +00:00
J. LavoieandHosted Weblate 8ea35a0b36 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.3% (305 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-27 04:45:16 +00:00
J. LavoieandHosted Weblate ad90bd3d05 Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.1% (249 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-27 04:45:15 +00:00
J. LavoieandHosted Weblate fa3856e845 Translated using Weblate (Italian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-06-27 04:45:15 +00:00
dex girlandHosted Weblate 752d4ddc8c Translated using Weblate (Croatian)
Currently translated at 69.0% (212 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-06-27 04:45:05 +00:00
J. LavoieandHosted Weblate fd2d7a05d1 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-27 04:45:04 +00:00
J. LavoieandHosted Weblate ccf4675867 Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-27 04:45:03 +00:00
J. LavoieandHosted Weblate bc043c73ca Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-27 04:45:02 +00:00
Allan NordhøyandHosted Weblate 291ab247bc Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate d901f470a1 Translated using Weblate (Vietnamese)
Currently translated at 85.6% (263 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/vi/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 0ebb01e39d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 20ac99bf8d Translated using Weblate (Swedish)
Currently translated at 59.2% (182 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 90e86e1575 Translated using Weblate (Russian)
Currently translated at 99.3% (305 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 28652031e7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.3% (305 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate cd8be40cc8 Translated using Weblate (Polish)
Currently translated at 90.2% (277 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 52fc937f96 Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.1% (249 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 68e4722387 Translated using Weblate (Dutch)
Currently translated at 57.0% (175 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 5f5b38b2f9 Translated using Weblate (Lithuanian)
Currently translated at 59.2% (182 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 65f8d3ce81 Translated using Weblate (Italian)
Currently translated at 96.4% (296 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate ac571fd63d Translated using Weblate (Hungarian)
Currently translated at 91.5% (281 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate ea2d6e9f31 Translated using Weblate (Croatian)
Currently translated at 46.5% (143 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 80b999d648 Translated using Weblate (Hindi)
Currently translated at 24.1% (74 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hi/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 54720039e4 Translated using Weblate (Gujarati)
Currently translated at 23.4% (72 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/gu/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 8c6717c201 Translated using Weblate (French)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 6610754e25 Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate d38622d8a2 Translated using Weblate (German)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate cd2750a489 Translated using Weblate (Danish)
Currently translated at 64.1% (197 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-06-25 16:34:48 +02:00
Allan NordhøyandHosted Weblate 30140629ae Translated using Weblate (Czech)
Currently translated at 96.0% (295 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-25 16:34:48 +02:00
Allan NordhøyandHosted Weblate 0ce29337ef Translated using Weblate (Norwegian Bokmål)
Currently translated at 69.7% (214 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-25 15:36:41 +02:00
Allan NordhøyandHosted Weblate 6715f29ebb Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.7% (211 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-25 13:32:12 +00:00
phewiandHosted Weblate b4a8dc9e26 Translated using Weblate (Finnish)
Currently translated at 36.4% (112 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-06-25 13:32:12 +00:00
advplyrandGitHub c349a0bf00 Merge pull request #1248 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-25 08:32:05 -05:00
SunSpringandHosted Weblate 0dc6c142be Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-21 04:45:03 +00:00
PlazecandHosted Weblate e46c1d9120 Translated using Weblate (Czech)
Currently translated at 96.4% (296 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-20 04:45:02 +00:00
advplyrandGitHub 27ad554ea1 Merge pull request #1245 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-19 15:18:59 -05:00
CharlieandHosted Weblate 5660772b46 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-19 06:45:03 +02:00
kuci-JKandHosted Weblate dab80642ab Translated using Weblate (Czech)
Currently translated at 95.7% (294 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-19 06:45:02 +02:00
Nicholas WandHosted Weblate da46191208 Translated using Weblate (Bengali)
Currently translated at 57.6% (177 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2024-06-18 22:01:23 +02:00
JLandHosted Weblate 6484c4de79 Translated using Weblate (Danish)
Currently translated at 64.1% (197 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-06-18 22:01:23 +02:00
kuci-JKandHosted Weblate 6716b6042a Translated using Weblate (Czech)
Currently translated at 94.1% (289 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-18 22:01:23 +02:00
PlazecandHosted Weblate 0686859fa7 Translated using Weblate (Czech)
Currently translated at 94.1% (289 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-18 22:01:23 +02:00
advplyr 4180862c3a Fix: Sleep Timer modal defaults to Custom Time during first access #1224 2024-06-17 16:44:06 -05:00
PlazecandHosted Weblate 7aa20d3211 Translated using Weblate (Czech)
Currently translated at 92.8% (285 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-17 23:42:10 +02:00
pmangroandHosted Weblate b694b55710 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-17 23:42:10 +02:00
pmangroandHosted Weblate 024b613063 Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.8% (288 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-17 23:42:10 +02:00
SmoukusandHosted Weblate 8768d11a20 Translated using Weblate (Croatian)
Currently translated at 46.5% (143 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-06-17 23:42:10 +02:00
ValentinandHosted Weblate 5898303ba4 Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-17 23:42:10 +02:00
advplyr e79d09941c Update:Editor config/vscode settings/prettierrc 2024-06-16 09:49:05 -05:00
advplyr c52a60fe77 Add:Finnish language option 2024-06-16 09:44:09 -05:00
advplyrandGitHub e64f9aa427 Merge pull request #1239 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-16 09:40:59 -05:00
Petteri HjortandHosted Weblate ee343f78ef Translated using Weblate (Finnish)
Currently translated at 27.3% (84 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-06-16 06:45:04 +02:00
Vito0912andHosted Weblate 1f20d5821f Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-16 06:45:03 +02:00
Nicholas WandHosted Weblate b0c512e946 Added translation using Weblate (Finnish) 2024-06-15 16:32:35 +02:00
CharlieandHosted Weblate 0d13b7abd1 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-15 14:31:50 +00:00
SunSpringandHosted Weblate 43fd8285c9 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-15 06:45:09 +02:00
Illia PyshniakandHosted Weblate 2f7e8d618a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-06-15 06:45:08 +02:00
DmitryandHosted Weblate a0b910deeb Translated using Weblate (Russian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-06-15 06:45:08 +02:00
Petras ŠukysandHosted Weblate e9dc45613c Translated using Weblate (Lithuanian)
Currently translated at 59.2% (182 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-06-15 06:45:07 +02:00
Pierrick GuillaumeandHosted Weblate b5e90ec3d2 Translated using Weblate (French)
Currently translated at 87.9% (270 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-15 06:45:06 +02:00
DiamondtipDRandHosted Weblate 3d9f42c1e1 Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-15 06:45:06 +02:00
SunSpringandHosted Weblate e62f430f37 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-14 11:19:29 +02:00
burghy86andHosted Weblate 9c53564c46 Translated using Weblate (Italian)
Currently translated at 96.7% (297 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-06-14 11:19:29 +02:00
Daniel SchosserandHosted Weblate f7add6f6cd Translated using Weblate (German)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-14 11:19:29 +02:00
MarioandHosted Weblate 7786e6a827 Translated using Weblate (German)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-14 11:19:29 +02:00
apineiro97andHosted Weblate b213e2aabf Translated using Weblate (Spanish)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-14 06:45:03 +02:00
Vito0912andHosted Weblate ebca0fafca Translated using Weblate (German)
Currently translated at 95.1% (292 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-14 06:45:02 +02:00
advplyrandGitHub ac8a2ca985 Merge pull request #1235 from mfcar/mf/downloadTweaks
Tweak the download page (add library size and play episode)
2024-06-12 18:14:35 -05:00
advplyr 6380d7cc02 Fix local episode play-item event, set PodcastEpisode size and AudioTrack FileMetadata for downloads 2024-06-12 18:01:44 -05:00
advplyrandGitHub 9056ba6b8d Merge pull request #1234 from nichwall/localization_workflow_update
Update i18n workflow to 1.3.0
2024-06-11 17:03:21 -05:00
advplyrandGitHub 23801363c4 Merge pull request #1233 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-11 17:03:01 -05:00
Marcos Carvalho 1cdcd7327f Add play downloaded episode and library size 2024-06-11 19:31:10 +01:00
Nicholas Wallace 50ec50003e Update i18n workflow to 1.3.0 2024-06-10 19:25:34 -07:00
gallegonovatoandHosted Weblate 5ff4db1a3f Translated using Weblate (Spanish)
Currently translated at 100.0% (304 of 304 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-10 21:56:03 +00:00
advplyrandGitHub 394b44c343 Merge pull request #1222 from BimBimSalaBim/master
Update font settings in EpubReader and Reader components
2024-06-10 16:55:55 -05:00
advplyr eb2aa6f773 Map locally stored ebook settings onto defaults 2024-06-10 16:38:29 -05:00
advplyr 81130e04e5 Merge master 2024-06-10 16:32:09 -05:00
advplyrandGitHub c0ec0758f4 Merge pull request #1228 from weblate/weblate-audiobookshelf-test-abs-mobile-app
Translations update from Hosted Weblate
2024-06-10 16:23:07 -05:00
Nicholas WandHosted Weblate e21f7c545e Translated using Weblate (Chinese (Simplified))
Currently translated at 96.0% (292 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/zh_Hans/
2024-06-09 21:51:48 +02:00
Nicholas WandHosted Weblate c16dd2fcf6 Translated using Weblate (Vietnamese)
Currently translated at 86.5% (263 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/vi/
2024-06-09 21:51:48 +02:00
Nicholas WandHosted Weblate 90974e6d73 Translated using Weblate (Ukrainian)
Currently translated at 95.7% (291 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/uk/
2024-06-09 21:51:48 +02:00
Nicholas WandHosted Weblate 8e4f75d85b Translated using Weblate (Swedish)
Currently translated at 59.5% (181 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/sv/
2024-06-09 21:51:48 +02:00
Nicholas WandHosted Weblate 792ee2adb8 Translated using Weblate (Russian)
Currently translated at 94.0% (286 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/ru/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate 2c57489c07 Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.0% (286 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/pt_BR/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate e3321770fd Translated using Weblate (Polish)
Currently translated at 91.4% (278 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/pl/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate 848673f028 Translated using Weblate (Norwegian Bokmål)
Currently translated at 57.8% (176 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/nb_NO/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate e6d3003ecf Translated using Weblate (Dutch)
Currently translated at 57.5% (175 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/nl/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate 90551b7ce7 Translated using Weblate (Lithuanian)
Currently translated at 59.5% (181 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/lt/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate aabac5da13 Translated using Weblate (Italian)
Currently translated at 92.4% (281 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/it/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate 4fad77f9c1 Translated using Weblate (Hungarian)
Currently translated at 92.7% (282 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/hu/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate 0fb6d746eb Translated using Weblate (Croatian)
Currently translated at 45.7% (139 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/hr/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate 7fc712d19b Translated using Weblate (Hindi)
Currently translated at 24.3% (74 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/hi/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate d6018cfe10 Translated using Weblate (Gujarati)
Currently translated at 23.3% (71 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/gu/
2024-06-09 21:51:45 +02:00
Nicholas WandHosted Weblate 5b8f231a98 Translated using Weblate (French)
Currently translated at 86.8% (264 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/fr/
2024-06-09 21:51:45 +02:00
Nicholas WandHosted Weblate cb503f84e1 Translated using Weblate (Spanish)
Currently translated at 93.0% (283 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/es/
2024-06-09 21:51:45 +02:00
Nicholas WandHosted Weblate a34f9b80ac Translated using Weblate (Danish)
Currently translated at 58.5% (178 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/da/
2024-06-09 21:51:44 +02:00
Nicholas WandHosted Weblate 3e6696b516 Translated using Weblate (Czech)
Currently translated at 93.0% (283 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/cs/
2024-06-09 21:51:44 +02:00
Nicholas WandHosted Weblate 9cd8bd439a Translated using Weblate (English)
Currently translated at 20.3% (62 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/en/
2024-06-09 21:51:44 +02:00
Nicholas WandHosted Weblate ca5b99e23d Added translation using Weblate (Bengali) 2024-06-09 21:44:05 +02:00
Nicholas WandHosted Weblate c44fd74f5b Translated using Weblate (German)
Currently translated at 99.0% (301 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/de/
2024-06-09 21:40:09 +02:00
advplyrandGitHub f82c4834c3 Merge pull request #1225 from lukasitaly/patch-1
Update de.json
2024-06-08 16:03:45 -05:00
advplyrandGitHub dda95bd69c Merge pull request #1217 from mfcar/mf/networkStatusSwift
Implement network monitoring on iOS
2024-06-08 16:02:53 -05:00
LukasandGitHub a623cb57a7 Update de.json
Changed "Mobilfunknetz" to "mobile Daten", but I wouldn't really change the remaining strings with "Mobilfunknetz" just for the sake of uniformity, since "Mobilfunknetz" definitely fits better than "mobile Daten".
2024-06-08 17:01:49 +00:00
LukasandGitHub e798d930fb Update de.json
Translation of not yet translated strings
2024-06-07 08:42:05 +02:00
advplyrandGitHub f91b0c9e2f Merge pull request #1210 from mfcar/mf/limitCellularData
Add settings to limit Cellular Data (Download/Streaming)
2024-06-04 16:56:01 -05:00
advplyr b6824cf242 Fix:Latest podcast episodes to play local 2024-06-04 16:50:55 -05:00
advplyr 41477fdac1 Merge translation strings 2024-06-04 16:28:35 -05:00
advplyr d4e8afc2f1 Map translation strings 2024-06-04 16:23:23 -05:00
advplyr d42cbfe3e4 Revert podcast episode local playback event 2024-06-04 16:21:28 -05:00
advplyr c7c3ef52e6 Update issue templates to 0.9.73 and 0.9.74 2024-06-04 10:25:01 -05:00
advplyr 92344325b6 Add 0.9.64 to issue templates 2024-06-04 10:23:21 -05:00
Faizan Zafar a8172fb3f9 Update font settings in EpubReader and Reader components to include a "Font Boldness" slider which applies a stroke to the text #3020 on main repo 2024-06-03 20:25:11 -07:00
advplyrandGitHub a73ec783d5 Merge pull request #1221 from c-franco/update-spanish-translation
Update Spanish translation
2024-06-03 17:20:00 -05:00
Christian Franco 5c9fc4c9e4 Update Spanish translation 2024-06-03 16:31:40 +01:00
advplyr 494ff38ef3 Fix:Epub rendition resize on screen orientation change #1213 2024-06-02 16:20:41 -05:00
advplyr b7e6cbf1ab Fix:PDF ereader flickering 2024-06-02 15:54:50 -05:00
advplyrandGitHub b33623b74f Merge pull request #1215 from Dalabad/patch-1
Update de.json
2024-05-28 17:24:57 -05:00
Marcos Carvalho 4f939ce6fc Implement network monitoring on iOS 2024-05-27 22:16:49 +01:00
Daniel SchosserandGitHub 4f45f360df Update de.json 2024-05-27 22:50:11 +02:00
Marcos Carvalho 4ad54f34fe Improvements and fixes on the cellular permissions 2024-05-26 22:08:53 +01:00
Marcos Carvalho 574dda5286 Fix android build 2024-05-24 08:10:25 +01:00
Marcos Carvalho d788623509 Initial work 2024-05-23 22:33:06 +01:00
advplyrandGitHub f047f6a23b Merge pull request #1208 from mfcar/mf/detectableTimestamp
feat: Add Time Marker Detection in Episode Descriptions
2024-05-21 17:11:49 -05:00
advplyr 48b30f6b03 iOS version bump 0.9.74 2024-05-20 09:42:43 -05:00
Marcos Carvalho 67d32164d9 Update the parse description to identify time marker in anchor links 2024-05-20 08:33:02 +01:00
advplyr 6672a21049 Version bump v0.9.74-beta 2024-05-19 16:58:20 -05:00
Marcos Carvalho b833019a14 Change href link to span, to avoid change the page and improve code 2024-05-19 14:14:47 +01:00
Marcos Carvalho 00eeeb2a5a Add detectable timestamp 2024-05-19 10:40:04 +01:00
advplyrandGitHub 06a0a68143 Merge pull request #1205 from indiefan/command-target-duplication-fix
Fix bug iOS AudioPlayer where multiple calls to setupRemoteTransportC…
2024-05-17 17:05:14 -05:00
advplyrandGitHub 1a7ae468bd Merge pull request #1206 from cor-bee/patch-1
Update uk.json
2024-05-17 16:50:49 -05:00
Illia PyshniakandGitHub e8f74d7358 Update uk.json 2024-05-17 14:00:58 +03:00
Marke Hallowell 984f01da23 Fix bug iOS AudioPlayer where multiple calls to setupRemoteTransportControls resulted in duplicate MPRemoteCommand targets being applied for the same action. 2024-05-16 19:11:38 -07:00
advplyrandGitHub d6e75410a5 Merge pull request #1201 from nichwall/bug_report
App bug report format
2024-05-12 12:41:45 -05:00
Nicholas Wallace 7627e1b58c App bug report format 2024-05-11 18:24:38 -07:00
advplyrandGitHub 5fb31b18ae Merge pull request #1199 from nichwall/feature_request
Add: feature request
2024-05-10 17:55:03 -05:00
Nicholas WandGitHub 20903e8217 App selection allows for multiple 2024-05-09 07:12:02 -07:00
Nicholas WandGitHub 8fe348d4eb Update feature_request.yaml 2024-05-09 06:40:06 -07:00
Nicholas Wallace 24cda8a179 Add: feature request 2024-05-08 23:30:13 -07:00
advplyr fc954a968b Remove old missing parts references 2024-04-28 16:38:40 -05:00
advplyr aae53a54cd Update:UI/UX: Add explicit indicator next to titles #1181 2024-04-28 16:34:31 -05:00
advplyr cfedcffc98 Add:No series filter option for book libraries #1184 2024-04-28 16:00:57 -05:00
advplyrandGitHub 0e74c74a6c Merge pull request #1179 from indiefan/rate-manager
Refactor AudioPlayer to use a Rate Manager protocol.
2024-04-27 16:49:27 -05:00
advplyr fb86c45e78 iOS add defaultRate fixing merge 2024-04-27 16:41:29 -05:00
advplyrandGitHub 124300b215 Merge branch 'master' into rate-manager 2024-04-27 16:06:20 -05:00
advplyrandGitHub 6746c0627b Merge pull request #1178 from indiefan/master
Update Now Playing info with correct default rate.
2024-04-27 16:01:06 -05:00
Marke Hallowell 0f2cd3b132 Update Now Playing info with correct default rate so that third party apps don't mistake current status as scrubbing. 2024-04-21 15:39:37 -07:00
advplyr e94d6fdc1e Update:Android notification image to not resize #657 2024-04-19 16:32:16 -05:00
advplyr e301b3071c Update:Add 1.7x playback speed to modal #1180 2024-04-19 14:39:00 -05:00
Marke Hallowell 3d3dad5e4a Adding new sources to project file. 2024-04-14 16:47:38 -07:00
Marke Hallowell f8a9033738 Add an AudioPlayerRateManager implementation for iOS 16+. 2024-04-14 16:12:55 -07:00
Marke Hallowell edb25f5bcd Refactor AudioPlayer to use an AudioPlayerRateManager protocol for managing rate/defaultRate state management. Implements a LegacyAudioPlayerRateManager implementation with existing (pre-iOS 16) behavior. 2024-04-14 11:45:03 -07:00
Marke Hallowell bcb239efd1 Update Now Playing info with correct default rate so that third party apps don't mistake current status as scrubbing. 2024-04-13 23:10:03 -07:00
advplyrandGitHub d208211c6f Merge pull request #1176 from Fymyte/patch-4
Update French Translation
2024-04-12 15:22:06 -05:00
Pierrick GuillaumeandGitHub 0b20b72241 Update fr.json 2024-04-12 20:32:47 +02:00
Pierrick GuillaumeandGitHub 5c29459af2 Update fr.json 2024-04-12 20:31:15 +02:00
advplyrandGitHub e54fd38784 Merge pull request #1174 from nichwall/i18n-update
Update i18n workflow to use 1.2.0
2024-04-11 17:17:23 -05:00
Nicholas Wallace 94774f13df Update i18n workflow to use 1.2.0 2024-04-10 18:38:11 -07:00
advplyr 43ec35fe67 Fix:Android device info client version 2024-04-05 16:39:24 -05:00
advplyrandGitHub b2645dc444 Merge pull request #1162 from springsunx/patch-1
Update zh-cn.json
2024-04-03 18:16:14 -05:00
SunXandGitHub c79624a82c Update zh-cn.json 2024-04-03 09:06:13 +08:00
advplyr 8201f3c9a6 Fix:Close sleep timer custom timer sub modal on sleep timer close #1112 2024-04-01 17:35:22 -05:00
advplyrandGitHub e0ac6a1c38 Merge pull request #1160 from pmangro/master
[PR-BR] Go to Web Client
2024-04-01 17:18:33 -05:00
pmangroandGitHub 400da5566d [PR-BR] Go to Web Client 2024-03-31 19:50:45 -03:00
advplyr 9b67037774 Update:Support for comic files with webp images https://github.com/advplyr/audiobookshelf/issues/2792 2024-03-27 16:13:13 -05:00
advplyr 48596542c7 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-03-25 16:33:36 -05:00
advplyr 9715d65d88 Update:Go to web client buttons visible to all users #1079 2024-03-25 16:33:30 -05:00
advplyrandGitHub 2e14b31a69 Merge pull request #1153 from burghy86/patch-4
Update it.json
2024-03-25 14:51:30 -05:00
burghy86andGitHub 1c8ce6c3db Update it.json
monthly translation of new strings
2024-03-25 16:29:07 +01:00
advplyrandGitHub dc74a6213d Merge pull request #1150 from nichwall/translation_faq_link
Translation guide link added
2024-03-24 05:06:47 -05:00
Nicholas Wallace e9a66bb7a7 Translation guide link added 2024-03-23 20:08:41 -07:00
advplyr 3c6d4b5f19 Add:Nav button in side drawer and item more menu to go to web client #1079 2024-03-22 17:54:56 -05:00
advplyrandGitHub f04eebe0d8 Merge pull request #1142 from nichwall/translation_workflow_check
Add: i18n workflow check
2024-03-20 16:43:26 -05:00
advplyrandGitHub 63b5227ccc Merge pull request #1140 from cor-bee/master
Update Ukrainian translation
2024-03-20 16:31:00 -05:00
Nicholas Wallace 934d9b1df4 Fixed release number of action 2024-03-19 21:19:38 -07:00
Nicholas Wallace 8994cb3a6e Added newline to force check 2024-03-19 21:12:52 -07:00
Nicholas Wallace 4a32de482b Example: German keys alphabetized 2024-03-19 21:09:49 -07:00
Nicholas Wallace 95f47793d2 Example: German strings out of order 2024-03-19 21:08:43 -07:00
Nicholas Wallace f27048618d Add: newline to end of file 2024-03-19 21:05:53 -07:00
Nicholas Wallace c90ad419e8 Add: i18n workflow check 2024-03-19 21:05:09 -07:00
Illia PyshniakandGitHub 2cdc3ecb9d Update uk.json 2024-03-19 23:09:35 +02:00
advplyr 9f89fcd968 Fix:Podcast library playlists page incorrect label in toolbar #1135 2024-03-19 12:52:14 -05:00
advplyrandGitHub 7c2d51f811 Merge pull request #1136 from cor-bee/master
Ukrainian translation
2024-03-18 17:44:49 -05:00
advplyr 41fe7a29cd Merge 2024-03-18 17:41:13 -05:00
advplyrandGitHub 54f3eec303 Merge pull request #1127 from nichwall/vietnamese_translations
Vietnamese translations
2024-03-18 17:36:58 -05:00
advplyr cd91013f69 Update Vietnamese datefnslocale code 2024-03-18 17:37:04 -05:00
advplyrandGitHub 0af16ef05f Merge pull request #1139 from pmangro/master
[PT-BR] Remove from Playlist
2024-03-18 11:57:25 -05:00
pmangroandGitHub 5ba830d56e [PT-BR] Remove from Playlist 2024-03-18 09:45:46 -03:00
Illia PyshniakandGitHub 81919e963a Add Ukrainian language to i18n.js 2024-03-15 22:28:00 +02:00
Illia PyshniakandGitHub 8a1d4bb958 Ukrainian app translation 2024-03-15 22:26:52 +02:00
Nicholas Wallace eabac0e709 Add: Vietnamese to language code map 2024-03-09 16:23:25 -07:00
Nicholas Wallace d90783d7c8 Fixed whitespace 2024-03-09 16:20:24 -07:00
Nicholas Wallace faf8211e5c Add: Vietnamese app translations 2024-03-09 16:20:00 -07:00
advplyr 65d90947c5 Fix:Remove from playlist button #987 2024-03-04 14:56:58 -06:00
advplyr 752e918d26 iOS version bump 0.9.73 & update dependencies 2024-03-03 16:28:55 -06:00
advplyr ad08cc69f4 Version bump 0.9.73-beta 2024-03-03 14:15:32 -06:00
advplyr da3882fd5e Fix:Playlist tab not shown after adding first playlist #1092 2024-03-03 13:29:58 -06:00
advplyr c04c7a55d9 Fix:iOS progress bar overflowing edge of screen 2024-03-03 12:52:17 -06:00
advplyr 3318846b43 Add:Remove from playlist option in more menu for items on playlist page #987 2024-03-02 15:25:18 -06:00
advplyrandGitHub e0860feeb9 Merge pull request #1109 from megamegax/hu
feat(i18n): add Hungarian localization
2024-03-01 15:39:15 -06:00
Hunyady Mihály c0253a69b9 feat(i18n): replace NoItemsFound translation to a more general one 2024-03-01 22:09:33 +01:00
Hunyady Mihály 9242c7e6e0 feat(i18n): small grammar adjustments 2024-03-01 22:04:09 +01:00
Hunyady Mihály 0bfc579ae4 feat(i18n): adjust translation 2024-03-01 21:39:06 +01:00
megamegax 5fad06756b feat(i18n): add Hungarian localization 2024-03-01 20:03:21 +01:00
advplyrandGitHub 5be6afa6c1 Merge pull request #1106 from pmangro/0.9.72.a-PT-BR
[PT-BR] Translated LabelAllowSeekingOnMediaControls
2024-02-27 15:58:36 -06:00
pmangroandGitHub 5ba3b8be7a [PT-BR] Translated LabelAllowSeekingOnMediaControls 2024-02-27 14:39:21 -03:00
advplyr 0eb62f541d Fix:Local media item page long titles overflowing screen pushing play & more menu button offscreen #1103 2024-02-25 15:35:23 -06:00
advplyrandGitHub 6baff881c7 Merge pull request #1094 from fidoriel/disable-widget-ios-scrubing
Disable ios/android scrubbing in MPRemoteCommandCenter/MediaBrowserServiceCompat
2024-02-25 15:29:52 -06:00
fidoriel 8c23c1e795 german translation for LabelAllowSeekingOnMediaControls 2024-02-25 22:25:17 +01:00
advplyr 9706c66a27 iOS Update Realm schema version for new setting, update remote transport controls on device settings updated 2024-02-25 15:04:54 -06:00
advplyr f4f7252537 Update wording for enable seeking on media controls settings, remove restart required help text, map translations 2024-02-25 14:44:40 -06:00
advplyr 430d200151 Update android disable media control seek setting to update in realtime 2024-02-25 14:11:28 -06:00
advplyr 7be82d03f3 Merge branch 'master' into disable-widget-ios-scrubing 2024-02-25 12:51:58 -06:00
advplyr 15aaef742d Fix:Ignore swiping for audio player when modal is open #1099 2024-02-24 15:00:29 -06:00
advplyrandGitHub e443ba09da Merge pull request #1095 from pmangro/0.9.72.a-PT-BR
Added PT-BR Strings
2024-02-24 14:49:35 -06:00
luca 8235d1f1f7 reorder setting and add info label 2024-02-22 20:27:33 +01:00
pmangroandGitHub 9a39dcceee Update i18n.js to add pt-br 2024-02-21 08:55:45 -03:00
pmangroandGitHub a707b7fc86 Typos corrected 2024-02-21 08:53:34 -03:00
fidoriel b6fea85ef4 fix compile android 2024-02-21 10:16:47 +01:00
advplyr 7e3b3f55ae Fix:Android auto not loading server items #1081 2024-02-20 17:05:07 -06:00
fidoriel d2d3bbe8a1 fix default false 2024-02-20 22:08:47 +01:00
fidoriel 41a65c0f9d add setting for widget scrubbing 2024-02-20 21:58:31 +01:00
pmangroandGitHub 165ddd58a6 Update pt-br.json - initial file 2024-02-20 17:29:03 -03:00
pmangroandGitHub 09dc04c0de Created pt-br.json 2024-02-20 15:40:29 -03:00
advplyrandGitHub 57d5dbc0d0 Merge pull request #1082 from arcao/patch-1
Translate cs.json
2024-02-14 18:42:25 -06:00
Martin SloupandGitHub 11af75e127 Translate cs.json
Translate untranslated texts to Czech and fix already translated texts.
2024-02-14 02:10:33 +01:00
advplyr f8d82a3020 iOS version bump 0.9.72 2024-02-11 08:54:54 -06:00
advplyr 81d7b1cfd7 Version bump 0.9.72-beta 2024-02-10 16:35:01 -06:00
advplyr e15c1bcb32 Fix:Continue listening shelves show on initial failed connection #1038 2024-02-09 17:52:24 -06:00
advplyr 826d414d9f Update:iOS do not resume playback after app suspended. Attempted fix for #973 2024-02-07 18:02:25 -06:00
advplyr 0da6fca727 Fix:iOS 0 second listening sessions #970 and include device info with offline sessions 2024-02-05 19:04:11 -06:00
advplyr 2327bc1705 Update playMethod to fallback to 0 2024-02-04 18:14:24 -06:00
advplyr 3bb76cecac Fix:Android respect offline mode on app restart 2024-02-04 17:46:56 -06:00
advplyr e3f82bb237 Fix:Include device info with sync local-all request 2024-02-04 17:27:57 -06:00
advplyr fcd6270f2d Fix:Hide library list button from app bar when no network connection 2024-02-04 15:53:05 -06:00
advplyr d5411703ca Update:Add 10 second timeout when loading home page shelves 2024-02-04 15:48:37 -06:00
advplyr 8fc2058db8 Update:Handle navigating to library item pages when server connection goes down
- Added loading indicator to library item page on initial request to server
- Redirect to downloaded item page if server item request fails
2024-02-04 15:46:26 -06:00
advplyr d03949f5d1 Update:Audio player title scrolls when overflow #740
- increase width of the title on minimized player to scale with screen width
- mini player shows media title and chapter title
2024-02-03 17:31:53 -06:00
advplyr ff01786063 Remove unused fastSort.js 2024-02-03 13:28:41 -06:00
advplyr bb117ab4da Update:Authors page increase size to fill width with 2 per row #1026 2024-02-02 18:33:23 -06:00
advplyr f5ec247b47 Update:Minimized audio player allow swipe up to fullscreen #1046 2024-02-01 18:20:10 -06:00
advplyr 6e6dd00523 Update:Allow swiping down anywhere on fullscreen audio player to minimize #1046 2024-02-01 17:51:10 -06:00
advplyr c9fb98f033 Fix:Android crash on startup due to SimpleStorage #1040
- Update SimpleStorage to 1.5.5-SNAPSHOT
2024-01-29 16:46:39 -06:00
advplyr 75ecff5e6d Remove @nuxtjs/tailwindcss dependency and replace with standalone tailwindcss 2024-01-25 17:39:22 -06:00
advplyrandGitHub 91a9f918fb Merge pull request #1052 from bloodscript/issue-1051-german-localization-improvement
[issue-1051] German Localization
2024-01-24 14:18:17 -06:00
Steffen Meiselbach 03305f90ca corrected ereader based on current Duden entry 2024-01-23 12:38:31 +01:00
Steffen Meiselbach 9428bc07c5 reverted to uniformly uncapitalized pronouns 2024-01-23 12:37:03 +01:00
Steffen Meiselbach f38f9c3502 ebook written as used in Duden and uniformly capitalized version of Du 2024-01-23 12:33:01 +01:00
Steffen Meiselbach 64a796aece standardized addressing (du), missing translations and uniformly starting with capital character 2024-01-23 07:24:40 +01:00
advplyr 63039fe8c9 Fix pl.json formatting 2024-01-22 17:34:59 -06:00
Steffen Meiselbach 3f2b723d0f (sleep) timer is typically used in german parlance 2024-01-22 22:55:28 +01:00
advplyr ffd7fb1cf9 Update workflows to use node 20 2024-01-21 15:56:24 -06:00
advplyrandGitHub b049b206cf Merge pull request #1049 from Bogumio/patch-2
Update pl.json
2024-01-21 15:31:27 -06:00
advplyr 35f56dc8a9 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-01-21 15:32:13 -06:00
advplyr a71cd326cc Update readme node version to 20 2024-01-21 15:32:06 -06:00
BogumioandGitHub cc841d1c7c Update pl.json
The rest of the translations have been completed. Minor corrections and unifications.
2024-01-21 12:04:06 +01:00
advplyrandGitHub 16ecdcf96e Merge pull request #1042 from Bogumio/patch-1
Update pl.json
2024-01-14 11:15:48 -06:00
BogumioandGitHub 96ec56d1ad Update pl.json
Most of the missing phrases have been completed. I will try to translate the rest soon.
2024-01-14 18:07:55 +01:00
advplyr 9b15bafc9f Fix:Settings page button/toggle touch area #1021 2024-01-11 17:27:41 -06:00
advplyr aa02f2e465 Update readme for iOS beta full 2024-01-06 14:15:19 -06:00
advplyrandGitHub 15464617ef Merge pull request #1032 from burghy86/patch-3
Update it.json
2024-01-05 10:21:43 -06:00
burghy86andGitHub 60ecceb59d Update it.json
all string translate
2024-01-05 10:18:48 +01:00
advplyr 3fe943e989 Add:Player setting to scale elapsed time by playback speed #1028
- Moved player settings to new playerSettings key in storage preferences
2024-01-04 15:28:25 -06:00
advplyr 5e7d039454 iOS version bump 0.9.71 2024-01-01 13:55:13 -06:00
advplyr f928df146a Version bump 0.9.71-beta 2024-01-01 12:56:12 -06:00
advplyr 6e57cb49c4 Update:Move modal titles down to not overlap with ios status bar 2024-01-01 12:24:41 -06:00
advplyr 2f8494b013 Add:Tags to item page & allow wrapping to multiple lines #1014 2024-01-01 12:08:15 -06:00
advplyr 6f1b93bd9f Update:Default to alt view so that wooden shelf does not show on initial load 2024-01-01 11:55:57 -06:00
advplyr 19b928d7f6 Fix:Download marker overlapping series sequence on book cover #996 2024-01-01 11:53:28 -06:00
advplyr b849be6f5e Add:Epub reader setting to force single page #1018 2024-01-01 11:31:45 -06:00
advplyrandGitHub 86096df1a4 Merge pull request #1003 from benonymity/playerSwipeBack
Disable iOS swipeBack in fullscreen audio player
2024-01-01 10:52:35 -06:00
advplyrandGitHub 98a465c7b4 Update components/app/AudioPlayer.vue 2024-01-01 10:51:46 -06:00
advplyr 3315f61ae4 Merge branch 'master' into playerSwipeBack 2024-01-01 10:46:01 -06:00
advplyrandGitHub f977445d7a Merge pull request #1004 from benonymity/statusBarTweaks
iOS Status Bar Tweaks
2024-01-01 10:31:58 -06:00
advplyr 095d5e1cec Fix item page gradient for light mode 2024-01-01 10:26:06 -06:00
advplyr 69fbfa4b33 Update nuxtjs/tailwindcss and update tailwind config 2024-01-01 10:19:55 -06:00
advplyr 1c89d33726 Update item page gradient to be absolute and below cover image, add transitions 2024-01-01 09:57:51 -06:00
advplyr acee907932 Merge branch 'master' into statusBarTweaks 2024-01-01 09:00:47 -06:00
advplyrandGitHub 08ad216fe2 Merge pull request #1002 from benonymity/iosChapterTrack
iOS Now Playing Chapter Track (updated #372)
2024-01-01 08:51:59 -06:00
advplyr 3cc7a0937c Fix seeking in NowPlaying to adjust for chapter start time 2024-01-01 08:49:39 -06:00
advplyr 322cad5548 Move AbsAudioPlayer.setChapterTrack to AudioPlayer instead of localStore 2024-01-01 08:22:20 -06:00
advplyr 73d70dd480 Merge branch 'master' into iosChapterTrack 2024-01-01 08:11:59 -06:00
advplyrandGitHub 56bc569d4a Merge pull request #1016 from KaiStarkk/hotfix-pr-1005
[Fix] Prevent crash from force unwrapping nil value in PR-1005
2023-12-31 16:06:50 -06:00
advplyr 981ae58f6a Update:Podfile for capacitor filesharer 2023-12-31 16:02:29 -06:00
Kieran Hannigan b2bfa4c002 [Fix] Prevent crash from force unwrapping nil value in PR #1005 2023-12-30 11:16:10 +08:00
advplyrandGitHub 8b6003c5e3 Merge pull request #1005 from benonymity/playPauseFix
Add togglePlayPauseCommand route to iOS
2023-12-29 15:53:20 -06:00
advplyr 017b6237ca Fix:Filter dialog clear filter button overflow notch on ios #999 2023-12-27 17:23:55 -06:00
advplyr d5496c2e9a Fix:Library list view long titles overflowing, clamped to 2 lines #1013 2023-12-27 17:10:57 -06:00
advplyr 5ef8961c33 Fix:Comma separated links on item page 2023-12-27 17:03:36 -06:00
advplyr e8bea48577 Map shelf translation strings 2023-12-26 16:40:28 -06:00
advplyrandGitHub 6cae78f5c2 Merge pull request #1007 from Jafeyyu/master
Update zh-cn.json
2023-12-26 16:34:01 -06:00
JafeyyuandGitHub d9d31a0620 Update zh-cn.json 2023-12-26 18:09:17 +08:00
advplyr 3682c5fca2 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-25 15:35:34 -06:00
advplyr 14b04a14b6 Add:Year in review cards to stats page 2023-12-25 15:35:29 -06:00
advplyrandGitHub f218d90bfb Merge pull request #997 from Jafeyyu/master
Update zh-cn.json
2023-12-24 12:02:17 -06:00
advplyrandGitHub 020f936ad4 Merge pull request #991 from Hallo951/master
Update de.json
2023-12-24 12:01:58 -06:00
benonymity 224e5efe48 add: togglePlayPause route 2023-12-23 01:51:37 -05:00
benonymity 439ab2c8c0 fix: item page background conflict 2023-12-23 01:45:40 -05:00
benonymity 26d0209763 fix: ios status bar mirror theme 2023-12-23 00:00:50 -05:00
benonymity 29086fc783 fix: no swipeback when audio player fullscreen 2023-12-22 22:34:24 -05:00
benonymity bb656a265f fix: broken isIos 2023-12-22 21:36:49 -05:00
benonymity b11ad9dc5b fix: only call on ios and remove...very professional logging 2023-12-22 21:18:55 -05:00
benonymity 2b1667e532 feat: now playing chapter track 2023-12-22 21:01:37 -05:00
JafeyyuandGitHub 228a198598 Update zh-cn.json 2023-12-21 16:09:21 +08:00
Hallo951andGitHub 2843308fb8 Update de.json 2023-12-19 09:03:06 +01:00
advplyr 793f0c05f7 iOS version bump 0.9.70 2023-12-18 17:15:22 -06:00
advplyr cf7271dc57 Version bump 0.9.70 2023-12-18 17:10:07 -06:00
advplyr 54664b0c32 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-18 17:05:40 -06:00
advplyr ecf910dc55 Fix:Play button loading indicator when offline #990 2023-12-18 17:05:34 -06:00
advplyr 460daec72e iOS version bump 0.9.69 2023-12-16 17:21:43 -06:00
advplyr 4dd67adcab Fix:Prevent pressing download buttons twice causing crash in iOS 2023-12-16 17:21:19 -06:00
advplyr 023c83ee78 Version bump 0.9.69-beta 2023-12-16 16:24:07 -06:00
advplyr 2b8ed16616 Update:UI for playlist & rss feed modal to add more padding on top and bottom to prevent iOS cutoff 2023-12-16 15:06:49 -06:00
advplyr 6ae4d72b68 Update:Settings UI cleanup and light theme updates 2023-12-16 14:43:48 -06:00
advplyr c6bd93ef5c Light theme updates 2023-12-16 14:37:05 -06:00
advplyr e1c02ce74c Update:Show loading indicator on play buttons when starting playback 2023-12-15 17:35:37 -06:00
advplyr efc6d68403 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-15 16:05:53 -06:00
advplyr 7412d0899f Fix:Syncing when media is open in audio player and playing on another device #984
- Local playback session IDs have been updated to UUIDs instead of prefixing local_play_
2023-12-15 16:05:19 -06:00
advplyrandGitHub 42eb0a8384 Merge pull request #988 from burghy86/patch-2
Update it.json
2023-12-15 15:03:25 -06:00
burghy86andGitHub 27e83e8ed9 Update it.json 2023-12-15 22:02:26 +01:00
advplyr dd66042766 Update:Dont show chapter number infront of chapter title #981 2023-12-14 16:59:47 -06:00
advplyr dc790d5450 Fix light mode for new podcast page 2023-12-14 16:55:17 -06:00
advplyr 01ac844024 Fix light theme on add podcast page 2023-12-14 16:39:55 -06:00
advplyr c8064ac7bc Rename page name from search to add-podcast 2023-12-14 16:25:59 -06:00
advplyrandGitHub f5da44dc6f Merge pull request #986 from Hallo951/master
Update de.json
2023-12-14 08:15:26 -06:00
Hallo951andGitHub cbdff29684 Update de.json 2023-12-14 08:19:27 +01:00
advplyr 19d6c84113 Update:Light theme colors for track progress bar and ereader settings #916 2023-12-13 16:57:44 -06:00
advplyrandGitHub d9f4ed3d54 Merge pull request #983 from Nab0y/master
Fix Russian localization
2023-12-12 17:50:06 -06:00
advplyr fdb26b7700 Update:Set device language to match the first server connected to #448 2023-12-12 17:50:04 -06:00
Dmitry Naboychenko 94cb7a65a2 Fix Russian localization 2023-12-12 22:34:53 +03:00
advplyr 9298065934 Add:Theme option setting #916 2023-12-11 18:11:17 -06:00
advplyr 1cf01a5d12 Add home page shelf translation strings #448 2023-12-11 17:42:29 -06:00
advplyr e3aa96cbd5 Fix:Setting device language code 2023-12-11 17:27:26 -06:00
advplyr c7678da664 Update:Setup css colors for themes, add light theme colors #916 2023-12-10 17:53:27 -06:00
advplyrandGitHub 52701048ad Merge pull request #967 from tonyedwardspz/master
Feature - Add a low feedback mode
2023-12-09 15:14:52 -06:00
advplyr 776c02828f Fix latest page to show download indicator, update download button to be responsive right away 2023-12-09 14:57:44 -06:00
advplyr 69308413f6 Fix bad merge with master 2023-12-08 17:04:03 -06:00
advplyr 84e376a7cf Remove commented out toasts 2023-12-08 17:03:08 -06:00
advplyr 1822b21af8 Remove socketConnected check on local items alert 2023-12-08 16:49:12 -06:00
advplyr 8b222435e6 Fix:Load local settings before attempting server connection #976 2023-12-08 16:10:57 -06:00
advplyrandGitHub fdd251ec2c Merge pull request #975 from Jafeyyu/master
Improve Chinese translation
2023-12-08 16:01:24 -06:00
Tony Edwards 78170d4e10 Potential toasts for removal 2023-12-08 09:59:22 +00:00
Tony Edwards 837981b8b6 Removes download prompt on podcast ep page 2023-12-08 09:56:07 +00:00
Tony Edwards be23c6703f Shows media link status when connected to a server 2023-12-08 09:08:51 +00:00
Tony Edwards 0c72889917 Remove successfully connected media link message 2023-12-08 07:56:45 +00:00
Tony Edwards 02746bc35f Fixes my introduced errors 2023-12-08 07:53:35 +00:00
Qi, Xiaoqiang 85a5c33bd8 Improve Chinese translation 2023-12-08 15:33:37 +08:00
Tony Edwards 5067c0f6d7 Removes download confirmation prompt 2023-12-08 07:30:54 +00:00
Tony Edwards 89938ab120 Removes download success toast 2023-12-08 07:25:51 +00:00
Tony Edwards 9a32396daa Removes low feedback toggle 2023-12-08 07:19:23 +00:00
advplyr ddb1796891 Update serverconfig to remove version before saving 2023-12-07 17:02:50 -06:00
advplyrandGitHub 76253b7b9d Merge pull request #969 from Sapd/sso-redirect-ri
SSO: Add redirect_uri to oAuth auth request
2023-12-07 16:08:19 -06:00
advplyr 74fef1d2f4 Make backwards compatible with server v2.6.0 2023-12-07 11:07:37 -06:00
advplyrandGitHub 7306ffa709 Merge pull request #972 from JannikGohr/german_translation
Add German Translations
2023-12-06 17:45:41 -06:00
JannikGohr 6f289293cc Continue german translation in strings/de.json 2023-12-07 00:09:34 +01:00
advplyr 783f924bb9 Add:Ebook library filters #971 2023-12-05 16:14:20 -06:00
Denis Arnst 044a35aacf SSO: Removed isRest as requirement
Removed in the backend to increase compatibility with possible 3rd party oauth libraries people might use
2023-12-05 09:44:20 +01:00
Tony Edwards 78448894d2 Adds hiding audio book feedback promt/toast 2023-12-05 07:31:03 +00:00
Tony Edwards aca348e783 Adds low feedback mode to podcast downloads 2023-12-05 07:31:03 +00:00
Tony Edwards e6c1874cbb Adds download success toasts to low feedback mode 2023-12-05 07:31:03 +00:00
Tony Edwards 1b06dbd78c Adds basic low feedback mode implementation 2023-12-05 07:31:03 +00:00
advplyr a26f37aa49 Add more translation strings, clean out unused strings #448 2023-12-04 17:53:36 -06:00
Denis Arnst 8af3c09458 SSO: Add redirect_uri to oAuth auth request
Also:
- Add client_id with the name of the app (for possible future use)
- Add response_type simply to provide all required oauth2 parameters for reference
- For the URL to be opened in Browser: Forward the callback/redirect_uri URL provided by the server
- Use a hardcoded variable for HTTPS enforcement, for easier debugging
2023-12-04 22:43:14 +01:00
advplyr fed6579e08 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-03 17:37:14 -06:00
advplyr 6621f8b2ee Add:Language code setting and translations #448 2023-12-03 17:37:01 -06:00
advplyrandGitHub 737b6d7c26 Merge pull request #964 from Sapd/seek-fix
Fix track skipping on iOS (Fixes #686)
2023-11-30 17:06:33 -06:00
Denis Arnst 59c71b71fb Fix track skipping on iOS (Fixes #686)
When we seek into a different chapter, we will have to reset the progress of the current one before seeking to the new one.
2023-11-29 17:30:34 +01:00
192 changed files with 13478 additions and 20249 deletions
+1 -6
View File
@@ -1,13 +1,8 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
trim_trailing_whitespace = true
-37
View File
@@ -1,37 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
labels: bug
---
### Steps to reproduce
1.
2.
3.
### Expected behaviour
- Tell us what should happen
### Actual behaviour
- Tell us what happens
### Environment data
Audiobookshelf Version:
- [ ] Android App?
- [ ] iOS App?
#### Android Issue
Android version:
Device model:
Stock or customized system:
#### iOS Issue
iOS Version:
iPhone model:
+88
View File
@@ -0,0 +1,88 @@
name: 🐞 ABS App Bug Report
description: File a bug/issue and help us improve the Audiobookshelf mobile apps.
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: "## App Bug Description"
- type: markdown
attributes:
value: "Thank you for filing a bug report! 🐛"
- type: markdown
attributes:
value: "Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug."
- type: textarea
id: what-happened
attributes:
label: What was the Problem?
description: Please help us understand the problem.
placeholder: Describe the problem you saw or experienced in detail! Include a screenshot or video, if applicable.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce the Issue
description: Please help us understand how we can reliably reproduce the issue.
placeholder: "1. Go to the library page of a Podcast library and..."
validations:
required: true
- type: textarea
id: what-was-expected
attributes:
label: What was Expected?
description: Please help us understand what should have happened.
placeholder: Please help us visualize by including a doodle or screenshot.
validations:
required: true
- type: markdown
attributes:
value: "## Mobile Environment"
- type: input
id: phone-model
attributes:
label: Phone Model
description: What kind of phone are you using?
placeholder: e.g. Pixel 6, iPhone 14, Samusung Galaxy s23, etc
validations:
required: true
- type: input
id: phone-os
attributes:
label: Phone OS
description: What operating system? If Android, is it stock or a custom system?
placeholder: e.g. Android 14, iOS 17.4, etc
validations:
required: true
- type: dropdown
id: app-version
attributes:
label: Audiobookshelf App Version
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true
options:
- Android App - 0.9.74
- iOS App - 0.9.74
- Android App - 0.9.73
- iOS App - 0.9.73
validations:
required: true
- type: dropdown
id: install-source
attributes:
label: Installation Source
multiple: true
options:
- Google Play Store
- Testflight
- SideStore
- Other (List in "Additional Notes")
validations:
required: true
- type: textarea
id: additional-notes
attributes:
label: Additional Notes
description: Anything else you want to add?
placeholder: "e.g. I have tried X, Y, and Z."
-18
View File
@@ -1,18 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
labels: enhancement
---
### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
### Describe the solution you'd like
A clear and concise description of what you want to happen.
### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
### Additional context
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,57 @@
name: 🚀 App Feature Request
description: Request a feature/enhancement
title: "[Enhancement]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: "## App Feature Request Description"
- type: markdown
attributes:
value: "Please first search in both issues & discussions for your enhancement and make sure your app is up to date."
- type: textarea
id: describe
attributes:
label: Describe the Feature/Enhancement
description: Please help us understand what you want.
placeholder: What is your vision?
validations:
required: true
- type: textarea
id: the-why
attributes:
label: Why would this be helpful?
description: Please help us understand why this would enhance your experience.
placeholder: Explain the "why" or "use case".
validations:
required: true
- type: textarea
id: image
attributes:
label: Future Implementation (Screenshot)
description: Please help us visualize by including a doodle or screenshot.
placeholder: How could this look or where could it go?
validations:
required: true
- type: markdown
attributes:
value: "## App Current Implementation"
- type: dropdown
id: version
attributes:
label: Audiobookshelf App Version
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true
options:
- Android App - 0.9.74
- iOS App - 0.9.74
- Android App - 0.9.73
- iOS App - 0.9.73
validations:
required: true
- type: textarea
id: current-image
attributes:
label: Current Implementation (Screenshot)
description: What screen were you looking at when you thought of this enhancement?
placeholder: If an image is not applicable, please explain why.
+2 -2
View File
@@ -16,10 +16,10 @@ jobs:
- name: checkout sources
uses: actions/checkout@v3
- name: use Node.js 16.x
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
- name: Set up Java
uses: actions/setup-java@v2
@@ -0,0 +1,20 @@
name: Close fixed issues on release.
on:
release:
types: [published]
permissions:
contents: read
issues: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Close issues marked as fixed upon a release.
uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5
with:
label: 'awaiting release'
removeLabel: true
applyToAll: true
message: Fixed in [${releaseTag}](${releaseUrl}).
+2 -2
View File
@@ -15,10 +15,10 @@ jobs:
- name: checkout sources
uses: actions/checkout@v3
- name: use Node.js 16.x
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
- name: Set up Java
uses: actions/setup-java@v2
+28
View File
@@ -0,0 +1,28 @@
name: Verify all i18n files are alphabetized
on:
push:
pull_request:
paths:
- strings/** # Should only check if any strings changed
jobs:
update_translations:
runs-on: ubuntu-latest
steps:
# Check out the repository
- name: Checkout repository
uses: actions/checkout@v4
# Set up node to run the javascript
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '20'
# The only argument is the `directory`, which is where the i18n files are
# stored.
- name: Run Update JSON Files action
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.3.0
with:
directory: 'strings/' # Adjust the directory path as needed
+17
View File
@@ -0,0 +1,17 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 400,
"proseWrap": "never",
"trailingComma": "none",
"overrides": [
{
"files": ["*.html"],
"options": {
"singleQuote": false,
"wrapAttributes": false,
"sortAttributes": false
}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"octref.vetur"
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"vetur.format.defaultFormatterOptions": {
"prettier": {
"semi": false,
"singleQuote": true,
"printWidth": 400,
"proseWrap": "never",
"trailingComma": "none"
},
"prettyhtml": {
"printWidth": 400,
"singleQuote": false,
"wrapAttributes": false,
"sortAttributes": false
}
},
"editor.formatOnSave": true,
"editor.detectIndentation": true,
"editor.tabSize": 2,
"javascript.format.semicolons": "remove",
"[javascript][json][jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
}
}
+5 -3
View File
@@ -33,8 +33,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 99
versionName "0.9.68-beta"
versionCode 107
versionName "0.9.76-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app"
@@ -64,6 +64,8 @@ repositories {
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
mavenCentral()
// TODO: Temporarily using SNAPSHOT version of Simple Storage that resolves https://github.com/anggrayudi/SimpleStorage/issues/133
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}
configurations.configureEach {
@@ -118,7 +120,7 @@ dependencies {
implementation 'io.github.pilgr:paperdb:2.7.2'
// Simple Storage
implementation "com.anggrayudi:storage:1.5.4"
implementation "com.anggrayudi:storage:1.5.5-SNAPSHOT"
// OK HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
+1
View File
@@ -9,6 +9,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':byteowls-capacitor-filesharer')
implementation project(':capacitor-app')
implementation project(':capacitor-browser')
implementation project(':capacitor-clipboard')
+1
View File
@@ -6,6 +6,7 @@
<!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@@ -1,4 +1,8 @@
[
{
"pkg": "@byteowls/capacitor-filesharer",
"classpath": "com.byteowls.capacitor.filesharer.FileSharerPlugin"
},
{
"pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
@@ -103,6 +103,9 @@ class MainActivity : BridgeActivity() {
}
}
fun isPlayerNotificationServiceInitialized():Boolean {
return ::foregroundService.isInitialized
}
fun stopMyService() {
if (mBounded) {
@@ -116,6 +119,7 @@ class MainActivity : BridgeActivity() {
override fun onSaveInstanceState(outState: Bundle) {
storageHelper.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
outState.clear()
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -96,7 +96,7 @@ class Podcast(
@JsonIgnore
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
val localEpisodeId = "local_ep_" + episode.id
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,0, episode.id, localEpisodeId)
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,episode.size, episode.id, localEpisodeId)
episodes?.add(newEpisode)
var index = 1
@@ -20,6 +20,14 @@ enum class ShakeSensitivitySetting {
VERY_LOW, LOW, MEDIUM, HIGH, VERY_HIGH
}
enum class DownloadUsingCellularSetting {
ASK, ALWAYS, NEVER
}
enum class StreamingUsingCellularSetting {
ASK, ALWAYS, NEVER
}
data class ServerConnectionConfig(
var id:String,
var index:Int,
@@ -107,6 +115,7 @@ data class PlayItemRequestPayload(
data class DeviceSettings(
var disableAutoRewind:Boolean,
var enableAltView:Boolean,
var allowSeekingOnMediaControls:Boolean,
var jumpBackwardsTime:Int,
var jumpForwardTime:Int,
var enableMp3IndexSeeking:Boolean,
@@ -121,7 +130,10 @@ data class DeviceSettings(
var autoSleepTimerAutoRewindTime: Long, //Time in milliseconds
var sleepTimerLength: Long, // Time in milliseconds
var disableSleepTimerFadeOut: Boolean,
var disableSleepTimerResetFeedback: Boolean
var disableSleepTimerResetFeedback: Boolean,
var languageCode: String,
var downloadUsingCellular: DownloadUsingCellularSetting,
var streamingUsingCellular: StreamingUsingCellularSetting
) {
companion object {
// Static method to get default device settings
@@ -129,6 +141,7 @@ data class DeviceSettings(
return DeviceSettings(
disableAutoRewind = false,
enableAltView = true,
allowSeekingOnMediaControls = false,
jumpBackwardsTime = 10,
jumpForwardTime = 10,
enableMp3IndexSeeking = false,
@@ -143,7 +156,10 @@ data class DeviceSettings(
autoSleepTimerAutoRewind = false,
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false
disableSleepTimerResetFeedback = false,
languageCode = "en-us",
downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS,
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
)
}
}
@@ -81,7 +81,7 @@ class LocalLibraryItem(
@JsonIgnore
fun getPlaybackSession(episode:PodcastEpisode?, deviceInfo:DeviceInfo):PlaybackSession {
val localEpisodeId = episode?.id
val sessionId = "play_local_${UUID.randomUUID()}"
val sessionId = "${UUID.randomUUID()}"
// Get current progress for local media
val mediaProgressId = if (localEpisodeId.isNullOrEmpty()) id else "$id-$localEpisodeId"
@@ -48,6 +48,19 @@ object DeviceManager {
if (deviceData.deviceSettings?.autoSleepTimerAutoRewindTime == null) {
deviceData.deviceSettings?.autoSleepTimerAutoRewindTime = 300000L // 5 minutes
}
// Language added in v0.9.69
if (deviceData.deviceSettings?.languageCode == null) {
deviceData.deviceSettings?.languageCode = "en-us"
}
if (deviceData.deviceSettings?.downloadUsingCellular == null) {
deviceData.deviceSettings?.downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS
}
if (deviceData.deviceSettings?.streamingUsingCellular == null) {
deviceData.deviceSettings?.streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
}
}
fun getBase64Id(id:String):String {
@@ -70,6 +70,7 @@ class FolderScanner(var ctx: Context) {
)
localLibraryItem.localFiles.add(localFile)
val trackFileMetadata = FileMetadata(file.name, file.extension, file.absolutePath, file.getBasePath(ctx), file.length())
// Create new audio track
val track = AudioTrack(
audioTrackFromServer.index,
@@ -78,7 +79,7 @@ class FolderScanner(var ctx: Context) {
localFile.filename ?: "",
localFile.contentUrl,
localFile.mimeType ?: "",
null,
trackFileMetadata,
true,
localFileId,
null,
@@ -274,7 +275,8 @@ class FolderScanner(var ctx: Context) {
localLibraryItem.localFiles.add(localFile)
// Create new audio track
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, null, audioTrackFromServer.index)
val trackFileMetadata = FileMetadata(docFile.name ?: "", docFile.extension ?: "", docFile.getAbsolutePath(ctx), docFile.getBasePath(ctx), docFile.length())
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", trackFileMetadata, true, localFileId, null, audioTrackFromServer.index)
audioTracks.add(track)
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
@@ -247,14 +247,18 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
Log.d(tag, "checkSetValidServerConnectionConfig | serverConfigIdUsed=$serverConfigIdUsed | lastServerConnectionConfigId=${DeviceManager.deviceData.lastServerConnectionConfigId}")
coroutineScope {
if (!DeviceManager.checkConnectivity(ctx)) {
serverUserMediaProgress = mutableListOf()
Log.d(tag, "checkSetValidServerConnectionConfig: No connectivity")
cb(false)
} else if (DeviceManager.deviceData.lastServerConnectionConfigId.isNullOrBlank()) { // If in offline mode last server connection config is unset
serverUserMediaProgress = mutableListOf()
Log.d(tag, "checkSetValidServerConnectionConfig: No last server connection config")
cb(false)
} else {
var hasValidConn = false
var lookupMediaProgress = true
@@ -332,6 +336,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
checkSetValidServerConnectionConfig { isConnected ->
if (isConnected) {
serverConfigIdUsed = DeviceManager.serverConnectionConfigId
Log.d(tag, "loadAndroidAutoItems: Connected to server config id=$serverConfigIdUsed")
loadLibraries { libraries ->
if (libraries.isEmpty()) {
@@ -358,6 +363,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
}
} else { // Not connected to server
Log.d(tag, "loadAndroidAutoItems: Not connected to server")
cb()
}
}
@@ -238,6 +238,8 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
saveLocalProgress(it)
lastSyncTime = System.currentTimeMillis()
Log.d(tag, "Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}")
// Local library item is linked to a server library item
// Send sync to server also if connected to this server and local item belongs to this server
if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
@@ -15,8 +15,6 @@ import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import kotlinx.coroutines.*
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, private val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
private val tag = "MediaDescriptionAdapter"
@@ -77,7 +75,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
.load(uri)
.placeholder(R.drawable.icon)
.error(R.drawable.icon)
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.submit()
.get()
} catch (e: Exception) {
e.printStackTrace()
@@ -85,7 +83,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
Glide.with(playerNotificationService)
.asBitmap()
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.submit()
.get()
}
}
@@ -34,6 +34,7 @@ import com.audiobookshelf.app.managers.SleepTimerManager
import com.audiobookshelf.app.media.MediaManager
import com.audiobookshelf.app.media.MediaProgressSyncer
import com.audiobookshelf.app.server.ApiHandler
import com.audiobookshelf.app.BuildConfig
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
@@ -329,15 +330,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
}
mediaSessionConnector.setEnabledPlaybackActions(
PlaybackStateCompat.ACTION_PLAY_PAUSE
or PlaybackStateCompat.ACTION_PLAY
or PlaybackStateCompat.ACTION_PAUSE
or PlaybackStateCompat.ACTION_SEEK_TO
or PlaybackStateCompat.ACTION_FAST_FORWARD
or PlaybackStateCompat.ACTION_REWIND
or PlaybackStateCompat.ACTION_STOP
)
setMediaSessionConnectorPlaybackActions()
mediaSessionConnector.setQueueNavigator(queueNavigator)
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
@@ -512,6 +505,20 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray())
}
fun setMediaSessionConnectorPlaybackActions() {
var playbackActions = PlaybackStateCompat.ACTION_PLAY_PAUSE or
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_FAST_FORWARD or
PlaybackStateCompat.ACTION_REWIND or
PlaybackStateCompat.ACTION_STOP
if (deviceSettings.allowSeekingOnMediaControls) {
playbackActions = playbackActions or PlaybackStateCompat.ACTION_SEEK_TO
}
mediaSessionConnector.setEnabledPlaybackActions(playbackActions)
}
fun handlePlayerPlaybackError(errorMessage:String) {
// On error and was attempting to direct play - fallback to transcode
currentPlaybackSession?.let { playbackSession ->
@@ -941,7 +948,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
appVersion: 0.9.46-beta
*/
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, Build.VERSION.RELEASE)
return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
}
private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
@@ -1,5 +1,7 @@
package com.audiobookshelf.app.plugins
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.*
@@ -493,9 +495,16 @@ class AbsDatabase : Plugin() {
fun updateDeviceSettings(call:PluginCall) { // Returns device data
Log.d(tag, "updateDeviceSettings ${call.data}")
val newDeviceSettings = jacksonMapper.readValue<DeviceSettings>(call.data.toString())
GlobalScope.launch(Dispatchers.IO) {
Handler(Looper.getMainLooper()).post {
DeviceManager.deviceData.deviceSettings = newDeviceSettings
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
// Updates playback actions for media notification (handles media control seek locking setting)
if (mainActivity.isPlayerNotificationServiceInitialized()) {
mainActivity.foregroundService.setMediaSessionConnectorPlaybackActions()
}
call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.deviceData)))
}
}
@@ -1,6 +1,9 @@
package com.audiobookshelf.app.server
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.provider.Settings
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
@@ -8,6 +11,7 @@ import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.media.MediaProgressSyncData
import com.audiobookshelf.app.media.SyncResult
import com.audiobookshelf.app.models.User
import com.audiobookshelf.app.BuildConfig
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@@ -31,7 +35,7 @@ class ApiHandler(var ctx:Context) {
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>)
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>, val deviceInfo:DeviceInfo)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>)
@@ -322,7 +326,11 @@ class ApiHandler(var ctx:Context) {
}
fun sendSyncLocalSessions(playbackSessions:List<PlaybackSession>, cb: (Boolean, String?) -> Unit) {
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions)))
@SuppressLint("HardwareIds")
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo)))
Log.d(tag, "Sending ${playbackSessions.size} saved local playback sessions to server")
postRequest("/api/session/local-all", payload, null) {
if (!it.getString("error").isNullOrEmpty()) {
+3
View File
@@ -2,6 +2,9 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':byteowls-capacitor-filesharer'
project(':byteowls-capacitor-filesharer').projectDir = new File('../node_modules/@byteowls/capacitor-filesharer/android')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
+1 -1
View File
@@ -1,7 +1,7 @@
ext {
minSdkVersion = 24
compileSdkVersion = 34
targetSdkVersion = 33
targetSdkVersion = 34
androidxActivityVersion = '1.7.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
+117
View File
@@ -0,0 +1,117 @@
class WrappingMarquee {
#scrollDelay = 2000
#scrollSpeed = 30
/**
* @param {HTMLElement} el
*/
constructor(el) {
this.el = el
/** @type {HTMLElement} */
this.pEl = el?.firstElementChild
this.innerText = ''
this.isScrolling = false
/** @type {NodeJS.Timeout} */
this.timer = null
/** @type {number} */
this.animationId = null
}
/**
* Transparent gradient mask shown when text is scrolling left and overflowing right
*
* @param {boolean} showLeft
*/
setMask(showLeft) {
if (!this.el) return
this.el.style.maskImage = showLeft ? 'linear-gradient(90deg, transparent 0%, #fff 10%, #000 90%, transparent)' : 'linear-gradient(90deg, #000 90%, transparent)'
}
startScroll() {
if (this.isScrolling) {
console.warn('Already scrolling')
return
}
this.isScrolling = true
this.setMask(true)
let textScrollAmount = this.el.scrollWidth
this.pEl.innerHTML = this.innerText + '&nbsp;'.repeat(15)
let totalScrollAmount = this.el.scrollWidth
let scrollDuration = totalScrollAmount * this.#scrollSpeed
this.pEl.innerHTML = this.pEl.innerHTML + this.innerText
let done = false
let start, previousTimeStamp
const step = (timeStamp) => {
if (start === undefined) {
start = timeStamp
}
const elapsed = timeStamp - start
if (this.isScrolling && previousTimeStamp !== timeStamp) {
const amountToMove = Math.min(elapsed / scrollDuration * totalScrollAmount, totalScrollAmount)
this.pEl.style.transform = `translateX(-${amountToMove}px)`
if (amountToMove === totalScrollAmount) done = true
if (amountToMove > textScrollAmount) this.setMask(false)
}
if (!this.isScrolling || done) { // canceled or done
this.isScrolling = false
this.pEl.style.transform = 'translateX(0px)'
this.pEl.innerText = this.innerText
this.setMask(false)
if (done) {
this.startTimer()
}
} else if (elapsed < scrollDuration) { // step
previousTimeStamp = timeStamp
this.animationId = window.requestAnimationFrame(step)
}
}
this.animationId = window.requestAnimationFrame(step)
}
startTimer() {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.startScroll()
}, this.#scrollDelay)
}
reset() {
clearTimeout(this.timer)
this.timer = null
this.isScrolling = false
window.cancelAnimationFrame(this.animationId)
}
/**
* Initialize and start marquee if text overflows container
* resets the marquee if already active
*
* @param {string} innerText
*/
init(innerText) {
if (!this.el || !this.pEl) return
this.reset()
this.innerText = innerText
this.pEl.innerText = innerText
this.pEl.style.transform = 'translateX(0px)'
if (this.el.scrollWidth > this.el.clientWidth) {
this.setMask(false)
this.startTimer()
} else {
this.el.style.maskImage = ''
}
}
}
export default WrappingMarquee
+5 -7
View File
@@ -1,6 +1,8 @@
@import "./tailwind.css";
@import "./fonts.css";
@import './defaultStyles.css';
@import './absicons.css';
@import './transitions.css';
* {
-webkit-touch-callout: none;
@@ -15,10 +17,6 @@ textarea {
user-select: auto;
}
body {
background-color: #262626;
}
.layout-wrapper {
height: calc(100vh - env(safe-area-inset-top));
min-height: calc(100vh - env(safe-area-inset-top));
@@ -148,15 +146,15 @@ Bookshelf Label
.tracksTable {
border-collapse: collapse;
width: 100%;
border: 1px solid #474747;
border: 1px solid rgb(var(--color-secondary));
}
.tracksTable tr:nth-child(even) {
background-color: #2e2e2e;
background-color: rgb(var(--color-secondary));
}
.tracksTable tr {
background-color: #373838;
background-color: rgb(var(--color-bg));
}
.tracksTable td {
-158
View File
@@ -1,158 +0,0 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['fast-sort'] = {}));
}(this, (function (exports) {
'use strict';
// >>> INTERFACES <<<
// >>> HELPERS <<<
var castComparer = function (comparer) { return function (a, b, order) { return comparer(a, b, order) * order; }; };
var throwInvalidConfigErrorIfTrue = function (condition, context) {
if (condition)
throw Error("Invalid sort config: " + context);
};
var unpackObjectSorter = function (sortByObj) {
var _a = sortByObj || {}, asc = _a.asc, desc = _a.desc;
var order = asc ? 1 : -1;
var sortBy = (asc || desc);
// Validate object config
throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property');
throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties');
var comparer = sortByObj.comparer && castComparer(sortByObj.comparer);
return { order: order, sortBy: sortBy, comparer: comparer };
};
// >>> SORTERS <<<
var multiPropertySorterProvider = function (defaultComparer) {
return function multiPropertySorter(sortBy, sortByArr, depth, order, comparer, a, b) {
var valA;
var valB;
if (typeof sortBy === 'string') {
valA = a[sortBy];
valB = b[sortBy];
}
else if (typeof sortBy === 'function') {
valA = sortBy(a);
valB = sortBy(b);
}
else {
var objectSorterConfig = unpackObjectSorter(sortBy);
return multiPropertySorter(objectSorterConfig.sortBy, sortByArr, depth, objectSorterConfig.order, objectSorterConfig.comparer || defaultComparer, a, b);
}
var equality = comparer(valA, valB, order);
if ((equality === 0 || (valA == null && valB == null)) &&
sortByArr.length > depth) {
return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b);
}
return equality;
};
};
function getSortStrategy(sortBy, comparer, order) {
// Flat array sorter
if (sortBy === undefined || sortBy === true) {
return function (a, b) { return comparer(a, b, order); };
}
// Sort list of objects by single object key
if (typeof sortBy === 'string') {
throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.');
return function (a, b) { return comparer(a[sortBy], b[sortBy], order); };
}
// Sort list of objects by single function sorter
if (typeof sortBy === 'function') {
return function (a, b) { return comparer(sortBy(a), sortBy(b), order); };
}
// Sort by multiple properties
if (Array.isArray(sortBy)) {
var multiPropSorter_1 = multiPropertySorterProvider(comparer);
return function (a, b) { return multiPropSorter_1(sortBy[0], sortBy, 1, order, comparer, a, b); };
}
// Unpack object config to get actual sorter strategy
var objectSorterConfig = unpackObjectSorter(sortBy);
return getSortStrategy(objectSorterConfig.sortBy, objectSorterConfig.comparer || comparer, objectSorterConfig.order);
}
var sortArray = function (order, ctx, sortBy, comparer) {
var _a;
if (!Array.isArray(ctx)) {
return ctx;
}
// Unwrap sortBy if array with only 1 value to get faster sort strategy
if (Array.isArray(sortBy) && sortBy.length < 2) {
_a = sortBy, sortBy = _a[0];
}
return ctx.sort(getSortStrategy(sortBy, comparer, order));
};
// >>> Public <<<
var createNewSortInstance = function (opts) {
var comparer = castComparer(opts.comparer);
return function (_ctx) {
var ctx = Array.isArray(_ctx) && !opts.inPlaceSorting
? _ctx.slice()
: _ctx;
return {
/**
* Sort array in ascending order.
* @example
* sort([3, 1, 4]).asc();
* sort(users).asc(u => u.firstName);
* sort(users).asc([
* U => u.firstName
* u => u.lastName,
* ]);
*/
asc: function (sortBy) {
return sortArray(1, ctx, sortBy, comparer);
},
/**
* Sort array in descending order.
* @example
* sort([3, 1, 4]).desc();
* sort(users).desc(u => u.firstName);
* sort(users).desc([
* U => u.firstName
* u => u.lastName,
* ]);
*/
desc: function (sortBy) {
return sortArray(-1, ctx, sortBy, comparer);
},
/**
* Sort array in ascending or descending order. It allows sorting on multiple props
* in different order for each of them.
* @example
* sort(users).by([
* { asc: u => u.score }
* { desc: u => u.age }
* ]);
*/
by: function (sortBy) {
return sortArray(1, ctx, sortBy, comparer);
},
};
};
};
var defaultComparer = function (a, b, order) {
if (a == null)
return order;
if (b == null)
return -order;
if (a < b)
return -1;
if (a === b)
return 0;
return 1;
};
var sort = createNewSortInstance({
comparer: defaultComparer,
});
var inPlaceSort = createNewSortInstance({
comparer: defaultComparer,
inPlaceSorting: true,
});
exports.createNewSortInstance = createNewSortInstance;
exports.inPlaceSort = inPlaceSort;
exports.sort = sort;
Object.defineProperty(exports, '__esModule', { value: true });
})));
+47
View File
@@ -0,0 +1,47 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
color: white;
--color-bg: 56 56 56;
--color-bg-hover: 102 104 107;
--color-fg: 230 237 243;
--color-fg-muted: 142 147 153;
--color-primary: 35 35 35;
--color-secondary: 47 48 48;
--color-border: 75 85 89;
--color-bg-toggle: 56 56 56;
--color-bg-toggle-selected: 81 82 84;
--color-track-cursor: 229 231 235;
--color-track: 107 114 128;
--color-track-buffered: 75 85 99;
--gradient-item-page: linear-gradient(169deg, rgba(0, 0, 0, 0.4) 0%, rgba(55, 56, 56, 1) 80%);
--gradient-audio-player: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%);
--gradient-minimized-audio-player: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%);
}
html[data-theme='light'] {
color: black;
--color-bg: 255 255 255;
--color-bg-hover: 208 210 212;
--color-fg: 37 37 37;
--color-fg-muted: 101 109 118;
--color-primary: 222 222 222;
--color-secondary: 246 248 250;
--color-border: 189 191 191;
--color-bg-toggle: 222 222 222;
--color-bg-toggle-selected: 255 255 255;
--color-track-cursor: 101 109 118;
--color-track: 189 191 191;
--color-track-buffered: 129 131 131;
--gradient-item-page: linear-gradient(169deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 1) 80%);
--gradient-audio-player: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 80%);
--gradient-minimized-audio-player: linear-gradient(145deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0.9) 20%, rgb(255, 255, 255) 60%);
}
body {
@apply bg-primary
}
}
+51
View File
@@ -0,0 +1,51 @@
.slide-enter-active {
-moz-transition-duration: 0.1s;
-webkit-transition-duration: 0.1s;
-o-transition-duration: 0.1s;
transition-duration: 0.1s;
-moz-transition-timing-function: ease-in;
-webkit-transition-timing-function: ease-in;
-o-transition-timing-function: ease-in;
transition-timing-function: ease-in;
}
.slide-leave-active {
-moz-transition-duration: 0.2s;
-webkit-transition-duration: 0.2s;
-o-transition-duration: 0.2s;
transition-duration: 0.2s;
-moz-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
-webkit-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
-o-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
.slide-enter-to,
.slide-leave {
max-height: 600px;
overflow: hidden;
}
.slide-enter,
.slide-leave-to {
overflow: hidden;
max-height: 0;
}
.menu-enter,
.menu-leave-active {
transform: translateY(-15px);
}
.menu-enter-active {
transition: all 0.2s;
}
.menu-leave-active {
transition: all 0.1s;
}
.menu-enter,
.menu-leave-active {
opacity: 0;
}
+10 -7
View File
@@ -4,13 +4,13 @@
<nuxt-link v-show="!showBack" to="/" class="mr-3">
<img src="/Logo.png" class="h-10 w-10" />
</nuxt-link>
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
<span class="material-icons text-3xl text-white">arrow_back</span>
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
<span class="material-icons text-3xl text-fg">arrow_back</span>
</a>
<div v-if="user && currentLibrary">
<div v-if="user && currentLibrary && networkConnected">
<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" />
<p class="text-smleading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
<p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
</div>
</div>
@@ -54,14 +54,17 @@ export default {
this.$store.commit('setCastAvailable', val)
}
},
networkConnected() {
return this.$store.state.networkConnected
},
currentLibrary() {
return this.$store.getters['libraries/getCurrentLibrary']
},
currentLibraryName() {
return this.currentLibrary ? this.currentLibrary.name : ''
return this.currentLibrary?.name || ''
},
currentLibraryIcon() {
return this.currentLibrary ? this.currentLibrary.icon : 'database'
return this.currentLibrary?.icon || 'database'
},
showBack() {
if (!this.$route.name) return true
@@ -71,7 +74,7 @@ export default {
return this.$store.state.user.user
},
username() {
return this.user ? this.user.username : 'err'
return this.user?.username || 'err'
},
isCasting() {
return this.$store.state.isCasting
+207 -106
View File
@@ -1,7 +1,7 @@
<template>
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }">
<div v-if="showFullscreen" class="w-full h-full z-10 absolute top-0 left-0 pointer-events-auto" :style="{ backgroundColor: coverRgb }">
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(169deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%)" />
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-audio-player)" />
<div class="top-4 left-4 absolute cursor-pointer">
<span class="material-icons text-5xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="collapseFullscreen">expand_more</span>
@@ -12,20 +12,20 @@
<div class="top-6 right-4 absolute cursor-pointer">
<span class="material-icons text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="showMoreMenuDialog = true">more_vert</span>
</div>
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
</div>
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
<div v-if="playerSettings.useChapterTrack && playerSettings.useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
<div class="flex">
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
<div class="flex-grow" />
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
</div>
<div class="w-full">
<div class="h-1 w-full bg-gray-500 bg-opacity-50 relative rounded-full">
<div ref="totalReadyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalBufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalPlayedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none rounded-full" />
<div class="h-1 w-full bg-track/50 relative rounded-full">
<div ref="totalReadyTrack" class="h-full bg-track-buffered absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalBufferedTrack" class="h-full bg-track absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalPlayedTrack" class="h-full bg-track-cursor absolute top-0 left-0 pointer-events-none rounded-full" />
</div>
</div>
</div>
@@ -41,64 +41,66 @@
</div>
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
<p class="title-text truncate">{{ title }}</p>
<p class="author-text text-white text-opacity-75 truncate">{{ authorName }}</p>
<div ref="titlewrapper" class="overflow-hidden relative">
<p class="title-text whitespace-nowrap"></p>
</div>
<p class="author-text text-fg text-opacity-75 truncate">{{ authorName }}</p>
</div>
<div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer">
<div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full pb-4 pt-2 mx-auto px-6" style="max-width: 414px">
<div class="flex items-center justify-between pointer-events-auto">
<span v-if="!isPodcast && serverLibraryItemId && networkConnected" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
<span v-if="!isPodcast && serverLibraryItemId && networkConnected" class="material-icons text-3xl text-fg-muted cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
<!-- hidden for podcasts but still using this as a placeholder -->
<span v-else class="material-icons text-3xl text-white text-opacity-0">bookmark</span>
<span class="font-mono text-white text-opacity-75 cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ currentPlaybackRate }}x</span>
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-white text-opacity-75 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')">
<span class="font-mono text-fg-muted cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ currentPlaybackRate }}x</span>
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-fg-muted cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<div v-else class="h-7 w-7 flex items-center justify-around cursor-pointer" @click.stop="$emit('showSleepTimer')">
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
</div>
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="clickChaptersBtn">format_list_bulleted</span>
<span class="material-icons text-3xl text-fg cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="clickChaptersBtn">format_list_bulleted</span>
</div>
</div>
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%)" />
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" />
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
<div class="flex items-center max-w-full" :class="lockUi ? 'justify-center' : 'justify-between'">
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
<div class="flex items-center max-w-full" :class="playerSettings.lockUi ? 'justify-center' : 'justify-between'">
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
<div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
<span v-if="!isLoading" class="material-icons" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
<widgets-spinner-icon v-else class="h-8 w-8" />
</div>
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
<span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
</div>
</div>
<div id="playerTrack" class="absolute left-0 w-full px-6">
<div class="flex pointer-events-none">
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
<p class="font-mono text-fg" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
<div class="flex-grow" />
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
</div>
<div ref="track" class="h-1.5 w-full bg-gray-500 bg-opacity-50 relative rounded-full" :class="{ 'animate-pulse': isLoading }" @click.stop>
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': lockUi || !showFullscreen }" @touchstart="touchstartCursor">
<div class="bg-gray-200 rounded-full w-3.5 h-3.5 pointer-events-none" />
<div ref="track" class="h-1.5 w-full bg-track/50 relative rounded-full" :class="{ 'animate-pulse': isLoading }" @click.stop>
<div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': playerSettings.lockUi || !showFullscreen }" @touchstart="touchstartCursor">
<div class="bg-track-cursor rounded-full w-3.5 h-3.5 pointer-events-none" />
</div>
</div>
</div>
</div>
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction" />
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" width="80vw" @action="clickMenuAction" />
</div>
</template>
@@ -106,6 +108,7 @@
import { Capacitor } from '@capacitor/core'
import { AbsAudioPlayer } from '@/plugins/capacitor'
import { FastAverageColor } from 'fast-average-color'
import WrappingMarquee from '@/assets/WrappingMarquee.js'
export default {
props: {
@@ -145,10 +148,12 @@ export default {
onPlaybackSpeedChangedListener: null,
touchStartY: 0,
touchStartTime: 0,
touchEndY: 0,
useChapterTrack: false,
useTotalTrack: true,
lockUi: false,
playerSettings: {
useChapterTrack: false,
useTotalTrack: true,
scaleElapsedTimeBySpeed: true,
lockUi: false
},
isLoading: false,
isDraggingCursor: false,
draggingTouchStartX: 0,
@@ -157,16 +162,22 @@ export default {
syncStatus: 0,
showMoreMenuDialog: false,
coverRgb: 'rgb(55, 56, 56)',
coverBgIsLight: false
coverBgIsLight: false,
titleMarquee: null,
isRefreshingUI: false
}
},
watch: {
showFullscreen(val) {
this.updateScreenSize()
this.$store.commit('setPlayerFullscreen', !!val)
document.querySelector('body').style.backgroundColor = this.showFullscreen ? this.coverRgb : ''
},
bookCoverAspectRatio() {
this.updateScreenSize()
},
title(val) {
if (this.titleMarquee) this.titleMarquee.init(val)
}
},
computed: {
@@ -175,7 +186,7 @@ export default {
// TODO: Implement on iOS
if (this.$platform !== 'ios' && !this.isPodcast && this.mediaId) {
items.push({
text: 'History',
text: this.$strings.ButtonHistory,
value: 'history',
icon: 'history'
})
@@ -184,22 +195,27 @@ export default {
items.push(
...[
{
text: 'Total Track',
text: this.$strings.LabelTotalTrack,
value: 'total_track',
icon: this.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
icon: this.playerSettings.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
},
{
text: 'Chapter Track',
text: this.$strings.LabelChapterTrack,
value: 'chapter_track',
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
icon: this.playerSettings.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
},
{
text: this.lockUi ? 'Unlock Player' : 'Lock Player',
text: this.$strings.LabelScaleElapsedTimeBySpeed,
value: 'scale_elapsed_time',
icon: this.playerSettings.scaleElapsedTimeBySpeed ? 'check_box' : 'check_box_outline_blank'
},
{
text: this.playerSettings.lockUi ? this.$strings.LabelUnlockPlayer : this.$strings.LabelLockPlayer,
value: 'lock',
icon: this.lockUi ? 'lock' : 'lock_open'
icon: this.playerSettings.lockUi ? 'lock' : 'lock_open'
},
{
text: 'Close Player',
text: this.$strings.LabelClosePlayer,
value: 'close',
icon: 'close'
}
@@ -256,22 +272,22 @@ export default {
return this.mediaPlayer === 'cast-player'
},
mediaPlayer() {
return this.playbackSession ? this.playbackSession.mediaPlayer : null
return this.playbackSession?.mediaPlayer || null
},
mediaType() {
return this.playbackSession ? this.playbackSession.mediaType : null
return this.playbackSession?.mediaType || null
},
isPodcast() {
return this.mediaType === 'podcast'
},
mediaMetadata() {
return this.playbackSession ? this.playbackSession.mediaMetadata : null
return this.playbackSession?.mediaMetadata || null
},
libraryItem() {
return this.playbackSession ? this.playbackSession.libraryItem || null : null
return this.playbackSession?.libraryItem || null
},
localLibraryItem() {
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
return this.playbackSession?.localLibraryItem || null
},
localLibraryItemCoverSrc() {
var localItemCover = this.localLibraryItem?.coverContentUrl || null
@@ -279,7 +295,7 @@ export default {
return null
},
playMethod() {
return this.playbackSession ? this.playbackSession.playMethod : null
return this.playbackSession?.playMethod || 0
},
isLocalPlayMethod() {
return this.playMethod == this.$constants.PlayMethod.LOCAL
@@ -288,19 +304,19 @@ export default {
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
},
title() {
if (this.currentChapterTitle && this.showFullscreen) return this.currentChapterTitle
if (this.playbackSession) return this.playbackSession.displayTitle
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
const mediaItemTitle = this.playbackSession?.displayTitle || this.mediaMetadata?.title || 'Title'
if (this.currentChapterTitle) {
if (this.showFullscreen) return this.currentChapterTitle
return `${mediaItemTitle} | ${this.currentChapterTitle}`
}
return mediaItemTitle
},
authorName() {
if (this.playbackSession) return this.playbackSession.displayAuthor
return this.mediaMetadata ? this.mediaMetadata.authorName : 'Author'
return this.mediaMetadata?.authorName || 'Author'
},
chapters() {
if (this.playbackSession && this.playbackSession.chapters) {
return this.playbackSession.chapters
}
return []
return this.playbackSession?.chapters || []
},
currentChapter() {
if (!this.chapters.length) return null
@@ -311,7 +327,7 @@ export default {
return this.chapters.find((c) => Number(Number(c.start).toFixed(2)) > this.currentTime)
},
currentChapterTitle() {
return this.currentChapter ? this.currentChapter.title : ''
return this.currentChapter?.title || ''
},
currentChapterDuration() {
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
@@ -321,11 +337,14 @@ export default {
},
currentTimePretty() {
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
return this.$secondsToTimestamp(currentTimeToUse / this.currentPlaybackRate)
if (this.playerSettings.scaleElapsedTimeBySpeed) {
currentTimeToUse = currentTimeToUse / this.currentPlaybackRate
}
return this.$secondsToTimestamp(currentTimeToUse)
},
timeRemaining() {
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) {
if (this.playerSettings.useChapterTrack && this.currentChapter) {
var currChapTime = currentTimeToUse - this.currentChapter.start
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
}
@@ -411,7 +430,11 @@ export default {
AbsAudioPlayer.requestSession()
},
clickContainer() {
this.expandToFullscreen()
},
expandToFullscreen() {
this.showFullscreen = true
if (this.titleMarquee) this.titleMarquee.reset()
// Update track for total time bar if useChapterTrack is set
this.$nextTick(() => {
@@ -420,6 +443,8 @@ export default {
},
collapseFullscreen() {
this.showFullscreen = false
if (this.titleMarquee) this.titleMarquee.reset()
this.forceCloseDropdownMenu()
},
async jumpNextChapter() {
@@ -493,7 +518,7 @@ export default {
this.updateReadyTrack()
},
updateReadyTrack() {
if (this.useChapterTrack) {
if (this.playerSettings.useChapterTrack) {
if (this.$refs.totalReadyTrack) {
this.$refs.totalReadyTrack.style.width = this.readyTrackWidth + 'px'
}
@@ -510,11 +535,14 @@ export default {
}
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) {
if (this.playerSettings.useChapterTrack && this.currentChapter) {
currentTime = Math.max(0, currentTime - this.currentChapter.start)
}
if (this.playerSettings.scaleElapsedTimeBySpeed) {
currentTime = currentTime / this.currentPlaybackRate
}
ts.innerText = this.$secondsToTimestamp(currentTime / this.currentPlaybackRate)
ts.innerText = this.$secondsToTimestamp(currentTime)
},
timeupdate() {
if (!this.$refs.playedTrack) {
@@ -542,7 +570,7 @@ export default {
let bufferedPercent = this.bufferedTime / this.totalDuration
const totalBufferedPercent = bufferedPercent
if (this.useChapterTrack && this.currentChapter) {
if (this.playerSettings.useChapterTrack && this.currentChapter) {
const currChapTime = currentTimeToUse - this.currentChapter.start
percentDone = currChapTime / this.currentChapterDuration
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
@@ -556,7 +584,7 @@ export default {
this.$refs.trackCursor.style.left = ptWidth - 14 + 'px'
}
if (this.useChapterTrack) {
if (this.playerSettings.useChapterTrack) {
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
}
@@ -583,7 +611,7 @@ export default {
}
},
async touchstartCursor(e) {
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.playerSettings.lockUi) return
await this.$hapticsImpact()
this.isDraggingCursor = true
@@ -624,38 +652,49 @@ export default {
resetStream(startTime) {
this.closePlayback()
},
handleGesture() {
var touchDistance = this.touchEndY - this.touchStartY
if (touchDistance > 100) {
this.collapseFullscreen()
}
},
touchstart(e) {
if (!this.showFullscreen || !e.changedTouches) return
if (!e.changedTouches || this.$store.state.globals.isModalOpen) return
const touchPosY = e.changedTouches[0].pageY
// when minimized only listen to touchstart on the player
if (!this.showFullscreen && touchPosY < window.innerHeight - 120) return
this.touchStartY = e.changedTouches[0].screenY
if (this.touchStartY > window.innerHeight / 3) {
// console.log('touch too low')
return
// for ios
if (!this.showFullscreen && e.pageX < 20) {
e.preventDefault()
e.stopImmediatePropagation()
}
this.touchStartY = touchPosY
this.touchStartTime = Date.now()
},
touchend(e) {
if (!e.changedTouches) return
const touchDuration = Date.now() - this.touchStartTime
const touchEndY = e.changedTouches[0].pageY
const touchDistanceY = touchEndY - this.touchStartY
// reset touch start data
this.touchStartTime = 0
this.touchStartY = 0
if (this.isDraggingCursor) {
if (this.draggingCurrentTime !== this.currentTime) {
this.seek(this.draggingCurrentTime)
}
this.isDraggingCursor = false
} else if (this.showFullscreen) {
this.touchEndY = e.changedTouches[0].screenY
var touchDuration = Date.now() - this.touchStartTime
} else {
if (touchDuration > 1200) {
// console.log('touch too long', touchDuration)
return
}
this.handleGesture()
if (this.showFullscreen) {
// Touch start higher than touchend
if (touchDistanceY > 100) {
this.collapseFullscreen()
}
} else if (touchDistanceY < -100) {
this.expandToFullscreen()
}
}
},
touchmove(e) {
@@ -665,7 +704,7 @@ export default {
let duration = this.totalDuration
let minTime = 0
let maxTime = duration
if (this.useChapterTrack && this.currentChapter) {
if (this.playerSettings.useChapterTrack && this.currentChapter) {
duration = this.currentChapterDuration
minTime = this.currentChapter.start
maxTime = minTime + duration
@@ -685,32 +724,42 @@ export default {
if (action === 'history') {
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
this.showFullscreen = false
} else if (action === 'scale_elapsed_time') {
this.playerSettings.scaleElapsedTimeBySpeed = !this.playerSettings.scaleElapsedTimeBySpeed
this.updateTimestamp()
this.savePlayerSettings()
} else if (action === 'lock') {
this.lockUi = !this.lockUi
this.$localStore.setPlayerLock(this.lockUi)
this.playerSettings.lockUi = !this.playerSettings.lockUi
this.savePlayerSettings()
} else if (action === 'chapter_track') {
this.useChapterTrack = !this.useChapterTrack
this.useTotalTrack = !this.useChapterTrack || this.useTotalTrack
this.playerSettings.useChapterTrack = !this.playerSettings.useChapterTrack
this.playerSettings.useTotalTrack = !this.playerSettings.useChapterTrack || this.playerSettings.useTotalTrack
this.updateTimestamp()
this.updateTrack()
this.updateReadyTrack()
this.$localStore.setUseTotalTrack(this.useTotalTrack)
this.$localStore.setUseChapterTrack(this.useChapterTrack)
this.updateUseChapterTrack()
this.savePlayerSettings()
} else if (action === 'total_track') {
this.useTotalTrack = !this.useTotalTrack
this.useChapterTrack = !this.useTotalTrack || this.useChapterTrack
this.playerSettings.useTotalTrack = !this.playerSettings.useTotalTrack
this.playerSettings.useChapterTrack = !this.playerSettings.useTotalTrack || this.playerSettings.useChapterTrack
this.updateTimestamp()
this.updateTrack()
this.updateReadyTrack()
this.$localStore.setUseTotalTrack(this.useTotalTrack)
this.$localStore.setUseChapterTrack(this.useChapterTrack)
this.updateUseChapterTrack()
this.savePlayerSettings()
} else if (action === 'close') {
this.closePlayback()
}
})
},
updateUseChapterTrack() {
// Chapter track in NowPlaying only supported on iOS for now
if (this.$platform === 'ios') {
AbsAudioPlayer.setChapterTrack({ enabled: this.playerSettings.useChapterTrack })
}
},
forceCloseDropdownMenu() {
if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) {
this.$refs.dropdownMenu.closeMenu()
@@ -727,6 +776,30 @@ export default {
this.isLoading = false
this.playbackSession = null
},
async loadPlayerSettings() {
const savedPlayerSettings = await this.$localStore.getPlayerSettings()
if (!savedPlayerSettings) {
// In 0.9.72-beta 'useChapterTrack', 'useTotalTrack' and 'playerLock' was replaced with 'playerSettings' JSON object
// Check if this old key was set and if so migrate them over to 'playerSettings'
const chapterTrackPref = await this.$localStore.getPreferenceByKey('useChapterTrack')
if (chapterTrackPref) {
this.playerSettings.useChapterTrack = chapterTrackPref === '1'
const totalTrackPref = await this.$localStore.getPreferenceByKey('useTotalTrack')
this.playerSettings.useTotalTrack = totalTrackPref === '1'
const playerLockPref = await this.$localStore.getPreferenceByKey('playerLock')
this.playerSettings.lockUi = playerLockPref === '1'
}
this.savePlayerSettings()
} else {
this.playerSettings.useChapterTrack = !!savedPlayerSettings.useChapterTrack
this.playerSettings.useTotalTrack = !!savedPlayerSettings.useTotalTrack
this.playerSettings.lockUi = !!savedPlayerSettings.lockUi
this.playerSettings.scaleElapsedTimeBySpeed = !!savedPlayerSettings.scaleElapsedTimeBySpeed
}
},
savePlayerSettings() {
return this.$localStore.setPlayerSettings({ ...this.playerSettings })
},
//
// Listeners from audio AbsAudioPlayer
//
@@ -771,6 +844,10 @@ export default {
// Set track width
this.$nextTick(() => {
if (this.titleMarquee) this.titleMarquee.reset()
this.titleMarquee = new WrappingMarquee(this.$refs.titlewrapper)
this.titleMarquee.init(this.title)
if (this.$refs.track) {
this.trackWidth = this.$refs.track.clientWidth
} else {
@@ -793,9 +870,7 @@ export default {
this.updateTimestamp()
},
async init() {
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
this.useTotalTrack = await this.$localStore.getUseTotalTrack()
this.lockUi = await this.$localStore.getPlayerLock()
await this.loadPlayerSettings()
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
@@ -806,26 +881,51 @@ export default {
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
this.onPlaybackSpeedChangedListener = AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
},
screenOrientationChange() {
setTimeout(() => {
this.updateScreenSize()
if (this.$refs.track) {
this.trackWidth = this.$refs.track.clientWidth
this.updateTrack()
this.updateReadyTrack()
async screenOrientationChange() {
if (this.isRefreshingUI) return
this.isRefreshingUI = true
const windowWidth = window.innerWidth
this.refreshUI()
// Window width does not always change right away. Wait up to 250ms for a change.
// iPhone 10 on iOS 16 took between 100 - 200ms to update when going from portrait to landscape
// but landscape to portrait was immediate
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 50))
if (window.innerWidth !== windowWidth) {
this.refreshUI()
break
}
}, 50)
}
this.isRefreshingUI = false
},
refreshUI() {
this.updateScreenSize()
if (this.$refs.track) {
this.trackWidth = this.$refs.track.clientWidth
this.updateTrack()
this.updateReadyTrack()
}
},
updateScreenSize() {
setTimeout(() => {
if (this.titleMarquee) this.titleMarquee.init(this.title)
}, 500)
this.windowHeight = window.innerHeight
this.windowWidth = window.innerWidth
const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
const coverImageWidthCollapsed = 46 / this.bookCoverAspectRatio
const titleAuthorLeftOffsetCollapsed = 30 + coverImageWidthCollapsed
const titleAuthorWidthCollapsed = this.windowWidth - 128 - titleAuthorLeftOffsetCollapsed - 10
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px')
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 30 + coverImageWidthCollapsed + 'px')
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', titleAuthorLeftOffsetCollapsed + 'px')
document.documentElement.style.setProperty('--title-author-width-collapsed', titleAuthorWidthCollapsed + 'px')
},
minimizePlayerEvt() {
this.collapseFullscreen()
@@ -848,7 +948,7 @@ export default {
window.addEventListener('resize', this.screenOrientationChange)
this.$eventBus.$on('minimize-player', this.minimizePlayerEvt)
document.body.addEventListener('touchstart', this.touchstart)
document.body.addEventListener('touchstart', this.touchstart, { passive: false })
document.body.addEventListener('touchend', this.touchend)
document.body.addEventListener('touchmove', this.touchmove)
this.$nextTick(this.init)
@@ -893,6 +993,7 @@ export default {
--cover-image-width-collapsed: 46px;
--cover-image-height-collapsed: 46px;
--title-author-left-offset-collapsed: 80px;
--title-author-width-collapsed: 40%;
}
.playerContainer {
@@ -940,7 +1041,7 @@ export default {
transition-property: left, bottom, width, height;
transform-origin: left bottom;
width: 40%;
width: var(--title-author-width-collapsed);
bottom: 76px;
left: var(--title-author-left-offset-collapsed);
text-align: left;
+18 -2
View File
@@ -11,6 +11,7 @@
<script>
import { AbsAudioPlayer } from '@/plugins/capacitor'
import { Dialog } from '@capacitor/dialog'
import CellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
export default {
data() {
@@ -39,6 +40,7 @@ export default {
serverEpisodeId: null
}
},
mixins: [CellularPermissionHelpers],
computed: {
bookmarks() {
if (!this.serverLibraryItemId) return []
@@ -193,17 +195,27 @@ export default {
const startTime = payload.startTime
const startWhenReady = !payload.paused
const isLocal = libraryItemId.startsWith('local')
if (!isLocal) {
const hasPermission = await this.checkCellularPermission('streaming')
if (!hasPermission) {
this.$store.commit('setPlayerDoneStartingPlayback')
return
}
}
// 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
const serverLibraryItemId = payload.serverLibraryItemId || null
const serverEpisodeId = payload.serverEpisodeId || null
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
if (isLocal && this.$store.state.isCasting) {
const { value } = await Dialog.confirm({
title: 'Warning',
message: `Cannot cast downloaded media items. Confirm to close cast and play on your device.`
})
if (!value) {
this.$store.commit('setPlayerDoneStartingPlayback')
return
}
}
@@ -217,6 +229,7 @@ export default {
} else if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.play()
}
this.$store.commit('setPlayerDoneStartingPlayback')
return
}
@@ -254,6 +267,9 @@ export default {
console.error('Failed', error)
this.$toast.error('Failed to play')
})
.finally(() => {
this.$store.commit('setPlayerDoneStartingPlayback')
})
},
pauseItem() {
if (this.$refs.audioPlayer && this.$refs.audioPlayer.isPlaying) {
@@ -368,4 +384,4 @@ export default {
this.$eventBus.$off('device-focus-update', this.deviceFocused)
}
}
</script>
</script>
+28 -15
View File
@@ -1,7 +1,7 @@
<template>
<div class="fixed top-0 left-0 right-0 layout-wrapper w-full z-50 overflow-hidden pointer-events-none">
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
<div class="absolute top-0 right-0 w-64 h-full bg-primary transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
<div class="absolute top-0 right-0 w-64 h-full bg-bg transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
<div class="px-6 mb-4">
<p v-if="user" class="text-base">
Welcome,
@@ -11,25 +11,25 @@
<div class="w-full overflow-y-auto">
<template v-for="item in navItems">
<button v-if="item.action" :key="item.text" class="w-full hover:bg-bg hover:bg-opacity-60 flex items-center py-3 px-6 text-gray-300" @click="clickAction(item.action)">
<button v-if="item.action" :key="item.text" class="w-full hover:bg-bg/60 flex items-center py-3 px-6 text-fg-muted" @click="clickAction(item.action)">
<span class="text-lg" :class="item.iconOutlined ? 'material-icons-outlined' : 'material-icons'">{{ item.icon }}</span>
<p class="pl-4">{{ item.text }}</p>
</button>
<nuxt-link v-else :to="item.to" :key="item.text" class="w-full hover:bg-bg hover:bg-opacity-60 flex items-center py-3 px-6 text-gray-300" :class="currentRoutePath.startsWith(item.to) ? 'bg-bg bg-opacity-60' : ''">
<nuxt-link v-else :to="item.to" :key="item.text" class="w-full hover:bg-bg/60 flex items-center py-3 px-6 text-fg" :class="currentRoutePath.startsWith(item.to) ? 'bg-bg-hover/50' : 'text-fg-muted'">
<span class="text-lg" :class="item.iconOutlined ? 'material-icons-outlined' : 'material-icons'">{{ item.icon }}</span>
<p class="pl-4">{{ item.text }}</p>
</nuxt-link>
</template>
</div>
<div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300">
<div class="absolute bottom-0 left-0 w-full py-6 px-6 text-fg">
<div v-if="serverConnectionConfig" class="mb-4 flex justify-center">
<p class="text-xs text-gray-400" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p>
<p class="text-xs text-fg-muted" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p>
</div>
<div class="flex items-center">
<p class="text-xs">{{ $config.version }}</p>
<div class="flex-grow" />
<div v-if="user" class="flex items-center" @click="disconnect">
<p class="text-xs pr-2">Disconnect</p>
<p class="text-xs pr-2">{{ $strings.ButtonDisconnect }}</p>
<i class="material-icons text-sm -mb-0.5">cloud_off</i>
</div>
</div>
@@ -79,13 +79,16 @@ export default {
return this.$store.state.serverSettings || {}
},
username() {
return this.user ? this.user.username : ''
return this.user?.username || ''
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
navItems() {
var items = [
{
icon: 'home',
text: 'Home',
text: this.$strings.ButtonHome,
to: '/bookshelf'
}
]
@@ -93,19 +96,19 @@ export default {
items = [
{
icon: 'cloud_off',
text: 'Connect to Server',
text: this.$strings.ButtonConnectToServer,
to: '/connect'
}
].concat(items)
} else {
items.push({
icon: 'person',
text: 'Account',
text: this.$strings.HeaderAccount,
to: '/account'
})
items.push({
icon: 'equalizer',
text: 'User Stats',
text: this.$strings.ButtonUserStats,
to: '/stats'
})
}
@@ -114,27 +117,33 @@ export default {
items.push({
icon: 'folder',
iconOutlined: true,
text: 'Local Media',
text: this.$strings.ButtonLocalMedia,
to: '/localMedia/folders'
})
} else {
items.push({
icon: 'download',
iconOutlined: false,
text: 'Downloads',
text: this.$strings.HeaderDownloads,
to: '/downloads'
})
}
items.push({
icon: 'settings',
text: 'Settings',
text: this.$strings.HeaderSettings,
to: '/settings'
})
if (this.serverConnectionConfig) {
items.push({
icon: 'language',
text: this.$strings.ButtonGoToWebClient,
action: 'openWebClient'
})
items.push({
icon: 'login',
text: 'Switch Server/User',
text: this.$strings.ButtonSwitchServerUser,
action: 'logout'
})
}
@@ -151,6 +160,10 @@ export default {
if (action === 'logout') {
await this.logout()
this.$router.push('/connect')
} else if (action === 'openWebClient') {
this.show = false
let path = `/library/${this.$store.state.libraries.currentLibraryId}`
await this.$store.dispatch('user/openWebClient', path)
}
},
clickBackground() {
+2 -2
View File
@@ -9,7 +9,7 @@
<div v-show="!entities.length && initialized" class="w-full py-16 text-center text-xl">
<div class="py-4 capitalize">No {{ entityName }}</div>
<ui-btn v-if="hasFilter" @click="clearFilter">Clear Filter</ui-btn>
<ui-btn v-if="hasFilter" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
</div>
</div>
</template>
@@ -79,7 +79,7 @@ export default {
return this.page
},
hasFilter() {
if (this.page === 'series' || this.page === 'collections') return false
if (this.page === 'series' || this.page === 'collections' || this.page === 'playlists') return false
return this.filterBy !== 'all'
},
orderBy() {
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div class="w-full relative">
<div v-if="altViewEnabled" class="px-5 pb-3 pt-4">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ label }}</p>
<p class="font-semibold" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ label }}</p>
</div>
<div class="flex items-end px-3 max-w-full overflow-x-auto" :class="altViewEnabled ? '' : 'bookshelfRow'" :style="{ height: shelfHeight + 'px', paddingBottom: entityPaddingBottom + 'px' }">
+2 -2
View File
@@ -6,8 +6,8 @@
<!-- Author name & num books overlay -->
<div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
<p class="text-center font-semibold truncate text-white" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
</div>
<!-- Loading spinner -->
+3 -3
View File
@@ -5,10 +5,10 @@
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
<p v-else class="truncate text-sm" v-html="matchHtml" />
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-fg-muted">{{ matchHtml }}</p>
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
<p v-if="matchKey !== 'authors'" class="text-xs text-fg truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-fg" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
</div>
+42 -22
View File
@@ -7,23 +7,26 @@
<!-- Alternative bookshelf title/author/sort -->
<div v-if="isAltViewEnabled" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }}
</p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }" class="flex items-center">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }}
</p>
<widgets-explicit-indicator v-if="isExplicit" />
</div>
<p class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div>
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #78350f">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p>
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p>
</div>
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ booksInSeries }}</p>
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ booksInSeries }}</p>
</div>
<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' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-gray-300 text-center">{{ title }}</p>
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-fg-muted text-center">{{ title }}</p>
</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 }" />
@@ -46,15 +49,20 @@
</div>
<!-- Play/pause button for podcast episode -->
<div v-if="recentEpisode" class="absolute z-10 top-0 left-0 bottom-0 right-0 m-auto flex items-center justify-center w-12 h-12 rounded-full bg-white bg-opacity-70" @click.stop="playEpisode">
<span class="material-icons text-6xl text-black text-opacity-80">{{ streamIsPlaying ? 'pause_circle' : 'play_circle_filled' }}</span>
<div v-if="recentEpisode" class="absolute z-10 top-0 left-0 bottom-0 right-0 m-auto flex items-center justify-center w-12 h-12 rounded-full" :class="{ 'bg-white/70': !playerIsStartingForThisMedia }" @click.stop="playEpisode">
<span v-if="!playerIsStartingForThisMedia" class="material-icons text-6xl text-black/80">{{ streamIsPlaying ? 'pause_circle' : 'play_circle_filled' }}</span>
<div v-else class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center bg-black/80 rounded-full overflow-hidden">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</div>
<!-- No progress shown for collapsed series in library -->
<div v-if="!collapsedSeries && (!isPodcast || recentEpisode)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<!-- Downloaded icon -->
<div v-if="showHasLocalDownload" class="absolute right-0 top-0 z-20" :style="{ top: (isPodcast ? 1.75 : 0.375) * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<div v-if="showHasLocalDownload" class="absolute right-0 top-0 z-20" :style="{ top: (isPodcast || (seriesSequence && showSequence) ? 1.75 : 0.375) * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
</div>
@@ -75,19 +83,19 @@
<!-- Podcast Episode # -->
<div v-if="recentEpisodeNumber !== null && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
</p>
</div>
<!-- Podcast Num Episodes -->
<div v-else-if="numEpisodes && !numEpisodesIncomplete && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
</div>
<!-- Podcast Num Episodes Incomplete -->
<div v-else-if="numEpisodesIncomplete && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
</div>
</div>
</template>
@@ -305,7 +313,7 @@ export default {
return !!this.userProgress?.isFinished
},
showError() {
return this.numMissingParts || this.isMissing || this.isInvalid
return this.isMissing || this.isInvalid
},
localLibraryItemId() {
if (this.isLocal) return this.libraryItemId
@@ -322,15 +330,22 @@ export default {
streamIsPlaying() {
return this.store.state.playerIsPlaying && this.isStreaming
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
const mediaId = this.store.state.playerStartingPlaybackMediaId
return mediaId === this.recentEpisode?.id
},
isMissing() {
return this._libraryItem.isMissing
},
isInvalid() {
return this._libraryItem.isInvalid
},
numMissingParts() {
if (this.isPodcast) return 0
return this.media.numMissingParts
isExplicit() {
return !!this.mediaMetadata.explicit
},
overlayWrapperClasslist() {
var classes = []
@@ -353,9 +368,6 @@ export default {
userCanDownload() {
return this.store.getters['user/getUserCanDownload']
},
userIsRoot() {
return this.store.getters['user/getIsRoot']
},
titleFontSize() {
return 0.75 * this.sizeMultiplier
},
@@ -447,6 +459,8 @@ export default {
},
async play() {},
async playEpisode() {
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
const eventBus = this.$eventBus || this.$nuxt.$eventBus
if (this.streamIsPlaying) {
@@ -454,6 +468,7 @@ export default {
return
}
this.store.commit('setPlayerIsStartingPlayback', this.recentEpisode.id)
if (this.localEpisode) {
// Play episode locally
eventBus.$emit('play-item', {
@@ -477,7 +492,12 @@ export default {
if (router) {
if (this.recentEpisode) router.push(`/item/${this.libraryItemId}/${this.recentEpisode.id}`)
else if (this.collapsedSeries) router.push(`/bookshelf/series/${this.collapsedSeries.id}`)
else router.push(`/item/${this.libraryItemId}`)
else if (this.localLibraryItem) {
// Pass local library item id to server page to allow falling back to offline page
router.push(`/item/${this.libraryItemId}?localLibraryItemId=${this.localLibraryItemId}`)
} else {
router.push(`/item/${this.libraryItemId}`)
}
}
}
},
+1 -1
View File
@@ -5,7 +5,7 @@
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<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="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-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` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
+9 -13
View File
@@ -15,13 +15,13 @@
<div v-if="!isPodcast && !collapsedSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
</div>
<div class="flex-grow px-2">
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
<p class="whitespace-normal line-clamp-2" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
<span v-if="seriesSequence">#{{ seriesSequence }}&nbsp;</span>{{ displayTitle }}
</p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
<p v-if="duration" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ $elapsedPretty(duration) }}</p>
<p v-if="episodes" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ episodes }}</p>
<p class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
<p v-if="duration" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ $elapsedPretty(duration) }}</p>
<p v-if="episodes" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ episodes }}</p>
</div>
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
@@ -176,7 +176,7 @@ export default {
return this.mediaMetadata.series
},
seriesSequence() {
return this.series ? this.series.sequence : null
return this.series?.sequence || null
},
collapsedSeries() {
// Only added to item object when collapseSeries is enabled
@@ -209,13 +209,13 @@ export default {
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
},
userProgressPercent() {
return this.userProgress ? this.userProgress.progress || 0 : 0
return this.userProgress?.progress || 0
},
itemIsFinished() {
return this.userProgress ? !!this.userProgress.isFinished : false
return !!this.userProgress?.isFinished
},
showError() {
return this.numMissingParts || this.isMissing || this.isInvalid
return this.isMissing || this.isInvalid
},
isStreaming() {
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
@@ -235,10 +235,6 @@ export default {
isInvalid() {
return this._libraryItem.isInvalid
},
numMissingParts() {
if (this.isPodcast) return 0
return this.media.numMissingParts
},
store() {
return this.$store || this.$nuxt.$store
},
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
</div>
<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="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-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` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
+1 -1
View File
@@ -10,7 +10,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">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
<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 v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-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` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<div class="w-10 h-10 flex items-center justify-center">
<span class="material-icons text-2xl text-gray-200">local_offer</span>
<span class="material-icons text-2xl text-fg">local_offer</span>
</div>
<div class="flex-grow px-2 tagSearchCardContent h-full">
<p class="truncate text-sm">{{ tag }}</p>
+56 -27
View File
@@ -3,16 +3,16 @@
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg px-4 py-6 w-full">
<!-- list of server connection configs -->
<template v-if="!showForm">
<div v-for="config in serverConnectionConfigs" :key="config.id" class="flex items-center py-4 my-1 border-b border-white border-opacity-10 relative" @click="connectToServer(config)">
<span class="material-icons-outlined text-xl text-gray-300">dns</span>
<p class="pl-3 pr-6 text-base text-gray-200">{{ config.name }}</p>
<div v-for="config in serverConnectionConfigs" :key="config.id" class="flex items-center py-4 my-1 border-b border-fg/10 relative" @click="connectToServer(config)">
<span class="material-icons-outlined text-xl text-fg-muted">dns</span>
<p class="pl-3 pr-6 text-base text-fg">{{ config.name }}</p>
<div class="absolute top-0 right-0 h-full px-4 flex items-center" @click.stop="editServerConfig(config)">
<span class="material-icons text-lg text-gray-300">more_vert</span>
<span class="material-icons text-lg text-fg-muted">more_vert</span>
</div>
</div>
<div class="my-1 py-4 w-full">
<ui-btn class="w-full" @click="newServerConfigClick">Add New Server</ui-btn>
<ui-btn class="w-full" @click="newServerConfigClick">{{ $strings.ButtonAddNewServer }}</ui-btn>
</div>
</template>
<!-- form to add a new server connection config -->
@@ -20,37 +20,37 @@
<!-- server address input -->
<form v-if="!showAuth" @submit.prevent="submit" novalidate class="w-full">
<div v-if="serverConnectionConfigs.length" class="flex items-center mb-4" @click="showServerList">
<span class="material-icons text-gray-300">arrow_back</span>
<span class="material-icons text-fg-muted">arrow_back</span>
</div>
<h2 class="text-lg leading-7 mb-2">Server address</h2>
<h2 class="text-lg leading-7 mb-2">{{ $strings.LabelServerAddress }}</h2>
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || !!serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full h-10" />
<div class="flex justify-end items-center mt-6">
<ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
<ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10">{{ networkConnected ? $strings.ButtonSubmit : $strings.MessageNoNetworkConnection }}</ui-btn>
</div>
</form>
<!-- username/password and auth methods -->
<template v-else>
<div v-if="serverConfig.id" class="flex items-center mb-4" @click="showServerList">
<span class="material-icons text-gray-300">arrow_back</span>
<span class="material-icons text-fg-muted">arrow_back</span>
</div>
<div class="flex items-center">
<p class="text-gray-300">{{ serverConfig.address }}</p>
<p class="text-fg-muted">{{ serverConfig.address }}</p>
<div class="flex-grow" />
<span v-if="!serverConfig.id" class="material-icons" style="font-size: 1.1rem" @click="editServerAddress">edit</span>
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="w-full h-px bg-fg/10 my-2" />
<form v-if="isLocalAuthEnabled" @submit.prevent="submitAuth" class="pt-3">
<ui-text-input v-model="serverConfig.username" :disabled="processing" placeholder="username" class="w-full mb-2 text-lg" />
<ui-text-input v-model="password" type="password" :disabled="processing" placeholder="password" class="w-full mb-2 text-lg" />
<ui-text-input v-model="serverConfig.username" :disabled="processing" :placeholder="$strings.LabelUsername" class="w-full mb-2 text-lg" />
<ui-text-input v-model="password" type="password" :disabled="processing" :placeholder="$strings.LabelPassword" class="w-full mb-2 text-lg" />
<div class="flex items-center pt-2">
<ui-icon-btn v-if="serverConfig.id" small bg-color="error" icon="delete" @click="removeServerConfigClick" />
<ui-icon-btn v-if="serverConfig.id" small bg-color="error" icon="delete" type="button" @click="removeServerConfigClick" />
<div class="flex-grow" />
<ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
<ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? $strings.ButtonSubmit : $strings.MessageNoNetworkConnection }}</ui-btn>
</div>
</form>
<div v-if="isLocalAuthEnabled && isOpenIDAuthEnabled" class="w-full h-px bg-white bg-opacity-10 my-4" />
<div v-if="isLocalAuthEnabled && isOpenIDAuthEnabled" class="w-full h-px bg-fg/10 my-4" />
<ui-btn v-if="isOpenIDAuthEnabled" :disabled="processing" class="h-10 w-full" @click="clickLoginWithOpenId">{{ oauth.buttonText }}</ui-btn>
</template>
</div>
@@ -62,7 +62,7 @@
</div>
</div>
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black bg-opacity-75 flex items-center justify-center z-30 transition-opacity duration-500">
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black/75 flex items-center justify-center z-30 transition-opacity duration-500">
<div>
<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" />
@@ -73,7 +73,7 @@
</div>
</div>
<p v-if="!serverConnectionConfigs.length" class="mt-2 text-center text-error"><strong>Important!</strong> This app is designed to work with an Audiobookshelf server that you or someone you know is hosting. This app does not provide any content.</p>
<p v-if="!serverConnectionConfigs.length" class="mt-2 text-center text-error" v-html="$strings.MessageAudiobookshelfServerRequired" />
<modals-custom-headers-modal v-model="showAddCustomHeaders" :custom-headers.sync="serverConfig.customHeaders" />
</div>
@@ -95,6 +95,7 @@ export default {
processing: false,
serverConfig: {
address: null,
version: null,
username: null,
customHeaders: null
},
@@ -107,7 +108,8 @@ export default {
state: null,
verifier: null,
challenge: null,
buttonText: 'Login with OpenID'
buttonText: 'Login with OpenID',
enforceHTTPs: true // RFC 6749, Section 10.9 requires https
}
}
},
@@ -115,6 +117,9 @@ export default {
deviceData() {
return this.$store.state.deviceData || {}
},
deviceSettings() {
return this.deviceData.deviceSettings || {}
},
networkConnected() {
return this.$store.state.networkConnected
},
@@ -155,7 +160,7 @@ export default {
*/
async clickLoginWithOpenId() {
// oauth standard requires https explicitly
if (!this.serverConfig.address.startsWith('https')) {
if (!this.serverConfig.address.startsWith('https') && this.oauth.enforceHTTPs) {
console.warn(`[SSO] Oauth2 requires HTTPS`)
this.$toast.error(`SSO: The URL to the server must be https:// secured`)
return
@@ -179,14 +184,19 @@ export default {
const client_id = redirectUrl.searchParams.get('client_id')
const scope = redirectUrl.searchParams.get('scope')
const state = redirectUrl.searchParams.get('state')
let redirect_uri_param = redirectUrl.searchParams.get('redirect_uri')
// Backwards compatability with 2.6.0
if (this.serverConfig.version === '2.6.0') {
redirect_uri_param = 'audiobookshelf://oauth'
}
if (!client_id || !scope || !state) {
console.warn(`[SSO] Invalid OpenID URL - client_id scope or state missing: ${redirectUrl}`)
if (!client_id || !scope || !state || !redirect_uri_param) {
console.warn(`[SSO] Invalid OpenID URL - client_id scope state or redirect_uri missing: ${redirectUrl}`)
this.$toast.error(`SSO: Invalid answer`)
return
}
if (redirectUrl.protocol !== 'https:') {
if (redirectUrl.protocol !== 'https:' && this.oauth.enforceHTTPs) {
console.warn(`[SSO] Insecure Redirection by SSO provider: ${redirectUrl.protocol} is not allowed. Use HTTPS`)
this.$toast.error(`SSO: The SSO provider must return a HTTPS secured URL`)
return
@@ -195,8 +205,8 @@ export default {
// We need to verify if the state is the same later
this.oauth.state = state
const host = `https://${redirectUrl.host}`
const buildUrl = `${host}${redirectUrl.pathname}?response_type=code` + `&client_id=${encodeURIComponent(client_id)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}` + `&redirect_uri=${encodeURIComponent('audiobookshelf://oauth')}` + `&code_challenge=${encodeURIComponent(this.oauth.challenge)}&code_challenge_method=S256`
const host = `${redirectUrl.protocol}//${redirectUrl.host}`
const buildUrl = `${host}${redirectUrl.pathname}?response_type=code` + `&client_id=${encodeURIComponent(client_id)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}` + `&redirect_uri=${encodeURIComponent(redirect_uri_param)}` + `&code_challenge=${encodeURIComponent(this.oauth.challenge)}&code_challenge_method=S256`
// example url for authentik
// const authURL = "https://authentik/application/o/authorize/?response_type=code&client_id=41cd96f...&redirect_uri=audiobookshelf%3A%2F%2Foauth&scope=openid%20openid%20email%20profile&state=asdds..."
@@ -243,8 +253,11 @@ export default {
this.oauth.verifier = verifier
this.oauth.challenge = challenge
// set parameter isRest to true, so the backend wont attempt a redirect after we call backend:/callback in exchangeCodeForToken
const backendEndpoint = `${url}/auth/openid?code_challenge=${challenge}&code_challenge_method=S256&isRest=true`
let backendEndpoint = `${url}/auth/openid?code_challenge=${challenge}&code_challenge_method=S256&redirect_uri=${encodeURIComponent('audiobookshelf://oauth')}&client_id=${encodeURIComponent('Audiobookshelf-App')}&response_type=code`
// Backwards compatability with 2.6.0
if (this.serverConfig.version === '2.6.0') {
backendEndpoint += '&isRest=true'
}
try {
const response = await CapacitorHttp.get({
@@ -608,6 +621,7 @@ export default {
this.showAuth = true
this.authMethods = statusData.data.authMethods || []
this.oauth.buttonText = statusData.data.authFormData?.authOpenIDButtonText || 'Login with OpenID'
this.serverConfig.version = statusData.data.serverVersion
if (statusData.data.authFormData?.authOpenIDAutoLaunch) {
this.clickLoginWithOpenId()
@@ -789,6 +803,7 @@ export default {
this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
this.$setServerLanguageCode(serverSettings.language)
// Set library - Use last library if set and available fallback to default user library
var lastLibraryId = await this.$localStore.getLastLibraryId()
@@ -801,9 +816,23 @@ export default {
this.serverConfig.userId = user.id
this.serverConfig.token = user.token
this.serverConfig.username = user.username
delete this.serverConfig.version
var serverConnectionConfig = await this.$db.setServerConnectionConfig(this.serverConfig)
// Set the device language to match the servers if this is the first server connection
if (!this.serverConnectionConfigs.length && serverSettings.language !== 'en-us') {
const deviceSettings = {
...this.deviceSettings,
languageCode: serverSettings.language
}
const updatedDeviceData = await this.$db.updateDeviceSettings(deviceSettings)
if (updatedDeviceData) {
this.$store.commit('setDeviceData', updatedDeviceData)
this.$setLanguageCode(updatedDeviceData.deviceSettings?.languageCode || 'en-us')
}
}
this.$store.commit('user/setUser', user)
this.$store.commit('user/setServerConnectionConfig', serverConnectionConfig)
+1 -1
View File
@@ -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 class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
<p class="text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
<p class="text-fg-muted text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
</div>
</div>
</template>
+11 -11
View File
@@ -2,24 +2,24 @@
<div class="w-full px-2">
<img v-if="podcast.imageUrl" :src="podcast.imageUrl" class="h-36 w-36 object-contain mx-auto mb-2" />
<ui-text-input-with-label v-model="podcast.title" :label="'Title'" class="mb-2 text-sm" @input="titleUpdated" />
<ui-text-input-with-label v-model="podcast.title" :label="$strings.LabelTitle" class="mb-2 text-sm" @input="titleUpdated" />
<ui-text-input-with-label v-model="podcast.author" :label="'Author'" class="mb-2 text-sm" />
<ui-text-input-with-label v-model="podcast.author" :label="$strings.LabelAuthor" class="mb-2 text-sm" />
<ui-text-input-with-label v-model="podcast.feedUrl" :label="'Feed URL'" readonly class="mb-2 text-sm" />
<ui-text-input-with-label v-model="podcast.feedUrl" :label="$strings.LabelFeedURL" readonly class="mb-2 text-sm" />
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="'Genres'" class="mb-2 text-sm" />
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="$strings.LabelGenres" class="mb-2 text-sm" />
<ui-textarea-with-label v-model="podcast.description" :label="'Description'" :rows="3" class="mb-2 text-sm" />
<ui-textarea-with-label v-model="podcast.description" :label="$strings.LabelDescription" :rows="3" class="mb-2 text-sm" />
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" :label="'Folder'" class="mb-2 text-sm" @input="folderUpdated" />
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" :label="$strings.LabelFolder" class="mb-2 text-sm" @input="folderUpdated" />
<ui-text-input-with-label v-model="fullPath" :label="'Podcast Path'" input-class="h-10" readonly class="mb-2 text-sm" />
<ui-text-input-with-label v-model="fullPath" :label="$strings.LabelPath" input-class="h-10" readonly class="mb-2 text-sm" />
<div class="flex items-center py-4 px-2">
<ui-checkbox v-model="podcast.autoDownloadEpisodes" :label="'Auto Download Episodes'" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm font-semibold" />
<ui-checkbox v-model="podcast.autoDownloadEpisodes" :label="$strings.LabelAutoDownloadEpisodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm font-semibold" />
<div class="flex-grow" />
<ui-btn color="success" @click="submit">Submit</ui-btn>
<ui-btn color="success" @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</div>
</template>
@@ -144,11 +144,11 @@ export default {
.post('/api/podcasts', podcastPayload)
.then((libraryItem) => {
this._processing = false
this.$toast.success('Podcast added')
this.$toast.success(this.$strings.ToastPodcastCreateSuccess)
this.$router.push(`/item/${libraryItem.id}`)
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to add podcast'
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastPodcastCreateFailed
console.error('Failed to create podcast', error)
this._processing = false
this.$toast.error(errorMsg)
+15 -15
View File
@@ -1,7 +1,7 @@
<template>
<div class="w-full h-9 bg-bg relative">
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary text-gray-200">
<nuxt-link v-for="item in items" :key="item.to" :to="item.to" class="h-full flex-grow flex items-center justify-center" :class="routeName === item.routeName ? 'bg-primary' : 'text-gray-400'">
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary">
<nuxt-link v-for="item in items" :key="item.to" :to="item.to" class="h-full flex-grow flex items-center justify-center" :class="routeName === item.routeName ? 'bg-primary' : 'text-fg-muted'">
<p v-if="routeName === item.routeName" class="text-sm font-semibold">{{ item.text }}</p>
<span v-else-if="item.iconPack === 'abs-icons'" class="abs-icons" :class="`icon-${item.icon} ${item.iconClass || ''}`"></span>
<span v-else :class="`${item.iconPack} ${item.iconClass || ''}`">{{ item.icon }}</span>
@@ -20,7 +20,7 @@ export default {
return this.$store.getters['libraries/getCurrentLibrary']
},
currentLibraryIcon() {
return this.currentLibrary ? this.currentLibrary.icon : 'database'
return this.currentLibrary?.icon || 'database'
},
userHasPlaylists() {
return this.$store.state.libraries.numUserPlaylists
@@ -38,7 +38,7 @@ export default {
iconPack: 'abs-icons',
icon: 'home',
iconClass: 'text-xl',
text: 'Home'
text: this.$strings.ButtonHome
},
{
to: '/bookshelf/latest',
@@ -46,7 +46,7 @@ export default {
iconPack: 'abs-icons',
icon: 'list',
iconClass: 'text-xl',
text: 'Latest'
text: this.$strings.ButtonLatest
},
{
to: '/bookshelf/library',
@@ -54,18 +54,18 @@ export default {
iconPack: 'abs-icons',
icon: this.currentLibraryIcon,
iconClass: 'text-lg',
text: 'Library'
text: this.$strings.ButtonLibrary
}
]
if (this.userIsAdminOrUp) {
items.push({
to: '/bookshelf/search',
routeName: 'bookshelf-search',
to: '/bookshelf/add-podcast',
routeName: 'bookshelf-add-podcast',
iconPack: 'material-icons',
icon: 'podcasts',
iconClass: 'text-xl',
text: 'Add'
text: this.$strings.ButtonAdd
})
}
} else {
@@ -76,7 +76,7 @@ export default {
iconPack: 'abs-icons',
icon: 'home',
iconClass: 'text-xl',
text: 'Home'
text: this.$strings.ButtonHome
},
{
to: '/bookshelf/library',
@@ -84,7 +84,7 @@ export default {
iconPack: 'abs-icons',
icon: this.currentLibraryIcon,
iconClass: 'text-lg',
text: 'Library'
text: this.$strings.ButtonLibrary
},
{
to: '/bookshelf/series',
@@ -92,7 +92,7 @@ export default {
iconPack: 'abs-icons',
icon: 'columns',
iconClass: 'text-lg pt-px',
text: 'Series'
text: this.$strings.ButtonSeries
},
{
to: '/bookshelf/collections',
@@ -100,7 +100,7 @@ export default {
iconPack: 'material-icons-outlined',
icon: 'collections_bookmark',
iconClass: 'text-xl',
text: 'Collections'
text: this.$strings.ButtonCollections
},
{
to: '/bookshelf/authors',
@@ -108,7 +108,7 @@ export default {
iconPack: 'abs-icons',
icon: 'authors',
iconClass: 'text-2xl',
text: 'Authors'
text: this.$strings.ButtonAuthors
}
]
}
@@ -119,7 +119,7 @@ export default {
routeName: 'bookshelf-playlists',
iconPack: 'material-icons',
icon: 'queue_music',
text: 'Playlists'
text: this.$strings.ButtonPlaylists
})
}
+10 -10
View File
@@ -64,16 +64,16 @@ export default {
return this.$route.query || {}
},
entityTitle() {
if (this.isPodcast) return 'Podcasts'
if (this.page === 'library') return 'Books'
else if (this.page === 'series') {
return 'Series'
} else if (this.page === 'collections') {
return 'Collections'
if (this.page === 'library') {
return this.isPodcast ? this.$strings.LabelPodcasts : this.$strings.LabelBooks
} else if (this.page === 'playlists') {
return 'Playlists'
return this.$strings.ButtonPlaylists
} else if (this.page === 'series') {
return this.$strings.LabelSeries
} else if (this.page === 'collections') {
return this.$strings.ButtonCollections
} else if (this.page === 'authors') {
return 'Authors'
return this.$strings.LabelAuthors
}
return ''
},
@@ -92,7 +92,7 @@ export default {
if (this.seriesBookPage) {
return [
{
text: 'Collapse Sub-Series',
text: this.$strings.LabelCollapseSeries,
value: 'collapse_subseries',
icon: this.settings.collapseBookSeries ? 'check_box' : 'check_box_outline_blank'
}
@@ -100,7 +100,7 @@ export default {
} else {
return [
{
text: 'Collapse Series',
text: this.$strings.LabelCollapseSeries,
value: 'collapse_series',
icon: this.settings.collapseSeries ? 'check_box' : 'check_box_outline_blank'
}
@@ -1,29 +1,30 @@
<template>
<modals-modal v-model="show" :width="200" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Auto Rewind Time</p>
<div class="absolute top-11 left-4 z-40">
<p class="text-white text-2xl truncate capitalize">{{ $strings.LabelAutoRewindTime }}</p>
</div>
</template>
<div
class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center"
@click="
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="
show = false
manualTimerModal = false
"
>
">
<div 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 v-if="manualTimerModal" class="p-4">
<div class="flex mb-4" @click="manualTimerModal = false">
<span class="material-icons text-3xl">arrow_back</span>
</div>
<div class="flex my-2 justify-between">
<ui-btn @click="decreaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">remove</span></ui-btn>
<ui-btn @click="decreaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px">
<span class="material-icons">remove</span>
</ui-btn>
<p class="text-2xl font-mono text-center">{{ manualTimeoutMin }} min</p>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px">
<span class="material-icons">add</span>
</ui-btn>
</div>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">Set Timer</ui-btn>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">{{ $strings.ButtonSetTimer }}</ui-btn>
</div>
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="timeout in timeouts">
@@ -35,7 +36,7 @@
</template>
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="manualTimerModal = true">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">Custom time</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelCustomTime }}</span>
</div>
</li>
</ul>
+12 -16
View File
@@ -1,12 +1,12 @@
<template>
<modals-modal v-model="show" :width="400" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Bookmarks</p>
<div class="absolute top-11 left-4 z-40">
<p class="text-white text-2xl truncate">{{ $strings.LabelYourBookmarks }}</p>
</div>
</template>
<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 rounded-lg bg-primary border border-white border-opacity-20 overflow-y-auto overflow-x-hidden" style="max-height: 80vh" @click.stop.prevent>
<div ref="container" class="w-full rounded-lg bg-primary border border-border overflow-y-auto overflow-x-hidden" style="max-height: 80vh" @click.stop.prevent>
<div class="w-full h-full p-4" v-show="showBookmarkTitleInput">
<div class="flex mb-4 items-center">
<div class="w-9 h-9 flex items-center justify-center rounded-full hover:bg-white hover:bg-opacity-10 cursor-pointer" @click.stop="showBookmarkTitleInput = false">
@@ -14,9 +14,7 @@
</div>
<p class="text-xl pl-2">{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}</p>
<div class="flex-grow" />
<p class="text-xl font-mono">
{{ this.$secondsToTimestamp(currentTime) }}
</p>
<p class="text-xl font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p>
</div>
<ui-text-input-with-label v-model="newBookmarkTitle" label="Note" />
@@ -29,14 +27,12 @@
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @edit="editBookmark" @delete="deleteBookmark" />
</template>
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
<p class="text-xl">No Bookmarks</p>
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
</div>
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center justify-between border-b border-white border-opacity-10 bg-blue-500 bg-opacity-20 cursor-pointer text-white text-opacity-80 hover:bg-opacity-40 hover:text-opacity-100" @click.stop="createBookmark">
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center justify-between border-b border-fg/10 bg-success cursor-pointer text-white text-opacity-80" @click.stop="createBookmark">
<span class="material-icons">add</span>
<p class="text-base pl-2">Create Bookmark</p>
<p class="text-sm font-mono">
{{ this.$secondsToTimestamp(currentTime) }}
</p>
<p class="text-base pl-2">{{ $strings.ButtonCreateBookmark }}</p>
<p class="text-sm font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p>
</div>
</div>
</div>
@@ -98,7 +94,7 @@ export default {
await this.$hapticsImpact()
const { value } = await Dialog.confirm({
title: 'Remove Bookmark',
message: `Are you sure you want to remove bookmark?`
message: this.$strings.MessageConfirmRemoveBookmark
})
if (!value) return
@@ -108,7 +104,7 @@ export default {
this.$store.commit('user/deleteBookmark', { libraryItemId: this.libraryItemId, time: bm.time })
})
.catch((error) => {
this.$toast.error(`Failed to remove bookmark`)
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
console.error(error)
})
},
@@ -124,7 +120,7 @@ export default {
this.showBookmarkTitleInput = false
})
.catch((error) => {
this.$toast.error(`Failed to update bookmark`)
this.$toast.error(this.$strings.ToastBookmarkUpdateFailed)
console.error(error)
})
},
@@ -142,7 +138,7 @@ export default {
this.$toast.success('Bookmark added')
})
.catch((error) => {
this.$toast.error(`Failed to create bookmark`)
this.$toast.error(this.$strings.ToastBookmarkCreateFailed)
console.error(error)
})
+6 -6
View File
@@ -2,19 +2,19 @@
<modals-modal v-model="show" :width="400" height="100%">
<template #outer>
<div v-if="currentChapter" class="absolute top-10 left-4 z-40 pt-1" style="max-width: 80%">
<p class="text-white text-lg truncate">{{ chapters.length }} Chapters</p>
<p class="text-white text-lg truncate">{{ chapters.length }} {{ $strings.LabelChapters }}</p>
</div>
</template>
<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-secondary 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-secondary rounded-lg border border-fg/20" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="(chapter, index) in chapters">
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-gray-50 select-none relative py-4 cursor-pointer" :class="currentChapterId === chapter.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
<template v-for="chapter in chapters">
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-fg select-none relative py-4 cursor-pointer" :class="currentChapterId === chapter.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
<div class="relative flex items-center pl-3 pr-20">
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ index + 1 }} - {{ chapter.title }}</p>
<p class="font-normal block truncate text-sm text-fg/80">{{ chapter.title }}</p>
<div class="absolute top-0 right-3 -mt-0.5">
<span class="font-mono text-white text-opacity-90 leading-3 text-sm" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start / _playbackRate) }}</span>
<span class="font-mono text-fg-muted leading-3 text-sm" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start / _playbackRate) }}</span>
</div>
</div>
+5 -5
View File
@@ -7,14 +7,14 @@
</template>
<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/20 p-2" style="max-height: 75%" @click.stop>
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-fg/20 p-2" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow">
<slot :name="item.value" :item="item" :selected="item.value === selected">
<li :key="item.value" :ref="`item-${item.value}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" :style="{ paddingTop: itemPaddingY, paddingBottom: itemPaddingY }" role="option" @click="clickedOption(item.value)">
<li :key="item.value" :ref="`item-${item.value}`" class="text-fg select-none relative cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" :style="{ paddingTop: itemPaddingY, paddingBottom: itemPaddingY }" role="option" @click="clickedOption(item.value)">
<div class="relative flex items-center px-3">
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-white text-opacity-80">{{ item.icon }}</span>
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-fg text-opacity-80">{{ item.icon }}</span>
<p class="font-normal block truncate text-base text-fg text-opacity-80">{{ item.text }}</p>
</div>
</li>
</slot>
@@ -40,7 +40,7 @@ export default {
default: '16px'
},
width: {
type: Number,
type: [String, Number],
default: 300
}
},
+48 -24
View File
@@ -1,15 +1,15 @@
<template>
<modals-modal v-model="show" width="90%" height="100%">
<template #outer>
<div v-show="selected !== 'all'" class="absolute top-10 left-4 z-40">
<ui-btn class="text-xl border-yellow-400 border-opacity-40" @click="clearSelected">Clear</ui-btn>
<div v-show="selected !== 'all'" class="absolute top-12 left-4 z-40">
<ui-btn class="text-lg border-yellow-400 border-opacity-40 h-10" :padding-y="0" @click="clearSelected">{{ $strings.ButtonClearFilter }}</ui-btn>
</div>
</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 overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-fg/20 mt-8" style="max-height: 75%" @click.stop>
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items">
<li :key="item.value" class="text-gray-50 select-none relative py-4 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
<div class="flex items-center justify-between">
<span class="font-normal ml-3 block truncate text-lg">{{ item.text }}</span>
</div>
@@ -20,12 +20,12 @@
</template>
</ul>
<ul v-show="sublist" class="h-full w-full rounded-lg" role="listbox" aria-labelledby="listbox-label">
<li class="text-gray-50 select-none relative py-3 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
<li class="text-fg select-none relative py-3 pl-9 cursor-pointer" role="option" @click="sublist = null">
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
<span class="material-icons text-2xl">arrow_left</span>
</div>
<div class="flex items-center justify-between">
<span class="font-normal ml-3 block truncate text-lg">Back</span>
<span class="font-normal ml-3 block truncate text-lg">{{ $strings.ButtonBack }}</span>
</div>
</li>
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
@@ -34,7 +34,7 @@
</div>
</li>
<template v-for="item in sublistItems">
<li :key="item.value" class="text-gray-50 select-none relative px-4 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item.value}` === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item.value)">
<li :key="item.value" class="text-fg select-none relative px-4 cursor-pointer" :class="`${sublist}.${item.value}` === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item.value)">
<div class="flex items-center">
<span class="font-normal truncate py-3 text-base">{{ item.text }}</span>
</div>
@@ -57,62 +57,67 @@ export default {
sublist: null,
bookItems: [
{
text: 'All',
text: this.$strings.LabelAll,
value: 'all'
},
{
text: 'Genre',
text: this.$strings.LabelGenre,
value: 'genres',
sublist: true
},
{
text: 'Tag',
text: this.$strings.LabelTag,
value: 'tags',
sublist: true
},
{
text: 'Series',
text: this.$strings.LabelSeries,
value: 'series',
sublist: true
},
{
text: 'Authors',
text: this.$strings.LabelAuthor,
value: 'authors',
sublist: true
},
{
text: 'Narrator',
text: this.$strings.LabelNarrator,
value: 'narrators',
sublist: true
},
{
text: 'Language',
text: this.$strings.LabelLanguage,
value: 'languages',
sublist: true
},
{
text: 'Progress',
text: this.$strings.LabelProgress,
value: 'progress',
sublist: true
},
{
text: 'Issues',
text: this.$strings.LabelEbooks,
value: 'ebooks',
sublist: true
},
{
text: this.$strings.ButtonIssues,
value: 'issues',
sublist: false
}
],
podcastItems: [
{
text: 'All',
text: this.$strings.LabelAll,
value: 'all'
},
{
text: 'Genre',
text: this.$strings.LabelGenre,
value: 'genres',
sublist: true
},
{
text: 'Tag',
text: this.$strings.LabelTag,
value: 'tags',
sublist: true
}
@@ -176,24 +181,36 @@ export default {
return [
{
id: 'finished',
name: 'Finished'
name: this.$strings.LabelFinished
},
{
id: 'in-progress',
name: 'In Progress'
name: this.$strings.LabelInProgress
},
{
id: 'not-started',
name: 'Not Started'
name: this.$strings.LabelNotStarted
},
{
id: 'not-finished',
name: 'Not Finished'
name: this.$strings.LabelNotFinished
}
]
},
ebooks() {
return [
{
id: 'ebook',
name: this.$strings.LabelHasEbook
},
{
id: 'supplementary',
name: this.$strings.LabelHasSupplementaryEbook
}
]
},
sublistItems() {
return (this[this.sublist] || []).map((item) => {
const sublistItems = (this[this.sublist] || []).map((item) => {
if (typeof item === 'string') {
return {
text: item,
@@ -206,6 +223,13 @@ export default {
}
}
})
if (this.sublist === 'series') {
sublistItems.unshift({
text: this.$strings.MessageNoSeries,
value: this.$encode('no-series')
})
}
return sublistItems
},
filterData() {
return this.$store.state.libraries.filterData || {}
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div ref="wrapper" class="modal modal-bg w-screen fixed bottom-0 left-0 flex items-center justify-center z-50" :class="halfScreen ? 'h-[50vh]' : 'h-screen'" @click.stop @touchstart.stop @touchend.stop>
<div ref="content" class="relative text-white h-full w-full bg-bg">
<div ref="wrapper" class="modal modal-bg w-screen fixed bottom-0 left-0 flex items-center justify-center z-50" :class="halfScreen ? 'h-[50vh] min-h-[400px]' : 'h-screen'" @click.stop @touchstart.stop @touchend.stop>
<div ref="content" class="relative text-fg h-full w-full bg-bg">
<slot />
</div>
</div>
+5 -5
View File
@@ -1,18 +1,18 @@
<template>
<modals-modal v-model="show" :width="400" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Details</p>
<div class="absolute top-11 left-4 z-40">
<p class="text-white text-2xl truncate">{{ $strings.HeaderDetails }}</p>
</div>
</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 overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-2" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-border p-2" style="max-height: 75%" @click.stop>
<p class="mb-2">{{ mediaMetadata.title }}</p>
<div v-if="size" class="text-sm mb-2">Size: {{ $bytesPretty(size) }}</div>
<div v-if="size" class="text-sm mb-2">{{ $strings.LabelSize }}: {{ $bytesPretty(size) }}</div>
<p class="mb-1 text-xs text-gray-200">ID: {{ _libraryItem.id }}</p>
<p class="mb-1 text-xs text-fg">ID: {{ _libraryItem.id }}</p>
</div>
</div>
</modals-modal>
+65 -27
View File
@@ -2,7 +2,7 @@
<div>
<modals-dialog v-model="show" :items="moreMenuItems" @action="moreMenuAction" />
<modals-item-details-modal v-model="showDetailsModal" :library-item="libraryItem" />
<modals-dialog v-model="showSendEbookDevicesModal" title="Select a device" :items="ereaderDeviceItems" @action="sendEbookToDeviceAction" />
<modals-dialog v-model="showSendEbookDevicesModal" :title="$strings.LabelSelectADevice" :items="ereaderDeviceItems" @action="sendEbookToDeviceAction" />
</div>
</template>
@@ -26,6 +26,10 @@ export default {
type: Object,
default: () => null
},
playlist: {
type: Object,
default: () => null
},
hideRssFeedOption: Boolean
},
data() {
@@ -52,7 +56,7 @@ export default {
// TODO: Implement on iOS
if (this.$platform !== 'ios' && !this.isPodcast) {
items.push({
text: 'History',
text: this.$strings.ButtonHistory,
value: 'history',
icon: 'history'
})
@@ -61,7 +65,7 @@ export default {
if (!this.isPodcast || this.episode) {
if (!this.userIsFinished) {
items.push({
text: 'Mark as Finished',
text: this.$strings.MessageMarkAsFinished,
value: 'markFinished',
icon: 'beenhere'
})
@@ -69,7 +73,7 @@ export default {
if (this.progressPercent > 0) {
items.push({
text: 'Discard Progress',
text: this.$strings.MessageDiscardProgress,
value: 'discardProgress',
icon: 'backspace'
})
@@ -78,23 +82,32 @@ export default {
if ((!this.isPodcast && this.serverLibraryItemId) || (this.episode && this.serverEpisodeId)) {
items.push({
text: 'Add to Playlist',
text: this.$strings.LabelAddToPlaylist,
value: 'playlist',
icon: 'playlist_add'
})
if (this.ereaderDeviceItems.length) {
items.push({
text: 'Send ebook to device',
text: this.$strings.ButtonSendEbookToDevice,
value: 'sendEbook',
icon: 'send'
})
}
}
// If on playlist page show remove from playlist option
if (this.playlist) {
items.push({
text: this.$strings.LabelRemoveFromPlaylist,
value: 'removeFromPlaylist',
icon: 'playlist_remove'
})
}
if (this.showRSSFeedOption) {
items.push({
text: this.rssFeed ? 'RSS Feed' : 'Open RSS Feed',
text: this.rssFeed ? this.$strings.HeaderRSSFeed : this.$strings.HeaderOpenRSSFeed,
value: 'rssFeed',
icon: 'rss_feed'
})
@@ -102,29 +115,37 @@ export default {
if (this.localLibraryItemId) {
items.push({
text: 'Manage Local Files',
text: this.$strings.ButtonManageLocalFiles,
value: 'manageLocal',
icon: 'folder'
})
if (!this.isPodcast) {
items.push({
text: 'Delete Local Item',
text: this.$strings.ButtonDeleteLocalItem,
value: 'deleteLocal',
icon: 'delete'
})
} else if (this.localEpisodeId) {
items.push({
text: 'Delete Local Episode',
text: this.$strings.ButtonDeleteLocalEpisode,
value: 'deleteLocalEpisode',
icon: 'delete'
})
}
}
if (this.isConnectedToServer) {
items.push({
text: this.$strings.ButtonGoToWebClient,
value: 'openWebClient',
icon: 'language'
})
}
if (!this.episode) {
items.push({
text: 'More Info',
text: this.$strings.LabelMoreInfo,
value: 'details',
icon: 'info'
})
@@ -255,6 +276,8 @@ export default {
} else if (action === 'playlist') {
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.episode }])
this.$store.commit('globals/setShowPlaylistsAddCreateModal', true)
} else if (action === 'removeFromPlaylist') {
this.removeFromPlaylistClick()
} else if (action === 'markFinished') {
if (this.episode) this.toggleEpisodeFinished()
else this.toggleFinished()
@@ -270,6 +293,8 @@ export default {
this.clickRSSFeed()
} else if (action === 'sendEbook') {
this.showSendEbookDevicesModal = true
} else if (action === 'openWebClient') {
this.$store.dispatch('user/openWebClient', `/item/${this.serverLibraryItemId}`)
}
},
async toggleFinished() {
@@ -279,7 +304,7 @@ export default {
if (this.userItemProgress && this.userItemProgress.progress > 0 && !this.userIsFinished) {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to mark this item as Finished?'
message: this.$strings.MessageConfirmMarkAsFinished
})
if (!value) return
}
@@ -304,7 +329,7 @@ export default {
}
await this.$nativeHttp.patch(`/api/me/progress/${this.serverLibraryItemId}`, updatePayload).catch((error) => {
console.error('Failed', error)
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
})
}
this.$emit('update:processing', false)
@@ -335,7 +360,7 @@ export default {
}
await this.$nativeHttp.patch(`/api/me/progress/${this.serverLibraryItemId}/${this.serverEpisodeId}`, updatePayload).catch((error) => {
console.error('Failed', error)
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
})
}
this.$emit('update:processing', false)
@@ -345,7 +370,7 @@ export default {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to reset your progress?'
message: this.$strings.MessageConfirmDiscardProgress
})
if (value) {
this.$emit('update:processing', true)
@@ -381,18 +406,13 @@ export default {
return
}
let confirmMessage = `Remove local episode "${localFile.basePath}" from your device?`
if (this.serverLibraryItemId) {
confirmMessage += ' The file on the server will be unaffected.'
}
const { value } = await Dialog.confirm({
title: 'Confirm',
message: confirmMessage
message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath])
})
if (value) {
const res = await AbsFileSystem.deleteTrackFromItem({ id: this.localLibraryItemId, trackLocalFileId: localFile.id, trackContentUrl: localEpisodeAudioTrack.contentUrl })
if (res?.id) {
this.$toast.success('Deleted episode successfully')
if (this.isLocal) {
// If this is local episode then redirect to server episode when available
if (this.serverEpisodeId) {
@@ -411,18 +431,13 @@ export default {
async deleteLocalItem() {
await this.$hapticsImpact()
let confirmMessage = 'Remove local files of this item from your device?'
if (this.serverLibraryItemId) {
confirmMessage += ' The files on the server and your progress will be unaffected.'
}
const { value } = await Dialog.confirm({
title: 'Confirm',
message: confirmMessage
message: this.$strings.MessageConfirmDeleteLocalFiles
})
if (value) {
const res = await AbsFileSystem.deleteItem(this.localLibraryItem)
if (res?.success) {
this.$toast.success('Deleted successfully')
if (this.isLocal) {
// If local then redirect to server version when available
if (this.serverLibraryItemId) {
@@ -466,6 +481,29 @@ export default {
.finally(() => {
this.$emit('update:processing', false)
})
},
removeFromPlaylistClick() {
if (!this.playlist) {
this.$toast.error('Invalid: No Playlist')
return
}
this.$emit('update:processing', true)
let url = `/api/playlists/${this.playlist.id}/item/${this.serverLibraryItemId}`
if (this.serverEpisodeId) url += `/${this.serverEpisodeId}`
this.$nativeHttp
.delete(url)
.then(() => {
this.$toast.success('Item removed from playlist')
})
.catch((error) => {
const errorMsg = error.response?.data || 'Unknown error'
console.error('Failed to remove item from playlist', error)
this.$toast.error('Failed to remove from playlist: ' + errorMsg)
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
mounted() {}
+4 -4
View File
@@ -1,16 +1,16 @@
<template>
<modals-modal v-model="show" :width="300" :processing="processing" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40" style="max-width: 80%">
<p class="text-white text-2xl truncate">Libraries</p>
<div class="absolute top-11 left-4 z-40" style="max-width: 80%">
<p class="text-white text-2xl truncate">{{ $strings.HeaderLibraries }}</p>
</div>
</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 overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-border" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="library in libraries">
<li :key="library.id" class="text-gray-50 select-none relative py-3 cursor-pointer" :class="currentLibraryId === library.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(library)">
<li :key="library.id" class="text-fg select-none relative py-3 cursor-pointer" :class="currentLibraryId === library.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(library)">
<div v-show="currentLibraryId === library.id" class="absolute top-0 left-0 w-0.5 bg-warning h-full" />
<div class="flex items-center px-3">
<ui-library-icon :icon="library.icon" />
+2 -2
View File
@@ -2,11 +2,11 @@
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0">
<div class="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-black to-transparent opacity-90 pointer-events-none" />
<div class="absolute z-40 top-10 right-4 h-10 w-10 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
<div class="absolute z-40 top-11 right-4 h-10 w-10 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
<span class="material-icons text-4xl">close</span>
</div>
<slot name="outer" />
<div ref="content" style="min-height: 200px" class="relative text-white max-h-screen" :style="{ height: modalHeight, width: modalWidth, maxWidth: maxWidth }" v-click-outside="clickBg">
<div ref="content" style="min-height: 200px" class="relative text-fg max-h-screen" :style="{ height: modalHeight, width: modalWidth, maxWidth: maxWidth }" v-click-outside="clickBg">
<slot />
</div>
</div>
+29 -21
View File
@@ -1,9 +1,9 @@
<template>
<modals-modal v-model="show" width="90%">
<div class="w-full h-full bg-primary rounded-lg border border-white border-opacity-20">
<div class="w-full h-full bg-primary rounded-lg border border-fg/20">
<ul class="w-full rounded-lg text-base" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items">
<li :key="item.value" class="text-gray-50 select-none relative py-4 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">
<span class="font-normal ml-3 block truncate text-lg">{{ item.text }}</span>
</div>
@@ -29,83 +29,91 @@ export default {
return {
bookItems: [
{
text: 'Title',
text: this.$strings.LabelTitle,
value: 'media.metadata.title'
},
{
text: 'Author (First Last)',
text: this.$strings.LabelAuthorFirstLast,
value: 'media.metadata.authorName'
},
{
text: 'Author (Last, First)',
text: this.$strings.LabelAuthorLastFirst,
value: 'media.metadata.authorNameLF'
},
{
text: 'Published Year',
text: this.$strings.LabelPublishYear,
value: 'media.metadata.publishedYear'
},
{
text: 'Added At',
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: 'Size',
text: this.$strings.LabelSize,
value: 'size'
},
{
text: 'Duration',
text: this.$strings.LabelDuration,
value: 'media.duration'
},
{
text: 'File Birthtime',
text: this.$strings.LabelFileBirthtime,
value: 'birthtimeMs'
},
{
text: 'File Modified',
text: this.$strings.LabelFileModified,
value: 'mtimeMs'
},
{
text: this.$strings.LabelRandomly,
value: 'random'
}
],
podcastItems: [
{
text: 'Title',
text: this.$strings.LabelTitle,
value: 'media.metadata.title'
},
{
text: 'Author',
text: this.$strings.LabelAuthor,
value: 'media.metadata.author'
},
{
text: 'Added At',
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: 'Size',
text: this.$strings.LabelSize,
value: 'size'
},
{
text: 'File Birthtime',
text: this.$strings.LabelFileBirthtime,
value: 'birthtimeMs'
},
{
text: 'File Modified',
text: this.$strings.LabelFileModified,
value: 'mtimeMs'
},
{
text: this.$strings.LabelRandomly,
value: 'random'
}
],
episodeItems: [
{
text: 'Pub Date',
text: this.$strings.LabelPubDate,
value: 'publishedAt'
},
{
text: 'Title',
text: this.$strings.LabelTitle,
value: 'title'
},
{
text: 'Season',
text: this.$strings.LabelSeason,
value: 'season'
},
{
text: 'Episode',
text: this.$strings.LabelEpisode,
value: 'episode'
}
]
+7 -7
View File
@@ -2,29 +2,29 @@
<modals-modal v-model="show" @input="modalInput" :width="200" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Playback Speed</p>
<p class="text-white text-2xl truncate">{{ $strings.LabelPlaybackSpeed }}</p>
</div>
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center">
<div 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 class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-border" style="max-height: 75%" @click.stop>
<ul class="w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="rate in rates">
<li :key="rate" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="rate === selected ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(rate)">
<li :key="rate" class="text-fg select-none relative py-4" :class="rate === selected ? 'bg-bg-hover/50' : ''" role="option" @click="clickedOption(rate)">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg">{{ rate }}x</span>
</div>
</li>
</template>
</ul>
<div class="flex items-center justify-center py-3 border-t border-white border-opacity-10">
<button :disabled="!canDecrement" @click="decrement" class="icon-num-btn w-8 h-8 text-white text-opacity-75 rounded border border-white border-opacity-20 flex items-center justify-center">
<div class="flex items-center justify-center py-3 border-t border-fg/10">
<button :disabled="!canDecrement" @click="decrement" class="icon-num-btn w-8 h-8 text-fg-muted rounded border border-border flex items-center justify-center">
<span class="material-icons">remove</span>
</button>
<div class="w-24 text-center">
<p class="text-xl">{{ playbackRate }}<span class="text-lg"></span></p>
</div>
<button :disabled="!canIncrement" @click="increment" class="icon-num-btn w-8 h-8 text-white text-opacity-75 rounded border border-white border-opacity-20 flex items-center justify-center">
<button :disabled="!canIncrement" @click="increment" class="icon-num-btn w-8 h-8 text-fg-muted rounded border border-border flex items-center justify-center">
<span class="material-icons">add</span>
</button>
</div>
@@ -71,7 +71,7 @@ export default {
}
},
rates() {
return [0.5, 1, 1.2, 1.5, 2, 3]
return [0.5, 1, 1.2, 1.5, 1.7, 2, 3]
},
canIncrement() {
return this.playbackRate + 0.1 <= this.MAX_SPEED
+2 -2
View File
@@ -2,7 +2,7 @@
<modals-modal v-model="show" :width="300" height="100%">
<template #outer>
<div class="absolute top-10 left-4 z-40" style="max-width: 80%">
<p class="text-white text-lg truncate">Select Download Location</p>
<p class="text-white text-lg truncate">{{ $strings.HeaderSelectDownloadLocation }}</p>
</div>
</template>
@@ -71,7 +71,7 @@ export default {
if (!localFolders.some((lf) => lf.id === `internal-${this.mediaType}`)) {
localFolders.push({
id: `internal-${this.mediaType}`,
name: 'Internal App Storage',
name: this.$strings.LabelInternalAppStorage,
mediaType: this.mediaType
})
}
+7 -7
View File
@@ -2,7 +2,7 @@
<modals-modal v-model="show" :width="200" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Sleep Timer</p>
<p class="text-white text-2xl truncate">{{ $strings.HeaderSleepTimer }}</p>
</div>
</template>
@@ -23,24 +23,24 @@
<p class="text-2xl font-mono text-center">{{ manualTimeoutMin }} min</p>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
</div>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">Set Timer</ui-btn>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">{{ $strings.ButtonSetTimer }}</ui-btn>
</div>
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="timeout in timeouts">
<li :key="timeout" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(timeout)">
<li :key="timeout" class="text-fg select-none relative py-4" role="option" @click="clickedOption(timeout)">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg">{{ timeout }} min</span>
</div>
</li>
</template>
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedChapterOption">
<li class="text-fg select-none relative py-4" role="option" @click="clickedChapterOption">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">End of Chapter</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelEndOfChapter }}</span>
</div>
</li>
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="manualTimerModal = true">
<li class="text-fg select-none relative py-4" role="option" @click="manualTimerModal = true">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">Custom time</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelCustomTime }}</span>
</div>
</li>
</ul>
+14 -17
View File
@@ -2,18 +2,12 @@
<modals-modal v-model="show" :width="200" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Sleep Timer</p>
<p class="text-white text-2xl truncate">{{ $strings.HeaderSleepTimer }}</p>
</div>
</template>
<div
class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center"
@click="
show = false
manualTimerModal = false
"
>
<div 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 class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-border" style="max-height: 75%" @click.stop>
<div v-if="manualTimerModal" class="p-4">
<div class="flex mb-4" @click="manualTimerModal = false">
<span class="material-icons text-3xl">arrow_back</span>
@@ -23,24 +17,24 @@
<p class="text-2xl font-mono text-center">{{ manualTimeoutMin }} min</p>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
</div>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">Set Timer</ui-btn>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">{{ $strings.ButtonSetTimer }}</ui-btn>
</div>
<ul v-else-if="!sleepTimerRunning" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="timeout in timeouts">
<li :key="timeout" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(timeout)">
<li :key="timeout" class="text-fg select-none relative py-4" role="option" @click="clickedOption(timeout)">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg">{{ timeout }} min</span>
</div>
</li>
</template>
<li v-if="currentEndOfChapterTime" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedChapterOption(timeout)">
<li v-if="currentEndOfChapterTime" class="text-fg select-none relative py-4" role="option" @click="clickedChapterOption(timeout)">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">End of Chapter</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelEndOfChapter }}</span>
</div>
</li>
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="manualTimerModal = true">
<li class="text-fg select-none relative py-4" role="option" @click="manualTimerModal = true">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">Custom time</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelCustomTime }}</span>
</div>
</li>
</ul>
@@ -51,7 +45,7 @@
<ui-btn @click="increaseSleepTime" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
</div>
<ui-btn @click="cancelSleepTimer" class="w-full">{{ isAuto ? 'Disable Auto Timer' : 'Cancel Timer' }}</ui-btn>
<ui-btn @click="cancelSleepTimer" class="w-full">{{ isAuto ? $strings.ButtonDisableAutoTimer : $strings.ButtonCancelTimer }}</ui-btn>
</div>
</div>
</div>
@@ -71,7 +65,7 @@ export default {
},
data() {
return {
manualTimerModal: null,
manualTimerModal: false,
manualTimeoutMin: 1
}
},
@@ -81,6 +75,9 @@ export default {
return this.value
},
set(val) {
if (!val) {
this.manualTimerModal = false
}
this.$emit('input', val)
}
},
+5 -5
View File
@@ -1,17 +1,17 @@
<template>
<div :key="bookmark.id" :id="`bookmark-row-${bookmark.id}`" class="flex items-center px-1 py-4 justify-start cursor-pointer hover:bg-bg relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click">
<div :key="bookmark.id" :id="`bookmark-row-${bookmark.id}`" class="flex items-center px-1 py-4 justify-start relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click">
<div class="flex-grow overflow-hidden px-2">
<div class="flex items-center mb-0.5">
<i class="material-icons text-lg pr-1 -mb-1" :class="highlight ? 'text-success' : 'text-white/60'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</i>
<i class="material-icons text-lg pr-1 -mb-1" :class="highlight ? 'text-success' : 'text-fg-muted'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</i>
<p class="truncate text-sm">
{{ bookmark.title }}
</p>
</div>
<p class="text-sm font-mono text-gray-300 flex items-center"><span class="material-icons text-base text-white/60 pl-px pr-1">schedule</span>{{ $secondsToTimestamp(bookmark.time) }}</p>
<p class="text-sm font-mono text-fg-muted flex items-center"><span class="material-icons text-base pl-px pr-1">schedule</span>{{ $secondsToTimestamp(bookmark.time) }}</p>
</div>
<div class="h-full flex items-center justify-end transform w-16 pr-2" @click.stop>
<span class="material-icons text-2xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
<span class="material-icons text-2xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span>
<span class="material-icons text-2xl mr-2 text-fg hover:text-yellow-400" @click.stop="editClick">edit</span>
<span class="material-icons text-2xl text-fg hover:text-error" @click.stop="deleteClick">delete</span>
</div>
</div>
</template>
+13 -14
View File
@@ -1,46 +1,46 @@
<template>
<modals-fullscreen-modal v-model="show" :processing="processing">
<div class="flex items-end justify-between h-16 px-4 pb-2">
<h1 class="text-lg">Add to Playlist</h1>
<div class="flex items-end justify-between h-24 px-4 pb-2">
<h1 class="text-lg">{{ $strings.LabelAddToPlaylist }}</h1>
<button class="flex" @click="show = false">
<span class="material-icons">close</span>
</button>
</div>
<!-- create new playlist form -->
<div v-if="showPlaylistNameInput" class="w-full h-full max-h-[calc(100vh-128px)] flex items-center">
<div v-if="showPlaylistNameInput" class="w-full h-full max-h-[calc(100vh-176px)] flex items-center">
<div class="w-full px-4">
<div class="flex mb-4 items-center">
<div class="w-9 h-9 flex items-center justify-center rounded-full hover:bg-white hover:bg-opacity-10 cursor-pointer" @click.stop="showPlaylistNameInput = false">
<div class="w-9 h-9 flex items-center justify-center rounded-full cursor-pointer" @click.stop="showPlaylistNameInput = false">
<span class="material-icons text-3xl">arrow_back</span>
</div>
<p class="text-xl pl-2">New Playlist</p>
<p class="text-xl pl-2 leading-none">{{ $strings.HeaderNewPlaylist }}</p>
<div class="flex-grow" />
</div>
<ui-text-input-with-label v-model="newPlaylistName" label="Name" />
<ui-text-input-with-label v-model="newPlaylistName" :label="$strings.LabelName" />
<div class="flex justify-end mt-6">
<ui-btn color="success" :loading="processing" class="w-full" @click.stop="submitCreatePlaylist">Create</ui-btn>
<ui-btn color="success" :loading="processing" class="w-full" @click.stop="submitCreatePlaylist">{{ $strings.ButtonCreate }}</ui-btn>
</div>
</div>
</div>
<!-- playlists list -->
<div v-if="!showPlaylistNameInput" class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-128px)]">
<div v-if="!showPlaylistNameInput" class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-176px)]">
<div class="w-full h-full" v-show="!showPlaylistNameInput">
<template v-for="playlist in sortedPlaylists">
<modals-playlists-playlist-row :key="playlist.id" :in-playlist="playlist.isItemIncluded" :playlist="playlist" @click="clickPlaylist" @close="show = false" />
</template>
<div v-if="!playlists.length" class="flex h-full items-center justify-center">
<p class="text-xl">{{ loading ? 'Loading..' : 'No Playlists' }}</p>
<p class="text-xl">{{ loading ? $strings.MessageLoading : $strings.MessageNoUserPlaylists }}</p>
</div>
</div>
</div>
<!-- create playlist btn -->
<div v-if="!showPlaylistNameInput" class="flex items-start justify-between h-16 pt-2 absolute bottom-0 left-0 w-full">
<ui-btn :loading="processing" color="success" class="w-full h-full flex items-center justify-center" @click.stop="createPlaylist">
<p class="text-base">Create New Playlist</p>
<div v-if="!showPlaylistNameInput" class="flex items-start justify-between h-20 pt-2 absolute bottom-0 left-0 w-full">
<ui-btn :loading="processing" color="success" class="w-full h-14 flex items-center justify-center" @click.stop="createPlaylist">
<p class="text-base">{{ $strings.ButtonCreateNewPlaylist }}</p>
</ui-btn>
</div>
</modals-fullscreen-modal>
@@ -193,8 +193,7 @@ export default {
})
.catch((error) => {
console.error('Failed to create playlist', error)
var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(`Failed to create playlist: ${errMsg}`)
this.$toast.error(this.$strings.ToastPlaylistCreateFailed)
})
.finally(() => {
this.processing = false
+2 -2
View File
@@ -8,8 +8,8 @@
<p class="px-2 truncate text-sm">{{ playlist.name }}</p>
</div>
<div class="w-24 min-w-[96px] px-1">
<ui-btn v-if="inPlaylist" small class="w-full" @click.stop="click">Remove</ui-btn>
<ui-btn v-else small class="w-full" @click.stop="click">Add</ui-btn>
<ui-btn v-if="inPlaylist" small class="w-full" @click.stop="click">{{ $strings.ButtonRemove }}</ui-btn>
<ui-btn v-else small class="w-full" @click.stop="click">{{ $strings.ButtonAdd }}</ui-btn>
</div>
</div>
</template>
@@ -1,22 +1,22 @@
<template>
<div class="w-full py-2 text-sm">
<div class="flex -mb-px">
<div class="w-1/2 h-6 rounded-tl-md relative border border-white/10 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg border-b-bg' : 'text-gray-400 bg-primary bg-opacity-70'" @click="showAdvancedView = false">
<div class="w-1/2 h-6 rounded-tl-md relative border border-fg/10 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-fg bg-bg border-b-bg' : 'text-fg-muted bg-primary bg-opacity-70'" @click="showAdvancedView = false">
<p class="text-sm">{{ $strings.HeaderRSSFeedGeneral }}</p>
</div>
<div class="w-1/2 h-6 rounded-tr-md relative border border-white/10 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg border-b-bg' : 'text-gray-400 bg-primary bg-opacity-70'" @click="showAdvancedView = true">
<div class="w-1/2 h-6 rounded-tr-md relative border border-fg/10 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-fg bg-bg border-b-bg' : 'text-fg-muted bg-primary bg-opacity-70'" @click="showAdvancedView = true">
<p class="text-sm">{{ $strings.HeaderAdvanced }}</p>
</div>
</div>
<div class="px-2 py-4 md:p-4 border border-white/10 rounded-b-md mr-px" style="min-height: 220px">
<div class="px-2 py-4 md:p-4 border border-fg/10 rounded-b-md mr-px" style="min-height: 220px">
<template v-if="!showAdvancedView">
<div class="flex-grow pt-2 mb-2">
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="border" label-class="pl-2" />
</div>
</template>
<template v-else>
<div class="flex-grow pt-2 mb-2">
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="border" label-class="pl-2" />
</div>
<div class="w-full relative mb-1">
<ui-text-input-with-label v-model="ownerName" :label="$strings.LabelRSSFeedCustomOwnerName" />
+12 -13
View File
@@ -1,38 +1,38 @@
<template>
<modals-fullscreen-modal v-model="show" :processing="processing">
<div class="flex items-end justify-end h-16 pr-4 pb-2 mb-2">
<div class="flex items-end justify-end h-24 pr-4 pb-2">
<!-- <h1 class="text-lg">RSS Feed</h1> -->
<button class="flex" @click="show = false">
<span class="material-icons">close</span>
</button>
</div>
<div class="w-full px-2 h-[calc(100%-138px)] overflow-y-auto">
<div class="w-full px-2 h-[calc(100%-176px)] overflow-y-auto">
<div v-if="currentFeed" class="w-full">
<div class="w-full relative">
<h1 class="text-lg mb-4">RSS feed is open</h1>
<h1 class="text-lg mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</h1>
<ui-text-input v-model="currentFeed.feedUrl" class="text-sm" readonly />
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base" :class="linkCopied ? 'text-success' : 'text-gray-300'" @click="copyToClipboard(currentFeed.feedUrl)">{{ linkCopied ? 'done' : 'content_copy' }}</span>
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base" :class="linkCopied ? 'text-success' : 'text-fg-muted'" @click="copyToClipboard(currentFeed.feedUrl)">{{ linkCopied ? 'done' : 'content_copy' }}</span>
</div>
<div v-if="currentFeed.meta" class="mt-5">
<div class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
<span class="text-fg-muted uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
</div>
<div>{{ currentFeed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
<div>{{ currentFeed.meta.preventIndexing ? $strings.ButtonYes : $strings.LabelNo }}</div>
</div>
<div v-if="currentFeed.meta.ownerName" class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
<span class="text-fg-muted uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
</div>
<div>{{ currentFeed.meta.ownerName }}</div>
</div>
<div v-if="currentFeed.meta.ownerEmail" class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
<span class="text-fg-muted uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
</div>
<div>{{ currentFeed.meta.ownerEmail }}</div>
</div>
@@ -41,7 +41,7 @@
<div v-else class="w-full">
<div class="w-full relative mb-2">
<ui-text-input-with-label v-model="newFeedSlug" :label="$strings.LabelRSSFeedSlug" />
<p class="text-xs text-gray-400 py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
<p class="text-xs text-fg-muted py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
</div>
<modals-rssfeeds-rss-feed-metadata-builder v-model="metadataDetails" />
@@ -50,10 +50,9 @@
</div>
</div>
<div v-show="userIsAdminOrUp" class="flex items-center px-2 h-16">
<div class="flex-grow" />
<ui-btn v-if="currentFeed" color="error" class="w-full" @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
<ui-btn v-else color="success" class="w-full" @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
<div v-show="userIsAdminOrUp" class="flex items-start pt-2 h-20">
<ui-btn v-if="currentFeed" color="error" class="w-full h-14" @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
<ui-btn v-else color="success" class="w-full h-14" @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
</div>
</modals-fullscreen-modal>
</template>
+3 -3
View File
@@ -2,7 +2,7 @@
<div id="comic-reader" class="w-full h-full relative">
<modals-modal v-model="showInfoMenu" height="90%">
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click.stop="showInfoMenu = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-bg rounded-lg border border-border text-fg" style="max-height: 75%" @click.stop>
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
<p class="text-xs">
<strong>{{ key }}</strong>
@@ -23,7 +23,7 @@
</div>
</div>
<div class="fixed left-0 h-8 w-full bg-primary px-4 flex items-center text-white/80" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="fixed left-0 h-8 w-full bg-bg px-4 flex items-center text-fg-muted" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="flex-grow" />
<p class="text-xs">{{ page }} / {{ numPages }}</p>
</div>
@@ -349,7 +349,7 @@ export default {
}
},
parseFilenames(filenames) {
const acceptableImages = ['.jpeg', '.jpg', '.png']
const acceptableImages = ['.jpeg', '.jpg', '.png', '.webp']
var imageFiles = filenames.filter((f) => {
return acceptableImages.includes((Path.extname(f) || '').toLowerCase())
})
+52 -14
View File
@@ -2,7 +2,7 @@
<div id="epub-frame" class="w-full">
<div id="viewer" class="h-full w-full"></div>
<div class="fixed left-0 h-8 w-full px-4 flex items-center" :class="isLightTheme ? 'bg-white text-black' : 'bg-primary text-white/80'" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="fixed left-0 h-8 w-full px-4 flex items-center" :class="isLightTheme ? 'bg-white text-black' : 'bg-[#232323] text-white/80'" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<p v-if="totalLocations" class="text-xs text-slate-600">Location {{ currentLocationNum }} of {{ totalLocations }}</p>
<div class="flex-grow" />
<p class="text-xs">{{ progress }}%</p>
@@ -34,16 +34,18 @@ export default {
currentLocationNum: 0,
currentLocationCfi: null,
inittingDisplay: true,
isRefreshingUI: false,
ereaderSettings: {
theme: 'dark',
fontScale: 100,
lineSpacing: 115
lineSpacing: 115,
textStroke: 0
}
}
},
watch: {
isPlayerOpen() {
this.updateHeight()
this.refreshUI()
}
},
computed: {
@@ -112,7 +114,8 @@ export default {
'*': {
color: `${fontColor}!important`,
'background-color': `${backgroundColor}!important`,
'line-height': this.ereaderSettings.lineSpacing + '%!important'
'line-height': this.ereaderSettings.lineSpacing + '%!important',
'-webkit-text-stroke': this.ereaderSettings.textStroke/100 + 'px ' + fontColor + '!important'
},
a: {
color: `${fontColor}!important`
@@ -135,11 +138,6 @@ export default {
goToChapter(href) {
return this.rendition?.display(href)
},
updateHeight() {
if (this.rendition && this.rendition.resize) {
this.rendition.resize(window.innerWidth, window.innerHeight - this.readerHeightOffset)
}
},
prev() {
if (this.rendition) {
this.rendition.prev()
@@ -382,14 +380,54 @@ export default {
this.rendition.getContents().forEach((c) => {
c.addStylesheetRules(this.themeRules)
})
},
async screenOrientationChange() {
if (this.isRefreshingUI) return
this.isRefreshingUI = true
const windowWidth = window.innerWidth
this.refreshUI()
// Window width does not always change right away. Wait up to 250ms for a change.
// iPhone 10 on iOS 16 took between 100 - 200ms to update when going from portrait to landscape
// but landscape to portrait was immediate
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 50))
if (window.innerWidth !== windowWidth) {
this.refreshUI()
break
}
}
this.isRefreshingUI = false
},
refreshUI() {
if (this.rendition?.resize) {
this.rendition.resize(window.innerWidth, window.innerHeight - this.readerHeightOffset)
}
}
},
beforeDestroy() {
this.book?.destroy()
},
mounted() {
this.initEpub()
}
if (screen.orientation) {
// Not available on ios
screen.orientation.addEventListener('change', this.screenOrientationChange)
} else {
document.addEventListener('orientationchange', this.screenOrientationChange)
}
window.addEventListener('resize', this.screenOrientationChange)
},
beforeDestroy() {
this.book?.destroy()
if (screen.orientation) {
// Not available on ios
screen.orientation.removeEventListener('change', this.screenOrientationChange)
} else {
document.removeEventListener('orientationchange', this.screenOrientationChange)
}
window.removeEventListener('resize', this.screenOrientationChange)
},
}
</script>
@@ -404,4 +442,4 @@ export default {
max-height: calc(100% - 132px);
overflow: hidden;
}
</style>
</style>
+17 -12
View File
@@ -14,11 +14,11 @@
<div class="h-full flex items-center justify-center">
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="w-full h-full overflow-auto">
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md bg-white" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
<pdf v-if="pdfDocInitParams" ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md bg-white" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
</div>
</div>
<div class="fixed left-0 h-8 w-full bg-primary px-4 flex items-center text-white/80" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="fixed left-0 h-8 w-full bg-bg px-4 flex items-center text-fg-muted" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="flex-grow" />
<p class="text-xs">{{ page }} / {{ numPages }}</p>
</div>
@@ -48,7 +48,8 @@ export default {
page: 1,
numPages: 0,
windowWidth: 0,
windowHeight: 0
windowHeight: 0,
pdfDocInitParams: null
}
},
computed: {
@@ -106,14 +107,6 @@ export default {
if (!this.userItemProgress?.ebookLocation || isNaN(this.userItemProgress.ebookLocation)) return 0
return Number(this.userItemProgress.ebookLocation)
},
pdfDocInitParams() {
return {
url: this.url,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
},
isPlayerOpen() {
return this.$store.getters['getIsPlayerOpen']
}
@@ -157,6 +150,8 @@ export default {
}
},
numPagesLoaded(e) {
if (!e) return
this.numPages = e
},
prev() {
@@ -175,6 +170,14 @@ export default {
screenOrientationChange() {
this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight
},
init() {
this.pdfDocInitParams = {
url: this.url,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
}
},
mounted() {
@@ -188,6 +191,8 @@ export default {
document.addEventListener('orientationchange', this.screenOrientationChange)
}
window.addEventListener('resize', this.screenOrientationChange)
this.init()
},
beforeDestroy() {
if (screen.orientation) {
@@ -199,4 +204,4 @@ export default {
window.removeEventListener('resize', this.screenOrientationChange)
}
}
</script>
</script>
+69 -36
View File
@@ -1,20 +1,20 @@
<template>
<div v-if="show" :data-theme="ereaderTheme" class="group fixed top-0 left-0 right-0 layout-wrapper w-full z-40 pt-8 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': isPlayerOpen }">
<div v-if="show" :data-theme="ereaderTheme" class="group fixed top-0 left-0 right-0 layout-wrapper w-full z-40 pt-8 data-[theme=dark]:bg-[#232323] data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': isPlayerOpen }">
<!-- toolbar -->
<div class="h-32 pt-10 w-full px-2 fixed top-0 left-0 z-30 transition-transform bg-bg text-white" :class="showingToolbar ? 'translate-y-0' : '-translate-y-32'" @touchstart.stop @mousedown.stop @touchend.stop @mouseup.stop>
<div class="h-32 pt-10 w-full px-2 fixed top-0 left-0 z-30 transition-transform bg-bg text-fg" :class="showingToolbar ? 'translate-y-0' : '-translate-y-32'" :style="{ boxShadow: showingToolbar ? '0px 8px 8px #11111155' : '' }" @touchstart.stop @mousedown.stop @touchend.stop @mouseup.stop>
<div class="flex items-center mb-2">
<button type="button" class="inline-flex mx-2" @click.stop="show = false">
<span class="material-icons-outlined text-3xl text-white">chevron_left</span>
<span class="material-icons-outlined text-3xl text-fg">chevron_left</span>
</button>
<div class="flex-grow" />
<button v-if="isComic || isEpub" type="button" class="inline-flex mx-2" @click.stop="clickTOCBtn">
<span class="material-icons-outlined text-2xl text-white">format_list_bulleted</span>
<span class="material-icons-outlined text-2xl text-fg">format_list_bulleted</span>
</button>
<button v-if="isEpub" type="button" class="inline-flex mx-2" @click.stop="clickSettingsBtn">
<span class="material-icons text-2xl text-white">settings</span>
<span class="material-icons text-2xl text-fg">settings</span>
</button>
<button v-if="comicHasMetadata" type="button" class="inline-flex mx-2" @click.stop="clickMetadataBtn">
<span class="material-icons text-2xl text-white">more</span>
<span class="material-icons text-2xl text-fg">more</span>
</button>
</div>
@@ -27,7 +27,7 @@
<!-- table of contents modal -->
<modals-fullscreen-modal v-model="showTOCModal" :theme="ereaderTheme">
<div class="flex items-end justify-between h-20 px-4 pb-2">
<h1 class="text-lg">Table of Contents</h1>
<h1 class="text-lg">{{ $strings.HeaderTableOfContents }}</h1>
<button class="flex" @click.stop="showTOCModal = false">
<span class="material-icons">close</span>
</button>
@@ -47,7 +47,7 @@
</li>
</ul>
<div v-if="!chapters.length" class="flex h-full items-center justify-center">
<p class="text-xl">{{ 'No Chapters' }}</p>
<p class="text-xl">{{ $strings.MessageNoChapters }}</p>
</div>
</div>
</div>
@@ -55,31 +55,45 @@
<!-- ereader settings modal -->
<modals-fullscreen-modal v-model="showSettingsModal" :theme="ereaderTheme" half-screen>
<div class="flex items-end justify-between h-20 px-4 pb-2 mb-8">
<h1 class="text-lg">Ereader Settings</h1>
<button class="flex" @click="showSettingsModal = false">
<span class="material-icons">close</span>
</button>
</div>
<div class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-85px)]">
<div class="w-full h-full px-4">
<div class="flex items-center mb-8">
<div class="w-32">
<p class="text-base">Theme:</p>
<div style="box-shadow: 0px -8px 8px #11111155">
<div class="flex items-end justify-between h-20 px-4 pb-2 mb-6">
<h1 class="text-lg">{{ $strings.HeaderEreaderSettings }}</h1>
<button class="flex" @click="showSettingsModal = false">
<span class="material-icons">close</span>
</button>
</div>
<div class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-85px)]">
<div class="w-full h-full px-4">
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-base">{{ $strings.LabelTheme }}:</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
</div>
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-8">
<div class="w-32">
<p class="text-base">Font scale:</p>
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-base">{{ $strings.LabelFontScale }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-8">
<div class="w-32">
<p class="text-base">Line spacing:</p>
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-base">{{ $strings.LabelLineSpacing }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-base">{{ $strings.LabelFontBoldness }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.textStroke" :min="0" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<div class="flex items-center">
<div class="w-32">
<p class="text-base">{{ $strings.LabelLayout }}:</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.spread" :items="spreadItems" @input="settingsUpdated" />
</div>
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
</div>
</div>
@@ -107,7 +121,9 @@ export default {
ereaderSettings: {
theme: 'dark',
fontScale: 100,
lineSpacing: 115
lineSpacing: 115,
spread: 'auto',
textStroke: 0
}
}
},
@@ -150,16 +166,28 @@ export default {
},
ereaderTheme() {
if (this.isEpub) return this.ereaderSettings.theme
return 'dark'
return document.documentElement.dataset.theme || 'dark'
},
spreadItems() {
return [
{
text: this.$strings.LabelLayoutSinglePage,
value: 'none'
},
{
text: this.$strings.LabelLayoutAuto,
value: 'auto'
}
]
},
themeItems() {
return [
{
text: 'Dark',
text: this.$strings.LabelThemeDark,
value: 'dark'
},
{
text: 'Light',
text: this.$strings.LabelThemeLight,
value: 'light'
}
]
@@ -348,7 +376,12 @@ export default {
try {
const settings = localStorage.getItem('ereaderSettings')
if (settings) {
this.ereaderSettings = JSON.parse(settings)
const _ereaderSettings = JSON.parse(settings)
for (const key in this.ereaderSettings) {
if (_ereaderSettings[key] !== undefined) {
this.ereaderSettings[key] = _ereaderSettings[key]
}
}
this.settingsUpdated()
}
} catch (error) {
@@ -370,4 +403,4 @@ export default {
this.unregisterListeners()
}
}
</script>
</script>
+9 -9
View File
@@ -1,6 +1,6 @@
<template>
<div class="w-96 my-6 mx-auto">
<h1 class="text-2xl mb-4">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsMinutesListeningChart }}</h1>
<div class="relative w-96 h-72">
<div class="absolute top-0 left-0">
<template v-for="lbl in yAxisLabels">
@@ -32,24 +32,24 @@
</div>
<div class="flex justify-between pt-12">
<div>
<p class="text-sm text-center">Week Listening</p>
<p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
<p class="text-sm text-center">minutes</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">Daily Average</p>
<p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
<p class="text-sm text-center">minutes</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">Best Day</p>
<p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
<p class="text-sm text-center">minutes</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">Days</p>
<p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
<p class="text-sm text-center">in a row</p>
<p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
</div>
</div>
</div>
+283
View File
@@ -0,0 +1,283 @@
<template>
<div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
</div>
</template>
<script>
import { FileSharer } from '@byteowls/capacitor-filesharer'
export default {
props: {
variant: {
type: Number,
default: 0
},
year: Number,
processing: Boolean
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
watch: {
variant() {
this.init()
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined`
ctx.fillText(icon, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
const bookCovers = this.yearStats.finishedBooksWithCovers
bookCovers.push(...this.yearStats.booksWithCovers)
let finishedBookCoverImgs = {}
if (bookCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-130, -120)
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
const coverIndex = index % bookCovers.length
let libraryItemId = bookCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
resolve()
if (this.yearStats.finishedBooksWithCovers.includes(libraryItemId) && !finishedBookCoverImgs[libraryItemId]) {
finishedBookCoverImgs[libraryItemId] = {
img,
sx,
sy,
sw
}
}
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(50, 100, 340, 160)
addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165)
addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210)
const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 })
ctx.fillStyle = '#ffffff'
ctx.fill(readIconPath)
// Box top right
createRoundedRect(410, 100, 340, 160)
addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165)
addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205)
addIcon('watch_later', 'white', '52px', 440, 180)
// Box bottom left
createRoundedRect(50, 280, 340, 160)
addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345)
addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390)
addIcon('headphones', 'white', '52px', 95, 360)
// Box bottom right
createRoundedRect(410, 280, 340, 160)
addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345)
addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390)
addIcon('local_library', 'white', '52px', 440, 360)
if (!this.variant) {
// Text stats
const topNarrator = this.yearStats.mostListenedNarrator
if (topNarrator) {
addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520)
addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330)
addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
}
const topGenre = this.yearStats.topGenres[0]
if (topGenre) {
addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520)
addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330)
addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
}
const topAuthor = this.yearStats.topAuthors[0]
if (topAuthor) {
addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670)
addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330)
addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
}
if (this.yearStats.mostListenedMonth?.time) {
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
const monthName = this.$formatJsDate(jsdate, 'LLLL')
addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670)
addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330)
addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
}
} else if (this.variant === 1) {
// Bottom images
finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
if (finishedBookCoverImgs.length > 0) {
ctx.textAlign = 'center'
addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) {
let imgToAdd = finishedBookCoverImgs[i]
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 570, 140, 140)
}
}
} else if (this.variant === 2) {
// Text stats
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
}
}
if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524)
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
}
}
}
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
refresh() {
this.init()
},
share() {
const base64Data = this.dataUrl.split(';base64,').pop()
FileSharer.share({
filename: `audiobookshelf_my_${this.year}.png`,
contentType: 'image/png',
base64Data
}).catch((error) => {
if (error.message !== 'USER_CANCELLED') {
console.error('Failed to share', error.message)
this.$toast.error('Failed to share: ' + error.message)
}
})
},
init() {
this.$emit('update:processing', true)
this.$nativeHttp
.get(`/api/me/stats/year/${this.year}`)
.then((data) => {
this.yearStats = data || []
return this.initCanvas()
})
.catch((error) => {
console.error('Failed', error)
this.$toast.error('Failed to load year stats')
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
mounted() {
this.init()
}
}
</script>
+134
View File
@@ -0,0 +1,134 @@
<template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-2 sm:p-4 mb-4">
<!-- hack to get icon fonts loaded on init -->
<div class="h-0 w-0 overflow-hidden opacity-0">
<span class="material-icons-outlined">close</span>
<span class="abs-icons icon-audiobookshelf" />
</div>
<div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p>
<div class="hidden md:block flex-grow" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Hide Year in Review' : `See ${yearInReviewYear} Year in Review` }}</ui-btn>
</div>
<!-- your year in review -->
<div v-if="showYearInReview">
<div class="w-full h-px bg-slate-200/10 my-4" />
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
<!-- previous button -->
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">Previous</span>
</ui-btn>
<!-- share button -->
<ui-btn small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview"> Share </ui-btn>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Your Year in Review ({{ yearInReviewVariant + 1 }})</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
<div class="flex-grow" />
<!-- refresh button -->
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
<span class="hidden sm:inline-block">Refresh</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- next button -->
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
<span class="hidden sm:inline-block pl-2">Next</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
<!-- your year in review short -->
<div class="w-full max-w-[800px] mx-auto my-4">
<!-- share button -->
<ui-btn small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort"> Share </ui-btn>
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
</div>
<!-- your server in review -->
<div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
<div class="flex items-center justify-center mb-2">
<!-- previous button -->
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">Previous</span>
</ui-btn>
<!-- share button -->
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer"> Share </ui-btn>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Server Year in Review ({{ yearInReviewServerVariant + 1 }})</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
<div class="flex-grow" />
<!-- refresh button -->
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
<span class="hidden sm:inline-block">Refresh</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- next button -->
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
<span class="hidden sm:inline-block pl-2">Next</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>
</div>
<stats-year-in-review-server v-if="isAdminOrUp" ref="yearInReviewServer" :year="yearInReviewYear" :variant="yearInReviewServerVariant" :processing.sync="processingYearInReviewServer" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
showYearInReview: false,
yearInReviewYear: 0,
yearInReviewVariant: 0,
yearInReviewServerVariant: 0,
processingYearInReview: false,
processingYearInReviewShort: false,
processingYearInReviewServer: false
}
},
computed: {
isAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
}
},
methods: {
shareYearInReviewServer() {
this.$refs.yearInReviewServer.share()
},
shareYearInReview() {
this.$refs.yearInReview.share()
},
shareYearInReviewShort() {
this.$refs.yearInReviewShort.share()
},
refreshYearInReviewServer() {
this.$refs.yearInReviewServer.refresh()
},
refreshYearInReview() {
this.$refs.yearInReview.refresh()
this.$refs.yearInReviewShort.refresh()
},
clickShowYearInReview() {
this.showYearInReview = !this.showYearInReview
}
},
beforeMount() {
this.yearInReviewYear = new Date().getFullYear()
// When not December show previous year
if (new Date().getMonth() < 11) {
this.yearInReviewYear--
}
},
mounted() {}
}
</script>
+262
View File
@@ -0,0 +1,262 @@
<template>
<div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
</div>
</template>
<script>
import { FileSharer } from '@byteowls/capacitor-filesharer'
export default {
props: {
variant: {
type: Number,
default: 0
},
processing: Boolean,
year: Number
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
watch: {
variant() {
this.init()
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
let imgsToAdd = {}
if (this.yearStats.booksAddedWithCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-130, -120)
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
const coverIndex = index % this.yearStats.booksAddedWithCovers.length
let libraryItemId = this.yearStats.booksAddedWithCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
if (!imgsToAdd[libraryItemId]) {
imgsToAdd[libraryItemId] = {
img,
sx,
sy,
sw
}
}
resolve()
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(40, 100, 230, 100)
ctx.textAlign = 'center'
addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140)
addText('books added', '18px', 'normal', tanColor, '0px', 155, 170)
// Box top right
createRoundedRect(285, 100, 230, 100)
addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140)
addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170)
// Box bottom left
createRoundedRect(530, 100, 230, 100)
addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140)
addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170)
// Text stats
if (this.yearStats.totalBooksAddedSize) {
addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300)
addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
}
if (this.yearStats.totalBooksAddedDuration) {
addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440)
addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
}
if (!this.variant) {
// Bottom images
imgsToAdd = Object.values(imgsToAdd)
if (imgsToAdd.length > 0) {
addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) {
let imgToAdd = imgsToAdd[i]
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 580, 140, 140)
}
}
} else if (this.variant === 1) {
// Text stats
ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
if (this.yearStats.topNarrators.length) {
addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topNarrators.length; i++) {
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
}
} else if (this.variant === 2) {
// Text stats
ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
}
}
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
share() {
const base64Data = this.dataUrl.split(';base64,').pop()
FileSharer.share({
filename: `audiobookshelf_server_${this.year}.png`,
contentType: 'image/png',
base64Data
}).catch((error) => {
if (error.message !== 'USER_CANCELLED') {
console.error('Failed to share', error.message)
this.$toast.error('Failed to share: ' + error.message)
}
})
},
refresh() {
this.init()
},
init() {
this.$emit('update:processing', true)
this.$nativeHttp
.get(`/api/stats/year/${this.year}`)
.then((data) => {
this.yearStats = data || []
return this.initCanvas()
})
.catch((error) => {
console.error('Failed', error)
this.$toast.error('Failed to load year stats')
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
mounted() {
this.init()
}
}
</script>
+192
View File
@@ -0,0 +1,192 @@
<template>
<div>
<div v-if="processing" class="max-w-[600px] h-32 sm:h-[200px] flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" />
</div>
</template>
<script>
import { FileSharer } from '@byteowls/capacitor-filesharer'
export default {
props: {
processing: Boolean,
year: Number
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 600
canvas.height = 200
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined`
ctx.fillText(icon, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
const bookCovers = this.yearStats.finishedBooksWithCovers
bookCovers.push(...this.yearStats.booksWithCovers)
if (bookCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-10, -90)
for (let x = 0; x < 4; x++) {
for (let y = 0; y < 3; y++) {
const coverIndex = index % bookCovers.length
let libraryItemId = bookCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 155 * x, 155 * y, 155, 155)
resolve()
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(15, 75, 280, 110)
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155)
const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 })
ctx.fillStyle = '#ffffff'
ctx.fill(readIconPath)
createRoundedRect(305, 75, 280, 110)
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155)
addIcon('local_library', 'white', '42px', 345, 130)
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
share() {
const base64Data = this.dataUrl.split(';base64,').pop()
FileSharer.share({
filename: `audiobookshelf_my_${this.year}_short.png`,
contentType: 'image/png',
base64Data
}).catch((error) => {
if (error.message !== 'USER_CANCELLED') {
console.error('Failed to share', error.message)
this.$toast.error('Failed to share: ' + error.message)
}
})
},
refresh() {
this.init()
},
init() {
this.$emit('update:processing', true)
this.$nativeHttp
.get(`/api/me/stats/year/${this.year}`)
.then((data) => {
this.yearStats = data || []
return this.initCanvas()
})
.catch((error) => {
console.error('Failed', error)
this.$toast.error('Failed to load year stats')
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
mounted() {
this.init()
}
}
</script>
+4 -4
View File
@@ -1,8 +1,8 @@
<template>
<div class="w-full my-4">
<div class="w-full bg-primary px-4 py-2 flex items-center" :class="expanded ? 'rounded-t-md' : 'rounded-md'" @click.stop="clickBar">
<p class="pr-2">Chapters</p>
<div class="h-6 w-6 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<p class="pr-2">{{ $strings.HeaderChapters }}</p>
<div class="h-6 w-6 rounded-full bg-fg/10 flex items-center justify-center">
<span class="text-xs font-mono">{{ chapters.length }}</span>
</div>
<div class="flex-grow" />
@@ -13,8 +13,8 @@
<transition name="slide">
<table class="text-xs tracksTable" v-show="expanded">
<tr>
<th class="text-left">Title</th>
<th class="text-center w-16">Start</th>
<th class="text-left">{{ $strings.LabelTitle }}</th>
<th class="text-center w-16">{{ $strings.LabelStart }}</th>
</tr>
<tr v-for="chapter in chapters" :key="chapter.id">
<td>
+4 -8
View File
@@ -1,8 +1,8 @@
<template>
<div class="w-full my-4">
<div class="w-full bg-primary px-4 py-2 flex items-center" :class="showTracks ? 'rounded-t-md' : 'rounded-md'" @click.stop="clickBar">
<p class="pr-2">{{ title }}</p>
<div class="h-6 w-6 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<p class="pr-2">{{ $strings.HeaderAudioTracks }}</p>
<div class="h-6 w-6 rounded-full bg-fg/10 flex items-center justify-center">
<span class="text-xs font-mono">{{ tracks.length }}</span>
</div>
<div class="flex-grow" />
@@ -14,8 +14,8 @@
<div class="w-full" v-show="showTracks">
<table class="text-xs tracksTable">
<tr>
<th class="text-left">Filename</th>
<th class="text-center w-16">Duration</th>
<th class="text-left">{{ $strings.LabelFilename }}</th>
<th class="text-center w-16">{{ $strings.LabelDuration }}</th>
</tr>
<template v-for="track in tracks">
<tr :key="track.index">
@@ -34,10 +34,6 @@
<script>
export default {
props: {
title: {
type: String,
default: 'Audio Tracks'
},
tracks: {
type: Array,
default: () => []
@@ -7,8 +7,8 @@
<div class="book-table-content h-full px-2 flex items-center">
<div class="max-w-full">
<p class="truncate block text-sm">{{ bookTitle }}</p>
<p class="truncate block text-gray-400 text-xs">{{ bookAuthor }}</p>
<p v-if="media.duration" class="text-xxs text-gray-500">{{ bookDuration }}</p>
<p class="truncate block text-fg-muted text-xs">{{ bookAuthor }}</p>
<p v-if="media.duration" class="text-xxs text-fg-muted">{{ bookDuration }}</p>
</div>
</div>
<div class="w-8 min-w-8 flex justify-center">
@@ -1,14 +1,14 @@
<template>
<div class="w-full bg-primary bg-opacity-40">
<div class="w-full h-14 flex items-center px-4 bg-primary">
<p class="pr-4">Collection List</p>
<p class="pr-4">{{ $strings.HeaderCollectionItems }}</p>
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
<div class="w-6 h-6 md:w-7 md:h-7 bg-fg/10 rounded-full flex items-center justify-center">
<span class="text-xs md:text-sm font-mono leading-none">{{ books.length }}</span>
</div>
<div class="flex-grow" />
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
<p v-if="totalDuration" class="text-sm text-fg">{{ totalDurationPretty }}</p>
</div>
<template v-for="book in booksCopy">
<tables-collection-book-table-row :key="book.id" :book="book" :collection-id="collectionId" class="item collection-book-item" @edit="editBook" />
+4 -4
View File
@@ -1,8 +1,8 @@
<template>
<div class="w-full my-2">
<div class="w-full bg-primary px-4 py-2 flex items-center" :class="showFiles ? 'rounded-t-md' : 'rounded-md'" @click.stop="clickBar">
<p class="pr-2">Ebook Files</p>
<div class="h-6 w-6 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<p class="pr-2">{{ $strings.HeaderEbookFiles }}</p>
<div class="h-6 w-6 rounded-full bg-fg/10 flex items-center justify-center">
<span class="text-xs font-mono">{{ ebookFiles.length }}</span>
</div>
<div class="flex-grow" />
@@ -14,8 +14,8 @@
<div class="w-full" v-show="showFiles">
<table class="text-sm tracksTable">
<tr>
<th class="text-left px-4">Filename</th>
<th class="text-left px-4 w-16">Read</th>
<th class="text-left px-4">{{ $strings.LabelFilename }}</th>
<th class="text-left px-4 w-16">{{ $strings.LabelRead }}</th>
<th v-if="userCanUpdate && !libraryIsAudiobooksOnly" class="text-center w-16"></th>
</tr>
<template v-for="file in ebookFiles">
@@ -40,7 +40,7 @@ export default {
const items = []
if (this.userCanUpdate && !this.libraryIsAudiobooksOnly) {
items.push({
text: this.isPrimary ? 'Set as supplementary' : 'Set as primary',
text: this.isPrimary ? this.$strings.LabelSetEbookAsSupplementary : this.$strings.LabelSetEbookAsPrimary,
value: 'updateStatus'
})
}
+23 -4
View File
@@ -8,13 +8,16 @@
<div class="item-table-content h-full px-2 flex items-center">
<div class="max-w-full">
<p class="truncate block text-sm">{{ itemTitle }} <span v-if="localLibraryItem" class="material-icons text-success text-base align-text-bottom">download_done</span></p>
<p v-if="authorName" class="truncate block text-gray-300 text-xs">{{ authorName }}</p>
<p class="text-xxs text-gray-400">{{ itemDuration }}</p>
<p v-if="authorName" class="truncate block text-fg-muted text-xs">{{ authorName }}</p>
<p class="text-xxs text-fg-muted">{{ itemDuration }}</p>
</div>
</div>
<div class="w-8 min-w-8 flex justify-center">
<button v-if="showPlayBtn" class="w-8 h-8 rounded-full border border-white border-opacity-20 flex items-center justify-center" @click.stop.prevent="playClick">
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<button v-if="showPlayBtn" class="w-8 h-8 rounded-full border border-white/20 flex items-center justify-center" @click.stop.prevent="playClick">
<span v-if="!playerIsStartingForThisMedia" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<svg v-else class="animate-spin" style="width: 18px; height: 18px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</button>
</div>
<div class="w-8 min-w-8 flex justify-center">
@@ -117,6 +120,17 @@ export default {
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
const mediaId = this.$store.state.playerStartingPlaybackMediaId
if (!mediaId) return false
let thisMediaId = this.episodeId || this.libraryItem.id
return mediaId === thisMediaId
},
userItemProgress() {
return this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, this.episodeId)
},
@@ -142,10 +156,14 @@ export default {
this.$emit('showMore', playlistItem)
},
async playClick() {
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
let mediaId = this.episodeId || this.libraryItem.id
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else if (this.localLibraryItem) {
this.$store.commit('setPlayerIsStartingPlayback', mediaId)
this.$eventBus.$emit('play-item', {
libraryItemId: this.localLibraryItem.id,
episodeId: this.localEpisode?.id,
@@ -153,6 +171,7 @@ export default {
serverEpisodeId: this.episodeId
})
} else {
this.$store.commit('setPlayerIsStartingPlayback', mediaId)
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItem.id,
episodeId: this.episodeId
@@ -1,14 +1,14 @@
<template>
<div class="w-full bg-primary/50 rounded-lg">
<div class="w-full h-14 flex items-center px-3">
<p class="pr-2">Playlist Items</p>
<p class="pr-2">{{ $strings.HeaderPlaylistItems }}</p>
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
<div class="w-6 h-6 md:w-7 md:h-7 bg-fg bg-opacity-10 rounded-full flex items-center justify-center">
<span class="text-xs md:text-sm font-mono leading-none">{{ items.length }}</span>
</div>
<div class="flex-grow" />
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
<p v-if="totalDuration" class="text-sm text-fg">{{ totalDurationPretty }}</p>
</div>
<template v-for="item in items">
<tables-playlist-item-table-row :key="item.id" :item="item" :playlist-id="playlistId" @showMore="showMore" />
+42 -25
View File
@@ -9,27 +9,26 @@
<p v-else>No Server Media Progress</p>
<p v-if="localMediaProgress">Local Media Progress {{ Math.round(localMediaProgress.progress * 100) }}</p>
<p v-else>No Local Media Progress</p>
</template> -->
</template>-->
<p v-if="publishedAt" class="text-xs text-gray-400 mb-1">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
<p v-if="publishedAt" class="text-xs text-fg-muted mb-1">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
<p class="text-sm font-semibold">
{{ title }}
</p>
<p class="text-sm font-semibold">{{ title }}</p>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<p class="text-sm text-fg episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<div v-if="episodeNumber || season || episodeType" class="flex py-2 items-center -mx-0.5">
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200 capitalize">
{{ episodeType }}
</div>
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg capitalize">{{ episodeType }}</div>
</div>
<div class="flex items-center pt-2">
<div class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<div class="h-8 px-4 border border-border rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<span v-if="!playerIsStartingForThisMedia" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<svg v-else class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
</div>
@@ -41,7 +40,7 @@
<div v-if="userCanDownload">
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
<span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
<span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="(downloadItem || startingDownload) ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ (downloadItem || startingDownload) ? 'downloading' : 'download' }}</span>
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
</div>
@@ -58,8 +57,8 @@
</template>
<script>
import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
import cellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
export default {
props: {
@@ -75,10 +74,12 @@ export default {
},
isLocal: Boolean
},
mixins: [cellularPermissionHelpers],
data() {
return {
isProcessingReadUpdate: false,
processing: false
processing: false,
startingDownload: false
}
},
computed: {
@@ -119,6 +120,16 @@ export default {
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
const mediaId = this.$store.state.playerStartingPlaybackMediaId
if (!mediaId) return false
return mediaId === this.episode?.id
},
itemProgress() {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id)
@@ -169,7 +180,16 @@ export default {
return folderObj
},
async downloadClick() {
if (this.downloadItem) return
if (this.downloadItem || this.startingDownload) return
const hasPermission = await this.checkCellularPermission('download')
if (!hasPermission) return
this.startingDownload = true
setTimeout(() => {
this.startingDownload = false
}, 1000)
await this.$hapticsImpact()
if (this.isIos) {
// no local folders on iOS
@@ -210,14 +230,7 @@ export default {
console.log('Local folder', JSON.stringify(localFolder))
var startDownloadMessage = `Start download for "${this.title}" to folder ${localFolder.name}?`
const { value } = await Dialog.confirm({
title: 'Confirm',
message: startDownloadMessage
})
if (value) {
this.startDownload(localFolder)
}
this.startDownload(localFolder)
},
async startDownload(localFolder) {
var payload = {
@@ -235,10 +248,14 @@ export default {
}
},
async playClick() {
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.$store.commit('setPlayerIsStartingPlayback', this.episode.id)
if (this.localEpisode && this.localLibraryItemId) {
console.log('Play local episode', this.localEpisode.id, this.localLibraryItemId)
+54 -50
View File
@@ -1,40 +1,40 @@
<template>
<div class="w-full">
<!-- Podcast episode downloads queue -->
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 my-2 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-gray-100 relative w-full">
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 my-2 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-fg relative w-full">
<div class="flex items-center">
<p class="text-sm py-1">{{ episodeDownloadsQueued.length }} Episode(s) queued for download</p>
<p class="text-sm py-1">{{ $getString('MessageEpisodesQueuedForDownload', [episodeDownloadsQueued.length]) }}</p>
<div class="flex-grow" />
<span v-if="isAdminOrUp" class="material-icons text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span>
</div>
</div>
<!-- Podcast episodes currently downloading -->
<div v-if="episodesDownloading.length" class="px-4 py-2 my-2 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-gray-100 relative w-full">
<div v-if="episodesDownloading.length" class="px-4 py-2 my-2 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-fg relative w-full">
<div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center">
<widgets-loading-spinner />
<p class="text-sm py-1 pl-4">Downloading episode "{{ episode.episodeDisplayTitle }}"</p>
<p class="text-sm py-1 pl-4">{{ $strings.MessageDownloadingEpisode }} "{{ episode.episodeDisplayTitle }}"</p>
</div>
</div>
<div class="flex items-center">
<p class="text-lg mb-1 font-semibold">Episodes ({{ episodesFiltered.length }})</p>
<p class="text-lg mb-1 font-semibold">{{ $strings.HeaderEpisodes }} ({{ episodesFiltered.length }})</p>
<div class="flex-grow" />
<button v-if="isAdminOrUp && !fetchingRSSFeed" class="outline:none mx-1 pt-0.5 relative" @click="searchEpisodes">
<span class="material-icons text-xl text-gray-200">search</span>
<span class="material-icons text-xl text-fg">search</span>
</button>
<widgets-loading-spinner v-else-if="fetchingRSSFeed" class="mx-1" />
<button class="outline:none mx-3 pt-0.5 relative" @click="showFilters">
<span class="material-icons text-xl text-gray-200">filter_alt</span>
<span class="material-icons text-xl text-fg">filter_alt</span>
<div v-show="filterKey !== 'all' && episodesAreFiltered" class="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-success border border-green-300 shadow-sm z-10 pointer-events-none" />
</button>
<div class="flex items-center border border-white border-opacity-25 rounded px-2" @click="clickSort">
<p class="text-sm text-gray-200">{{ sortText }}</p>
<span class="material-icons ml-1 text-gray-200">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
<p class="text-sm text-fg">{{ sortText }}</p>
<span class="material-icons ml-1 text-fg">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</div>
@@ -82,46 +82,6 @@ export default {
sortKey: 'publishedAt',
sortDesc: true,
filterKey: 'incomplete',
episodeSortItems: [
{
text: 'Pub Date',
value: 'publishedAt'
},
{
text: 'Title',
value: 'title'
},
{
text: 'Season',
value: 'season'
},
{
text: 'Episode',
value: 'episode'
}
],
filterItems: [
{
text: 'Show All',
value: 'all'
},
{
text: 'Incomplete',
value: 'incomplete'
},
{
text: 'In Progress',
value: 'inProgress'
},
{
text: 'Complete',
value: 'complete'
},
{
text: 'Downloaded',
value: 'downloaded'
}
],
fetchingRSSFeed: false,
podcastFeedEpisodes: [],
showPodcastEpisodeFeed: false,
@@ -156,6 +116,50 @@ export default {
episodesAreFiltered() {
return this.episodesFiltered.length !== this.episodesCopy.length
},
episodeSortItems() {
return [
{
text: this.$strings.LabelPubDate,
value: 'publishedAt'
},
{
text: this.$strings.LabelTitle,
value: 'title'
},
{
text: this.$strings.LabelSeason,
value: 'season'
},
{
text: this.$strings.LabelEpisode,
value: 'episode'
}
]
},
filterItems() {
return [
{
text: this.$strings.LabelShowAll,
value: 'all'
},
{
text: this.$strings.LabelIncomplete,
value: 'incomplete'
},
{
text: this.$strings.LabelInProgress,
value: 'inProgress'
},
{
text: this.$strings.LabelComplete,
value: 'complete'
},
{
text: this.$strings.LabelDownloaded,
value: 'downloaded'
}
]
},
episodesFiltered() {
return this.episodesCopy.filter((ep) => {
if (this.filterKey === 'downloaded') {
@@ -230,7 +234,7 @@ export default {
},
async searchEpisodes() {
if (!this.networkConnected) {
return this.$toast.error('No network connection')
return this.$toast.error(this.$strings.MessageNoNetworkConnection)
}
if (!this.mediaMetadata.feedUrl) {
+57 -33
View File
@@ -1,35 +1,37 @@
<template>
<div class="w-full py-4 overflow-hidden relative border-b border-white border-opacity-10" @click.stop="goToEpisodePage">
<div class="w-full py-4 overflow-hidden relative border-b border-border" @click.stop="goToEpisodePage">
<div v-if="episode" class="w-full px-1">
<div class="flex mb-2">
<div class="w-10 min-w-10">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)" :width="40" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
</div>
<div class="flex-grow px-2">
<div class="-mt-0.5 mb-0.5" @click.stop>
<nuxt-link :to="`/item/${libraryItemId}`" class="text-sm text-gray-200 underline">{{ podcast.metadata.title }}</nuxt-link>
<div class="flex items-center">
<div class="-mt-0.5 mb-0.5" @click.stop>
<nuxt-link :to="`/item/${libraryItemId}`" class="text-sm text-fg underline">{{ podcast.metadata.title }}</nuxt-link>
</div>
<widgets-explicit-indicator v-if="podcast.metadata.explicit" />
</div>
<p v-if="publishedAt" class="text-xs text-gray-300">{{ $dateDistanceFromNow(publishedAt) }}</p>
<p v-if="publishedAt" class="text-xs text-fg-muted">{{ $dateDistanceFromNow(publishedAt) }}</p>
</div>
</div>
<p class="text-sm font-semibold">
{{ title }}
</p>
<p class="text-sm font-semibold">{{ title }}</p>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<p class="text-sm text-fg episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<div v-if="episodeNumber || season || episodeType" class="flex pt-2 items-center -mx-0.5">
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200 capitalize">
{{ episodeType }}
</div>
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg">{{ $strings.LabelEpisode }} #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg">{{ $strings.LabelSeason }} #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg capitalize">{{ episodeType }}</div>
</div>
<div class="flex items-center pt-2">
<div class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<div class="h-8 px-4 border border-border hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-fg text-opacity-40' : ''" @click.stop="playClick">
<span v-if="!playerIsStartingForThisMedia" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<svg v-else class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
</div>
@@ -41,7 +43,7 @@
<div v-if="userCanDownload">
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
<span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
<span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="downloadItem || pendingDownload ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ downloadItem || pendingDownload ? 'downloading' : 'download' }}</span>
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
</div>
@@ -58,8 +60,8 @@
</template>
<script>
import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
import CellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
export default {
props: {
@@ -78,9 +80,11 @@ export default {
data() {
return {
isProcessingReadUpdate: false,
pendingDownload: false,
processing: false
}
},
mixins: [CellularPermissionHelpers],
computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
@@ -122,6 +126,15 @@ export default {
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
if (!this.episode?.id) return false
const mediaId = this.$store.state.playerStartingPlaybackMediaId
return mediaId === this.episode.id
},
itemProgress() {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id)
@@ -135,10 +148,10 @@ export default {
}
},
itemProgressPercent() {
return this.itemProgress ? this.itemProgress.progress : 0
return this.itemProgress?.progress || 0
},
userIsFinished() {
return this.itemProgress ? !!this.itemProgress.isFinished : false
return !!this.itemProgress?.isFinished
},
timeRemaining() {
if (this.streamIsPlaying) return 'Playing'
@@ -154,7 +167,7 @@ export default {
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId, this.episode.id)
},
localEpisodeId() {
return this.localEpisode ? this.localEpisode.id : null
return this.localEpisode?.id || null
},
podcast() {
return this.episode.podcast || {}
@@ -175,14 +188,23 @@ export default {
return folderObj
},
async downloadClick() {
if (this.downloadItem) return
if (this.downloadItem || this.pendingDownload) return
const hasPermission = await this.checkCellularPermission('download')
if (!hasPermission) return
this.pendingDownload = true
await this.$hapticsImpact()
if (this.isIos) {
// no local folders on iOS
this.startDownload()
await this.startDownload()
} else {
this.download()
await this.download()
}
setTimeout(() => {
this.pendingDownload = false
}, 1000)
},
async download(selectedLocalFolder = null) {
let localFolder = selectedLocalFolder
@@ -216,14 +238,7 @@ export default {
console.log('Local folder', JSON.stringify(localFolder))
var startDownloadMessage = `Start download for "${this.title}" to folder ${localFolder.name}?`
const { value } = await Dialog.confirm({
title: 'Confirm',
message: startDownloadMessage
})
if (value) {
this.startDownload(localFolder)
}
return this.startDownload(localFolder)
},
async startDownload(localFolder) {
var payload = {
@@ -238,16 +253,21 @@ export default {
var errorMsg = downloadRes.error || 'Unknown error'
console.error('Download error', errorMsg)
this.$toast.error(errorMsg)
} else {
console.log('Download completed', JSON.stringify(downloadRes))
}
},
async playClick() {
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.$store.commit('setPlayerIsStartingPlayback', this.episode.id)
if (this.localEpisode && this.localLibraryItemId) {
console.log('Play local episode', this.localEpisode.id, this.localLibraryItemId)
this.$eventBus.$emit('play-item', {
libraryItemId: this.localLibraryItemId,
episodeId: this.localEpisode.id,
@@ -270,7 +290,11 @@ export default {
const isFinished = !this.userIsFinished
const localLibraryItemId = this.isLocal ? this.libraryItemId : this.localLibraryItemId
const localEpisodeId = this.isLocal ? this.episode.id : this.localEpisode.id
const payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId, localEpisodeId, isFinished })
const payload = await this.$db.updateLocalMediaProgressFinished({
localLibraryItemId,
localEpisodeId,
isFinished
})
console.log('toggleFinished payload', JSON.stringify(payload))
if (payload?.error) {
this.$toast.error(payload?.error || 'Unknown error')
+13 -7
View File
@@ -1,15 +1,15 @@
<template>
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-border text-center" :disabled="disabled || loading" :class="classList">
<slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<div v-if="loading" class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</nuxt-link>
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
<button v-else class="btn outline-none rounded-md shadow-md relative border border-border" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
<slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<div v-if="loading" class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
@@ -30,6 +30,7 @@ export default {
default: ''
},
paddingX: Number,
paddingY: Number,
small: Boolean,
loading: Boolean,
disabled: Boolean
@@ -41,19 +42,24 @@ export default {
classList() {
var list = []
if (this.loading) list.push('text-opacity-0')
list.push('text-white')
if (this.color === 'success') {
list.push('text-white')
}
list.push(`bg-${this.color}`)
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')
}
+1 -1
View File
@@ -4,7 +4,7 @@
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
<svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
</div>
<div v-if="label" class="select-none text-gray-100" :class="labelClassname">{{ label }}</div>
<div v-if="label" class="select-none text-fg" :class="labelClassname">{{ label }}</div>
</label>
</template>
+7 -7
View File
@@ -1,8 +1,8 @@
<template>
<div class="relative w-full" v-click-outside="clickedOutside">
<p class="text-sm font-semibold" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center" :class="!selectedText ? 'text-gray-300' : 'text-white'">
<p class="text-sm font-semibold" :class="disabled ? 'text-fg-muted' : ''">{{ label }}</p>
<button type="button" :disabled="disabled" class="relative w-full border border-border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center" :class="!selectedText ? 'text-fg-muted' : 'text-fg'">
<span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText || placeholder || '' }}</span>
</span>
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
@@ -11,9 +11,9 @@
</button>
<transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-gray-600 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-border shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-bg ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox">
<template v-for="item in items">
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(item.value)">
<li :key="item.value" class="text-fg select-none relative py-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">
<span class="font-normal ml-3 block truncate font-sans text-sm">{{ item.text }}</span>
</div>
@@ -65,8 +65,8 @@ export default {
if (this.small) classes.push('h-9')
else classes.push('h-10')
if (this.disabled) classes.push('cursor-not-allowed border-gray-600 bg-primary bg-opacity-70 border-opacity-70 text-gray-400')
else classes.push('cursor-pointer border-gray-600 bg-primary text-gray-100')
if (this.disabled) classes.push('cursor-not-allowed border-border bg-primary bg-opacity-70 border-opacity-70 text-fg-muted')
else classes.push('cursor-pointer border-border bg-primary text-fg')
return classes.join(' ')
}
+3 -3
View File
@@ -4,17 +4,17 @@
<slot />
</div>
<transition name="menu">
<ul ref="menu" v-show="showMenu" class="absolute z-50 -mt-px bg-primary border border-gray-600 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" style="width: 160px">
<ul ref="menu" v-show="showMenu" class="absolute z-50 -mt-px bg-primary border border-border shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-bg ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" style="width: 160px">
<template v-for="item in items">
<nuxt-link :key="item.value" v-if="item.to" :to="item.to">
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<li :key="item.value" class="text-fg select-none relative py-2" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center px-2">
<span v-if="item.icon" class="material-icons-outlined text-lg mr-2" :class="item.iconClass ? item.iconClass : ''">{{ item.icon }}</span>
<span class="font-normal block truncate font-sans text-center">{{ item.text }}</span>
</div>
</li>
</nuxt-link>
<li v-else :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<li v-else :key="item.value" class="text-fg select-none relative py-2" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center px-2">
<span v-if="item.icon" class="material-icons-outlined text-lg mr-2" :class="item.iconClass ? item.iconClass : ''">{{ item.icon }}</span>
<span class="font-normal block truncate font-sans text-center">{{ item.text }}</span>
+7 -3
View File
@@ -1,6 +1,6 @@
<template>
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :disabled="disabled || loading" :class="className" :type="type" @mousedown.prevent @click="clickBtn">
<div v-if="loading" class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
@@ -13,6 +13,10 @@
export default {
props: {
icon: String,
type: {
type: String,
default: 'button'
},
disabled: Boolean,
bgColor: {
type: String,
@@ -77,4 +81,4 @@ button.icon-btn:disabled::before {
button.icon-btn:disabled span {
color: #777;
}
</style>
</style>
+2 -2
View File
@@ -1,13 +1,13 @@
<template>
<div class="w-40">
<div class="bg-bg border border-gray-500 py-2 px-5 rounded-lg flex items-center flex-col box-shadow-md">
<div class="bg-bg border border-border py-2 px-5 rounded-lg flex items-center flex-col box-shadow-md">
<div class="loader-dots block relative w-20 h-5 mt-2">
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
</div>
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ text }}</div>
<div class="text-fg text-xs font-light mt-2 text-center">{{ text }}</div>
</div>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More