diff options
311 files changed, 8348 insertions, 6886 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index 1a97e73c5..8063f259c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,7 +45,7 @@ workflows: destination: app-play-debug.apk - run: name: Execute debug unit tests - command: ./gradlew :core:testPlayDebugUnitTest -PdisablePreDex + command: ./gradlew testPlayDebugUnitTest -PdisablePreDex - build: name: Build release build-steps: @@ -54,19 +54,19 @@ workflows: command: ./gradlew assembleRelease -PdisablePreDex - run: name: Execute release unit tests - command: ./gradlew :core:testPlayReleaseUnitTest -PdisablePreDex + command: ./gradlew testPlayReleaseUnitTest -PdisablePreDex - build: name: Build integration tests build-steps: - run: name: Build integration tests - command: ./gradlew :app:assemblePlayDebugAndroidTest -PdisablePreDex + command: ./gradlew assemblePlayDebugAndroidTest -PdisablePreDex - build: name: Build free build-steps: - run: name: Build free (for F-Droid) - command: ./gradlew assembleFreeRelease -PdisablePreDex -PfreeBuild + command: ./gradlew assembleFreeRelease -PdisablePreDex static-analysis: jobs: @@ -85,19 +85,8 @@ workflows: curl -s -L https://github.com/yangziwen/diff-checkstyle/releases/download/0.0.4/diff-checkstyle.jar > diff-checkstyle.jar java -Dconfig_loc=config/checkstyle -jar diff-checkstyle.jar -c config/checkstyle/checkstyle-new-code.xml --git-dir . --base-rev $branchBaseCommit - build: - name: Lint app + name: Lint build-steps: - run: - name: Lint app - command: ./gradlew app:lintPlayRelease - - run: - name: Lint core - command: ./gradlew core:lintPlayRelease - - store_artifacts: - name: Uploading app lint reports - path: app/build/reports/lint-results-playRelease.html - destination: lint-results-app.html - - store_artifacts: - name: Uploading core lint reports - path: core/build/reports/lint-results-playRelease.html - destination: lint-results-core.html + name: Lint + command: ./gradlew lintPlayRelease diff --git a/.github/PULL_REQUEST_TEMPLATE/default.md b/.github/pull_request_template.md index f9fea783a..f9fea783a 100644 --- a/.github/PULL_REQUEST_TEMPLATE/default.md +++ b/.github/pull_request_template.md diff --git a/.tx/config b/.tx/config index 43e05555a..bc37f39fe 100644 --- a/.tx/config +++ b/.tx/config @@ -4,6 +4,7 @@ host = https://www.transifex.com [antennapod.core-values] source_file = core/src/main/res/values/strings.xml source_lang = en +trans.ar = core/src/main/res/values-ar/strings.xml trans.br = core/src/main/res/values-br/strings.xml trans.ca = core/src/main/res/values-ca/strings.xml trans.cs_CZ = core/src/main/res/values-cs/strings.xml @@ -29,6 +30,7 @@ trans.pl_PL = core/src/main/res/values-pl/strings.xml trans.pt = core/src/main/res/values-pt/strings.xml trans.pt_BR = core/src/main/res/values-pt-rBR/strings.xml trans.ru_RU = core/src/main/res/values-ru/strings.xml +trans.sk = core/src/main/res/values-sk/strings.xml trans.sv_SE = core/src/main/res/values-sv/strings.xml trans.tr = core/src/main/res/values-tr/strings.xml trans.uk_UA = core/src/main/res/values-uk/strings.xml @@ -58,7 +60,6 @@ trans.he_IL = app/src/main/play/listings/iw-IL/full-description.txt trans.hu = app/src/main/play/listings/hu-HU/full-description.txt trans.id = app/src/main/play/listings/id/full-description.txt trans.it_IT = app/src/main/play/listings/it-IT/full-description.txt -trans.iw = app/src/main/play/listings/iw-IL/full-description.txt trans.ja = app/src/main/play/listings/ja-JP/full-description.txt trans.ko = app/src/main/play/listings/ko-KR/full-description.txt trans.lt = app/src/main/play/listings/lt/full-description.txt @@ -68,6 +69,7 @@ trans.pt_BR = app/src/main/play/listings/pt-BR/full-description.txt trans.pt = app/src/main/play/listings/pt-PT/full-description.txt trans.ro_RO = app/src/main/play/listings/ro/full-description.txt trans.ru_RU = app/src/main/play/listings/ru-RU/full-description.txt +trans.sk = app/src/main/play/listings/sk/full-description.txt trans.sl_SI = app/src/main/play/listings/sl/full-description.txt trans.sv_SE = app/src/main/play/listings/sv-SE/full-description.txt trans.tr = app/src/main/play/listings/tr-TR/full-description.txt @@ -98,7 +100,6 @@ trans.he_IL = app/src/main/play/listings/iw-IL/short-description.txt trans.hu = app/src/main/play/listings/hu-HU/short-description.txt trans.id = app/src/main/play/listings/id/short-description.txt trans.it_IT = app/src/main/play/listings/it-IT/short-description.txt -trans.iw = app/src/main/play/listings/iw-IL/short-description.txt trans.ja = app/src/main/play/listings/ja-JP/short-description.txt trans.ko = app/src/main/play/listings/ko-KR/short-description.txt trans.lt = app/src/main/play/listings/lt/short-description.txt @@ -108,6 +109,7 @@ trans.pt_BR = app/src/main/play/listings/pt-BR/short-description.txt trans.pt = app/src/main/play/listings/pt-PT/short-description.txt trans.ro_RO = app/src/main/play/listings/ro/short-description.txt trans.ru_RU = app/src/main/play/listings/ru-RU/short-description.txt +trans.sk = app/src/main/play/listings/sk/short-description.txt trans.sl_SI = app/src/main/play/listings/sl/short-description.txt trans.sv_SE = app/src/main/play/listings/sv-SE/short-description.txt trans.tr = app/src/main/play/listings/tr-TR/short-description.txt diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 8fb58a64a..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,471 +0,0 @@ -Change Log -========== - -Version 1.8.1 -------------- -* Enabled picture-in-picture for video podcasts by default (by @ByteHamster) -* Fixed podcast discovery not showing local trends (by @tonytamsf) -* Various bug fixes and improvements (by @ByteHamster) - -Version 1.8.0 -------------- -* Added per-feed playback speed setting (by @spacecowboy) -* Support sorting in Podcast screen (by @orionlee) -* Option to show stream button rather than download in lists (by @dsmith47) -* Option to replace Episode cover with Podcast cover (by @xgouchet) -* Transparent widget (by @M-arcel) -* User interface tweaks (by @ByteHamster) -* Tons of bug fixes and improvements - -Version 1.7.3 -------------- -* Display episode image on widget (by @brad) -* Added checkbox to keep queue sorted (by @damoasda) -* New UI for "Add podcast" screen (by @ByteHamster) -* Added batch editing to the queue (by @ByteHamster) -* Added option to adapt remaining time to playback speed (by @CedricCabessa) -* Removed broken Flattr integration (by @ByteHamster) -* Added filter to "All episodes" list (by @jhunnius) -* Tons of bug fixes and performance improvements - -Version 1.7.2 -------------- -* Added configurable behavior of the back button -* Added delete option to episode's context menu -* New UI for batch edit feature -* Set number of columns in subscription list -* Lots of bug fixes - -Version 1.7.1 -------------- - -* Fix for database corruption - -Version 1.7.0 -------------- - -* NEW ExoPlayer (experimental) -* Fix for Bluetooth Forward (Oreo) -* Preference redesign + search -* Notification improvements -* Different screens for feed info and settings -* Sort Queue with Random or Smart Shuffle -* True Black Theme for AMOLED -* Improvements to feed parsing -* Fix for app being killed by Android Oreo - -Version 1.6.5 -------------- - -* Fix database corruption -* Improvements to Feed parsing - -Version 1.6.4 -------------- - -* Fixes issues on Android Oreo -* Avoids duplicate chapters -* Experimental: Database import & export - -Version 1.6.3 -------------- - -* New features: - * Support for Android Auto - * Sort feeds by number of played episodes - * Statistics modes - * Setting: Enqueue downloaded - * Launch screen -* Improvements - * Chapter duration - * Feed title in deletion confirmation -* Fixes: - * Episodes refresh spinner - * Publication date parsing - * Unknown mime type - -Version 1.6.2 -------------- - -* New features: - * Integration of fyyd Podcast Search Engine - * Export subscriptions as HTML - * Rename feeds - * Auto-enable sleep timer - * "has media" filter - * Force gpodder full sync -* Improvements: - * Better support for Atom feeds, e.g. summary tag - * Confirmation dialog on mark all as seen - * Number of downloaded episodes in subscription counter - * Gpodder sync error optional - * Search results - * MRSS support - * Sanitize HTML from Atom feed -* Fixes: - * Reset sleep timer on shake to current waiting time - * Cast dialog image - * Mini player not showing up - * Audio player cover fragment - * Prevent out of memory and casting crashes - -Version 1.6.0 -------------- -* New features: - * Experimental Chromecast support - * Subscription overview - * Proxy support - * Statistics - * Manual gpodder.net sync -* Fixes: - * Audioplayer controls - * Audio ducking - * Video control fade-out - * External media controls - * Feed parsing - -Version 1.5.0 -------------- -* Exclude episodes from auto download by keyword -* Configure feeds to prevent them from refreshing automatically -* Improved audio player -* Improved UI -* Bug fixes - -Version 1.4.1 -------------- -* Performance improvements -* Hardware buttons now ff and rewind instead of skipping -* Option to have forward button skip -* Option to send crash reports directly to developers -* Highlight currently playing episode -* Widget improvements - -Version 1.4.0.12 ----------------- -* Fix for crash on Huawei devices (media buttons may not work) - -Version 1.4 ------------ -* BLUETOOTH PERMISSION: Needed to be able to resume playback when a Bluetooth device reconnects with your phone -* VIBRATE PERMISSION: Used optionally with the sleep timer -* Native variable speed playback (experimental via options) -* Improved sleep timer -* Mark episodes as 'favorite' -* Notification can skip episodes -* Keep episodes when skipping them -* Episode art on lock screen -* Flexible episode cleanup -* Rewind after pause -* Usability improvements -* Bug fixes - -Version 1.3 ------------ -* Bulk actions on feed episodes (download, queue, delete) -* Reduced space used by images -* Automatic refresh at a certain time of day -* Customizable indicators and sorting for feeds -* Ability to share feeds -* Improved auto download -* Many fixes and usability improvements - -Version 1.2 ------------ -* Optionally disable swiping and dragging in the queue -* Resume playback after phone call -* Filter episodes in the Podcast feed -* Hide items in the Nav drawer -* Customize times for fast forward and rewind -* Resolved issues with opening some OPML files -* Various bug fixes and usability improvements - -Version 1.1 ------------ -* iTunes podcast integration -* Swipe to remove items from the queue -* Set the number of parallel downloads -* Fix for gpodder.net on old devices -* Fixed date problems for some feeds -* Display improvements -* Usability improvements -* Several other bugfixes - -Version 1.0 ------------ -* The queue can now be sorted -* Added option to delete episode after playback -* Fixed a bug that caused chapters to be displayed multiple times -* Several other improvements and bugfixes - - -Version 0.9.9.6 ---------------- -* Fixed problems related to variable playback speed plugins -* Fixed automatic feed update problems -* Several other bugfixes and improvements - -Version 0.9.9.5 ---------------- -* Added support for paged feeds -* Improved user interface -* Added Japanese and Turkish translations -* Fixed more image loading problems -* Other bugfixes and improvements - -Version 0.9.9.4 ---------------- -* Added option to keep notification and lockscreen controls when playback is paused -* Fixed a bug where episode images were not loaded correctly -* Fixed battery usage problems - -Version 0.9.9.3 ---------------- -* Fixed video playback problems -* Improved image loading -* Other bugfixes and improvements - -Version 0.9.9.2 ---------------- -* Added support for feed discovery if a website URL is entered -* Added support for 'next'/'previous' media keys -* Improved sleep timer -* Timestamps in shownotes can now be used to jump to a specific position -* Automatic Flattring is now configurable -* Several bugfixes and improvements - -Version 0.9.9.1 ---------------- -* Several bugfixes and improvements - -Version 0.9.9.0 ---------------- -* New user interface -* Failed downloads are now resumed when restarted -* Added support for Podlove Alternate Feeds -* Added support for "pcast"-protocol -* Added backup & restore functionality. This feature has to be enabled in the Android settings in order to work -* Several bugfixes and improvements - -Version 0.9.8.3 ---------------- -* Added support for password-protected feeds and episodes -* Added support for more types of episode images -* Added Hebrew translation -* Several bugfixes and improvements - -Version 0.9.8.2 ---------------- -* Several bugfixes and improvements -* Added Korean translation - -Version 0.9.8.1 ---------------- -* Added option to flattr an episode automatically after 80 percent of the episode have been played -* Added Polish translation -* Several bugfixes and improvements - -Version 0.9.8.0 ---------------- -* Added access to the gpodder.net directory -* Added ability to sync podcast subscriptions with the gpodder.net service -* Automatic download can now be turned on or off for specific podcasts -* Added option to pause playback when another app is playing sounds -* Added Dutch and Hindi translation -* Resolved a problem with automatic podcast updates -* Resolved a problem with the buttons' visibility in the episode screen -* Resolved a problem where episodes would be re-downloaded unnecessarily -* Several other bugfixes and usability improvements - -Version 0.9.7.5 ---------------- -* Reduced application startup time -* Reduced memory usage -* Added option to change the playback speed -* Added Swedish translation -* Several bugfixes and improvements - -Version 0.9.7.4 ---------------- -* Episode cache size can now be set to unlimited -* Removing an episode in the queue via sliding can now be undone -* Added support for Links in MP3 chapters -* Added Czech(Czech Republic), Azerbaijani and Portuguese translations -* Several bugfixes and improvements - -Version 0.9.7.3 ---------------- -* Bluetooth devices now display metadata during playback (requires AVRCP 1.3 or higher) -* User interface improvements -* Several bugfixes - -Version 0.9.7.2 ---------------- -* Automatic download can now be disabled -* Added Italian (Italy) translation -* Several bugfixes - -Version 0.9.7.1 ---------------- -* Added automatic download of new episodes -* Added option to specify the number of downloaded episodes to keep on the device -* Added support for playback of external media files -* Several improvements and bugfixes -* Added Catalan translation - -Version 0.9.7 -------------- -* Improved user interface -* OPML files can now be imported by selecting them in a file browser -* The queue can now be organized via drag & drop -* Added expandable notifications (only supported on Android 4.1 and above) -* Added Danish, French, Romanian (Romania) and Ukrainian (Ukraine) translation (thanks to all translators!) -* Several bugfixes and minor improvements - -Version 0.9.6.4 ---------------- -* Added Chinese translation (Thanks tupunco!) -* Added Portuguese (Brazil) translation (Thanks mbaltar!) -* Several bugfixes - -Version 0.9.6.3 ---------------- -* Added the ability change the location of AntennaPod's data folder -* Added Spanish translation (Thanks frandavid100!) -* Solved problems with several feeds - -Version 0.9.6.2 ---------------- -* Fixed import problems with some OPML files -* Fixed download problems -* AntennaPod now recognizes changes of episode information -* Other improvements and bugfixes - -Version 0.9.6.1 ---------------- -* Added dark theme -* Several bugfixes and improvements - -Version 0.9.6 -------------- -* Added support for VorbisComment chapters -* AntennaPod now shows items as 'in progress' when playback has started -* Reduced memory usage -* Added support for more feed types -* Several bugfixes - - -Version 0.9.5.3 ---------------- -* Fixed crash when trying to start playback on some devices -* Fixed problems with some feeds -* Other bugfixes and improvements - -Version 0.9.5.2 ---------------- -* Media player now doesn't use network bandwidth anymore if not in use -* Other improvements and bugfixes - -Version 0.9.5.1 ---------------- -* Added playback history -* Improved behavior of download report notifications -* Improved support for headset controls -* Bugfixes in the feed parser -* Moved 'OPML import' button into the 'add feed' screen and the 'OPML export' button into the settings screen - -Version 0.9.5 -------------- -* Experimental support for MP3 chapters -* New menu options for the 'new' list and the queue -* Auto-delete feature -* Better Download error reports -* Several Bugfixes - -Version 0.9.4.6 ---------------- -* Enabled support for small-screen devices -* Disabling the sleep timer should now work again - -Version 0.9.4.5 ---------------- -* Added Russian translation (Thanks older!) -* Added German translation -* Several bugfixes - -Version 0.9.4.4 ---------------- -* Added player controls at the bottom of the main screen and the feedlist screens -* Improved media playback - -Version 0.9.4.3 ---------------- -* Fixed several bugs in the feed parser -* Improved behavior of download reports - -Version 0.9.4.2 ---------------- -* Fixed bug in the OPML importer -* Reduced memory usage of images -* Fixed download problems on some devices - -Version 0.9.4.1 ---------------- -* Changed behavior of download notifications - -Version 0.9.4 -------------- -* Faster and more reliable downloads -* Added lockscreen player controls for Android 4.x devices -* Several bugfixes - -Version 0.9.3.1 ---------------- -* Added preference to hide feed items which don't have an episode -* Improved image size for some some screen sizes -* Added grid view for large screens -* Several bugfixes - -Version 0.9.3 -------------- -* MiroGuide integration -* Bugfixes in the audio- and videoplayer -* Automatically add feeds to the queue when they have been downloaded - -Version 0.9.2 -------------- -* Bugfixes in the user interface -* GUID and ID attributes are now recognized by the Feedparser -* Stability improvements when adding several feeds at the same time -* Fixed bugs that occured when adding certain feeds - -Version 0.9.1.1 --------------------- -* Changed Flattr credentials -* Improved layout of Feed information screen -* AntennaPod is now open source! The source code is available at https://github.com/danieloeh/AntennaPod - -Version 0.9.1 ------------------ -* Added support for links in SimpleChapters -* Bugfix: Current Chapter wasn't always displayed correctly - -Version 0.9 --------------- - -* OPML export -* Flattr integration -* Sleep timer - -Version 0.8.2 -------------- - -* Added search -* Improved OPML import experience -* More bugfixes - -Version 0.8.1 ------------- - -* Added support for SimpleChapters -* OPML import diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7cb59fc2..614e76d87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,3 +35,24 @@ Submit a pull request - Please do not upgrade dependencies or build tools unless you have a good reason for it. Doing so can easily introduce bugs that are hard to track down. - If you plan to do a change that touches many files (10+), please ask beforehand. This usually causes merge conflicts for other developers. - Please follow our code style. You can use Checkstyle within Android Studio using our [coniguration file](https://github.com/AntennaPod/AntennaPod/blob/develop/config/checkstyle/checkstyle-new-code.xml). +- Please only change the English string resources. Translations are handled on [Transifex](https://www.transifex.com/antennapod/antennapod/). + + +Testing and Verifying +-------------------------- +As a developer contributing to AntennaPod, we ask that you test the feature yourself manually and better yet, add unit and functional tests to any feature of bug you fix. + +### Running Unit Tests +* `./gradlew :core:testPlayDebugUnitTest` + +### Running Integration Tests + +#### Using Android Studio +* Create a configuration via 'Run->Edit Configurations...' + +<img width="768" alt="antennapod-run-tests" +src="https://user-images.githubusercontent.com/149837/105122859-e1317180-5a8b-11eb-8d45-d54a3b051a9b.png"> + +#### Using the command line +* Start an AVD or plug in your phone +* `sh .github/workflows/runTests.sh` diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a2a44e10e..97b6a3c9c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,27 +1,27 @@ # Developers -[ByteHamster](https://github.com/ByteHamster), [danieloeh](https://github.com/danieloeh), [mfietz](https://github.com/mfietz), [TomHennen](https://github.com/TomHennen), [orionlee](https://github.com/orionlee), [domingos86](https://github.com/domingos86), [tonytamsf](https://github.com/tonytamsf), [andersonvom](https://github.com/andersonvom), [damoasda](https://github.com/damoasda), [TacoTheDank](https://github.com/TacoTheDank), [shortspider](https://github.com/shortspider), [ebraminio](https://github.com/ebraminio), [asdoi](https://github.com/asdoi), [spacecowboy](https://github.com/spacecowboy), [patheticpat](https://github.com/patheticpat), [brad](https://github.com/brad), [Cj-Malone](https://github.com/Cj-Malone), [maxbechtold](https://github.com/maxbechtold), [gaul](https://github.com/gaul), [qkolj](https://github.com/qkolj), [keunes](https://github.com/keunes), [pachecosf](https://github.com/pachecosf), [gerardolgvr](https://github.com/gerardolgvr), [bws9000](https://github.com/bws9000), [ahangarha](https://github.com/ahangarha), [hannesa2](https://github.com/hannesa2), [rharriso](https://github.com/rharriso), [xgouchet](https://github.com/xgouchet), [sevenmaster](https://github.com/sevenmaster), [TheRealFalcon](https://github.com/TheRealFalcon), [Slinger](https://github.com/Slinger), [johnjohndoe](https://github.com/johnjohndoe), [jas14](https://github.com/jas14), [udif](https://github.com/udif), [malockin](https://github.com/malockin), [dirkmueller](https://github.com/dirkmueller), [jatinkumarg](https://github.com/jatinkumarg), [peschmae0](https://github.com/peschmae0), [orelogo](https://github.com/orelogo), [txtd](https://github.com/txtd), [ydinath](https://github.com/ydinath), [CedricCabessa](https://github.com/CedricCabessa), [mchelen](https://github.com/mchelen), [dethstar](https://github.com/dethstar), [drabux](https://github.com/drabux), [saqura](https://github.com/saqura), [bibz](https://github.com/bibz), [hzulla](https://github.com/hzulla), [deandreamatias](https://github.com/deandreamatias), [MeirAtIMDDE](https://github.com/MeirAtIMDDE), [egsavage](https://github.com/egsavage), [ligi](https://github.com/ligi), [Xeitor](https://github.com/Xeitor), [dreiss](https://github.com/dreiss), [liesen](https://github.com/liesen), [nereocystis](https://github.com/nereocystis), [rezanejati](https://github.com/rezanejati), [twiceyuan](https://github.com/twiceyuan), [JessieVela](https://github.com/JessieVela), [HaBaLeS](https://github.com/HaBaLeS), [volhol](https://github.com/volhol), [michaelmwhite](https://github.com/michaelmwhite), [CameronBanga](https://github.com/CameronBanga), [HrBDev](https://github.com/HrBDev), [HolgerJeromin](https://github.com/HolgerJeromin), [xisberto](https://github.com/xisberto), [jmue](https://github.com/jmue), [katrinleinweber](https://github.com/katrinleinweber), [LatinSuD](https://github.com/LatinSuD), [24hours](https://github.com/24hours), [SosoTughushi](https://github.com/SosoTughushi), [fabolhak](https://github.com/fabolhak), [archibishop](https://github.com/archibishop), [alifeflow](https://github.com/alifeflow), [avirajrsingh](https://github.com/avirajrsingh), [toggles](https://github.com/toggles), [matdb](https://github.com/matdb), [damlayildiz](https://github.com/damlayildiz), [kingargyle](https://github.com/kingargyle), [dsmith47](https://github.com/dsmith47), [hannesaa2](https://github.com/hannesaa2), [jhunnius](https://github.com/jhunnius), [ShadowIce](https://github.com/ShadowIce), [Niffler](https://github.com/Niffler), [raghulj](https://github.com/raghulj), [raghulrm](https://github.com/raghulrm), [mamehacker](https://github.com/mamehacker), [skitt](https://github.com/skitt), [wseemann](https://github.com/wseemann), [markamaze](https://github.com/markamaze), [mohitshah3111999](https://github.com/mohitshah3111999), [moralesg](https://github.com/moralesg), [mr-intj](https://github.com/mr-intj), [tuxayo](https://github.com/tuxayo), [schlch](https://github.com/schlch), [alimemonzx](https://github.com/alimemonzx), [dev-darrell](https://github.com/dev-darrell), [jmdouglas](https://github.com/jmdouglas), [olivoto](https://github.com/olivoto), [PtilopsisLeucotis](https://github.com/PtilopsisLeucotis), [abhinavg1997](https://github.com/abhinavg1997), [alanorth](https://github.com/alanorth), [alexte](https://github.com/alexte), [andrey-krutov](https://github.com/andrey-krutov), [arantius](https://github.com/arantius), [BoJacobs](https://github.com/BoJacobs), [chetan882777](https://github.com/chetan882777), [chrissicool](https://github.com/chrissicool), [cszucko](https://github.com/cszucko), [CWftw](https://github.com/CWftw), [connectety](https://github.com/connectety), [danielm5](https://github.com/danielm5), [ariedov](https://github.com/ariedov), [brettle](https://github.com/brettle), [edwinhere](https://github.com/edwinhere), [eirikv](https://github.com/eirikv), [eerden](https://github.com/eerden), [jklippel](https://github.com/jklippel), [jannic](https://github.com/jannic), [Foso](https://github.com/Foso), [Kaligule](https://github.com/Kaligule), [kvithayathil](https://github.com/kvithayathil), [luiscruz](https://github.com/luiscruz), [mlasson](https://github.com/mlasson), [schwedenmut](https://github.com/schwedenmut), [M-arcel](https://github.com/M-arcel), [msoose](https://github.com/msoose), [mo](https://github.com/mo), [mdeveloper20](https://github.com/mdeveloper20), [mschuetz](https://github.com/mschuetz), [max-wittig](https://github.com/max-wittig), [MolarAmbiguity](https://github.com/MolarAmbiguity), [mounirlamouri](https://github.com/mounirlamouri), [nikhil097](https://github.com/nikhil097), [panoreak](https://github.com/panoreak), [patrickjkennedy](https://github.com/patrickjkennedy), [ortylp](https://github.com/ortylp), [ramzan](https://github.com/ramzan), [iamrichR](https://github.com/iamrichR), [SamWhited](https://github.com/SamWhited), [selivan](https://github.com/selivan), [sonnayasomnambula](https://github.com/sonnayasomnambula), [sethoscope](https://github.com/sethoscope), [shantanahardy](https://github.com/shantanahardy), [danners](https://github.com/danners), [corecode](https://github.com/corecode), [vimsick](https://github.com/vimsick), [lyallemma](https://github.com/lyallemma), [edent](https://github.com/edent), [atrus6](https://github.com/atrus6), [heyyviv](https://github.com/heyyviv), [waylife](https://github.com/waylife), [amhokies](https://github.com/amhokies), [andrewc1](https://github.com/andrewc1), [axq](https://github.com/axq), [binarytoto](https://github.com/binarytoto), [chrk2205](https://github.com/chrk2205), [fossterer](https://github.com/fossterer), [lightonflux](https://github.com/lightonflux), [minusf](https://github.com/minusf), [zawad2221](https://github.com/zawad2221) +[ByteHamster](https://github.com/ByteHamster), [danieloeh](https://github.com/danieloeh), [mfietz](https://github.com/mfietz), [TomHennen](https://github.com/TomHennen), [orionlee](https://github.com/orionlee), [domingos86](https://github.com/domingos86), [damoasda](https://github.com/damoasda), [tonytamsf](https://github.com/tonytamsf), [andersonvom](https://github.com/andersonvom), [TacoTheDank](https://github.com/TacoTheDank), [shortspider](https://github.com/shortspider), [spacecowboy](https://github.com/spacecowboy), [ebraminio](https://github.com/ebraminio), [asdoi](https://github.com/asdoi), [patheticpat](https://github.com/patheticpat), [brad](https://github.com/brad), [Cj-Malone](https://github.com/Cj-Malone), [maxbechtold](https://github.com/maxbechtold), [gaul](https://github.com/gaul), [qkolj](https://github.com/qkolj), [keunes](https://github.com/keunes), [pachecosf](https://github.com/pachecosf), [gerardolgvr](https://github.com/gerardolgvr), [bws9000](https://github.com/bws9000), [ahangarha](https://github.com/ahangarha), [hannesa2](https://github.com/hannesa2), [rharriso](https://github.com/rharriso), [xgouchet](https://github.com/xgouchet), [sevenmaster](https://github.com/sevenmaster), [TheRealFalcon](https://github.com/TheRealFalcon), [Slinger](https://github.com/Slinger), [johnjohndoe](https://github.com/johnjohndoe), [jas14](https://github.com/jas14), [udif](https://github.com/udif), [malockin](https://github.com/malockin), [dirkmueller](https://github.com/dirkmueller), [jatinkumarg](https://github.com/jatinkumarg), [peschmae0](https://github.com/peschmae0), [orelogo](https://github.com/orelogo), [txtd](https://github.com/txtd), [ydinath](https://github.com/ydinath), [CedricCabessa](https://github.com/CedricCabessa), [mchelen](https://github.com/mchelen), [dethstar](https://github.com/dethstar), [drabux](https://github.com/drabux), [saqura](https://github.com/saqura), [binarytoto](https://github.com/binarytoto), [bibz](https://github.com/bibz), [hzulla](https://github.com/hzulla), [deandreamatias](https://github.com/deandreamatias), [MeirAtIMDDE](https://github.com/MeirAtIMDDE), [egsavage](https://github.com/egsavage), [ligi](https://github.com/ligi), [Xeitor](https://github.com/Xeitor), [dreiss](https://github.com/dreiss), [liesen](https://github.com/liesen), [nereocystis](https://github.com/nereocystis), [rezanejati](https://github.com/rezanejati), [twiceyuan](https://github.com/twiceyuan), [JessieVela](https://github.com/JessieVela), [HaBaLeS](https://github.com/HaBaLeS), [volhol](https://github.com/volhol), [michaelmwhite](https://github.com/michaelmwhite), [CameronBanga](https://github.com/CameronBanga), [HrBDev](https://github.com/HrBDev), [HolgerJeromin](https://github.com/HolgerJeromin), [xisberto](https://github.com/xisberto), [jmue](https://github.com/jmue), [jonasburian](https://github.com/jonasburian), [katrinleinweber](https://github.com/katrinleinweber), [LatinSuD](https://github.com/LatinSuD), [24hours](https://github.com/24hours), [SosoTughushi](https://github.com/SosoTughushi), [fabolhak](https://github.com/fabolhak), [archibishop](https://github.com/archibishop), [alifeflow](https://github.com/alifeflow), [avirajrsingh](https://github.com/avirajrsingh), [toggles](https://github.com/toggles), [connectety](https://github.com/connectety), [matdb](https://github.com/matdb), [damlayildiz](https://github.com/damlayildiz), [kingargyle](https://github.com/kingargyle), [dsmith47](https://github.com/dsmith47), [hannesaa2](https://github.com/hannesaa2), [jhunnius](https://github.com/jhunnius), [a1291762](https://github.com/a1291762), [ShadowIce](https://github.com/ShadowIce), [Niffler](https://github.com/Niffler), [raghulj](https://github.com/raghulj), [raghulrm](https://github.com/raghulrm), [mamehacker](https://github.com/mamehacker), [skitt](https://github.com/skitt), [Thom-Merrilin](https://github.com/Thom-Merrilin), [wseemann](https://github.com/wseemann), [markamaze](https://github.com/markamaze), [mohitshah3111999](https://github.com/mohitshah3111999), [moralesg](https://github.com/moralesg), [mr-intj](https://github.com/mr-intj), [tuxayo](https://github.com/tuxayo), [alimemonzx](https://github.com/alimemonzx), [dev-darrell](https://github.com/dev-darrell), [jmdouglas](https://github.com/jmdouglas), [olivoto](https://github.com/olivoto), [PtilopsisLeucotis](https://github.com/PtilopsisLeucotis), [abhinavg1997](https://github.com/abhinavg1997), [alanorth](https://github.com/alanorth), [alexte](https://github.com/alexte), [andrey-krutov](https://github.com/andrey-krutov), [arantius](https://github.com/arantius), [BoJacobs](https://github.com/BoJacobs), [chetan882777](https://github.com/chetan882777), [chrissicool](https://github.com/chrissicool), [britiger](https://github.com/britiger), [cszucko](https://github.com/cszucko), [CWftw](https://github.com/CWftw), [danielm5](https://github.com/danielm5), [ariedov](https://github.com/ariedov), [brettle](https://github.com/brettle), [edwinhere](https://github.com/edwinhere), [eirikv](https://github.com/eirikv), [eerden](https://github.com/eerden), [Geist5000](https://github.com/Geist5000), [jklippel](https://github.com/jklippel), [jannic](https://github.com/jannic), [Foso](https://github.com/Foso), [Kaligule](https://github.com/Kaligule), [kvithayathil](https://github.com/kvithayathil), [luiscruz](https://github.com/luiscruz), [MStrecke](https://github.com/MStrecke), [mlasson](https://github.com/mlasson), [schwedenmut](https://github.com/schwedenmut), [M-arcel](https://github.com/M-arcel), [mgborowiec](https://github.com/mgborowiec), [msoose](https://github.com/msoose), [mo](https://github.com/mo), [mdeveloper20](https://github.com/mdeveloper20), [mschuetz](https://github.com/mschuetz), [max-wittig](https://github.com/max-wittig), [MolarAmbiguity](https://github.com/MolarAmbiguity), [mounirlamouri](https://github.com/mounirlamouri), [nikhil097](https://github.com/nikhil097), [panoreak](https://github.com/panoreak), [patrickjkennedy](https://github.com/patrickjkennedy), [ortylp](https://github.com/ortylp), [ramzan](https://github.com/ramzan), [iamrichR](https://github.com/iamrichR), [SamWhited](https://github.com/SamWhited), [SebiderSushi](https://github.com/SebiderSushi), [selivan](https://github.com/selivan), [sonnayasomnambula](https://github.com/sonnayasomnambula), [sethoscope](https://github.com/sethoscope), [shantanahardy](https://github.com/shantanahardy), [danners](https://github.com/danners), [corecode](https://github.com/corecode), [vimsick](https://github.com/vimsick), [lyallemma](https://github.com/lyallemma), [edent](https://github.com/edent), [atrus6](https://github.com/atrus6), [timakro](https://github.com/timakro), [heyyviv](https://github.com/heyyviv), [waylife](https://github.com/waylife), [yarons](https://github.com/yarons), [amhokies](https://github.com/amhokies), [andrewc1](https://github.com/andrewc1), [axq](https://github.com/axq), [chrk2205](https://github.com/chrk2205), [fossterer](https://github.com/fossterer), [lightonflux](https://github.com/lightonflux), [minusf](https://github.com/minusf), [s3lph](https://github.com/s3lph), [tamizh138](https://github.com/tamizh138), [zawad2221](https://github.com/zawad2221) # Translators | Language | Translators | | :-- | :-- | -| Arabic | abuzar3.khalid, badarotti, keunes, nabilMaghura, rex07, shubbar | +| Arabic | abuzar3.khalid, badarotti, keunes, MustafaAlgurabi, nabilMaghura, rex07, shubbar | | Asturian (ast_ES) | enolp | | Basque | gaztainalde, keunes, Osoitz, pospolos | | Breton | Belvar, keunes | -| Bulgarian | keunes, solusitor | +| Bulgarian | keunes, ma4ko, solusitor | | Catalan | carles.llacer, dvd1985, exort12, IvanAmarante, javiercoll, keunes, Kintu, lambdani, marcmetallextrem, xc70 | | Chinese (zh_CN) | brnme, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, keunes, kyleehee, molisiye, owen8877, RainSlide, RangerNJU, Sak94664, spice2wolf, tupunco, wongsyrone, yangyang, yiqiok | | Chinese (zh_TW) | bobchao, ijliao, keunes, mapobi, pggdt, ymhuang0808 | -| Czech (cs_CZ) | anotheranonymoususer, elich, Hanzmeister, svetlemodry, Thomaash | -| Danish | JFreak, jhertel, keunes, SebastianKiwiDk, twikedk | +| Czech (cs_CZ) | anotheranonymoususer, elich, Hanzmeister, md.share, svetlemodry, Thomaash | +| Danish | JFreak, jhertel, keunes, petterbejo, SebastianKiwiDk | | Dutch | e2jk, keunes, rwv, Vistaus | | Estonian | Eraser, keunes, mahfiaz | | Finnish | Ban3, keunes, Sahtor | -| French | ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, Poussinou, sterylmreep | +| French | ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, petterbejo, Poussinou, RomainTT, sterylmreep | | Galician | antiparvos, pikamoku, Raichely | -| German | _Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, mfietz, pudeeh, Quiss42, repat, tomte, tweimer, Willhelm, ypid | +| German | _Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, forght, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, max.wittig, mfietz, Michael_Strecke, petterbejo, pudeeh, Quiss42, repat, toaskoas, tomte, tweimer, Willhelm, ypid | | Modern Greek (1453-) | AnimaRain, antonist, keunes, pavlosv | | Hebrew (he_IL) | amir.dafnyman, E1i9, mongoose4004, pinkasey, rellieberman, Yaron | | Hindi (hi_IN) | keunes, purple.coder, siddhusengar, thelazyoxymoron | @@ -32,20 +32,20 @@ | Japanese | keunes, KotaKato, Naofumi, sh3llc4t, TranslatorG | | Kannada (kn_IN) | chiraag.nataraj, keunes, thejeshgn | | Ko | changwoo, keunes, libliboom | -| Lithuanian | keunes, naglis | +| Lithuanian | keunes, naglis, Sharper | | Macedonian | krisfremen | | Malayalam | joice, keunes, rashivkp | | Norwegian Bokmål (nb_NO) | abstrakct, ahysing, bablecopherye, corkie, forteller, heraldo, jakobkg, keunes, kongk, sevenmaster, timbast | | Persian | ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, keunes, sinamoghaddas | -| Polish (pl_PL) | befeleme, hiro2020, Iwangelion, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle | +| Polish (pl_PL) | befeleme, hiro2020, Iwangelion, kamila.miodek1991, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle | | Portuguese | emansije, keunes, smarquespt | -| Portuguese (pt_BR) | alexupits, alysonborges, andersonvom, arua, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam | +| Portuguese (pt_BR) | alexupits, alysonborges, andersonvom, aracnus, arua, bandreghetti, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam | | Romanian (ro_RO) | corneliu.e, fuzzmz, keunes, ralienpp | -| Russian (ru_RU) | ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, un_logic, Vladryyu, whereisthetea | -| Slovak | ati3, keunes, marulinko, tiborepcek | -| Slovenian (sl_SI) | keunes, panter23 | -| Spanish | AleksSyntek, andersonvom, andrespelaezp, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo | -| Swahili (macrolanguage) | keunes, kmtra | +| Russian (ru_RU) | ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, tepxd, un_logic, Vladryyu, whereisthetea | +| Slovak | ati3, jose1711, keunes, marulinko, tiborepcek | +| Slovenian (sl_SI) | asovic, keunes, panter23, trus2 | +| Spanish | AleksSyntek, andersonvom, andrespelaezp, Atreyu94, CaeM0R, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo | +| Swahili (macrolanguage) | 1silvester, keunes, kmtra | | Swedish (sv_SE) | bpnilsson, keunes, nilso, TwoD | | Telugu | keunes, veeven | | Turkish | AhmedDuran, brsata, Erdy, keunes, overbite, Slsdem | @@ -18,7 +18,7 @@ You can use the [AntennaPod Forum](https://forum.antennapod.org/) for discussion Bug reports and feature requests can be submitted [here](https://github.com/AntennaPod/AntennaPod/issues) (please read the [instructions](https://github.com/AntennaPod/AntennaPod/blob/master/CONTRIBUTING.md) on how to report a bug and how to submit a feature request first!). ## Help to test AntennaPod -AntennaPod has many users and we don't want them to run into trouble when we add a new feature. It's important that we have a significant group test our app, so that we know all possible combinations of phones, Android versions and use cases work as expected. Check out our wiki on how to join our [Alpha and Beta testing programmes](https://github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod)! +AntennaPod has many users and we don't want them to run into trouble when we add a new feature. It's important that we have a significant group test our app, so that we know all possible combinations of phones, Android versions and use cases work as expected. Check out our wiki on how to join our [Beta testing program](https://antennapod.org/documentation/general/beta)! If a bug is reported during the beta period, chances are high that it will be fixed before the stable version. If it is reported later, fixing might take another full beta cycle. So definitely let us know if something is not right. ## License diff --git a/app/build.gradle b/app/build.gradle index 1d2456dd7..81325a1bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,11 +19,10 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion // Version code schema: - // "1.2.3-SNAPSHOT" -> 1020300 - // "1.2.3-RC4" -> 1020304 + // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 2020000 - versionName "2.2.0" + versionCode 2020001 + versionName "2.2.0-beta1" multiDexEnabled false vectorDrawables.useSupportLibrary true @@ -156,14 +155,9 @@ android { } dependencies { - freeImplementation project(":core") - // free build hack: skip some dependencies - if (!doFreeBuild()) { - playImplementation project(":core") - implementation 'com.google.android.play:core:1.8.0' - } else { - System.out.println("app: free build hack, skipping some dependencies") - } + implementation project(":core") + implementation project(':ui:app-start-intent') + implementation project(':ui:common') annotationProcessor "androidx.annotation:annotation:$annotationVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion" @@ -191,14 +185,15 @@ dependencies { implementation "com.joanzapata.iconify:android-iconify-fontawesome:$iconifyVersion" implementation "com.joanzapata.iconify:android-iconify-material:$iconifyVersion" - implementation 'com.yqritc:recyclerview-flexibledivider:1.4.0' implementation 'com.github.shts:TriangleLabelView:1.1.2' - implementation 'com.leinardi.android:speed-dial:3.1.1' + implementation 'com.github.leinardi:FloatingActionButtonSpeedDial:3.1.1' implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" implementation 'com.github.mfietz:fyydlin:v0.5.0' implementation 'com.github.ByteHamster:SearchPreference:v2.0.0' implementation 'com.github.skydoves:balloon:1.1.5' + // Non-free dependencies: + playImplementation 'com.google.android.play:core:1.8.0' compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion" diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java index 3c8c5d7f0..21498effd 100644 --- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java @@ -3,8 +3,10 @@ package de.test.antennapod; import android.content.Context; import android.content.Intent; import androidx.annotation.IdRes; +import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.preference.PreferenceManager; +import androidx.test.espresso.NoMatchingViewException; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.espresso.PerformException; import androidx.test.espresso.UiController; @@ -15,6 +17,9 @@ import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.espresso.util.HumanReadables; import androidx.test.espresso.util.TreeIterables; import android.view.View; + +import junit.framework.AssertionFailedError; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -33,6 +38,7 @@ import java.util.concurrent.TimeoutException; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; @@ -57,7 +63,7 @@ public class EspressoTestUtils { @Override public String getDescription() { - return "wait for a specific view for" + millis + " millis."; + return "wait for a specific view for " + millis + " millis."; } @Override @@ -88,6 +94,33 @@ public class EspressoTestUtils { } /** + * Wait until a certain view becomes visible, but at the longest until the timeout. + * Unlike {@link #waitForView(Matcher, long)} it doesn't stick to the initial root view. + * + * @param viewMatcher The view to wait for. + * @param timeoutMillis Maximum waiting period in milliseconds. + * @throws Exception Throws an Exception in case of a timeout. + */ + public static void waitForViewGlobally(@NonNull Matcher<View> viewMatcher, long timeoutMillis) throws Exception { + long startTime = System.currentTimeMillis(); + long endTime = startTime + timeoutMillis; + + do { + try { + onView(viewMatcher).check(matches(isDisplayed())); + // no Exception thrown -> check successful + return; + } catch (NoMatchingViewException | AssertionFailedError ignore) { + // check was not successful "not found" -> continue waiting + } + //noinspection BusyWait + Thread.sleep(50); + } while (System.currentTimeMillis() < endTime); + + throw new Exception("Timeout after " + timeoutMillis + " ms"); + } + + /** * Perform action of waiting for a specific view id. * https://stackoverflow.com/a/30338665/ * @param id The id of the child to click. @@ -113,7 +146,7 @@ public class EspressoTestUtils { } /** - * Clear all app databases + * Clear all app databases. */ public static void clearPreferences() { File root = InstrumentationRegistry.getInstrumentation().getTargetContext().getFilesDir().getParentFile(); diff --git a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java index 8c628efd5..e31838671 100644 --- a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java +++ b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java @@ -11,15 +11,11 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.List; - import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.ui.UITestUtils; @@ -70,7 +66,6 @@ public class ShareDialogTest { onView(withText(R.string.all_episodes_short_label)).perform(click()); Matcher<View> allEpisodesMatcher; - final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); allEpisodesMatcher = Matchers.allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2)); onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000)); onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, click())); diff --git a/app/src/androidTest/java/de/test/antennapod/feed/FeedItemTest.java b/app/src/androidTest/java/de/test/antennapod/feed/FeedItemTest.java deleted file mode 100644 index 0b9a67d0a..000000000 --- a/app/src/androidTest/java/de/test/antennapod/feed/FeedItemTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.test.antennapod.feed; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.FeedItem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -@SmallTest -public class FeedItemTest { - private static final String TEXT_LONG = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; - private static final String TEXT_SHORT = "Lorem ipsum"; - - /** - * If one of `description` or `content:encoded` is null, use the other one. - */ - @Test - public void testShownotesNullValues() throws Exception { - testShownotes(null, TEXT_LONG); - testShownotes(TEXT_LONG, null); - } - - /** - * If `description` is reasonably longer than `content:encoded`, use `description`. - */ - @Test - public void testShownotesLength() throws Exception { - testShownotes(TEXT_SHORT, TEXT_LONG); - testShownotes(TEXT_LONG, TEXT_SHORT); - } - - /** - * Checks if the shownotes equal TEXT_LONG, using the given `description` and `content:encoded` - * @param description Description of the feed item - * @param contentEncoded `content:encoded` of the feed item - */ - private void testShownotes(String description, String contentEncoded) throws Exception { - FeedItem item = new FeedItem(); - item.setDescription(description); - item.setContentEncoded(contentEncoded); - assertEquals(TEXT_LONG, item.loadShownotes().call()); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/AtomParserTest.java b/app/src/androidTest/java/de/test/antennapod/handler/AtomParserTest.java deleted file mode 100644 index de9f53ae2..000000000 --- a/app/src/androidTest/java/de/test/antennapod/handler/AtomParserTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.test.antennapod.handler; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.test.antennapod.util.syndication.feedgenerator.AtomGenerator; -import org.junit.Test; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for Atom feeds in FeedHandler. - */ -@SmallTest -public class AtomParserTest extends FeedParserTestBase { - @Test - public void testAtomBasic() throws Exception { - Feed f1 = createTestFeed(10, true); - Feed f2 = runFeedTest(f1, new AtomGenerator(), "UTF-8", 0); - feedValid(f1, f2, Feed.TYPE_ATOM1); - } - - @Test - public void testLogoWithWhitespace() throws Exception { - String logo = "https://example.com/image.png"; - Feed f1 = createTestFeed(0, false); - f1.setImageUrl(null); - Feed f2 = runFeedTest(f1, new AtomGenerator() { - @Override - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - xml.startTag(null, "logo"); - xml.text(" " + logo + "\n"); - xml.endTag(null, "logo"); - } - }, "UTF-8", 0); - assertEquals(logo, f2.getImageUrl()); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/FeedParserTestBase.java b/app/src/androidTest/java/de/test/antennapod/handler/FeedParserTestBase.java deleted file mode 100644 index 83f334633..000000000 --- a/app/src/androidTest/java/de/test/antennapod/handler/FeedParserTestBase.java +++ /dev/null @@ -1,154 +0,0 @@ -package de.test.antennapod.handler; - -import android.content.Context; -import androidx.test.platform.app.InstrumentationRegistry; -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.syndication.handler.FeedHandler; -import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; -import de.test.antennapod.util.syndication.feedgenerator.FeedGenerator; -import org.junit.After; -import org.junit.Before; -import org.xml.sax.SAXException; - -import javax.xml.parsers.ParserConfigurationException; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Tests for FeedHandler. - */ -public abstract class FeedParserTestBase { - private static final String FEEDS_DIR = "testfeeds"; - - private File file = null; - private OutputStream outputStream = null; - - @Before - public void setUp() throws Exception { - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - File destDir = context.getExternalFilesDir(FEEDS_DIR); - assertNotNull(destDir); - - file = new File(destDir, "feed.xml"); - file.delete(); - - assertNotNull(file); - assertFalse(file.exists()); - - outputStream = new FileOutputStream(file); - } - - - @After - public void tearDown() throws Exception { - file.delete(); - file = null; - - outputStream.close(); - outputStream = null; - } - - protected Feed runFeedTest(Feed feed, FeedGenerator g, String encoding, long flags) - throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException { - g.writeFeed(feed, outputStream, encoding, flags); - FeedHandler handler = new FeedHandler(); - Feed parsedFeed = new Feed(feed.getDownload_url(), feed.getLastUpdate()); - parsedFeed.setFile_url(file.getAbsolutePath()); - parsedFeed.setDownloaded(true); - handler.parseFeed(parsedFeed); - return parsedFeed; - } - - protected void feedValid(Feed feed, Feed parsedFeed, String feedType) { - assertEquals(feed.getTitle(), parsedFeed.getTitle()); - if (feedType.equals(Feed.TYPE_ATOM1)) { - assertEquals(feed.getFeedIdentifier(), parsedFeed.getFeedIdentifier()); - } else { - assertEquals(feed.getLanguage(), parsedFeed.getLanguage()); - } - - assertEquals(feed.getLink(), parsedFeed.getLink()); - assertEquals(feed.getDescription(), parsedFeed.getDescription()); - assertEquals(feed.getPaymentLink(), parsedFeed.getPaymentLink()); - assertEquals(feed.getImageUrl(), parsedFeed.getImageUrl()); - - if (feed.getItems() != null) { - assertNotNull(parsedFeed.getItems()); - assertEquals(feed.getItems().size(), parsedFeed.getItems().size()); - - for (int i = 0; i < feed.getItems().size(); i++) { - FeedItem item = feed.getItems().get(i); - FeedItem parsedItem = parsedFeed.getItems().get(i); - - if (item.getItemIdentifier() != null) { - assertEquals(item.getItemIdentifier(), parsedItem.getItemIdentifier()); - } - assertEquals(item.getTitle(), parsedItem.getTitle()); - assertEquals(item.getDescription(), parsedItem.getDescription()); - assertEquals(item.getContentEncoded(), parsedItem.getContentEncoded()); - assertEquals(item.getLink(), parsedItem.getLink()); - assertEquals(item.getPubDate().getTime(), parsedItem.getPubDate().getTime()); - assertEquals(item.getPaymentLink(), parsedItem.getPaymentLink()); - - if (item.hasMedia()) { - assertTrue(parsedItem.hasMedia()); - FeedMedia media = item.getMedia(); - FeedMedia parsedMedia = parsedItem.getMedia(); - - assertEquals(media.getDownload_url(), parsedMedia.getDownload_url()); - assertEquals(media.getSize(), parsedMedia.getSize()); - assertEquals(media.getMime_type(), parsedMedia.getMime_type()); - } - - assertEquals(feed.getImageUrl(), item.getImageLocation()); - - if (item.getChapters() != null) { - assertNotNull(parsedItem.getChapters()); - assertEquals(item.getChapters().size(), parsedItem.getChapters().size()); - List<Chapter> chapters = item.getChapters(); - List<Chapter> parsedChapters = parsedItem.getChapters(); - for (int j = 0; j < chapters.size(); j++) { - Chapter chapter = chapters.get(j); - Chapter parsedChapter = parsedChapters.get(j); - - assertEquals(chapter.getTitle(), parsedChapter.getTitle()); - assertEquals(chapter.getLink(), parsedChapter.getLink()); - } - } - } - } - } - - protected Feed createTestFeed(int numItems, boolean withFeedMedia) { - Feed feed = new Feed(0, null, "title", "http://example.com", "This is the description", - "http://example.com/payment", "Daniel", "en", null, "http://example.com/feed", - "http://example.com/picture", file.getAbsolutePath(), "http://example.com/feed", true); - feed.setItems(new ArrayList<>()); - - for (int i = 0; i < numItems; i++) { - FeedItem item = new FeedItem(0, "item-" + i, "http://example.com/item-" + i, - "http://example.com/items/" + i, new Date(i * 60000), FeedItem.UNPLAYED, feed); - feed.getItems().add(item); - if (withFeedMedia) { - item.setMedia(new FeedMedia(0, item, 4711, 0, 1024 * 1024, "audio/mp3", null, - "http://example.com/media-" + i, false, null, 0, 0)); - } - } - - return feed; - } - -} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/RssParserTest.java b/app/src/androidTest/java/de/test/antennapod/handler/RssParserTest.java deleted file mode 100644 index c2e319233..000000000 --- a/app/src/androidTest/java/de/test/antennapod/handler/RssParserTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package de.test.antennapod.handler; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.syndication.namespace.NSMedia; -import de.test.antennapod.util.syndication.feedgenerator.Rss2Generator; -import org.junit.Test; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for RSS feeds in FeedHandler. - */ -@SmallTest -public class RssParserTest extends FeedParserTestBase { - @Test - public void testRss2Basic() throws Exception { - Feed f1 = createTestFeed(10, true); - Feed f2 = runFeedTest(f1, new Rss2Generator(), "UTF-8", Rss2Generator.FEATURE_WRITE_GUID); - feedValid(f1, f2, Feed.TYPE_RSS2); - } - - @Test - public void testImageWithWhitespace() throws Exception { - String image = "https://example.com/image.png"; - Feed f1 = createTestFeed(0, false); - f1.setImageUrl(null); - Feed f2 = runFeedTest(f1, new Rss2Generator() { - @Override - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - xml.startTag(null, "image"); - xml.startTag(null, "url"); - xml.text(" " + image + "\n"); - xml.endTag(null, "url"); - xml.endTag(null, "image"); - } - }, "UTF-8", 0); - assertEquals(image, f2.getImageUrl()); - } - - @Test - public void testMediaContentMime() throws Exception { - Feed f1 = createTestFeed(0, false); - f1.setImageUrl(null); - Feed f2 = runFeedTest(f1, new Rss2Generator() { - @Override - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - xml.setPrefix(NSMedia.NSTAG, NSMedia.NSURI); - xml.startTag(null, "item"); - xml.startTag(NSMedia.NSURI, "content"); - xml.attribute(null, "url", "https://www.example.com/file.mp4"); - xml.attribute(null, "medium", "video"); - xml.endTag(NSMedia.NSURI, "content"); - xml.endTag(null, "item"); - } - }, "UTF-8", 0); - assertEquals(MediaType.VIDEO, f2.getItems().get(0).getMedia().getMediaType()); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java index 419cf2096..e16451763 100644 --- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java @@ -10,6 +10,7 @@ import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.junit.After; @@ -107,7 +108,12 @@ public class PlaybackTest { } private void setupPlaybackController() { - controller = new PlaybackController(activityTestRule.getActivity()); + controller = new PlaybackController(activityTestRule.getActivity()) { + @Override + public void loadMediaInfo() { + // Do nothing + } + }; controller.init(); } @@ -252,7 +258,7 @@ public class PlaybackTest { onView(isRoot()).perform(waitForView(withText(R.string.all_episodes_short_label), 1000)); onView(withText(R.string.all_episodes_short_label)).perform(click()); - final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); + final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); Matcher<View> allEpisodesMatcher = allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2)); onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000)); onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, clickChildViewWithId(R.id.secondaryActionButton))); @@ -287,7 +293,7 @@ public class PlaybackTest { uiTestUtils.addLocalFeedData(true); DBWriter.clearQueue().get(); activityTestRule.launchActivity(new Intent()); - final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); + final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); startLocalPlayback(); FeedMedia media = episodes.get(0).getMedia(); diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java index f039c8bdf..ddd4fe899 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java @@ -6,6 +6,7 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.filters.LargeTest; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; +import de.danoeh.antennapod.core.widget.WidgetUpdater; import org.awaitility.Awaitility; import org.greenrobot.eventbus.EventBus; import org.junit.After; @@ -187,8 +188,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override @@ -248,8 +249,9 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { + public WidgetUpdater.WidgetState requestWidgetState() { countDownLatch.countDown(); + return null; } @Override @@ -348,8 +350,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override @@ -391,8 +393,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override @@ -449,8 +451,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override diff --git a/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java b/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java index 1d2e3d9e8..e74cf49b7 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java @@ -3,13 +3,13 @@ package de.test.antennapod.storage; import android.content.Context; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; -import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.ui.UITestUtils; @@ -29,8 +29,7 @@ public class AutoDownloadTest { private Context context; private UITestUtils stubFeedsServer; - - private AutomaticDownloadAlgorithm automaticDownloadAlgorithmOrig; + private StubDownloadAlgorithm stubDownloadAlgorithm; @Before public void setUp() throws Exception { @@ -39,16 +38,19 @@ public class AutoDownloadTest { stubFeedsServer = new UITestUtils(context); stubFeedsServer.setup(); - automaticDownloadAlgorithmOrig = ClientConfig.automaticDownloadAlgorithm; - EspressoTestUtils.clearPreferences(); EspressoTestUtils.clearDatabase(); UserPreferences.setAllowMobileStreaming(true); + + // Setup: enable automatic download + // it is not needed, as the actual automatic download is stubbed. + stubDownloadAlgorithm = new StubDownloadAlgorithm(); + DBTasks.setDownloadAlgorithm(stubDownloadAlgorithm); } @After public void tearDown() throws Exception { - ClientConfig.automaticDownloadAlgorithm = automaticDownloadAlgorithmOrig; + DBTasks.setDownloadAlgorithm(new AutomaticDownloadAlgorithm()); EspressoTestUtils.tryKillPlaybackService(); stubFeedsServer.tearDown(); } @@ -74,11 +76,6 @@ public class AutoDownloadTest { FeedItem item0 = queue.get(0); FeedItem item1 = queue.get(1); - // Setup: enable automatic download - // it is not needed, as the actual automatic download is stubbed. - StubDownloadAlgorithm stubDownloadAlgorithm = new StubDownloadAlgorithm(); - ClientConfig.automaticDownloadAlgorithm = stubDownloadAlgorithm; - // Actual test // Play the first one in the queue playEpisode(item0); @@ -92,11 +89,10 @@ public class AutoDownloadTest { } catch (ConditionTimeoutException cte) { long actual = stubDownloadAlgorithm.getCurrentlyPlayingAtDownload(); fail("when auto download is triggered, the next episode should be playing: (" - + item1.getId() + ", " + item1.getTitle() + ") . " + + item1.getId() + ", " + item1.getTitle() + ") . " + "Actual playing: (" + actual + ")" ); } - } private void playEpisode(@NonNull FeedItem item) { @@ -111,7 +107,7 @@ public class AutoDownloadTest { .until(() -> item.getMedia().getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId()); } - private static class StubDownloadAlgorithm implements AutomaticDownloadAlgorithm { + private static class StubDownloadAlgorithm extends AutomaticDownloadAlgorithm { private long currentlyPlaying = -1; @Override diff --git a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java index 3f7ebb48b..417a78f02 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java @@ -2,16 +2,14 @@ package de.test.antennapod.ui; import android.app.Activity; import android.content.Intent; -import androidx.test.platform.app.InstrumentationRegistry; + import androidx.test.espresso.Espresso; import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + import com.robotium.solo.Solo; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.test.antennapod.EspressoTestUtils; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -20,6 +18,12 @@ import org.junit.runner.RunWith; import java.io.IOException; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.test.antennapod.EspressoTestUtils; + import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.replaceText; @@ -28,18 +32,17 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.contrib.ActivityResultMatchers.hasResultCode; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isRoot; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static de.test.antennapod.EspressoTestUtils.clickPreference; import static de.test.antennapod.EspressoTestUtils.openNavDrawer; -import static de.test.antennapod.EspressoTestUtils.waitForView; +import static de.test.antennapod.EspressoTestUtils.waitForViewGlobally; import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; /** - * User interface tests for MainActivity + * User interface tests for MainActivity. */ @RunWith(AndroidJUnit4.class) public class MainActivityTest { @@ -48,19 +51,19 @@ public class MainActivityTest { private UITestUtils uiTestUtils; @Rule - public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(MainActivity.class, false, false); + public IntentsTestRule<MainActivity> activityRule = new IntentsTestRule<>(MainActivity.class, false, false); @Before public void setUp() throws IOException { EspressoTestUtils.clearPreferences(); EspressoTestUtils.clearDatabase(); - mActivityRule.launchActivity(new Intent()); + activityRule.launchActivity(new Intent()); uiTestUtils = new UITestUtils(InstrumentationRegistry.getInstrumentation().getTargetContext()); uiTestUtils.setup(); - solo = new Solo(InstrumentationRegistry.getInstrumentation(), mActivityRule.getActivity()); + solo = new Solo(InstrumentationRegistry.getInstrumentation(), activityRule.getActivity()); } @After @@ -71,6 +74,7 @@ public class MainActivityTest { @Test public void testAddFeed() throws Exception { + // connect to podcast feed uiTestUtils.addHostedFeedData(); final Feed feed = uiTestUtils.hostedFeeds.get(0); openNavDrawer(); @@ -78,9 +82,14 @@ public class MainActivityTest { onView(withId(R.id.addViaUrlButton)).perform(scrollTo(), click()); onView(withId(R.id.urlEditText)).perform(replaceText(feed.getDownload_url())); onView(withText(R.string.confirm_label)).perform(scrollTo(), click()); + + // subscribe podcast Espresso.closeSoftKeyboard(); + waitForViewGlobally(withText(R.string.subscribe_label), 15000); onView(withText(R.string.subscribe_label)).perform(click()); - onView(isRoot()).perform(waitForView(withId(R.id.butShowSettings), 5000)); + + // wait for podcast feed item list + waitForViewGlobally(withId(R.id.butShowSettings), 15000); } @Test @@ -100,7 +109,7 @@ public class MainActivityTest { onView(allOf(withId(R.id.toolbar), isDisplayed())).check( matches(hasDescendant(withText(R.string.subscriptions_label)))); solo.goBack(); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } @Test @@ -113,7 +122,7 @@ public class MainActivityTest { solo.goBackToActivity(MainActivity.class.getSimpleName()); solo.goBack(); solo.goBack(); - assertTrue(((MainActivity)solo.getCurrentActivity()).isDrawerOpen()); + assertTrue(((MainActivity) solo.getCurrentActivity()).isDrawerOpen()); } @Test @@ -127,7 +136,7 @@ public class MainActivityTest { solo.goBack(); solo.goBack(); solo.goBack(); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } @Test @@ -142,7 +151,7 @@ public class MainActivityTest { solo.goBack(); onView(withText(R.string.yes)).perform(click()); Thread.sleep(100); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } @Test @@ -155,6 +164,6 @@ public class MainActivityTest { solo.goBackToActivity(MainActivity.class.getSimpleName()); solo.goBack(); solo.goBack(); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java index 3cdb09605..bba546a88 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java @@ -15,6 +15,7 @@ import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.ExceptFavoriteCleanupAlgorithm; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; @@ -372,6 +373,17 @@ public class PreferencesTest { } @Test + public void testEpisodeCleanupFavoriteOnly() { + clickPreference(R.string.network_pref); + onView(withText(R.string.pref_automatic_download_title)).perform(click()); + onView(withText(R.string.pref_episode_cleanup_title)).perform(click()); + onView(isRoot()).perform(waitForView(withText(R.string.episode_cleanup_except_favorite_removal), 1000)); + onView(withText(R.string.episode_cleanup_except_favorite_removal)).perform(click()); + Awaitility.await().atMost(1000, MILLISECONDS) + .until(() -> UserPreferences.getEpisodeCleanupAlgorithm() instanceof ExceptFavoriteCleanupAlgorithm); + } + + @Test public void testEpisodeCleanupQueueOnly() { clickPreference(R.string.network_pref); onView(withText(R.string.pref_automatic_download_title)).perform(click()); diff --git a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java b/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java index 904e17ebf..8027b7dc2 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java @@ -15,6 +15,7 @@ import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.IgnoreOnCi; import org.awaitility.Awaitility; @@ -71,8 +72,13 @@ public class SpeedChangeTest { UserPreferences.setPlaybackSpeedArray(Arrays.asList(1.0f, 2.0f, 3.0f)); EspressoTestUtils.tryKillPlaybackService(); - activityRule.launchActivity(new Intent().putExtra(MainActivity.EXTRA_OPEN_PLAYER, true)); - controller = new PlaybackController(activityRule.getActivity()); + activityRule.launchActivity(new Intent().putExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, true)); + controller = new PlaybackController(activityRule.getActivity()) { + @Override + public void loadMediaInfo() { + // Do nothing + } + }; controller.init(); controller.getMedia(); // To load media } diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java deleted file mode 100644 index c80e3bbb1..000000000 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java +++ /dev/null @@ -1,131 +0,0 @@ -package de.test.antennapod.util.syndication.feedgenerator; - -import android.util.Xml; - -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; -import java.io.OutputStream; - -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.util.DateUtils; - -/** - * Creates Atom feeds. See FeedGenerator for more information. - */ -public class AtomGenerator implements FeedGenerator { - - private static final String NS_ATOM = "http://www.w3.org/2005/Atom"; - - private static final long FEATURE_USE_RFC3339LOCAL = 1; - - @Override - public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException { - if (feed == null) throw new IllegalArgumentException("feed = null"); - if (outputStream == null) throw new IllegalArgumentException("outputStream = null"); - if (encoding == null) throw new IllegalArgumentException("encoding = null"); - - XmlSerializer xml = Xml.newSerializer(); - xml.setOutput(outputStream, encoding); - xml.startDocument(encoding, null); - - xml.startTag(null, "feed"); - xml.attribute(null, "xmlns", NS_ATOM); - - // Write Feed data - if (feed.getIdentifyingValue() != null) { - xml.startTag(null, "id"); - xml.text(feed.getIdentifyingValue()); - xml.endTag(null, "id"); - } - if (feed.getTitle() != null) { - xml.startTag(null, "title"); - xml.text(feed.getTitle()); - xml.endTag(null, "title"); - } - if (feed.getLink() != null) { - xml.startTag(null, "link"); - xml.attribute(null, "rel", "alternate"); - xml.attribute(null, "href", feed.getLink()); - xml.endTag(null, "link"); - } - if (feed.getDescription() != null) { - xml.startTag(null, "subtitle"); - xml.text(feed.getDescription()); - xml.endTag(null, "subtitle"); - } - if (feed.getImageUrl() != null) { - xml.startTag(null, "logo"); - xml.text(feed.getImageUrl()); - xml.endTag(null, "logo"); - } - - if (feed.getPaymentLink() != null) { - GeneratorUtil.addPaymentLink(xml, feed.getPaymentLink(), false); - } - - // Write FeedItem data - if (feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - xml.startTag(null, "entry"); - - if (item.getIdentifyingValue() != null) { - xml.startTag(null, "id"); - xml.text(item.getIdentifyingValue()); - xml.endTag(null, "id"); - } - if (item.getTitle() != null) { - xml.startTag(null, "title"); - xml.text(item.getTitle()); - xml.endTag(null, "title"); - } - if (item.getLink() != null) { - xml.startTag(null, "link"); - xml.attribute(null, "rel", "alternate"); - xml.attribute(null, "href", item.getLink()); - xml.endTag(null, "link"); - } - if (item.getPubDate() != null) { - xml.startTag(null, "published"); - if ((flags & FEATURE_USE_RFC3339LOCAL) != 0) { - xml.text(DateUtils.formatRFC3339Local(item.getPubDate())); - } else { - xml.text(DateUtils.formatRFC3339UTC(item.getPubDate())); - } - xml.endTag(null, "published"); - } - if (item.getDescription() != null) { - xml.startTag(null, "content"); - xml.text(item.getDescription()); - xml.endTag(null, "content"); - } - if (item.getMedia() != null) { - FeedMedia media = item.getMedia(); - xml.startTag(null, "link"); - xml.attribute(null, "rel", "enclosure"); - xml.attribute(null, "href", media.getDownload_url()); - xml.attribute(null, "type", media.getMime_type()); - xml.attribute(null, "length", String.valueOf(media.getSize())); - xml.endTag(null, "link"); - } - - if (item.getPaymentLink() != null) { - GeneratorUtil.addPaymentLink(xml, item.getPaymentLink(), false); - } - - xml.endTag(null, "entry"); - } - } - - writeAdditionalAttributes(xml); - - xml.endTag(null, "feed"); - xml.endDocument(); - } - - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fb205b1c3..697624337 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,7 +60,8 @@ <activity android:name=".activity.SplashActivity" android:label="@string/app_name" - android:configChanges="keyboardHidden|orientation|screenSize"> + android:configChanges="keyboardHidden|orientation|screenSize" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> @@ -83,11 +84,39 @@ android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|density|uiMode|keyboard|navigation" android:windowSoftInputMode="stateAlwaysHidden" android:launchMode="singleTask" - android:label="@string/app_name"> + android:label="@string/app_name" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + + <data + android:host="antennapod.org" + android:pathPrefix="/deeplink/main" + android:scheme="https" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + + <data + android:host="antennapod.org" + android:pathPrefix="/deeplink/search" + android:scheme="https" /> + </intent-filter> + <intent-filter> + <action android:name="de.danoeh.antennapod.intents.MAIN_ACTIVITY" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> </activity> <activity android:name=".activity.DownloadAuthenticationActivity" + android:theme="@style/Theme.AntennaPod.Dark.Translucent" android:launchMode="singleInstance"/> <activity @@ -101,7 +130,8 @@ <activity android:name=".activity.WidgetConfigActivity" - android:label="@string/widget_settings"> + android:label="@string/widget_settings" + android:exported="true"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_CONFIGUR"/> </intent-filter> @@ -114,21 +144,21 @@ android:exported="false"> </service> - <receiver android:name=".core.receiver.PlayerWidget"> + <receiver + android:name=".core.receiver.PlayerWidget" + android:exported="true"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> </intent-filter> <intent-filter> <action android:name="de.danoeh.antennapod.FORCE_WIDGET_UPDATE"/> </intent-filter> - - <meta-data - android:name="android.appwidget.provider" - android:resource="@xml/player_widget_info"/> - <intent-filter> <action android:name="de.danoeh.antennapod.STOP_WIDGET_UPDATE"/> </intent-filter> + <meta-data + android:name="android.appwidget.provider" + android:resource="@xml/player_widget_info"/> </receiver> <activity android:name=".activity.StorageErrorActivity"> @@ -136,7 +166,8 @@ <activity android:name=".activity.OpmlImportActivity" android:configChanges="keyboardHidden|orientation|screenSize" - android:label="@string/opml_import_label"> + android:label="@string/opml_import_label" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW"/> @@ -185,18 +216,14 @@ android:name=".activity.VideoplayerActivity" android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize" android:supportsPictureInPicture="true" - android:screenOrientation="sensorLandscape"> + android:screenOrientation="sensorLandscape" + android:exported="false"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="de.danoeh.antennapod.activity.MainActivity"/> <intent-filter> - <action android:name="android.intent.action.VIEW"/> - - <category android:name="android.intent.category.DEFAULT"/> - <category android:name="android.intent.category.BROWSABLE"/> - - <data android:scheme="file"/> - <data android:mimeType="video/*"/> + <action android:name="de.danoeh.antennapod.intents.VIDEO_PLAYER" /> + <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> @@ -204,7 +231,8 @@ android:name=".activity.OnlineFeedViewActivity" android:configChanges="orientation|screenSize" android:theme="@style/Theme.AntennaPod.Dark.Translucent" - android:label="@string/add_feed_label"> + android:label="@string/add_feed_label" + android:exported="true"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="de.danoeh.antennapod.activity.MainActivity"/> @@ -292,33 +320,26 @@ </activity> - <activity - android:name=".activity.gpoddernet.GpodnetAuthenticationActivity" - android:configChanges="orientation" - android:label="@string/gpodnet_auth_label"> - <intent-filter> - <action android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"/> - <category android:name="android.intent.category.DEFAULT"/> - </intent-filter> - <meta-data - android:name="android.support.PARENT_ACTIVITY" - android:value="de.danoeh.antennapod.activity.PreferenceActivity"/> - </activity> - - <receiver android:name=".receiver.ConnectivityActionReceiver"> + <receiver + android:name=".receiver.ConnectivityActionReceiver" + android:exported="true"> <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> </intent-filter> </receiver> - <receiver android:name=".receiver.PowerConnectionReceiver"> + <receiver + android:name=".receiver.PowerConnectionReceiver" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/> <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/> </intent-filter> </receiver> - <receiver android:name=".receiver.SPAReceiver"> + <receiver + android:name=".receiver.SPAReceiver" + android:exported="true"> <intent-filter> <action android:name="de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"/> </intent-filter> @@ -333,6 +354,10 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider> + + <meta-data + android:name="com.google.android.actions" + android:resource="@xml/actions" /> </application> </manifest> diff --git a/app/src/main/assets/developers.csv b/app/src/main/assets/developers.csv index a2c54723d..77269ba97 100644 --- a/app/src/main/assets/developers.csv +++ b/app/src/main/assets/developers.csv @@ -4,14 +4,14 @@ mfietz;6860662;Maintainer (retired) TomHennen;5216560;Maintainer (retired) orionlee;250644;Contributor domingos86;9538859;Contributor +damoasda;46045854;Contributor tonytamsf;149837;Contributor andersonvom;69922;Contributor -damoasda;46045854;Contributor TacoTheDank;32376686;Contributor shortspider;5712543;Contributor +spacecowboy;223655;Contributor ebraminio;833473;Contributor asdoi;36813904;Contributor -spacecowboy;223655;Contributor patheticpat;16046;Contributor brad;1614;Contributor Cj-Malone;10121513;Contributor @@ -44,6 +44,7 @@ mchelen;30691;Contributor dethstar;1239177;Contributor drabux;10663142;Contributor saqura;1935380;Contributor +binarytoto;75904760;Contributor bibz;5141956;Contributor hzulla;1705654;Contributor deandreamatias;21011641;Contributor @@ -65,6 +66,7 @@ HrBDev;25826502;Contributor HolgerJeromin;2410353;Contributor xisberto;1914956;Contributor jmue;898577;Contributor +jonasburian;15125616;Contributor katrinleinweber;9948149;Contributor LatinSuD;451487;Contributor 24hours;650407;Contributor @@ -74,25 +76,27 @@ archibishop;36948493;Contributor alifeflow;24603829;Contributor avirajrsingh;69088913;Contributor toggles;14695;Contributor +connectety;26038710;Contributor matdb;48329535;Contributor damlayildiz;56313500;Contributor kingargyle;177042;Contributor dsmith47;14109426;Contributor hannesaa2;18496079;Contributor jhunnius;9149031;Contributor +a1291762;327162;Contributor ShadowIce;59123;Contributor Niffler;8172446;Contributor raghulj;57007;Contributor raghulrm;5362986;Contributor mamehacker;16738348;Contributor skitt;2128935;Contributor +Thom-Merrilin;76849828;Contributor wseemann;2296196;Contributor markamaze;17114678;Contributor mohitshah3111999;42018918;Contributor moralesg;14352147;Contributor mr-intj;6268767;Contributor tuxayo;2678215;Contributor -schlch;56929215;Contributor alimemonzx;44647595;Contributor dev-darrell;52300159;Contributor jmdouglas;10855634;Contributor @@ -106,24 +110,27 @@ arantius;84729;Contributor BoJacobs;25435640;Contributor chetan882777;36985543;Contributor chrissicool;232590;Contributor +britiger;2057760;Contributor cszucko;1810383;Contributor CWftw;1498303;Contributor -connectety;26038710;Contributor danielm5;66779;Contributor ariedov;958646;Contributor brettle;118192;Contributor edwinhere;19705425;Contributor eirikv;4076243;Contributor eerden;277513;Contributor +Geist5000;37940313;Contributor jklippel;8657220;Contributor jannic;232606;Contributor Foso;5015532;Contributor Kaligule;3586246;Contributor kvithayathil;1056073;Contributor luiscruz;1080714;Contributor +MStrecke;5202211;Contributor mlasson;5814258;Contributor schwedenmut;9077622;Contributor M-arcel;56698158;Contributor +mgborowiec;29843126;Contributor msoose;30473690;Contributor mo;7117;Contributor mdeveloper20;2319126;Contributor @@ -138,6 +145,7 @@ ortylp;470439;Contributor ramzan;55637406;Contributor iamrichR;44210678;Contributor SamWhited;512573;Contributor +SebiderSushi;23618858;Contributor selivan;1208989;Contributor sonnayasomnambula;7716779;Contributor sethoscope;534043;Contributor @@ -148,14 +156,17 @@ vimsick;20211590;Contributor lyallemma;25173082;Contributor edent;837136;Contributor atrus6;357881;Contributor +timakro;8438790;Contributor heyyviv;56256802;Contributor waylife;3348620;Contributor +yarons;406826;Contributor amhokies;3124968;Contributor andrewc1;19559401;Contributor axq;5077221;Contributor -binarytoto;75904760;Contributor chrk2205;44704035;Contributor fossterer;4236021;Contributor lightonflux;1377943;Contributor minusf;3632883;Contributor +s3lph;5564491;Contributor +tamizh138;26201258;Contributor zawad2221;32180355;Contributor diff --git a/app/src/main/assets/licenses.xml b/app/src/main/assets/licenses.xml index b6e12cf54..aa0ad740b 100644 --- a/app/src/main/assets/licenses.xml +++ b/app/src/main/assets/licenses.xml @@ -91,12 +91,6 @@ license="Apache 2.0" licenseText="LICENSE_APACHE-2.0.txt" /> <library - name="RecyclerView-FlexibleDivider" - author="yqritc" - website="https://github.com/yqritc/RecyclerView-FlexibleDivider" - license="Apache 2.0" - licenseText="LICENSE_APACHE-2.0.txt" /> - <library name="RxAndroid" author="ReactiveX" website="https://github.com/ReactiveX/RxAndroid" diff --git a/app/src/main/assets/translators.csv b/app/src/main/assets/translators.csv index fddad789b..4c9dc0c8e 100644 --- a/app/src/main/assets/translators.csv +++ b/app/src/main/assets/translators.csv @@ -1,19 +1,19 @@ -Arabic;abuzar3.khalid, badarotti, keunes, nabilMaghura, rex07, shubbar +Arabic;abuzar3.khalid, badarotti, keunes, MustafaAlgurabi, nabilMaghura, rex07, shubbar Asturian (ast_ES);enolp Basque;gaztainalde, keunes, Osoitz, pospolos Breton;Belvar, keunes -Bulgarian;keunes, solusitor +Bulgarian;keunes, ma4ko, solusitor Catalan;carles.llacer, dvd1985, exort12, IvanAmarante, javiercoll, keunes, Kintu, lambdani, marcmetallextrem, xc70 Chinese (zh_CN);brnme, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, keunes, kyleehee, molisiye, owen8877, RainSlide, RangerNJU, Sak94664, spice2wolf, tupunco, wongsyrone, yangyang, yiqiok Chinese (zh_TW);bobchao, ijliao, keunes, mapobi, pggdt, ymhuang0808 -Czech (cs_CZ);anotheranonymoususer, elich, Hanzmeister, svetlemodry, Thomaash -Danish;JFreak, jhertel, keunes, SebastianKiwiDk, twikedk +Czech (cs_CZ);anotheranonymoususer, elich, Hanzmeister, md.share, svetlemodry, Thomaash +Danish;JFreak, jhertel, keunes, petterbejo, SebastianKiwiDk Dutch;e2jk, keunes, rwv, Vistaus Estonian;Eraser, keunes, mahfiaz Finnish;Ban3, keunes, Sahtor -French;ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, Poussinou, sterylmreep +French;ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, petterbejo, Poussinou, RomainTT, sterylmreep Galician;antiparvos, pikamoku, Raichely -German;_Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, mfietz, pudeeh, Quiss42, repat, tomte, tweimer, Willhelm, ypid +German;_Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, forght, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, max.wittig, mfietz, Michael_Strecke, petterbejo, pudeeh, Quiss42, repat, toaskoas, tomte, tweimer, Willhelm, ypid Modern Greek (1453-);AnimaRain, antonist, keunes, pavlosv Hebrew (he_IL);amir.dafnyman, E1i9, mongoose4004, pinkasey, rellieberman, Yaron Hindi (hi_IN);keunes, purple.coder, siddhusengar, thelazyoxymoron @@ -24,20 +24,20 @@ Italian (it_IT);aalex70, allin, alvami, Bonnee, dontknowcris, giuseppep, Guybrus Japanese;keunes, KotaKato, Naofumi, sh3llc4t, TranslatorG Kannada (kn_IN);chiraag.nataraj, keunes, thejeshgn Ko;changwoo, keunes, libliboom -Lithuanian;keunes, naglis +Lithuanian;keunes, naglis, Sharper Macedonian;krisfremen Malayalam;joice, keunes, rashivkp Norwegian Bokmål (nb_NO);abstrakct, ahysing, bablecopherye, corkie, forteller, heraldo, jakobkg, keunes, kongk, sevenmaster, timbast Persian;ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, keunes, sinamoghaddas -Polish (pl_PL);befeleme, hiro2020, Iwangelion, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle +Polish (pl_PL);befeleme, hiro2020, Iwangelion, kamila.miodek1991, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle Portuguese;emansije, keunes, smarquespt -Portuguese (pt_BR);alexupits, alysonborges, andersonvom, arua, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam +Portuguese (pt_BR);alexupits, alysonborges, andersonvom, aracnus, arua, bandreghetti, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam Romanian (ro_RO);corneliu.e, fuzzmz, keunes, ralienpp -Russian (ru_RU);ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, un_logic, Vladryyu, whereisthetea -Slovak;ati3, keunes, marulinko, tiborepcek -Slovenian (sl_SI);keunes, panter23 -Spanish;AleksSyntek, andersonvom, andrespelaezp, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo -Swahili (macrolanguage);keunes, kmtra +Russian (ru_RU);ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, tepxd, un_logic, Vladryyu, whereisthetea +Slovak;ati3, jose1711, keunes, marulinko, tiborepcek +Slovenian (sl_SI);asovic, keunes, panter23, trus2 +Spanish;AleksSyntek, andersonvom, andrespelaezp, Atreyu94, CaeM0R, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo +Swahili (macrolanguage);1silvester, keunes, kmtra Swedish (sv_SE);bpnilsson, keunes, nilso, TwoD Telugu;keunes, veeven Turkish;AhmedDuran, brsata, Erdy, keunes, overbite, Slsdem diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java index 912038e4c..0f1d38db6 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java @@ -1,95 +1,76 @@ package de.danoeh.antennapod.activity; -import android.app.Activity; -import android.content.Intent; import android.os.Bundle; - -import androidx.annotation.NonNull; +import android.text.TextUtils; import androidx.appcompat.app.AppCompatActivity; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; - -import org.apache.commons.lang3.Validate; - import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.dialog.AuthenticationDialog; +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import org.apache.commons.lang3.Validate; + /** * Shows a username and a password text field. * The activity MUST be started with the ARG_DOWNlOAD_REQUEST argument set to a non-null value. - * Other arguments are optional. - * The activity's result will be the same DownloadRequest with the entered username and password. */ public class DownloadAuthenticationActivity extends AppCompatActivity { /** - * The download request object that contains information about the resource that requires a username and a password + * The download request object that contains information about the resource that requires a username and a password. */ public static final String ARG_DOWNLOAD_REQUEST = "request"; - /** - * True if the request should be sent to the DownloadRequester when this activity is finished, false otherwise. - * The default value is false. - */ - public static final String ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL = "send_to_downloadrequester"; - - private static final String RESULT_REQUEST = "request"; - - private EditText etxtUsername; - private EditText etxtPassword; @Override protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getNoTitleTheme()); + setTheme(UserPreferences.getTranslucentTheme()); super.onCreate(savedInstanceState); - setContentView(R.layout.download_authentication_activity); - TextView txtvDescription = findViewById(R.id.txtvDescription); - etxtUsername = findViewById(R.id.etxtUsername); - etxtPassword = findViewById(R.id.etxtPassword); - Button butConfirm = findViewById(R.id.butConfirm); - Button butCancel = findViewById(R.id.butCancel); - Validate.isTrue(getIntent().hasExtra(ARG_DOWNLOAD_REQUEST), "Download request missing"); DownloadRequest request = getIntent().getParcelableExtra(ARG_DOWNLOAD_REQUEST); - boolean sendToDownloadRequester = getIntent().getBooleanExtra(ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, false); - - String newDescription = txtvDescription.getText() + ":\n\n" + request.getTitle(); - txtvDescription.setText(newDescription); - - if (savedInstanceState != null) { - etxtUsername.setText(savedInstanceState.getString("username")); - etxtPassword.setText(savedInstanceState.getString("password")); - } - butConfirm.setOnClickListener(v -> { - String username = etxtUsername.getText().toString(); - String password = etxtPassword.getText().toString(); - request.setUsername(username); - request.setPassword(password); - Intent result = new Intent(); - result.putExtra(RESULT_REQUEST, request); - setResult(Activity.RESULT_OK, result); - - if (sendToDownloadRequester) { - DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request); + new AuthenticationDialog(this, R.string.authentication_label, true, "", "") { + @Override + protected void onConfirmed(String username, String password) { + Completable.fromAction( + () -> { + request.setUsername(username); + request.setPassword(password); + + if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + long mediaId = request.getFeedfileId(); + FeedMedia media = DBReader.getFeedMedia(mediaId); + if (media != null) { + FeedPreferences preferences = media.getItem().getFeed().getPreferences(); + if (TextUtils.isEmpty(preferences.getPassword()) + || TextUtils.isEmpty(preferences.getUsername())) { + preferences.setUsername(username); + preferences.setPassword(password); + DBWriter.setFeedPreferences(preferences); + } + } + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request); + finish(); + }); } - finish(); - }); - - butCancel.setOnClickListener(v -> { - setResult(Activity.RESULT_CANCELED); - finish(); - }); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString("username", etxtUsername.getText().toString()); - outState.putString("password", etxtPassword.getText().toString()); + @Override + protected void onCancelled() { + finish(); + } + }.show(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index d1716e009..b5edcc878 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.media.AudioManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -40,7 +41,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.StorageUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.RatingDialog; import de.danoeh.antennapod.fragment.AddFeedFragment; @@ -51,9 +52,11 @@ import de.danoeh.antennapod.fragment.FeedItemlistFragment; import de.danoeh.antennapod.fragment.NavDrawerFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.fragment.SearchFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.fragment.TransitionEffect; import de.danoeh.antennapod.preferences.PreferenceUpgrader; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.view.LockableBottomSheetBehavior; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; @@ -75,7 +78,6 @@ public class MainActivity extends CastEnabledActivity { public static final String EXTRA_FRAGMENT_TAG = "fragment_tag"; public static final String EXTRA_FRAGMENT_ARGS = "fragment_args"; public static final String EXTRA_FEED_ID = "fragment_feed_id"; - public static final String EXTRA_OPEN_PLAYER = "open_player"; public static final String EXTRA_REFRESH_ON_START = "refresh_on_start"; public static final String EXTRA_STARTED_FROM_SEARCH = "started_from_search"; public static final String KEY_GENERATED_VIEW_ID = "generated_view_id"; @@ -184,16 +186,16 @@ public class MainActivity extends CastEnabledActivity { } }; - public void setupToolbarToggle(@Nullable Toolbar toolbar) { + public void setupToolbarToggle(@NonNull Toolbar toolbar, boolean displayUpArrow) { if (drawerLayout != null) { // Tablet layout does not have a drawer drawerLayout.removeDrawerListener(drawerToggle); drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.drawer_open, R.string.drawer_close); drawerLayout.addDrawerListener(drawerToggle); drawerToggle.syncState(); - drawerToggle.setDrawerIndicatorEnabled(getSupportFragmentManager().getBackStackEntryCount() == 0); + drawerToggle.setDrawerIndicatorEnabled(!displayUpArrow); drawerToggle.setToolbarNavigationClickListener(v -> getSupportFragmentManager().popBackStack()); - } else if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + } else if (!displayUpArrow) { toolbar.setNavigationIcon(null); } else { toolbar.setNavigationIcon(ThemeUtils.getDrawableFromAttr(this, R.attr.homeAsUpIndicator)); @@ -508,9 +510,11 @@ public class MainActivity extends CastEnabledActivity { } } sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } else if (intent.getBooleanExtra(EXTRA_OPEN_PLAYER, false)) { + } else if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false)) { sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); bottomSheetCallback.onSlide(null, 1.0f); + } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { + handleDeeplink(intent.getData()); } // to avoid handling the intent twice when the configuration changes setIntent(new Intent(MainActivity.this, MainActivity.class)); @@ -520,6 +524,7 @@ public class MainActivity extends CastEnabledActivity { protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); + handleNavIntent(); } public Snackbar showSnackbarAbovePlayer(CharSequence text, int duration) { @@ -540,6 +545,59 @@ public class MainActivity extends CastEnabledActivity { return showSnackbarAbovePlayer(getResources().getText(text), duration); } + /** + * Handles the deep link incoming via App Actions. + * Performs an in-app search or opens the relevant feature of the app + * depending on the query. + * + * @param uri incoming deep link + */ + private void handleDeeplink(Uri uri) { + if (uri == null || uri.getPath() == null) { + return; + } + Log.d(TAG, "Handling deeplink: " + uri.toString()); + switch (uri.getPath()) { + case "/deeplink/search": + String query = uri.getQueryParameter("query"); + if (query == null) { + return; + } + + this.loadChildFragment(SearchFragment.newInstance(query)); + break; + case "/deeplink/main": + String feature = uri.getQueryParameter("page"); + if (feature == null) { + return; + } + switch (feature) { + case "DOWNLOADS": + loadFragment(DownloadsFragment.TAG, null); + break; + case "HISTORY": + loadFragment(PlaybackHistoryFragment.TAG, null); + break; + case "EPISODES": + loadFragment(EpisodesFragment.TAG, null); + break; + case "QUEUE": + loadFragment(QueueFragment.TAG, null); + break; + case "SUBSCRIPTIONS": + loadFragment(SubscriptionFragment.TAG, null); + break; + default: + showSnackbarAbovePlayer(getString(R.string.app_action_not_found, feature), + Snackbar.LENGTH_LONG); + return; + } + break; + default: + break; + } + } + //Hardware keyboard support @Override public boolean onKeyUp(int keyCode, KeyEvent event) { @@ -592,5 +650,4 @@ public class MainActivity extends CastEnabledActivity { } return super.onKeyUp(keyCode, event); } - } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java index deb2fe0db..56a66ba93 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -1,10 +1,8 @@ package de.danoeh.antennapod.activity; -import android.Manifest; import android.annotation.TargetApi; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.graphics.PixelFormat; import android.os.Build; import android.os.Bundle; @@ -17,7 +15,6 @@ import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; -import android.widget.Toast; import com.bumptech.glide.Glide; @@ -28,17 +25,16 @@ import org.greenrobot.eventbus.ThreadMode; import java.text.NumberFormat; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityCompat; +import androidx.cardview.widget.CardView; import androidx.core.app.ActivityOptionsCompat; -import androidx.core.content.ContextCompat; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; @@ -50,11 +46,9 @@ import de.danoeh.antennapod.core.util.ShareUtils; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; -import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.MediaPlayerError; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.dialog.PlaybackControlsDialog; import de.danoeh.antennapod.dialog.ShareDialog; import de.danoeh.antennapod.dialog.SkipPreferenceDialog; @@ -64,7 +58,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; - /** * Provides general features which are both needed for playing audio and video * files. @@ -72,9 +65,6 @@ import io.reactivex.schedulers.Schedulers; public abstract class MediaplayerActivity extends CastEnabledActivity implements OnSeekBarChangeListener { private static final String TAG = "MediaplayerActivity"; private static final String PREFS = "MediaPlayerActivityPreferences"; - private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; - private static final int REQUEST_CODE_STORAGE_PLAY_VIDEO = 42; - private static final int REQUEST_CODE_STORAGE_PLAY_AUDIO = 43; PlaybackController controller; @@ -87,6 +77,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements private ImageButton butFF; private TextView txtvFF; private ImageButton butSkip; + private CardView cardViewSeek; + private TextView txtvSeek; private boolean showTimeLeft = false; @@ -96,12 +88,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements private PlaybackController newPlaybackController() { return new PlaybackController(this) { - - @Override - public void setupGUI() { - MediaplayerActivity.this.setupGUI(); - } - @Override public void onPositionObserverUpdate() { MediaplayerActivity.this.onPositionObserverUpdate(); @@ -143,8 +129,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } @Override - public boolean loadMediaInfo() { - return MediaplayerActivity.this.loadMediaInfo(); + public void loadMediaInfo() { + MediaplayerActivity.this.loadMediaInfo(); } @Override @@ -467,17 +453,15 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements * to the PlaybackService to ensure that the activity has the right * FeedMedia object. */ - boolean loadMediaInfo() { + void loadMediaInfo() { Log.d(TAG, "loadMediaInfo()"); - if(controller == null || controller.getMedia() == null) { - return false; + if (controller == null || controller.getMedia() == null) { + return; } - SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); onPositionObserverUpdate(); checkFavorite(); updatePlaybackSpeedButton(); - return true; } void updatePlaybackSpeedButton() { @@ -492,9 +476,11 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements setContentView(getContentViewResourceId()); sbPosition = findViewById(R.id.sbPosition); txtvPosition = findViewById(R.id.txtvPosition); + cardViewSeek = findViewById(R.id.cardViewSeek); + txtvSeek = findViewById(R.id.txtvSeek); SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); Log.d("timeleft", showTimeLeft ? "true" : "false"); txtvLength = findViewById(R.id.txtvLength); if (txtvLength != null) { @@ -518,9 +504,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } txtvLength.setText(length); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft); - editor.apply(); + UserPreferences.setShowRemainTimeSetting(showTimeLeft); Log.d("timeleft on click", showTimeLeft ? "true" : "false"); }); } @@ -618,21 +602,21 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } if (fromUser) { prog = progress / ((float) seekBar.getMax()); - int duration = controller.getDuration(); TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int position = converter.convert((int) (prog * duration)); - txtvPosition.setText(Converter.getDurationStringLong(position)); - - if (showTimeLeft) { - int timeLeft = converter.convert(duration - (int) (prog * duration)); - txtvLength.setText("-" + Converter.getDurationStringLong(timeLeft)); - } + int position = converter.convert((int) (prog * controller.getDuration())); + txtvSeek.setText(Converter.getDurationStringLong(position)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { - + cardViewSeek.setScaleX(.8f); + cardViewSeek.setScaleY(.8f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(200) + .start(); } @Override @@ -640,6 +624,13 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements if (controller != null) { controller.seekTo((int) (prog * controller.getDuration())); } + cardViewSeek.setScaleX(1f); + cardViewSeek.setScaleY(1f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(200) + .start(); } private void checkFavorite() { @@ -663,50 +654,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements }, error -> Log.e(TAG, Log.getStackTraceString(error))); } - void playExternalMedia(Intent intent, MediaType type) { - if (intent == null || intent.getData() == null) { - return; - } - if (Build.VERSION.SDK_INT >= 23 - && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { - Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show(); - } - - int code = REQUEST_CODE_STORAGE_PLAY_AUDIO; - if (type == MediaType.VIDEO) { - code = REQUEST_CODE_STORAGE_PLAY_VIDEO; - } - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, code); - return; - } - - Log.d(TAG, "Received VIEW intent: " + intent.getData().getPath()); - ExternalMedia media = new ExternalMedia(intent.getData().getPath(), type); - - new PlaybackServiceStarter(this, media) - .callEvenIfRunning(true) - .startWhenPrepared(true) - .shouldStream(false) - .prepareImmediately(true) - .start(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, int[] grantResults) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - if (requestCode == REQUEST_CODE_STORAGE_PLAY_AUDIO) { - playExternalMedia(getIntent(), MediaType.AUDIO); - } else if (requestCode == REQUEST_CODE_STORAGE_PLAY_VIDEO) { - playExternalMedia(getIntent(), MediaType.VIDEO); - } - } else { - Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show(); - } - } - @Nullable private static FeedItem getFeedItem(@Nullable Playable playable) { if (playable instanceof FeedMedia) { diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index 18620a56a..a5883ca14 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -4,10 +4,14 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.LightingColorFilter; import android.os.Build; import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableString; import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.MenuItem; import android.view.View; @@ -15,6 +19,7 @@ import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -28,7 +33,6 @@ import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.FeedListUpdateEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.core.glide.ApGlideSettings; @@ -41,6 +45,7 @@ import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.service.download.HttpDownloader; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.syndication.handler.FeedHandler; @@ -49,7 +54,6 @@ import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeExceptio import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.FileNameGenerator; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.Optional; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.URLChecker; import de.danoeh.antennapod.core.util.playback.RemoteMedia; @@ -58,9 +62,11 @@ import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.databinding.OnlinefeedviewActivityBinding; import de.danoeh.antennapod.dialog.AuthenticationDialog; import de.danoeh.antennapod.discovery.PodcastSearcherRegistry; +import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; +import io.reactivex.observers.DisposableMaybeObserver; import io.reactivex.schedulers.Schedulers; import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.EventBus; @@ -87,6 +93,9 @@ public class OnlineFeedViewActivity extends AppCompatActivity { // Optional argument: specify a title for the actionbar. private static final int RESULT_ERROR = 2; private static final String TAG = "OnlineFeedViewActivity"; + private static final String PREFS = "OnlineFeedViewActivityPreferences"; + private static final String PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"; + private volatile List<Feed> feeds; private Feed feed; private String selectedDownloadUrl; @@ -248,7 +257,8 @@ public class OnlineFeedViewActivity extends AppCompatActivity { url = URLChecker.prepareURL(url); feed = new Feed(url, null); if (username != null && password != null) { - feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password)); + feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, + VolumeAdaptionSetting.OFF, username, password)); } String fileUrl = new File(getExternalCacheDir(), FileNameGenerator.generateFileName(feed.getDownload_url())).toString(); @@ -283,11 +293,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { dialog.show(); } } else { - String errorMsg = status.getReason().getErrorString(OnlineFeedViewActivity.this); - if (status.getReasonDetailed() != null) { - errorMsg += " (" + status.getReasonDetailed() + ")"; - } - showErrorDialog(errorMsg); + showErrorDialog(status.getReason().getErrorString(OnlineFeedViewActivity.this), status.getReasonDetailed()); } } @@ -316,37 +322,47 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } Log.d(TAG, "Parsing feed"); - parser = Observable.fromCallable(this::doParseFeed) + parser = Maybe.fromCallable(this::doParseFeed) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(optionalResult -> { - if(optionalResult.isPresent()) { - FeedHandlerResult result = optionalResult.get(); - beforeShowFeedInformation(result.feed); + .subscribeWith(new DisposableMaybeObserver<FeedHandlerResult>() { + @Override + public void onSuccess(@NonNull FeedHandlerResult result) { showFeedInformation(result.feed, result.alternateFeedUrls); } - }, error -> { - String errorMsg = DownloadError.ERROR_PARSER_EXCEPTION.getErrorString( - OnlineFeedViewActivity.this) + " (" + error.getMessage() + ")"; - showErrorDialog(errorMsg); - Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error)); + + @Override + public void onComplete() { + // Ignore null result: We showed the discovery dialog. + } + + @Override + public void onError(@NonNull Throwable error) { + showErrorDialog(error.getMessage(), ""); + Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error)); + } }); } - @NonNull - private Optional<FeedHandlerResult> doParseFeed() throws Exception { + /** + * Try to parse the feed. + * @return The FeedHandlerResult if successful. + * Null if unsuccessful but we started another attempt. + * @throws Exception If unsuccessful but we do not know a resolution. + */ + @Nullable + private FeedHandlerResult doParseFeed() throws Exception { FeedHandler handler = new FeedHandler(); try { - return Optional.of(handler.parseFeed(feed)); + return handler.parseFeed(feed); } catch (UnsupportedFeedtypeException e) { Log.d(TAG, "Unsupported feed type detected"); if ("html".equalsIgnoreCase(e.getRootElement())) { boolean dialogShown = showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url()); if (dialogShown) { - return Optional.empty(); + return null; // Should not display an error message } else { - Log.d(TAG, "Supplied feed is an HTML web page that has no references to any feed"); - throw e; + throw new UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html)); } } else { throw e; @@ -361,23 +377,6 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } /** - * Called after the feed has been downloaded and parsed and before showFeedInformation is called. - * This method is executed on a background thread - */ - private void beforeShowFeedInformation(Feed feed) { - Log.d(TAG, "Removing HTML from feed description"); - - feed.setDescription(HtmlToPlainText.getPlainText(feed.getDescription())); - - Log.d(TAG, "Removing HTML from shownotes"); - if (feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - item.setDescription(HtmlToPlainText.getPlainText(item.getDescription())); - } - } - } - - /** * Called when feed parsed successfully. * This method is executed on the GUI thread. */ @@ -420,7 +419,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { viewBinding.titleLabel.setText(feed.getTitle()); viewBinding.authorLabel.setText(feed.getAuthor()); - description.setText(feed.getDescription()); + description.setText(HtmlToPlainText.getPlainText(feed.getDescription())); viewBinding.subscribeButton.setOnClickListener(v -> { if (feedInFeedlist(feed)) { @@ -445,6 +444,11 @@ public class OnlineFeedViewActivity extends AppCompatActivity { IntentUtils.sendLocalBroadcast(this, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE); }); + if (UserPreferences.isEnableAutodownload()) { + SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); + viewBinding.autoDownloadCheckBox.setChecked(preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)); + } + final int MAX_LINES_COLLAPSED = 10; description.setMaxLines(MAX_LINES_COLLAPSED); description.setOnClickListener(v -> { @@ -511,10 +515,17 @@ public class OnlineFeedViewActivity extends AppCompatActivity { if (didPressSubscribe) { didPressSubscribe = false; if (UserPreferences.isEnableAutodownload()) { + boolean autoDownload = viewBinding.autoDownloadCheckBox.isChecked(); + Feed feed1 = DBReader.getFeed(getFeedId(feed)); FeedPreferences feedPreferences = feed1.getPreferences(); - feedPreferences.setAutoDownload(viewBinding.autoDownloadCheckBox.isChecked()); - feed1.savePreferences(); + feedPreferences.setAutoDownload(autoDownload); + DBWriter.setFeedPreferences(feedPreferences); + + SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload); + editor.apply(); } openFeed(); } @@ -553,12 +564,16 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } @UiThread - private void showErrorDialog(String errorMsg) { + private void showErrorDialog(String errorMsg, String details) { if (!isFinishing() && !isPaused) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.error_label); if (errorMsg != null) { - builder.setMessage(errorMsg); + String total = errorMsg + "\n\n" + details; + SpannableString errorMessage = new SpannableString(total); + errorMessage.setSpan(new ForegroundColorSpan(0x88888888), + errorMsg.length(), total.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setMessage(errorMessage); } else { builder.setMessage(R.string.download_error_error_unknown); } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java index 1c8619e99..15d0bec4a 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -17,7 +17,6 @@ import android.widget.ImageView; import androidx.core.view.WindowCompat; import androidx.appcompat.app.ActionBar; -import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.Menu; @@ -37,12 +36,12 @@ import java.lang.ref.WeakReference; import java.util.concurrent.atomic.AtomicBoolean; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.view.AspectRatioVideoView; /** @@ -88,9 +87,7 @@ public class VideoplayerActivity extends MediaplayerActivity { @Override protected void onResume() { super.onResume(); - if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { - playExternalMedia(getIntent(), MediaType.VIDEO); - } else if (PlaybackService.isCasting()) { + if (PlaybackService.isCasting()) { Intent intent = PlaybackService.getPlayerActivityIntent(this); if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) { destroyingDueToReload = true; @@ -135,17 +132,13 @@ public class VideoplayerActivity extends MediaplayerActivity { } @Override - protected boolean loadMediaInfo() { - if (!super.loadMediaInfo() || controller == null) { - return false; - } + protected void loadMediaInfo() { + super.loadMediaInfo(); Playable media = controller.getMedia(); if (media != null) { getSupportActionBar().setSubtitle(media.getEpisodeTitle()); getSupportActionBar().setTitle(media.getFeedTitle()); - return true; } - return false; } @Override @@ -347,7 +340,7 @@ public class VideoplayerActivity extends MediaplayerActivity { Log.d(TAG, "ReloadNotification received, switching to Castplayer now"); destroyingDueToReload = true; finish(); - startActivity(new Intent(this, MainActivity.class).putExtra(MainActivity.EXTRA_OPEN_PLAYER, true)); + new MainActivityStarter(this).withOpenPlayer().start(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java index 4805dba10..3020aba43 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java @@ -2,33 +2,34 @@ package de.danoeh.antennapod.activity; import android.Manifest; import android.app.WallpaperManager; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.widget.ImageView; -import androidx.appcompat.app.AppCompatActivity; - import android.appwidget.AppWidgetManager; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.view.View; -import android.widget.RelativeLayout; +import android.widget.CheckBox; +import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; - +import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.PlayerWidget; -import de.danoeh.antennapod.core.service.PlayerWidgetJobService; +import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService; public class WidgetConfigActivity extends AppCompatActivity { private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; private SeekBar opacitySeekBar; private TextView opacityTextView; - private RelativeLayout widgetPreview; + private View widgetPreview; + private CheckBox ckRewind; + private CheckBox ckFastForward; + private CheckBox ckSkip; @Override protected void onCreate(Bundle savedInstanceState) { @@ -73,6 +74,32 @@ public class WidgetConfigActivity extends AppCompatActivity { } }); + + widgetPreview.findViewById(R.id.txtNoPlaying).setVisibility(View.GONE); + TextView title = widgetPreview.findViewById(R.id.txtvTitle); + title.setVisibility(View.VISIBLE); + title.setText(R.string.app_name); + TextView progress = widgetPreview.findViewById(R.id.txtvProgress); + progress.setVisibility(View.VISIBLE); + progress.setText(R.string.position_default_label); + + ckRewind = findViewById(R.id.ckRewind); + ckRewind.setOnClickListener(v -> displayPreviewPanel()); + ckFastForward = findViewById(R.id.ckFastForward); + ckFastForward.setOnClickListener(v -> displayPreviewPanel()); + ckSkip = findViewById(R.id.ckSkip); + ckSkip.setOnClickListener(v -> displayPreviewPanel()); + } + + private void displayPreviewPanel() { + boolean showExtendedPreview = ckRewind.isChecked() || ckFastForward.isChecked() || ckSkip.isChecked(); + widgetPreview.findViewById(R.id.extendedButtonsContainer) + .setVisibility(showExtendedPreview ? View.VISIBLE : View.GONE); + widgetPreview.findViewById(R.id.butPlay).setVisibility(showExtendedPreview ? View.GONE : View.VISIBLE); + widgetPreview.findViewById(R.id.butFastForward) + .setVisibility(ckFastForward.isChecked() ? View.VISIBLE : View.GONE); + widgetPreview.findViewById(R.id.butSkip).setVisibility(ckSkip.isChecked() ? View.VISIBLE : View.GONE); + widgetPreview.findViewById(R.id.butRew).setVisibility(ckRewind.isChecked() ? View.VISIBLE : View.GONE); } private void displayDeviceBackground() { @@ -91,13 +118,16 @@ public class WidgetConfigActivity extends AppCompatActivity { SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor); + editor.putBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, ckSkip.isChecked()); + editor.putBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, ckRewind.isChecked()); + editor.putBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, ckFastForward.isChecked()); editor.apply(); Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); setResult(RESULT_OK, resultValue); finish(); - PlayerWidgetJobService.updateWidget(this); + WidgetUpdaterJobService.performBackgroundUpdate(this); } private int getColorWithAlpha(int color, int opacity) { diff --git a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java deleted file mode 100644 index cfd6ec702..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ /dev/null @@ -1,395 +0,0 @@ -package de.danoeh.antennapod.activity.gpoddernet; - -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ProgressBar; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.ViewFlipper; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; - -/** - * Guides the user through the authentication process - * Step 1: Request username and password from user - * Step 2: Choose device from a list of available devices or create a new one - * Step 3: Choose from a list of actions - */ -public class GpodnetAuthenticationActivity extends AppCompatActivity { - private static final String TAG = "GpodnetAuthActivity"; - - private ViewFlipper viewFlipper; - - private static final int STEP_DEFAULT = -1; - private static final int STEP_LOGIN = 0; - private static final int STEP_DEVICE = 1; - private static final int STEP_FINISH = 2; - - private int currentStep = -1; - - private GpodnetService service; - private volatile String username; - private volatile String password; - private volatile GpodnetDevice selectedDevice; - - private View[] views; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setContentView(R.layout.gpodnetauth_activity); - service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname()); - - viewFlipper = findViewById(R.id.viewflipper); - LayoutInflater inflater = (LayoutInflater) - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - views = new View[]{ - inflater.inflate(R.layout.gpodnetauth_credentials, viewFlipper, false), - inflater.inflate(R.layout.gpodnetauth_device, viewFlipper, false), - inflater.inflate(R.layout.gpodnetauth_finish, viewFlipper, false) - }; - for (View view : views) { - viewFlipper.addView(view); - } - advance(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void setupLoginView(View view) { - final EditText username = view.findViewById(R.id.etxtUsername); - final EditText password = view.findViewById(R.id.etxtPassword); - final Button login = view.findViewById(R.id.butLogin); - final TextView txtvError = view.findViewById(R.id.txtvError); - final ProgressBar progressBar = view.findViewById(R.id.progBarLogin); - - password.setOnEditorActionListener((v, actionID, event) -> - actionID == EditorInfo.IME_ACTION_GO && login.performClick()); - - login.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - - final String usernameStr = username.getText().toString(); - final String passwordStr = password.getText().toString(); - - if (usernameHasUnwantedChars(usernameStr)) { - txtvError.setText(R.string.gpodnetsync_username_characters_error); - txtvError.setVisibility(View.VISIBLE); - return; - } - if (BuildConfig.DEBUG) Log.d(TAG, "Checking login credentials"); - AsyncTask<GpodnetService, Void, Void> authTask = new AsyncTask<GpodnetService, Void, Void>() { - - volatile Exception exception; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - login.setEnabled(false); - progressBar.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - // hide the keyboard - InputMethodManager inputManager = (InputMethodManager) - getSystemService(Context.INPUT_METHOD_SERVICE); - inputManager.hideSoftInputFromWindow(login.getWindowToken(), - InputMethodManager.HIDE_NOT_ALWAYS); - - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - login.setEnabled(true); - progressBar.setVisibility(View.GONE); - - if (exception == null) { - advance(); - } else { - txtvError.setText(exception.getCause().getMessage()); - txtvError.setVisibility(View.VISIBLE); - } - } - - @Override - protected Void doInBackground(GpodnetService... params) { - try { - params[0].authenticate(usernameStr, passwordStr); - GpodnetAuthenticationActivity.this.username = usernameStr; - GpodnetAuthenticationActivity.this.password = passwordStr; - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - } - return null; - } - }; - authTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, service); - } - }); - } - - private void setupDeviceView(View view) { - final EditText deviceID = view.findViewById(R.id.etxtDeviceID); - final EditText caption = view.findViewById(R.id.etxtCaption); - final Button createNewDevice = view.findViewById(R.id.butCreateNewDevice); - final Button chooseDevice = view.findViewById(R.id.butChooseExistingDevice); - final TextView txtvError = view.findViewById(R.id.txtvError); - final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice); - final Spinner spinnerDevices = view.findViewById(R.id.spinnerChooseDevice); - - - // load device list - final AtomicReference<List<GpodnetDevice>> devices = new AtomicReference<>(); - new AsyncTask<GpodnetService, Void, List<GpodnetDevice>>() { - - @Override - protected void onPreExecute() { - super.onPreExecute(); - chooseDevice.setEnabled(false); - spinnerDevices.setEnabled(false); - createNewDevice.setEnabled(false); - } - - @Override - protected void onPostExecute(List<GpodnetDevice> gpodnetDevices) { - super.onPostExecute(gpodnetDevices); - if (gpodnetDevices != null) { - List<String> deviceNames = new ArrayList<>(); - for (GpodnetDevice device : gpodnetDevices) { - deviceNames.add(device.getCaption()); - } - spinnerDevices.setAdapter(new ArrayAdapter<>(GpodnetAuthenticationActivity.this, - android.R.layout.simple_spinner_dropdown_item, deviceNames)); - spinnerDevices.setEnabled(true); - if (!deviceNames.isEmpty()) { - chooseDevice.setEnabled(true); - } - devices.set(gpodnetDevices); - deviceID.setText(generateDeviceID(gpodnetDevices)); - createNewDevice.setEnabled(true); - } - } - - @Override - protected List<GpodnetDevice> doInBackground(GpodnetService... params) { - try { - return params[0].getDevices(); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - return null; - } - } - }.execute(service); - - - createNewDevice.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (checkDeviceIDText(deviceID, caption, txtvError, devices.get())) { - final String deviceStr = deviceID.getText().toString(); - final String captionStr = caption.getText().toString(); - - new AsyncTask<GpodnetService, Void, GpodnetDevice>() { - - private volatile Exception exception; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - createNewDevice.setEnabled(false); - chooseDevice.setEnabled(false); - progBarCreateDevice.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - } - - @Override - protected void onPostExecute(GpodnetDevice result) { - super.onPostExecute(result); - createNewDevice.setEnabled(true); - chooseDevice.setEnabled(true); - progBarCreateDevice.setVisibility(View.GONE); - if (exception == null) { - selectedDevice = result; - advance(); - } else { - txtvError.setText(exception.getMessage()); - txtvError.setVisibility(View.VISIBLE); - } - } - - @Override - protected GpodnetDevice doInBackground(GpodnetService... params) { - try { - params[0].configureDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE); - return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - } - return null; - } - }.execute(service); - } - } - }); - - chooseDevice.setOnClickListener(v -> { - final int position = spinnerDevices.getSelectedItemPosition(); - if (position != AdapterView.INVALID_POSITION) { - selectedDevice = devices.get().get(position); - advance(); - } - }); - } - - - private String generateDeviceID(List<GpodnetDevice> gpodnetDevices) { - // devices names must be of a certain form: - // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices - // This is more restrictive than needed, but I think it makes for more readable names. - String baseId = Build.MODEL.replaceAll("\\W", ""); - String id = baseId; - int num = 0; - - while (isDeviceWithIdInList(id, gpodnetDevices)) { - id = baseId + "_" + num; - num++; - } - - return id; - } - - private boolean isDeviceWithIdInList(String id, List<GpodnetDevice> gpodnetDevices) { - if (gpodnetDevices == null) { - return false; - } - for (GpodnetDevice device : gpodnetDevices) { - if (device.getId().equals(id)) { - return true; - } - } - return false; - } - - private boolean checkDeviceIDText(EditText deviceID, EditText caption, TextView txtvError, List<GpodnetDevice> devices) { - String text = deviceID.getText().toString(); - if (text.length() == 0) { - txtvError.setText(R.string.gpodnetauth_device_errorEmpty); - txtvError.setVisibility(View.VISIBLE); - return false; - } else if (caption.length() == 0) { - txtvError.setText(R.string.gpodnetauth_device_caption_errorEmpty); - txtvError.setVisibility(View.VISIBLE); - return false; - } else { - if (devices != null) { - if (isDeviceWithIdInList(text, devices)) { - txtvError.setText(R.string.gpodnetauth_device_errorAlreadyUsed); - txtvError.setVisibility(View.VISIBLE); - return false; - } - txtvError.setVisibility(View.GONE); - return true; - } - return true; - } - - } - - private void setupFinishView(View view) { - final Button sync = view.findViewById(R.id.butSyncNow); - final Button back = view.findViewById(R.id.butGoMainscreen); - - sync.setOnClickListener(v -> { - finish(); - SyncService.sync(getApplicationContext()); - }); - back.setOnClickListener(v -> { - Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - }); - } - - private void writeLoginCredentials() { - if (BuildConfig.DEBUG) Log.d(TAG, "Writing login credentials"); - GpodnetPreferences.setUsername(username); - GpodnetPreferences.setPassword(password); - GpodnetPreferences.setDeviceID(selectedDevice.getId()); - } - - private void advance() { - if (currentStep < STEP_FINISH) { - - View view = views[currentStep + 1]; - if (currentStep == STEP_DEFAULT) { - setupLoginView(view); - } else if (currentStep == STEP_LOGIN) { - if (username == null || password == null) { - throw new IllegalStateException("Username and password must not be null here"); - } else { - setupDeviceView(view); - } - } else if (currentStep == STEP_DEVICE) { - if (selectedDevice == null) { - throw new IllegalStateException("Device must not be null here"); - } else { - writeLoginCredentials(); - setupFinishView(view); - } - } - if (currentStep != STEP_DEFAULT) { - viewFlipper.showNext(); - } - currentStep++; - } else { - finish(); - } - } - - private boolean usernameHasUnwantedChars(String username) { - Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); - Matcher containsUnwantedChars = special.matcher(username); - return containsUnwantedChars.find(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java index 4fa8acc43..d4b32ee06 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java @@ -20,9 +20,9 @@ import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.playback.Playable; -import de.danoeh.antennapod.view.CircularProgressBar; +import de.danoeh.antennapod.ui.common.CircularProgressBar; public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapter.ChapterHolder> { private Playable media; @@ -42,7 +42,7 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte hasImages = false; if (media.getChapters() != null) { for (Chapter chapter : media.getChapters()) { - if (!ignoreChapter(chapter) && !TextUtils.isEmpty(chapter.getImageUrl())) { + if (!TextUtils.isEmpty(chapter.getImageUrl())) { hasImages = true; } } @@ -125,14 +125,7 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte if (media == null || media.getChapters() == null) { return 0; } - // ignore invalid chapters - int counter = 0; - for (Chapter chapter : media.getChapters()) { - if (!ignoreChapter(chapter)) { - counter++; - } - } - return counter; + return media.getChapters().size(); } static class ChapterHolder extends RecyclerView.ViewHolder { @@ -171,22 +164,8 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte notifyItemChanged(currentChapterIndex, "foo"); } - private boolean ignoreChapter(Chapter c) { - return media.getDuration() > 0 && media.getDuration() < c.getStart(); - } - public Chapter getItem(int position) { - int i = 0; - for (Chapter chapter : media.getChapters()) { - if (!ignoreChapter(chapter)) { - if (i == position) { - return chapter; - } else { - i++; - } - } - } - return null; + return media.getChapters().get(position); } public interface Callback { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index 0c4aaf6ed..811e1e31b 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -20,7 +20,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.view.viewholder.DownloadItemViewHolder; /** @@ -68,16 +68,14 @@ public class DownloadLogAdapter extends BaseAdapter { holder.icon.setContentDescription(context.getString(R.string.download_successful)); holder.secondaryActionButton.setVisibility(View.INVISIBLE); holder.reason.setVisibility(View.GONE); + holder.tapForDetails.setVisibility(View.GONE); } else { holder.icon.setTextColor(ContextCompat.getColor(context, R.color.download_failed_red)); holder.icon.setText("{fa-times-circle}"); holder.icon.setContentDescription(context.getString(R.string.error_label)); - String reasonText = status.getReason().getErrorString(context); - if (status.getReasonDetailed() != null) { - reasonText += ": " + status.getReasonDetailed(); - } - holder.reason.setText(reasonText); + holder.reason.setText(status.getReason().getErrorString(context)); holder.reason.setVisibility(View.VISIBLE); + holder.tapForDetails.setVisibility(View.VISIBLE); if (newerWasSuccessful(position, status.getFeedfileType(), status.getFeedfileId())) { holder.secondaryActionButton.setVisibility(View.INVISIBLE); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java index 268a21409..9363edc9f 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java @@ -15,8 +15,8 @@ import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.core.util.ThemeUtils; -import de.danoeh.antennapod.view.CircularProgressBar; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.common.CircularProgressBar; public class DownloadlistAdapter extends BaseAdapter { @@ -68,8 +68,8 @@ public class DownloadlistAdapter extends BaseAdapter { holder.secondaryActionButton.setContentDescription(context.getString(R.string.cancel_download_label)); holder.secondaryActionButton.setTag(downloader); holder.secondaryActionButton.setOnClickListener(butSecondaryListener); - holder.secondaryActionProgress.setPercentage(0, request); + boolean percentageWasSet = false; String status = ""; if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { status += context.getString(R.string.download_type_feed); @@ -85,8 +85,12 @@ public class DownloadlistAdapter extends BaseAdapter { status += " / " + Formatter.formatShortFileSize(context, request.getSize()); holder.secondaryActionProgress.setPercentage( 0.01f * Math.max(1, request.getProgressPercent()), request); + percentageWasSet = true; } } + if (!percentageWasSet) { + holder.secondaryActionProgress.setPercentage(0, request); + } holder.status.setText(status); return convertView; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java index 8cb0fd30a..50b924e49 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -20,6 +20,7 @@ import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; +import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.dialog.StreamingConfirmationDialog; import java.util.List; @@ -59,7 +60,7 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> { holder.title.setText(item.getTitle()); holder.pubDate.setText(DateUtils.formatAbbrev(getContext(), item.getPubDate())); if (item.getDescription() != null) { - String description = item.getDescription() + String description = HtmlToPlainText.getPlainText(item.getDescription()) .replaceAll("\n", " ") .replaceAll("\\s+", " ") .trim(); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java index 2e5ba31c9..dbb9ce0d0 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java @@ -10,7 +10,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.fragment.FeedItemlistFragment; -import de.danoeh.antennapod.view.SquareImageView; +import de.danoeh.antennapod.ui.common.SquareImageView; import java.lang.ref.WeakReference; import java.util.ArrayList; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java index 5533197b9..8bfcf66cc 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -24,7 +24,6 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.NavDrawerData; -import de.danoeh.antennapod.core.util.ThemeUtils; import de.danoeh.antennapod.fragment.AddFeedFragment; import de.danoeh.antennapod.fragment.DownloadsFragment; import de.danoeh.antennapod.fragment.EpisodesFragment; @@ -32,6 +31,7 @@ import de.danoeh.antennapod.fragment.NavDrawerFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; +import de.danoeh.antennapod.ui.common.ThemeUtils; import org.apache.commons.lang3.ArrayUtils; import java.lang.ref.WeakReference; @@ -76,7 +76,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder> } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS)) { + if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS.equals(key)) { loadItems(); } } @@ -314,7 +314,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder> } Glide.with(context) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.light_gray) .error(R.color.light_gray) diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java index 72482b06d..23b5cfdce 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java @@ -66,7 +66,7 @@ public abstract class StatisticsListAdapter extends RecyclerView.Adapter<Recycle StatisticsHolder holder = (StatisticsHolder) h; StatisticsItem statsItem = statisticsData.get(position - 1); Glide.with(context) - .load(statsItem.feed.getImageLocation()) + .load(statsItem.feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.light_gray) .error(R.color.light_gray) diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java index 5c0ecfa3c..1d85cdaff 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java @@ -22,8 +22,8 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.LocalFeedUpdater; import de.danoeh.antennapod.core.storage.NavDrawerData; -import de.danoeh.antennapod.core.util.ThemeUtils; import de.danoeh.antennapod.fragment.FeedItemlistFragment; +import de.danoeh.antennapod.ui.common.ThemeUtils; import jp.shts.android.library.TriangleLabelView; /** @@ -112,7 +112,7 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI boolean textAndImageCombined = feed.isLocalFeed() && LocalFeedUpdater.getDefaultIconUrl(convertView.getContext()).equals(feed.getImageUrl()); new CoverLoader(mainActivityRef.get()) - .withUri(feed.getImageLocation()) + .withUri(feed.getImageUrl()) .withPlaceholderView(holder.feedTitle, textAndImageCombined) .withCoverView(holder.imageView) .load(); @@ -123,7 +123,6 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI .withCoverView(holder.imageView) .load(); } - return convertView; } diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java index 7a5cf431f..a45eb5199 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java +++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java @@ -2,21 +2,19 @@ package de.danoeh.antennapod.config; import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.storage.APDownloadAlgorithm; /** * Configures the ClientConfig class of the core package. */ class ClientConfigurator { - private ClientConfigurator(){} + private ClientConfigurator() { + } static { ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME; ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl(); ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl(); - ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl(); - ClientConfig.automaticDownloadAlgorithm = new APDownloadAlgorithm(); ClientConfig.castCallbacks = new CastCallbackImpl(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java index 55bf05e43..f782308d1 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java +++ b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java @@ -30,8 +30,8 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks { @Override public PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request) { final Intent activityIntent = new Intent(context.getApplicationContext(), DownloadAuthenticationActivity.class); + activityIntent.setAction("request" + request.getFeedfileId()); activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, request); - activityIntent.putExtra(DownloadAuthenticationActivity.ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, true); return PendingIntent.getActivity(context.getApplicationContext(), R.id.pending_intent_download_service_auth, activityIntent, PendingIntent.FLAG_ONE_SHOT); } diff --git a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java deleted file mode 100644 index f70cb26ff..000000000 --- a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.danoeh.antennapod.config; - -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.activity.VideoplayerActivity; -import de.danoeh.antennapod.core.PlaybackServiceCallbacks; -import de.danoeh.antennapod.core.feed.MediaType; - -public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks { - @Override - public Intent getPlayerActivityIntent(Context context, MediaType mediaType, boolean remotePlayback) { - if (mediaType == MediaType.AUDIO || remotePlayback) { - return new Intent(context, MainActivity.class).putExtra(MainActivity.EXTRA_OPEN_PLAYER, true); - } else { - Intent i = new Intent(context, VideoplayerActivity.class); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - i.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - } - return i; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java index 39d321f18..d7b2dc536 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java @@ -1,37 +1,50 @@ package de.danoeh.antennapod.dialog; import android.content.Context; -import android.view.View; -import android.widget.EditText; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.view.LayoutInflater; import androidx.appcompat.app.AlertDialog; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.AuthenticationDialogBinding; /** * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. */ public abstract class AuthenticationDialog extends AlertDialog.Builder { + boolean passwordHidden = true; public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField, String usernameInitialValue, String passwordInitialValue) { super(context); setTitle(titleRes); - View rootView = View.inflate(context, R.layout.authentication_dialog, null); - setView(rootView); + AuthenticationDialogBinding viewBinding = AuthenticationDialogBinding.inflate(LayoutInflater.from(context)); + setView(viewBinding.getRoot()); - final EditText etxtUsername = rootView.findViewById(R.id.etxtUsername); - final EditText etxtPassword = rootView.findViewById(R.id.etxtPassword); - - etxtUsername.setEnabled(enableUsernameField); + viewBinding.usernameEditText.setEnabled(enableUsernameField); if (usernameInitialValue != null) { - etxtUsername.setText(usernameInitialValue); + viewBinding.usernameEditText.setText(usernameInitialValue); } if (passwordInitialValue != null) { - etxtPassword.setText(passwordInitialValue); + viewBinding.passwordEditText.setText(passwordInitialValue); } + viewBinding.showPasswordButton.setOnClickListener(v -> { + if (passwordHidden) { + viewBinding.passwordEditText.setTransformationMethod(HideReturnsTransformationMethod.getInstance()); + viewBinding.showPasswordButton.setAlpha(1.0f); + } else { + viewBinding.passwordEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + viewBinding.showPasswordButton.setAlpha(0.6f); + } + passwordHidden = !passwordHidden; + }); + setOnCancelListener(dialog -> onCancelled()); + setOnDismissListener(dialog -> onCancelled()); setNegativeButton(R.string.cancel_label, null); setPositiveButton(R.string.confirm_label, (dialog, which) - -> onConfirmed(etxtUsername.getText().toString(), etxtPassword.getText().toString())); + -> onConfirmed(viewBinding.usernameEditText.getText().toString(), + viewBinding.passwordEditText.getText().toString())); } protected void onCancelled() { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java index efaff1da3..e1e8f1c2e 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -28,7 +28,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.SortOrder; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import java.util.ArrayList; import java.util.Arrays; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java index 80df87891..779248e2f 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java @@ -16,7 +16,7 @@ import java.util.Set; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.feed.FeedItemFilterGroup; -import de.danoeh.antennapod.view.RecursiveRadioGroup; +import de.danoeh.antennapod.ui.common.RecursiveRadioGroup; public abstract class FilterDialog { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java deleted file mode 100644 index 8119dffcb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import androidx.appcompat.app.AlertDialog; -import android.text.Editable; -import android.text.InputType; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.LinearLayout; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; - -/** - * Creates a dialog that lets the user change the hostname for the gpodder.net service. - */ -public class GpodnetSetHostnameDialog { - - private GpodnetSetHostnameDialog(){} - - private static final String TAG = "GpodnetSetHostnameDialog"; - - public static AlertDialog createDialog(final Context context) { - AlertDialog.Builder dialog = new AlertDialog.Builder(context); - final EditText et = new EditText(context); - et.setText(GpodnetPreferences.getHostname()); - et.setInputType(InputType.TYPE_TEXT_VARIATION_URI); - dialog.setTitle(R.string.pref_gpodnet_sethostname_title) - .setView(setupContentView(context, et)) - .setPositiveButton(R.string.confirm_label, (dialog1, which) -> { - final Editable e = et.getText(); - if (e != null) { - GpodnetPreferences.setHostname(e.toString()); - } - dialog1.dismiss(); - }) - .setNegativeButton(R.string.cancel_label, (dialog1, which) -> dialog1.cancel()) - .setNeutralButton(R.string.pref_gpodnet_sethostname_use_default_host, (dialog1, which) -> { - GpodnetPreferences.setHostname(GpodnetService.DEFAULT_BASE_HOST); - dialog1.dismiss(); - }) - .setCancelable(true); - return dialog.show(); - } - - private static View setupContentView(Context context, EditText et) { - LinearLayout ll = new LinearLayout(context); - ll.addView(et); - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) et.getLayoutParams(); - if (params != null) { - params.setMargins(8, 8, 8, 8); - params.width = ViewGroup.LayoutParams.MATCH_PARENT; - params.height = ViewGroup.LayoutParams.MATCH_PARENT; - } - return ll; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java index 98f6cc117..195891499 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java @@ -41,7 +41,7 @@ public class PlaybackControlsDialog extends DialogFragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public void setupGUI() { + public void loadMediaInfo() { setupUi(); setupAudioTracks(); } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java index f1a41d753..fa5c2d8c3 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java @@ -47,12 +47,12 @@ public class SleepTimerDialog extends DialogFragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public void setupGUI() { + public void onSleepTimerUpdate() { updateTime(); } @Override - public void onSleepTimerUpdate() { + public void loadMediaInfo() { updateTime(); } }; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java index 8a87fef25..29172bb5e 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java @@ -20,7 +20,7 @@ import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.feed.SubscriptionsFilterGroup; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.view.RecursiveRadioGroup; +import de.danoeh.antennapod.ui.common.RecursiveRadioGroup; public class SubscriptionsFilterDialog { public static void showDialog(Context context) { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java index 1fc7a77b2..65e7c4424 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java @@ -56,12 +56,12 @@ public class VariableSpeedDialog extends DialogFragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public void setupGUI() { + public void onPlaybackSpeedChange() { updateSpeed(); } @Override - public void onPlaybackSpeedChange() { + public void loadMediaInfo() { updateSpeed(); } }; diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java index 53237579f..6de2186e0 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java @@ -18,7 +18,7 @@ public class GpodnetPodcastSearcher implements PodcastSearcher { return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> { try { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHostname()); + GpodnetPreferences.getHosturl()); List<GpodnetPodcast> gpodnetPodcasts = service.searchPodcasts(query, 0); List<PodcastSearchResult> results = new ArrayList<>(); for (GpodnetPodcast podcast : gpodnetPodcasts) { diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java index ad574cab6..16c5548be 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java @@ -15,11 +15,11 @@ public class PodcastSearcherRegistry { public static List<SearcherInfo> getSearchProviders() { if (searchProviders == null) { searchProviders = new ArrayList<>(); - searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.f)); - searchProviders.add(new SearcherInfo(new ItunesPodcastSearcher(), 1.f)); - searchProviders.add(new SearcherInfo(new FyydPodcastSearcher(), 1.f)); + searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.0f)); searchProviders.add(new SearcherInfo(new GpodnetPodcastSearcher(), 0.0f)); - searchProviders.add(new SearcherInfo(new PodcastIndexPodcastSearcher(), 0.0f)); + searchProviders.add(new SearcherInfo(new FyydPodcastSearcher(), 1.0f)); + searchProviders.add(new SearcherInfo(new ItunesPodcastSearcher(), 1.0f)); + searchProviders.add(new SearcherInfo(new PodcastIndexPodcastSearcher(), 1.0f)); } return searchProviders; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java index 06a974dfd..08e23fc7f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -50,9 +50,11 @@ public class AddFeedFragment extends Fragment { public static final String TAG = "AddFeedFragment"; private static final int REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH = 1; private static final int REQUEST_CODE_ADD_LOCAL_FOLDER = 2; + private static final String KEY_UP_ARROW = "up_arrow"; private AddfeedBinding viewBinding; private MainActivity activity; + private boolean displayUpArrow; @Override @Nullable @@ -64,7 +66,11 @@ public class AddFeedFragment extends Fragment { activity = (MainActivity) getActivity(); Toolbar toolbar = viewBinding.toolbar; - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); viewBinding.searchItunesButton.setOnClickListener(v -> activity.loadChildFragment(OnlineSearchFragment.newInstance(ItunesPodcastSearcher.class))); @@ -119,6 +125,12 @@ public class AddFeedFragment extends Fragment { return viewBinding.getRoot(); } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + private void showAddViaUrlDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle(R.string.add_podcast_by_url); @@ -196,7 +208,11 @@ public class AddFeedFragment extends Fragment { if (documentFile == null) { throw new IllegalArgumentException("Unable to retrieve document tree"); } - Feed dirFeed = new Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, documentFile.getName()); + String title = documentFile.getName(); + if (title == null) { + title = getString(R.string.local_folder); + } + Feed dirFeed = new Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, title); dirFeed.setItems(Collections.emptyList()); dirFeed.setSortOrder(SortOrder.EPISODE_TITLE_A_Z); Feed fromDatabase = DBTasks.updateFeed(getContext(), dirFeed, false); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java index 4423a2ebe..612959c04 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -104,13 +104,12 @@ public class AllEpisodesFragment extends EpisodesListFragment { @NonNull @Override protected List<FeedItem> loadData() { - return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE)); + return DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE, feedItemFilter); } @NonNull @Override protected List<FeedItem> loadMoreData() { - return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, - EPISODES_PER_PAGE)); + return DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, feedItemFilter); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java index 82e2b3a6a..51f264e56 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java @@ -1,8 +1,6 @@ package de.danoeh.antennapod.fragment; -import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -17,7 +15,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; +import androidx.cardview.widget.CardView; import androidx.fragment.app.Fragment; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.bottomsheet.BottomSheetBehavior; @@ -29,11 +29,14 @@ import de.danoeh.antennapod.activity.CastEnabledActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.event.FavoritesEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.TimeSpeedConverter; @@ -45,7 +48,8 @@ import de.danoeh.antennapod.dialog.SkipPreferenceDialog; import de.danoeh.antennapod.dialog.SleepTimerDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.view.PlaybackSpeedIndicatorView; +import de.danoeh.antennapod.view.ChapterSeekBar; +import de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView; import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -62,14 +66,13 @@ import java.util.List; * Shows the audio player. */ public class AudioPlayerFragment extends Fragment implements - SeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener { + ChapterSeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener { public static final String TAG = "AudioPlayerFragment"; private static final int POS_COVER = 0; private static final int POS_DESCR = 1; private static final int POS_CHAPTERS = 2; private static final int NUM_CONTENT_FRAGMENTS = 3; - private static final String PREFS = "AudioPlayerFragmentPreferences"; - private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; + public static final String PREFS = "AudioPlayerFragmentPreferences"; private static final float EPSILON = 0.001f; PlaybackSpeedIndicatorView butPlaybackSpeed; @@ -77,7 +80,7 @@ public class AudioPlayerFragment extends Fragment implements private ViewPager2 pager; private TextView txtvPosition; private TextView txtvLength; - private SeekBar sbPosition; + private ChapterSeekBar sbPosition; private ImageButton butRev; private TextView txtvRev; private ImageButton butPlay; @@ -86,6 +89,8 @@ public class AudioPlayerFragment extends Fragment implements private ImageButton butSkip; private Toolbar toolbar; private ProgressBar progressIndicator; + private CardView cardViewSeek; + private TextView txtvSeek; private PlaybackController controller; private Disposable disposable; @@ -122,6 +127,8 @@ public class AudioPlayerFragment extends Fragment implements txtvFF = root.findViewById(R.id.txtvFF); butSkip = root.findViewById(R.id.butSkip); progressIndicator = root.findViewById(R.id.progLoading); + cardViewSeek = root.findViewById(R.id.cardViewSeek); + txtvSeek = root.findViewById(R.id.txtvSeek); setupLengthTextView(); setupControlButtons(); @@ -168,12 +175,33 @@ public class AudioPlayerFragment extends Fragment implements return root; } - public void setHasChapters(boolean hasChapters) { + private void setHasChapters(boolean hasChapters) { this.hasChapters = hasChapters; tabLayoutMediator.detach(); tabLayoutMediator.attach(); } + private void setChapterDividers(Playable media) { + + if (media == null) { + return; + } + + float[] dividerPos = null; + + if (hasChapters) { + List<Chapter> chapters = media.getChapters(); + dividerPos = new float[chapters.size()]; + float duration = media.getDuration(); + + for (int i = 0; i < chapters.size(); i++) { + dividerPos[i] = chapters.get(i).getStart() / duration; + } + } + + sbPosition.setDividerPos(dividerPos); + } + public View getExternalPlayerHolder() { return getView().findViewById(R.id.playerFragment); } @@ -211,16 +239,25 @@ public class AudioPlayerFragment extends Fragment implements IntentUtils.sendLocalBroadcast(getActivity(), PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsUpdate(UnreadItemsUpdateEvent event) { + if (controller == null) { + return; + } + updatePosition(new PlaybackPositionEvent(controller.getPosition(), + controller.getDuration())); + } + private void setupLengthTextView() { - SharedPreferences prefs = getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE); - showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); txtvLength.setOnClickListener(v -> { if (controller == null) { return; } showTimeLeft = !showTimeLeft; - prefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft).apply(); - updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration())); + UserPreferences.setShowRemainTimeSetting(showTimeLeft); + updatePosition(new PlaybackPositionEvent(controller.getPosition(), + controller.getDuration())); }); } @@ -285,26 +322,21 @@ public class AudioPlayerFragment extends Fragment implements disposable = Maybe.create(emitter -> { Playable media = controller.getMedia(); if (media != null) { + ChapterUtils.loadChapters(media, getContext()); emitter.onSuccess(media); } else { emitter.onComplete(); } }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(media -> updateUi((Playable) media), - error -> Log.e(TAG, Log.getStackTraceString(error)), - () -> updateUi(null)); + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(media -> updateUi((Playable) media), + error -> Log.e(TAG, Log.getStackTraceString(error)), + () -> updateUi(null)); } private PlaybackController newPlaybackController() { return new PlaybackController(getActivity()) { - - @Override - public void setupGUI() { - AudioPlayerFragment.this.loadMediaInfo(); - } - @Override public void onBufferStart() { progressIndicator.setVisibility(View.VISIBLE); @@ -352,9 +384,8 @@ public class AudioPlayerFragment extends Fragment implements } @Override - public boolean loadMediaInfo() { + public void loadMediaInfo() { AudioPlayerFragment.this.loadMediaInfo(); - return true; } @Override @@ -383,8 +414,15 @@ public class AudioPlayerFragment extends Fragment implements if (controller == null) { return; } + + if (media != null && media.getChapters() != null) { + setHasChapters(media.getChapters().size() > 0); + } else { + setHasChapters(false); + } updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration())); updatePlaybackSpeedButton(media); + setChapterDividers(media); setupOptionsMenu(media); } @@ -433,6 +471,7 @@ public class AudioPlayerFragment extends Fragment implements return; } txtvPosition.setText(Converter.getDurationStringLong(currentPosition)); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); if (showTimeLeft) { txtvLength.setText("-" + Converter.getDurationStringLong(remainingTime)); } else { @@ -454,22 +493,22 @@ public class AudioPlayerFragment extends Fragment implements } if (fromUser) { float prog = progress / ((float) seekBar.getMax()); - int duration = controller.getDuration(); TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int position = converter.convert((int) (prog * duration)); - txtvPosition.setText(Converter.getDurationStringLong(position)); - - if (showTimeLeft && prog != 0) { - int timeLeft = converter.convert(duration - (int) (prog * duration)); - String length = "-" + Converter.getDurationStringLong(timeLeft); - txtvLength.setText(length); - } + int position = converter.convert((int) (prog * controller.getDuration())); + txtvSeek.setText(Converter.getDurationStringLong(position)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { // interrupt position Observer, restart later + cardViewSeek.setScaleX(.8f); + cardViewSeek.setScaleY(.8f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(200) + .start(); } @Override @@ -478,6 +517,13 @@ public class AudioPlayerFragment extends Fragment implements float prog = seekBar.getProgress() / ((float) seekBar.getMax()); controller.seekTo((int) (prog * controller.getDuration())); } + cardViewSeek.setScaleX(1f); + cardViewSeek.setScaleY(1f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(200) + .start(); } public void setupOptionsMenu(Playable media) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java index d781d0774..acda462bd 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -8,9 +8,9 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChaptersListAdapter; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; @@ -45,7 +45,8 @@ public class ChaptersFragment extends Fragment { RecyclerView recyclerView = root.findViewById(R.id.recyclerView); layoutManager = new LinearLayoutManager(getActivity()); recyclerView.setLayoutManager(layoutManager); - recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build()); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), + layoutManager.getOrientation())); adapter = new ChaptersListAdapter(getActivity(), pos -> { if (controller.getStatus() != PlayerStatus.PLAYING) { @@ -71,13 +72,7 @@ public class ChaptersFragment extends Fragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public boolean loadMediaInfo() { - ChaptersFragment.this.loadMediaInfo(); - return true; - } - - @Override - public void setupGUI() { + public void loadMediaInfo() { ChaptersFragment.this.loadMediaInfo(); } @@ -123,7 +118,7 @@ public class ChaptersFragment extends Fragment { disposable = Maybe.create(emitter -> { Playable media = controller.getMedia(); if (media != null) { - media.loadChapterMarks(getContext()); + ChapterUtils.loadChapters(media, getContext()); emitter.onSuccess(media); } else { emitter.onComplete(); @@ -142,7 +137,6 @@ public class ChaptersFragment extends Fragment { return; } adapter.setMedia(media); - ((AudioPlayerFragment) getParentFragment()).setHasChapters(adapter.getItemCount() > 0); int positionOfCurrentChapter = getCurrentChapter(media); updateChapterSelection(positionOfCurrentChapter); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java index 648fc614a..d8c382cb2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -13,11 +13,9 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; - import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; @@ -25,9 +23,11 @@ import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; @@ -35,6 +35,7 @@ import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -93,7 +94,12 @@ public class CoverFragment extends Fragment { } private void displayMediaInfo(@NonNull Playable media) { - txtvPodcastTitle.setText(media.getFeedTitle()); + String pubDateStr = DateUtils.formatAbbrev(getActivity(), ((FeedMedia) media).getPubDate()); + txtvPodcastTitle.setText(StringUtils.stripToEmpty(media.getFeedTitle()) + + "\u00A0" + + "・" + + "\u00A0" + + StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0")); txtvEpisodeTitle.setText(media.getEpisodeTitle()); displayedChapterIndex = -2; // Force refresh displayCoverImage(media.getPosition()); @@ -111,13 +117,7 @@ public class CoverFragment extends Fragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public boolean loadMediaInfo() { - CoverFragment.this.loadMediaInfo(); - return true; - } - - @Override - public void setupGUI() { + public void loadMediaInfo() { CoverFragment.this.loadMediaInfo(); } }; @@ -151,23 +151,25 @@ public class CoverFragment extends Fragment { if (chapter != displayedChapterIndex) { displayedChapterIndex = chapter; + RequestOptions options = new RequestOptions() + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .dontAnimate() + .transforms(new FitCenter(), + new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))); + RequestBuilder<Drawable> cover = Glide.with(this) - .load(ImageResourceUtils.getImageLocation(media)) - .apply(new RequestOptions() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .dontAnimate() - .transforms(new FitCenter(), - new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density)))); + .load(media.getImageLocation()) + .error(Glide.with(this) + .load(ImageResourceUtils.getFallbackImageLocation(media)) + .apply(options)) + .apply(options); + if (chapter == -1 || TextUtils.isEmpty(media.getChapters().get(chapter).getImageUrl())) { cover.into(imgvCover); } else { Glide.with(this) .load(EmbeddedChapterImage.getModelFor(media, chapter)) - .apply(new RequestOptions() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .dontAnimate() - .transforms(new FitCenter(), - new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density)))) + .apply(options) .thumbnail(cover) .error(cover) .into(imgvCover); @@ -208,7 +210,7 @@ public class CoverFragment extends Fragment { imgvCover.setLayoutParams(params); } } else { - double percentageHeight = ratio * 0.8; + double percentageHeight = ratio * 0.6; mainContainer.setOrientation(LinearLayout.HORIZONTAL); if (newConfig.screenHeightDp > 0) { params.height = (int) (convertDpToPixel(newConfig.screenHeightDp) * percentageHeight); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java index ffb3e71fa..5c83cee57 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java @@ -28,16 +28,17 @@ public class DownloadsFragment extends PagedToolbarFragment { public static final String TAG = "DownloadsFragment"; public static final String ARG_SELECTED_TAB = "selected_tab"; + private static final String PREF_LAST_TAB_POSITION = "tab_position"; + private static final String KEY_UP_ARROW = "up_arrow"; public static final int POS_RUNNING = 0; private static final int POS_COMPLETED = 1; public static final int POS_LOG = 2; private static final int TOTAL_COUNT = 3; - private static final String PREF_LAST_TAB_POSITION = "tab_position"; - private ViewPager2 viewPager; private TabLayout tabLayout; + private boolean displayUpArrow; @Override public View onCreateView(@NonNull LayoutInflater inflater, @@ -48,7 +49,11 @@ public class DownloadsFragment extends PagedToolbarFragment { Toolbar toolbar = root.findViewById(R.id.toolbar); toolbar.setTitle(R.string.downloads_label); toolbar.inflateMenu(R.menu.downloads); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); viewPager = root.findViewById(R.id.viewpager); viewPager.setAdapter(new DownloadsPagerAdapter(this)); @@ -82,6 +87,12 @@ public class DownloadsFragment extends PagedToolbarFragment { } @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + + @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (getArguments() != null) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java index eff23f7a3..1ca5d524b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java @@ -24,6 +24,7 @@ public class EpisodesFragment extends PagedToolbarFragment { public static final String TAG = "EpisodesFragment"; private static final String PREF_LAST_TAB_POSITION = "tab_position"; + private static final String KEY_UP_ARROW = "up_arrow"; private static final int POS_NEW_EPISODES = 0; private static final int POS_ALL_EPISODES = 1; @@ -31,6 +32,7 @@ public class EpisodesFragment extends PagedToolbarFragment { private static final int TOTAL_COUNT = 3; private TabLayout tabLayout; + private boolean displayUpArrow; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -44,7 +46,11 @@ public class EpisodesFragment extends PagedToolbarFragment { toolbar.setTitle(R.string.episodes_label); toolbar.inflateMenu(R.menu.episodes); MenuItemUtils.setupSearchItem(toolbar.getMenu(), (MainActivity) getActivity(), 0, ""); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); ViewPager2 viewPager = rootView.findViewById(R.id.viewpager); viewPager.setAdapter(new EpisodesPagerAdapter(this)); @@ -88,6 +94,12 @@ public class EpisodesFragment extends PagedToolbarFragment { editor.apply(); } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + static class EpisodesPagerAdapter extends FragmentStateAdapter { EpisodesPagerAdapter(@NonNull Fragment fragment) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java index 8dae310ba..39f935bbe 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -383,6 +383,14 @@ public abstract class EpisodesListFragment extends Fragment { @NonNull protected abstract List<FeedItem> loadData(); + /** + * Load a new page of data as defined by {@link #page} and {@link #EPISODES_PER_PAGE}. + * If the number of items returned is less than {@link #EPISODES_PER_PAGE}, + * it will be assumed that the underlying data is exhausted + * and this method will not be called again. + * + * @return The items from the next page of data + */ @NonNull protected abstract List<FeedItem> loadMoreData(); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index 5d701472f..d77935910 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -108,12 +108,7 @@ public class ExternalPlayerFragment extends Fragment { } @Override - public boolean loadMediaInfo() { - return ExternalPlayerFragment.this.loadMediaInfo(); - } - - @Override - public void setupGUI() { + public void loadMediaInfo() { ExternalPlayerFragment.this.loadMediaInfo(); } @@ -170,11 +165,11 @@ public class ExternalPlayerFragment extends Fragment { } } - private boolean loadMediaInfo() { + private void loadMediaInfo() { Log.d(TAG, "Loading media info"); if (controller == null) { Log.w(TAG, "loadMediaInfo was called while PlaybackController was null!"); - return false; + return; } if (disposable != null) { @@ -186,7 +181,6 @@ public class ExternalPlayerFragment extends Fragment { .subscribe(this::updateUi, error -> Log.e(TAG, Log.getStackTraceString(error)), () -> ((MainActivity) getActivity()).setPlayerVisible(false)); - return true; } private void updateUi(Playable media) { @@ -198,14 +192,19 @@ public class ExternalPlayerFragment extends Fragment { feedName.setText(media.getFeedTitle()); onPositionObserverUpdate(); + RequestOptions options = new RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .fitCenter() + .dontAnimate(); + Glide.with(getActivity()) - .load(ImageResourceUtils.getImageLocation(media)) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .fitCenter() - .dontAnimate()) + .load(ImageResourceUtils.getEpisodeListImageLocation(media)) + .error(Glide.with(getActivity()) + .load(ImageResourceUtils.getFallbackImageLocation(media)) + .apply(options)) + .apply(options) .into(imgvCover); if (controller != null && controller.isPlayingVideoLocally()) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java index abb597e60..25ab925eb 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java @@ -45,7 +45,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.StatisticsItem; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.fragment.preferences.StatisticsFragment; import de.danoeh.antennapod.menuhandler.FeedMenuHandler; @@ -130,6 +130,8 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic protected void doTint(Context themedContext) { toolbar.getMenu().findItem(R.id.visit_website_item) .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.location_web_site)); + toolbar.getMenu().findItem(R.id.share_parent) + .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.ic_share)); } }; iconTintManager.updateTint(); @@ -201,7 +203,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic Log.d(TAG, "Author is " + feed.getAuthor()); Log.d(TAG, "URL is " + feed.getDownload_url()); Glide.with(getContext()) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.light_gray) .error(R.color.light_gray) @@ -210,7 +212,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic .dontAnimate()) .into(imgvCover); Glide.with(getContext()) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.image_readability_tint) .error(R.color.image_readability_tint) @@ -284,9 +286,13 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic } private void refreshToolbarState() { + boolean shareLinkVisible = feed != null && feed.getLink() != null; + boolean downloadUrlVisible = feed != null && !feed.isLocalFeed(); + toolbar.getMenu().findItem(R.id.reconnect_local_folder).setVisible(feed != null && feed.isLocalFeed()); - toolbar.getMenu().findItem(R.id.share_download_url_item).setVisible(feed != null && !feed.isLocalFeed()); - toolbar.getMenu().findItem(R.id.share_link_item).setVisible(feed != null && feed.getLink() != null); + toolbar.getMenu().findItem(R.id.share_download_url_item).setVisible(downloadUrlVisible); + toolbar.getMenu().findItem(R.id.share_link_item).setVisible(shareLinkVisible); + toolbar.getMenu().findItem(R.id.share_parent).setVisible(downloadUrlVisible || shareLinkVisible); toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null && IntentUtils.isCallable(getContext(), new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink())))); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index 8e14214d2..acb929dd2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -16,7 +16,6 @@ import android.widget.AdapterView; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ProgressBar; -import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -57,8 +56,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.Optional; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.dialog.FilterDialog; @@ -89,6 +87,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem Toolbar.OnMenuItemClickListener { private static final String TAG = "ItemlistFragment"; private static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id"; + private static final String KEY_UP_ARROW = "up_arrow"; private FeedItemListAdapter adapter; private MoreContentListFooterUtil nextPageLoader; @@ -106,6 +105,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem private View header; private Toolbar toolbar; private ToolbarIconTintManager iconTintManager; + private boolean displayUpArrow; private long feedID; private Feed feed; @@ -146,7 +146,11 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem toolbar = root.findViewById(R.id.toolbar); toolbar.inflateMenu(R.menu.feedlist); toolbar.setOnMenuItemClickListener(this); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); refreshToolbarState(); recyclerView = root.findViewById(R.id.recyclerView); @@ -231,6 +235,12 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem adapter = null; } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = new MenuItemUtils.UpdateRefreshMenuItemChecker() { @Override public boolean isRefreshing() { @@ -451,10 +461,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem if (feed.getItemFilter() != null) { FeedItemFilter filter = feed.getItemFilter(); if (filter.getValues().length > 0) { - if (feed.hasLastUpdateFailed()) { - RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) txtvInformation.getLayoutParams(); - p.addRule(RelativeLayout.BELOW, R.id.txtvFailure); - } txtvInformation.setText("{md-info-outline} " + this.getString(R.string.filtered_label)); Iconify.addIcons(txtvInformation); txtvInformation.setOnClickListener((l) -> { @@ -514,7 +520,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem private void loadFeedImage() { Glide.with(getActivity()) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.image_readability_tint) .error(R.color.image_readability_tint) @@ -524,7 +530,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem .into(imgvBackground); Glide.with(getActivity()) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.light_gray) .error(R.color.light_gray) @@ -542,27 +548,32 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem disposable = Observable.fromCallable(this::loadData) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - feed = result.orElse(null); - refreshHeaderView(); - displayList(); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); + .subscribe( + result -> { + feed = result; + refreshHeaderView(); + displayList(); + }, error -> { + feed = null; + refreshHeaderView(); + displayList(); + Log.e(TAG, Log.getStackTraceString(error)); + }); } - @NonNull - private Optional<Feed> loadData() { - Feed feed = DBReader.getFeed(feedID); - if (feed != null && feed.getItemFilter() != null) { - DBReader.loadAdditionalFeedItemListData(feed.getItems()); - FeedItemFilter filter = feed.getItemFilter(); - feed.setItems(filter.filter(feed.getItems())); + @Nullable + private Feed loadData() { + Feed feed = DBReader.getFeed(feedID, true); + if (feed == null) { + return null; } - if (feed != null && feed.getSortOrder() != null) { + DBReader.loadAdditionalFeedItemListData(feed.getItems()); + if (feed.getSortOrder() != null) { List<FeedItem> feedItems = feed.getItems(); FeedItemPermutors.getPermutor(feed.getSortOrder()).reorder(feedItems); feed.setItems(feedItems); } - return Optional.ofNullable(feed); + return feed; } private static class FeedItemListAdapter extends EpisodeItemListAdapter { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java index 1253a8ad2..c000107a7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -164,6 +164,7 @@ public class FeedSettingsFragment extends Fragment { setupEpisodeFilterPreference(); setupPlaybackSpeedPreference(); setupFeedAutoSkipPreference(); + setupEpisodeNotificationPreference(); setupTags(); updateAutoDeleteSummary(); @@ -198,7 +199,7 @@ public class FeedSettingsFragment extends Fragment { protected void onConfirmed(int skipIntro, int skipEnding) { feedPreferences.setFeedSkipIntro(skipIntro); feedPreferences.setFeedSkipEnding(skipEnding); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); EventBus.getDefault().post( new SkipIntroEndingChangedEvent(feedPreferences.getFeedSkipIntro(), feedPreferences.getFeedSkipEnding(), @@ -226,7 +227,7 @@ public class FeedSettingsFragment extends Fragment { feedPlaybackSpeedPreference.setEntries(entries); feedPlaybackSpeedPreference.setOnPreferenceChangeListener((preference, newValue) -> { feedPreferences.setFeedPlaybackSpeed(Float.parseFloat((String) newValue)); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); updatePlaybackSpeedPreference(); EventBus.getDefault().post( new SpeedPresetChangedEvent(feedPreferences.getFeedPlaybackSpeed(), feed.getId())); @@ -240,7 +241,7 @@ public class FeedSettingsFragment extends Fragment { @Override protected void onConfirmed(FeedFilter filter) { feedPreferences.setFilter(filter); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); } }.show(); return false; @@ -256,7 +257,7 @@ public class FeedSettingsFragment extends Fragment { protected void onConfirmed(String username, String password) { feedPreferences.setUsername(username); feedPreferences.setPassword(password); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); } }.show(); return false; @@ -276,7 +277,7 @@ public class FeedSettingsFragment extends Fragment { feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); break; } - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); updateAutoDeleteSummary(); return false; }); @@ -322,7 +323,7 @@ public class FeedSettingsFragment extends Fragment { feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_REDUCTION); break; } - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); updateVolumeReductionValue(); EventBus.getDefault().post( new VolumeAdaptionChangedEvent(feedPreferences.getVolumeAdaptionSetting(), feed.getId())); @@ -353,7 +354,7 @@ public class FeedSettingsFragment extends Fragment { pref.setOnPreferenceChangeListener((preference, newValue) -> { boolean checked = newValue == Boolean.TRUE; feedPreferences.setKeepUpdated(checked); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); pref.setChecked(checked); return false; }); @@ -384,7 +385,7 @@ public class FeedSettingsFragment extends Fragment { boolean checked = newValue == Boolean.TRUE; feedPreferences.setAutoDownload(checked); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); updateAutoDownloadEnabled(); ApplyToEpisodesDialog dialog = new ApplyToEpisodesDialog(getActivity(), checked); dialog.createNewDialog().show(); @@ -412,7 +413,7 @@ public class FeedSettingsFragment extends Fragment { feedPreferences.getTags().clear(); feedPreferences.getTags().addAll(new HashSet<>(Arrays.asList( foldersString.split(FeedPreferences.TAG_SEPARATOR)))); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); }) .setNegativeButton(R.string.cancel_label, null) .show(); @@ -420,6 +421,19 @@ public class FeedSettingsFragment extends Fragment { }); } + private void setupEpisodeNotificationPreference() { + SwitchPreferenceCompat pref = findPreference("episodeNotification"); + + pref.setChecked(feedPreferences.getShowEpisodeNotification()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + boolean checked = newValue == Boolean.TRUE; + feedPreferences.setShowEpisodeNotification(checked); + DBWriter.setFeedPreferences(feedPreferences); + pref.setChecked(checked); + return false; + }); + } + private class ApplyToEpisodesDialog extends ConfirmationDialog { private final boolean autoDownload; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 18a61f1e6..2e13bbd79 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -10,6 +10,9 @@ import android.view.View; import android.view.ViewGroup; import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.view.ShownotesWebView; @@ -82,7 +85,15 @@ public class ItemDescriptionFragment extends Fragment { webViewLoader.dispose(); } webViewLoader = Maybe.<String>create(emitter -> { - Timeline timeline = new Timeline(getActivity(), controller.getMedia()); + Playable media = controller.getMedia(); + if (media instanceof FeedMedia) { + FeedMedia feedMedia = ((FeedMedia) media); + if (feedMedia.getItem() == null) { + feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId())); + } + DBReader.loadDescriptionOfFeedItem(feedMedia.getItem()); + } + Timeline timeline = new Timeline(getActivity(), media.getDescription(), media.getDuration()); emitter.onSuccess(timeline.processShownotes()); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -140,14 +151,8 @@ public class ItemDescriptionFragment extends Fragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public boolean loadMediaInfo() { + public void loadMediaInfo() { load(); - return true; - } - - @Override - public void setupGUI() { - ItemDescriptionFragment.this.load(); } }; controller.init(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index 07f59bb42..224210d63 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -57,7 +57,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.view.ShownotesWebView; @@ -238,7 +238,12 @@ public class ItemFragment extends Fragment { public void onStart() { super.onStart(); EventBus.getDefault().register(this); - controller = new PlaybackController(getActivity()); + controller = new PlaybackController(getActivity()) { + @Override + public void loadMediaInfo() { + // Do nothing + } + }; controller.init(); } @@ -291,14 +296,19 @@ public class ItemFragment extends Fragment { txtvPublished.setContentDescription(DateUtils.formatForAccessibility(getContext(), item.getPubDate())); } + RequestOptions options = new RequestOptions() + .error(R.color.light_gray) + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .transforms(new FitCenter(), + new RoundedCorners((int) (4 * getResources().getDisplayMetrics().density))) + .dontAnimate(); + Glide.with(getActivity()) - .load(ImageResourceUtils.getImageLocation(item)) - .apply(new RequestOptions() - .error(R.color.light_gray) - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .transforms(new FitCenter(), - new RoundedCorners((int) (4 * getResources().getDisplayMetrics().density))) - .dontAnimate()) + .load(item.getImageLocation()) + .error(Glide.with(getActivity()) + .load(ImageResourceUtils.getFallbackImageLocation(item)) + .apply(options)) + .apply(options) .into(imgvCover); updateButtons(); } @@ -429,7 +439,9 @@ public class ItemFragment extends Fragment { FeedItem feedItem = DBReader.getFeedItem(itemId); Context context = getContext(); if (feedItem != null && context != null) { - Timeline t = new Timeline(context, feedItem); + int duration = feedItem.getMedia() != null ? feedItem.getMedia().getDuration() : Integer.MAX_VALUE; + DBReader.loadDescriptionOfFeedItem(feedItem); + Timeline t = new Timeline(context, feedItem.getDescription(), duration); webviewData = t.processShownotes(); } return feedItem; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java index 3d82bf7a1..e8c04336f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java @@ -424,7 +424,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS flatItemList = result.second; updateSelection(); // Selected item might be a feed navAdapter.notifyDataSetChanged(); - progressBar.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); // Stays hidden once there is something in the list }, error -> { Log.e(TAG, Log.getStackTraceString(error)); progressBar.setVisibility(View.GONE); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index 973fcb978..e97b7cd7f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -41,6 +41,7 @@ import java.util.List; public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuItemClickListener { public static final String TAG = "PlaybackHistoryFragment"; + private static final String KEY_UP_ARROW = "up_arrow"; private List<FeedItem> playbackHistory; private PlaybackHistoryListAdapter adapter; @@ -49,6 +50,7 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI private EmptyViewHandler emptyView; private ProgressBar progressBar; private Toolbar toolbar; + private boolean displayUpArrow; @Override public void onCreate(Bundle savedInstanceState) { @@ -63,7 +65,11 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI toolbar = root.findViewById(R.id.toolbar); toolbar.setTitle(R.string.playback_history_label); toolbar.setOnMenuItemClickListener(this); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); toolbar.inflateMenu(R.menu.playback_history); refreshToolbarState(); @@ -98,6 +104,12 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI } } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index 983bf4de1..2850acc15 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -12,6 +12,7 @@ import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; @@ -67,6 +68,7 @@ import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_REM */ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickListener { public static final String TAG = "QueueFragment"; + private static final String KEY_UP_ARROW = "up_arrow"; private TextView infoBar; private EpisodeItemListRecyclerView recyclerView; @@ -74,6 +76,7 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi private EmptyViewHandler emptyView; private ProgressBar progLoading; private Toolbar toolbar; + private boolean displayUpArrow; private List<FeedItem> queue; @@ -420,7 +423,11 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi View root = inflater.inflate(R.layout.queue_fragment, container, false); toolbar = root.findViewById(R.id.toolbar); toolbar.setOnMenuItemClickListener(this); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); toolbar.inflateMenu(R.menu.queue); MenuItemUtils.setupSearchItem(toolbar.getMenu(), (MainActivity) getActivity(), 0, ""); refreshToolbarState(); @@ -530,6 +537,12 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi return root; } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + private void onFragmentLoaded(final boolean restoreScrollPosition) { if (queue != null && queue.size() > 0) { if (recyclerAdapter == null) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java index bb00d88e1..3c529d941 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -8,6 +8,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.widget.ProgressBar; +import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; @@ -67,6 +68,8 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem public static final String TAG = "SubscriptionFragment"; private static final String PREFS = "SubscriptionFragment"; private static final String PREF_NUM_COLUMNS = "columns"; + private static final String KEY_UP_ARROW = "up_arrow"; + private static final int MIN_NUM_COLUMNS = 2; private static final int[] COLUMN_CHECKBOX_IDS = { R.id.subscription_num_columns_2, @@ -85,6 +88,7 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem private int mPosition = -1; private boolean isUpdatingFeeds = false; + private boolean displayUpArrow; private Disposable disposable; private SharedPreferences prefs; @@ -103,7 +107,11 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem View root = inflater.inflate(R.layout.fragment_subscriptions, container, false); toolbar = root.findViewById(R.id.toolbar); toolbar.setOnMenuItemClickListener(this); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); toolbar.inflateMenu(R.menu.subscriptions); for (int i = 0; i < COLUMN_CHECKBOX_IDS.length; i++) { // Do this in Java to localize numbers @@ -130,6 +138,12 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem return root; } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + private void refreshToolbarState() { int columns = prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()); toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[columns - MIN_NUM_COLUMNS]).setChecked(true); @@ -218,16 +232,19 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem disposable.dispose(); } emptyView.hide(); - progressBar.setVisibility(View.VISIBLE); disposable = Observable.fromCallable(DBReader::getNavDrawerData) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - navDrawerData = result; - subscriptionAdapter.notifyDataSetChanged(); - emptyView.updateVisibility(); - progressBar.setVisibility(View.GONE); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); + .subscribe( + result -> { + navDrawerData = result; + subscriptionAdapter.notifyDataSetChanged(); + emptyView.updateVisibility(); + progressBar.setVisibility(View.GONE); // Keep hidden to avoid flickering while refreshing + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + }); if (UserPreferences.getSubscriptionsFilter().isEnabled()) { feedsFilteredMsg.setText("{md-info-outline} " + getString(R.string.subscriptions_are_filtered)); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java index 1f5434688..7ee0936d0 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -1,10 +1,7 @@ package de.danoeh.antennapod.fragment.gpodnet; -import android.content.Context; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; -import androidx.fragment.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -13,9 +10,7 @@ import android.widget.Button; import android.widget.GridView; import android.widget.ProgressBar; import android.widget.TextView; - -import java.util.List; - +import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.OnlineFeedViewActivity; @@ -25,6 +20,12 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import java.util.List; /** * Displays a list of GPodnetPodcast-Objects in a GridView @@ -36,6 +37,7 @@ public abstract class PodcastListFragment extends Fragment { private ProgressBar progressBar; private TextView txtvError; private Button butRetry; + private Disposable disposable; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -64,60 +66,44 @@ public abstract class PodcastListFragment extends Fragment { protected abstract List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException; final void loadData() { - AsyncTask<Void, Void, List<GpodnetPodcast>> loaderTask = new AsyncTask<Void, Void, List<GpodnetPodcast>>() { - volatile Exception exception = null; - - @Override - protected List<GpodnetPodcast> doInBackground(Void... params) { - try { + if (disposable != null) { + disposable.dispose(); + } + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + disposable = Observable.fromCallable( + () -> { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHostname()); + GpodnetPreferences.getHosturl()); return loadPodcastData(service); - } catch (GpodnetServiceException e) { - exception = e; - e.printStackTrace(); - return null; - } - } - - @Override - protected void onPostExecute(List<GpodnetPodcast> gpodnetPodcasts) { - super.onPostExecute(gpodnetPodcasts); - final Context context = getActivity(); - if (context != null && gpodnetPodcasts != null && gpodnetPodcasts.size() > 0) { - PodcastListAdapter listAdapter = new PodcastListAdapter(context, 0, gpodnetPodcasts); - gridView.setAdapter(listAdapter); - listAdapter.notifyDataSetChanged(); - - progressBar.setVisibility(View.GONE); - gridView.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - butRetry.setVisibility(View.GONE); - } else if (context != null && gpodnetPodcasts != null) { - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - txtvError.setText(getString(R.string.search_status_no_results)); - txtvError.setVisibility(View.VISIBLE); - butRetry.setVisibility(View.GONE); - } else if (context != null) { - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - txtvError.setText(getString(R.string.error_msg_prefix) + exception.getMessage()); - txtvError.setVisibility(View.VISIBLE); - butRetry.setVisibility(View.VISIBLE); - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - butRetry.setVisibility(View.GONE); - } - }; - - loaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + podcasts -> { + progressBar.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + + if (podcasts.size() > 0) { + PodcastListAdapter listAdapter = new PodcastListAdapter(getContext(), 0, podcasts); + gridView.setAdapter(listAdapter); + listAdapter.notifyDataSetChanged(); + gridView.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + } else { + gridView.setVisibility(View.GONE); + txtvError.setText(getString(R.string.search_status_no_results)); + txtvError.setVisibility(View.VISIBLE); + } + }, error -> { + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtvError.setText(getString(R.string.error_msg_prefix) + error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setVisibility(View.VISIBLE); + Log.e(TAG, Log.getStackTraceString(error)); + }); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index 2c41ee070..9d0f99aa9 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -1,32 +1,34 @@ package de.danoeh.antennapod.fragment.gpodnet; -import android.content.Context; -import android.os.AsyncTask; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.fragment.app.ListFragment; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.gpodnet.TagListAdapter; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag; - -import java.util.List; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; public class TagListFragment extends ListFragment { private static final int COUNT = 50; + private static final String TAG = "TagListFragment"; + private Disposable disposable; @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getListView().setOnItemClickListener((parent, view1, position, id) -> { GpodnetTag tag = (GpodnetTag) getListAdapter().getItem(position); - MainActivity activity = (MainActivity) getActivity(); - activity.loadChildFragment(TagFragment.newInstance(tag)); + ((MainActivity) getActivity()).loadChildFragment(TagFragment.newInstance(tag)); }); startLoadTask(); @@ -35,59 +37,36 @@ public class TagListFragment extends ListFragment { @Override public void onDestroyView() { super.onDestroyView(); - cancelLoadTask(); - } - private AsyncTask<Void, Void, List<GpodnetTag>> loadTask; - - private void cancelLoadTask() { - if (loadTask != null && !loadTask.isCancelled()) { - loadTask.cancel(true); + if (disposable != null) { + disposable.dispose(); } } private void startLoadTask() { - cancelLoadTask(); - loadTask = new AsyncTask<Void, Void, List<GpodnetTag>>() { - private Exception exception; - - @Override - protected List<GpodnetTag> doInBackground(Void... params) { + if (disposable != null) { + disposable.dispose(); + } + setListShown(false); + disposable = Observable.fromCallable( + () -> { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHostname()); - try { - return service.getTopTags(COUNT); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - return null; - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - setListShown(false); - } - - @Override - protected void onPostExecute(List<GpodnetTag> gpodnetTags) { - super.onPostExecute(gpodnetTags); - final Context context = getActivity(); - if (context != null) { - if (gpodnetTags != null) { - setListAdapter(new TagListAdapter(context, android.R.layout.simple_list_item_1, gpodnetTags)); - } else if (exception != null) { + GpodnetPreferences.getHosturl()); + return service.getTopTags(COUNT); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + tags -> { + setListAdapter(new TagListAdapter(getContext(), android.R.layout.simple_list_item_1, tags)); + setListShown(true); + }, error -> { TextView txtvError = new TextView(getActivity()); - txtvError.setText(exception.getMessage()); + txtvError.setText(error.getMessage()); getListView().setEmptyView(txtvError); - } - setListShown(true); - - } - } - }; - loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + setListShown(true); + Log.e(TAG, Log.getStackTraceString(error)); + }); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java index 0d6e79e84..ec61c82f2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java @@ -174,7 +174,9 @@ public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat { String[] entries = new String[values.length]; for (int x = 0; x < values.length; x++) { int v = Integer.parseInt(values[x]); - if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { + if (v == UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE) { + entries[x] = res.getString(R.string.episode_cleanup_except_favorite_removal); + } else if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { entries[x] = res.getString(R.string.episode_cleanup_queue_removal); } else if (v == UserPreferences.EPISODE_CLEANUP_NULL){ entries[x] = res.getString(R.string.episode_cleanup_never); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java new file mode 100644 index 000000000..6eb19aff2 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java @@ -0,0 +1,307 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Paint; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.ViewFlipper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.textfield.TextInputLayout; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.IntentUtils; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Guides the user through the authentication process. + */ +public class GpodderAuthenticationFragment extends DialogFragment { + public static final String TAG = "GpodnetAuthActivity"; + + private ViewFlipper viewFlipper; + + private static final int STEP_DEFAULT = -1; + private static final int STEP_HOSTNAME = 0; + private static final int STEP_LOGIN = 1; + private static final int STEP_DEVICE = 2; + private static final int STEP_FINISH = 3; + + private int currentStep = -1; + + private GpodnetService service; + private volatile String username; + private volatile String password; + private volatile GpodnetDevice selectedDevice; + private List<GpodnetDevice> devices; + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder dialog = new AlertDialog.Builder(getContext()); + dialog.setTitle(GpodnetService.DEFAULT_BASE_HOST); + dialog.setNegativeButton(R.string.cancel_label, null); + dialog.setCancelable(false); + this.setCancelable(false); + + View root = View.inflate(getContext(), R.layout.gpodnetauth_dialog, null); + viewFlipper = root.findViewById(R.id.viewflipper); + advance(); + dialog.setView(root); + + return dialog.create(); + } + + private void setupHostView(View view) { + final Button selectHost = view.findViewById(R.id.chooseHostButton); + final RadioGroup serverRadioGroup = view.findViewById(R.id.serverRadioGroup); + final EditText serverUrlText = view.findViewById(R.id.serverUrlText); + + if (!GpodnetService.DEFAULT_BASE_HOST.equals(GpodnetPreferences.getHosturl())) { + serverUrlText.setText(GpodnetPreferences.getHosturl()); + } + final TextInputLayout serverUrlTextInput = view.findViewById(R.id.serverUrlTextInput); + serverRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { + serverUrlTextInput.setVisibility(checkedId == R.id.customServerRadio ? View.VISIBLE : View.GONE); + }); + selectHost.setOnClickListener(v -> { + if (serverRadioGroup.getCheckedRadioButtonId() == R.id.customServerRadio) { + GpodnetPreferences.setHosturl(serverUrlText.getText().toString()); + } else { + GpodnetPreferences.setHosturl(GpodnetService.DEFAULT_BASE_HOST); + } + service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHosturl()); + getDialog().setTitle(GpodnetPreferences.getHosturl()); + advance(); + }); + } + + private void setupLoginView(View view) { + final EditText username = view.findViewById(R.id.etxtUsername); + final EditText password = view.findViewById(R.id.etxtPassword); + final Button login = view.findViewById(R.id.butLogin); + final TextView txtvError = view.findViewById(R.id.credentialsError); + final ProgressBar progressBar = view.findViewById(R.id.progBarLogin); + final TextView createAccount = view.findViewById(R.id.createAccountButton); + final TextView createAccountWarning = view.findViewById(R.id.createAccountWarning); + + createAccount.setPaintFlags(createAccount.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + createAccount.setOnClickListener(v -> IntentUtils.openInBrowser(getContext(), "https://gpodder.net/register/")); + + if (GpodnetPreferences.getHosturl().startsWith("http://")) { + createAccountWarning.setVisibility(View.VISIBLE); + } + password.setOnEditorActionListener((v, actionID, event) -> + actionID == EditorInfo.IME_ACTION_GO && login.performClick()); + + login.setOnClickListener(v -> { + final String usernameStr = username.getText().toString(); + final String passwordStr = password.getText().toString(); + + if (usernameHasUnwantedChars(usernameStr)) { + txtvError.setText(R.string.gpodnetsync_username_characters_error); + txtvError.setVisibility(View.VISIBLE); + return; + } + + login.setEnabled(false); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + InputMethodManager inputManager = (InputMethodManager) getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(login.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + + Completable.fromAction(() -> { + service.authenticate(usernameStr, passwordStr); + devices = service.getDevices(); + GpodderAuthenticationFragment.this.username = usernameStr; + GpodderAuthenticationFragment.this.password = passwordStr; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + advance(); + }, error -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.getCause().getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + + }); + } + + private void setupDeviceView(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final LinearLayout devicesContainer = view.findViewById(R.id.devicesContainer); + deviceName.setText(generateDeviceName()); + + MaterialButton createDeviceButton = view.findViewById(R.id.createDeviceButton); + createDeviceButton.setOnClickListener(v -> createDevice(view)); + + for (GpodnetDevice device : devices) { + View row = View.inflate(getContext(), R.layout.gpodnetauth_device_row, null); + Button selectDeviceButton = row.findViewById(R.id.selectDeviceButton); + selectDeviceButton.setOnClickListener(v -> { + selectedDevice = device; + advance(); + }); + selectDeviceButton.setText(device.getCaption()); + devicesContainer.addView(row); + } + } + + private void createDevice(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final TextView txtvError = view.findViewById(R.id.deviceSelectError); + final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice); + + String deviceNameStr = deviceName.getText().toString(); + if (isDeviceInList(deviceNameStr)) { + return; + } + progBarCreateDevice.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + deviceName.setEnabled(false); + + Observable.fromCallable(() -> { + String deviceId = generateDeviceId(deviceNameStr); + service.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE); + return new GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(device -> { + progBarCreateDevice.setVisibility(View.GONE); + selectedDevice = device; + advance(); + }, error -> { + deviceName.setEnabled(true); + progBarCreateDevice.setVisibility(View.GONE); + txtvError.setText(error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + } + + private String generateDeviceName() { + String baseName = getString(R.string.gpodnetauth_device_name_default, Build.MODEL); + String name = baseName; + int num = 1; + while (isDeviceInList(name)) { + name = baseName + " (" + num + ")"; + num++; + } + return name; + } + + private String generateDeviceId(String name) { + // devices names must be of a certain form: + // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices + return FileNameGenerator.generateFileName(name).replaceAll("\\W", "_").toLowerCase(Locale.US); + } + + private boolean isDeviceInList(String name) { + if (devices == null) { + return false; + } + String id = generateDeviceId(name); + for (GpodnetDevice device : devices) { + if (device.getId().equals(id) || device.getCaption().equals(name)) { + return true; + } + } + return false; + } + + private GpodnetDevice findDevice(String id) { + if (devices == null) { + return null; + } + for (GpodnetDevice device : devices) { + if (device.getId().equals(id)) { + return device; + } + } + return null; + } + + private void setupFinishView(View view) { + final Button sync = view.findViewById(R.id.butSyncNow); + + sync.setOnClickListener(v -> { + dismiss(); + SyncService.sync(getContext()); + }); + } + + private void writeLoginCredentials() { + GpodnetPreferences.setUsername(username); + GpodnetPreferences.setPassword(password); + GpodnetPreferences.setDeviceID(selectedDevice.getId()); + } + + private void advance() { + if (currentStep < STEP_FINISH) { + + View view = viewFlipper.getChildAt(currentStep + 1); + if (currentStep == STEP_DEFAULT) { + setupHostView(view); + } else if (currentStep == STEP_HOSTNAME) { + setupLoginView(view); + } else if (currentStep == STEP_LOGIN) { + if (username == null || password == null) { + throw new IllegalStateException("Username and password must not be null here"); + } else { + setupDeviceView(view); + } + } else if (currentStep == STEP_DEVICE) { + if (selectedDevice == null) { + throw new IllegalStateException("Device must not be null here"); + } else { + writeLoginCredentials(); + setupFinishView(view); + } + } + if (currentStep != STEP_DEFAULT) { + viewFlipper.showNext(); + } + currentStep++; + } else { + dismiss(); + } + } + + private boolean usernameHasUnwantedChars(String username) { + Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); + Matcher containsUnwantedChars = special.matcher(username); + return containsUnwantedChars.find(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java index eb23a5eb1..4fb734e17 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java @@ -14,19 +14,16 @@ import de.danoeh.antennapod.core.event.SyncServiceEvent; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.sync.SyncService; import de.danoeh.antennapod.dialog.AuthenticationDialog; -import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; - public class GpodderPreferencesFragment extends PreferenceFragmentCompat { private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate"; private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"; private static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync"; private static final String PREF_GPODNET_FORCE_FULL_SYNC = "pref_gpodnet_force_full_sync"; private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout"; - private static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname"; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -51,6 +48,7 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void syncStatusChanged(SyncServiceEvent event) { + updateGpodnetPreferenceScreen(); if (!GpodnetPreferences.loggedIn()) { return; } @@ -66,6 +64,10 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { private void setupGpodderScreen() { final Activity activity = getActivity(); + findPreference(PREF_GPODNET_LOGIN).setOnPreferenceClickListener(preference -> { + new GpodderAuthenticationFragment().show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG); + return true; + }); findPreference(PREF_GPODNET_SETLOGIN_INFORMATION) .setOnPreferenceClickListener(preference -> { AuthenticationDialog dialog = new AuthenticationDialog(activity, @@ -94,11 +96,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { updateGpodnetPreferenceScreen(); return true; }); - findPreference(PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener(preference -> { - GpodnetSetHostnameDialog.createDialog(activity).setOnDismissListener( - dialog -> updateGpodnetPreferenceScreen()); - return true; - }); } private void updateGpodnetPreferenceScreen() { @@ -119,7 +116,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { } else { findPreference(PREF_GPODNET_LOGOUT).setSummary(null); } - findPreference(PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname()); } private void updateLastGpodnetSyncReport(boolean successful, long lastTime) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java index 77f8063f2..3889034fa 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java @@ -71,13 +71,13 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat { Context context = getActivity().getApplicationContext(); String val; long interval = UserPreferences.getUpdateInterval(); - if(interval > 0) { + if (interval > 0) { int hours = (int) TimeUnit.MILLISECONDS.toHours(interval); - String hoursStr = context.getResources().getQuantityString(R.plurals.time_hours_quantified, hours, hours); - val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_every), hoursStr); + val = context.getResources().getQuantityString( + R.plurals.pref_autoUpdateIntervallOrTime_every_hours, hours, hours); } else { int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); - if(timeOfDay.length == 2) { + if (timeOfDay.length == 2) { Calendar cal = new GregorianCalendar(); cal.set(Calendar.HOUR_OF_DAY, timeOfDay[0]); cal.set(Calendar.MINUTE, timeOfDay[1]); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java index 689a72ba7..4d1b79965 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java @@ -9,11 +9,14 @@ import androidx.preference.PreferenceFragmentCompat; import android.widget.ListView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.core.event.PlayerStatusEvent; +import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; import de.danoeh.antennapod.dialog.FeedSortDialog; import de.danoeh.antennapod.fragment.NavDrawerFragment; import org.apache.commons.lang3.ArrayUtils; +import org.greenrobot.eventbus.EventBus; import java.util.List; @@ -37,8 +40,17 @@ public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { (preference, newValue) -> { getActivity().recreate(); return true; - } - ); + }); + + findPreference(UserPreferences.PREF_SHOW_TIME_LEFT) + .setOnPreferenceChangeListener( + (preference, newValue) -> { + UserPreferences.setShowRemainTimeSetting((Boolean) newValue); + EventBus.getDefault().post(new UnreadItemsUpdateEvent()); + EventBus.getDefault().post(new PlayerStatusEvent()); + return true; + }); + findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS) .setOnPreferenceClickListener(preference -> { showDrawerPreferencesDialog(); diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java index 9c54a529b..fbfdf537f 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java @@ -9,7 +9,7 @@ import androidx.appcompat.widget.SearchView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.fragment.SearchFragment; import java.util.HashMap; diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java index 311f44881..03a8edbf0 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.preferences; import android.content.Context; import android.content.SharedPreferences; +import android.view.KeyEvent; import androidx.preference.PreferenceManager; import de.danoeh.antennapod.BuildConfig; @@ -92,5 +93,16 @@ public class PreferenceUpgrader { if (oldVersion < 1080100) { prefs.edit().putString(UserPreferences.PREF_VIDEO_BEHAVIOR, "pip").apply(); } + if (oldVersion < 2010300) { + // Migrate hardware button preferences + if (prefs.getBoolean("prefHardwareForwardButtonSkips", false)) { + prefs.edit().putString(UserPreferences.PREF_HARDWARE_FORWARD_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_NEXT)).apply(); + } + if (prefs.getBoolean("prefHardwarePreviousButtonRestarts", false)) { + prefs.edit().putString(UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_PREVIOUS)).apply(); + } + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java b/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java new file mode 100644 index 000000000..5e80198d5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java @@ -0,0 +1,129 @@ +package de.danoeh.antennapod.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import de.danoeh.antennapod.ui.common.ThemeUtils; + +public class ChapterSeekBar extends androidx.appcompat.widget.AppCompatSeekBar { + + private float top; + private float width; + private float bottom; + private float density; + private float progressPrimary; + private float progressSecondary; + private float[] dividerPos; + private final Paint paintBackground = new Paint(); + private final Paint paintProgressPrimary = new Paint(); + private final Paint paintProgressSecondary = new Paint(); + + public ChapterSeekBar(Context context) { + super(context); + init(context); + } + + public ChapterSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ChapterSeekBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + setBackground(null); // Removes the thumb shadow + dividerPos = null; + density = context.getResources().getDisplayMetrics().density; + paintBackground.setColor(ThemeUtils.getColorFromAttr(getContext(), + de.danoeh.antennapod.core.R.attr.currently_playing_background)); + paintBackground.setAlpha(128); + paintProgressPrimary.setColor(ThemeUtils.getColorFromAttr(getContext(), + de.danoeh.antennapod.core.R.attr.colorPrimary)); + paintProgressSecondary.setColor(ThemeUtils.getColorFromAttr(getContext(), + de.danoeh.antennapod.core.R.attr.seek_background)); + } + + /** + * Sets the relative positions of the chapter dividers. + * @param dividerPos of the chapter dividers relative to the duration of the media. + */ + public void setDividerPos(final float[] dividerPos) { + if (dividerPos != null) { + this.dividerPos = new float[dividerPos.length + 2]; + this.dividerPos[0] = 0; + System.arraycopy(dividerPos, 0, this.dividerPos, 1, dividerPos.length); + this.dividerPos[this.dividerPos.length - 1] = 1; + } else { + this.dividerPos = null; + } + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + top = getTop() + density * 7.5f; + bottom = getBottom() - density * 7.5f; + width = (float) (getRight() - getPaddingRight() - getLeft() - getPaddingLeft()); + progressSecondary = getSecondaryProgress() / (float) getMax() * width; + progressPrimary = getProgress() / (float) getMax() * width; + + if (dividerPos == null) { + drawProgress(canvas); + } else { + drawProgressChapters(canvas); + } + drawThumb(canvas); + } + + private void drawProgress(Canvas canvas) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft(), getPaddingTop()); + canvas.drawRect(0, top, width, bottom, paintBackground); + canvas.drawRect(0, top, progressSecondary, bottom, paintProgressSecondary); + canvas.drawRect(0, top, progressPrimary, bottom, paintProgressPrimary); + canvas.restoreToCount(saveCount); + } + + private void drawProgressChapters(Canvas canvas) { + final int saveCount = canvas.save(); + int currChapter = 1; + float chapterMargin = density * 0.6f; + float topExpanded = getTop() + density * 7; + float bottomExpanded = getBottom() - density * 7; + + canvas.translate(getPaddingLeft(), getPaddingTop()); + + for (int i = 1; i < dividerPos.length; i++) { + float right = dividerPos[i] * width - chapterMargin; + float left = dividerPos[i - 1] * width + chapterMargin; + float rightCurr = dividerPos[currChapter] * width - chapterMargin; + float leftCurr = dividerPos[currChapter - 1] * width + chapterMargin; + + canvas.drawRect(left, top, right, bottom, paintBackground); + + if (right < progressPrimary) { + currChapter = i + 1; + canvas.drawRect(left, top, right, bottom, paintProgressPrimary); + } else if (isPressed()) { + canvas.drawRect(leftCurr, topExpanded, rightCurr, bottomExpanded, paintBackground); + canvas.drawRect(leftCurr, topExpanded, progressPrimary, bottomExpanded, paintProgressPrimary); + } else { + if (progressSecondary > leftCurr) { + canvas.drawRect(leftCurr, top, progressSecondary, bottom, paintProgressSecondary); + } + canvas.drawRect(leftCurr, top, progressPrimary, bottom, paintProgressPrimary); + } + } + canvas.restoreToCount(saveCount); + } + + private void drawThumb(Canvas canvas) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop()); + getThumb().draw(canvas); + canvas.restoreToCount(saveCount); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java b/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java index 83d90f98b..fb1c533c5 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java +++ b/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java @@ -6,9 +6,9 @@ import android.content.res.Configuration; import android.util.AttributeSet; import android.view.View; import androidx.appcompat.view.ContextThemeWrapper; +import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; import de.danoeh.antennapod.R; import io.reactivex.annotations.Nullable; @@ -39,7 +39,7 @@ public class EpisodeItemListRecyclerView extends RecyclerView { layoutManager.setRecycleChildrenOnDetach(true); setLayoutManager(layoutManager); setHasFixedSize(true); - addItemDecoration(new HorizontalDividerItemDecoration.Builder(getContext()).build()); + addItemDecoration(new DividerItemDecoration(getContext(), layoutManager.getOrientation())); setClipToPadding(false); } diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java index 274dd4ea8..0e446fb84 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java @@ -20,6 +20,7 @@ public class DownloadItemViewHolder extends RecyclerView.ViewHolder { public final TextView type; public final TextView date; public final TextView reason; + public final TextView tapForDetails; public DownloadItemViewHolder(Context context, ViewGroup parent) { super(LayoutInflater.from(context).inflate(R.layout.downloadlog_item, parent, false)); @@ -27,6 +28,7 @@ public class DownloadItemViewHolder extends RecyclerView.ViewHolder { type = itemView.findViewById(R.id.txtvType); icon = itemView.findViewById(R.id.txtvIcon); reason = itemView.findViewById(R.id.txtvReason); + tapForDetails = itemView.findViewById(R.id.txtvTapForDetails); secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); title = itemView.findViewById(R.id.txtvTitle); diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java index 35744227f..8b46a781f 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java @@ -13,9 +13,7 @@ import android.widget.TextView; import androidx.cardview.widget.CardView; import androidx.recyclerview.widget.RecyclerView; - import com.joanzapata.iconify.Iconify; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.CoverLoader; @@ -25,13 +23,15 @@ import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; -import de.danoeh.antennapod.view.CircularProgressBar; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.common.CircularProgressBar; /** * Holds the view which shows FeedItems. @@ -121,8 +121,8 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { if (coverHolder.getVisibility() == View.VISIBLE) { new CoverLoader(activity) - .withUri(ImageResourceUtils.getImageLocation(item)) - .withFallbackUri(item.getFeed().getImageLocation()) + .withUri(ImageResourceUtils.getEpisodeListImageLocation(item)) + .withFallbackUri(item.getFeed().getImageUrl()) .withPlaceholderView(placeholder) .withCoverView(cover) .load(); @@ -132,9 +132,6 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { private void bind(FeedMedia media) { isVideo.setVisibility(media.getMediaType() == MediaType.VIDEO ? View.VISIBLE : View.GONE); duration.setVisibility(media.getDuration() > 0 ? View.VISIBLE : View.GONE); - duration.setText(Converter.getDurationStringLong(media.getDuration())); - duration.setContentDescription(activity.getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(activity, media.getDuration()))); if (media.isCurrentlyPlaying()) { itemView.setBackgroundColor(ThemeUtils.getColorFromAttr(activity, R.attr.currently_playing_background)); @@ -152,6 +149,9 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { secondaryActionProgress.setPercentage(0, item); // Animate X% -> 0% } + duration.setText(Converter.getDurationStringLong(media.getDuration())); + duration.setContentDescription(activity.getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(activity, media.getDuration()))); if (item.getState() == FeedItem.State.PLAYING || item.getState() == FeedItem.State.IN_PROGRESS) { int progress = (int) (100.0 * media.getPosition() / media.getDuration()); progressBar.setProgress(progress); @@ -160,6 +160,11 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { Converter.getDurationStringLocalized(activity, media.getPosition()))); progressBar.setVisibility(View.VISIBLE); position.setVisibility(View.VISIBLE); + if (UserPreferences.shouldShowRemainingTime()) { + duration.setText("-" + Converter.getDurationStringLong(media.getDuration() - media.getPosition())); + duration.setContentDescription(activity.getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(activity, (media.getDuration() - media.getPosition())))); + } } else { progressBar.setVisibility(View.GONE); position.setVisibility(View.GONE); @@ -186,6 +191,22 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { } } + private void updateDuration(PlaybackPositionEvent event) { + int currentPosition = event.getPosition(); + int timeDuration = event.getDuration(); + int remainingTime = event.getDuration() - event.getPosition(); + Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); + if (currentPosition == PlaybackService.INVALID_TIME || timeDuration == PlaybackService.INVALID_TIME) { + Log.w(TAG, "Could not react to position observer update because of invalid time"); + return; + } + if (UserPreferences.shouldShowRemainingTime()) { + duration.setText("-" + Converter.getDurationStringLong(remainingTime)); + } else { + duration.setText(Converter.getDurationStringLong(timeDuration)); + } + } + public FeedItem getFeedItem() { return item; } @@ -197,7 +218,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); position.setText(Converter.getDurationStringLong(event.getPosition())); - duration.setText(Converter.getDurationStringLong(event.getDuration())); + updateDuration(event); duration.setVisibility(View.VISIBLE); // Even if the duration was previously unknown, it is now known } diff --git a/app/src/main/play/listings/cs-CZ/full-description.txt b/app/src/main/play/listings/cs-CZ/full-description.txt index 8411a2e7f..dc71e754c 100644 --- a/app/src/main/play/listings/cs-CZ/full-description.txt +++ b/app/src/main/play/listings/cs-CZ/full-description.txt @@ -2,9 +2,9 @@ AntennaPod je správce a přehrávač podcastů, co vám umožňuje okamžitý p Stahujte, streamujte nebo si vytvořte frontu epizod a užijte si poslech tak, jak ho máte rádi s nastavitelnou rychlostí přehrávání, podporou kapitol a s časovačem vypnutí. Ušetři si námahu, baterku i mobilní data s pomocí robustní automatické kontroly nad stahováním epizod (urči časy, intervaly a WIFI sítě) a mazáním epizod (na základě oblíbenosti a nastavení zpoždění). -Vytvořeno nadšenci do podcastů, AntennaPod je otevřený software (OSS), zdarma a bez reklam. +Vytvořeno nadšenci do podcastů, AntennaPod je software s otevřeným zdrojovým kódem, zdarma a bez reklam. -<b>Importujte, zorganizujte a přehrávejte</b> +<b>Importujte, organizujte a přehrávejte</b> • Ovládejte přehrávání odkudkoli: z widgetu na domovské obrazovce, z oznámení nebo pomocí tlačítek na sluchátkách včetně Bluetooth • Přidejte a importujte podcasty přes iTunes anebo gPodder.net, OPML soubory a RSS anebo Atom odkazy • Užijte si poslech s nastavitelnou rychlostí přehrávání, podporou kapitol, zapamatování poslední pozice přehrávání a pokročilým časovačem vypnutí (restart zatřesením, snížení hlasitosti) @@ -21,7 +21,7 @@ Vytvořeno nadšenci do podcastů, AntennaPod je otevřený software (OSS), zdar • Přizpůsobte si aplikaci svému prostředí pomocí světlého nebo tmavého motivu • Zálohujte své sbírky pomocí služby gPodder.net nebo exportem OPML souborů -<b>Přidejte se do komunity AntennaPodu!</b> +<b>Přidejte se ke komunitě AntennaPod!</b> AntennaPod je aktivně vyvíjen dobrovolníky. Můžete přispět také svým kódem nebo komentáři! Naše přátelská členská základna vám ráda zodpoví jakékoli dotazy. Zveme vás k diskuzi o AntennaPodu nebo i podcastech obecně. diff --git a/app/src/main/play/listings/de-DE/full-description.txt b/app/src/main/play/listings/de-DE/full-description.txt index b342dcdee..bd1c7cb1b 100644 --- a/app/src/main/play/listings/de-DE/full-description.txt +++ b/app/src/main/play/listings/de-DE/full-description.txt @@ -1,4 +1,4 @@ -AntennaPod ist ein Podcast-Manager und -Player, der dir unmittelbar Zugriff auf Millionen von freien und bezahlten Podcasts ermöglicht. Angefangen von unabhängigen Podcastern zu großen Rundfunkanstalten oder Hörfunksendern wie BBC, NPR und CNN. Abonniere, importiere und exportiere deine Feeds mühelos mit Hilfe des iTunes-Verzeichnisses, OPML-Dateien oder einfachen RSS-URLs. +AntennaPod ist ein Podcast-Manager und -Player, mit dem Du direkten Zugriff auf Millionen von freien und kostenpflichtigen Podcasts hast. Angefangen von unabhängigen Podcastern zu großen Rundfunkanstalten oder Hörfunksendern wie BBC, NPR und CNN. Abonniere, importiere und exportiere deine Feeds mühelos mit Hilfe des iTunes-Podcast-Verzeichnisses, OPML-Dateien oder RSS-URLs. Downloade, streame oder sortiere Episoden in der Abspielliste und genieße sie mit einstellbarer Abspielgeschwindigkeit, Unterstützung von Kapiteln und Schlummerfunktion. Reduziere Aufwand, Stromverbrauch und Datenverbrauch durch leistungsfähige Kontrolle der Downloads (bestimmte Uhrzeiten, Intervalle, WiFi-Netze) und des Löschens (basierend auf deinen Favoriten und weiteren Einstellungen). diff --git a/app/src/main/play/listings/en-US/graphics/promo-graphic/promo-graphic.png b/app/src/main/play/listings/en-US/graphics/promo-graphic/promo-graphic.png Binary files differdeleted file mode 100644 index 77a6e1c70..000000000 --- a/app/src/main/play/listings/en-US/graphics/promo-graphic/promo-graphic.png +++ /dev/null diff --git a/app/src/main/play/listings/pl-PL/full-description.txt b/app/src/main/play/listings/pl-PL/full-description.txt index 8c7236fac..efa98abd1 100644 --- a/app/src/main/play/listings/pl-PL/full-description.txt +++ b/app/src/main/play/listings/pl-PL/full-description.txt @@ -24,8 +24,8 @@ Dodawaj i importuj kanały z iTunes i gPodder.net, plików OPML oraz z adresów <b>Dołącz do społeczności AntennaPod</b> AntennaPod jest ciągle rozwijane przez ochotników. Ty też możesz pomóc, kodem lub komentarzem! -Chcesz zgłosić błąd lub brakuje Ci jakiejś funkcji, a może programujesz? Odwiedź nasz GitHub: -https://www.github.com/AntennaPod/AntennaPod +Życzliwi użytkownicy forum chętnie odpowiedzą na twoje pytania. Zapraszamy do rozmów o funkcjach programu i generalnie o podcastingu. +https://forum.antennapod.org/ Chcesz pomóc tłumaczyć AntennaPod - możesz to zrobić na Transifex: https://www.transifex.com/antennapod/antennapod
\ No newline at end of file diff --git a/app/src/main/play/listings/sk/full-description.txt b/app/src/main/play/listings/sk/full-description.txt new file mode 100644 index 000000000..c929fa327 --- /dev/null +++ b/app/src/main/play/listings/sk/full-description.txt @@ -0,0 +1,31 @@ +AntennaPod je správca a prehrávač podcastov, ktorý vám sprostredkuje okamžitý prístup k miliónom bezplatným a spoplatneným podcastom - od nezávislých podcastérov až k vydavateľstvám ako BBC, NPR a CNN. Pridávajte, importujte a exportujte ich zdroje bez problémov pomocou databázy podcastov iTunes, OPML súborov alebo odkazov na RSS. +Stiahnite, streamujte alebo plánujte epizódy a užívajte si ich ako sa vám páči s nastaviteľnou rýchlosťou prehrávania, podporou kapitol a časovačom vypnutia. +Ušetrite si námahu, baterku a mobilné dáta pomocou výkonnej automatickej kontroly sťahovania epizód (určite čas, intervaly a WiFi siete) a mazania epizód (založené na obľúbených epizódach a nastavení oneskorenia). + +AntennaPod je spravovaný podcastovými entuziastami a je slobodný v každom slova zmysle: otvorený zdrojový kód, zadarmo, bez reklamy. + +<b>Importovať, spravovať a prehrať</b> +• Ovládajte prehrávanie odkiaľkoľvek: domovská obrazovka, systémové upozornenia a handsfree a ovládanie cez bluetooth +• Pridať a importovať zdroje pomocou priečinkov iTunes a gPodder.net, OPML súborov a odkazov RSS alebo Atom +• Užívajte si počúvanie s nastaviteľnou rýchlosťou prehrávania, podporou kapitol, zapamätanou pozíciou prehrávania a pokročilým nastavením vypnutia (zatrasením resetuj, stíš hlasitosť) +Počúvajte heslom chránené zdroje a epizódy + +<b>Sledovať, zdielať a oceniť</b> +• Sledujte najlepšie z najlepších pomocou označenia ako obľúbené +• Nájdite epizódu v histórií prehrávaní alebo v nadpisoch a popisoch +• Zdielajte epizódy a zdroje pomocou pokročilých nastavení sociálnych sietí a e-mailu, služieb gPodder.net a cez export do OPML súboru + +<b>Spravovať systém</b> +• Spravujte automatické sťahovanie: vyberte zdroje, vylúčte mobilné siete, vyberte konkrétne WiFi siete, vyžadujte nabíjanie telefónu a nastavte časy a intervaly +• Spravujte úložisko nastavením počtu uložených epizód, inteligentným mazaním a zvolením umiestnenia súborov +• Prispôsobte si vzhľad na svetlý alebo tmavý +• Zálohujte si odbery pomocou gPodder.net a OPML exportu + +<b>Pridať sa do komunity AntennaPod!</b> +AntennaPod aktívne vyvíjajú dobrovoľníci. Tiež môžete prispieť kódom alebo komentárom. + +Na našom fóre môžete v priateľskej atmosfére diskutovať a nájsť odpovede na vaše otázky o nových funkciách alebo všeobecne o podcastingu. +https://forum.antennapod.org/ + +Transifex je miesto, kde môžete pomôcť s prekladom: +https://www.transifex.com/antennapod/antennapod
\ No newline at end of file diff --git a/app/src/main/play/listings/sk/short-description.txt b/app/src/main/play/listings/sk/short-description.txt new file mode 100644 index 000000000..d0162e6f9 --- /dev/null +++ b/app/src/main/play/listings/sk/short-description.txt @@ -0,0 +1 @@ +Ľahko použíteľný, flexibilný a open source správca a prehrávač podcastov
\ No newline at end of file diff --git a/app/src/main/play/listings/sk/title.txt b/app/src/main/play/listings/sk/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/sk/title.txt @@ -0,0 +1 @@ +AntennaPod
\ No newline at end of file diff --git a/app/src/main/play/release-notes/en-US/default.txt b/app/src/main/play/release-notes/en-US/default.txt index c911a4f1f..4b4805c6a 100644 --- a/app/src/main/play/release-notes/en-US/default.txt +++ b/app/src/main/play/release-notes/en-US/default.txt @@ -1,7 +1,12 @@ -- A long-standing wish of many: playing local files! In the 'Add podcast' screen simply tap 'Add local folder' and select a location on your phone! (@ByteHamster, @igoralmeida & @damoasda) -- Pick a country for the 'Discover' screen (@tonytamsf) -- Keyboard shortcuts (@asdoi) -- Search the PodcastIndex.org database (@edwinhere) -- Pull to refresh (@asdoi) -- Playback speed & filter dialogs (@ByteHamster & @bws9000) -- Smooth sleep timer volume (@olivoto) +NEW +- Optional notifications for new episodes (@connectety) +- Use PodcastIndex for main search (@tonytamsf) +- Sleep timer extend buttons (@max-wittig) +- Optional rewind, forward & skip buttons on widget (@tonytamsf) +- 'When not favorited' as Episode Cleanup (@spacecowboy) + +IMPROVED +- More actions for hardware buttons (@timakro) +- Android Auto & chapter support (@tonytamsf, @ByteHamster) +- Fixed stuck notification (@a1291762) +- Player screen usability for visually impaired (@ByteHamster) diff --git a/app/src/main/res/layout/activity_widget_config.xml b/app/src/main/res/layout/activity_widget_config.xml index ca8aba52d..6e31aec0d 100644 --- a/app/src/main/res/layout/activity_widget_config.xml +++ b/app/src/main/res/layout/activity_widget_config.xml @@ -22,7 +22,7 @@ android:id="@+id/widget_config_preview" layout="@layout/player_widget" android:layout_width="match_parent" - android:layout_height="80dp" + android:layout_height="96dp" android:layout_gravity="center" android:layout_margin="16dp" /> </FrameLayout> @@ -68,13 +68,38 @@ android:max="100" android:progress="100" /> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <CheckBox + android:id="@+id/ckRewind" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Rewind" /> + + <CheckBox + android:id="@+id/ckFastForward" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Forward" /> + + <CheckBox + android:id="@+id/ckSkip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Skip" /> + </LinearLayout> <Button android:id="@+id/butConfirm" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/widget_create_button" /> - </LinearLayout> </LinearLayout> diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml index 3b065cefc..f77e96338 100644 --- a/app/src/main/res/layout/audioplayer_fragment.xml +++ b/app/src/main/res/layout/audioplayer_fragment.xml @@ -51,6 +51,34 @@ app:tint="?android:attr/windowBackground" android:importantForAccessibility="no"/> + <androidx.cardview.widget.CardView + android:id="@+id/cardViewSeek" + android:alpha="0" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@+id/pager" + android:layout_centerHorizontal="true" + android:layout_marginBottom="12dp" + app:cardCornerRadius="8dp" + app:cardBackgroundColor="?attr/seek_background" + app:cardElevation="0dp" + tools:alpha="1"> + + <TextView + android:id="@+id/txtvSeek" + android:gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="24dp" + android:paddingTop="4dp" + android:paddingRight="24dp" + android:paddingBottom="4dp" + android:textColor="@color/white" + android:textSize="24sp" + tools:text="1:06:29" /> + + </androidx.cardview.widget.CardView> + <LinearLayout android:id="@+id/playtime_layout" android:layout_width="match_parent" @@ -60,7 +88,7 @@ android:layoutDirection="ltr" android:orientation="vertical"> - <SeekBar + <de.danoeh.antennapod.view.ChapterSeekBar android:id="@+id/sbPosition" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -128,7 +156,7 @@ android:scaleType="fitCenter" tools:srcCompat="@drawable/ic_av_play_white_24dp"/> - <de.danoeh.antennapod.view.CircularProgressBar + <de.danoeh.antennapod.ui.common.CircularProgressBar android:layout_width="@dimen/audioplayer_playercontrols_length_big" android:layout_height="@dimen/audioplayer_playercontrols_length_big" android:layout_marginLeft="16dp" @@ -136,7 +164,8 @@ android:layout_marginRight="16dp" android:layout_marginEnd="16dp" android:layout_centerHorizontal="true" - android:layout_centerVertical="true"/> + android:layout_centerVertical="true" + app:foregroundColor="?attr/action_icon_color"/> <ProgressBar style="?android:attr/progressBarStyle" @@ -177,7 +206,7 @@ android:textColor="?android:attr/textColorSecondary" android:clickable="false"/> - <de.danoeh.antennapod.view.PlaybackSpeedIndicatorView + <de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView android:id="@+id/butPlaybackSpeed" android:layout_width="@dimen/audioplayer_playercontrols_length" android:layout_height="@dimen/audioplayer_playercontrols_length" @@ -186,7 +215,8 @@ android:layout_centerVertical="true" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="@string/playback_speed" - tools:srcCompat="@drawable/ic_playback_speed_white"/> + tools:srcCompat="@drawable/ic_playback_speed_white" + app:foregroundColor="?attr/action_icon_color"/> <TextView android:id="@+id/txtvPlaybackSpeed" diff --git a/app/src/main/res/layout/authentication_dialog.xml b/app/src/main/res/layout/authentication_dialog.xml index 9c6f3e2bb..f311fc1dd 100644 --- a/app/src/main/res/layout/authentication_dialog.xml +++ b/app/src/main/res/layout/authentication_dialog.xml @@ -1,30 +1,59 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" > - - <EditText - android:id="@+id/etxtUsername" - android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" - android:layout_margin="16dp" - android:hint="@string/username_label" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true"/> - - <EditText - android:id="@+id/etxtPassword" +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" - android:layout_margin="16dp" - android:inputType="textPassword" - android:hint="@string/password_label" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true"/> + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp"> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="8dp"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/usernameEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/username_label" + android:lines="1"/> + + </com.google.android.material.textfield.TextInputLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/passwordEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/password_label" + android:inputType="textPassword" + android:lines="1"/> + + </com.google.android.material.textfield.TextInputLayout> + + <com.joanzapata.iconify.widget.IconTextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/showPasswordButton" + android:text="{fa-eye}" + android:padding="8dp" + android:textColor="?android:attr/textColorPrimary" + android:background="?attr/selectableItemBackgroundBorderless" + android:alpha="0.6" + android:textSize="20sp" + android:layout_marginLeft="8dp" + android:layout_marginStart="8dp"/> + </LinearLayout> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/cover_fragment.xml b/app/src/main/res/layout/cover_fragment.xml index 5460d0609..0ec46cbcd 100644 --- a/app/src/main/res/layout/cover_fragment.xml +++ b/app/src/main/res/layout/cover_fragment.xml @@ -10,7 +10,7 @@ android:padding="8dp" android:gravity="center"> - <de.danoeh.antennapod.view.SquareImageView + <de.danoeh.antennapod.ui.common.SquareImageView android:id="@+id/imgvCover" android:layout_width="0dp" android:layout_height="200dp" diff --git a/app/src/main/res/layout/download_authentication_activity.xml b/app/src/main/res/layout/download_authentication_activity.xml deleted file mode 100644 index e16a8b3a8..000000000 --- a/app/src/main/res/layout/download_authentication_activity.xml +++ /dev/null @@ -1,64 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:padding="16dp"> - - <TextView - android:id="@+id/txtvTitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/authentication_notification_title" - android:textSize="@dimen/text_size_large" - android:textColor="?android:attr/textColorPrimary"/> - - <TextView - android:id="@+id/txtvDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/authentication_notification_msg" - android:textColor="?android:attr/textColorSecondary"/> - - <EditText - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/etxtUsername" - android:hint="@string/username_label" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true"/> - - <EditText - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/etxtPassword" - android:hint="@string/password_label" - android:inputType="textPassword" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true"/> - - <LinearLayout - android:layout_width="fill_parent" - android:layout_height="48dp" - android:orientation="horizontal" - android:gravity="end"> - - <Button - android:id="@+id/butCancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/cancel_label" - style="@style/Widget.MaterialComponents.Button.TextButton"/> - - <Button - android:id="@+id/butConfirm" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/confirm_label" - style="@style/Widget.MaterialComponents.Button.TextButton"/> - </LinearLayout> - -</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/downloadlog_item.xml b/app/src/main/res/layout/downloadlog_item.xml index 5cde763a0..60c916cdc 100644 --- a/app/src/main/res/layout/downloadlog_item.xml +++ b/app/src/main/res/layout/downloadlog_item.xml @@ -83,6 +83,14 @@ android:textColor="?android:attr/textColorSecondary" tools:text="@string/design_time_downloaded_log_failure_reason"/> + <TextView + android:id="@+id/txtvTapForDetails" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="14sp" + android:textColor="?android:attr/textColorSecondary" + android:text="@string/download_error_tap_for_details"/> + </LinearLayout> <include layout="@layout/secondary_action"/> diff --git a/app/src/main/res/layout/feeditemlist_header.xml b/app/src/main/res/layout/feeditemlist_header.xml index 005702c59..2b59845f7 100644 --- a/app/src/main/res/layout/feeditemlist_header.xml +++ b/app/src/main/res/layout/feeditemlist_header.xml @@ -94,17 +94,6 @@ </LinearLayout> - <TextView - android:id="@+id/txtvInformation" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:padding="2dp" - android:background="?android:attr/windowBackground" - android:visibility="gone" - android:gravity="center" - tools:visibility="visible" - tools:text="(i) Information"/> - <com.joanzapata.iconify.widget.IconTextView android:id="@+id/txtvFailure" android:layout_width="match_parent" @@ -117,4 +106,15 @@ android:text="@string/refresh_failed_msg" tools:visibility="visible" tools:text="(!) Last refresh failed"/> + + <TextView + android:id="@+id/txtvInformation" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="2dp" + android:background="?android:attr/windowBackground" + android:visibility="gone" + android:gravity="center" + tools:visibility="visible" + tools:text="(i) Information"/> </LinearLayout> diff --git a/app/src/main/res/layout/feeditemlist_item.xml b/app/src/main/res/layout/feeditemlist_item.xml index a8ae5743e..e1f382e46 100644 --- a/app/src/main/res/layout/feeditemlist_item.xml +++ b/app/src/main/res/layout/feeditemlist_item.xml @@ -145,6 +145,7 @@ android:layout_marginRight="4dp" android:layout_marginEnd="4dp" android:text="·" + android:importantForAccessibility="no" tools:background="@android:color/holo_blue_light"/> <TextView @@ -163,6 +164,7 @@ android:layout_marginRight="4dp" android:layout_marginEnd="4dp" android:text="·" + android:importantForAccessibility="no" tools:background="@android:color/holo_blue_light"/> <TextView diff --git a/app/src/main/res/layout/filter_dialog_row.xml b/app/src/main/res/layout/filter_dialog_row.xml index 5011812d9..914525387 100644 --- a/app/src/main/res/layout/filter_dialog_row.xml +++ b/app/src/main/res/layout/filter_dialog_row.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<de.danoeh.antennapod.view.RecursiveRadioGroup +<de.danoeh.antennapod.ui.common.RecursiveRadioGroup xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -58,4 +58,4 @@ android:layout_gravity="center_vertical" android:checked="true" /> -</de.danoeh.antennapod.view.RecursiveRadioGroup> +</de.danoeh.antennapod.ui.common.RecursiveRadioGroup> diff --git a/app/src/main/res/layout/gpodnetauth_activity.xml b/app/src/main/res/layout/gpodnetauth_activity.xml deleted file mode 100644 index c096c20cf..000000000 --- a/app/src/main/res/layout/gpodnetauth_activity.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent"> -<ViewFlipper - android:id="@+id/viewflipper" - android:layout_width="match_parent" - android:layout_height="wrap_content"/> -</ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_credentials.xml b/app/src/main/res/layout/gpodnetauth_credentials.xml index 895b0999c..9fcf67cff 100644 --- a/app/src/main/res/layout/gpodnetauth_credentials.xml +++ b/app/src/main/res/layout/gpodnetauth_credentials.xml @@ -1,96 +1,106 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="16dp"> - - <ImageView - android:id="@id/icon" - android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/gpodder_icon" /> + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginBottom="8dp"> + + <ImageView + android:layout_width="64dp" + android:layout_height="64dp" + android:src="@drawable/gpodder_icon"/> + + <TextView + android:id="@+id/createAccountButton" + android:layout_width="0dp" + android:textAlignment="textEnd" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:textColor="?colorAccent" + android:layout_weight="1" + android:layout_gravity="center_vertical|end" + android:text="@string/create_account"/> + </LinearLayout> <TextView - android:id="@id/txtvDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/gpodnetauth_login_descr" - android:layout_below="@id/icon" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary"/> + android:id="@+id/createAccountWarning" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_encryption_warning" + android:textColor="#F44336" + android:textStyle="bold" + android:visibility="invisible" /> - <EditText - android:id="@+id/etxtUsername" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/username_label" - android:layout_below="@id/txtvDescription" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true" - android:maxLines="1" - android:inputType="text" - android:imeOptions="actionNext|flagNoFullscreen" - android:nextFocusForward="@id/etxtPassword"/> + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <EditText - android:id="@+id/etxtPassword" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/password_label" - android:layout_below="@id/etxtUsername" - android:inputType="textPassword" - android:focusable="true" - android:focusableInTouchMode="true" - android:cursorVisible="true" - android:imeOptions="actionGo|flagNoFullscreen" - android:imeActionLabel="@string/gpodnetauth_login_butLabel"/> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/etxtUsername" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/username_label" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen"/> - <Button - android:id="@+id/butLogin" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@id/etxtPassword" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:text="@string/gpodnetauth_login_butLabel"/> + </com.google.android.material.textfield.TextInputLayout> - <TextView - android:id="@+id/txtvError" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_below="@id/etxtPassword" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:layout_toLeftOf="@id/butLogin" - android:layout_toStartOf="@id/butLogin" - android:textColor="@color/download_failed_red" - android:textSize="@dimen/text_size_small" - android:maxLines="2" - android:ellipsize="end" - android:gravity="center" - android:layout_margin="16dp" - tools:text="Error message" - tools:background="@android:color/holo_green_dark" /> + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <ProgressBar - android:id="@+id/progBarLogin" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:visibility="gone" - android:layout_alignTop="@+id/butLogin" - android:layout_toLeftOf="@+id/butLogin" - android:layout_toStartOf="@+id/butLogin"/> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/etxtPassword" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/password_label" + android:inputType="textPassword" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen" + android:imeActionLabel="@string/gpodnetauth_login_butLabel"/> - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary" - android:layout_marginTop="16dp" - android:text="@string/gpodnetauth_login_register" - android:autoLink="web" - android:layout_below="@id/butLogin"/> -</RelativeLayout>
\ No newline at end of file + </com.google.android.material.textfield.TextInputLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="end|center_vertical"> + + <TextView + android:id="@+id/credentialsError" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:textColor="@color/download_failed_red" + android:textSize="@dimen/text_size_small" + android:maxLines="2" + android:ellipsize="end" + android:gravity="center" + tools:text="Error message" + tools:background="@android:color/holo_green_dark"/> + + <ProgressBar + android:id="@+id/progBarLogin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_gravity="right"/> + + <Button + android:id="@+id/butLogin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_login_butLabel"/> + </LinearLayout> + +</LinearLayout> diff --git a/app/src/main/res/layout/gpodnetauth_device.xml b/app/src/main/res/layout/gpodnetauth_device.xml index 7837121e1..656ba0889 100644 --- a/app/src/main/res/layout/gpodnetauth_device.xml +++ b/app/src/main/res/layout/gpodnetauth_device.xml @@ -1,114 +1,61 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="16dp"> - - <TextView - android:id="@+id/txtvTitle" - android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_title" - android:layout_alignParentTop="true" - android:layout_marginBottom="16dp" - style="@style/AntennaPod.TextView.Heading"/> + android:orientation="vertical"> - <TextView - android:id="@+id/txtvDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_descr" - android:layout_below="@id/txtvTitle" - android:textSize="@dimen/text_size_medium" - android:textColor="?android:attr/textColorPrimary"/> - - <EditText - android:id="@+id/etxtCaption" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/gpodnetauth_device_caption" - android:layout_below="@id/txtvDescription" - android:imeOptions="flagNoFullscreen"/> + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <TextView - android:id="@+id/txtvDeviceID" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_deviceID" - android:textSize="@dimen/text_size_medium" - android:layout_below="@id/etxtCaption"/> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/deviceName" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/gpodnetauth_device_name" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen"/> - <EditText - android:id="@+id/etxtDeviceID" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvDeviceID" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:imeOptions="flagNoFullscreen"/> + </com.google.android.material.textfield.TextInputLayout> <Button - android:id="@+id/butCreateNewDevice" - android:layout_width="wrap_content" + android:id="@+id/createDeviceButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right|end" + android:text="@string/gpodnetauth_create_device"/> + + <TextView + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:layout_below="@id/etxtDeviceID" - android:text="@string/gpodnetauth_device_butCreateNewDevice"/> + style="@style/AntennaPod.TextView.Heading" + android:layout_marginTop="16dp" + android:text="@string/gpodnetauth_existing_devices"/> <TextView - android:id="@+id/txtvError" - android:layout_width="0dp" + android:id="@+id/deviceSelectError" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:layout_below="@id/etxtDeviceID" - android:layout_toLeftOf="@id/butCreateNewDevice" - android:layout_toStartOf="@id/butCreateNewDevice" android:textColor="@color/download_failed_red" android:textSize="@dimen/text_size_small" + android:visibility="gone" tools:text="Error message" tools:background="@android:color/holo_green_dark" /> - <ProgressBar - android:id="@+id/progbarCreateDevice" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignTop="@id/butCreateNewDevice" - android:layout_toLeftOf="@id/butCreateNewDevice" - android:layout_toStartOf="@id/butCreateNewDevice" - android:textColor="@color/download_failed_red" - android:textSize="@dimen/text_size_medium" - android:visibility="gone" - /> - - <TextView - android:id="@+id/txtvChooseExistingDevice" + <LinearLayout + android:id="@+id/devicesContainer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_chooseExistingDevice" - android:layout_below="@id/butCreateNewDevice" - android:textColor="?android:attr/textColorPrimary" - android:textSize="@dimen/text_size_medium" - android:layout_marginTop="32dp"/> + android:orientation="vertical" /> - <Button - android:id="@+id/butChooseExistingDevice" + <ProgressBar + android:id="@+id/progbarCreateDevice" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/gpodnetauth_device_butChoose" - android:layout_below="@+id/spinnerChooseDevice" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true"/> - - <Spinner - android:id="@+id/spinnerChooseDevice" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvChooseExistingDevice" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true"/> + android:textColor="@color/download_failed_red" + android:visibility="gone" /> -</RelativeLayout>
\ No newline at end of file +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_device_row.xml b/app/src/main/res/layout/gpodnetauth_device_row.xml new file mode 100644 index 000000000..d39c00571 --- /dev/null +++ b/app/src/main/res/layout/gpodnetauth_device_row.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp"> + + <Button + android:id="@+id/selectDeviceButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="?attr/materialButtonOutlinedStyle" /> +</FrameLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_dialog.xml b/app/src/main/res/layout/gpodnetauth_dialog.xml new file mode 100644 index 000000000..a70b76a49 --- /dev/null +++ b/app/src/main/res/layout/gpodnetauth_dialog.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:clipToPadding="false"> + <ViewFlipper + android:id="@+id/viewflipper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inAnimation="@anim/slide_right_in" + android:outAnimation="@anim/slide_left_out"> + + <include layout="@layout/gpodnetauth_host" /> + <include layout="@layout/gpodnetauth_credentials" /> + <include layout="@layout/gpodnetauth_device" /> + <include layout="@layout/gpodnetauth_finish" /> + + </ViewFlipper> +</ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_finish.xml b/app/src/main/res/layout/gpodnetauth_finish.xml index fdaa0d5d0..f0bcfd4dc 100644 --- a/app/src/main/res/layout/gpodnetauth_finish.xml +++ b/app/src/main/res/layout/gpodnetauth_finish.xml @@ -1,46 +1,28 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="16dp"> + android:layout_height="wrap_content" + android:orientation="vertical"> <ImageView android:id="@id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="64dp" + android:layout_height="64dp" android:src="@drawable/gpodder_icon" /> <TextView - android:id="@+id/txtvTitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/icon" - android:text="@string/gpodnetauth_finish_title" - style="@style/AntennaPod.TextView.Heading"/> - - <TextView android:id="@+id/txtvDescription" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/gpodnetauth_finish_descr" - android:layout_below="@id/txtvTitle" - android:textSize="@dimen/text_size_medium" android:textColor="?android:attr/textColorPrimary" /> <Button android:id="@+id/butSyncNow" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/txtvDescription" - android:layout_marginTop="16dp" + android:layout_marginTop="8dp" android:text="@string/gpodnetauth_finish_butsyncnow"/> - <Button - android:id="@+id/butGoMainscreen" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/butSyncNow" - android:text="@string/gpodnetauth_finish_butgomainscreen"/> - -</RelativeLayout>
\ No newline at end of file +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/gpodnetauth_host.xml b/app/src/main/res/layout/gpodnetauth_host.xml new file mode 100644 index 000000000..52c5fdb5d --- /dev/null +++ b/app/src/main/res/layout/gpodnetauth_host.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <RadioGroup + android:id="@+id/serverRadioGroup" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <RadioButton + android:id="@+id/officialServerRadio" + android:text="@string/gpodnetauth_server_official" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:checked="true"/> + <RadioButton + android:id="@+id/customServerRadio" + android:text="@string/gpodnetauth_server_custom" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + </RadioGroup> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/serverUrlTextInput" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/serverUrlText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/gpodnetauth_host" + android:inputType="textNoSuggestions" + android:lines="1" + android:imeOptions="actionNext|flagNoFullscreen" /> + + </com.google.android.material.textfield.TextInputLayout> + + <Button + android:id="@+id/chooseHostButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right|end" + android:text="@string/gpodnetauth_select_server"/> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/quick_feed_discovery.xml b/app/src/main/res/layout/quick_feed_discovery.xml index dd720afed..9ef3db180 100644 --- a/app/src/main/res/layout/quick_feed_discovery.xml +++ b/app/src/main/res/layout/quick_feed_discovery.xml @@ -34,7 +34,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <de.danoeh.antennapod.view.WrappingGridView + <de.danoeh.antennapod.ui.common.WrappingGridView android:id="@+id/discover_grid" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/app/src/main/res/layout/quick_feed_discovery_item.xml b/app/src/main/res/layout/quick_feed_discovery_item.xml index e1c91f9d8..cb03b6677 100644 --- a/app/src/main/res/layout/quick_feed_discovery_item.xml +++ b/app/src/main/res/layout/quick_feed_discovery_item.xml @@ -7,7 +7,7 @@ android:padding="4dp" android:clipToPadding="false"> - <de.danoeh.antennapod.view.SquareImageView + <de.danoeh.antennapod.ui.common.SquareImageView android:id="@+id/discovery_cover" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/app/src/main/res/layout/searchlist_item_feed.xml b/app/src/main/res/layout/searchlist_item_feed.xml index 607a3197f..c16911f99 100644 --- a/app/src/main/res/layout/searchlist_item_feed.xml +++ b/app/src/main/res/layout/searchlist_item_feed.xml @@ -7,7 +7,7 @@ android:padding="4dp" android:clipToPadding="false"> - <de.danoeh.antennapod.view.SquareImageView + <de.danoeh.antennapod.ui.common.SquareImageView android:id="@+id/discovery_cover" android:layout_width="match_parent" android:layout_height="96dp" diff --git a/app/src/main/res/layout/secondary_action.xml b/app/src/main/res/layout/secondary_action.xml index 73ca174a6..e5bff480e 100644 --- a/app/src/main/res/layout/secondary_action.xml +++ b/app/src/main/res/layout/secondary_action.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginRight="12dp" @@ -19,9 +20,10 @@ tools:ignore="ContentDescription" tools:src="@sample/secondaryaction"/> - <de.danoeh.antennapod.view.CircularProgressBar + <de.danoeh.antennapod.ui.common.CircularProgressBar android:id="@+id/secondaryActionProgress" android:layout_width="40dp" android:layout_gravity="center" - android:layout_height="40dp"/> + android:layout_height="40dp" + app:foregroundColor="?attr/action_icon_color"/> </FrameLayout> diff --git a/app/src/main/res/layout/subscription_item.xml b/app/src/main/res/layout/subscription_item.xml index e0c821868..7fa738f12 100644 --- a/app/src/main/res/layout/subscription_item.xml +++ b/app/src/main/res/layout/subscription_item.xml @@ -8,7 +8,7 @@ android:layout_height="match_parent" android:foreground="?attr/selectableItemBackground"> - <de.danoeh.antennapod.view.SquareImageView + <de.danoeh.antennapod.ui.common.SquareImageView android:id="@+id/imgvCover" android:layout_width="fill_parent" android:layout_height="fill_parent" diff --git a/app/src/main/res/layout/videoplayer_activity.xml b/app/src/main/res/layout/videoplayer_activity.xml index c978a1e4d..e0632ef41 100644 --- a/app/src/main/res/layout/videoplayer_activity.xml +++ b/app/src/main/res/layout/videoplayer_activity.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" android:background="@color/black" android:orientation="vertical" android:id="@+id/videoframe"> @@ -75,6 +76,33 @@ android:layout_gravity="bottom|center" android:orientation="vertical"> + <androidx.cardview.widget.CardView + android:id="@+id/cardViewSeek" + android:alpha="0" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="12dp" + android:layout_gravity="center" + app:cardCornerRadius="8dp" + app:cardBackgroundColor="?attr/seek_background" + app:cardElevation="0dp" + tools:alpha="1"> + + <TextView + android:id="@+id/txtvSeek" + android:gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="24dp" + android:paddingTop="4dp" + android:paddingRight="24dp" + android:paddingBottom="4dp" + android:textColor="@color/white" + android:textSize="24sp" + tools:text="1:06:29" /> + + </androidx.cardview.widget.CardView> + <RelativeLayout android:id="@+id/timecontrol" android:layout_width="match_parent" diff --git a/app/src/main/res/menu/feedinfo.xml b/app/src/main/res/menu/feedinfo.xml index b1daf1f36..a5fbe0c20 100644 --- a/app/src/main/res/menu/feedinfo.xml +++ b/app/src/main/res/menu/feedinfo.xml @@ -8,17 +8,25 @@ android:title="@string/visit_website_label" android:visible="true"/> <item - android:id="@+id/share_link_item" - custom:showAsAction="collapseActionView" - android:title="@string/share_website_url_label"/> - <item - android:id="@+id/share_download_url_item" - custom:showAsAction="collapseActionView" - android:title="@string/share_feed_url_label"/> + android:id="@+id/share_parent" + custom:showAsAction="ifRoom" + android:title="@string/share_label_with_ellipses" + android:icon="?attr/ic_share" + android:visible="true"> + <menu android:id="@+id/share_submenu"> + <item + android:id="@+id/share_link_item" + custom:showAsAction="collapseActionView" + android:title="@string/share_website_url_label"/> + <item + android:id="@+id/share_download_url_item" + custom:showAsAction="collapseActionView" + android:title="@string/share_feed_url_label"/> + </menu> + </item> <item android:id="@+id/reconnect_local_folder" custom:showAsAction="collapseActionView" android:title="@string/reconnect_local_folder" android:visible="false" /> - -</menu> +</menu>
\ No newline at end of file diff --git a/app/src/main/res/xml/actions.xml b/app/src/main/res/xml/actions.xml new file mode 100644 index 000000000..20dc3dc9b --- /dev/null +++ b/app/src/main/res/xml/actions.xml @@ -0,0 +1,25 @@ +<?xml version ="1.0" encoding ="utf-8"?> +<actions> + <action intentName="actions.intent.OPEN_APP_FEATURE"> + <fulfillment urlTemplate="https://antennapod.org/deeplink/main{?page}"> + <parameter-mapping intentParameter="feature" urlParameter="page" /> + </fulfillment> + <parameter name="feature"> + <entity-set-reference entitySetId="featureEntitySet" /> + </parameter> + </action> + + <action intentName="actions.intent.GET_THING"> + <fulfillment urlTemplate="https://antennapod.org/deeplink/search{?query}"> + <parameter-mapping intentParameter="thing.name" urlParameter="query"/> + </fulfillment> + </action> + + <entity-set entitySetId="featureEntitySet"> + <entity identifier="QUEUE" name="@string/queue_label" /> + <entity identifier="EPISODES" name="@string/episodes_label" /> + <entity identifier="DOWNLOADS" name="@string/downloads_label" /> + <entity identifier="SUBSCRIPTIONS" name="@string/subscriptions_label" /> + <entity identifier="HISTORY" name="@string/playback_history_label" /> + </entity-set> +</actions> diff --git a/app/src/main/res/xml/feed_settings.xml b/app/src/main/res/xml/feed_settings.xml index a0142b7b9..8a63ac8e9 100644 --- a/app/src/main/res/xml/feed_settings.xml +++ b/app/src/main/res/xml/feed_settings.xml @@ -9,6 +9,14 @@ android:title="@string/keep_updated" android:summary="@string/keep_updated_summary"/> + <SwitchPreferenceCompat + android:key="episodeNotification" + android:defaultValue="false" + android:dependency="keepUpdated" + android:icon="?attr/ic_notifications" + android:title="@string/episode_notification" + android:summary="@string/episode_notification_summary"/> + <Preference android:key="authentication" android:icon="?attr/ic_key" diff --git a/app/src/main/res/xml/player_widget_info.xml b/app/src/main/res/xml/player_widget_info.xml index 79cdd4a69..803cc89ed 100644 --- a/app/src/main/res/xml/player_widget_info.xml +++ b/app/src/main/res/xml/player_widget_info.xml @@ -8,5 +8,4 @@ android:minWidth="250dp" android:minResizeWidth="40dp" android:configure="de.danoeh.antennapod.activity.WidgetConfigActivity"> - -</appwidget-provider>
\ No newline at end of file +</appwidget-provider> diff --git a/app/src/main/res/xml/preferences_gpodder.xml b/app/src/main/res/xml/preferences_gpodder.xml index 7bddbf245..a210b8e11 100644 --- a/app/src/main/res/xml/preferences_gpodder.xml +++ b/app/src/main/res/xml/preferences_gpodder.xml @@ -1,13 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> - - <PreferenceScreen + <Preference + android:key="pref_gpodnet_description" + android:icon="@drawable/gpodder_icon" + android:summary="@string/gpodnet_description"/> + <Preference android:key="pref_gpodnet_authenticate" android:title="@string/pref_gpodnet_authenticate_title" - android:summary="@string/pref_gpodnet_authenticate_sum"> - <intent android:action=".activity.gpoddernet.GpodnetAuthenticationActivity"/> - </PreferenceScreen> + android:summary="@string/pref_gpodnet_authenticate_sum"/> <Preference android:key="pref_gpodnet_setlogin_information" android:title="@string/pref_gpodnet_setlogin_information_title" @@ -23,8 +24,5 @@ <Preference android:key="pref_gpodnet_logout" android:title="@string/pref_gpodnet_logout_title"/> - <Preference - android:key="pref_gpodnet_hostname" - android:title="@string/pref_gpodnet_sethostname_title"/> </PreferenceScreen> diff --git a/app/src/main/res/xml/preferences_playback.xml b/app/src/main/res/xml/preferences_playback.xml index d2999c59d..2be8492eb 100644 --- a/app/src/main/res/xml/preferences_playback.xml +++ b/app/src/main/res/xml/preferences_playback.xml @@ -44,18 +44,6 @@ </PreferenceCategory> <PreferenceCategory android:title="@string/playback_control"> - <SwitchPreferenceCompat - android:defaultValue="false" - android:enabled="true" - android:key="prefHardwareForwardButtonSkips" - android:summary="@string/pref_hardwareForwardButtonSkips_sum" - android:title="@string/pref_hardwareForwardButtonSkips_title"/> - <SwitchPreferenceCompat - android:defaultValue="false" - android:enabled="true" - android:key="prefHardwarePreviousButtonRestarts" - android:summary="@string/pref_hardwarePreviousButtonRestarts_sum" - android:title="@string/pref_hardwarePreviousButtonRestarts_title"/> <Preference android:key="prefPlaybackFastForwardDeltaLauncher" android:summary="@string/pref_fast_forward_sum" @@ -80,6 +68,23 @@ android:title="@string/pref_stream_over_download_title"/> </PreferenceCategory> + <PreferenceCategory android:title="@string/reassign_hardware_buttons"> + <ListPreference + android:defaultValue="@string/keycode_media_fast_forward" + android:entries="@array/button_action_options" + android:entryValues="@array/button_action_values" + android:key="prefHardwareForwardButton" + android:title="@string/pref_hardware_forward_button_title" + android:summary="@string/pref_hardware_forward_button_summary"/> + <ListPreference + android:defaultValue="@string/keycode_media_rewind" + android:entries="@array/button_action_options" + android:entryValues="@array/button_action_values" + android:key="prefHardwarePreviousButton" + android:title="@string/pref_hardware_previous_button_title" + android:summary="@string/pref_hardware_previous_button_summary"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/queue_label"> <SwitchPreferenceCompat android:defaultValue="true" diff --git a/app/src/main/res/xml/preferences_user_interface.xml b/app/src/main/res/xml/preferences_user_interface.xml index a3cb53307..f8e80cdff 100644 --- a/app/src/main/res/xml/preferences_user_interface.xml +++ b/app/src/main/res/xml/preferences_user_interface.xml @@ -21,6 +21,12 @@ android:summary="@string/pref_episode_cover_summary" android:defaultValue="true" android:enabled="true"/> + <SwitchPreferenceCompat + android:title="@string/pref_show_remain_time_title" + android:key="showTimeLeft" + android:summary="@string/pref_show_remain_time_summary" + android:defaultValue="false" + android:enabled="true"/> </PreferenceCategory> <PreferenceCategory android:title="@string/subscriptions_label"> <Preference diff --git a/build.gradle b/build.gradle index b0d59f24c..e4c95ae30 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ buildscript { repositories { google() - jcenter() mavenCentral() maven { url "https://plugins.gradle.org/m2/" } + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.6.3' @@ -15,8 +15,9 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() maven { url "https://jitpack.io" } + jcenter() } gradle.projectsEvaluated { @@ -72,6 +73,7 @@ project.ext { // Google Play build wearableSupportVersion = "2.6.0" + playServicesVersion = "8.4.0" //Tests awaitilityVersion = "3.1.6" @@ -85,11 +87,6 @@ wrapper { gradleVersion = "6.3" } -// free build hack: common functions -def doFreeBuild() { - return hasProperty("freeBuild") -} - apply plugin: "checkstyle" checkstyle { toolVersion '8.24' diff --git a/config/checkstyle/checkstyle-new-code.xml b/config/checkstyle/checkstyle-new-code.xml index 3bb35f2cc..7d7270a9c 100644 --- a/config/checkstyle/checkstyle-new-code.xml +++ b/config/checkstyle/checkstyle-new-code.xml @@ -204,10 +204,6 @@ </module> <module name="NonEmptyAtclauseDescription"/> <module name="JavadocTagContinuationIndentation"/> - <module name="SummaryJavadoc"> - <property name="forbiddenSummaryFragments" - value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/> - </module> <module name="JavadocParagraph"/> <module name="AtclauseOrder"> <property name="tagOrder" value="@param, @return, @throws, @deprecated"/> diff --git a/core/build.gradle b/core/build.gradle index f443ebb9b..75ad7faad 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -71,6 +71,10 @@ android { } dependencies { + implementation project(':net:ssl') + implementation project(':ui:app-start-intent') + implementation project(':ui:common') + annotationProcessor "androidx.annotation:annotation:$annotationVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation 'androidx.documentfile:documentfile:1.0.1' @@ -96,20 +100,12 @@ dependencies { implementation 'com.google.android.exoplayer:exoplayer:2.11.8' implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" - // Add casting features - // free build hack: skip some dependencies - if (!doFreeBuild()) { - playApi 'com.google.android.libraries.cast.companionlibrary:ccl:2.9.1' - api 'androidx.mediarouter:mediarouter:1.0.0' - playApi 'com.google.android.gms:play-services-cast:8.4.0' - api "com.google.android.support:wearable:$wearableSupportVersion" - compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" - } else { - System.out.println("core: free build hack, skipping some dependencies") - } - - // bundle conscrypt with free builds - freeImplementation "org.conscrypt:conscrypt-android:$conscryptVersion" + // Non-free dependencies: + playApi 'com.google.android.libraries.cast.companionlibrary:ccl:2.9.1' + playApi 'androidx.mediarouter:mediarouter:1.0.0' + playApi "com.google.android.gms:play-services-cast:$playServicesVersion" + playApi "com.google.android.support:wearable:$wearableSupportVersion" + compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" testImplementation "org.awaitility:awaitility:$awaitilityVersion" testImplementation 'junit:junit:4.13' diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java index 8fd1df35c..755bec14e 100644 --- a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java @@ -1,15 +1,14 @@ package de.danoeh.antennapod.core; import android.content.Context; -import java.security.Security; -import org.conscrypt.Conscrypt; + +import de.danoeh.antennapod.net.ssl.SslProviderInstaller; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; import de.danoeh.antennapod.core.preferences.UsageStatistics; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm; import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.gui.NotificationUtils; @@ -31,10 +30,6 @@ public class ClientConfig { public static DownloadServiceCallbacks downloadServiceCallbacks; - public static PlaybackServiceCallbacks playbackServiceCallbacks; - - public static AutomaticDownloadAlgorithm automaticDownloadAlgorithm; - public static CastCallbacks castCallbacks; private static boolean initialized = false; @@ -47,16 +42,11 @@ public class ClientConfig { UserPreferences.init(context); UsageStatistics.init(context); PlaybackPreferences.init(context); - installSslProvider(context); + SslProviderInstaller.install(context); NetworkUtils.init(context); AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp")); SleepTimerPreferences.init(context); NotificationUtils.createChannels(context); initialized = true; } - - private static void installSslProvider(Context context) { - // Insert bundled conscrypt as highest security provider (overrides OS version). - Security.insertProviderAt(Conscrypt.newProvider(), 1); - } } diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml index ae5e56e55..59267fa39 100644 --- a/core/src/main/AndroidManifest.xml +++ b/core/src/main/AndroidManifest.xml @@ -45,6 +45,11 @@ android:label="@string/feed_update_receiver_name" android:exported="true" tools:ignore="ExportedReceiver" /> <!-- allow feeds update to be triggered by external apps --> + + <service + android:name=".widget.WidgetUpdaterJobService" + android:permission="android.permission.BIND_JOB_SERVICE" + android:exported="true"/> </application> </manifest> diff --git a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java deleted file mode 100644 index 3dcaac4dc..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java +++ /dev/null @@ -1,22 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.content.Context; -import android.content.Intent; - -import de.danoeh.antennapod.core.feed.MediaType; - -/** - * Callbacks for the PlaybackService of the core module - */ -public interface PlaybackServiceCallbacks { - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. - * - * @param mediaType The type of media that is being played. - * @param remotePlayback true if the media is played on a remote device. - * @return A non-null activity intent. - */ - Intent getPlayerActivityIntent(Context context, MediaType mediaType, boolean remotePlayback); -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java deleted file mode 100644 index b01e3f3ba..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java +++ /dev/null @@ -1,15 +0,0 @@ -package de.danoeh.antennapod.core.asynctask; - -/** - * Classes that implement this interface provide access to an image resource that can - * be loaded by the Picasso library. - */ -public interface ImageResource { - - /** - * Returns the location of the image or null if no image is available. - * <p/> - * The location can either be an URL or a local path - */ - String getImageLocation(); -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java index a3b66c951..dd8a466eb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java @@ -1,6 +1,5 @@ package de.danoeh.antennapod.core.feed; -import android.database.Cursor; import android.text.TextUtils; import androidx.annotation.Nullable; @@ -9,26 +8,28 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import de.danoeh.antennapod.core.asynctask.ImageResource; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.SortOrder; /** - * Data Object for a whole feed + * Data Object for a whole feed. * * @author daniel */ -public class Feed extends FeedFile implements ImageResource { +public class Feed extends FeedFile { public static final int FEEDFILETYPE_FEED = 0; public static final String TYPE_RSS2 = "rss"; public static final String TYPE_ATOM1 = "atom"; public static final String PREFIX_LOCAL_FOLDER = "antennapod_local:"; - /* title as defined by the feed */ + /** + * title as defined by the feed. + */ private String feedTitle; - /* custom title set by the user */ + + /** + * custom title set by the user. + */ private String customTitle; /** @@ -42,25 +43,25 @@ public class Feed extends FeedFile implements ImageResource { private String description; private String language; /** - * Name of the author + * Name of the author. */ private String author; private String imageUrl; private List<FeedItem> items; /** - * String that identifies the last update (adopted from Last-Modified or ETag header) + * String that identifies the last update (adopted from Last-Modified or ETag header). */ private String lastUpdate; private String paymentLink; /** - * Feed type, for example RSS 2 or Atom + * Feed type, for example RSS 2 or Atom. */ private String type; /** - * Feed preferences + * Feed preferences. */ private FeedPreferences preferences; @@ -122,7 +123,7 @@ public class Feed extends FeedFile implements ImageResource { this.paged = paged; this.nextPageLink = nextPageLink; this.items = new ArrayList<>(); - if(filter != null) { + if (filter != null) { this.itemfilter = new FeedItemFilter(filter); } else { this.itemfilter = new FeedItemFilter(new String[0]); @@ -132,7 +133,7 @@ public class Feed extends FeedFile implements ImageResource { } /** - * This constructor is used for test purposes + * This constructor is used for test purposes. */ public Feed(long id, String lastUpdate, String title, String link, String description, String paymentLink, String author, String language, String type, String feedIdentifier, String imageUrl, String fileUrl, @@ -175,56 +176,6 @@ public class Feed extends FeedFile implements ImageResource { preferences = new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password); } - public static Feed fromCursor(Cursor cursor) { - int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); - int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE); - int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE); - int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE); - int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK); - int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION); - int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK); - int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR); - int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE); - int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE); - int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER); - int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL); - int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL); - int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED); - int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED); - int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK); - int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE); - int indexSortOrder = cursor.getColumnIndex(PodDBAdapter.KEY_SORT_ORDER); - int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED); - int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL); - - Feed feed = new Feed( - cursor.getLong(indexId), - cursor.getString(indexLastUpdate), - cursor.getString(indexTitle), - cursor.getString(indexCustomTitle), - cursor.getString(indexLink), - cursor.getString(indexDescription), - cursor.getString(indexPaymentLink), - cursor.getString(indexAuthor), - cursor.getString(indexLanguage), - cursor.getString(indexType), - cursor.getString(indexFeedIdentifier), - cursor.getString(indexImageUrl), - cursor.getString(indexFileUrl), - cursor.getString(indexDownloadUrl), - cursor.getInt(indexDownloaded) > 0, - cursor.getInt(indexIsPaged) > 0, - cursor.getString(indexNextPageLink), - cursor.getString(indexHide), - SortOrder.fromCodeString(cursor.getString(indexSortOrder)), - cursor.getInt(indexLastUpdateFailed) > 0 - ); - - FeedPreferences preferences = FeedPreferences.fromCursor(cursor); - feed.setPreferences(preferences); - return feed; - } - /** * Returns the item at the specified index. * @@ -384,7 +335,7 @@ public class Feed extends FeedFile implements ImageResource { } public void setCustomTitle(String customTitle) { - if(customTitle == null || customTitle.equals(feedTitle)) { + if (customTitle == null || customTitle.equals(feedTitle)) { this.customTitle = null; } else { this.customTitle = customTitle; @@ -479,10 +430,6 @@ public class Feed extends FeedFile implements ImageResource { return preferences; } - public void savePreferences() { - DBWriter.setFeedPreferences(preferences); - } - @Override public void setId(long id) { super.setId(id); @@ -491,11 +438,6 @@ public class Feed extends FeedFile implements ImageResource { } } - @Override - public String getImageLocation() { - return imageUrl; - } - public int getPageNr() { return pageNr; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java index 131cbe563..d6926385e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -4,7 +4,6 @@ import android.database.Cursor; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.text.TextUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -14,20 +13,16 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.asynctask.ImageResource; -import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.danoeh.antennapod.core.util.ShownotesProvider; /** - * Data Object for a XML message + * Item (episode) within a feed. * * @author daniel */ -public class FeedItem extends FeedComponent implements ShownotesProvider, ImageResource, Serializable { +public class FeedItem extends FeedComponent implements Serializable { /** tag that indicates this item is in the queue */ public static final String TAG_QUEUE = "Queue"; @@ -43,10 +38,6 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR * The description of a feeditem. */ private String description; - /** - * The content of the content-encoded tag of a feeditem. - */ - private String contentEncoded; private String link; private Date pubDate; @@ -182,9 +173,6 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR if (other.getDescription() != null) { description = other.getDescription(); } - if (other.getContentEncoded() != null) { - contentEncoded = other.contentEncoded; - } if (other.link != null) { link = other.link; } @@ -240,10 +228,6 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR return description; } - public void setDescription(String description) { - this.description = description; - } - public String getLink() { return link; } @@ -307,7 +291,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR } public void setPlayed(boolean played) { - if(played) { + if (played) { state = PLAYED; } else { state = UNPLAYED; @@ -318,12 +302,19 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR return (media != null && media.isInProgress()); } - public String getContentEncoded() { - return contentEncoded; - } - - public void setContentEncoded(String contentEncoded) { - this.contentEncoded = contentEncoded; + /** + * Updates this item's description property if the given argument is longer than the already stored description + * @param newDescription The new item description, content:encoded, itunes:description, etc. + */ + public void setDescriptionIfLonger(String newDescription) { + if (newDescription == null) { + return; + } + if (this.description == null) { + this.description = newDescription; + } else if (this.description.length() < newDescription.length()) { + this.description = newDescription; + } } public String getPaymentLink() { @@ -358,32 +349,13 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR return media != null && media.isPlaying(); } - @Override - public Callable<String> loadShownotes() { - return () -> { - if (contentEncoded == null || description == null) { - DBReader.loadDescriptionOfFeedItem(FeedItem.this); - } - if (TextUtils.isEmpty(contentEncoded)) { - return description; - } else if (TextUtils.isEmpty(description)) { - return contentEncoded; - } else if (description.length() > 1.25 * contentEncoded.length()) { - return description; - } else { - return contentEncoded; - } - }; - } - - @Override public String getImageLocation() { if (imageUrl != null) { return imageUrl; } else if (media != null && media.hasEmbeddedPicture()) { return FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER + media.getLocalMediaUrl(); } else if (feed != null) { - return feed.getImageLocation(); + return feed.getImageUrl(); } else { return null; } @@ -472,17 +444,23 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR /** * @return true if the item has this tag */ - public boolean isTagged(String tag) { return tags.contains(tag); } + public boolean isTagged(String tag) { + return tags.contains(tag); + } /** * @param tag adds this tag to the item. NOTE: does NOT persist to the database */ - public void addTag(String tag) { tags.add(tag); } + public void addTag(String tag) { + tags.add(tag); + } /** * @param tag the to remove */ - public void removeTag(String tag) { tags.remove(tag); } + public void removeTag(String tag) { + tags.remove(tag); + } @NonNull @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java index 787f0e5e7..ac742e765 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java @@ -1,134 +1,60 @@ package de.danoeh.antennapod.core.feed; import android.text.TextUtils; - -import java.util.ArrayList; -import java.util.List; - -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.LongList; - -import static de.danoeh.antennapod.core.feed.FeedItem.TAG_FAVORITE; +import java.util.Arrays; public class FeedItemFilter { - private final String[] mProperties; - - private boolean showPlayed = false; - private boolean showUnplayed = false; - private boolean showPaused = false; - private boolean showNotPaused = false; - private boolean showQueued = false; - private boolean showNotQueued = false; - private boolean showDownloaded = false; - private boolean showNotDownloaded = false; - private boolean showHasMedia = false; - private boolean showNoMedia = false; - private boolean showIsFavorite = false; - private boolean showNotFavorite = false; + private final String[] properties; + + public final boolean showPlayed; + public final boolean showUnplayed; + public final boolean showPaused; + public final boolean showNotPaused; + public final boolean showQueued; + public final boolean showNotQueued; + public final boolean showDownloaded; + public final boolean showNotDownloaded; + public final boolean showHasMedia; + public final boolean showNoMedia; + public final boolean showIsFavorite; + public final boolean showNotFavorite; + + public static FeedItemFilter unfiltered() { + return new FeedItemFilter(""); + } public FeedItemFilter(String properties) { this(TextUtils.split(properties, ",")); } public FeedItemFilter(String[] properties) { - this.mProperties = properties; - for (String property : properties) { - // see R.arrays.feed_filter_values - switch (property) { - case "unplayed": - showUnplayed = true; - break; - case "paused": - showPaused = true; - break; - case "not_paused": - showNotPaused = true; - break; - case "played": - showPlayed = true; - break; - case "queued": - showQueued = true; - break; - case "not_queued": - showNotQueued = true; - break; - case "downloaded": - showDownloaded = true; - break; - case "not_downloaded": - showNotDownloaded = true; - break; - case "has_media": - showHasMedia = true; - break; - case "no_media": - showNoMedia = true; - break; - case "is_favorite": - showIsFavorite = true; - break; - case "not_favorite": - showNotFavorite = true; - break; - default: - break; - } - } + this.properties = properties; + + // see R.arrays.feed_filter_values + showUnplayed = hasProperty("unplayed"); + showPaused = hasProperty("paused"); + showNotPaused = hasProperty("not_paused"); + showPlayed = hasProperty("played"); + showQueued = hasProperty("queued"); + showNotQueued = hasProperty("not_queued"); + showDownloaded = hasProperty("downloaded"); + showNotDownloaded = hasProperty("not_downloaded"); + showHasMedia = hasProperty("has_media"); + showNoMedia = hasProperty("no_media"); + showIsFavorite = hasProperty("is_favorite"); + showNotFavorite = hasProperty("not_favorite"); } - /** - * Run a list of feed items through the filter. - */ - public List<FeedItem> filter(List<FeedItem> items) { - if(mProperties.length == 0) return items; - - List<FeedItem> result = new ArrayList<>(); - - // Check for filter combinations that will always return an empty list - // (e.g. requiring played and unplayed at the same time) - if (showPlayed && showUnplayed) return result; - if (showQueued && showNotQueued) return result; - if (showDownloaded && showNotDownloaded) return result; - - final LongList queuedIds = DBReader.getQueueIDList(); - for (FeedItem item : items) { - // If the item does not meet a requirement, skip it. - - if (showPlayed && !item.isPlayed()) continue; - if (showUnplayed && item.isPlayed()) continue; - - if (showPaused && item.getState() != FeedItem.State.IN_PROGRESS) continue; - if (showNotPaused && item.getState() == FeedItem.State.IN_PROGRESS) continue; - - boolean queued = queuedIds.contains(item.getId()); - if (showQueued && !queued) continue; - if (showNotQueued && queued) continue; - - boolean downloaded = item.getMedia() != null && item.getMedia().isDownloaded(); - if (showDownloaded && !downloaded) continue; - if (showNotDownloaded && downloaded) continue; - - if (showHasMedia && !item.hasMedia()) continue; - if (showNoMedia && item.hasMedia()) continue; - - if (showIsFavorite && !item.isTagged(TAG_FAVORITE)) continue; - if (showNotFavorite && item.isTagged(TAG_FAVORITE)) continue; - - // If the item reaches here, it meets all criteria - result.add(item); - } - - return result; + private boolean hasProperty(String property) { + return Arrays.asList(properties).contains(property); } public String[] getValues() { - return mProperties.clone(); + return properties.clone(); } public boolean isShowDownloaded() { return showDownloaded; } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index 4857e899d..3070f882c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -12,10 +12,8 @@ import androidx.annotation.Nullable; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaDescriptionCompat; -import java.util.Collections; import java.util.Date; import java.util.List; -import java.util.concurrent.Callable; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; @@ -24,10 +22,10 @@ import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.sync.SyncService; import de.danoeh.antennapod.core.sync.model.EpisodeAction; +import de.danoeh.antennapod.core.util.playback.PlayableException; public class FeedMedia extends FeedFile implements Playable { private static final String TAG = "FeedMedia"; @@ -175,8 +173,8 @@ public class FeedMedia extends FeedFile implements Playable { // getImageLocation() also loads embedded images, which we can not send to external devices if (item.getImageUrl() != null) { builder.setIconUri(Uri.parse(item.getImageUrl())); - } else if (item.getFeed() != null && item.getFeed().getImageLocation() != null) { - builder.setIconUri(Uri.parse(item.getFeed().getImageLocation())); + } else if (item.getFeed() != null && item.getFeed().getImageUrl() != null) { + builder.setIconUri(Uri.parse(item.getFeed().getImageUrl())); } } return new MediaBrowserCompat.MediaItem(builder.build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE); @@ -287,6 +285,14 @@ public class FeedMedia extends FeedFile implements Playable { this.size = size; } + @Override + public String getDescription() { + if (item != null) { + return item.getDescription(); + } + return null; + } + /** * Indicates we asked the service what the size was, but didn't * get a valid answer and we shoudln't check using the network again. @@ -385,40 +391,6 @@ public class FeedMedia extends FeedFile implements Playable { } @Override - public void loadChapterMarks(Context context) { - if (item == null && itemID != 0) { - item = DBReader.getFeedItem(itemID); - } - if (item == null || item.getChapters() != null) { - return; - } - - List<Chapter> chapters = loadChapters(context); - if (chapters == null) { - // Do not try loading again. There are no chapters. - item.setChapters(Collections.emptyList()); - } else { - item.setChapters(chapters); - } - } - - private List<Chapter> loadChapters(Context context) { - List<Chapter> chaptersFromDatabase = null; - if (item.hasChapters()) { - chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(item); - } - - List<Chapter> chaptersFromMediaFile; - if (localFileAvailable()) { - chaptersFromMediaFile = ChapterUtils.loadChaptersFromFileUrl(this); - } else { - chaptersFromMediaFile = ChapterUtils.loadChaptersFromStreamUrl(this, context); - } - - return ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile); - } - - @Override public String getEpisodeTitle() { if (item == null) { return null; @@ -478,6 +450,18 @@ public class FeedMedia extends FeedFile implements Playable { } @Override + public Date getPubDate() { + if (item == null) { + return null; + } + if (item.getPubDate() != null) { + return item.getPubDate(); + } else { + return null; + } + } + + @Override public boolean localFileAvailable() { return isDownloaded() && file_url != null; } @@ -487,6 +471,10 @@ public class FeedMedia extends FeedFile implements Playable { return download_url != null; } + public long getItemId() { + return itemID; + } + @Override public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timeStamp) { if(item != null && item.isNew()) { @@ -543,21 +531,11 @@ public class FeedMedia extends FeedFile implements Playable { @Override public void setChapters(List<Chapter> chapters) { - if(item != null) { + if (item != null) { item.setChapters(chapters); } } - @Override - public Callable<String> loadShownotes() { - return () -> { - if (item == null) { - item = DBReader.getFeedItem(itemID); - } - return item.loadShownotes().call(); - }; - } - public static final Parcelable.Creator<FeedMedia> CREATOR = new Parcelable.Creator<FeedMedia>() { public FeedMedia createFromParcel(Parcel in) { final long id = in.readLong(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java index bd4690684..794c71cf3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java @@ -1,12 +1,10 @@ package de.danoeh.antennapod.core.feed; -import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; import android.text.TextUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.PodDBAdapter; import java.util.Arrays; @@ -22,36 +20,37 @@ public class FeedPreferences { public static final String TAG_ROOT = "#root"; public static final String TAG_SEPARATOR = ","; - @NonNull - private FeedFilter filter; - private long feedID; - private boolean autoDownload; - private boolean keepUpdated; - public enum AutoDeleteAction { GLOBAL, YES, NO } - private AutoDeleteAction autoDeleteAction; + @NonNull + private FeedFilter filter; + private long feedID; + private boolean autoDownload; + private boolean keepUpdated; + private AutoDeleteAction autoDeleteAction; private VolumeAdaptionSetting volumeAdaptionSetting; - private String username; private String password; private float feedPlaybackSpeed; private int feedSkipIntro; private int feedSkipEnding; + private boolean showEpisodeNotification; private final Set<String> tags = new HashSet<>(); - public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction autoDeleteAction, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password) { + public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction autoDeleteAction, + VolumeAdaptionSetting volumeAdaptionSetting, String username, String password) { this(feedID, autoDownload, true, autoDeleteAction, volumeAdaptionSetting, - username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0, new HashSet<>()); + username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0, false, new HashSet<>()); } - private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction autoDeleteAction, - VolumeAdaptionSetting volumeAdaptionSetting, String username, String password, - @NonNull FeedFilter filter, float feedPlaybackSpeed, int feedSkipIntro, int feedSkipEnding, + private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, + AutoDeleteAction autoDeleteAction, VolumeAdaptionSetting volumeAdaptionSetting, + String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed, + int feedSkipIntro, int feedSkipEnding, boolean showEpisodeNotification, Set<String> tags) { this.feedID = feedID; this.autoDownload = autoDownload; @@ -64,6 +63,7 @@ public class FeedPreferences { this.feedPlaybackSpeed = feedPlaybackSpeed; this.feedSkipIntro = feedSkipIntro; this.feedSkipEnding = feedSkipEnding; + this.showEpisodeNotification = showEpisodeNotification; this.tags.addAll(tags); } @@ -80,6 +80,7 @@ public class FeedPreferences { int indexFeedPlaybackSpeed = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_PLAYBACK_SPEED); int indexAutoSkipIntro = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_INTRO); int indexAutoSkipEnding = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_ENDING); + int indexEpisodeNotification = cursor.getColumnIndex(PodDBAdapter.KEY_EPISODE_NOTIFICATION); int indexTags = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_TAGS); long feedId = cursor.getLong(indexId); @@ -96,11 +97,11 @@ public class FeedPreferences { float feedPlaybackSpeed = cursor.getFloat(indexFeedPlaybackSpeed); int feedAutoSkipIntro = cursor.getInt(indexAutoSkipIntro); int feedAutoSkipEnding = cursor.getInt(indexAutoSkipEnding); + boolean showNotification = cursor.getInt(indexEpisodeNotification) > 0; String tagsString = cursor.getString(indexTags); if (TextUtils.isEmpty(tagsString)) { tagsString = TAG_ROOT; } - return new FeedPreferences(feedId, autoDownload, autoRefresh, @@ -112,6 +113,7 @@ public class FeedPreferences { feedPlaybackSpeed, feedAutoSkipIntro, feedAutoSkipEnding, + showNotification, new HashSet<>(Arrays.asList(tagsString.split(TAG_SEPARATOR)))); } @@ -192,8 +194,8 @@ public class FeedPreferences { return volumeAdaptionSetting; } - public void setAutoDeleteAction(AutoDeleteAction auto_delete_action) { - this.autoDeleteAction = auto_delete_action; + public void setAutoDeleteAction(AutoDeleteAction autoDeleteAction) { + this.autoDeleteAction = autoDeleteAction; } public void setVolumeAdaptionSetting(VolumeAdaptionSetting volumeAdaptionSetting) { @@ -204,18 +206,12 @@ public class FeedPreferences { switch (autoDeleteAction) { case GLOBAL: return UserPreferences.isAutoDelete(); - case YES: return true; - case NO: + default: // fall-through return false; } - return false; // TODO - add exceptions here - } - - public void save(Context context) { - DBWriter.setFeedPreferences(this); } public String getUsername() { @@ -265,4 +261,16 @@ public class FeedPreferences { public String getTagsAsString() { return TextUtils.join(TAG_SEPARATOR, tags); } + + /** + * getter for preference if notifications should be display for new episodes. + * @return true for displaying notifications + */ + public boolean getShowEpisodeNotification() { + return showEpisodeNotification; + } + + public void setShowEpisodeNotification(boolean showEpisodeNotification) { + this.showEpisodeNotification = showEpisodeNotification; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index 4e59fd750..1418a4e78 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -6,15 +6,13 @@ import android.media.MediaMetadataRetriever; import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.NonNull; import androidx.documentfile.provider.DocumentFile; -import org.apache.commons.lang3.StringUtils; - import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; @@ -34,6 +32,8 @@ import de.danoeh.antennapod.core.util.DownloadError; public class LocalFeedUpdater { + static final String[] PREFERRED_FEED_IMAGE_FILENAMES = { "folder.jpg", "Folder.jpg", "folder.png", "Folder.png" }; + public static void updateFeed(Feed feed, Context context) { try { tryUpdateFeed(feed, context); @@ -97,18 +97,7 @@ public class LocalFeedUpdater { } } - List<String> iconLocations = Arrays.asList("folder.jpg", "Folder.jpg", "folder.png", "Folder.png"); - for (String iconLocation : iconLocations) { - DocumentFile image = documentFolder.findFile(iconLocation); - if (image != null) { - feed.setImageUrl(image.getUri().toString()); - break; - } - } - if (StringUtils.isBlank(feed.getImageUrl())) { - // set default feed image - feed.setImageUrl(getDefaultIconUrl(context)); - } + feed.setImageUrl(getImageUrl(context, documentFolder)); feed.getPreferences().setAutoDownload(false); feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); @@ -123,6 +112,31 @@ public class LocalFeedUpdater { } /** + * Returns the image URL for the local feed. + */ + @NonNull + static String getImageUrl(@NonNull Context context, @NonNull DocumentFile documentFolder) { + // look for special file names + for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) { + DocumentFile image = documentFolder.findFile(iconLocation); + if (image != null) { + return image.getUri().toString(); + } + } + + // use the first image in the folder if existing + for (DocumentFile file : documentFolder.listFiles()) { + String mime = file.getType(); + if (mime != null && (mime.startsWith("image/jpeg") || mime.startsWith("image/png"))) { + return file.getUri().toString(); + } + } + + // use default icon as fallback + return getDefaultIconUrl(context); + } + + /** * Returns the URL of the default icon for a local feed. The URL refers to an app resource file. */ public static String getDefaultIconUrl(Context context) { @@ -155,13 +169,13 @@ public class LocalFeedUpdater { try { loadMetadata(item, file, context); } catch (Exception e) { - item.setDescription(e.getMessage()); + item.setDescriptionIfLonger(e.getMessage()); } return item; } - private static void loadMetadata(FeedItem item, DocumentFile file, Context context) throws Exception { + private static void loadMetadata(FeedItem item, DocumentFile file, Context context) { MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(context, file.getUri()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/util/ImageResourceUtils.java b/core/src/main/java/de/danoeh/antennapod/core/feed/util/ImageResourceUtils.java index 674663a6d..b0aee3d77 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/util/ImageResourceUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/util/ImageResourceUtils.java @@ -1,45 +1,66 @@ package de.danoeh.antennapod.core.feed.util; -import de.danoeh.antennapod.core.asynctask.ImageResource; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.playback.Playable; /** - * Utility class to use the appropriate image resource based on {@link UserPreferences} + * Utility class to use the appropriate image resource based on {@link UserPreferences}. */ public final class ImageResourceUtils { private ImageResourceUtils() { } - public static String getImageLocation(ImageResource resource) { + /** + * returns the image location, does prefer the episode cover if available and enabled in settings. + */ + @Nullable + public static String getEpisodeListImageLocation(@NonNull Playable playable) { if (UserPreferences.getUseEpisodeCoverSetting()) { - return resource.getImageLocation(); + return playable.getImageLocation(); } else { - return getShowImageLocation(resource); + return getFallbackImageLocation(playable); } } - private static String getShowImageLocation(ImageResource resource) { + /** + * returns the image location, does prefer the episode cover if available and enabled in settings. + */ + @Nullable + public static String getEpisodeListImageLocation(@NonNull FeedItem feedItem) { + if (UserPreferences.getUseEpisodeCoverSetting()) { + return feedItem.getImageLocation(); + } else { + return getFallbackImageLocation(feedItem); + } + } - if (resource instanceof FeedItem) { - FeedItem item = (FeedItem) resource; - if (item.getFeed() != null) { - return item.getFeed().getImageLocation(); - } else { - return null; - } - } else if (resource instanceof FeedMedia) { - FeedMedia media = (FeedMedia) resource; + @Nullable + public static String getFallbackImageLocation(@NonNull Playable playable) { + if (playable instanceof FeedMedia) { + FeedMedia media = (FeedMedia) playable; FeedItem item = media.getItem(); if (item != null && item.getFeed() != null) { - return item.getFeed().getImageLocation(); + return item.getFeed().getImageUrl(); } else { return null; } } else { - return resource.getImageLocation(); + return playable.getImageLocation(); + } + } + + @Nullable + public static String getFallbackImageLocation(@NonNull FeedItem feedItem) { + if (feedItem.getFeed() != null) { + return feedItem.getFeed().getImageUrl(); + } else { + return null; } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java index 0a72b5d5c..209558b19 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java @@ -26,7 +26,7 @@ public class GpodnetPreferences { private static String username; private static String password; private static String deviceID; - private static String hostname; + private static String hosturl; private static boolean preferencesLoaded = false; @@ -40,7 +40,7 @@ public class GpodnetPreferences { username = prefs.getString(PREF_GPODNET_USERNAME, null); password = prefs.getString(PREF_GPODNET_PASSWORD, null); deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null); - hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST)); + hosturl = prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST); preferencesLoaded = true; } @@ -82,17 +82,16 @@ public class GpodnetPreferences { writePreference(PREF_GPODNET_DEVICEID, deviceID); } - public static String getHostname() { + public static String getHosturl() { ensurePreferencesLoaded(); - return hostname; + return hosturl; } - public static void setHostname(String value) { - value = checkGpodnetHostname(value); - if (!value.equals(hostname)) { + public static void setHosturl(String value) { + if (!value.equals(hosturl)) { logout(); writePreference(PREF_GPODNET_HOSTNAME, value); - hostname = value; + hosturl = value; } } @@ -113,13 +112,4 @@ public class GpodnetPreferences { UserPreferences.setGpodnetNotificationsEnabled(); } - private static String checkGpodnetHostname(String value) { - int startIndex = 0; - if (value.startsWith("http://")) { - startIndex = "http://".length(); - } else if (value.startsWith("https://")) { - startIndex = "https://".length(); - } - return value.substring(startIndex); - } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java index 08ea27434..95b828e28 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -100,7 +100,7 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals(PREF_CURRENT_PLAYER_STATUS)) { + if (PREF_CURRENT_PLAYER_STATUS.equals(key)) { EventBus.getDefault().post(new PlayerStatusEvent()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index ed9c519a6..cbfe28ded 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -6,6 +6,7 @@ import android.content.res.Configuration; import android.os.Build; import android.text.TextUtils; import android.util.Log; +import android.view.KeyEvent; import androidx.annotation.IntRange; import androidx.annotation.NonNull; @@ -35,6 +36,7 @@ import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.service.download.ProxyConfig; import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.ExceptFavoriteCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; @@ -60,6 +62,7 @@ public class UserPreferences { private static final String PREF_DRAWER_FEED_COUNTER = "prefDrawerFeedIndicator"; public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; public static final String PREF_USE_EPISODE_COVER = "prefEpisodeCover"; + public static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; public static final String PREF_COMPACT_NOTIFICATION_BUTTONS = "prefCompactNotificationButtons"; public static final String PREF_LOCKSCREEN_BACKGROUND = "prefLockscreenBackground"; @@ -76,8 +79,8 @@ public class UserPreferences { public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; public static final String PREF_UNPAUSE_ON_HEADSET_RECONNECT = "prefUnpauseOnHeadsetReconnect"; private static final String PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT = "prefUnpauseOnBluetoothReconnect"; - private static final String PREF_HARDWARE_FOWARD_BUTTON_SKIPS = "prefHardwareForwardButtonSkips"; - private static final String PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS = "prefHardwarePreviousButtonRestarts"; + public static final String PREF_HARDWARE_FORWARD_BUTTON = "prefHardwareForwardButton"; + public static final String PREF_HARDWARE_PREVIOUS_BUTTON = "prefHardwarePreviousButton"; public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; public static final String PREF_SKIP_KEEPS_EPISODE = "prefSkipKeepsEpisode"; private static final String PREF_FAVORITE_KEEPS_EPISODE = "prefFavoriteKeepsEpisode"; @@ -136,6 +139,7 @@ public class UserPreferences { public static final String PREF_CAST_ENABLED = "prefCast"; //Used for enabling Chromecast support public static final int EPISODE_CLEANUP_QUEUE = -1; public static final int EPISODE_CLEANUP_NULL = -2; + public static final int EPISODE_CLEANUP_EXCEPT_FAVORITE = -3; public static final int EPISODE_CLEANUP_DEFAULT = 0; // Constants @@ -265,6 +269,23 @@ public class UserPreferences { } /** + * @return {@code true} if we should show remaining time or the duration + */ + public static boolean shouldShowRemainingTime() { + return prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + } + + /** + * Sets the preference for whether we show the remain time, if not show the duration. This will + * send out events so the current playing screen, queue and the episode list would refresh + * + * @return {@code true} if we should show remaining time or the duration + */ + public static void setShowRemainTimeSetting(Boolean showRemain) { + prefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showRemain).apply(); + } + + /** * Returns notification priority. * * @return NotificationCompat.PRIORITY_MAX or NotificationCompat.PRIORITY_DEFAULT @@ -373,12 +394,14 @@ public class UserPreferences { return prefs.getBoolean(PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT, false); } - public static boolean shouldHardwareButtonSkip() { - return prefs.getBoolean(PREF_HARDWARE_FOWARD_BUTTON_SKIPS, false); + public static int getHardwareForwardButton() { + return Integer.parseInt(prefs.getString(PREF_HARDWARE_FORWARD_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD))); } - public static boolean shouldHardwarePreviousButtonRestart() { - return prefs.getBoolean(PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS, false); + public static int getHardwarePreviousButton() { + return Integer.parseInt(prefs.getString(PREF_HARDWARE_PREVIOUS_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_REWIND))); } @@ -879,7 +902,9 @@ public class UserPreferences { return new APNullCleanupAlgorithm(); } int cleanupValue = getEpisodeCleanupValue(); - if (cleanupValue == EPISODE_CLEANUP_QUEUE) { + if (cleanupValue == EPISODE_CLEANUP_EXCEPT_FAVORITE) { + return new ExceptFavoriteCleanupAlgorithm(); + } else if (cleanupValue == EPISODE_CLEANUP_QUEUE) { return new APQueueCleanupAlgorithm(); } else if (cleanupValue == EPISODE_CLEANUP_NULL) { return new APNullCleanupAlgorithm(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java index 2e592bdf5..cf0debed2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java @@ -9,20 +9,23 @@ import android.util.Log; import java.util.Arrays; -import de.danoeh.antennapod.core.service.PlayerWidgetJobService; +import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService; public class PlayerWidget extends AppWidgetProvider { private static final String TAG = "PlayerWidget"; public static final String PREFS_NAME = "PlayerWidgetPrefs"; private static final String KEY_ENABLED = "WidgetEnabled"; public static final String KEY_WIDGET_COLOR = "widget_color"; + public static final String KEY_WIDGET_SKIP = "widget_skip"; + public static final String KEY_WIDGET_FAST_FORWARD = "widget_fast_forward"; + public static final String KEY_WIDGET_REWIND = "widget_rewind"; public static final int DEFAULT_COLOR = 0x00262C31; @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "onReceive"); super.onReceive(context, intent); - PlayerWidgetJobService.updateWidget(context); + WidgetUpdaterJobService.performBackgroundUpdate(context); } @Override @@ -30,13 +33,14 @@ public class PlayerWidget extends AppWidgetProvider { super.onEnabled(context); Log.d(TAG, "Widget enabled"); setEnabled(context, true); - PlayerWidgetJobService.updateWidget(context); + WidgetUpdaterJobService.performBackgroundUpdate(context); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = [" + appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]"); - PlayerWidgetJobService.updateWidget(context); + Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = [" + + appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]"); + WidgetUpdaterJobService.performBackgroundUpdate(context); } @Override @@ -52,6 +56,9 @@ public class PlayerWidget extends AppWidgetProvider { for (int appWidgetId : appWidgetIds) { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); prefs.edit().remove(KEY_WIDGET_COLOR + appWidgetId).apply(); + prefs.edit().remove(KEY_WIDGET_REWIND + appWidgetId).apply(); + prefs.edit().remove(KEY_WIDGET_FAST_FORWARD + appWidgetId).apply(); + prefs.edit().remove(KEY_WIDGET_SKIP + appWidgetId).apply(); } super.onDeleted(context, appWidgetIds); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java deleted file mode 100644 index 74735a264..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java +++ /dev/null @@ -1,243 +0,0 @@ -package de.danoeh.antennapod.core.service; - -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.os.Bundle; -import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.core.app.SafeJobIntentService; -import android.util.Log; -import android.view.KeyEvent; -import android.view.View; -import android.widget.RemoteViews; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; - -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.glide.ApGlideSettings; -import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; -import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.core.receiver.PlayerWidget; -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; -import de.danoeh.antennapod.core.util.Converter; -import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; -import de.danoeh.antennapod.core.util.TimeSpeedConverter; -import de.danoeh.antennapod.core.util.playback.Playable; - -/** - * Updates the state of the player widget - */ -public class PlayerWidgetJobService extends SafeJobIntentService { - - private static final String TAG = "PlayerWidgetJobService"; - - private PlaybackService playbackService; - private final Object waitForService = new Object(); - private final Object waitUsingService = new Object(); - - private static final int JOB_ID = -17001; - - public static void updateWidget(Context context) { - enqueueWork(context, PlayerWidgetJobService.class, JOB_ID, new Intent(context, PlayerWidgetJobService.class)); - } - - @Override - protected void onHandleWork(@NonNull Intent intent) { - if (!PlayerWidget.isEnabled(getApplicationContext())) { - return; - } - - synchronized (waitForService) { - if (PlaybackService.isRunning && playbackService == null) { - bindService(new Intent(this, PlaybackService.class), mConnection, 0); - while (playbackService == null) { - try { - waitForService.wait(); - } catch (InterruptedException e) { - return; - } - } - } - } - - synchronized (waitUsingService) { - updateViews(); - } - - if (playbackService != null) { - try { - unbindService(mConnection); - } catch (IllegalArgumentException e) { - Log.w(TAG, "IllegalArgumentException when trying to unbind service"); - } - } - } - - /** - * Returns number of cells needed for given size of the widget. - * - * @param size Widget size in dp. - * @return Size in number of cells. - */ - private static int getCellsForSize(int size) { - int n = 2; - while (70 * n - 30 < size) { - ++n; - } - return n - 1; - } - - private void updateViews() { - - ComponentName playerWidget = new ComponentName(this, PlayerWidget.class); - AppWidgetManager manager = AppWidgetManager.getInstance(this); - int[] widgetIds = manager.getAppWidgetIds(playerWidget); - RemoteViews views = new RemoteViews(getPackageName(), R.layout.player_widget); - final PendingIntent startMediaPlayer = PendingIntent.getActivity(this, R.id.pending_intent_player_activity, - PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT); - - boolean nothingPlaying = false; - Playable media; - PlayerStatus status; - if (playbackService != null) { - media = playbackService.getPlayable(); - status = playbackService.getStatus(); - } else { - media = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()); - status = PlayerStatus.STOPPED; - } - - if (media != null) { - views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer); - - try { - Bitmap icon; - int iconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); - icon = Glide.with(PlayerWidgetJobService.this) - .asBitmap() - .load(ImageResourceUtils.getImageLocation(media)) - .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY)) - .submit(iconSize, iconSize) - .get(500, TimeUnit.MILLISECONDS); - views.setImageViewBitmap(R.id.imgvCover, icon); - } catch (Throwable tr) { - Log.e(TAG, "Error loading the media icon for the widget", tr); - views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round); - } - - views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); - views.setViewVisibility(R.id.txtvTitle, View.VISIBLE); - views.setViewVisibility(R.id.txtNoPlaying, View.GONE); - - String progressString; - if (playbackService != null) { - progressString = getProgressString(playbackService.getCurrentPosition(), - playbackService.getDuration(), playbackService.getCurrentPlaybackSpeed()); - } else { - progressString = getProgressString(media.getPosition(), media.getDuration(), PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)); - } - - if (progressString != null) { - views.setViewVisibility(R.id.txtvProgress, View.VISIBLE); - views.setTextViewText(R.id.txtvProgress, progressString); - } - - if (status == PlayerStatus.PLAYING) { - views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_pause_white_48dp); - views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); - } else { - views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp); - views.setContentDescription(R.id.butPlay, getString(R.string.play_label)); - } - views.setOnClickPendingIntent(R.id.butPlay, createMediaButtonIntent()); - } else { - nothingPlaying = true; - } - - if (nothingPlaying) { - // start the app if they click anything - views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer); - views.setOnClickPendingIntent(R.id.butPlay, startMediaPlayer); - views.setViewVisibility(R.id.txtvProgress, View.GONE); - views.setViewVisibility(R.id.txtvTitle, View.GONE); - views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE); - views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round); - views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp); - } - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - for (int id : widgetIds) { - Bundle options = manager.getAppWidgetOptions(id); - int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); - int columns = getCellsForSize(minWidth); - if (columns < 3) { - views.setViewVisibility(R.id.layout_center, View.INVISIBLE); - } else { - views.setViewVisibility(R.id.layout_center, View.VISIBLE); - } - - SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE); - int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR); - views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor); - - manager.updateAppWidget(id, views); - } - } else { - manager.updateAppWidget(playerWidget, views); - } - } - - /** - * Creates an intent which fakes a mediabutton press - */ - private PendingIntent createMediaButtonIntent() { - KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); - Intent startingIntent = new Intent(getBaseContext(), MediaButtonReceiver.class); - startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER); - startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); - - return PendingIntent.getBroadcast(this, 0, startingIntent, 0); - } - - private String getProgressString(int position, int duration, float speed) { - if (position >= 0 && duration > 0) { - TimeSpeedConverter converter = new TimeSpeedConverter(speed); - position = converter.convert(position); - duration = converter.convert(duration); - return Converter.getDurationStringLong(position) + " / " - + Converter.getDurationStringLong(duration); - } else { - return null; - } - } - - private final ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - Log.d(TAG, "Connection to service established"); - if (service instanceof PlaybackService.LocalBinder) { - synchronized (waitForService) { - playbackService = ((PlaybackService.LocalBinder) service).getService(); - waitForService.notifyAll(); - } - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - synchronized (waitUsingService) { - playbackService = null; - } - Log.d(TAG, "Disconnected from service"); - } - }; -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java index a01b3cb52..c4029d57f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java @@ -1,19 +1,14 @@ package de.danoeh.antennapod.core.service.download; -import android.os.Build; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.BasicAuthorizationInterceptor; import de.danoeh.antennapod.core.service.UserAgentInterceptor; -import de.danoeh.antennapod.core.ssl.BackportTrustManager; -import de.danoeh.antennapod.core.ssl.NoV1SslSocketFactory; import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.util.Flavors; +import de.danoeh.antennapod.net.ssl.SslClientSetup; import okhttp3.Cache; -import okhttp3.CipherSuite; -import okhttp3.ConnectionSpec; import okhttp3.Credentials; import okhttp3.HttpUrl; import okhttp3.JavaNetCookieJar; @@ -21,8 +16,6 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.internal.http.StatusLine; - -import javax.net.ssl.X509TrustManager; import java.io.File; import java.net.CookieManager; import java.net.CookiePolicy; @@ -30,9 +23,6 @@ import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -140,28 +130,7 @@ public class AntennapodHttpClient { } } - if (Flavors.FLAVOR == Flavors.FREE) { - // The Free flavor bundles a modern conscrypt (security provider), so CustomSslSocketFactory - // is only used to make sure that modern protocols (TLSv1.3 and TLSv1.2) are enabled and - // that old, deprecated, protocols (like SSLv3, TLSv1.0 and TLSv1.1) are disabled. - X509TrustManager trustManager = BackportTrustManager.create(); - builder.sslSocketFactory(new NoV1SslSocketFactory(trustManager), trustManager); - } else if (Build.VERSION.SDK_INT < 21) { - X509TrustManager trustManager = BackportTrustManager.create(); - builder.sslSocketFactory(new NoV1SslSocketFactory(trustManager), trustManager); - - // workaround for Android 4.x for certain web sites. - // see: https://github.com/square/okhttp/issues/4053#issuecomment-402579554 - List<CipherSuite> cipherSuites = new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites()); - cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA); - cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); - - ConnectionSpec legacyTls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .cipherSuites(cipherSuites.toArray(new CipherSuite[0])) - .build(); - builder.connectionSpecs(Arrays.asList(legacyTls, ConnectionSpec.CLEARTEXT)); - } - + SslClientSetup.installCertificates(builder); return builder; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index 5a2c653d6..2e0cb705b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -25,7 +25,6 @@ import org.greenrobot.eventbus.EventBus; import java.io.File; import java.io.IOException; -import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -96,6 +95,7 @@ public class DownloadService extends Service { private final CompletionService<Downloader> downloadExecutor; private final DownloadRequester requester; private DownloadServiceNotification notificationManager; + private final NewEpisodesNotification newEpisodesNotification; /** * Currently running downloads. @@ -118,7 +118,7 @@ public class DownloadService extends Service { private ScheduledFuture<?> notificationUpdaterFuture; private ScheduledFuture<?> downloadPostFuture; private static final int SCHED_EX_POOL_SIZE = 1; - private ScheduledThreadPoolExecutor schedExecutor; + private final ScheduledThreadPoolExecutor schedExecutor; private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory(); private final IBinder mBinder = new LocalBinder(); @@ -134,12 +134,16 @@ public class DownloadService extends Service { downloads = Collections.synchronizedList(new ArrayList<>()); numberOfDownloads = new AtomicInteger(0); requester = DownloadRequester.getInstance(); + newEpisodesNotification = new NewEpisodesNotification(); syncExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "SyncThread"); t.setPriority(Thread.MIN_PRIORITY); return t; }); + // Must be the first runnable in syncExecutor + syncExecutor.execute(newEpisodesNotification::loadCountersBeforeRefresh); + Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads()); downloadExecutor = new ExecutorCompletionService<>( Executors.newFixedThreadPool(UserPreferences.getParallelDownloads(), @@ -165,10 +169,10 @@ public class DownloadService extends Service { Notification notification = notificationManager.updateNotifications( requester.getNumberOfDownloads(), downloads); startForeground(R.id.notification_downloading, notification); + setupNotificationUpdaterIfNecessary(); syncExecutor.execute(() -> onDownloadQueued(intent)); } else if (numberOfDownloads.get() == 0) { - stopForeground(true); - stopSelf(); + shutdown(); } else { Log.d(TAG, "onStartCommand: Unknown intent"); } @@ -188,10 +192,6 @@ public class DownloadService extends Service { registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); downloadCompletionThread.start(); - - Notification notification = notificationManager.updateNotifications( - requester.getNumberOfDownloads(), downloads); - startForeground(R.id.notification_downloading, notification); } @Override @@ -226,10 +226,6 @@ public class DownloadService extends Service { } unregisterReceiver(cancelDownloadReceiver); - stopForeground(true); - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(R.id.notification_downloading); - // if this was the initial gpodder sync, i.e. we just synced the feeds successfully, // it is now time to sync the episode actions SyncService.sync(this); @@ -254,13 +250,13 @@ public class DownloadService extends Service { handleSuccessfulDownload(downloader); removeDownload(downloader); numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); + stopServiceIfEverythingDoneAsync(); }); } else { handleFailedDownload(downloader); removeDownload(downloader); numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); + stopServiceIfEverythingDoneAsync(); } } catch (InterruptedException e) { Log.e(TAG, "DownloadCompletionThread was interrupted"); @@ -290,6 +286,10 @@ public class DownloadService extends Service { if (log.size() > 0 && !log.get(0).isSuccessful()) { saveDownloadStatus(task.getDownloadStatus()); } + if (request.getFeedfileId() != 0 && !request.isInitiatedByUser()) { + // Was stored in the database before and not initiated manually + newEpisodesNotification.showIfNeeded(DownloadService.this, task.getSavedFeed()); + } } else { DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); saveDownloadStatus(task.getDownloadStatus()); @@ -325,18 +325,11 @@ public class DownloadService extends Service { if (item == null) { return; } - boolean httpNotFound = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && String.valueOf(HttpURLConnection.HTTP_NOT_FOUND).equals(status.getReasonDetailed()); - boolean forbidden = status.getReason() == DownloadError.ERROR_FORBIDDEN - && String.valueOf(HttpURLConnection.HTTP_FORBIDDEN).equals(status.getReasonDetailed()); - boolean notEnoughSpace = status.getReason() == DownloadError.ERROR_NOT_ENOUGH_SPACE; - boolean wrongFileType = status.getReason() == DownloadError.ERROR_FILE_TYPE; - boolean httpGone = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && String.valueOf(HttpURLConnection.HTTP_GONE).equals(status.getReasonDetailed()); - boolean httpBadReq = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && String.valueOf(HttpURLConnection.HTTP_BAD_REQUEST).equals(status.getReasonDetailed()); - - if (httpNotFound || forbidden || notEnoughSpace || wrongFileType || httpGone || httpBadReq ) { + boolean unknownHost = status.getReason() == DownloadError.ERROR_UNKNOWN_HOST; + boolean unsupportedType = status.getReason() == DownloadError.ERROR_UNSUPPORTED_TYPE; + boolean wrongSize = status.getReason() == DownloadError.ERROR_IO_WRONG_SIZE; + + if (! (unknownHost || unsupportedType || wrongSize)) { try { DBWriter.saveFeedItemAutoDownloadFailed(item).get(); } catch (ExecutionException | InterruptedException e) { @@ -412,7 +405,7 @@ public class DownloadService extends Service { } postDownloaders(); } - queryDownloads(); + stopServiceIfEverythingDone(); } }; @@ -483,7 +476,7 @@ public class DownloadService extends Service { postDownloaders(); }); } - handler.post(this::queryDownloads); + handler.post(this::stopServiceIfEverythingDone); } private static boolean isEnqueued(@NonNull DownloadRequest request, @@ -540,30 +533,19 @@ public class DownloadService extends Service { * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is * used from a thread other than the main thread. */ - private void queryDownloadsAsync() { - handler.post(DownloadService.this::queryDownloads); + private void stopServiceIfEverythingDoneAsync() { + handler.post(DownloadService.this::stopServiceIfEverythingDone); } /** * Check if there's something else to download, otherwise stop. */ - private void queryDownloads() { + private void stopServiceIfEverythingDone() { Log.d(TAG, numberOfDownloads.get() + " downloads left"); if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { - Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); - stopForeground(true); - stopSelf(); - if (notificationUpdater != null) { - notificationUpdater.run(); - } else { - Log.d(TAG, "Skipping notification update"); - } - } else { - setupNotificationUpdater(); - Notification notification = notificationManager.updateNotifications( - requester.getNumberOfDownloads(), downloads); - startForeground(R.id.notification_downloading, notification); + Log.d(TAG, "Attempting shutdown"); + shutdown(); } } @@ -616,7 +598,7 @@ public class DownloadService extends Service { /** * Schedules the notification updater task if it hasn't been scheduled yet. */ - private void setupNotificationUpdater() { + private void setupNotificationUpdaterIfNecessary() { if (notificationUpdater == null) { Log.d(TAG, "Setting up notification updater"); notificationUpdater = new NotificationUpdater(); @@ -653,4 +635,16 @@ public class DownloadService extends Service { new PostDownloaderTask(downloads), 1, 1, TimeUnit.SECONDS); } } + + private void shutdown() { + // If the service was run for a very short time, the system may delay closing + // the notification. Set the notification text now so that a misleading message + // is not left on the notification. + if (notificationUpdater != null) { + notificationUpdater.run(); + } + cancelNotificationUpdater(); + stopForeground(true); + stopSelf(); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java index fb6009c02..7b7879409 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java @@ -28,7 +28,10 @@ public class DownloadServiceNotification { private void setupNotificationBuilders() { notificationCompatBuilder = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_DOWNLOADING) - .setOngoing(true) + .setOngoing(false) + .setWhen(0) + .setOnlyAlertOnce(true) + .setShowWhen(false) .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(context)) .setSmallIcon(R.drawable.ic_notification_sync); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -50,7 +53,7 @@ public class DownloadServiceNotification { String contentTitle = context.getString(R.string.download_notification_title); String downloadsLeft = (numDownloads > 0) ? context.getResources().getQuantityString(R.plurals.downloads_left, numDownloads, numDownloads) - : context.getString(R.string.downloads_processing); + : context.getString(R.string.service_shutting_down); String bigText = compileNotificationString(downloads); notificationCompatBuilder.setContentTitle(contentTitle); @@ -106,6 +109,23 @@ public class DownloadServiceNotification { return sb.toString(); } + private String createFailedDownloadNotificationContent(List<DownloadStatus> statuses) { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < statuses.size(); i++) { + if (statuses.get(i).isSuccessful()) { + continue; + } + sb.append("• ").append(statuses.get(i).getTitle()); + sb.append(": ").append(statuses.get(i).getReason().getErrorString(context)); + if (i != statuses.size() - 1) { + sb.append("\n"); + } + } + + return sb.toString(); + } + /** * Creates a notification at the end of the service lifecycle to notify the * user about the number of completed downloads. A report will only be @@ -143,7 +163,7 @@ public class DownloadServiceNotification { // We are generating an auto-download report channelId = NotificationUtils.CHANNEL_ID_AUTO_DOWNLOAD; titleId = R.string.auto_download_report_title; - iconId = R.drawable.ic_notification_auto_download_complete; + iconId = R.drawable.ic_notification_new; intent = ClientConfig.downloadServiceCallbacks.getAutoDownloadReportNotificationContentIntent(context); id = R.id.notification_auto_download_report; content = createAutoDownloadNotificationContent(reportQueue); @@ -153,11 +173,7 @@ public class DownloadServiceNotification { iconId = R.drawable.ic_notification_sync_error; intent = ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(context); id = R.id.notification_download_report; - content = context.getResources() - .getQuantityString(R.plurals.download_report_content, - successfulDownloads, - successfulDownloads, - failedDownloads); + content = createFailedDownloadNotificationContent(reportQueue); } NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index 393592cf9..2d955859f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -19,6 +19,8 @@ import java.net.URI; import java.net.UnknownHostException; import java.util.Collections; import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.FeedMedia; @@ -37,6 +39,7 @@ public class HttpDownloader extends Downloader { private static final String TAG = "HttpDownloader"; private static final int BUFFER_SIZE = 8 * 1024; + private static final String REGEX_PATTERN_IP_ADDRESS = "([0-9]{1,3}[\\.]){3}[0-9]{1,3}"; public HttpDownloader(@NonNull DownloadRequest request) { super(request); @@ -134,6 +137,9 @@ public class HttpDownloader extends Downloader { } else if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) { error = DownloadError.ERROR_FORBIDDEN; details = String.valueOf(response.code()); + } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) { + error = DownloadError.ERROR_NOT_FOUND; + details = String.valueOf(response.code()); } else { error = DownloadError.ERROR_HTTP_DATA_ERROR; details = String.valueOf(response.code()); @@ -223,7 +229,7 @@ public class HttpDownloader extends Downloader { // written file. This check cannot be made if compression was used if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN && request.getSoFar() != request.getSize()) { - onFail(DownloadError.ERROR_IO_ERROR, "Download completed but size: " + + onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: " + request.getSoFar() + " does not equal expected size " + request.getSize()); return; } else if (request.getSize() > 0 && request.getSoFar() == 0) { @@ -250,6 +256,22 @@ public class HttpDownloader extends Downloader { onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); } catch (IOException e) { e.printStackTrace(); + String message = e.getMessage(); + if (message != null) { + // Try to parse message for a more detailed error message + Pattern pattern = Pattern.compile(REGEX_PATTERN_IP_ADDRESS); + Matcher matcher = pattern.matcher(message); + if (matcher.find()) { + String ip = matcher.group(); + if (ip.startsWith("127.") || ip.startsWith("0.")) { + onFail(DownloadError.ERROR_IO_BLOCKED, e.getMessage()); + return; + } + } else if (message.contains("Trust anchor for certification path not found")) { + onFail(DownloadError.ERROR_CERTIFICATE, e.getMessage()); + return; + } + } onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); } catch (NullPointerException e) { // might be thrown by connection.getInputStream() diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java new file mode 100644 index 000000000..799a68037 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java @@ -0,0 +1,132 @@ +package de.danoeh.antennapod.core.service.download; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; + +import android.graphics.Bitmap; +import android.util.Log; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.glide.ApGlideSettings; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.LongIntMap; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; + +public class NewEpisodesNotification { + private static final String TAG = "NewEpisodesNotification"; + private static final String GROUP_KEY = "de.danoeh.antennapod.EPISODES"; + + private LongIntMap countersBefore; + + public NewEpisodesNotification() { + } + + public void loadCountersBeforeRefresh() { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + countersBefore = adapter.getFeedCounters(UserPreferences.FEED_COUNTER_SHOW_NEW); + adapter.close(); + } + + public void showIfNeeded(Context context, Feed feed) { + FeedPreferences prefs = feed.getPreferences(); + if (!prefs.getKeepUpdated() || !prefs.getShowEpisodeNotification()) { + return; + } + + int newEpisodesBefore = countersBefore.get(feed.getId()); + int newEpisodesAfter = getNewEpisodeCount(feed.getId()); + + Log.d(TAG, "New episodes before: " + newEpisodesBefore + ", after: " + newEpisodesAfter); + if (newEpisodesAfter > newEpisodesBefore) { + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + showNotification(newEpisodesAfter, feed, context, notificationManager); + } + } + + private static void showNotification(int newEpisodes, Feed feed, Context context, + NotificationManagerCompat notificationManager) { + Resources res = context.getResources(); + String text = res.getQuantityString( + R.plurals.new_episode_notification_message, newEpisodes, newEpisodes, feed.getTitle() + ); + String title = res.getQuantityString(R.plurals.new_episode_notification_title, newEpisodes); + + Intent intent = new Intent(); + intent.setAction("NewEpisodes" + feed.getId()); + intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity")); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra("fragment_feed_id", feed.getId()); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + + Notification notification = new NotificationCompat.Builder( + context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS) + .setSmallIcon(R.drawable.ic_notification_new) + .setContentTitle(title) + .setLargeIcon(loadIcon(context, feed)) + .setContentText(text) + .setContentIntent(pendingIntent) + .setGroup(GROUP_KEY) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setAutoCancel(true) + .build(); + + notificationManager.notify(NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS, feed.hashCode(), notification); + showGroupSummaryNotification(context, notificationManager); + } + + private static void showGroupSummaryNotification(Context context, NotificationManagerCompat notificationManager) { + Intent intent = new Intent(); + intent.setAction("NewEpisodes"); + intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity")); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra("fragment_tag", "EpisodesFragment"); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + + Notification notificationGroupSummary = new NotificationCompat.Builder( + context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS) + .setSmallIcon(R.drawable.ic_notification_new) + .setContentTitle(context.getString(R.string.new_episode_notification_group_text)) + .setContentIntent(pendingIntent) + .setGroup(GROUP_KEY) + .setGroupSummary(true) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setAutoCancel(true) + .build(); + notificationManager.notify(NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS, 0, notificationGroupSummary); + } + + private static Bitmap loadIcon(Context context, Feed feed) { + int iconSize = (int) (128 * context.getResources().getDisplayMetrics().density); + try { + return Glide.with(context) + .asBitmap() + .load(feed.getImageUrl()) + .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY)) + .apply(new RequestOptions().centerCrop()) + .submit(iconSize, iconSize) + .get(); + } catch (Throwable tr) { + return null; + } + } + + private static int getNewEpisodeCount(long feedId) { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + int episodeCount = adapter.getFeedCounters(UserPreferences.FEED_COUNTER_SHOW_NEW, feedId).get(feedId); + adapter.close(); + return episodeCount; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java index 18c5fce27..d07018f13 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java @@ -58,6 +58,9 @@ public class FeedParserTask implements Callable<FeedHandlerResult> { e.printStackTrace(); successful = false; reason = DownloadError.ERROR_UNSUPPORTED_TYPE; + if ("html".equalsIgnoreCase(e.getRootElement())) { + reason = DownloadError.ERROR_UNSUPPORTED_TYPE_HTML; + } reasonDetailed = e.getMessage(); } catch (InvalidFeedException e) { e.printStackTrace(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java index 483a2aa56..e2d9ee614 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.service.download.handler; import android.content.Context; import android.util.Log; + import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; @@ -15,6 +16,7 @@ public class FeedSyncTask { private final DownloadRequest request; private final Context context; private DownloadStatus downloadStatus; + private Feed savedFeed; public FeedSyncTask(Context context, DownloadRequest request) { this.request = request; @@ -30,7 +32,7 @@ public class FeedSyncTask { return false; } - Feed savedFeed = DBTasks.updateFeed(context, result.feed, false); + savedFeed = DBTasks.updateFeed(context, result.feed, false); // If loadAllPages=true, check if another page is available and queue it for download final boolean loadAllPages = request.getArguments().getBoolean(DownloadRequester.REQUEST_ARG_LOAD_ALL_PAGES); final Feed feed = result.feed; @@ -48,4 +50,8 @@ public class FeedSyncTask { public DownloadStatus getDownloadStatus() { return downloadStatus; } + + public Feed getSavedFeed() { + return savedFeed; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java index 501214399..7712ca36b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -56,7 +56,7 @@ public class MediaDownloadedHandler implements Runnable { // check if file has chapters if (media.getItem() != null && !media.getItem().hasChapters()) { - media.setChapters(ChapterUtils.loadChaptersFromFileUrl(media)); + media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context)); } // Get duration diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java index 71bbf2efd..9a8248984 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.service.playback; import android.content.Context; import android.net.Uri; +import android.text.TextUtils; import android.util.Log; import android.view.SurfaceHolder; import com.google.android.exoplayer2.C; @@ -28,8 +29,10 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; + import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.HttpDownloader; import de.danoeh.antennapod.core.util.playback.IPlayer; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -184,14 +187,22 @@ public class ExoPlayerWrapper implements IPlayer { exoPlayer.setAudioAttributes(b.build()); } - @Override - public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException { + public void setDataSource(String s, String user, String password) + throws IllegalArgumentException, IllegalStateException { Log.d(TAG, "setDataSource: " + s); DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory( ClientConfig.USER_AGENT, null, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, true); + + if (!TextUtils.isEmpty(user) && !TextUtils.isEmpty(password)) { + httpDataSourceFactory.getDefaultRequestProperties().set("Authorization", + HttpDownloader.encodeCredentials( + user, + password, + "ISO-8859-1")); + } DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, null, httpDataSourceFactory); DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); extractorsFactory.setConstantBitrateSeekingEnabled(true); @@ -200,6 +211,11 @@ public class ExoPlayerWrapper implements IPlayer { } @Override + public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException { + setDataSource(s, null, null); + } + + @Override public void setDisplay(SurfaceHolder sh) { exoPlayer.setVideoSurfaceHolder(sh); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index 325b04e9a..28d8a0e29 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -1,6 +1,8 @@ package de.danoeh.antennapod.core.service.playback; +import android.app.UiModeManager; import android.content.Context; +import android.content.res.Configuration; import android.media.AudioManager; import android.os.PowerManager; import androidx.annotation.NonNull; @@ -36,6 +38,7 @@ import de.danoeh.antennapod.core.util.RewindAfterPauseUtils; import de.danoeh.antennapod.core.util.playback.AudioPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlayableException; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.core.util.playback.VideoPlayer; @@ -260,13 +263,25 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { callback.onMediaChanged(false); setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence()); if (stream) { - mediaPlayer.setDataSource(media.getStreamUrl()); + if (playable instanceof FeedMedia) { + FeedMedia feedMedia = (FeedMedia) playable; + FeedPreferences preferences = feedMedia.getItem().getFeed().getPreferences(); + mediaPlayer.setDataSource( + media.getStreamUrl(), + preferences.getUsername(), + preferences.getPassword()); + } else { + mediaPlayer.setDataSource(media.getStreamUrl()); + } } else if (media.getLocalMediaUrl() != null && new File(media.getLocalMediaUrl()).canRead()) { mediaPlayer.setDataSource(media.getLocalMediaUrl()); } else { throw new IOException("Unable to read local file " + media.getLocalMediaUrl()); } - setPlayerStatus(PlayerStatus.INITIALIZED, media); + UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() != Configuration.UI_MODE_TYPE_CAR) { + setPlayerStatus(PlayerStatus.INITIALIZED, media); + } if (prepareImmediately) { setPlayerStatus(PlayerStatus.PREPARING, media); @@ -274,7 +289,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { onPrepared(startWhenPrepared); } - } catch (Playable.PlayableException | IOException | IllegalStateException e) { + } catch (PlayableException | IOException | IllegalStateException e) { e.printStackTrace(); setPlayerStatus(PlayerStatus.ERROR, null); } @@ -924,9 +939,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - if (playerStatus != PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.INDETERMINATE, media); - } // we're relying on the position stored in the Playable object for post-playback processing if (media != null) { int position = getPosition(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index c1500d78b..9430e2e3c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -50,7 +50,6 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; @@ -69,7 +68,6 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.core.service.PlayerWidgetJobService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; @@ -78,9 +76,13 @@ import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.gui.NotificationUtils; -import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlayableException; +import de.danoeh.antennapod.core.util.playback.PlayableUtils; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; +import de.danoeh.antennapod.core.widget.WidgetUpdater; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -245,24 +247,31 @@ public class PlaybackService extends MediaBrowserServiceCompat { * running, the type of the last played media will be looked up. */ public static Intent getPlayerActivityIntent(Context context) { + boolean showVideoPlayer; + if (isRunning) { - return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType, isCasting); + showVideoPlayer = currentMediaType == MediaType.VIDEO && !isCasting; } else { - if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { - return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO, isCasting); - } else { - return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO, isCasting); - } + showVideoPlayer = PlaybackPreferences.getCurrentEpisodeIsVideo(); + } + + if (showVideoPlayer) { + return new VideoPlayerActivityStarter(context).getIntent(); + } else { + return new MainActivityStarter(context).withOpenPlayer().getIntent(); } } /** - * Same as getPlayerActivityIntent(context), but here the type of activity + * Same as {@link #getPlayerActivityIntent(Context)}, but here the type of activity * depends on the FeedMedia that is provided as an argument. */ public static Intent getPlayerActivityIntent(Context context, Playable media) { - MediaType mt = media.getMediaType(); - return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt, isCasting); + if (media.getMediaType() == MediaType.VIDEO && !isCasting) { + return new VideoPlayerActivityStarter(context).getIntent(); + } else { + return new MainActivityStarter(context).withOpenPlayer().getIntent(); + } } @Override @@ -401,8 +410,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { .setTitle(feed.getTitle()) .setDescription(feed.getDescription()) .setSubtitle(feed.getCustomTitle()); - if (feed.getImageLocation() != null) { - builder.setIconUri(Uri.parse(feed.getImageLocation())); + if (feed.getImageUrl() != null) { + builder.setIconUri(Uri.parse(feed.getImageUrl())); } if (feed.getLink() != null) { builder.setMediaUri(Uri.parse(feed.getLink())); @@ -509,8 +518,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - //If the user asks to play External Media, the casting session, if on, should end. - flavorHelper.castDisconnect(playable instanceof ExternalMedia); if (allowStreamAlways) { UserPreferences.setAllowMobileStreaming(true); } @@ -668,18 +675,14 @@ public class PlaybackService extends MediaBrowserServiceCompat { } return false; case KeyEvent.KEYCODE_MEDIA_NEXT: - if (getStatus() != PlayerStatus.PLAYING && getStatus() != PlayerStatus.PAUSED) { - return false; - } else if (notificationButton || UserPreferences.shouldHardwareButtonSkip()) { - // assume the skip command comes from a notification or the lockscreen - // a >| skip button should actually skip + if (!notificationButton) { + // Handle remapped button as notification button which is not remapped again. + return handleKeycode(UserPreferences.getHardwareForwardButton(), true); + } else if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) { mediaPlayer.skip(); - } else { - // assume skip command comes from a (bluetooth) media button - // user actually wants to fast-forward - seekDelta(UserPreferences.getFastForwardSecs() * 1000); + return true; } - return true; + return false; case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) { mediaPlayer.seekDelta(UserPreferences.getFastForwardSecs() * 1000); @@ -687,23 +690,20 @@ public class PlaybackService extends MediaBrowserServiceCompat { } return false; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - if (getStatus() != PlayerStatus.PLAYING && getStatus() != PlayerStatus.PAUSED) { - return false; - } else if (UserPreferences.shouldHardwarePreviousButtonRestart()) { - // user wants to restart current episode + if (!notificationButton) { + // Handle remapped button as notification button which is not remapped again. + return handleKeycode(UserPreferences.getHardwarePreviousButton(), true); + } else if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) { mediaPlayer.seekTo(0); - } else { - // user wants to rewind current episode - mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); + return true; } - return true; + return false; case KeyEvent.KEYCODE_MEDIA_REWIND: if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) { mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); - } else { - return false; + return true; } - return true; + return false; case KeyEvent.KEYCODE_MEDIA_STOP: if (status == PlayerStatus.PLAYING) { mediaPlayer.pause(true, true); @@ -722,7 +722,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { } private void startPlayingFromPreferences() { - Observable.fromCallable(() -> Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext())) + Observable.fromCallable(() -> PlayableUtils.createInstanceFromPreferences(getApplicationContext())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( @@ -801,8 +801,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public void onWidgetUpdaterTick() { - PlayerWidgetJobService.updateWidget(getBaseContext()); + public WidgetUpdater.WidgetState requestWidgetState() { + return new WidgetUpdater.WidgetState(getPlayable(), getStatus(), + getCurrentPosition(), getDuration(), getCurrentPlaybackSpeed(), isCasting()); } @Override @@ -873,9 +874,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { } IntentUtils.sendLocalBroadcast(getApplicationContext(), ACTION_PLAYER_STATUS_CHANGED); - PlayerWidgetJobService.updateWidget(getBaseContext()); bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); + taskManager.requestWidgetUpdate(); } @Override @@ -994,7 +995,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { FeedMedia media = (FeedMedia) currentMedia; try { media.loadMetadata(); - } catch (Playable.PlayableException e) { + } catch (PlayableException e) { Log.e(TAG, "Unable to load metadata to get next in queue", e); return null; } @@ -1240,7 +1241,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { capabilities = capabilities | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; } - UiModeManager uiModeManager = (UiModeManager) getApplicationContext().getSystemService(Context.UI_MODE_SERVICE); + UiModeManager uiModeManager = (UiModeManager) getApplicationContext() + .getSystemService(Context.UI_MODE_SERVICE); if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { sessionState.addCustomAction( new PlaybackStateCompat.CustomAction.Builder( @@ -1303,21 +1305,32 @@ public class PlaybackService extends MediaBrowserServiceCompat { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle()); builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle()); - String imageLocation = ImageResourceUtils.getImageLocation(p); + String imageLocation = p.getImageLocation(); if (!TextUtils.isEmpty(imageLocation)) { if (UserPreferences.setLockscreenBackground()) { + Bitmap art; builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, imageLocation); try { - Bitmap art = Glide.with(this) + art = Glide.with(this) .asBitmap() .load(imageLocation) .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY)) .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .get(); builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art); - } catch (Throwable tr) { - Log.e(TAG, Log.getStackTraceString(tr)); + } catch (Throwable tr1) { + try { + art = Glide.with(this) + .asBitmap() + .load(ImageResourceUtils.getFallbackImageLocation(p)) + .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY)) + .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .get(); + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art); + } catch (Throwable tr2) { + Log.e(TAG, Log.getStackTraceString(tr2)); + } } } else if (isCasting) { // In the absence of metadata art, the controller dialog takes care of creating it. @@ -1897,7 +1910,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void onSkipToNext() { Log.d(TAG, "onSkipToNext()"); - if (UserPreferences.shouldHardwareButtonSkip()) { + UiModeManager uiModeManager = (UiModeManager) getApplicationContext() + .getSystemService(Context.UI_MODE_SERVICE); + if (UserPreferences.getHardwareForwardButton() == KeyEvent.KEYCODE_MEDIA_NEXT + || uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { mediaPlayer.skip(); } else { seekDelta(UserPreferences.getFastForwardSecs() * 1000); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java index 9d249620d..cbfc36266 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java @@ -29,6 +29,8 @@ import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.core.util.gui.NotificationUtils; import de.danoeh.antennapod.core.util.playback.Playable; import java.util.ArrayList; +import java.util.concurrent.ExecutionException; + import org.apache.commons.lang3.ArrayUtils; public class PlaybackServiceNotificationBuilder { @@ -73,11 +75,23 @@ public class PlaybackServiceNotificationBuilder { try { icon = Glide.with(context) .asBitmap() - .load(ImageResourceUtils.getImageLocation(playable)) + .load(playable.getImageLocation()) .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY)) .apply(new RequestOptions().centerCrop()) .submit(iconSize, iconSize) .get(); + } catch (ExecutionException e) { + try { + icon = Glide.with(context) + .asBitmap() + .load(ImageResourceUtils.getFallbackImageLocation(playable)) + .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY)) + .apply(new RequestOptions().centerCrop()) + .submit(iconSize, iconSize) + .get(); + } catch (Throwable tr) { + Log.e(TAG, "Error loading the media icon for the notification", tr); + } } catch (Throwable tr) { Log.e(TAG, "Error loading the media icon for the notification", tr); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index 05d64ea3e..556d9b3c0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -8,6 +8,8 @@ import androidx.annotation.NonNull; import android.util.Log; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.widget.WidgetUpdater; import io.reactivex.disposables.Disposable; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -199,11 +201,10 @@ public class PlaybackServiceTaskManager { */ public synchronized void startWidgetUpdater() { if (!isWidgetUpdaterActive() && !schedExecutor.isShutdown()) { - Runnable widgetUpdater = callback::onWidgetUpdaterTick; + Runnable widgetUpdater = this::requestWidgetUpdate; widgetUpdater = useMainThreadIfNecessary(widgetUpdater); - widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL, - WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); - + widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, + WIDGET_UPDATER_NOTIFICATION_INTERVAL, WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); Log.d(TAG, "Started WidgetUpdater"); } else { Log.d(TAG, "Call to startWidgetUpdater was ignored."); @@ -211,6 +212,18 @@ public class PlaybackServiceTaskManager { } /** + * Retrieves information about the widget state in the calling thread and then displays it in a background thread. + */ + public synchronized void requestWidgetUpdate() { + WidgetUpdater.WidgetState state = callback.requestWidgetState(); + if (!schedExecutor.isShutdown()) { + schedExecutor.execute(() -> WidgetUpdater.updateWidget(context, state)); + } else { + Log.d(TAG, "Call to requestWidgetUpdate was ignored."); + } + } + + /** * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be * cancelled first. * After waitingTime has elapsed, onSleepTimerExpired() will be called. @@ -303,7 +316,7 @@ public class PlaybackServiceTaskManager { if (media.getChapters() == null) { chapterLoaderFuture = Completable.create(emitter -> { - media.loadChapterMarks(context); + ChapterUtils.loadChapters(media, context); emitter.onComplete(); }) .subscribeOn(Schedulers.io()) @@ -464,7 +477,7 @@ public class PlaybackServiceTaskManager { void onSleepTimerReset(); - void onWidgetUpdaterTick(); + WidgetUpdater.WidgetState requestWidgetState(); void onChapterLoaded(Playable media); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java deleted file mode 100644 index 061d6cf3f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java +++ /dev/null @@ -1,103 +0,0 @@ -package de.danoeh.antennapod.core.storage; - -import android.content.Context; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import de.danoeh.antennapod.core.feed.FeedFilter; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.PowerUtils; - -/** - * Implements the automatic download algorithm used by AntennaPod. This class assumes that - * the client uses the APEpisodeCleanupAlgorithm. - */ -public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { - private static final String TAG = "APDownloadAlgorithm"; - - /** - * Looks for undownloaded episodes in the queue or list of new items and request a download if - * 1. Network is available - * 2. The device is charging or the user allows auto download on battery - * 3. There is free space in the episode cache - * This method is executed on an internal single thread executor. - * - * @param context Used for accessing the DB. - * @return A Runnable that will be submitted to an ExecutorService. - */ - @Override - public Runnable autoDownloadUndownloadedItems(final Context context) { - return () -> { - - // true if we should auto download based on network status - boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable() - && UserPreferences.isEnableAutodownload(); - - // true if we should auto download based on power status - boolean powerShouldAutoDl = PowerUtils.deviceCharging(context) - || UserPreferences.isEnableAutodownloadOnBattery(); - - // we should only auto download if both network AND power are happy - if (networkShouldAutoDl && powerShouldAutoDl) { - - Log.d(TAG, "Performing auto-dl of undownloaded episodes"); - - List<FeedItem> candidates; - final List<FeedItem> queue = DBReader.getQueue(); - final List<FeedItem> newItems = DBReader.getNewItemsList(0, Integer.MAX_VALUE); - candidates = new ArrayList<>(queue.size() + newItems.size()); - candidates.addAll(queue); - for (FeedItem newItem : newItems) { - FeedPreferences feedPrefs = newItem.getFeed().getPreferences(); - FeedFilter feedFilter = feedPrefs.getFilter(); - if (!candidates.contains(newItem) && feedFilter.shouldAutoDownload(newItem)) { - candidates.add(newItem); - } - } - - // filter items that are not auto downloadable - Iterator<FeedItem> it = candidates.iterator(); - while (it.hasNext()) { - FeedItem item = it.next(); - if (!item.isAutoDownloadable() || item.getFeed().isLocalFeed()) { - it.remove(); - } - } - - int autoDownloadableEpisodes = candidates.size(); - int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(); - int deletedEpisodes = UserPreferences.getEpisodeCleanupAlgorithm() - .makeRoomForEpisodes(context, autoDownloadableEpisodes); - boolean cacheIsUnlimited = - UserPreferences.getEpisodeCacheSize() == UserPreferences.getEpisodeCacheSizeUnlimited(); - int episodeCacheSize = UserPreferences.getEpisodeCacheSize(); - - int episodeSpaceLeft; - if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) { - episodeSpaceLeft = autoDownloadableEpisodes; - } else { - episodeSpaceLeft = episodeCacheSize - (downloadedEpisodes - deletedEpisodes); - } - - FeedItem[] itemsToDownload = candidates.subList(0, episodeSpaceLeft) - .toArray(new FeedItem[episodeSpaceLeft]); - - if (itemsToDownload.length > 0) { - Log.d(TAG, "Enqueueing " + itemsToDownload.length + " items for download"); - - try { - DownloadRequester.getInstance().downloadMedia(false, context, false, itemsToDownload); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - } - } - }; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java index dbb77e19c..f8b643ccf 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java @@ -1,11 +1,28 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; +import android.util.Log; -public interface AutomaticDownloadAlgorithm { +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import de.danoeh.antennapod.core.feed.FeedFilter; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.PowerUtils; + +/** + * Implements the automatic download algorithm used by AntennaPod. This class assumes that + * the client uses the {@link EpisodeCleanupAlgorithm}. + */ +public class AutomaticDownloadAlgorithm { + private static final String TAG = "DownloadAlgorithm"; /** - * Looks for undownloaded episodes and request a download if + * Looks for undownloaded episodes in the queue or list of new items and request a download if * 1. Network is available * 2. The device is charging or the user allows auto download on battery * 3. There is free space in the episode cache @@ -14,5 +31,72 @@ public interface AutomaticDownloadAlgorithm { * @param context Used for accessing the DB. * @return A Runnable that will be submitted to an ExecutorService. */ - Runnable autoDownloadUndownloadedItems(Context context); + public Runnable autoDownloadUndownloadedItems(final Context context) { + return () -> { + + // true if we should auto download based on network status + boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable() + && UserPreferences.isEnableAutodownload(); + + // true if we should auto download based on power status + boolean powerShouldAutoDl = PowerUtils.deviceCharging(context) + || UserPreferences.isEnableAutodownloadOnBattery(); + + // we should only auto download if both network AND power are happy + if (networkShouldAutoDl && powerShouldAutoDl) { + + Log.d(TAG, "Performing auto-dl of undownloaded episodes"); + + List<FeedItem> candidates; + final List<FeedItem> queue = DBReader.getQueue(); + final List<FeedItem> newItems = DBReader.getNewItemsList(0, Integer.MAX_VALUE); + candidates = new ArrayList<>(queue.size() + newItems.size()); + candidates.addAll(queue); + for (FeedItem newItem : newItems) { + FeedPreferences feedPrefs = newItem.getFeed().getPreferences(); + FeedFilter feedFilter = feedPrefs.getFilter(); + if (!candidates.contains(newItem) && feedFilter.shouldAutoDownload(newItem)) { + candidates.add(newItem); + } + } + + // filter items that are not auto downloadable + Iterator<FeedItem> it = candidates.iterator(); + while (it.hasNext()) { + FeedItem item = it.next(); + if (!item.isAutoDownloadable() || item.getFeed().isLocalFeed()) { + it.remove(); + } + } + + int autoDownloadableEpisodes = candidates.size(); + int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(); + int deletedEpisodes = UserPreferences.getEpisodeCleanupAlgorithm() + .makeRoomForEpisodes(context, autoDownloadableEpisodes); + boolean cacheIsUnlimited = + UserPreferences.getEpisodeCacheSize() == UserPreferences.getEpisodeCacheSizeUnlimited(); + int episodeCacheSize = UserPreferences.getEpisodeCacheSize(); + + int episodeSpaceLeft; + if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) { + episodeSpaceLeft = autoDownloadableEpisodes; + } else { + episodeSpaceLeft = episodeCacheSize - (downloadedEpisodes - deletedEpisodes); + } + + FeedItem[] itemsToDownload = candidates.subList(0, episodeSpaceLeft) + .toArray(new FeedItem[episodeSpaceLeft]); + + if (itemsToDownload.length > 0) { + Log.d(TAG, "Enqueueing " + itemsToDownload.length + " items for download"); + + try { + DownloadRequester.getInstance().downloadMedia(false, context, false, itemsToDownload); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + } + }; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 74e8e23cb..e45d53af3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -18,11 +18,13 @@ import java.util.Map; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper; import de.danoeh.antennapod.core.util.LongIntMap; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; @@ -160,11 +162,15 @@ public final class DBReader { * The method does NOT change the items-attribute of the feed. */ public static List<FeedItem> getFeedItemList(final Feed feed) { + return getFeedItemList(feed, FeedItemFilter.unfiltered()); + } + + public static List<FeedItem> getFeedItemList(final Feed feed, final FeedItemFilter filter) { Log.d(TAG, "getFeedItemList() called with: " + "feed = [" + feed + "]"); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - try (Cursor cursor = adapter.getAllItemsOfFeedCursor(feed)) { + try (Cursor cursor = adapter.getItemsOfFeedCursor(feed, filter)) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); Collections.sort(items, new FeedItemPubdateComparator()); for (FeedItem item : items) { @@ -204,7 +210,7 @@ public final class DBReader { } private static Feed extractFeedFromCursorRow(Cursor cursor) { - Feed feed = Feed.fromCursor(cursor); + Feed feed = FeedCursorMapper.convert(cursor); FeedPreferences preferences = FeedPreferences.fromCursor(cursor); feed.setPreferences(preferences); return feed; @@ -367,18 +373,19 @@ public final class DBReader { } /** - * Loads a list of FeedItems sorted by pubDate in descending order. + * Loads a filtered list of FeedItems sorted by pubDate in descending order. * * @param offset The first episode that should be loaded. * @param limit The maximum number of episodes that should be loaded. + * @param filter The filter describing which episodes to filter out. */ @NonNull - public static List<FeedItem> getRecentlyPublishedEpisodes(int offset, int limit) { - Log.d(TAG, "getRecentlyPublishedEpisodes() called with: " + "offset = [" + offset + "]" + " limit = [" + limit + "]" ); + public static List<FeedItem> getRecentlyPublishedEpisodes(int offset, int limit, FeedItemFilter filter) { + Log.d(TAG, "getRecentlyPublishedEpisodes() called with: offset=" + offset + ", limit=" + limit); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - try (Cursor cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit)) { + try (Cursor cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit, filter)) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); loadAdditionalFeedItemListData(items); return items; @@ -478,31 +485,41 @@ public final class DBReader { * * @param feedId The ID of the Feed * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the - * database and the items-attribute will be set correctly. + * database and the items-attribute will be set correctly. */ + @Nullable public static Feed getFeed(final long feedId) { - Log.d(TAG, "getFeed() called with: " + "feedId = [" + feedId + "]"); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - try { - return getFeed(feedId, adapter); - } finally { - adapter.close(); - } + return getFeed(feedId, false); } + /** + * Loads a specific Feed from the database. + * + * @param feedId The ID of the Feed + * @param filtered <code>true</code> if only the visible items should be loaded according to the feed filter. + * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the + * database and the items-attribute will be set correctly. + */ @Nullable - static Feed getFeed(final long feedId, PodDBAdapter adapter) { + public static Feed getFeed(final long feedId, boolean filtered) { + Log.d(TAG, "getFeed() called with: " + "feedId = [" + feedId + "]"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); Feed feed = null; try (Cursor cursor = adapter.getFeedCursor(feedId)) { if (cursor.moveToNext()) { feed = extractFeedFromCursorRow(cursor); - feed.setItems(getFeedItemList(feed)); + if (filtered) { + feed.setItems(getFeedItemList(feed, feed.getItemFilter())); + } else { + feed.setItems(getFeedItemList(feed)); + } } else { Log.e(TAG, "getFeed could not find feed with id " + feedId); } return feed; + } finally { + adapter.close(); } } @@ -635,10 +652,7 @@ public final class DBReader { if (cursor.moveToFirst()) { int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION); String description = cursor.getString(indexDescription); - int indexContentEncoded = cursor.getColumnIndex(PodDBAdapter.KEY_CONTENT_ENCODED); - String contentEncoded = cursor.getString(indexContentEncoded); - item.setDescription(description); - item.setContentEncoded(contentEncoded); + item.setDescriptionIfLonger(description); } } finally { adapter.close(); @@ -801,15 +815,9 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - List<Feed> feeds = getFeedList(adapter); - long[] feedIds = new long[feeds.size()]; - for (int i = 0; i < feeds.size(); i++) { - feedIds[i] = feeds.get(i).getId(); - } - final LongIntMap feedCounters = adapter.getFeedCounters(feedIds); - + final LongIntMap feedCounters = adapter.getFeedCounters(); SubscriptionsFilter subscriptionsFilter = UserPreferences.getSubscriptionsFilter(); - feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters); + List<Feed> feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters); Comparator<Feed> comparator; int feedOrder = UserPreferences.getFeedOrder(); @@ -839,7 +847,7 @@ public final class DBReader { } }; } else if (feedOrder == UserPreferences.FEED_ORDER_MOST_PLAYED) { - final LongIntMap playedCounters = adapter.getPlayedEpisodesCounters(feedIds); + final LongIntMap playedCounters = adapter.getPlayedEpisodesCounters(); comparator = (lhs, rhs) -> { long counterLhs = playedCounters.get(lhs.getId()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index ec39e7144..d16432cd6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -6,7 +6,9 @@ import android.database.Cursor; import android.os.Looper; import android.text.TextUtils; import android.util.Log; -import de.danoeh.antennapod.core.ClientConfig; + +import androidx.annotation.VisibleForTesting; + import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.FeedListUpdateEvent; @@ -14,10 +16,10 @@ import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.feed.LocalFeedUpdater; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper; import de.danoeh.antennapod.core.sync.SyncService; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.LongList; @@ -29,6 +31,7 @@ import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.ListIterator; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -53,6 +56,8 @@ public final class DBTasks { */ private static final ExecutorService autodownloadExec; + private static AutomaticDownloadAlgorithm downloadAlgorithm = new AutomaticDownloadAlgorithm(); + static { autodownloadExec = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r); @@ -117,7 +122,18 @@ public final class DBTasks { throw new IllegalStateException("DBTasks.refreshAllFeeds() must not be called from the main thread."); } - refreshFeeds(context, DBReader.getFeedList(), initiatedByUser); + List<Feed> feeds = DBReader.getFeedList(); + ListIterator<Feed> iterator = feeds.listIterator(); + while (iterator.hasNext()) { + if (!iterator.next().getPreferences().getKeepUpdated()) { + iterator.remove(); + } + } + try { + refreshFeeds(context, feeds, false, false, false); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } isRefreshing.set(false); SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); @@ -131,38 +147,6 @@ public final class DBTasks { } /** - * @param context - * @param feedList the list of feeds to refresh - * @param initiatedByUser a boolean indicating if the refresh was triggered by user action. - */ - private static void refreshFeeds(final Context context, - final List<Feed> feedList, - boolean initiatedByUser) { - - for (Feed feed : feedList) { - FeedPreferences prefs = feed.getPreferences(); - // feeds with !getKeepUpdated can only be refreshed - // directly from the FeedActivity - if (prefs.getKeepUpdated()) { - try { - refreshFeed(context, feed); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus( - new DownloadStatus(feed, - feed.getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, - e.getMessage(), - initiatedByUser) - ); - } - } - } - - } - - /** * Downloads all pages of the given feed even if feed has not been modified since last refresh * * @param context Used for requesting the download. @@ -170,7 +154,7 @@ public final class DBTasks { */ public static void forceRefreshCompleteFeed(final Context context, final Feed feed) { try { - refreshFeed(context, feed, true, true, false); + refreshFeeds(context, Collections.singletonList(feed), true, true, false); } catch (DownloadRequestException e) { e.printStackTrace(); DBWriter.addDownloadStatus( @@ -206,19 +190,6 @@ public final class DBTasks { } /** - * Refresh a specific Feed. The refresh may get canceled if the feed does not seem to be modified - * and the last update was only few days ago. - * - * @param context Used for requesting the download. - * @param feed The Feed object. - */ - private static void refreshFeed(Context context, Feed feed) - throws DownloadRequestException { - Log.d(TAG, "refreshFeed(feed.id: " + feed.getId() +")"); - refreshFeed(context, feed, false, false, false); - } - - /** * Refresh a specific feed even if feed has not been modified since last refresh * * @param context Used for requesting the download. @@ -226,26 +197,32 @@ public final class DBTasks { */ public static void forceRefreshFeed(Context context, Feed feed, boolean initiatedByUser) throws DownloadRequestException { - Log.d(TAG, "refreshFeed(feed.id: " + feed.getId() +")"); - refreshFeed(context, feed, false, true, initiatedByUser); + Log.d(TAG, "refreshFeed(feed.id: " + feed.getId() + ")"); + refreshFeeds(context, Collections.singletonList(feed), false, true, initiatedByUser); } - private static void refreshFeed(Context context, Feed feed, boolean loadAllPages, boolean force, boolean initiatedByUser) - throws DownloadRequestException { - Feed f; - String lastUpdate = feed.hasLastUpdateFailed() ? null : feed.getLastUpdate(); - if (feed.getPreferences() == null) { - f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle()); - } else { - f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle(), - feed.getPreferences().getUsername(), feed.getPreferences().getPassword()); + private static void refreshFeeds(Context context, List<Feed> feeds, boolean loadAllPages, + boolean force, boolean initiatedByUser) throws DownloadRequestException { + List<Feed> localFeeds = new ArrayList<>(); + List<Feed> normalFeeds = new ArrayList<>(); + + for (Feed feed : feeds) { + if (feed.isLocalFeed()) { + localFeeds.add(feed); + } else { + normalFeeds.add(feed); + } } - f.setId(feed.getId()); - if (f.isLocalFeed()) { - new Thread(() -> LocalFeedUpdater.updateFeed(f, context)).start(); - } else { - DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force, initiatedByUser); + if (!localFeeds.isEmpty()) { + new Thread(() -> { + for (Feed feed : localFeeds) { + LocalFeedUpdater.updateFeed(feed, context); + } + }).start(); + } + if (!normalFeeds.isEmpty()) { + DownloadRequester.getInstance().downloadFeeds(context, feeds, loadAllPages, force, initiatedByUser); } } @@ -278,7 +255,7 @@ public final class DBTasks { } /** - * Looks for undownloaded episodes in the queue or list of unread items and request a download if + * Looks for non-downloaded episodes in the queue or list of unread items and request a download if * 1. Network is available * 2. The device is charging or the user allows auto download on battery * 3. There is free space in the episode cache @@ -289,9 +266,15 @@ public final class DBTasks { */ public static Future<?> autodownloadUndownloadedItems(final Context context) { Log.d(TAG, "autodownloadUndownloadedItems"); - return autodownloadExec.submit(ClientConfig.automaticDownloadAlgorithm - .autoDownloadUndownloadedItems(context)); + return autodownloadExec.submit(downloadAlgorithm.autoDownloadUndownloadedItems(context)); + } + /** + * For testing purpose only. + */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public static void setDownloadAlgorithm(AutomaticDownloadAlgorithm newDownloadAlgorithm) { + downloadAlgorithm = newDownloadAlgorithm; } /** @@ -337,7 +320,7 @@ public final class DBTasks { private static Feed searchFeedByIdentifyingValueOrID(PodDBAdapter adapter, Feed feed) { if (feed.getId() != 0) { - return DBReader.getFeed(feed.getId(), adapter); + return DBReader.getFeed(feed.getId()); } else { List<Feed> feeds = DBReader.getFeedList(); for (Feed f : feeds) { @@ -534,7 +517,7 @@ public final class DBTasks { List<Feed> items = new ArrayList<>(); if (cursor.moveToFirst()) { do { - items.add(Feed.fromCursor(cursor)); + items.add(FeedCursorMapper.convert(cursor)); } while (cursor.moveToNext()); } setResult(items); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java index 622389ed8..4e2eb6e5a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java @@ -312,6 +312,14 @@ class DBUpgrader { } if (oldVersion < 2020000) { db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_EPISODE_NOTIFICATION + " INTEGER DEFAULT 0;"); + } + if (oldVersion < 2030000) { + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " SET " + PodDBAdapter.KEY_DESCRIPTION + " = content_encoded, content_encoded = NULL " + + "WHERE length(" + PodDBAdapter.KEY_DESCRIPTION + ") < length(content_encoded)"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + " SET content_encoded = NULL"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + PodDBAdapter.KEY_FEED_TAGS + " TEXT;"); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index 84cc4b6a8..a86bdaa65 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -48,6 +48,7 @@ import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.Permutor; import de.danoeh.antennapod.core.util.SortOrder; import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlayableUtils; /** * Provides methods for writing data to AntennaPod's database. @@ -382,7 +383,7 @@ public class DBWriter { List<FeedItem> updatedItems = new ArrayList<>(); ItemEnqueuePositionCalculator positionCalculator = new ItemEnqueuePositionCalculator(UserPreferences.getEnqueueLocation()); - Playable currentlyPlaying = Playable.PlayableUtils.createInstanceFromPreferences(context); + Playable currentlyPlaying = PlayableUtils.createInstanceFromPreferences(context); int insertPosition = positionCalculator.calcPosition(queue, currentlyPlaying); for (long itemId : itemIds) { if (!itemListContains(queue, itemId)) { @@ -789,6 +790,7 @@ public class DBWriter { adapter.open(); adapter.setFeedItemlist(items); adapter.close(); + EventBus.getDefault().post(FeedItemEvent.updated(items)); }); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java index e3121caa2..638c1bef5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -17,6 +17,7 @@ import org.apache.commons.io.FilenameUtils; import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -184,16 +185,31 @@ public class DownloadRequester implements DownloadStateProvider { } /** - * Downloads a feed + * Downloads a feed. * * @param context The application's environment. - * @param feed Feed to download + * @param feed Feeds to download * @param loadAllPages Set to true to download all pages */ public synchronized void downloadFeed(Context context, Feed feed, boolean loadAllPages, - boolean force, boolean initiatedByUser) - throws DownloadRequestException { - if (feedFileValid(feed)) { + boolean force, boolean initiatedByUser) throws DownloadRequestException { + downloadFeeds(context, Collections.singletonList(feed), loadAllPages, force, initiatedByUser); + } + + /** + * Downloads a list of feeds. + * + * @param context The application's environment. + * @param feeds Feeds to download + * @param loadAllPages Set to true to download all pages + */ + public synchronized void downloadFeeds(Context context, List<Feed> feeds, boolean loadAllPages, + boolean force, boolean initiatedByUser) throws DownloadRequestException { + List<DownloadRequest> requests = new ArrayList<>(); + for (Feed feed : feeds) { + if (!feedFileValid(feed)) { + continue; + } String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null; String lastModified = feed.isPaged() || force ? null : feed.getLastUpdate(); @@ -206,9 +222,12 @@ public class DownloadRequester implements DownloadStateProvider { true, username, password, lastModified, true, args, initiatedByUser ); if (request != null) { - download(context, request); + requests.add(request); } } + if (!requests.isEmpty()) { + download(context, requests.toArray(new DownloadRequest[0])); + } } public synchronized void downloadFeed(Context context, Feed feed) throws DownloadRequestException { diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java new file mode 100644 index 000000000..f0788db33 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java @@ -0,0 +1,99 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ExecutionException; + +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +/** + * A cleanup algorithm that removes any item that isn't a favorite but only if space is needed. + */ +public class ExceptFavoriteCleanupAlgorithm extends EpisodeCleanupAlgorithm { + + private static final String TAG = "ExceptFavCleanupAlgo"; + + /** + * The maximum number of episodes that could be cleaned up. + * + * @return the number of episodes that *could* be cleaned up, if needed + */ + public int getReclaimableItems() { + return getCandidates().size(); + } + + @Override + public int performCleanup(Context context, int numberOfEpisodesToDelete) { + List<FeedItem> candidates = getCandidates(); + List<FeedItem> delete; + + // in the absence of better data, we'll sort by item publication date + Collections.sort(candidates, (lhs, rhs) -> { + Date l = lhs.getPubDate(); + Date r = rhs.getPubDate(); + + if (l != null && r != null) { + return l.compareTo(r); + } else { + // No date - compare by id which should be always incremented + return Long.compare(lhs.getId(), rhs.getId()); + } + }); + + if (candidates.size() > numberOfEpisodesToDelete) { + delete = candidates.subList(0, numberOfEpisodesToDelete); + } else { + delete = candidates; + } + + for (FeedItem item : delete) { + try { + DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + int counter = delete.size(); + Log.i(TAG, String.format(Locale.US, + "Auto-delete deleted %d episodes (%d requested)", counter, + numberOfEpisodesToDelete)); + + return counter; + } + + @NonNull + private List<FeedItem> getCandidates() { + List<FeedItem> candidates = new ArrayList<>(); + List<FeedItem> downloadedItems = DBReader.getDownloadedItems(); + for (FeedItem item : downloadedItems) { + if (item.hasMedia() + && item.getMedia().isDownloaded() + && !item.isTagged(FeedItem.TAG_FAVORITE)) { + candidates.add(item); + } + } + return candidates; + } + + @Override + public int getDefaultCleanupParameter() { + int cacheSize = UserPreferences.getEpisodeCacheSize(); + if (cacheSize != UserPreferences.getEpisodeCacheSizeUnlimited()) { + int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(); + if (downloadedEpisodes > cacheSize) { + return downloadedEpisodes - cacheSize; + } + } + return 0; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index 8f47675a8..98d5e6310 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -15,7 +15,9 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import de.danoeh.antennapod.core.storage.mapper.FeedItemFilterQuery; import org.apache.commons.io.FileUtils; import java.io.File; @@ -30,6 +32,7 @@ import java.util.Set; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -49,7 +52,7 @@ public class PodDBAdapter { private static final String TAG = "PodDBAdapter"; public static final String DATABASE_NAME = "Antennapod.db"; - public static final int VERSION = 2020000; + public static final int VERSION = 2030000; /** * Maximum number of arguments for IN-operator. @@ -81,7 +84,6 @@ public class PodDBAdapter { public static final String KEY_FEEDFILETYPE = "feedfile_type"; public static final String KEY_COMPLETION_DATE = "completion_date"; public static final String KEY_FEEDITEM = "feeditem"; - public static final String KEY_CONTENT_ENCODED = "content_encoded"; public static final String KEY_PAYMENT_LINK = "payment_link"; public static final String KEY_START = "start"; public static final String KEY_LANGUAGE = "language"; @@ -114,16 +116,17 @@ public class PodDBAdapter { public static final String KEY_FEED_SKIP_INTRO = "feed_skip_intro"; public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending"; public static final String KEY_FEED_TAGS = "tags"; + public static final String KEY_EPISODE_NOTIFICATION = "episode_notification"; // Table names - static final String TABLE_NAME_FEEDS = "Feeds"; - static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; - static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; - static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; - static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; - static final String TABLE_NAME_QUEUE = "Queue"; - static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; - static final String TABLE_NAME_FAVORITES = "Favorites"; + public static final String TABLE_NAME_FEEDS = "Feeds"; + public static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; + public static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; + public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; + public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; + public static final String TABLE_NAME_QUEUE = "Queue"; + public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; + public static final String TABLE_NAME_FAVORITES = "Favorites"; // SQL Statements for creating new tables private static final String TABLE_PRIMARY_KEY = KEY_ID @@ -152,12 +155,13 @@ public class PodDBAdapter { + KEY_FEED_VOLUME_ADAPTION + " INTEGER DEFAULT 0," + KEY_FEED_TAGS + " TEXT," + KEY_FEED_SKIP_INTRO + " INTEGER DEFAULT 0," - + KEY_FEED_SKIP_ENDING + " INTEGER DEFAULT 0)"; + + KEY_FEED_SKIP_ENDING + " INTEGER DEFAULT 0," + + KEY_EPISODE_NOTIFICATION + " INTEGER DEFAULT 0)"; private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " - + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE - + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE - + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," + + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + + KEY_TITLE + " TEXT," + KEY_PUBDATE + " INTEGER," + + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," @@ -255,7 +259,8 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_FEED_PLAYBACK_SPEED, TABLE_NAME_FEEDS + "." + KEY_FEED_TAGS, TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_INTRO, - TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_ENDING + TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_ENDING, + TABLE_NAME_FEEDS + "." + KEY_EPISODE_NOTIFICATION }; /** @@ -308,8 +313,7 @@ public class PodDBAdapter { private static final String SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION = "SELECT " + KEYS_FEED_ITEM_WITHOUT_DESCRIPTION + ", " + KEYS_FEED_MEDIA + ", " - + TABLE_NAME_FEED_ITEMS + "." + KEY_DESCRIPTION + ", " - + TABLE_NAME_FEED_ITEMS + "." + KEY_CONTENT_ENCODED + + TABLE_NAME_FEED_ITEMS + "." + KEY_DESCRIPTION + " FROM " + TABLE_NAME_FEED_ITEMS + JOIN_FEED_ITEM_AND_MEDIA; private static final String SELECT_FEED_ITEMS_AND_MEDIA = @@ -370,6 +374,7 @@ public class PodDBAdapter { * For more information see * <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p> */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) public static void tearDownTests() { getInstance().dbHelper.close(); instance = null; @@ -448,6 +453,7 @@ public class PodDBAdapter { values.put(KEY_FEED_TAGS, prefs.getTagsAsString()); values.put(KEY_FEED_SKIP_INTRO, prefs.getFeedSkipIntro()); values.put(KEY_FEED_SKIP_ENDING, prefs.getFeedSkipEnding()); + values.put(KEY_EPISODE_NOTIFICATION, prefs.getShowEpisodeNotification()); db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); } @@ -623,9 +629,6 @@ public class PodDBAdapter { if (item.getDescription() != null) { values.put(KEY_DESCRIPTION, item.getDescription()); } - if (item.getContentEncoded() != null) { - values.put(KEY_CONTENT_ENCODED, item.getContentEncoded()); - } values.put(KEY_PUBDATE, item.getPubDate().getTime()); values.put(KEY_PAYMENT_LINK, item.getPaymentLink()); if (saveFeed && item.getFeed() != null) { @@ -947,9 +950,12 @@ public class PodDBAdapter { * @param feed The feed you want to get the FeedItems from. * @return The cursor of the query */ - public final Cursor getAllItemsOfFeedCursor(final Feed feed) { + public final Cursor getItemsOfFeedCursor(final Feed feed, FeedItemFilter filter) { + String filterQuery = FeedItemFilterQuery.generateFrom(filter); + String whereClauseAnd = "".equals(filterQuery) ? "" : " AND " + filterQuery; final String query = SELECT_FEED_ITEMS_AND_MEDIA - + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + feed.getId(); + + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + feed.getId() + + whereClauseAnd; return db.rawQuery(query, null); } @@ -957,7 +963,7 @@ public class PodDBAdapter { * Return the description and content_encoded of item */ public final Cursor getDescriptionOfItem(final FeedItem item) { - final String query = "SELECT " + KEY_DESCRIPTION + ", " + KEY_CONTENT_ENCODED + final String query = "SELECT " + KEY_DESCRIPTION + " FROM " + TABLE_NAME_FEED_ITEMS + " WHERE " + KEY_ID + "=" + item.getId(); return db.rawQuery(query, null); @@ -1048,9 +1054,11 @@ public class PodDBAdapter { return db.rawQuery(query, null); } - public final Cursor getRecentlyPublishedItemsCursor(int offset, int limit) { - final String query = SELECT_FEED_ITEMS_AND_MEDIA - + "ORDER BY " + KEY_PUBDATE + " DESC LIMIT " + offset + ", " + limit; + public final Cursor getRecentlyPublishedItemsCursor(int offset, int limit, FeedItemFilter filter) { + String filterQuery = FeedItemFilterQuery.generateFrom(filter); + String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery; + final String query = SELECT_FEED_ITEMS_AND_MEDIA + whereClause + + " ORDER BY " + KEY_PUBDATE + " DESC LIMIT " + offset + ", " + limit; return db.rawQuery(query, null); } @@ -1164,6 +1172,11 @@ public class PodDBAdapter { public final LongIntMap getFeedCounters(long... feedIds) { int setting = UserPreferences.getFeedCounterSetting(); + + return getFeedCounters(setting, feedIds); + } + + public final LongIntMap getFeedCounters(int setting, long... feedIds) { String whereRead; switch (setting) { case UserPreferences.FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM: @@ -1188,24 +1201,26 @@ public class PodDBAdapter { } private LongIntMap conditionalFeedCounterRead(String whereRead, long... feedIds) { - // work around TextUtils.join wanting only boxed items - // and StringUtils.join() causing NoSuchMethodErrors on MIUI - StringBuilder builder = new StringBuilder(); - for (long id : feedIds) { - builder.append(id); - builder.append(','); - } + String limitFeeds = ""; if (feedIds.length > 0) { + // work around TextUtils.join wanting only boxed items + // and StringUtils.join() causing NoSuchMethodErrors on MIUI + StringBuilder builder = new StringBuilder(); + for (long id : feedIds) { + builder.append(id); + builder.append(','); + } // there's an extra ',', get rid of it builder.deleteCharAt(builder.length() - 1); + limitFeeds = KEY_FEED + " IN (" + builder.toString() + ") AND "; } final String query = "SELECT " + KEY_FEED + ", COUNT(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ") AS count " + " FROM " + TABLE_NAME_FEED_ITEMS + " LEFT JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM - + " WHERE " + KEY_FEED + " IN (" + builder.toString() + ") " - + " AND " + whereRead + " GROUP BY " + KEY_FEED; + + " WHERE " + limitFeeds + " " + + whereRead + " GROUP BY " + KEY_FEED; Cursor c = db.rawQuery(query, null); LongIntMap result = new LongIntMap(c.getCount()); @@ -1301,8 +1316,6 @@ public class PodDBAdapter { .append("(") .append(KEY_DESCRIPTION + " LIKE '%").append(queryWords[i]) .append("%' OR ") - .append(KEY_CONTENT_ENCODED).append(" LIKE '%").append(queryWords[i]) - .append("%' OR ") .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i]) .append("%') "); @@ -1368,7 +1381,16 @@ public class PodDBAdapter { } /** - * Called when a database corruption happens + * Insert raw data to the database. * + * Call method only for unit tests. + */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public void insertTestData(@NonNull String table, @NonNull ContentValues values) { + db.insert(table, null, values); + } + + /** + * Called when a database corruption happens. */ public static class PodDbErrorHandler implements DatabaseErrorHandler { @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java new file mode 100644 index 000000000..783fba596 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java @@ -0,0 +1,70 @@ +package de.danoeh.antennapod.core.storage.mapper; + +import android.database.Cursor; + +import androidx.annotation.NonNull; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.SortOrder; + +/** + * Converts a {@link Cursor} to a {@link Feed} object. + */ +public abstract class FeedCursorMapper { + + /** + * Create a {@link Feed} instance from a database row (cursor). + */ + @NonNull + public static Feed convert(@NonNull Cursor cursor) { + int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); + int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE); + int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE); + int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE); + int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK); + int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION); + int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK); + int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR); + int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE); + int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE); + int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER); + int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL); + int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL); + int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED); + int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED); + int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK); + int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE); + int indexSortOrder = cursor.getColumnIndex(PodDBAdapter.KEY_SORT_ORDER); + int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED); + int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL); + + Feed feed = new Feed( + cursor.getLong(indexId), + cursor.getString(indexLastUpdate), + cursor.getString(indexTitle), + cursor.getString(indexCustomTitle), + cursor.getString(indexLink), + cursor.getString(indexDescription), + cursor.getString(indexPaymentLink), + cursor.getString(indexAuthor), + cursor.getString(indexLanguage), + cursor.getString(indexType), + cursor.getString(indexFeedIdentifier), + cursor.getString(indexImageUrl), + cursor.getString(indexFileUrl), + cursor.getString(indexDownloadUrl), + cursor.getInt(indexDownloaded) > 0, + cursor.getInt(indexIsPaged) > 0, + cursor.getString(indexNextPageLink), + cursor.getString(indexHide), + SortOrder.fromCodeString(cursor.getString(indexSortOrder)), + cursor.getInt(indexLastUpdateFailed) > 0 + ); + + FeedPreferences preferences = FeedPreferences.fromCursor(cursor); + feed.setPreferences(preferences); + return feed; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java new file mode 100644 index 000000000..f6963b5ac --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java @@ -0,0 +1,76 @@ +package de.danoeh.antennapod.core.storage.mapper; + +import de.danoeh.antennapod.core.feed.FeedItemFilter; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +import java.util.ArrayList; +import java.util.List; + +public class FeedItemFilterQuery { + private FeedItemFilterQuery() { + // Must not be instantiated + } + + /** + * Express the filter using an SQL boolean statement that can be inserted into an SQL WHERE clause + * to yield output filtered according to the rules of this filter. + * + * @return An SQL boolean statement that matches the desired items, + * empty string if there is nothing to filter + */ + public static String generateFrom(FeedItemFilter filter) { + // The keys used within this method, but explicitly combined with their table + String keyRead = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_READ; + String keyPosition = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_POSITION; + String keyDownloaded = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DOWNLOADED; + String keyMediaId = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_ID; + String keyItemId = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID; + String keyFeedItem = PodDBAdapter.KEY_FEEDITEM; + String tableQueue = PodDBAdapter.TABLE_NAME_QUEUE; + String tableFavorites = PodDBAdapter.TABLE_NAME_FAVORITES; + + List<String> statements = new ArrayList<>(); + if (filter.showPlayed) { + statements.add(keyRead + " = 1 "); + } else if (filter.showUnplayed) { + statements.add(" NOT " + keyRead + " = 1 "); // Match "New" items (read = -1) as well + } + if (filter.showPaused) { + statements.add(" (" + keyPosition + " NOT NULL AND " + keyPosition + " > 0 " + ") "); + } else if (filter.showNotPaused) { + statements.add(" (" + keyPosition + " IS NULL OR " + keyPosition + " = 0 " + ") "); + } + if (filter.showQueued) { + statements.add(keyItemId + " IN (SELECT " + keyFeedItem + " FROM " + tableQueue + ") "); + } else if (filter.showNotQueued) { + statements.add(keyItemId + " NOT IN (SELECT " + keyFeedItem + " FROM " + tableQueue + ") "); + } + if (filter.showDownloaded) { + statements.add(keyDownloaded + " = 1 "); + } else if (filter.showNotDownloaded) { + statements.add(keyDownloaded + " = 0 "); + } + if (filter.showHasMedia) { + statements.add(keyMediaId + " NOT NULL "); + } else if (filter.showNoMedia) { + statements.add(keyMediaId + " IS NULL "); + } + if (filter.showIsFavorite) { + statements.add(keyItemId + " IN (SELECT " + keyFeedItem + " FROM " + tableFavorites + ") "); + } else if (filter.showNotFavorite) { + statements.add(keyItemId + " NOT IN (SELECT " + keyFeedItem + " FROM " + tableFavorites + ") "); + } + + if (statements.isEmpty()) { + return ""; + } + + StringBuilder query = new StringBuilder(" (" + statements.get(0)); + for (String r : statements.subList(1, statements.size())) { + query.append(" AND "); + query.append(r); + } + query.append(") "); + return query.toString(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java index 7563ab715..670a65e44 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java @@ -80,7 +80,7 @@ public class SyncService extends Worker { if (!GpodnetPreferences.loggedIn()) { return Result.success(); } - syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname()); + syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHosturl()); SharedPreferences.Editor prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) .edit(); prefs.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()).apply(); @@ -474,6 +474,7 @@ public class SyncService extends Worker { } } DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray()); + DBReader.loadAdditionalFeedItemListData(updatedItems); DBWriter.setItemList(updatedItems); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java index 62c8ce5f3..cecfc0d2c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.sync.gpoddernet; import android.util.Log; import androidx.annotation.NonNull; +import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; import de.danoeh.antennapod.core.sync.model.EpisodeAction; @@ -36,27 +37,63 @@ import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Communicates with the gpodder.net service. */ public class GpodnetService implements ISyncService { public static final String TAG = "GpodnetService"; public static final String DEFAULT_BASE_HOST = "gpodder.net"; - private static final String BASE_SCHEME = "https"; private static final int UPLOAD_BULK_SIZE = 30; private static final MediaType TEXT = MediaType.parse("plain/text; charset=utf-8"); private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - private final String baseHost; + private String baseScheme; + private String baseHost; + private int basePort; + private final OkHttpClient httpClient; private String username = null; - public GpodnetService(OkHttpClient httpClient, String baseHost) { + // split into schema, host and port - missing parts are null + private static Pattern urlsplit_regex = Pattern.compile("(?:(https?)://)?([^:]+)(?::(\\d+))?"); + + public GpodnetService(OkHttpClient httpClient, String baseHosturl) { this.httpClient = httpClient; - this.baseHost = baseHost; + + Matcher m = urlsplit_regex.matcher(baseHosturl); + if (m.matches()) { + this.baseScheme = m.group(1); + this.baseHost = m.group(2); + if (m.group(3) == null) { + this.basePort = -1; + } else { + this.basePort = Integer.parseInt(m.group(3)); // regex -> can only be digits + } + } else { + // URL does not match regex: use it anyway -> this will cause an exception on connect + this.baseScheme = "https"; + this.baseHost = baseHosturl; + this.basePort = 443; + } + + if (this.baseScheme == null) { // assume https + this.baseScheme = "https"; + } + + if (this.baseScheme.equals("https") && this.basePort == -1) { + this.basePort = 443; + } + + if (this.baseScheme.equals("http") && this.basePort == -1) { + this.basePort = 80; + } } private void requireLoggedIn() { @@ -71,7 +108,8 @@ public class GpodnetService implements ISyncService { public List<GpodnetTag> getTopTags(int count) throws GpodnetServiceException { URL url; try { - url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, "/api/2/tags/%d.json", count), null).toURL(); + url = new URI(baseScheme, null, baseHost, basePort, + String.format(Locale.US, "/api/2/tags/%d.json", count), null, null).toURL(); } catch (MalformedURLException | URISyntaxException e) { e.printStackTrace(); throw new GpodnetServiceException(e); @@ -104,8 +142,8 @@ public class GpodnetService implements ISyncService { public List<GpodnetPodcast> getPodcastsForTag(@NonNull GpodnetTag tag, int count) throws GpodnetServiceException { try { - URL url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, - "/api/2/tag/%s/%d.json", tag.getTag(), count), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format(Locale.US, "/api/2/tag/%s/%d.json", tag.getTag(), count), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -130,7 +168,8 @@ public class GpodnetService implements ISyncService { } try { - URL url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, "/toplist/%d.json", count), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format(Locale.US, "/toplist/%d.json", count), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -161,8 +200,8 @@ public class GpodnetService implements ISyncService { } try { - URL url = new URI(BASE_SCHEME, baseHost, - String.format(Locale.US, "/suggestions/%d.json", count), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format(Locale.US, "/suggestions/%d.json", count), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -187,7 +226,7 @@ public class GpodnetService implements ISyncService { .format(Locale.US, "q=%s&scale_logo=%d", query, scaledLogoSize) : String .format("q=%s", query); try { - URL url = new URI(BASE_SCHEME, null, baseHost, -1, "/search.json", + URL url = new URI(baseScheme, null, baseHost, basePort, "/search.json", parameters, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -214,7 +253,8 @@ public class GpodnetService implements ISyncService { public List<GpodnetDevice> getDevices() throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/devices/%s.json", username), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format("/api/2/devices/%s.json", username), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); JSONArray devicesArray = new JSONArray(response); @@ -226,6 +266,45 @@ public class GpodnetService implements ISyncService { } /** + * Returns synchronization status of devices. + * <p/> + * This method requires authentication. + * + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public List<List<String>> getSynchronizedDevices() throws GpodnetServiceException { + requireLoggedIn(); + try { + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format("/api/2/sync-devices/%s.json", username), null, null).toURL(); + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + JSONObject syncStatus = new JSONObject(response); + List<List<String>> result = new ArrayList<>(); + + JSONArray synchronizedDevices = syncStatus.getJSONArray("synchronized"); + for (int i = 0; i < synchronizedDevices.length(); i++) { + JSONArray groupDevices = synchronizedDevices.getJSONArray(i); + List<String> group = new ArrayList<>(); + for (int j = 0; j < groupDevices.length(); j++) { + group.add(groupDevices.getString(j)); + } + result.add(group); + } + + JSONArray notSynchronizedDevices = syncStatus.getJSONArray("not-synchronized"); + for (int i = 0; i < notSynchronizedDevices.length(); i++) { + result.add(Collections.singletonList(notSynchronizedDevices.getString(i))); + } + + return result; + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** * Configures the device of a given user. * <p/> * This method requires authentication. @@ -237,8 +316,8 @@ public class GpodnetService implements ISyncService { throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/api/2/devices/%s/%s.json", username, deviceId), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format("/api/2/devices/%s/%s.json", username, deviceId), null, null).toURL(); String content; if (caption != null || type != null) { JSONObject jsonContent = new JSONObject(); @@ -262,6 +341,39 @@ public class GpodnetService implements ISyncService { } /** + * Links devices for synchronization. + * <p/> + * This method requires authentication. + * + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public void linkDevices(@NonNull List<String> deviceIds) throws GpodnetServiceException { + requireLoggedIn(); + try { + final URL url = new URI(baseScheme, null, baseHost, basePort, + String.format("/api/2/sync-devices/%s.json", username), null, null).toURL(); + JSONObject jsonContent = new JSONObject(); + JSONArray group = new JSONArray(); + for (String deviceId : deviceIds) { + group.put(deviceId); + } + + JSONArray synchronizedGroups = new JSONArray(); + synchronizedGroups.put(group); + jsonContent.put("synchronize", synchronizedGroups); + jsonContent.put("stop-synchronize", new JSONArray()); + + Log.d("aaaa", jsonContent.toString()); + RequestBody body = RequestBody.create(JSON, jsonContent.toString()); + Request.Builder request = new Request.Builder().post(body).url(url); + executeRequest(request); + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** * Returns the subscriptions of a specific device. * <p/> * This method requires authentication. @@ -273,8 +385,8 @@ public class GpodnetService implements ISyncService { public String getSubscriptionsOfDevice(@NonNull String deviceId) throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/subscriptions/%s/%s.opml", username, deviceId), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format("/subscriptions/%s/%s.opml", username, deviceId), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); return executeRequest(request); } catch (MalformedURLException | URISyntaxException e) { @@ -295,7 +407,8 @@ public class GpodnetService implements ISyncService { public String getSubscriptionsOfUser() throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format("/subscriptions/%s.opml", username), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format("/subscriptions/%s.opml", username), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); return executeRequest(request); } catch (MalformedURLException | URISyntaxException e) { @@ -319,8 +432,8 @@ public class GpodnetService implements ISyncService { throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/subscriptions/%s/%s.txt", username, deviceId), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format("/subscriptions/%s/%s.txt", username, deviceId), null, null).toURL(); StringBuilder builder = new StringBuilder(); for (String s : subscriptions) { builder.append(s); @@ -353,8 +466,8 @@ public class GpodnetService implements ISyncService { @NonNull Collection<String> removed) throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/api/2/subscriptions/%s/%s.json", username, deviceId), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format("/api/2/subscriptions/%s/%s.json", username, deviceId), null, null).toURL(); final JSONObject requestObject = new JSONObject(); requestObject.put("add", new JSONArray(added)); @@ -389,8 +502,7 @@ public class GpodnetService implements ISyncService { String params = String.format(Locale.US, "since=%d", timestamp); String path = String.format("/api/2/subscriptions/%s/%s.json", username, deviceId); try { - URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params, - null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, path, params, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -432,8 +544,8 @@ public class GpodnetService implements ISyncService { throws SyncServiceException { try { Log.d(TAG, "Uploading partial actions " + from + " to " + to + " of " + episodeActions.size()); - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/api/2/episodes/%s.json", username), null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, + String.format("/api/2/episodes/%s.json", username), null, null).toURL(); final JSONArray list = new JSONArray(); for (int i = from; i < to; i++) { @@ -471,7 +583,7 @@ public class GpodnetService implements ISyncService { String params = String.format(Locale.US, "since=%d", timestamp); String path = String.format("/api/2/episodes/%s.json", username); try { - URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params, null).toURL(); + URL url = new URI(baseScheme, null, baseHost, basePort, path, params, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -497,7 +609,8 @@ public class GpodnetService implements ISyncService { public void authenticate(@NonNull String username, @NonNull String password) throws GpodnetServiceException { URL url; try { - url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/auth/%s/login.json", username), null).toURL(); + url = new URI(baseScheme, null, baseHost, basePort, + String.format("/api/2/auth/%s/login.json", username), null, null).toURL(); } catch (MalformedURLException | URISyntaxException e) { e.printStackTrace(); throw new GpodnetServiceException(e); @@ -567,6 +680,13 @@ public class GpodnetService implements ISyncService { if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { throw new GpodnetServiceAuthenticationException("Wrong username or password"); } else { + if (BuildConfig.DEBUG) { + try { + Log.d(TAG, response.body().string()); + } catch (IOException e) { + e.printStackTrace(); + } + } throw new GpodnetServiceBadStatusCodeException("Bad response code: " + responseCode, responseCode); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java index 11588967a..c9f9f19c8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java @@ -36,9 +36,6 @@ public class UnsupportedFeedtypeException extends Exception { if (message != null) { return message; } else if (type == TypeGetter.Type.INVALID) { - if ("html".equals(rootElement)) { - return "The server returned a website, not a podcast feed"; - } return "Invalid type"; } else { return "Type " + type + " not supported"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java index 306b79c15..bedf377aa 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java @@ -5,23 +5,21 @@ import org.xml.sax.Attributes; import de.danoeh.antennapod.core.syndication.handler.HandlerState; public class NSContent extends Namespace { - public static final String NSTAG = "content"; - public static final String NSURI = "http://purl.org/rss/1.0/modules/content/"; - - private static final String ENCODED = "encoded"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - return new SyndElement(localName, this); - } + public static final String NSTAG = "content"; + public static final String NSURI = "http://purl.org/rss/1.0/modules/content/"; - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (ENCODED.equals(localName) && state.getCurrentItem() != null && - state.getContentBuf() != null) { - state.getCurrentItem().setContentEncoded(state.getContentBuf().toString()); - } - } + private static final String ENCODED = "encoded"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes) { + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (ENCODED.equals(localName) && state.getCurrentItem() != null && state.getContentBuf() != null) { + state.getCurrentItem().setDescriptionIfLonger(state.getContentBuf().toString()); + } + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java index 1e069a1f0..1dc8d8af3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java @@ -3,9 +3,10 @@ package de.danoeh.antennapod.core.syndication.namespace; import android.text.TextUtils; import android.util.Log; +import androidx.core.text.HtmlCompat; + import org.xml.sax.Attributes; -import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.syndication.handler.HandlerState; import de.danoeh.antennapod.core.syndication.parsers.DurationParser; @@ -62,7 +63,8 @@ public class NSITunes extends Namespace { private void parseAuthor(HandlerState state) { if (state.getFeed() != null) { String author = state.getContentBuf().toString(); - state.getFeed().setAuthor(author); + state.getFeed().setAuthor(HtmlCompat.fromHtml(author, + HtmlCompat.FROM_HTML_MODE_LEGACY).toString()); } } @@ -87,7 +89,7 @@ public class NSITunes extends Namespace { } if (state.getCurrentItem() != null) { if (TextUtils.isEmpty(state.getCurrentItem().getDescription())) { - state.getCurrentItem().setDescription(subtitle); + state.getCurrentItem().setDescriptionIfLonger(subtitle); } } else { if (state.getFeed() != null && TextUtils.isEmpty(state.getFeed().getDescription())) { @@ -102,16 +104,10 @@ public class NSITunes extends Namespace { return; } - FeedItem currentItem = state.getCurrentItem(); - String description = getDescription(currentItem); - if (currentItem != null && description.length() * 1.25 < summary.length()) { - currentItem.setDescription(summary); + if (state.getCurrentItem() != null) { + state.getCurrentItem().setDescriptionIfLonger(summary); } else if (NSRSS20.CHANNEL.equals(secondElementName) && state.getFeed() != null) { state.getFeed().setDescription(summary); } } - - private String getDescription(FeedItem item) { - return (item != null && item.getDescription() != null) ? item.getDescription() : ""; - } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java index 30b01f0bc..b5d5a1b3f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java @@ -121,9 +121,8 @@ public class NSMedia extends Namespace { public void handleElementEnd(String localName, HandlerState state) { if (DESCRIPTION.equals(localName)) { String content = state.getContentBuf().toString(); - if (state.getCurrentItem() != null && content != null - && state.getCurrentItem().getDescription() == null) { - state.getCurrentItem().setDescription(content); + if (state.getCurrentItem() != null) { + state.getCurrentItem().setDescriptionIfLonger(content); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java index 45c5d4884..b1cd6d1c2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java @@ -13,10 +13,7 @@ import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; import de.danoeh.antennapod.core.util.DateUtils; /** - * SAX-Parser for reading RSS-Feeds - * - * @author daniel - * + * SAX-Parser for reading RSS-Feeds. */ public class NSRSS20 extends Namespace { @@ -83,8 +80,7 @@ public class NSRSS20 extends Namespace { if (state.getCurrentItem() != null) { FeedItem currentItem = state.getCurrentItem(); // the title tag is optional in RSS 2.0. The description is used - // as a - // title if the item has no title-tag. + // as a title if the item has no title-tag. if (currentItem.getTitle() == null) { currentItem.setTitle(currentItem.getDescription()); } @@ -138,7 +134,7 @@ public class NSRSS20 extends Namespace { if (CHANNEL.equals(second) && state.getFeed() != null) { state.getFeed().setDescription(content); } else if (ITEM.equals(second) && state.getCurrentItem() != null) { - state.getCurrentItem().setDescription(content); + state.getCurrentItem().setDescriptionIfLonger(content); } } else if (LANGUAGE.equals(localName) && state.getFeed() != null) { state.getFeed().setLanguage(content.toLowerCase()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java index 7e4350fd4..42f787d98 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java @@ -198,10 +198,10 @@ public class NSAtom extends Namespace { state.getFeed().setDescription(textElement.getProcessedContent()); } else if (CONTENT.equals(top) && ENTRY.equals(second) && textElement != null && state.getCurrentItem() != null) { - state.getCurrentItem().setDescription(textElement.getProcessedContent()); - } else if (SUMMARY.equals(top) && ENTRY.equals(second) && textElement != null && - state.getCurrentItem() != null && state.getCurrentItem().getDescription() == null) { - state.getCurrentItem().setDescription(textElement.getProcessedContent()); + state.getCurrentItem().setDescriptionIfLonger(textElement.getProcessedContent()); + } else if (SUMMARY.equals(top) && ENTRY.equals(second) && textElement != null + && state.getCurrentItem() != null) { + state.getCurrentItem().setDescriptionIfLonger(textElement.getProcessedContent()); } else if (UPDATED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null && state.getCurrentItem().getPubDate() == null) { state.getCurrentItem().setPubDate(DateUtils.parse(content)); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java index d4a2cdca6..ca9689048 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java @@ -3,32 +3,30 @@ package de.danoeh.antennapod.core.util; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; -import androidx.annotation.NonNull; import android.util.Log; - -import java.net.URLConnection; -import de.danoeh.antennapod.core.ClientConfig; -import org.apache.commons.io.IOUtils; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.Collections; -import java.util.List; - +import androidx.annotation.NonNull; import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.ChapterMerger; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator; import de.danoeh.antennapod.core.util.id3reader.ChapterReader; import de.danoeh.antennapod.core.util.id3reader.ID3ReaderException; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentChapterReader; import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException; +import okhttp3.Request; +import okhttp3.Response; import org.apache.commons.io.input.CountingInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; + /** * Utility class for getting chapter data from media files. */ @@ -52,101 +50,84 @@ public class ChapterUtils { return chapters.size() - 1; } - public static List<Chapter> loadChaptersFromStreamUrl(Playable media, Context context) { - List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media, context); - if (chapters == null) { - chapters = ChapterUtils.readOggChaptersFromPlayableStreamUrl(media, context); + public static void loadChapters(Playable playable, Context context) { + if (playable.getChapters() != null) { + // Already loaded + return; } - return chapters; - } - public static List<Chapter> loadChaptersFromFileUrl(Playable media) { - if (!media.localFileAvailable()) { - Log.e(TAG, "Could not load chapters from file url: local file not available"); - return null; + List<Chapter> chaptersFromDatabase = null; + if (playable instanceof FeedMedia) { + FeedMedia feedMedia = (FeedMedia) playable; + if (feedMedia.getItem() == null) { + feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId())); + } + if (feedMedia.getItem().hasChapters()) { + chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem()); + } } - List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableFileUrl(media); + + List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context); + List<Chapter> chapters = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile); if (chapters == null) { - chapters = ChapterUtils.readOggChaptersFromPlayableFileUrl(media); + // Do not try loading again. There are no chapters. + playable.setChapters(Collections.emptyList()); + } else { + playable.setChapters(chapters); } - return chapters; } - /** - * Uses the download URL of a media object of a feeditem to read its ID3 - * chapters. - */ - private static List<Chapter> readID3ChaptersFromPlayableStreamUrl(Playable p, Context context) { - if (p == null || p.getStreamUrl() == null) { - Log.e(TAG, "Unable to read ID3 chapters: media or download URL was null"); - return null; - } - Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); - CountingInputStream in = null; - try { - if (p.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) { - Uri uri = Uri.parse(p.getStreamUrl()); - in = new CountingInputStream(context.getContentResolver().openInputStream(uri)); - } else { - URL url = new URL(p.getStreamUrl()); - URLConnection urlConnection = url.openConnection(); - urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT); - in = new CountingInputStream(urlConnection.getInputStream()); - } - List<Chapter> chapters = readChaptersFrom(in); + public static List<Chapter> loadChaptersFromMediaFile(Playable playable, Context context) { + try (CountingInputStream in = openStream(playable, context)) { + List<Chapter> chapters = readId3ChaptersFrom(in); if (!chapters.isEmpty()) { + Log.i(TAG, "Chapters loaded"); return chapters; } - Log.i(TAG, "Chapters loaded"); - } catch (IOException | ID3ReaderException | IllegalArgumentException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } finally { - IOUtils.closeQuietly(in); - } - return null; - } - - /** - * Uses the file URL of a media object of a feeditem to read its ID3 - * chapters. - */ - private static List<Chapter> readID3ChaptersFromPlayableFileUrl(Playable p) { - if (p == null || !p.localFileAvailable() || p.getLocalMediaUrl() == null) { - return null; - } - Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); - File source = new File(p.getLocalMediaUrl()); - if (!source.exists()) { - Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist"); - return null; + } catch (IOException | ID3ReaderException e) { + Log.e(TAG, "Unable to load ID3 chapters: " + e.getMessage()); } - CountingInputStream in = null; - try { - in = new CountingInputStream(new BufferedInputStream(new FileInputStream(source))); - List<Chapter> chapters = readChaptersFrom(in); + try (CountingInputStream in = openStream(playable, context)) { + List<Chapter> chapters = readOggChaptersFromInputStream(in); if (!chapters.isEmpty()) { + Log.i(TAG, "Chapters loaded"); return chapters; } - Log.i(TAG, "Chapters loaded"); - } catch (IOException | ID3ReaderException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } finally { - IOUtils.closeQuietly(in); + } catch (IOException | VorbisCommentReaderException e) { + Log.e(TAG, "Unable to load vorbis chapters: " + e.getMessage()); } return null; } + private static CountingInputStream openStream(Playable playable, Context context) throws IOException { + if (playable.localFileAvailable()) { + if (playable.getLocalMediaUrl() == null) { + throw new IOException("No local url"); + } + File source = new File(playable.getLocalMediaUrl()); + if (!source.exists()) { + throw new IOException("Local file does not exist"); + } + return new CountingInputStream(new FileInputStream(source)); + } else if (playable.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) { + Uri uri = Uri.parse(playable.getStreamUrl()); + return new CountingInputStream(context.getContentResolver().openInputStream(uri)); + } else { + Request request = new Request.Builder().url(playable.getStreamUrl()).build(); + Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute(); + if (response.body() == null) { + throw new IOException("Body is null"); + } + return new CountingInputStream(response.body().byteStream()); + } + } + @NonNull - private static List<Chapter> readChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException { - ChapterReader reader = new ChapterReader(); - reader.readInputStream(in); + private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException { + ChapterReader reader = new ChapterReader(in); + reader.readInputStream(); List<Chapter> chapters = reader.getChapters(); - - if (chapters == null) { - Log.i(TAG, "ChapterReader could not find any ID3 chapters"); - return Collections.emptyList(); - } Collections.sort(chapters, new ChapterStartTimeComparator()); enumerateEmptyChapterTitles(chapters); if (!chaptersValid(chapters)) { @@ -156,73 +137,20 @@ public class ChapterUtils { return chapters; } - private static List<Chapter> readOggChaptersFromPlayableStreamUrl(Playable media, Context context) { - if (media == null || !media.streamAvailable()) { - return null; - } - InputStream input = null; - try { - if (media.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) { - Uri uri = Uri.parse(media.getStreamUrl()); - input = context.getContentResolver().openInputStream(uri); - } else { - URL url = new URL(media.getStreamUrl()); - URLConnection urlConnection = url.openConnection(); - urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT); - input = urlConnection.getInputStream(); - } - if (input != null) { - return readOggChaptersFromInputStream(media, input); - } - } catch (IOException | IllegalArgumentException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } finally { - IOUtils.closeQuietly(input); - } - return null; - } - - private static List<Chapter> readOggChaptersFromPlayableFileUrl(Playable media) { - if (media == null || media.getLocalMediaUrl() == null) { - return null; - } - File source = new File(media.getLocalMediaUrl()); - if (source.exists()) { - InputStream input = null; - try { - input = new BufferedInputStream(new FileInputStream(source)); - return readOggChaptersFromInputStream(media, input); - } catch (FileNotFoundException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } finally { - IOUtils.closeQuietly(input); - } + @NonNull + private static List<Chapter> readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException { + VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); + reader.readInputStream(input); + List<Chapter> chapters = reader.getChapters(); + if (chapters == null) { + return Collections.emptyList(); } - return null; - } - - private static List<Chapter> readOggChaptersFromInputStream(Playable p, InputStream input) { - Log.d(TAG, "Trying to read chapters from item with title " + p.getEpisodeTitle()); - try { - VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); - reader.readInputStream(input); - List<Chapter> chapters = reader.getChapters(); - if (chapters == null) { - Log.i(TAG, "ChapterReader could not find any Ogg vorbis chapters"); - return null; - } - Collections.sort(chapters, new ChapterStartTimeComparator()); - enumerateEmptyChapterTitles(chapters); - if (chaptersValid(chapters)) { - Log.i(TAG, "Chapters loaded"); - return chapters; - } else { - Log.e(TAG, "Chapter data was invalid"); - } - } catch (VorbisCommentReaderException e) { - e.printStackTrace(); + Collections.sort(chapters, new ChapterStartTimeComparator()); + enumerateEmptyChapterTitles(chapters); + if (chaptersValid(chapters)) { + return chapters; } - return null; + return Collections.emptyList(); } /** @@ -248,5 +176,4 @@ public class ChapterUtils { } return true; } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java index 833ff33f1..196583bcd 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java @@ -30,9 +30,12 @@ public class DateUtils { } String date = input.trim().replace('/', '-').replaceAll("( ){2,}+", " "); + // remove colon from timezone to avoid differences between Android and Java SimpleDateFormat + date = date.replaceAll("([+-]\\d\\d):(\\d\\d)$", "$1$2"); + // CEST is widely used but not in the "ISO 8601 Time zone" list. Let's hack around. - date = date.replaceAll("CEST$", "+02:00"); - date = date.replaceAll("CET$", "+01:00"); + date = date.replaceAll("CEST$", "+0200"); + date = date.replaceAll("CET$", "+0100"); // some generators use "Sept" for September date = date.replaceAll("\\bSept\\b", "Sep"); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java index 0c9989b43..9c4a61cd8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java @@ -6,51 +6,54 @@ import de.danoeh.antennapod.core.R; /** Utility class for Download Errors. */ public enum DownloadError { - SUCCESS(0, R.string.download_successful), - ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception), - ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type), - ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error), - ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown), - ERROR_IO_ERROR(5, R.string.download_error_io_error), - ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown), - ERROR_DOWNLOAD_CANCELLED(7, R.string.download_error_error_unknown), - ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found), - ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error), - ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space), - ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host), - ERROR_REQUEST_ERROR(12, R.string.download_error_request_error), + SUCCESS(0, R.string.download_successful), + ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception), + ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type), + ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error), + ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown), + ERROR_IO_ERROR(5, R.string.download_error_io_error), + ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown), + ERROR_DOWNLOAD_CANCELLED(7, R.string.download_canceled_msg), + ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found), + ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error), + ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space), + ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host), + ERROR_REQUEST_ERROR(12, R.string.download_error_request_error), ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access), ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized), - ERROR_FILE_TYPE(15, R.string.download_error_file_type_type), - ERROR_FORBIDDEN(16, R.string.download_error_forbidden); - - - private final int code; - private final int resId; - - DownloadError(int code, int resId) { - this.code = code; - this.resId = resId; - } - - /** Return DownloadError from its associated code. */ - public static DownloadError fromCode(int code) { - for (DownloadError reason : values()) { - if (reason.getCode() == code) { - return reason; - } - } - throw new IllegalArgumentException("unknown code: " + code); - } - - /** Get machine-readable code. */ - public int getCode() { - return code; - } - - /** Get a human-readable string. */ - public String getErrorString(Context context) { - return context.getString(resId); - } - + ERROR_FILE_TYPE(15, R.string.download_error_file_type_type), + ERROR_FORBIDDEN(16, R.string.download_error_forbidden), + ERROR_IO_WRONG_SIZE(17, R.string.download_error_wrong_size), + ERROR_IO_BLOCKED(18, R.string.download_error_blocked), + ERROR_UNSUPPORTED_TYPE_HTML(19, R.string.download_error_unsupported_type_html), + ERROR_NOT_FOUND(20, R.string.download_error_not_found), + ERROR_CERTIFICATE(21, R.string.download_error_certificate); + + private final int code; + private final int resId; + + DownloadError(int code, int resId) { + this.code = code; + this.resId = resId; + } + + /** Return DownloadError from its associated code. */ + public static DownloadError fromCode(int code) { + for (DownloadError reason : values()) { + if (reason.getCode() == code) { + return reason; + } + } + throw new IllegalArgumentException("unknown code: " + code); + } + + /** Get machine-readable code. */ + public int getCode() { + return code; + } + + /** Get a human-readable string. */ + public String getErrorString(Context context) { + return context.getString(resId); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java index 2a387b7b0..69c23efc2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java @@ -13,7 +13,7 @@ import java.security.NoSuchAlgorithmException; /** Generates valid filenames for a given string. */ public class FileNameGenerator { @VisibleForTesting - public static final int MAX_FILENAME_LENGTH = 255; // Limited by ext4 + public static final int MAX_FILENAME_LENGTH = 242; // limited by CircleCI private static final int MD5_HEX_LENGTH = 32; private static final char[] validChars = diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java b/core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java deleted file mode 100644 index 5feb232e7..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import de.danoeh.antennapod.core.BuildConfig; - -/** - * Helper class to handle the different build flavors. - */ -public enum Flavors { - FREE, - PLAY, - UNKNOWN; - - public static final Flavors FLAVOR; - - static { - if (BuildConfig.FLAVOR.equals("free")) { - FLAVOR = FREE; - } else if (BuildConfig.FLAVOR.equals("play")) { - FLAVOR = PLAY; - } else { - FLAVOR = UNKNOWN; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java b/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java deleted file mode 100644 index 37f12c01c..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package de.danoeh.antennapod.core.util; - -import java.util.NoSuchElementException; -import java.util.Objects; - -// AntennaPod's stripped-down version of Java/Android platform's java.util.Optional -// so that it can be used on lower API level (API level 14) - -// Android-changed: removed ValueBased paragraph. -/** - * A container object which may or may not contain a non-null value. - * If a value is present, {@code isPresent()} will return {@code true} and - * {@code get()} will return the value. - * - * <p>Additional methods that depend on the presence or absence of a contained - * value are provided, such as {@link #orElse(java.lang.Object) orElse()} - * (return a default value if value not present) and - * {@link #ifPresent(java.util.function.Consumer) ifPresent()} (execute a block - * of code if the value is present). - * - * @since 1.8 - */ -public final class Optional<T> { - /** - * Common instance for {@code empty()}. - */ - private static final Optional<?> EMPTY = new Optional<>(); - - /** - * If non-null, the value; if null, indicates no value is present - */ - private final T value; - - /** - * Constructs an empty instance. - * - * @implNote Generally only one empty instance, {@link Optional#EMPTY}, - * should exist per VM. - */ - private Optional() { - this.value = null; - } - - /** - * Returns an empty {@code Optional} instance. No value is present for this - * Optional. - * - * @apiNote Though it may be tempting to do so, avoid testing if an object - * is empty by comparing with {@code ==} against instances returned by - * {@code Option.empty()}. There is no guarantee that it is a singleton. - * Instead, use {@link #isPresent()}. - * - * @param <T> Type of the non-existent value - * @return an empty {@code Optional} - */ - public static <T> Optional<T> empty() { - @SuppressWarnings("unchecked") - Optional<T> t = (Optional<T>) EMPTY; - return t; - } - - /** - * Constructs an instance with the value present. - * - * @param value the non-null value to be present - * @throws NullPointerException if value is null - */ - private Optional(T value) { - this.value = Objects.requireNonNull(value); - } - - /** - * Returns an {@code Optional} with the specified present non-null value. - * - * @param <T> the class of the value - * @param value the value to be present, which must be non-null - * @return an {@code Optional} with the value present - * @throws NullPointerException if value is null - */ - public static <T> Optional<T> of(T value) { - return new Optional<>(value); - } - - /** - * Returns an {@code Optional} describing the specified value, if non-null, - * otherwise returns an empty {@code Optional}. - * - * @param <T> the class of the value - * @param value the possibly-null value to describe - * @return an {@code Optional} with a present value if the specified value - * is non-null, otherwise an empty {@code Optional} - */ - public static <T> Optional<T> ofNullable(T value) { - return value == null ? empty() : of(value); - } - - /** - * If a value is present in this {@code Optional}, returns the value, - * otherwise throws {@code NoSuchElementException}. - * - * @return the non-null value held by this {@code Optional} - * @throws NoSuchElementException if there is no value present - * - * @see Optional#isPresent() - */ - public T get() { - if (value == null) { - throw new NoSuchElementException("No value present"); - } - return value; - } - - /** - * Return {@code true} if there is a value present, otherwise {@code false}. - * - * @return {@code true} if there is a value present, otherwise {@code false} - */ - public boolean isPresent() { - return value != null; - } - - - /** - * Return the value if present, otherwise return {@code other}. - * - * @param other the value to be returned if there is no value present, may - * be null - * @return the value, if present, otherwise {@code other} - */ - public T orElse(T other) { - return value != null ? value : other; - } - - /** - * Indicates whether some other object is "equal to" this Optional. The - * other object is considered equal if: - * <ul> - * <li>it is also an {@code Optional} and; - * <li>both instances have no value present or; - * <li>the present values are "equal to" each other via {@code equals()}. - * </ul> - * - * @param obj an object to be tested for equality - * @return {code true} if the other object is "equal to" this object - * otherwise {@code false} - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - - if (!(obj instanceof Optional)) { - return false; - } - - Optional<?> other = (Optional<?>) obj; - return (value == other.value) || (value != null && value.equals(other.value)); - } - - /** - * Returns the hash code value of the present value, if any, or 0 (zero) if - * no value is present. - * - * @return hash code value of the present value or 0 if no value is present - */ - @Override - public int hashCode() { - return value != null ? value.hashCode() : 0; - } - - /** - * Returns a non-empty string representation of this Optional suitable for - * debugging. The exact presentation format is unspecified and may vary - * between implementations and versions. - * - * @implSpec If a value is present the result must include its string - * representation in the result. Empty and present Optionals must be - * unambiguously differentiable. - * - * @return the string representation of this instance - */ - @Override - public String toString() { - return value != null - ? String.format("Optional[%s]", value) - : "Optional.empty"; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java deleted file mode 100644 index a4cd83f70..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import java.util.concurrent.Callable; - -/** - * Created by daniel on 04.08.13. - */ -public interface ShownotesProvider { - /** - * Loads shownotes. If the shownotes have to be loaded from a file or from a - * database, it should be done in a separate thread. After the shownotes - * have been loaded, callback.onShownotesLoaded should be called. - */ - Callable<String> loadShownotes(); - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java index 3101eac34..5895c5933 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java @@ -18,6 +18,7 @@ public class NotificationUtils { public static final String CHANNEL_ID_DOWNLOAD_ERROR = "error"; public static final String CHANNEL_ID_SYNC_ERROR = "sync_error"; public static final String CHANNEL_ID_AUTO_DOWNLOAD = "auto_download"; + public static final String CHANNEL_ID_EPISODE_NOTIFICATIONS = "episode_notifications"; public static final String GROUP_ID_ERRORS = "group_errors"; public static final String GROUP_ID_NEWS = "group_news"; @@ -38,6 +39,7 @@ public class NotificationUtils { mNotificationManager.createNotificationChannel(createChannelError(context)); mNotificationManager.createNotificationChannel(createChannelSyncError(context)); mNotificationManager.createNotificationChannel(createChannelAutoDownload(context)); + mNotificationManager.createNotificationChannel(createChannelEpisodeNotification(context)); } } @@ -111,6 +113,15 @@ public class NotificationUtils { } @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannel createChannelEpisodeNotification(Context c) { + NotificationChannel channel = new NotificationChannel(CHANNEL_ID_EPISODE_NOTIFICATIONS, + c.getString(R.string.notification_channel_new_episode), NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(c.getString(R.string.notification_channel_new_episode_description)); + channel.setGroup(GROUP_ID_NEWS); + return channel; + } + + @RequiresApi(api = Build.VERSION_CODES.O) private static NotificationChannelGroup createGroupErrors(Context c) { return new NotificationChannelGroup(GROUP_ID_ERRORS, c.getString(R.string.notification_group_errors)); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java index ce3577a9e..69d8316c2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java @@ -2,149 +2,114 @@ package de.danoeh.antennapod.core.util.id3reader; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.ID3Chapter; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; -import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; +import org.apache.commons.io.input.CountingInputStream; import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; -import org.apache.commons.io.input.CountingInputStream; +/** + * Reads ID3 chapters. + * See https://id3.org/id3v2-chapters-1.0 + */ public class ChapterReader extends ID3Reader { private static final String TAG = "ID3ChapterReader"; - private static final String FRAME_ID_CHAPTER = "CHAP"; - private static final String FRAME_ID_TITLE = "TIT2"; - private static final String FRAME_ID_LINK = "WXXX"; - private static final String FRAME_ID_PICTURE = "APIC"; - private static final int IMAGE_TYPE_COVER = 3; + public static final String FRAME_ID_CHAPTER = "CHAP"; + public static final String FRAME_ID_TITLE = "TIT2"; + public static final String FRAME_ID_LINK = "WXXX"; + public static final String FRAME_ID_PICTURE = "APIC"; + public static final String MIME_IMAGE_URL = "-->"; + public static final int IMAGE_TYPE_COVER = 3; - private List<Chapter> chapters; - private ID3Chapter currentChapter; + private final List<Chapter> chapters = new ArrayList<>(); - @Override - public int onStartTagHeader(TagHeader header) { - chapters = new ArrayList<>(); - Log.d(TAG, "header: " + header); - return ID3Reader.ACTION_DONT_SKIP; + public ChapterReader(CountingInputStream input) { + super(input); } @Override - public int onStartFrameHeader(FrameHeader header, CountingInputStream input) throws IOException, ID3ReaderException { - Log.d(TAG, "header: " + header); - switch (header.getId()) { - case FRAME_ID_CHAPTER: - if (currentChapter != null) { - if (!hasId3Chapter(currentChapter)) { - chapters.add(currentChapter); - Log.d(TAG, "Found chapter: " + currentChapter); - currentChapter = null; - } - } - StringBuilder elementId = new StringBuilder(); - readISOString(elementId, input, Integer.MAX_VALUE); - char[] startTimeSource = readChars(input, 4); - long startTime = ((int) startTimeSource[0] << 24) - | ((int) startTimeSource[1] << 16) - | ((int) startTimeSource[2] << 8) | startTimeSource[3]; - currentChapter = new ID3Chapter(elementId.toString(), startTime); - skipBytes(input, 12); - return ID3Reader.ACTION_DONT_SKIP; - case FRAME_ID_TITLE: - if (currentChapter != null && currentChapter.getTitle() == null) { - StringBuilder title = new StringBuilder(); - readString(title, input, header.getSize()); - currentChapter - .setTitle(title.toString()); - Log.d(TAG, "Found title: " + currentChapter.getTitle()); + protected void readFrame(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException { + if (FRAME_ID_CHAPTER.equals(frameHeader.getId())) { + Log.d(TAG, "Handling frame: " + frameHeader.toString()); + Chapter chapter = readChapter(frameHeader); + Log.d(TAG, "Chapter done: " + chapter); + chapters.add(chapter); + } else { + super.readFrame(frameHeader); + } + } - return ID3Reader.ACTION_DONT_SKIP; - } + public Chapter readChapter(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException { + int chapterStartedPosition = getPosition(); + String elementId = readIsoStringNullTerminated(100); + long startTime = readInt(); + skipBytes(12); // Ignore end time, start offset, end offset + ID3Chapter chapter = new ID3Chapter(elementId, startTime); + + // Read sub-frames + while (getPosition() < chapterStartedPosition + frameHeader.getSize()) { + FrameHeader subFrameHeader = readFrameHeader(); + readChapterSubFrame(subFrameHeader, chapter); + } + return chapter; + } + + public void readChapterSubFrame(@NonNull FrameHeader frameHeader, @NonNull Chapter chapter) + throws IOException, ID3ReaderException { + Log.d(TAG, "Handling subframe: " + frameHeader.toString()); + int frameStartPosition = getPosition(); + switch (frameHeader.getId()) { + case FRAME_ID_TITLE: + chapter.setTitle(readEncodingAndString(frameHeader.getSize())); + Log.d(TAG, "Found title: " + chapter.getTitle()); break; case FRAME_ID_LINK: - if (currentChapter != null) { - // skip description - int descriptionLength = readString(null, input, header.getSize()); - StringBuilder link = new StringBuilder(); - readISOString(link, input, header.getSize() - descriptionLength); - try { - String decodedLink = URLDecoder.decode(link.toString(), "UTF-8"); - currentChapter.setLink(decodedLink); - Log.d(TAG, "Found link: " + currentChapter.getLink()); - } catch (IllegalArgumentException iae) { - Log.w(TAG, "Bad URL found in ID3 data"); - } - - return ID3Reader.ACTION_DONT_SKIP; + readEncodingAndString(frameHeader.getSize()); // skip description + String url = readIsoStringNullTerminated(frameStartPosition + frameHeader.getSize() - getPosition()); + try { + String decodedLink = URLDecoder.decode(url, "ISO-8859-1"); + chapter.setLink(decodedLink); + Log.d(TAG, "Found link: " + chapter.getLink()); + } catch (IllegalArgumentException iae) { + Log.w(TAG, "Bad URL found in ID3 data"); } break; case FRAME_ID_PICTURE: - if (currentChapter != null) { - Log.d(TAG, header.toString()); - StringBuilder mime = new StringBuilder(); - int read = readString(mime, input, header.getSize()); - byte type = (byte) readChars(input, 1)[0]; - read++; - StringBuilder description = new StringBuilder(); - read += readISOString(description, input, header.getSize()); // Should use same encoding as mime - - Log.d(TAG, "Found apic: " + mime + "," + description); - if (mime.toString().equals("-->")) { - // Data contains a link to a picture - StringBuilder link = new StringBuilder(); - readISOString(link, input, header.getSize()); - Log.d(TAG, "link: " + link.toString()); - if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) { - currentChapter.setImageUrl(link.toString()); - } - } else { - // Data contains the picture - int length = header.getSize() - read; - if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) { - currentChapter.setImageUrl(EmbeddedChapterImage.makeUrl(input.getCount(), length)); - } - skipBytes(input, length); + byte encoding = readByte(); + String mime = readEncodedString(encoding, frameHeader.getSize()); + byte type = readByte(); + String description = readEncodedString(encoding, frameHeader.getSize()); + Log.d(TAG, "Found apic: " + mime + "," + description); + if (MIME_IMAGE_URL.equals(mime)) { + String link = readIsoStringNullTerminated(frameHeader.getSize()); + Log.d(TAG, "Link: " + link); + if (TextUtils.isEmpty(chapter.getImageUrl()) || type == IMAGE_TYPE_COVER) { + chapter.setImageUrl(link); + } + } else { + int alreadyConsumed = getPosition() - frameStartPosition; + int rawImageDataLength = frameHeader.getSize() - alreadyConsumed; + if (TextUtils.isEmpty(chapter.getImageUrl()) || type == IMAGE_TYPE_COVER) { + chapter.setImageUrl(EmbeddedChapterImage.makeUrl(getPosition(), rawImageDataLength)); } - return ID3Reader.ACTION_DONT_SKIP; } break; + default: + Log.d(TAG, "Unknown chapter sub-frame."); + break; } - return super.onStartFrameHeader(header, input); - } - - private boolean hasId3Chapter(ID3Chapter chapter) { - for (Chapter c : chapters) { - if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) { - return true; - } - } - return false; - } - - @Override - public void onEndTag() { - if (currentChapter != null) { - if (!hasId3Chapter(currentChapter)) { - chapters.add(currentChapter); - } - } - Log.d(TAG, "Reached end of tag"); - if (chapters != null) { - for (Chapter c : chapters) { - Log.d(TAG, "chapter: " + c); - } - } - } - - @Override - public void onNoTagHeaderFound() { - Log.d(TAG, "No tag header found"); - super.onNoTagHeaderFound(); + // Skip garbage to fill frame completely + // This also asserts that we are not reading too many bytes from this frame. + int alreadyConsumed = getPosition() - frameStartPosition; + skipBytes(frameHeader.getSize() - alreadyConsumed); } public List<Chapter> getChapters() { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java index 124388254..17313ca14 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java @@ -1,151 +1,112 @@ package de.danoeh.antennapod.core.util.id3reader; +import android.util.Log; +import androidx.annotation.NonNull; +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.CountingInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; -import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; -import org.apache.commons.io.input.CountingInputStream; - /** - * Reads the ID3 Tag of a given file. In order to use this class, you should - * create a subclass of it and overwrite the onStart* - or onEnd* - methods. + * Reads the ID3 Tag of a given file. + * See https://id3.org/id3v2.3.0 */ public class ID3Reader { - private static final int HEADER_LENGTH = 10; - private static final int ID3_LENGTH = 3; + private static final String TAG = "ID3Reader"; private static final int FRAME_ID_LENGTH = 4; - - private static final int ACTION_SKIP = 1; - static final int ACTION_DONT_SKIP = 2; - - private int readerPosition; - - private static final byte ENCODING_UTF16_WITH_BOM = 1; - private static final byte ENCODING_UTF16_WITHOUT_BOM = 2; - private static final byte ENCODING_UTF8 = 3; + public static final byte ENCODING_ISO = 0; + public static final byte ENCODING_UTF16_WITH_BOM = 1; + public static final byte ENCODING_UTF16_WITHOUT_BOM = 2; + public static final byte ENCODING_UTF8 = 3; private TagHeader tagHeader; + private final CountingInputStream inputStream; - ID3Reader() { + public ID3Reader(CountingInputStream input) { + inputStream = input; } - public final void readInputStream(CountingInputStream input) throws IOException, ID3ReaderException { - int rc; - readerPosition = 0; - char[] tagHeaderSource = readChars(input, HEADER_LENGTH); - tagHeader = createTagHeader(tagHeaderSource); - if (tagHeader == null) { - onNoTagHeaderFound(); - } else { - rc = onStartTagHeader(tagHeader); - if (rc != ACTION_SKIP) { - while (readerPosition < tagHeader.getSize()) { - FrameHeader frameHeader = createFrameHeader(readChars(input, HEADER_LENGTH)); - if (checkForNullString(frameHeader.getId())) { - break; - } - rc = onStartFrameHeader(frameHeader, input); - if (rc == ACTION_SKIP) { - if (frameHeader.getSize() + readerPosition > tagHeader.getSize()) { - break; - } - skipBytes(input, frameHeader.getSize()); - } - } + public void readInputStream() throws IOException, ID3ReaderException { + tagHeader = readTagHeader(); + int tagContentStartPosition = getPosition(); + while (getPosition() < tagContentStartPosition + tagHeader.getSize()) { + FrameHeader frameHeader = readFrameHeader(); + if (frameHeader.getId().charAt(0) < '0' || frameHeader.getId().charAt(0) > 'z') { + Log.d(TAG, "Stopping because of invalid frame: " + frameHeader.toString()); + return; } - onEndTag(); + readFrame(frameHeader); } } - /** Returns true if string only contains null-bytes. */ - private boolean checkForNullString(String s) { - if (!s.isEmpty()) { - int i = 0; - if (s.charAt(i) == 0) { - for (i = 1; i < s.length(); i++) { - if (s.charAt(i) != 0) { - return false; - } - } - return true; - } - return false; - } else { - return true; - } + protected void readFrame(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException { + Log.d(TAG, "Skipping frame: " + frameHeader.toString()); + skipBytes(frameHeader.getSize()); + } + int getPosition() { + return inputStream.getCount(); } /** - * Read a certain number of chars from the given input stream. This method - * changes the readerPosition-attribute. + * Skip a certain number of bytes on the given input stream. */ - char[] readChars(InputStream input, int number) throws IOException, ID3ReaderException { - char[] header = new char[number]; - for (int i = 0; i < number; i++) { - int b = input.read(); - readerPosition++; - if (b != -1) { - header[i] = (char) b; - } else { - throw new ID3ReaderException("Unexpected end of stream"); - } + void skipBytes(int number) throws IOException, ID3ReaderException { + if (number < 0) { + throw new ID3ReaderException("Trying to read a negative number of bytes"); } - return header; + IOUtils.skipFully(inputStream, number); } - /** - * Skip a certain number of bytes on the given input stream. This method - * changes the readerPosition-attribute. - */ - void skipBytes(InputStream input, int number) throws IOException { - if (number <= 0) { - number = 1; - } - IOUtils.skipFully(input, number); + byte readByte() throws IOException { + return (byte) inputStream.read(); + } - readerPosition += number; + short readShort() throws IOException { + char firstByte = (char) inputStream.read(); + char secondByte = (char) inputStream.read(); + return (short) ((firstByte << 8) | secondByte); } - private TagHeader createTagHeader(char[] source) throws ID3ReaderException { - boolean hasTag = (source[0] == 0x49) && (source[1] == 0x44) - && (source[2] == 0x33); - if (source.length != HEADER_LENGTH) { - throw new ID3ReaderException("Length of header must be " - + HEADER_LENGTH); - } - if (hasTag) { - String id = new String(source, 0, ID3_LENGTH); - char version = (char) ((source[3] << 8) | source[4]); - byte flags = (byte) source[5]; - int size = (source[6] << 24) | (source[7] << 16) | (source[8] << 8) - | source[9]; - size = unsynchsafe(size); - return new TagHeader(id, size, version, flags); - } else { - return null; - } + int readInt() throws IOException { + char firstByte = (char) inputStream.read(); + char secondByte = (char) inputStream.read(); + char thirdByte = (char) inputStream.read(); + char fourthByte = (char) inputStream.read(); + return (firstByte << 24) | (secondByte << 16) | (thirdByte << 8) | fourthByte; } - private FrameHeader createFrameHeader(char[] source) - throws ID3ReaderException { - if (source.length != HEADER_LENGTH) { - throw new ID3ReaderException("Length of header must be " - + HEADER_LENGTH); + void expectChar(char expected) throws ID3ReaderException, IOException { + char read = (char) inputStream.read(); + if (read != expected) { + throw new ID3ReaderException("Expected " + expected + " and got " + read); } - String id = new String(source, 0, FRAME_ID_LENGTH); + } - int size = (((int) source[4]) << 24) | (((int) source[5]) << 16) - | (((int) source[6]) << 8) | source[7]; + @NonNull + TagHeader readTagHeader() throws ID3ReaderException, IOException { + expectChar('I'); + expectChar('D'); + expectChar('3'); + short version = readShort(); + byte flags = readByte(); + int size = unsynchsafe(readInt()); + return new TagHeader("ID3", size, version, flags); + } + + @NonNull + FrameHeader readFrameHeader() throws IOException { + String id = readIsoStringFixed(FRAME_ID_LENGTH); + int size = readInt(); if (tagHeader != null && tagHeader.getVersion() >= 0x0400) { size = unsynchsafe(size); } - char flags = (char) ((source[8] << 8) | source[9]); + short flags = readShort(); return new FrameHeader(id, size, flags); } @@ -162,81 +123,73 @@ public class ID3Reader { return out; } - protected int readString(StringBuilder buffer, InputStream input, int max) throws IOException, - ID3ReaderException { - if (max > 0) { - char[] encoding = readChars(input, 1); - max--; - - if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) { - return readUnicodeString(buffer, input, max, Charset.forName("UTF-16")) + 1; // take encoding byte into account - } else if (encoding[0] == ENCODING_UTF8) { - return readUnicodeString(buffer, input, max, Charset.forName("UTF-8")) + 1; // take encoding byte into account - } else { - return readISOString(buffer, input, max) + 1; // take encoding byte into account - } - } else { - if (buffer != null) { - buffer.append(""); - } - return 0; - } + /** + * Reads a null-terminated string with encoding. + */ + protected String readEncodingAndString(int max) throws IOException { + byte encoding = readByte(); + return readEncodedString(encoding, max - 1); } - protected int readISOString(StringBuilder buffer, InputStream input, int max) - throws IOException, ID3ReaderException { + @SuppressWarnings("CharsetObjectCanBeUsed") + protected String readIsoStringFixed(int length) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); int bytesRead = 0; - char c; - while (++bytesRead <= max && (c = (char) input.read()) > 0) { - if (buffer != null) { - buffer.append(c); - } + while (bytesRead < length) { + bytes.write(readByte()); + bytesRead++; } - return bytesRead; - } - - private int readUnicodeString(StringBuilder strBuffer, InputStream input, int max, Charset charset) - throws IOException, ID3ReaderException { - byte[] buffer = new byte[max]; - int c; - int cZero = -1; - int i = 0; - for (; i < max; i++) { - c = input.read(); - if (c == -1) { - break; - } else if (c == 0) { - if (cZero == 0) { - // termination character found - break; - } else { - cZero = 0; - } - } else { - buffer[i] = (byte) c; - cZero = -1; - } - } - if (strBuffer != null) { - strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString()); - } - return i; + return Charset.forName("ISO-8859-1").newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString(); } - int onStartTagHeader(TagHeader header) { - return ACTION_SKIP; + protected String readIsoStringNullTerminated(int max) throws IOException { + return readEncodedString(ENCODING_ISO, max); } - int onStartFrameHeader(FrameHeader header, CountingInputStream input) throws IOException, ID3ReaderException { - return ACTION_SKIP; + @SuppressWarnings("CharsetObjectCanBeUsed") + String readEncodedString(int encoding, int max) throws IOException { + if (encoding == ENCODING_UTF16_WITH_BOM || encoding == ENCODING_UTF16_WITHOUT_BOM) { + return readEncodedString2(Charset.forName("UTF-16"), max); + } else if (encoding == ENCODING_UTF8) { + return readEncodedString2(Charset.forName("UTF-8"), max); + } else { + return readEncodedString1(Charset.forName("ISO-8859-1"), max); + } } - void onEndTag() { - + /** + * Reads chars where the encoding uses 1 char per symbol. + */ + private String readEncodedString1(Charset charset, int max) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int bytesRead = 0; + while (bytesRead < max) { + byte c = readByte(); + bytesRead++; + if (c == 0) { + break; + } + bytes.write(c); + } + return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString(); } - void onNoTagHeaderFound() { - + /** + * Reads chars where the encoding uses 2 chars per symbol. + */ + private String readEncodedString2(Charset charset, int max) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int bytesRead = 0; + while (bytesRead + 1 < max) { + byte c1 = readByte(); + byte c2 = readByte(); + if (c1 == 0 && c2 == 0) { + break; + } + bytesRead += 2; + bytes.write(c1); + bytes.write(c2); + } + return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString(); } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java index 2f3f378ab..e4af89a86 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java @@ -1,17 +1,19 @@ package de.danoeh.antennapod.core.util.id3reader.model; -public class FrameHeader extends Header { +import androidx.annotation.NonNull; - private final char flags; +public class FrameHeader extends Header { + private final short flags; - public FrameHeader(String id, int size, char flags) { - super(id, size); - this.flags = flags; - } + public FrameHeader(String id, int size, short flags) { + super(id, size); + this.flags = flags; + } - @Override - public String toString() { - return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, Integer.toBinaryString(size)); + @Override + @NonNull + public String toString() { + return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, size); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java index b652a139c..2590db029 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java @@ -1,26 +1,25 @@ package de.danoeh.antennapod.core.util.id3reader.model; -public class TagHeader extends Header { - - private final char version; - private final byte flags; +import androidx.annotation.NonNull; - public TagHeader(String id, int size, char version, byte flags) { - super(id, size); - this.version = version; - this.flags = flags; - } - - @Override - public String toString() { - return "TagHeader [version=" + version + ", flags=" + flags + ", id=" - + id + ", size=" + size + "]"; - } +public class TagHeader extends Header { + private final short version; + private final byte flags; - public char getVersion() { - return version; - } + public TagHeader(String id, int size, short version, byte flags) { + super(id, size); + this.version = version; + this.flags = flags; + } - + @Override + @NonNull + public String toString() { + return "TagHeader [version=" + version + ", flags=" + flags + ", id=" + + id + ", size=" + size + "]"; + } + public short getVersion() { + return version; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java index 0467c0a78..c948d98a3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java @@ -10,6 +10,7 @@ import org.antennapod.audio.MediaPlayer; import de.danoeh.antennapod.core.preferences.UserPreferences; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -20,7 +21,7 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { super(context, true, ClientConfig.USER_AGENT); PreferenceManager.getDefaultSharedPreferences(context) .registerOnSharedPreferenceChangeListener((sharedPreferences, key) -> { - if (key.equals(UserPreferences.PREF_MEDIA_PLAYER)) { + if (UserPreferences.PREF_MEDIA_PLAYER.equals(key)) { checkMpi(); } }); @@ -64,4 +65,9 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { public int getSelectedAudioTrack() { return -1; } + + @Override + public void setDataSource(String streamUrl, String username, String password) throws IOException { + setDataSource(streamUrl); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java deleted file mode 100644 index 6c107996f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java +++ /dev/null @@ -1,264 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.media.MediaMetadataRetriever; -import android.os.Parcel; -import android.os.Parcelable; -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.util.ChapterUtils; -import java.util.List; -import java.util.concurrent.Callable; -import org.apache.commons.io.FilenameUtils; - -/** Represents a media file that is stored on the local storage device. */ -public class ExternalMedia implements Playable { - public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2; - public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl"; - public static final String PREF_POSITION = "ExternalMedia.PrefPosition"; - public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType"; - public static final String PREF_LAST_PLAYED_TIME = "ExternalMedia.PrefLastPlayedTime"; - - private final String source; - private String episodeTitle; - private String feedTitle; - private MediaType mediaType; - private List<Chapter> chapters; - private int duration; - private int position; - private long lastPlayedTime; - - /** - * Creates a new playable for files on the sd card. - * @param source File path of the file - * @param mediaType Type of the file - */ - public ExternalMedia(String source, MediaType mediaType) { - super(); - this.source = source; - this.mediaType = mediaType; - } - - /** - * Creates a new playable for files on the sd card. - * @param source File path of the file - * @param mediaType Type of the file - * @param position Position to start from - * @param lastPlayedTime Timestamp when it was played last - */ - public ExternalMedia(String source, MediaType mediaType, int position, long lastPlayedTime) { - this(source, mediaType); - this.position = position; - this.lastPlayedTime = lastPlayedTime; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(source); - dest.writeString(mediaType.toString()); - dest.writeInt(position); - dest.writeLong(lastPlayedTime); - } - - @Override - public void writeToPreferences(Editor prefEditor) { - prefEditor.putString(PREF_SOURCE_URL, source); - prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString()); - prefEditor.putInt(PREF_POSITION, position); - prefEditor.putLong(PREF_LAST_PLAYED_TIME, lastPlayedTime); - } - - @Override - public void loadMetadata() throws PlayableException { - MediaMetadataRetriever mmr = new MediaMetadataRetriever(); - try { - mmr.setDataSource(source); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - throw new PlayableException("IllegalArgumentException when setting up MediaMetadataReceiver"); - } catch (RuntimeException e) { - // http://code.google.com/p/android/issues/detail?id=39770 - e.printStackTrace(); - throw new PlayableException("RuntimeException when setting up MediaMetadataRetriever"); - } - episodeTitle = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); - if (episodeTitle == null) { - episodeTitle = FilenameUtils.getName(source); - } - feedTitle = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM); - try { - duration = Integer.parseInt(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); - } catch (NumberFormatException e) { - e.printStackTrace(); - throw new PlayableException("NumberFormatException when reading duration of media file"); - } - setChapters(ChapterUtils.loadChaptersFromFileUrl(this)); - } - - @Override - public void loadChapterMarks(Context context) { - - } - - @Override - public String getEpisodeTitle() { - return episodeTitle; - } - - @Override - public Callable<String> loadShownotes() { - return () -> ""; - } - - @Override - public List<Chapter> getChapters() { - return chapters; - } - - @Override - public String getWebsiteLink() { - return null; - } - - @Override - public String getPaymentLink() { - return null; - } - - @Override - public String getFeedTitle() { - return feedTitle; - } - - @Override - public Object getIdentifier() { - return source; - } - - @Override - public int getDuration() { - return duration; - } - - @Override - public int getPosition() { - return position; - } - - @Override - public long getLastPlayedTime() { - return lastPlayedTime; - } - - @Override - public MediaType getMediaType() { - return mediaType; - } - - @Override - public String getLocalMediaUrl() { - return source; - } - - @Override - public String getStreamUrl() { - return null; - } - - @Override - public boolean localFileAvailable() { - return true; - } - - @Override - public boolean streamAvailable() { - return false; - } - - @Override - public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp) { - SharedPreferences.Editor editor = pref.edit(); - editor.putInt(PREF_POSITION, newPosition); - editor.putLong(PREF_LAST_PLAYED_TIME, timestamp); - position = newPosition; - lastPlayedTime = timestamp; - editor.apply(); - } - - @Override - public void setPosition(int newPosition) { - position = newPosition; - } - - @Override - public void setDuration(int newDuration) { - duration = newDuration; - } - - @Override - public void setLastPlayedTime(long lastPlayedTime) { - this.lastPlayedTime = lastPlayedTime; - } - - @Override - public void onPlaybackStart() { - - } - - @Override - public void onPlaybackPause(Context context) { - - } - - @Override - public void onPlaybackCompleted(Context context) { - - } - - @Override - public int getPlayableType() { - return PLAYABLE_TYPE_EXTERNAL_MEDIA; - } - - @Override - public void setChapters(List<Chapter> chapters) { - this.chapters = chapters; - } - - public static final Parcelable.Creator<ExternalMedia> CREATOR = new Parcelable.Creator<ExternalMedia>() { - public ExternalMedia createFromParcel(Parcel in) { - String source = in.readString(); - MediaType type = MediaType.valueOf(in.readString()); - int position = 0; - if (in.dataAvail() > 0) { - position = in.readInt(); - } - long lastPlayedTime = 0; - if (in.dataAvail() > 0) { - lastPlayedTime = in.readLong(); - } - - return new ExternalMedia(source, type, position, lastPlayedTime); - } - - public ExternalMedia[] newArray(int size) { - return new ExternalMedia[size]; - } - }; - - @Override - public String getImageLocation() { - if (localFileAvailable()) { - return getLocalMediaUrl(); - } else { - return null; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java index 363004709..a511916fa 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java @@ -35,6 +35,8 @@ public interface IPlayer { void setDataSource(String path) throws IllegalStateException, IOException, IllegalArgumentException, SecurityException; + void setDataSource(String streamUrl, String username, String password) throws IOException; + void setDisplay(SurfaceHolder sh); void setPlaybackParams(float speed, boolean skipSilence); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java index 5b15913c8..feba6db1c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java @@ -3,24 +3,18 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; import android.content.SharedPreferences; import android.os.Parcelable; -import androidx.preference.PreferenceManager; -import android.util.Log; + import androidx.annotation.Nullable; -import de.danoeh.antennapod.core.asynctask.ImageResource; import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.ShownotesProvider; - +import java.util.Date; import java.util.List; /** * Interface for objects that can be played by the PlaybackService. */ -public interface Playable extends Parcelable, - ShownotesProvider, ImageResource { +public interface Playable extends Parcelable { + public static final int INVALID_TIME = -1; /** * Save information about the playable in a preference so that it can be @@ -39,13 +33,6 @@ public interface Playable extends Parcelable, void loadMetadata() throws PlayableException; /** - * This method is called from a separate thread by the PlaybackService. - * Playable objects should load their chapter marks in this method if no - * local file was available when loadMetadata() was called. - */ - void loadChapterMarks(Context context); - - /** * Returns the title of the episode that this playable represents */ String getEpisodeTitle(); @@ -68,6 +55,11 @@ public interface Playable extends Parcelable, String getFeedTitle(); /** + * Returns the published date + */ + Date getPubDate(); + + /** * Returns a unique identifier, for example a file url or an ID from a * database. */ @@ -90,6 +82,13 @@ public interface Playable extends Parcelable, long getLastPlayedTime(); /** + * Returns the description of the item, if available. + * For FeedItems, the description needs to be loaded from the database first. + */ + @Nullable + String getDescription(); + + /** * Returns the type of media. This method should return the correct value * BEFORE loadMetadata() is called. */ @@ -172,99 +171,11 @@ public interface Playable extends Parcelable, void setChapters(List<Chapter> chapters); /** - * Provides utility methods for Playable objects. + * Returns the location of the image or null if no image is available. + * This can be the feed item image URL, the local embedded media image path, the feed image URL, + * or the remote media image URL, depending on what's available. */ - class PlayableUtils { - private PlayableUtils(){} - - private static final String TAG = "PlayableUtils"; - - /** - * Restores a playable object from a sharedPreferences file. This method might load data from the database, - * depending on the type of playable that was restored. - * - * @return The restored Playable object - */ - @Nullable - public static Playable createInstanceFromPreferences(Context context) { - long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMediaType(); - if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - return PlayableUtils.createInstanceFromPreferences(context, - (int) currentlyPlayingMedia, prefs); - } - return null; - } - - /** - * Restores a playable object from a sharedPreferences file. This method might load data from the database, - * depending on the type of playable that was restored. - * - * @param type An integer that represents the type of the Playable object - * that is restored. - * @param pref The SharedPreferences file from which the Playable object - * is restored - * @return The restored Playable object - */ - public static Playable createInstanceFromPreferences(Context context, int type, - SharedPreferences pref) { - Playable result = null; - // ADD new Playable types here: - switch (type) { - case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA: - result = createFeedMediaInstance(pref); - break; - case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA: - result = createExternalMediaInstance(pref); - break; - } - if (result == null) { - Log.e(TAG, "Could not restore Playable object from preferences"); - } - return result; - } - - private static Playable createFeedMediaInstance(SharedPreferences pref) { - Playable result = null; - long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); - if (mediaId != -1) { - result = DBReader.getFeedMedia(mediaId); - } - return result; - } - - private static Playable createExternalMediaInstance(SharedPreferences pref) { - Playable result = null; - String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, null); - String mediaType = pref.getString(ExternalMedia.PREF_MEDIA_TYPE, null); - if (source != null && mediaType != null) { - int position = pref.getInt(ExternalMedia.PREF_POSITION, 0); - long lastPlayedTime = pref.getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, 0); - result = new ExternalMedia(source, MediaType.valueOf(mediaType), - position, lastPlayedTime); - } - return result; - } - } - - class PlayableException extends Exception { - private static final long serialVersionUID = 1L; - - public PlayableException() { - super(); - } - - public PlayableException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public PlayableException(String detailMessage) { - super(detailMessage); - } - - public PlayableException(Throwable throwable) { - super(throwable); - } - - } + @Nullable + String getImageLocation(); + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableException.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableException.java new file mode 100644 index 000000000..c0c21d647 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableException.java @@ -0,0 +1,13 @@ +package de.danoeh.antennapod.core.util.playback; + +/** + * Exception thrown by {@link Playable} implementations. + */ +public class PlayableException extends Exception { + + private static final long serialVersionUID = 1L; + + public PlayableException(String detailMessage) { + super(detailMessage); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java new file mode 100644 index 000000000..861d42c1b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java @@ -0,0 +1,73 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; + +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.storage.DBReader; + +/** + * Provides utility methods for Playable objects. + */ +public abstract class PlayableUtils { + + private static final String TAG = "PlayableUtils"; + + /** + * Restores a playable object from a sharedPreferences file. This method might load data from the database, + * depending on the type of playable that was restored. + * + * @return The restored Playable object + */ + @Nullable + public static Playable createInstanceFromPreferences(@NonNull Context context) { + long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMediaType(); + if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + return PlayableUtils.createInstanceFromPreferences((int) currentlyPlayingMedia, prefs); + } + return null; + } + + /** + * Restores a playable object from a sharedPreferences file. This method might load data from the database, + * depending on the type of playable that was restored. + * + * @param type An integer that represents the type of the Playable object + * that is restored. + * @param pref The SharedPreferences file from which the Playable object + * is restored + * @return The restored Playable object + */ + private static Playable createInstanceFromPreferences(int type, SharedPreferences pref) { + Playable result; + // ADD new Playable types here: + switch (type) { + case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA: + result = createFeedMediaInstance(pref); + break; + default: + result = null; + break; + } + if (result == null) { + Log.e(TAG, "Could not restore Playable object from preferences"); + } + return result; + } + + private static Playable createFeedMediaInstance(SharedPreferences pref) { + Playable result = null; + long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); + if (mediaId != -1) { + result = DBReader.getFeedMedia(mediaId); + } + return result; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index e1b4c967c..117e32cd4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -17,12 +17,10 @@ import android.util.Pair; import android.view.SurfaceHolder; import android.widget.ImageButton; import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.event.ServiceEvent; import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; @@ -30,13 +28,9 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.core.service.playback.PlayerStatus; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.util.Optional; -import de.danoeh.antennapod.core.util.ThemeUtils; -import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import io.reactivex.Maybe; import io.reactivex.MaybeOnSubscribe; -import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; @@ -51,7 +45,7 @@ import java.util.List; * Communicates with the playback service. GUI classes should use this class to * control playback instead of communicating with the PlaybackService directly. */ -public class PlaybackController { +public abstract class PlaybackController { private static final String TAG = "PlaybackController"; private static final int INVALID_TIME = -1; @@ -66,7 +60,6 @@ public class PlaybackController { private boolean initialized = false; private boolean eventsRegistered = false; - private Disposable serviceBinder; private Disposable mediaLoader; public PlaybackController(@NonNull Activity activity) { @@ -153,9 +146,6 @@ public class PlaybackController { } private void unbind() { - if (serviceBinder != null) { - serviceBinder.dispose(); - } try { activity.unbindService(mConnection); } catch (IllegalArgumentException e) { @@ -178,56 +168,11 @@ public class PlaybackController { */ private void bindToService() { Log.d(TAG, "Trying to connect to service"); - if (serviceBinder != null) { - serviceBinder.dispose(); - } - serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(optionalIntent -> { - boolean bound = false; - if (!PlaybackService.isRunning) { - if (optionalIntent.isPresent()) { - Log.d(TAG, "Calling start service"); - ContextCompat.startForegroundService(activity, optionalIntent.get()); - bound = activity.bindService(optionalIntent.get(), mConnection, 0); - } else { - status = PlayerStatus.STOPPED; - setupGUI(); - handleStatus(); - } - } else { - Log.d(TAG, "PlaybackService is running, trying to connect without start command."); - bound = activity.bindService(new Intent(activity, PlaybackService.class), - mConnection, 0); - } - Log.d(TAG, "Result for service binding: " + bound); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - /** - * Returns an intent that starts the PlaybackService and plays the last - * played media or null if no last played media could be found. - */ - @NonNull - private Optional<Intent> getPlayLastPlayedMediaIntent() { - Log.d(TAG, "Trying to restore last played media"); - Playable media = PlayableUtils.createInstanceFromPreferences(activity); - if (media == null) { - Log.d(TAG, "No last played media found"); - return Optional.empty(); + if (!PlaybackService.isRunning) { + throw new IllegalStateException("Trying to bind but service is not running"); } - - boolean fileExists = media.localFileAvailable(); - boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream(); - if (!fileExists && !lastIsStream && media instanceof FeedMedia) { - DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media); - } - - return Optional.of(new PlaybackServiceStarter(activity, media) - .startWhenPrepared(false) - .shouldStream(lastIsStream || !fileExists) - .getIntent()); + boolean bound = activity.bindService(new Intent(activity, PlaybackService.class), mConnection, 0); + Log.d(TAG, "Result for service binding: " + bound); } private final ServiceConnection mConnection = new ServiceConnection() { @@ -331,8 +276,6 @@ public class PlaybackController { } }; - public void setupGUI() {} - public void onPositionObserverUpdate() {} @@ -431,7 +374,10 @@ public class PlaybackController { } private void checkMediaInfoLoaded() { - mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo()); + if (!mediaInfoLoaded) { + loadMediaInfo(); + } + mediaInfoLoaded = true; } private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) { @@ -446,9 +392,7 @@ public class PlaybackController { return null; } - public boolean loadMediaInfo() { - return false; - } + public abstract void loadMediaInfo(); public void onAwaitingVideoSurface() {} @@ -463,10 +407,9 @@ public class PlaybackController { status = info.playerStatus; media = info.playable; - setupGUI(); - handleStatus(); // make sure that new media is loaded if it's available mediaInfoLoaded = false; + handleStatus(); } else { Log.e(TAG, diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java index 29eb20aca..926eaa315 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java @@ -11,10 +11,8 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.util.ChapterUtils; import java.util.Date; import java.util.List; -import java.util.concurrent.Callable; import org.apache.commons.lang3.builder.HashCodeBuilder; /** @@ -129,11 +127,6 @@ public class RemoteMedia implements Playable { } @Override - public void loadChapterMarks(Context context) { - setChapters(ChapterUtils.loadChaptersFromStreamUrl(this, context)); - } - - @Override public String getEpisodeTitle() { return episodeTitle; } @@ -266,8 +259,8 @@ public class RemoteMedia implements Playable { } @Override - public Callable<String> loadShownotes() { - return () -> (notes != null) ? notes : ""; + public String getDescription() { + return notes; } @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java index 40849a262..e125c7e66 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java @@ -9,7 +9,7 @@ import android.text.TextUtils; import android.util.Log; import android.util.TypedValue; -import de.danoeh.antennapod.core.feed.FeedItem; +import androidx.annotation.Nullable; import org.apache.commons.io.IOUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -24,7 +24,6 @@ import java.util.regex.Pattern; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.util.Converter; -import de.danoeh.antennapod.core.util.ShownotesProvider; /** * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the @@ -42,17 +41,16 @@ public class Timeline { private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b((\\d+):)?(\\d+):(\\d{2})\\b"); private static final Pattern LINE_BREAK_REGEX = Pattern.compile("<br */?>"); - private final ShownotesProvider shownotesProvider; + private final String rawShownotes; private final String noShownotesLabel; + private final int playableDuration; private final String webviewStyle; - public Timeline(Context context, ShownotesProvider shownotesProvider) { - if (shownotesProvider == null) { - throw new IllegalArgumentException("shownotesProvider = null"); - } - this.shownotesProvider = shownotesProvider; + public Timeline(Context context, @Nullable String rawShownotes, int playableDuration) { + this.rawShownotes = rawShownotes; noShownotesLabel = context.getString(R.string.no_shownotes_label); + this.playableDuration = playableDuration; final String colorPrimary = colorToHtml(context, android.R.attr.textColorPrimary); final String colorAccent = colorToHtml(context, R.attr.colorAccent); final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, @@ -87,13 +85,7 @@ public class Timeline { */ @NonNull public String processShownotes() { - String shownotes; - try { - shownotes = shownotesProvider.loadShownotes().call(); - } catch (Exception e) { - Log.e(TAG, "processShownotes() - encounters exceptions unexpectedly in load, treat as if no shownotes.", e); - shownotes = ""; - } + String shownotes = rawShownotes; if (TextUtils.isEmpty(shownotes)) { Log.d(TAG, "shownotesProvider contained no shownotes. Returning 'no shownotes' message"); @@ -147,14 +139,6 @@ public class Timeline { // No elements with timecodes return; } - - int playableDuration = Integer.MAX_VALUE; - if (shownotesProvider instanceof Playable) { - playableDuration = ((Playable) shownotesProvider).getDuration(); - } else if (shownotesProvider instanceof FeedItem && ((FeedItem) shownotesProvider).getMedia() != null) { - playableDuration = ((FeedItem) shownotesProvider).getMedia().getDuration(); - } - boolean useHourFormat = true; if (playableDuration != Integer.MAX_VALUE) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java index d18801870..6728c027d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.util.playback; import android.media.MediaPlayer; import android.util.Log; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -52,4 +53,9 @@ public class VideoPlayer extends MediaPlayer implements IPlayer { public int getSelectedAudioTrack() { return -1; } + + @Override + public void setDataSource(String streamUrl, String username, String password) throws IOException { + setDataSource(streamUrl); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java new file mode 100644 index 000000000..afbe6526b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java @@ -0,0 +1,229 @@ +package de.danoeh.antennapod.core.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.widget.RemoteViews; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import java.util.concurrent.TimeUnit; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.glide.ApGlideSettings; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.receiver.PlayerWidget; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; +import de.danoeh.antennapod.core.util.TimeSpeedConverter; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter; + +/** + * Updates the state of the player widget. + */ +public abstract class WidgetUpdater { + private static final String TAG = "WidgetUpdater"; + + public static class WidgetState { + final Playable media; + final PlayerStatus status; + final int position; + final int duration; + final float playbackSpeed; + final boolean isCasting; + + public WidgetState(Playable media, PlayerStatus status, int position, int duration, + float playbackSpeed, boolean isCasting) { + this.media = media; + this.status = status; + this.position = position; + this.duration = duration; + this.playbackSpeed = playbackSpeed; + this.isCasting = isCasting; + } + + public WidgetState(PlayerStatus status) { + this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f, false); + } + } + + /** + * Update the widgets with the given parameters. Must be called in a background thread. + */ + public static void updateWidget(Context context, WidgetState widgetState) { + if (!PlayerWidget.isEnabled(context) || widgetState == null) { + return; + } + ComponentName playerWidget = new ComponentName(context, PlayerWidget.class); + AppWidgetManager manager = AppWidgetManager.getInstance(context); + int[] widgetIds = manager.getAppWidgetIds(playerWidget); + + PendingIntent startMediaPlayer; + if (widgetState.media != null && widgetState.media.getMediaType() == MediaType.VIDEO + && !widgetState.isCasting) { + startMediaPlayer = new VideoPlayerActivityStarter(context).getPendingIntent(); + } else { + startMediaPlayer = new MainActivityStarter(context).withOpenPlayer().getPendingIntent(); + } + RemoteViews views; + views = new RemoteViews(context.getPackageName(), R.layout.player_widget); + + if (widgetState.media != null) { + Bitmap icon; + int iconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); + views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer); + views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer); + + try { + icon = Glide.with(context) + .asBitmap() + .load(widgetState.media.getImageLocation()) + .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY)) + .submit(iconSize, iconSize) + .get(500, TimeUnit.MILLISECONDS); + views.setImageViewBitmap(R.id.imgvCover, icon); + } catch (Throwable tr1) { + try { + icon = Glide.with(context) + .asBitmap() + .load(ImageResourceUtils.getFallbackImageLocation(widgetState.media)) + .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY)) + .submit(iconSize, iconSize) + .get(500, TimeUnit.MILLISECONDS); + views.setImageViewBitmap(R.id.imgvCover, icon); + } catch (Throwable tr2) { + Log.e(TAG, "Error loading the media icon for the widget", tr2); + views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round); + } + } + + views.setTextViewText(R.id.txtvTitle, widgetState.media.getEpisodeTitle()); + views.setViewVisibility(R.id.txtvTitle, View.VISIBLE); + views.setViewVisibility(R.id.txtNoPlaying, View.GONE); + + String progressString = getProgressString(widgetState.position, + widgetState.duration, widgetState.playbackSpeed); + if (progressString != null) { + views.setViewVisibility(R.id.txtvProgress, View.VISIBLE); + views.setTextViewText(R.id.txtvProgress, progressString); + } + + if (widgetState.status == PlayerStatus.PLAYING) { + views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_pause_white_48dp); + views.setContentDescription(R.id.butPlay, context.getString(R.string.pause_label)); + views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_pause_white_48dp); + views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.pause_label)); + } else { + views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp); + views.setContentDescription(R.id.butPlay, context.getString(R.string.play_label)); + views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_play_white_48dp); + views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.play_label)); + } + views.setOnClickPendingIntent(R.id.butPlay, + createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + views.setOnClickPendingIntent(R.id.butPlayExtended, + createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + views.setOnClickPendingIntent(R.id.butRew, + createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_REWIND)); + views.setOnClickPendingIntent(R.id.butFastForward, + createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)); + views.setOnClickPendingIntent(R.id.butSkip, + createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT)); + } else { + // start the app if they click anything + views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer); + views.setOnClickPendingIntent(R.id.butPlay, startMediaPlayer); + views.setOnClickPendingIntent(R.id.butPlayExtended, + createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + views.setViewVisibility(R.id.txtvProgress, View.GONE); + views.setViewVisibility(R.id.txtvTitle, View.GONE); + views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE); + views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round); + views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp); + views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_play_white_48dp); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + for (int id : widgetIds) { + Bundle options = manager.getAppWidgetOptions(id); + SharedPreferences prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE); + int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); + int columns = getCellsForSize(minWidth); + if (columns < 3) { + views.setViewVisibility(R.id.layout_center, View.INVISIBLE); + } else { + views.setViewVisibility(R.id.layout_center, View.VISIBLE); + } + boolean showRewind = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, false); + boolean showFastForward = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, false); + boolean showSkip = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, false); + + if (showRewind || showSkip || showFastForward) { + views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE); + views.setInt(R.id.butPlay, "setVisibility", View.GONE); + views.setInt(R.id.butRew, "setVisibility", showRewind ? View.VISIBLE : View.GONE); + views.setInt(R.id.butFastForward, "setVisibility", showFastForward ? View.VISIBLE : View.GONE); + views.setInt(R.id.butSkip, "setVisibility", showSkip ? View.VISIBLE : View.GONE); + } + + int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR); + views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor); + + manager.updateAppWidget(id, views); + } + } else { + manager.updateAppWidget(playerWidget, views); + } + } + + /** + * Returns number of cells needed for given size of the widget. + * + * @param size Widget size in dp. + * @return Size in number of cells. + */ + private static int getCellsForSize(int size) { + int n = 2; + while (70 * n - 30 < size) { + ++n; + } + return n - 1; + } + + /** + * Creates an intent which fakes a mediabutton press. + */ + private static PendingIntent createMediaButtonIntent(Context context, int eventCode) { + KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, eventCode); + Intent startingIntent = new Intent(context, MediaButtonReceiver.class); + startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER); + startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); + + return PendingIntent.getBroadcast(context, eventCode, startingIntent, 0); + } + + private static String getProgressString(int position, int duration, float speed) { + if (position >= 0 && duration > 0) { + TimeSpeedConverter converter = new TimeSpeedConverter(speed); + position = converter.convert(position); + duration = converter.convert(duration); + return Converter.getDurationStringLong(position) + " / " + + Converter.getDurationStringLong(duration); + } else { + return null; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java new file mode 100644 index 000000000..004588945 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod.core.widget; + +import android.content.Context; +import android.content.Intent; +import androidx.annotation.NonNull; +import androidx.core.app.SafeJobIntentService; +import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlayableUtils; + +public class WidgetUpdaterJobService extends SafeJobIntentService { + private static final int JOB_ID = -17001; + + /** + * Loads the current media from the database and updates the widget in a background job. + */ + public static void performBackgroundUpdate(Context context) { + enqueueWork(context, WidgetUpdaterJobService.class, + WidgetUpdaterJobService.JOB_ID, new Intent(context, WidgetUpdaterJobService.class)); + } + + @Override + protected void onHandleWork(@NonNull Intent intent) { + Playable media = PlayableUtils.createInstanceFromPreferences(getApplicationContext()); + if (media != null) { + WidgetUpdater.updateWidget(this, new WidgetUpdater.WidgetState(media, PlayerStatus.STOPPED, + media.getPosition(), media.getDuration(), PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), + PlaybackPreferences.getCurrentEpisodeIsStream())); + } else { + WidgetUpdater.updateWidget(this, new WidgetUpdater.WidgetState(PlayerStatus.STOPPED)); + } + } +}
\ No newline at end of file diff --git a/core/src/main/res/drawable-hdpi/ic_notification_new.png b/core/src/main/res/drawable-hdpi/ic_notification_new.png Binary files differnew file mode 100644 index 000000000..28a8446e4 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_notification_new.png diff --git a/core/src/main/res/drawable-mdpi/ic_notification_new.png b/core/src/main/res/drawable-mdpi/ic_notification_new.png Binary files differnew file mode 100644 index 000000000..02530f5e4 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_notification_new.png diff --git a/core/src/main/res/drawable-xhdpi/ic_notification_new.png b/core/src/main/res/drawable-xhdpi/ic_notification_new.png Binary files differnew file mode 100644 index 000000000..49c696798 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_notification_new.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_notification_new.png b/core/src/main/res/drawable-xxhdpi/ic_notification_new.png Binary files differnew file mode 100644 index 000000000..ec6ef4f1e --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_notification_new.png diff --git a/core/src/main/res/drawable-xxxhdpi/ic_notification_new.png b/core/src/main/res/drawable-xxxhdpi/ic_notification_new.png Binary files differnew file mode 100644 index 000000000..66f968872 --- /dev/null +++ b/core/src/main/res/drawable-xxxhdpi/ic_notification_new.png diff --git a/core/src/main/res/drawable/ic_notification_auto_download_complete.xml b/core/src/main/res/drawable/ic_notification_auto_download_complete.xml deleted file mode 100644 index 0caf27836..000000000 --- a/core/src/main/res/drawable/ic_notification_auto_download_complete.xml +++ /dev/null @@ -1,9 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM16.59,7.58L10,14.17l-2.59,-2.58L6,13l4,4 8,-8z" - android:fillColor="#ffffff"/> -</vector> diff --git a/core/src/main/res/drawable/ic_share_black.xml b/core/src/main/res/drawable/ic_share_black.xml new file mode 100644 index 000000000..f396c50de --- /dev/null +++ b/core/src/main/res/drawable/ic_share_black.xml @@ -0,0 +1,7 @@ +<vector android:height="24dp" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path + android:fillColor="#FF000000" + android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_share_white.xml b/core/src/main/res/drawable/ic_share_white.xml new file mode 100644 index 000000000..ae1b3d12b --- /dev/null +++ b/core/src/main/res/drawable/ic_share_white.xml @@ -0,0 +1,7 @@ +<vector android:height="24dp" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> +</vector> diff --git a/core/src/main/res/layout/player_widget.xml b/core/src/main/res/layout/player_widget.xml index 8e38d7f6e..ab42e4cb4 100644 --- a/core/src/main/res/layout/player_widget.xml +++ b/core/src/main/res/layout/player_widget.xml @@ -27,7 +27,7 @@ <LinearLayout android:id="@+id/layout_left" - android:layout_width="0dp" + android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" @@ -59,7 +59,7 @@ android:maxLines="3" android:text="@string/no_media_playing_label" android:textColor="@color/white" - android:textSize="@dimen/text_size_medium" + android:textSize="16sp" android:textStyle="bold" /> <TextView @@ -67,8 +67,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="1" + android:ellipsize="end" android:textColor="@color/white" - android:textSize="@dimen/text_size_medium" + android:textSize="16sp" android:textStyle="bold" android:visibility="gone" /> @@ -78,9 +79,61 @@ android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textColor="@color/white" + android:textSize="14sp" android:visibility="gone" /> + + <LinearLayout + android:id="@+id/extendedButtonsContainer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:visibility="gone"> + + <ImageButton + android:id="@+id/butRew" + android:layout_width="36dp" + android:layout_height="36dp" + android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/rewind_label" + android:layout_marginRight="2dp" + android:layout_marginEnd="2dp" + android:scaleType="fitXY" + android:src="@drawable/ic_av_fast_rewind_white_48dp"/> + + <ImageButton + android:id="@+id/butPlayExtended" + android:layout_width="36dp" + android:layout_height="36dp" + android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/play_label" + android:layout_marginRight="2dp" + android:layout_marginEnd="2dp" + android:scaleType="fitXY" + android:src="@drawable/ic_av_play_white_48dp"/> + + <ImageButton + android:id="@+id/butFastForward" + android:layout_width="36dp" + android:layout_height="36dp" + android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/fast_forward_label" + android:layout_marginRight="2dp" + android:layout_marginEnd="2dp" + android:scaleType="fitXY" + android:src="@drawable/ic_av_fast_forward_white_48dp"/> + + <ImageButton + android:id="@+id/butSkip" + android:layout_width="36dp" + android:layout_height="36dp" + android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/skip_episode_label" + android:layout_marginRight="2dp" + android:layout_marginEnd="2dp" + android:scaleType="fitXY" + android:src="@drawable/ic_av_skip_white_24dp"/> + </LinearLayout> </LinearLayout> </LinearLayout> </RelativeLayout> - </FrameLayout>
\ No newline at end of file diff --git a/core/src/main/res/values-ar/strings.xml b/core/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..e087f10d5 --- /dev/null +++ b/core/src/main/res/values-ar/strings.xml @@ -0,0 +1,640 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activity and fragment titles--> + <string name="feed_update_receiver_name">تحديث الاشتراكات</string> + <string name="feeds_label">بودكاستات</string> + <string name="statistics_label">إحصائيات</string> + <string name="add_feed_label">إضافة بودكاست</string> + <string name="episodes_label">حلقات</string> + <string name="queue_label">لائحة الاستماع</string> + <string name="all_episodes_short_label">الكل</string> + <string name="new_episodes_label">جديد</string> + <string name="favorite_episodes_label">المفضلات</string> + <string name="new_label">جديد</string> + <string name="settings_label">إعدادات</string> + <string name="downloads_label">تنزيلات</string> + <string name="downloads_running_label">جارى التشغيل</string> + <string name="downloads_completed_label">منتهى</string> + <string name="downloads_log_label">سجل</string> + <string name="subscriptions_label">إشتراكات</string> + <string name="subscriptions_list_label">لائحة الإشتراكات</string> + <string name="playback_history_label">سجل التشغيل</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">تسجيل الدخول لموقع gpodder</string> + <string name="episode_cache_full_title">ذاكرة تخزين الحلقات ممتلئة</string> + <string name="episode_cache_full_message">لقد تم تجاوز الحد الأقصى لتخزين الحلقات. المرجو الرفع من قيمة التخزين في قائمة الإعدادات.</string> + <string name="playback_statistics_label">تشغيل</string> + <string name="download_statistics_label">تنزيلات</string> + <string name="notification_pref_fragment">إشعارات</string> + <!--Google Assistant--> + <!--Statistics fragment--> + <string name="total_time_listened_to_podcasts">الوقت الكلي للحلقات المشغلة:</string> + <string name="statistics_details_dialog">%1$d حلقة من %2$d بدأ.\n\nتشغيلها %3$s من مجموع %4$s.</string> + <string name="statistics_mode">نمط الإحصاءات</string> + <string name="statistics_mode_normal">أحسب وقت التشغيل الفعلي. التشغيل مرتين يحسب مرتين, بينما التحديد بـ مشغلة لا يحتسب</string> + <string name="statistics_mode_count_all">أحسب كل الحلقات المحددة بـ مشغلة</string> + <string name="statistics_speed_not_counted">ملاحظة: سرعة التشغيل لن تأخذ بالاعتبار.</string> + <string name="statistics_reset_data">صفر الاحصاءات</string> + <string name="statistics_reset_data_msg">هذا سيمسح سجل وقت التشغيل لكل الحلقات. هل أنت متأكد؟</string> + <string name="statistics_counting_since">منذ %s,\nقمت بتشغيلها</string> + <!--Download Statistics fragment--> + <string name="total_size_downloaded_podcasts">الحجم الكلي للحلقات على الجهاز:</string> + <!--Main activity--> + <string name="drawer_open">قائمة الفتح</string> + <string name="drawer_close">قائمة الاغلاف</string> + <string name="drawer_preferences">تفضيلات الدرج</string> + <string name="drawer_feed_order_unplayed_episodes">رتب بعدد الحلقات</string> + <string name="drawer_feed_order_alphabetical">رتب أبجديا</string> + <string name="drawer_feed_order_last_update">رتب بتاريخ النشر</string> + <string name="drawer_feed_order_most_played">رتب بعدد الحلقات المشغلة</string> + <string name="drawer_feed_counter_new_unplayed">عدد الحلقات الجديدة وغير المشغلة</string> + <string name="drawer_feed_counter_new">عدد الحلقات الجديدة</string> + <string name="drawer_feed_counter_unplayed">عدد الحلقات غير المشغلة</string> + <string name="drawer_feed_counter_downloaded">عدد الحلقات المنزلة</string> + <string name="drawer_feed_counter_none">بدون</string> + <!--Bug report activity--> + <string name="log_file_share_exception">لا يوجد برنامج متوافق</string> + <!--Webview actions--> + <string name="open_in_browser_label">افتح فى المتصفح</string> + <string name="copy_url_label">انسخ الرابط</string> + <string name="share_url_label">مشاركة الرابط</string> + <string name="copied_url_msg">تم نسخ الرابط لذاكرة القصاصات</string> + <string name="go_to_position_label">اذهب لهذا التوقيت</string> + <!--Playback history--> + <string name="clear_history_label">مسح السجل</string> + <!--Other--> + <string name="confirm_label">تأكيد</string> + <string name="cancel_label">الغاء</string> + <string name="yes">نعم</string> + <string name="no">لا</string> + <string name="reset">إعادة التعيين</string> + <string name="author_label">الناشر(ون)</string> + <string name="language_label">لغة</string> + <string name="url_label">عنوان الموقع</string> + <string name="cover_label">صورة</string> + <string name="error_label">خطأ</string> + <string name="error_msg_prefix">حدث خطأ:</string> + <string name="needs_storage_permission">هذه العلملية تحتاج السماح للوصول لمساحة التخزين</string> + <string name="refresh_label">تحديث</string> + <string name="external_storage_error_msg">لا توجد ذاكرة خارجية متاحة. فضلا تأكد من إتاحة الذاكرة الخارجية للتطبيق حتى يعمل جيدا.</string> + <string name="chapters_label">فصول</string> + <string name="chapter_duration">مدة: %1$s</string> + <string name="description_label">وصف</string> + <string name="episodes_suffix">\u0020حلقات</string> + <string name="processing_label">جارى المعالجة</string> + <string name="close_label">اغلاق</string> + <string name="retry_label">اعادة المحاولة</string> + <string name="auto_download_label">تضمين فى التنزيل التلقائي</string> + <string name="auto_download_apply_to_items_title">طبق هذا على الحلقات الماضية</string> + <string name="auto_download_apply_to_items_message">إعدادات <i>التنزيل التلقائي</i> الجديدة ستطبق على الحلقات الجديدة.\nهل تريد أيضا تطبيقها على الحلقات الماضية؟</string> + <string name="auto_delete_label">مسح الحلقات تلقائيا</string> + <string name="feed_volume_reduction">تقليل شدة الصوت</string> + <string name="feed_volume_reduction_summary">قلل شدة الصوت لحلقات هذه القناة: %1$s</string> + <string name="feed_volume_reduction_off">بدون</string> + <string name="feed_volume_reduction_light">خفيف</string> + <string name="feed_volume_reduction_heavy">شديد</string> + <string name="feed_auto_download_global">اعدادات افتراضية شاملة</string> + <string name="feed_auto_download_always">دائما</string> + <string name="feed_auto_download_never">ابدا</string> + <string name="send_label">ارسال ...</string> + <string name="episode_cleanup_never">ابدا</string> + <string name="episode_cleanup_queue_removal">إذا ليس في لائحة الاستماع</string> + <string name="episode_cleanup_after_listening">بعد الانتهاء</string> + <plurals name="episode_cleanup_hours_after_listening"> + <item quantity="zero">%d ساعة بعد الأنتهاء</item> + <item quantity="one">1 ساعة بعد الأنتهاء</item> + <item quantity="two">%d ساعتان بعد الأنتهاء</item> + <item quantity="few">%d ساعة بعد الأنتهاء</item> + <item quantity="many">%d ساعة بعد الأنتهاء</item> + <item quantity="other">%d ساعة بعد الأنتهاء</item> + </plurals> + <plurals name="episode_cleanup_days_after_listening"> + <item quantity="zero">%d يوم بعد الأنتهاء</item> + <item quantity="one">1 يوم بعد الأنتهاء</item> + <item quantity="two">%d يومان بعد الأنتهاء</item> + <item quantity="few">%d يوم بعد الأنتهاء</item> + <item quantity="many">%d يوم بعد الأنتهاء</item> + <item quantity="other">%d يوم بعد الأنتهاء</item> + </plurals> + <plurals name="num_selected_label"> + <item quantity="zero">%d مختار</item> + <item quantity="one">%d مختار</item> + <item quantity="two">%d مختار</item> + <item quantity="few">%d مختار</item> + <item quantity="many">%d مختار</item> + <item quantity="other">%d مختار</item> + </plurals> + <string name="loading_more">تحميل المزيد...</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">تعليم الكل بـ تم تشغيله</string> + <string name="mark_all_read_msg">علم كل الحلقات بـ تم تشغيلها</string> + <string name="mark_all_read_confirmation_msg">نرجو تأكيد تعليم كل الحلقات بـ تم تشغيلها.</string> + <string name="mark_all_read_feed_confirmation_msg">نرجو تأكيد تعليم كل الحلقات في هذه القناة بـ تم تشغيلها.</string> + <string name="remove_all_new_flags_label">ازل كل العلامات الجديدة</string> + <string name="removed_all_new_flags_msg">أزيلت كل العلامات الجديدة</string> + <string name="remove_all_new_flags_confirmation_msg">نرجو تأكيد إزالة كل العلامات الجديدة من كل الحلقات.</string> + <string name="show_info_label">اظهر معلومات</string> + <string name="show_feed_settings_label">أظهر اعدادات البودكاست</string> + <string name="feed_info_label">معلومات البودكاست</string> + <string name="feed_settings_label">إعدادات البودكاست</string> + <string name="rename_feed_label">تغيير اسم البودكاست</string> + <string name="remove_feed_label">ازل البودكاست</string> + <string name="share_label">مشاركة</string> + <string name="share_label_with_ellipses">مشاركة...</string> + <string name="share_file_label">مشاركة ملف</string> + <string name="share_website_url_label">عنوان موقع</string> + <string name="share_feed_url_label">عنوان URL قناة البودكاست</string> + <string name="feed_delete_confirmation_msg">نرجو تأكيد رغبتك مسح بودكاست \"%1$s\" وكل حلقاته (بما فيها المنزلة).</string> + <string name="feed_delete_confirmation_local_msg">نرجو تأكيد رغبتك إزالة بودكاست \"%1$s\". الملفات المنزلة لن يتم مسحها.</string> + <string name="feed_remover_msg">أزيل البودكاست</string> + <string name="load_complete_feed">حدث كامل البودكاست</string> + <string name="multi_select">اختيار متعدد</string> + <string name="select_all_above">اختار الكل أعلاه</string> + <string name="select_all_below">اختار الكل أدناه</string> + <string name="hide_unplayed_episodes_label">لم يتم تشغيله</string> + <string name="hide_queued_episodes_label">ضمن لائحة الاستماع</string> + <string name="hide_not_queued_episodes_label">ليس ضمن لائحة الاستماع</string> + <string name="hide_has_media_label">فيها وسائط</string> + <string name="filtered_label">مصفى</string> + <string name="open_podcast">افتح البودكاست</string> + <string name="please_wait_for_data">نرجو الانتظار حتى يتم تحميل البيانات</string> + <!--actions on feeditems--> + <string name="download_label">تنزيل</string> + <plurals name="downloading_batch_label"> + <item quantity="zero">تنزيل %d حلقة.</item> + <item quantity="one">تنزيل %d حلقة.</item> + <item quantity="two">تنزيل %d حلقتان.</item> + <item quantity="few">تنزيل %d حلقات.</item> + <item quantity="many">تنزيل %d حلقات.</item> + <item quantity="other">تنزيل %d حلقات.</item> + </plurals> + <string name="play_label">تشغيل</string> + <string name="pause_label">ايقاف مؤقت</string> + <string name="stream_label">مباشر</string> + <string name="delete_label">مسح</string> + <string name="delete_failed">لم نتمكن من مسح الملف. إعادة تشغيل الجهاز قد تساعد.</string> + <string name="delete_episode_label">مسح الحلقة</string> + <string name="remove_new_flag_label">أزل العلامات الجديدة</string> + <string name="removed_new_flag_label">أزيلت العلامات الجديدة</string> + <string name="mark_read_label">علمها كـ مشغلة</string> + <string name="marked_as_read_label">تم تعليمها كـ مشغلة</string> + <string name="mark_read_no_media_label">علمها كـ مقروءة</string> + <string name="marked_as_read_no_media_label">علمت كـ مقروءة</string> + <string name="play_this_to_seek_position">للإنتقال للتوقيتات, يجب أن تشغل الحلقة</string> + <plurals name="marked_read_batch_label"> + <item quantity="zero">%d حلقة علمت كـ مقروءة.</item> + <item quantity="one">%d حلقة علمت كـ مقروءة.</item> + <item quantity="two">%d حلقتان علمتا كـ مقروءة.</item> + <item quantity="few">%d حلقات علمت كـ مقروءة.</item> + <item quantity="many">%d حلقات علمت كـ مقروءة. </item> + <item quantity="other">%d حلقات علمت كـ مقروءة.</item> + </plurals> + <string name="mark_unread_label">تعليمه ك لم يتم تشغيله</string> + <string name="mark_unread_label_no_media">علمها كـ غير مقروءة</string> + <plurals name="marked_unread_batch_label"> + <item quantity="zero">%d حلقة علمت كـ غير مقروءة. </item> + <item quantity="one">%d حلقة علمت كـ غير مقروءة. </item> + <item quantity="two">%d حلقتان علمتا كـ غير مقروءة.</item> + <item quantity="few">%d حلقات علمت كـ غير مقروءة.</item> + <item quantity="many">%d حلقات علمت كـ غير مقروءة.</item> + <item quantity="other">%d حلقات علمت كـ غير مقروءة.</item> + </plurals> + <string name="add_to_queue_label">اضف للائحة الاستماع</string> + <string name="added_to_queue_label">أضيفت للائحة الاستماع</string> + <plurals name="added_to_queue_batch_label"> + <item quantity="zero">%d حلقة أضيفت الى لائحة الاستماع.</item> + <item quantity="one">%d حلقة أضيفت الى لائحة الاستماع.</item> + <item quantity="two">%d حلقتان أضيفتا الى لائحة الاستماع.</item> + <item quantity="few">%d حلقات أضيفت الى لائحة الاستماع.</item> + <item quantity="many">%d حلقات أضيفت الى لائحة الاستماع.</item> + <item quantity="other">%d حلقات أضيفت الى لائحة الاستماع.</item> + </plurals> + <string name="remove_from_queue_label">أزل من لائحة الاستماع</string> + <plurals name="removed_from_queue_batch_label"> + <item quantity="zero">%d حلقة أزيلت من لائحة الاستماع.</item> + <item quantity="one">%d حلقة أزيلت من لائحة الاستماع.</item> + <item quantity="two">%d حلقتان أزيلتا من لائحة الاستماع.</item> + <item quantity="few">%d حلقات أزيلت من لائحة الاستماع.</item> + <item quantity="many">%d حلقات أزيلت من لائحة الاستماع.</item> + <item quantity="other">%d حلقات أزيلت من لائحة الاستماع.</item> + </plurals> + <string name="add_to_favorite_label">اضافة للمفضلات</string> + <string name="added_to_favorites">تم اضافته للمفضلات</string> + <string name="remove_from_favorite_label">المسح من المفضلات</string> + <string name="removed_from_favorites">تم مسحه من المفضلات</string> + <string name="visit_website_label">زيارة الموقع</string> + <string name="skip_episode_label">تخطى الحلقة</string> + <string name="activate_auto_download">تفعيل التنزيل التلقائي</string> + <string name="deactivate_auto_download">ايقاف التنزيل التلقائي</string> + <string name="reset_position">صفر موضع التشغيل</string> + <string name="removed_item">تم حزف العنصر</string> + <string name="no_items_selected">لم يتم اختيار أي عنصر</string> + <!--Download messages and labels--> + <string name="download_successful">نجحت العملية</string> + <string name="download_pending">التنزيل فى الانتظار</string> + <string name="download_running">جارى التنزيل</string> + <string name="download_error_details">تفاصيل</string> + <string name="download_error_details_message">%1$s \n\nURL الملف:\n%2$s</string> + <string name="download_error_device_not_found">جهاز التخزين غير موجود</string> + <string name="download_error_http_data_error">خطاء فى بيانات HTTP</string> + <string name="download_error_error_unknown">خطاء غير معروف</string> + <string name="download_error_unsupported_type">نمط قناة غير مدعوم</string> + <string name="download_error_connection_error">خطاء فى الاتصال</string> + <string name="download_error_unauthorized">خطاء فى التحقق</string> + <string name="download_error_file_type_type">خطأ في نوع الملف</string> + <string name="download_canceled_msg">ألغي التنزيل</string> + <string name="download_canceled_autodownload_enabled_msg">ألغي التنزيل\nمعطل <i>التنزيل التلقائي</i> لهذه المادة</string> + <string name="download_report_title">أنتهى التنزيل مع خطأ (او أكثر)</string> + <string name="auto_download_report_title">التنزيل التلقائي أنتهى</string> + <string name="download_report_content_title">تقرير التنزيل</string> + <string name="download_error_malformed_url">غطأ في تنسيق URL</string> + <string name="download_error_io_error">خطأ إدخال/إخراج</string> + <string name="download_error_request_error">خطأ طلب</string> + <string name="download_error_db_access">خطأ وصول لقاعدة البيانات</string> + <plurals name="downloads_left"> + <item quantity="zero">%d تنزيل بقي</item> + <item quantity="one">%d تنزيل بقي</item> + <item quantity="two">%d تنزيلان بقيا</item> + <item quantity="few">%d تنزيلات بقت</item> + <item quantity="many">%d تنزيلات بقت</item> + <item quantity="other">%d تنزيلات بقت</item> + </plurals> + <string name="download_notification_title">تنزيل بيانات البودكاست</string> + <string name="download_log_title_unknown">عنوان غير معروف</string> + <string name="download_type_feed">قناة</string> + <string name="download_type_media">ملف وسائط</string> + <string name="download_request_error_dialog_message_prefix">حدث خطأ عند محاولة تنزيل ملف:\u0020</string> + <string name="null_value_podcast_error">لم نعطى بودكاست يمكن عرضه.</string> + <string name="authentication_notification_title">التحقق مطلوب</string> + <string name="authentication_notification_msg">المورد الذي طلبته يتطلب اسم مستخدم وكلمة مرور</string> + <string name="confirm_mobile_download_dialog_title">أكد التنزيل على بيانات الجوال</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">التنزيل على بيانات الجوال معطل في الإعدادات.\n\nبإمكانك اضافة الحلقة للائحة الإستماع أو السماح بالتنزيل مؤقتا.\n\n<small>سنتذكر هذا الخيار لمدة 10 دقائق.</small></string> + <string name="confirm_mobile_download_dialog_message">التنزيل على بيانات الجوال معطل في الاعدادات.\n\nهل تريد السماح بالتنزيل مؤقتا؟\n\n<small>خيارك سيتم تذكره لمدة 10 دقائق.</small></string> + <string name="confirm_mobile_streaming_notification_title">أكد البث على بيانات الجوال</string> + <string name="confirm_mobile_streaming_notification_message">البث على بيانات الجوال معطل في الاعدادات. اضغط للبث رغم ذلك. </string> + <string name="confirm_mobile_streaming_button_always">دائما</string> + <string name="confirm_mobile_streaming_button_once">مرة</string> + <string name="confirm_mobile_download_dialog_only_add_to_queue">Enqueue</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">السماح مؤقتا</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">خطأ!</string> + <string name="player_stopped_msg">لا وسائط تشغل</string> + <string name="player_preparing_msg">تجهيز</string> + <string name="player_ready_msg">جاهز</string> + <string name="player_seeking_msg">جارى القصد</string> + <string name="playback_error_server_died">تعطل الخادم</string> + <string name="playback_error_unsupported">نمط وسائط غير مدعوم</string> + <string name="playback_error_timeout">أنتهى وقت العملية</string> + <string name="playback_error_source">لا يمكن الوصول لملف الوسائط</string> + <string name="playback_error_unknown">خطاء غير معروف</string> + <string name="no_media_playing_label">لا وسائط تشغل</string> + <string name="player_buffering_msg">تخزين مؤقت</string> + <string name="player_go_to_picture_in_picture">نمط صورة-في-صورة</string> + <string name="unknown_media_key">AntennaPod - مفتاح وسائط غير معروف: %1$d</string> + <string name="error_file_not_found">ملف غير موجود</string> + <!--Queue operations--> + <string name="lock_queue">قفل لائحة الإستماع</string> + <string name="unlock_queue">فتح لائحة الإستماع</string> + <string name="queue_locked">لائحة الإستماع مقفلة</string> + <string name="queue_unlocked">لائحة الإستماع مفتوحة</string> + <string name="queue_lock_warning">إذا اقفلت لائحة الإستماع, لا يمكنت تبديل أو ترتيب الحلقات فيها.</string> + <string name="checkbox_do_not_show_again">لا تظهرها مرة ثانية</string> + <string name="clear_queue_label">صففي لائحة الإستماع</string> + <string name="undo">تراجع</string> + <string name="move_to_top_label">ارفعه للأعلى</string> + <string name="move_to_bottom_label">أنزله للأدني</string> + <string name="sort">رتب</string> + <string name="keep_sorted">أبقها مرتبة</string> + <string name="date">تاريخ</string> + <string name="duration">طول</string> + <string name="episode_title">عنوان الحلقة</string> + <string name="feed_title">عنوان البودكاست</string> + <string name="random">عشوائي</string> + <string name="smart_shuffle">خلط ذكي</string> + <string name="ascending">تصاعدي</string> + <string name="descending">تنازلي</string> + <string name="clear_queue_confirmation_msg">نرجو تأكيد تصفية لائحة الإستماع من كل الحلقات فيها</string> + <string name="time_left_label">الوقت المتبقى:\u0020</string> + <!--Variable Speed--> + <string name="download_plugin_label">تنزيل الإضافة</string> + <string name="no_playback_plugin_title">الإضافة غير منصبة</string> + <string name="no_playback_plugin_or_sonic_msg">للتشغيل متغير السرعات نقترح تفعيل مشغل وسائط سونيك المضمن.</string> + <string name="enable_sonic">تفعيل سونيك</string> + <string name="speed_presets">إعدادات أولية</string> + <string name="preset_already_exists">%1$.2fx محفوظة كإعداد أولي.</string> + <!--Empty list labels--> + <string name="no_items_header_label">لا حلقات بلائحة الاستماع</string> + <string name="no_items_label">أضف حلقة بإختيار تنزيل, أو أضغط مطولا على الحلقة وأختر \"أضف الى لائحة الاستماع\".</string> + <string name="no_shownotes_label">الحلقة ليس مكتوب بها أي تفاصيل</string> + <string name="no_run_downloads_head_label">لا يوجد تنزيل جاري</string> + <string name="no_run_downloads_label">يمكنك تنزيل حلقات من صفحة تفاصيل البودكاست.</string> + <string name="no_comp_downloads_head_label">لا توجد حلقات منزلة</string> + <string name="no_comp_downloads_label">يمكنك تنزيل حلقات من صفحة تفاصيل البودكاست.</string> + <string name="no_log_downloads_head_label">لا سجل للتنزيلات</string> + <string name="no_log_downloads_label">ستظهر سجلات التنزيل هنا عند توفرها.</string> + <string name="no_history_head_label">لا يوجد سجل +</string> + <string name="no_history_label">ستظهر هنا الحلقة بعد الاستماع إليها.</string> + <string name="no_all_episodes_head_label">لا توجد حلقات</string> + <string name="no_all_episodes_label">ستظهر هنا الحلقة بعد إضافتها.</string> + <string name="no_new_episodes_head_label">لا توجد حلقات جديدة</string> + <string name="no_new_episodes_label">ستظهر هنا اللحلقات التى وصلت حديثا.</string> + <string name="no_fav_episodes_head_label">لا توجد حلقات مفضلة</string> + <string name="no_fav_episodes_label">يمكنك إضافة حلقات للمفضلات بالضغط عليها لمدة طويلة.</string> + <string name="no_chapters_head_label">لا توجد فصول</string> + <string name="no_chapters_label">هذة الحلقة لا تتضمن فصول</string> + <string name="no_subscriptions_head_label">لا توجد إشتراكات</string> + <string name="no_subscriptions_label">للاشتراك في بودكاست ، اضغط على أيقونة علامة الجمع أدناه.</string> + <!--Preferences--> + <string name="storage_pref">تخزين</string> + <string name="storage_sum">الحذف التلقائي والاستيراد والتصدير للحلقة</string> + <string name="project_pref">المشروع</string> + <string name="synchronization_pref">مزامنة</string> + <string name="synchronization_sum">قم بالمزامنة مع الأجهزة الأخرى باستخدام gpodder.net</string> + <string name="automation">التشغيل الآلي</string> + <string name="download_pref_details">تفاصيل</string> + <string name="import_export_pref">استيراد/تصدير</string> + <string name="import_export_search_keywords">لنسخ الاحتياطي و استرجاع</string> + <string name="appearance">المظهر</string> + <string name="external_elements">العوامل الخارجية + +</string> + <string name="interruptions">المقاطعات</string> + <string name="playback_control">تحكم التشغيل</string> + <string name="preference_search_hint">بحث</string> + <string name="preference_search_no_results">لا توجد نتائج</string> + <string name="preference_search_clear_history">مسح السجل</string> + <string name="media_player">مشغل وسائط</string> + <string name="pref_episode_cleanup_title">مسح تلقائى</string> + <string name="pref_pauseOnDisconnect_sum">توقيف التشغيل عند نزع سماعات الأذن او البلوتوث</string> + <string name="pref_unpauseOnHeadsetReconnect_sum">عاود التشغيل عند ايصال سماعات الأذن او البلوتوث</string> + <string name="pref_unpauseOnBluetoothReconnect_sum">عاود التشغيل عند إيصال سماعات البلوتوث</string> + <string name="pref_followQueue_sum">أذهب الى الحلقة التالية في لائحة الاستماع عندما ينتهي استماع السابقة. </string> + <string name="pref_auto_delete_sum">أمسح الحلقة عندما ينتهي تشغيلها</string> + <string name="pref_auto_delete_title">مسح تلقائي</string> + <string name="pref_smart_mark_as_played_sum">علم على الحلقات على أنها إنتهت حتى لو بقي أقل من مقدار ثوانٍ معين من وقت التشغيل</string> + <string name="pref_smart_mark_as_played_title">علم بذكاء أنها انتهت</string> + <string name="pref_skip_keeps_episodes_sum">تعليم الحلقة كمفضلة يبقيها على الجهاز</string> + <string name="pref_skip_keeps_episodes_title">الاحتفاظ بالحلقات التي تم تخطيها</string> + <string name="pref_favorite_keeps_episodes_sum">تعليم الحلقة كمفضلة يبقيها على الجهاز</string> + <string name="pref_favorite_keeps_episodes_title">إبقاء الحلقات المفضلة</string> + <string name="playback_pref">تشغيل</string> + <string name="playback_pref_sum">تحكم سماعة الأذن, وقت التقدم, لائحة الاستماع</string> + <string name="network_pref">شبكة</string> + <string name="network_pref_sum">الفاصل الزمني للتحديث، التحكم بالتنزيل ،شبكة الجوال</string> + <string name="pref_autoUpdateIntervallOrTime_title">الفاصل الزمنى ووقت التحديث</string> + <string name="pref_autoUpdateIntervallOrTime_sum">حدد فاصل زمنى أو وقت محدد لتحديث البودكاستات تلقائيا</string> + <string name="pref_autoUpdateIntervallOrTime_Disable">تعطيل</string> + <string name="pref_autoUpdateIntervallOrTime_Interval">تعيين الفاصل الزمني</string> + <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">ضبط الوقت من اليوم</string> + <string name="pref_followQueue_title">تشغيل مستمر</string> + <string name="pref_pauseOnHeadsetDisconnect_title">قطع اتصال سماعات الرأس أو البلوتوث</string> + <string name="pref_unpauseOnHeadsetReconnect_title">إعادة توصيل سماعات الرأس</string> + <string name="pref_unpauseOnBluetoothReconnect_title">إعادة توصيل البلوتوث</string> + <string name="pref_stream_over_download_title">أفضل البث</string> + <string name="pref_stream_over_download_sum">عرض زر البث بدلاً من زر التنزيل في القوائم.</string> + <string name="pref_mobileUpdate_title">التحديث على شبكة الجوال</string> + <string name="pref_mobileUpdate_sum">حدد ما يجب السماح به أثناء الاتصال على شبكة الجوال</string> + <string name="pref_mobileUpdate_refresh">تحديث البودكاست</string> + <string name="pref_mobileUpdate_images">صور الغلاف</string> + <string name="pref_mobileUpdate_auto_download">تنزيل تلقائي</string> + <string name="pref_mobileUpdate_episode_download">تحميل الحلقة</string> + <string name="pref_mobileUpdate_streaming">بث</string> + <string name="user_interface_label">واجهة الاستخدام</string> + <string name="user_interface_sum">المظهر, الإشتراكات, شاشة الغلق</string> + <string name="pref_set_theme_title">اختيار النمط</string> + <string name="pref_nav_drawer_items_title">حدد عناصر قائمة البرنامج</string> + <string name="pref_nav_drawer_items_sum">قم بتغيير العناصر التي تظهر في قائمة البرنامج.</string> + <string name="pref_nav_drawer_feed_order_title">حدد ترتيب الإشتراكات</string> + <string name="pref_nav_drawer_feed_order_sum">غير ترتيب إشتراكاتك</string> + <string name="pref_nav_drawer_feed_counter_title">حدد عداد الاشتراكات</string> + <string name="pref_nav_drawer_feed_counter_sum">غير المعلومات المعروضة بعداد الإشتراكات. إيضا يغير ترتيب الإشتراكات إذا كان ترتيبها يحدد بالـ\'العداد\'</string> + <string name="pref_set_theme_sum">تغيير مظهر AntennaPod.</string> + <string name="pref_automatic_download_title">تنزيل تلقائي</string> + <string name="pref_automatic_download_sum">حدد معطيات التحميل التلقائي للحلقات.</string> + <string name="pref_autodl_wifi_filter_title">تمكين اختيار شبكة الWi-Fi المستخدمة</string> + <string name="pref_autodl_wifi_filter_sum">أسمح بالتنزيل التلقائي فقط على شبكات الواي فاي المختارة.</string> + <string name="pref_automatic_download_on_battery_title">التنزيل عند عدم الشحن الجهاز</string> + <string name="pref_automatic_download_on_battery_sum">السماح بالتنزيل التلقائي عندما لا يتم شحن البطارية</string> + <string name="pref_parallel_downloads_title">التنزيلات المتوازية</string> + <string name="pref_episode_cache_title">تخزين الحلقات</string> + <string name="pref_episode_cache_summary">العدد الإجمالي للحلقات التي تم تنزيلها والمخزنة مؤقتًا على الجهاز. سيتم تعليق التنزيل التلقائي إذا تم الوصول إلى هذا الرقم.</string> + <string name="pref_episode_cover_title">استخدم صورة غلاف الحلقة</string> + <string name="pref_theme_title_use_system">استخدم نمط نظام التشغيل</string> + <string name="pref_theme_title_light">فاتح</string> + <string name="pref_theme_title_dark">داكن</string> + <string name="pref_theme_title_trueblack">اسود (ِلأجهزة AMOLED)</string> + <string name="pref_episode_cache_unlimited">غير محدود</string> + <string name="pref_update_interval_hours_plural">ساعات</string> + <string name="pref_update_interval_hours_singular">ساعة</string> + <string name="pref_update_interval_hours_manual">يدوى</string> + <string name="pref_gpodnet_authenticate_title">تسجيل الدخول</string> + <string name="pref_gpodnet_authenticate_sum">سجل دخول بحساب gpodder.net لتتزامن إشتراكاتك</string> + <string name="pref_gpodnet_logout_title">تسجيل خروج</string> + <string name="pref_gpodnet_logout_toast">نجح تسجيل الخروج</string> + <string name="pref_gpodnet_setlogin_information_title">تغيير بيانات تسجيل الدخول</string> + <string name="pref_gpodnet_setlogin_information_sum">قم بتغيير بيانات تسجيل الدخول لحساب gpodder.net الخاص بك.</string> + <string name="pref_gpodnet_sync_changes_title">قم بعملية التزامن الآن</string> + <string name="pref_gpodnet_sync_changes_sum">زامن الإشتراكات وتغير حالة الحلقات مع gpodder.net </string> + <string name="pref_gpodnet_full_sync_title">ابدأ مزامنة كاملة</string> + <string name="pref_gpodnet_full_sync_sum">زامن كل الإشتراكات وحالة الحلقات مع gpodder.net</string> + <string name="pref_gpodnet_login_status"><![CDATA[تم الدخول بسم <i>%1$s</i> بجهاز <i>%2$s</i>]]></string> + <string name="pref_playback_speed_sum">حدد السرعات المتوفرة في التشغيل متعدد السرعات</string> + <string name="pref_feed_playback_speed_sum">سرعة التشغيل الصوتي عند بدء تشغيل حلقات من هذا البودكاست </string> + <string name="pref_feed_skip">تخطي تلقائي</string> + <string name="pref_feed_skip_sum">تخطى المقدمة والمؤخرة</string> + <string name="pref_feed_skip_ending">تخطى الآخر</string> + <string name="pref_feed_skip_intro">تخطى الأول</string> + <string name="pref_feed_skip_ending_toast">تخطى آخر %d ثانية</string> + <string name="pref_feed_skip_intro_toast">تخطى أول %d ثانية</string> + <string name="pref_playback_time_respects_speed_title">عدل بيانات الوسائط مع سرعة التشغيل</string> + <string name="pref_playback_time_respects_speed_sum">الوقت المنقضي والمتبقي يتناسب مع سرع التشغيل</string> + <string name="pref_fast_forward">وقت التخطي السريع</string> + <string name="pref_fast_forward_sum">قم بتخصيص عدد الثواني للانتقال إلى الأمام عند النقر فوق زر التقديم السريع</string> + <string name="pref_rewind">وقت التخطى للخلف</string> + <string name="pref_rewind_sum">قم بتخصيص عدد الثواني للانتقال للخلف عند النقر فوق زر الإرجاع</string> + <string name="pref_expandNotify_title">أولوية عالية للإشعار</string> + <string name="pref_expandNotify_sum">هذه عادة توسع الإشعار ليظهر أزرار التشغيل.</string> + <string name="pref_persistNotify_title">تحكم التشغيل ظاهر دوما</string> + <string name="pref_persistNotify_sum">أبقي تحكم التشغيل في الإشعارات وشاشة القفل عند التوقف.</string> + <string name="pref_compact_notification_buttons_title">تعيين أزرار الإشعار المضغوط</string> + <string name="pref_compact_notification_buttons_sum">غير أزرار التشغيل عند إزالة الإشعار. زري تشغيل/إيقاف سيكونا دوما ظاهران.</string> + <string name="pref_lockscreen_background_title">تعيين خلفية شاشة القفل</string> + <string name="pref_lockscreen_background_sum">اضبط خلفية شاشة القفل على صورة الحلقة الحالية. كأثر جانبي ، سيعرض هذا أيضًا الصورة في تطبيقات الطرف الثالث.</string> + <string name="pref_expand_notify_unsupport_toast">إصدارات Android قبل 4.1 لا تدعم الإشعارات الموسعة.</string> + <string name="pref_enqueue_location_title">مكان الـ Enqueue</string> + <string name="enqueue_location_back">خلف</string> + <string name="enqueue_location_front">أمام</string> + <string name="enqueue_location_after_current">بعد الحلقة الحالية</string> + <string name="pref_smart_mark_as_played_disabled">معطل</string> + <string name="pref_image_cache_size_title">حجم ذاكرة التخزين المؤقت للصور</string> + <string name="pref_image_cache_size_sum">حجم ذاكرة التخزين المؤقت على القرص للصور.</string> + <string name="visit_user_forum">منتدى المستخدم</string> + <string name="bug_report_title">بلغ عن خطأ بالتطبيق</string> + <string name="open_bug_tracker">إفتح نظام تتبع الأخطاء</string> + <string name="export_logs">تصدير السجلات</string> + <string name="copy_to_clipboard">إنسخ لذاكرة القصاصات</string> + <string name="copied_to_clipboard">تم النسخ لذاكرة القصاصات</string> + <string name="experimental_pref">تجريبي</string> + <string name="pref_media_player_message">حدد مشغل الوسائط الذي تريد استخدامه لتشغيل الملفات</string> + <string name="pref_proxy_title">خادم بروكسى</string> + <string name="pref_proxy_sum">حدد خادم وكيل</string> + <string name="pref_no_browser_found">لم يتم العثور على متصفح ويب.</string> + <string name="pref_cast_title">دعم Chromecast</string> + <string name="pref_cast_message_play_flavor">فعل تحكم التشغيل بأجهزة الكاست (مثل كرومكاست, سماعات الصوت, وتلفزيون اندرويد)</string> + <string name="pref_cast_message_free_flavor">يتطلب Chromecast مكتبات برمجية مملوكة لجهات خارجية معطلة في هذا الإصدار من AntennaPod</string> + <string name="pref_enqueue_downloaded_title">الـ Enqueue المنزلة</string> + <string name="pref_enqueue_downloaded_summary">أضف الحلقات المنزلة الى لائحة الاستماع</string> + <string name="media_player_exoplayer_recommended">ExoPlayer (موصى به)</string> + <string name="media_player_switch_to_exoplayer">قم بالتبديل إلى ExoPlayer</string> + <string name="media_player_switched_to_exoplayer">تم بالتبديل إلى ExoPlayer</string> + <string name="pref_skip_silence_title">تخطي الصمت في الصوت</string> + <string name="pref_videoBehavior_title">عند الخروج من الفيديو</string> + <string name="pref_videoBehavior_sum">التصرف عند ترك تشغيل فيديو</string> + <string name="stop_playback">اوقف التشغيل</string> + <string name="continue_playback">استمر في تشغيل الصوت</string> + <string name="pref_delete_removes_from_queue_title">المسح يزيل الحلقة من لائحة الاستماع</string> + <string name="pref_delete_removes_from_queue_sum">أزل الحلقة من لائحة الاستماع آليا عند مسحها.</string> + <string name="pref_filter_feed_title">مصفاة الإشتراكات</string> + <string name="pref_filter_feed_sum">صفي إشتراكاتك في درج الملاحة وشاشة الإشتراكات.</string> + <string name="subscriptions_are_filtered">الإشتراكات مصففاة</string> + <!--About screen--> + <!--Search--> + <!--Synchronization--> + <string name="sync_status_subscriptions">تتزامن الإشتراكات الآن...</string> + <string name="sync_status_success">نجحت المزامنة</string> + <string name="sync_status_error">فشلت المزامنة</string> + <!--import and export--> + <string name="import_export_summary">نقل الاشتراكات ولائحة الاستماع الى جهاز آخر</string> + <string name="html_export_summary">عرض إشتراكاتك لصديق</string> + <string name="opml_export_summary">نقل إشتراكاتك لبرنامج بوكاست آخر</string> + <string name="opml_import_summary">إستيراد اشتراكاتك من برنامج آخر</string> + <string name="database_export_summary">نقل الاشتراكات والحلقات المسموعة ولائحة الإستماع لـ AntennaPod على جهاز آخر</string> + <string name="opml_import_error_no_file">لم يتم اختيار أي ملف</string> + <string name="select_all_label">اختر الكل</string> + <string name="deselect_all_label">ألغ اختيار الكل</string> + <string name="opml_export_label">تصدير بصيغة OPML</string> + <string name="html_export_label">تصدير بصيغة HTML</string> + <string name="database_export_label">تصدير قاعدة البيانات</string> + <string name="database_import_label">استيراد قاعدة البيانات</string> + <string name="database_import_warning">استيراد قاعدة بيانات سيمسح كل اشتراكاتك الحالية وسجل الاستماع. الأفضل أن تصدر قاعدة البيانات الحالية لأرشيف. هل تريد تبديل البيانات؟</string> + <string name="export_error_label">حدث خطأ أثناء التصدير</string> + <string name="export_success_title">تم التصدير بنجاح</string> + <string name="opml_import_ask_read_permission">الوصول الى مساحة التخزين الخارجية مطلوب لقراءة ملف الـ OPML</string> + <string name="favorites_export_label">تصدير الحلقات المفضلة</string> + <string name="favorites_export_summary">تصدير الحلقات المفضلة لملف</string> + <!--Sleep timer--> + <string name="time_seconds">ثواني</string> + <string name="time_minutes">دقائق</string> + <string name="time_hours">ساعات</string> + <plurals name="time_seconds_quantified"> + <item quantity="zero">%d ثانية</item> + <item quantity="one">1 ثانية</item> + <item quantity="two">%d ثانيتان</item> + <item quantity="few">%d ثواني</item> + <item quantity="many">%d ثواني</item> + <item quantity="other">%d ثواني</item> + </plurals> + <plurals name="time_minutes_quantified"> + <item quantity="zero">%d دقيقة</item> + <item quantity="one">1 دقيقة</item> + <item quantity="two">%d دقيقتان</item> + <item quantity="few">%d دقائق</item> + <item quantity="many">%d دقائق</item> + <item quantity="other">%d دقائق</item> + </plurals> + <plurals name="time_hours_quantified"> + <item quantity="zero">%d ساعة</item> + <item quantity="one">1 ساعة</item> + <item quantity="two">%d ساعتان</item> + <item quantity="few">%d ساعة</item> + <item quantity="many">%d ساعة</item> + <item quantity="other">%d ساعة</item> + </plurals> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">الفئات</string> + <string name="gpodnet_toplist_header">أقوى البودكاستات</string> + <string name="gpodnet_suggestions_header">إقتراحات</string> + <string name="gpodnetauth_login_title">تسجيل الدخول</string> + <string name="gpodnetauth_login_butLabel">تسجيل الدخول</string> + <string name="username_label">إسم المستخدم</string> + <string name="password_label">كلمة المرور</string> + <string name="gpodnetauth_device_butChoose">إختر</string> + <string name="gpodnetauth_finish_descr">تهانينا! حسابك في gpodder.net مربوط الآن مغ جهازك. سيزامن AntennaPod من الآن وصاعدا إشتراكاتك على جهازك مع حسابك على gpodder.net.</string> + <string name="gpodnetauth_finish_butsyncnow">ابدأ المزامنة الآن</string> + <string name="gpodnetauth_finish_butgomainscreen">إذهب إلى الصفحة الرئيسية</string> + <string name="gpodnetsync_auth_error_descr">خطأ في إسم المستخدم أو كلمة المرور</string> + <string name="gpodnetsync_pref_report_successful">تم بنجاح</string> + <!--Directory chooser--> + <string name="selected_folder_label">إختيار المستند:</string> + <string name="create_folder_label">أنشأ مستندا جديدا</string> + <string name="choose_data_directory">إختيار مستند البيانات</string> + <string name="choose_data_directory_permission_rationale">الوصول الى مساحة التخزين الخارجية مطلوب لتغيير مجلد البيانات</string> + <string name="pref_pausePlaybackForFocusLoss_sum">توقف التشغيل بدل اخفات الصوت عندما برنامج يشغل صوت</string> + <string name="pref_resumeAfterCall_sum">واصل التشغيل عندما تنتهى مكالمة هاتفية</string> + <!--Online feed view--> + <!--Content descriptions for image buttons--> + <string name="rewind_label">التخطى للخلف</string> + <string name="fast_forward_label">التقدم السريع</string> + <string name="media_type_audio_label">صوت</string> + <string name="media_type_video_label">فيديو</string> + <string name="in_queue_label">الحلقة موجودة في لائحة الاستماع</string> + <string name="is_favorite_label">الحلقة علمت كمفضلة</string> + <string name="load_next_page_label">تحميل الصفحة التالية</string> + <!--Feed information screen--> + <string name="authentication_label">تسجيل الدخول</string> + <string name="auto_download_settings_label">إعدادات التنزيل التلقائي</string> + <string name="auto_download_disabled_globally">التنزيل التلقائي معطل في الإعدادات الرئيسية لـ AntennaPod</string> + <!--Progress information--> + <!--AntennaPodSP--> + <!--Add podcast fragment--> + <!--Local feeds--> + <!--Episodes apply actions--> + <string name="all_label">الكل</string> + <string name="selected_all_label">إختيار كل الحلقات</string> + <string name="unplayed_label">لم يتم تشغيله</string> + <string name="downloaded_label">تم التنزيل</string> + <string name="not_downloaded_label">لم يتم التنزيل</string> + <string name="selected_queued_label">حلقات مختارة ضمن لائحة الاستماع</string> + <string name="selected_not_queued_label">حلقات مختارة ليست ضمن لائحة الاستماع</string> + <string name="hide_is_favorite_label">في المفضلة</string> + <string name="not_favorite">ليست مفضلة</string> + <string name="hide_downloaded_episodes_label">تم التنزيل</string> + <string name="hide_not_downloaded_episodes_label">لم يتم التنزيل</string> + <string name="queued_label">ضمن لائحة الاستماع</string> + <string name="not_queued_label">ليست ضمن لائحة الاستماع</string> + <string name="hide_paused_episodes_label">ايقاف مؤقت</string> + <!--Sort--> + <!--Rating dialog--> + <!--Share episode dialog--> + <string name="share_playback_position_dialog_label">موضع التشغيل</string> + <!--Audio controls--> + <string name="playback_speed">سرعة التشغيل</string> + <string name="volume">مستوى الصوت</string> + <string name="left_short">يسار</string> + <string name="right_short">يمين</string> + <string name="audio_effects">تأثيرات صوتية</string> + <string name="stereo_to_mono">تحويل: ثنائي القناة الى أحادي</string> + <string name="sonic_only">سونيك فقط</string> + <string name="exoplayer_only">اكسوبلير فقط</string> + <!--proxy settings--> + <string name="proxy_type_label">النوع</string> + <string name="host_label">خادم</string> + <string name="port_label">رقم المنفذ</string> + <string name="optional_hint">(أختياري)</string> + <string name="proxy_test_label">اختبار</string> + <string name="proxy_checking">تفحص...</string> + <!--Subscriptions fragment--> + <!--Casting--> + <string name="cast_failed_to_play">فشل بدء تشغيل وسائط</string> + <string name="cast_failed_to_stop">فشل ايقاف تشغيل وسائط</string> + <string name="cast_failed_to_pause">فشل توقف تشغيل وسائط</string> + <!--Notification channels--> + <string name="notification_channel_downloading">تحميل</string> + <string name="notification_channel_playing">يشغل حاليا</string> + <string name="notification_channel_playing_description">تسمح بتحكم التشغيل. هذا الاشعار الرئيسي الذي تراه عند تشغيل البودكاست.</string> + <string name="notification_channel_sync_error_description">أظهر عندما تفشل مزامنة gpodder.</string> + <!--Widget settings--> + <string name="widget_settings">إعدادات أداة الشاشة</string> + <!--On-Demand configuration--> +</resources> diff --git a/core/src/main/res/values-br/strings.xml b/core/src/main/res/values-br/strings.xml index 3ff5df24f..8e9abc191 100644 --- a/core/src/main/res/values-br/strings.xml +++ b/core/src/main/res/values-br/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Stadegoù</string> <string name="add_feed_label">Ouzhpennañ ur podskignad</string> <string name="episodes_label">Rannoù</string> + <string name="queue_label">Lost</string> <string name="all_episodes_short_label">An holl</string> <string name="new_episodes_label">Nevez</string> <string name="favorite_episodes_label">Sinedoù</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">Kerzhlevr</string> <string name="subscriptions_label">Koumanantoù</string> <string name="subscriptions_list_label">Roll ar c\'houmanantoù</string> - <string name="cancel_download_label">Nullañ ar pellgargadennoù</string> <string name="playback_history_label">Roll istor seniñ</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">Titouroù kennaskañ gpodder.net</string> @@ -25,6 +25,7 @@ <string name="episode_cache_full_message">Tizhet eo bet bevenn pellgargadurioù ar rannoù. Gallout a rit kreskaat ment ar c\'hrubuilh en arventennoù.</string> <string name="playback_statistics_label">Lenn</string> <string name="download_statistics_label">Pellgargadurioù</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="statistics_details_dialog">%1$d war %2$d rann kroget.\n\nLennet %3$s war %4$s.</string> <string name="statistics_mode">Doareoù stadegoù</string> @@ -75,7 +76,6 @@ <string name="description_label">Deskrivadur</string> <string name="episodes_suffix">\u0020rannoù</string> <string name="processing_label">O keweriañ</string> - <string name="save_username_password_label">Enrollañ ho titouroù kennaskañ</string> <string name="close_label">Serriñ</string> <string name="retry_label">Klask en-dro</string> <string name="auto_download_label">Pellgargañ emgefreek</string> @@ -87,7 +87,6 @@ <string name="feed_volume_reduction_off">hini ebet</string> <string name="feed_volume_reduction_light">izel</string> <string name="feed_volume_reduction_heavy">pounner</string> - <string name="parallel_downloads_suffix">\u0020pellgargadurioù kenstur</string> <string name="feed_auto_download_global">Dre ziouer</string> <string name="feed_auto_download_always">Bepred</string> <string name="feed_auto_download_never">Morse</string> @@ -143,7 +142,6 @@ <string name="hide_not_queued_episodes_label">N\'emañ ket el lost</string> <string name="hide_has_media_label">Gant ur media</string> <string name="filtered_label">Silet</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} C\'hwitet war an azgrenaat diwezhañ</string> <string name="open_podcast">Digeriñ ar podskignad</string> <string name="please_wait_for_data">Gortozit dibenn pellgargadur ar roadennoù</string> <!--actions on feeditems--> @@ -219,16 +217,12 @@ <string name="download_error_details">Munudoù</string> <string name="download_error_details_message">%1$s \n\nURL ar restr:\n%2$s</string> <string name="download_error_device_not_found">N\'eo ket bet kavet ar c\'hadaviñ</string> - <string name="download_error_insufficient_space">N\'eus ket trawalc\'h a blas</string> <string name="download_error_http_data_error">Fazi roadennoù HTTP</string> <string name="download_error_error_unknown">Fazi dianav</string> - <string name="download_error_parser_exception">Kemennadenn Fazi</string> <string name="download_error_unsupported_type">Doare lanv anskor</string> <string name="download_error_connection_error">Fazi kennaskañ</string> - <string name="download_error_unknown_host">Ostiz dianav</string> <string name="download_error_unauthorized">Fazi dilesa</string> <string name="download_error_file_type_type">Fazi rizh ar restr</string> - <string name="download_error_forbidden">Difennet</string> <string name="download_canceled_msg">Pellgargadur nullet</string> <string name="download_canceled_autodownload_enabled_msg">Pellgargadur nullet\nDiweredekaet ar <i>pellgargadur emgefreek</i> evit an elfenn-mañ</string> <string name="download_report_title">Pellgargañ echuet gant fazi(où)</string> @@ -245,7 +239,6 @@ <item quantity="many">%d a bellgargadurioù a chom</item> <item quantity="other">%d pellgargadur a chom</item> </plurals> - <string name="downloads_processing">O keweriañ ar pellgargadennoù</string> <string name="download_notification_title">O pellgargañ roadennoù ar podskignad</string> <string name="download_log_title_unknown">Titl dianav</string> <string name="download_type_feed">Lanv</string> @@ -331,7 +324,6 @@ <string name="storage_pref">Kadaviñ</string> <string name="storage_sum">Dilemel emgefreek ar rannoù, Enporzhiañ, Ezporzhiañ</string> <string name="project_pref">Raktres</string> - <string name="queue_label">Lost</string> <string name="synchronization_pref">Goubredañ</string> <string name="synchronization_sum">Ober gant gpodder.net evit goubredañ gant binviji all</string> <string name="automation">Emgefreekañ</string> @@ -347,14 +339,9 @@ <string name="preference_search_clear_history">Skarzhañ ar roll istor</string> <string name="media_player">Lenner liesvedia</string> <string name="pref_episode_cleanup_title">Naetaat ar rannoù</string> - <string name="pref_episode_cleanup_summary">Ar rannoù n\'int ket el lost nag er sinedoù a c\'hall bezañ dilamet ma vez ezhomm muioc\'h a egor dieub gant ar pellgargañ emgefreek.</string> <string name="pref_pauseOnDisconnect_sum">Paouez gant al lenn pa vez diluget selaouelloù pe bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Kenderc\'hel al lenn pa vez adluget ar selaouelloù</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Adstagañ gant al lenn pa vez adkennasket ar bluetooth</string> - <string name="pref_hardwareForwardButtonSkips_title">An afell \'war-raok\' a dremen ar rann</string> - <string name="pref_hardwareForwardButtonSkips_sum">Tremen d\'ar rann da-heul kentoc\'h eget ober ul lamm en a-raok pa vez pouezet war \'lamm en a-raok\' war ur benveg bluetooth</string> - <string name="pref_hardwarePreviousButtonRestarts_title">An afell \'lamm a-dreñv\' a adloc\'h ar rann</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Adloc\'hañ adalek ar penn-kentañ pa vez pouezet war un afell \'lamm a-dreñv\' kentoc\'h eget mont war-gil</string> <string name="pref_followQueue_sum">Tremen d\'ar rann goude ur wech echuet gant unan</string> <string name="pref_auto_delete_sum">Dilemel ar rann p\'eo echuet gant al lenn</string> <string name="pref_auto_delete_title">Dilemel ent emgefreek</string> @@ -374,7 +361,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Diweredekaat</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Dibab un etremez</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Dibab un eur</string> - <string name="pref_autoUpdateIntervallOrTime_every">bep %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">da %1$s</string> <string name="pref_followQueue_title">Lenn kendalc\'hus</string> <string name="pref_unpauseOnHeadsetReconnect_title">Lugañ ar selaouelloù</string> @@ -407,7 +393,6 @@ <string name="pref_episode_cache_title">Niver a rannoù enrollet</string> <string name="pref_episode_cache_summary">Niver hollek a rannoù pellgarget lakaet e krubuilh ar benveg. Diweredekaet e vo ar pellgargañ emgefreek mard eo tizhet an niver-mañ.</string> <string name="pref_episode_cover_title">Skeudenn ar rannoù</string> - <string name="pref_episode_cover_summary">Ober gant golo ar rann pa vez dioutañ. Mard eo digevasket e vo graet gant golo ar podskignad.</string> <string name="pref_theme_title_use_system">Neuz ar reizhiad</string> <string name="pref_theme_title_light">Sklaer</string> <string name="pref_theme_title_dark">Teñval</string> @@ -427,7 +412,6 @@ <string name="pref_gpodnet_full_sync_title">Rediañ ur goubredañ klok</string> <string name="pref_gpodnet_full_sync_sum">Goubredañ an holl goumanantoù ha stadoù ar rannoù gant gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Kennasket evel <i>%1$s</i> gant ar benveg <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_sum">An arventenn-mañ na vez ket arloet d\'ar rebuzadurioù fazi.</string> <string name="pref_feed_playback_speed_sum">Tizh dre ziouer ar rannoù</string> <string name="pref_feed_skip">Lamm emgefreek</string> <string name="pref_feed_skip_sum">Tremen penn-kentañ ha dibenn ar rannoù</string> @@ -441,8 +425,6 @@ <string name="pref_fast_forward_sum">Personelaat an niver a eilennoù da lammat war-raok pa vez pouezet war an afell war-raok.</string> <string name="pref_rewind">Padelezh al lamm a-dreñv</string> <string name="pref_rewind_sum">Dibabit ar c\'hementad a eilennoù da lammat an a-dreñv pa vez pouezet war \'lamm a-dreñv\'</string> - <string name="pref_gpodnet_sethostname_title">Dibab un anv ostiz</string> - <string name="pref_gpodnet_sethostname_use_default_host">Ober gant an ostiz dre ziouer</string> <string name="pref_expandNotify_title">Tevet rebuzadurioù a live uhel</string> <string name="pref_expandNotify_sum">Sañset e brasa ar rebuzadur evit diskouez an afelloù lenn.</string> <string name="pref_persistNotify_title">Afelloù lenn peurzalc\'hus</string> @@ -451,8 +433,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Gallout a rit dibab %1$d elfenn d\'ar muiañ.</string> <string name="pref_lockscreen_background_title">Kemmañ skramm-prennañ an drekleur</string> <string name="pref_lockscreen_background_sum">Lakaat skeudenn ar rann e plas skeudenn drekleur ar skramm-prennañ. Diskouez a raio ivez ar skeudenn en arloadoù all.</string> - <string name="pref_showDownloadReport_sum">Ma c\'hwit ar pellgargadurioù, sevel un danevell a ziskouez munudoù ar c\'hwitadenn.</string> - <string name="pref_showAutoDownloadReport_sum">Diskouez ur rebuzadur evit ar rannoù pellgarget ent emgefreek.</string> <string name="pref_expand_notify_unsupport_toast">Handelvoù Android a-raok 4.1 na skoront ket ar rebuzadurioù astennet.</string> <string name="pref_enqueue_location_title">Lec\'hiadur ar rannoù el lost</string> <string name="pref_enqueue_location_sum">Ouzhpennañ ar rannoù da: %1$s</string> @@ -473,14 +453,12 @@ <string name="pref_current_value">Talvoud bremanel: %1$s</string> <string name="pref_proxy_title">Proksi</string> <string name="pref_proxy_sum">Arventennañ ur rouedad proksi</string> - <string name="pref_faq">Foar ar Goulennoù</string> <string name="pref_no_browser_found">N\'eus bet kavet merdeer ebet.</string> <string name="pref_cast_title">Skor Chromecast</string> <string name="pref_cast_message_play_flavor">Gweredekaat al lenn a-bell war ar binviji Cast (evel ChromeCast, Audio Speaker pe Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast a c\'houlenn levraouegoù diavaez a zo diweredekaet en handelv-mañ eus AntennaPod</string> <string name="pref_enqueue_downloaded_title">Ouzhpennañ el lost ur wech pellgarget</string> <string name="pref_enqueue_downloaded_summary">Ouzhpennañ ar rannoù pellgarget el lost</string> - <string name="media_player_builtin">Lenner genidik Android</string> <string name="media_player_switch_to_exoplayer">Ober gant ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Kemmet eo bet al lenner evit ExoPlayer</string> <string name="pref_skip_silence_title">Tremen ar mareoù didrouz en aodio</string> @@ -592,22 +570,12 @@ <string name="gpodnet_suggestions_header">ALIOÙ</string> <string name="gpodnet_search_hint">Klask war gpodder.ner</string> <string name="gpodnetauth_login_title">Kennaskañ</string> - <string name="gpodnetauth_login_descr">Donemat war an hentenn kennaskañ gpodder.net. Da gentañ penn, biziatait ho titouroù kennaskañ:</string> <string name="gpodnetauth_login_butLabel">Kennaskañ</string> - <string name="gpodnetauth_login_register">Ma n\'ho peus ket a gont c\'hoazh e c\'hallit krouiñ unan amañ:\nhttps://gpodder.net/register/</string> <string name="username_label">Anv arveriad</string> <string name="password_label">Ger-tremen</string> - <string name="gpodnetauth_device_title">Dibab ar benveg</string> <string name="gpodnetauth_device_descr">Krouiñ ur benveg nevez evit ho kont gpodder.net pe dibabit ur benveg a zo anezhañ:</string> - <string name="gpodnetauth_device_deviceID">Naoudi ar benveg:\u0020</string> - <string name="gpodnetauth_device_caption">Alc\'hwez</string> - <string name="gpodnetauth_device_butCreateNewDevice">Krouiñ ur benveg nevez</string> - <string name="gpodnetauth_device_chooseExistingDevice">Dibab ur benveg a zo anezhañ:</string> - <string name="gpodnetauth_device_errorEmpty">Ret eo deoc\'h leuniañ naoudi ar benveg</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Implijet eo an naoudi-se endeo</string> <string name="gpodnetauth_device_caption_errorEmpty">An anv n\'hall ket bezañ goullo</string> <string name="gpodnetauth_device_butChoose">Dibab</string> - <string name="gpodnetauth_finish_title">Kennasket gant berzh!</string> <string name="gpodnetauth_finish_descr">Gourc\'hemennoù! Liammet eo ho kont gpodder.net gant ho penveg. Goubredet e vo ent emgefreek ar c\'houmanantoù war ho penveg gant ho kont gpodder.net.</string> <string name="gpodnetauth_finish_butsyncnow">Kregiñ gant ar goubredañ bremañ </string> <string name="gpodnetauth_finish_butgomainscreen">Mont d\'ar skramm degemer</string> @@ -775,8 +743,6 @@ <string name="notification_channel_downloading_description">Diskouezet eo pa vez o pellgargañ.</string> <string name="notification_channel_playing">O lenn</string> <string name="notification_channel_playing_description">Evit reoliañ al lenn. Ar rebuzadur pennañ an hini eo pa lennit ur podskignat.</string> - <string name="notification_channel_error">Fazioù</string> - <string name="notification_channel_auto_download">Pellgargañ emgefreek</string> <string name="notification_channel_episode_auto_download">Diskouezet eo pa vez pellgarget rannoù ent emgefreek</string> <!--Widget settings--> <string name="widget_settings">Gwellvezioù ar widjet</string> diff --git a/core/src/main/res/values-ca/strings.xml b/core/src/main/res/values-ca/strings.xml index 616efedba..d1a602186 100644 --- a/core/src/main/res/values-ca/strings.xml +++ b/core/src/main/res/values-ca/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Estadístiques</string> <string name="add_feed_label">Afegeix podcast</string> <string name="episodes_label">Episodis</string> + <string name="queue_label">Cua</string> <string name="all_episodes_short_label">Tot</string> <string name="new_episodes_label">Nou</string> <string name="favorite_episodes_label">Preferits</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">Registre</string> <string name="subscriptions_label">Subscripcions</string> <string name="subscriptions_list_label">Llista de subscripcions</string> - <string name="cancel_download_label">Cancel·la\nBaixada</string> <string name="playback_history_label">Historial de reproducció</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">Inici de sessió a gpodder.net</string> @@ -25,6 +25,7 @@ <string name="episode_cache_full_message">S\'ha arribat al límit de la memòria cau d\'episodis. Pots incrementar-ne la capacitat a la configuració.</string> <string name="playback_statistics_label">Reproducció</string> <string name="download_statistics_label">Baixades</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="statistics_details_dialog">%1$d de %2$depisodis començats.\n\nReproduïts %3$s de %4$s.</string> <string name="statistics_mode">Mode d\'estadístiques</string> @@ -75,7 +76,6 @@ <string name="description_label">Descripció</string> <string name="episodes_suffix">\u0020episodis</string> <string name="processing_label">S\'està processant</string> - <string name="save_username_password_label">Desa nom d\'usuari i contrasenya</string> <string name="close_label">Tanca</string> <string name="retry_label">Reintenta</string> <string name="auto_download_label">Inclou a baixades automàtiques</string> @@ -87,7 +87,6 @@ <string name="feed_volume_reduction_off">Off</string> <string name="feed_volume_reduction_light">Lleuger</string> <string name="feed_volume_reduction_heavy">Fort</string> - <string name="parallel_downloads_suffix">\u0020baixades en paral·lel</string> <string name="feed_auto_download_global">Valor predeterminat global</string> <string name="feed_auto_download_always">Sempre</string> <string name="feed_auto_download_never">Mai</string> @@ -134,7 +133,6 @@ <string name="hide_not_queued_episodes_label">No en cua</string> <string name="hide_has_media_label">Conté medis</string> <string name="filtered_label">Filtrat</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Darrera actualització fallida</string> <string name="open_podcast">Obrir podcast</string> <string name="please_wait_for_data">Per favor, espera fins a que les dades estiguen carregades</string> <!--actions on feeditems--> @@ -195,16 +193,12 @@ <string name="download_error_details">Detalls</string> <string name="download_error_details_message">%1$s \n\nURL del fitxer:\n%2$s</string> <string name="download_error_device_not_found">No s\'ha trobat cap dispositiu d\'emmagatzemament</string> - <string name="download_error_insufficient_space">No hi ha prou espai</string> <string name="download_error_http_data_error">Error de dades HTTP</string> <string name="download_error_error_unknown">Error desconegut</string> - <string name="download_error_parser_exception">Error de l\'analitzador</string> <string name="download_error_unsupported_type">Tipus de canal no suportat</string> <string name="download_error_connection_error">Error de connexió</string> - <string name="download_error_unknown_host">Amfitrió desconegut</string> <string name="download_error_unauthorized">Error d\'autenticació</string> <string name="download_error_file_type_type">Error de tipus de fitxer</string> - <string name="download_error_forbidden">Prohibit</string> <string name="download_canceled_msg">S\'ha cancel·lat la baixada</string> <string name="download_canceled_autodownload_enabled_msg">Baixada cancel·lada\nDesactivada les <i>baixades automàtiques</i> per aquest element</string> <string name="download_report_title">Baixades completades amb error(s)</string> @@ -218,7 +212,6 @@ <item quantity="one">%d baixada pendent</item> <item quantity="other">%d baixades pendents</item> </plurals> - <string name="downloads_processing">S\'estan processant les baixades</string> <string name="download_notification_title">S\'estan baixant les dades del podcast</string> <string name="download_log_title_unknown">Títol desconegut</string> <string name="download_type_feed">Canal</string> @@ -304,7 +297,6 @@ <string name="storage_pref">Emmagatzematge</string> <string name="storage_sum">Auto-esborrat d\'episodis, Importar, Exportar</string> <string name="project_pref">Projecte</string> - <string name="queue_label">Cua</string> <string name="synchronization_pref">Sincronització</string> <string name="synchronization_sum">Sincronitza amb altres dispositius usant gpodder.net</string> <string name="automation">Automatització</string> @@ -320,14 +312,9 @@ <string name="preference_search_clear_history">Esborra l\'historial</string> <string name="media_player">Reproductor multimèdia</string> <string name="pref_episode_cleanup_title">Neteja l\'episodi</string> - <string name="pref_episode_cleanup_summary">Els episodis que no es troben a la cua i no són preferits seran candidats a ser suprimits si l\'Auto Descàrrega necessita espai per a nous episodis</string> <string name="pref_pauseOnDisconnect_sum">Pausa la reproducció en desconnectar els auriculars o el bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Continua la reproducció en connectar novament els auriculars</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Continua la reproducció en connectar novament el bluetooth</string> - <string name="pref_hardwareForwardButtonSkips_title">Endavant per saltar</string> - <string name="pref_hardwareForwardButtonSkips_sum">En prémer el botó d\'avançada en un dispositiu bluetooth bota al següent episodi en lloc d\'avançar.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Endarrere per reiniciar</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">En prémer un botó físic, reinicieu l\'episodi actual en lloc de rebobinar-lo</string> <string name="pref_followQueue_sum">Salta al següent element de la cua en acabar la reproducció</string> <string name="pref_auto_delete_sum">Suprimeix l\'episodi quan s\'acabi de reproduir</string> <string name="pref_auto_delete_title">Esborrat automàtic</string> @@ -347,7 +334,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Desactivar</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Establir interval</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Establir hora del dia</string> - <string name="pref_autoUpdateIntervallOrTime_every">cada %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">als %1$s</string> <string name="pref_followQueue_title">Reproducció continuada</string> <string name="pref_unpauseOnHeadsetReconnect_title">Connexió d\'auriculars</string> @@ -380,7 +366,6 @@ <string name="pref_episode_cache_title">Memòria cau d\'episodis</string> <string name="pref_episode_cache_summary">Nombre total d\'episodis baixats al dispositiu. La baixada automàtica serà suspesa si s\'arriba a aquest nombre.</string> <string name="pref_episode_cover_title">Usa la coberta de l\'episodi</string> - <string name="pref_episode_cover_summary">Usa la coberta específica de l\'episodi quan siga possible. Si no es marca aquesta opció s\'usarà sempre la imatge del podcast com a coberta.</string> <string name="pref_theme_title_use_system">Usa el tema del sistema</string> <string name="pref_theme_title_light">Clar</string> <string name="pref_theme_title_dark">Fosc</string> @@ -400,7 +385,6 @@ <string name="pref_gpodnet_full_sync_title">Força sincronització completa</string> <string name="pref_gpodnet_full_sync_sum">Sincronitza amb gpodder.net totes les subscripcions i els estats dels episodis.</string> <string name="pref_gpodnet_login_status"><![CDATA[Connectat com a <i>%1$s</i> amb el dispositiu <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_sum">Aquest paràmetre no s\'aplica als errors d\'autenticació. </string> <string name="pref_feed_playback_speed_sum">La velocitat a usar quan comence la reproducció per a episodis en aquest podcast</string> <string name="pref_feed_skip">Auto Ometre</string> <string name="pref_feed_skip_sum">Omet introduccions i crèdits finals</string> @@ -414,8 +398,6 @@ <string name="pref_fast_forward_sum">Personalitzar el nombre de segons del salt endavant quan es prem el botó d\'Avanç ràpid.</string> <string name="pref_rewind">Temps de salt del Rebobinat</string> <string name="pref_rewind_sum">Personalitza el nombre de segons del salt endarrere quan es prem el botó de Rebobinat</string> - <string name="pref_gpodnet_sethostname_title">Definex nom del servidor</string> - <string name="pref_gpodnet_sethostname_use_default_host">Utilitza el servidor per defecte</string> <string name="pref_expandNotify_title">Alta prioritat de notificacions</string> <string name="pref_expandNotify_sum">Açò normalment expandeix la notificació per a mostrar botons de reproducció</string> <string name="pref_persistNotify_title">Botons de reproducció persistents</string> @@ -424,8 +406,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Només pots seleccionar un màxim de %1$d elements.</string> <string name="pref_lockscreen_background_title">Estableix el fons del bloqueig de pantalla</string> <string name="pref_lockscreen_background_sum">Estableix el fons del bloqueig de pantalla a la imatge de l\'episodi actual. Com a efecte secundari, això també mostrarà la imatge en aplicacions de tercers.</string> - <string name="pref_showDownloadReport_sum">Si les descàrregues fallen, genera un informe que mostra els detalls de la fallada.</string> - <string name="pref_showAutoDownloadReport_sum">Mostra una notificació per a episodis descarregats automàticament</string> <string name="pref_expand_notify_unsupport_toast">Les versions d\'Android anteriors a la 4.1 no suporten les notificacions ampliades.</string> <string name="pref_enqueue_location_title">Posició d\'entrada en cola</string> <string name="pref_enqueue_location_sum">Afegit episodis a: %1$s</string> @@ -446,14 +426,12 @@ <string name="pref_current_value">Valor actual: %1$s</string> <string name="pref_proxy_title">Servidor intermediari</string> <string name="pref_proxy_sum">Estableix un servidor intermediari</string> - <string name="pref_faq">Preguntes Freqüents</string> <string name="pref_no_browser_found">No s\'ha trobat cap navegador web.</string> <string name="pref_cast_title">Suport per a Chromecast</string> <string name="pref_cast_message_play_flavor">Habilita el suport per la reproducció remota en dispositius de difusió (com ara Chromecast, Audio Speakers o Android TV). </string> <string name="pref_cast_message_free_flavor">Chromecast requereix de llibreries propietàries de terceres parts que estan inhabilitades en aquesta versió d\'AntennaPod</string> <string name="pref_enqueue_downloaded_title">Afegeix les baixades a la cua</string> <string name="pref_enqueue_downloaded_summary">Afegeix els episodis descarregats a la cua</string> - <string name="media_player_builtin">Reproductor Android estàndard</string> <string name="media_player_switch_to_exoplayer">Canvia a ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Canviat a ExoPlayer</string> <string name="pref_skip_silence_title">Omet Silenci en Audio</string> @@ -556,22 +534,12 @@ <string name="gpodnet_suggestions_header">SUGGERÈNCIES</string> <string name="gpodnet_search_hint">Cerca a gpodder.net</string> <string name="gpodnetauth_login_title">Inici de sessió</string> - <string name="gpodnetauth_login_descr">Benvingut al procés d\'inici de sessió a gpodder.net. Primerament, introduïu la informació d\'accés:</string> <string name="gpodnetauth_login_butLabel">Entra</string> - <string name="gpodnetauth_login_register">Si no teniu compte, podeu crear-ne un aquí:\nhttps://gpodder.net/register/</string> <string name="username_label">Nom d\'usuari</string> <string name="password_label">Contrasenya</string> - <string name="gpodnetauth_device_title">Selecció de dispositiu</string> <string name="gpodnetauth_device_descr">Per a utilitzar gpodder.net, creeu un nou dispositiu o seleccioneu-ne un d\'existent:</string> - <string name="gpodnetauth_device_deviceID">ID de dispositiu:\u0020</string> - <string name="gpodnetauth_device_caption">Llegenda</string> - <string name="gpodnetauth_device_butCreateNewDevice">Crea nou dispositiu</string> - <string name="gpodnetauth_device_chooseExistingDevice">Seleccioneu un dispositiu existent:</string> - <string name="gpodnetauth_device_errorEmpty">L\'ID de dispositiu no pot ser buit</string> - <string name="gpodnetauth_device_errorAlreadyUsed">L\'ID de dispositiu ja existeix</string> <string name="gpodnetauth_device_caption_errorEmpty">El títol no pot estar buit </string> <string name="gpodnetauth_device_butChoose">Selecciona</string> - <string name="gpodnetauth_finish_title">Heu iniciat la sessió!</string> <string name="gpodnetauth_finish_descr">Felicitats! El vostre compte de gpodder.net s\'ha enllaçat amb el dispositiu. D\'ara endavant, AntennaPod sincronitzarà automàticament les subscripcions del dispositiu al vostre compte.</string> <string name="gpodnetauth_finish_butsyncnow">Sincronitza ara</string> <string name="gpodnetauth_finish_butgomainscreen">Vés a la pantalla principal</string> @@ -739,8 +707,6 @@ <string name="notification_channel_downloading_description">Mostrar durant baixades.</string> <string name="notification_channel_playing">Reproducció actual</string> <string name="notification_channel_playing_description">Permet controlar la reproducció. Aquesta és la notificació principal que veureu durant la reproducció d\'un podcast.</string> - <string name="notification_channel_error">Errors</string> - <string name="notification_channel_auto_download">Baixades automàtiques</string> <string name="notification_channel_episode_auto_download">Mostrat quan els episodis han sigut descarregats automàticament</string> <!--Widget settings--> <string name="widget_settings">Settings del widget</string> diff --git a/core/src/main/res/values-cs/strings.xml b/core/src/main/res/values-cs/strings.xml index ba9ebd1f2..a44719c7d 100644 --- a/core/src/main/res/values-cs/strings.xml +++ b/core/src/main/res/values-cs/strings.xml @@ -1,11 +1,12 @@ <?xml version='1.0' encoding='UTF-8'?> <resources xmlns:tools="http://schemas.android.com/tools"> <!--Activity and fragment titles--> - <string name="feed_update_receiver_name">Aktualizovat sbírky</string> + <string name="feed_update_receiver_name">Aktualizovat odběry</string> <string name="feeds_label">Podcasty</string> <string name="statistics_label">Statistiky</string> <string name="add_feed_label">Přidat podcast</string> <string name="episodes_label">Epizody</string> + <string name="queue_label">Fronta</string> <string name="all_episodes_short_label">Vše</string> <string name="new_episodes_label">Nový</string> <string name="favorite_episodes_label">Oblíbené</string> @@ -15,8 +16,8 @@ <string name="downloads_running_label">Právě běží</string> <string name="downloads_completed_label">Dokončeno</string> <string name="downloads_log_label">Log</string> - <string name="subscriptions_label">Sbírky</string> - <string name="subscriptions_list_label">Seznam sbírek</string> + <string name="subscriptions_label">Odběry</string> + <string name="subscriptions_list_label">Seznam odběrů</string> <string name="cancel_download_label">Zrušit stahování</string> <string name="playback_history_label">Historie přehrávání</string> <string name="gpodnet_main_label">gpodder.net</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Přehrávání</string> <string name="download_statistics_label">Stažené</string> <string name="notification_pref_fragment">Upozornění</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" nenalezen</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Celkový čas přehraných epizod:</string> <string name="statistics_details_dialog">%1$d z %2$d započatých epizod.\n\nPřehraných %3$s z %4$s.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Žádné</string> <!--Bug report activity--> <string name="log_file_share_exception">Nenalezena kompatibilní aplikace</string> + <string name="export_logs_menu_title">Exportovat detailní záznamy</string> + <string name="confirm_export_log_dialog_message">Detailní záznamy mohou obsahovat citlivé informace jako seznam odběrů</string> <!--Webview actions--> <string name="open_in_browser_label">Otevřít v prohlížeči</string> <string name="copy_url_label">Kopírovat URL</string> @@ -82,7 +87,6 @@ <string name="description_label">Popis</string> <string name="episodes_suffix">\u0020epizod</string> <string name="processing_label">Zpracovávám</string> - <string name="save_username_password_label">Uložit uživatelské jméno a heslo</string> <string name="close_label">Zavřít</string> <string name="retry_label">Zkusit znovu</string> <string name="auto_download_label">Zahrnout do automaticky stahovaných</string> @@ -94,12 +98,13 @@ <string name="feed_volume_reduction_off">Vypnuto</string> <string name="feed_volume_reduction_light">Nízké</string> <string name="feed_volume_reduction_heavy">Vysoké</string> - <string name="parallel_downloads_suffix">\u0020paralelních stahování</string> + <string name="parallel_downloads">%1$d souběžných stahování</string> <string name="feed_auto_download_global">Globální nastavení</string> <string name="feed_auto_download_always">Vždy</string> <string name="feed_auto_download_never">Nikdy</string> <string name="send_label">Odeslat</string> <string name="episode_cleanup_never">Nikdy</string> + <string name="episode_cleanup_except_favorite_removal">Pokud není mezi oblíbenými</string> <string name="episode_cleanup_queue_removal">Pokud není ve frontě</string> <string name="episode_cleanup_after_listening">Po dokončení</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -121,6 +126,7 @@ <item quantity="other">%d vybráno</item> </plurals> <string name="loading_more">Načítají se další…</string> + <string name="episode_notification">Upozornění na epizody</string> <!--Actions on feeds--> <string name="mark_all_read_label">Označit vše jako poslechnuté</string> <string name="mark_all_read_msg">Všechny epizody označeny jako poslechnuté</string> @@ -152,7 +158,6 @@ <string name="hide_not_queued_episodes_label">Mimo frontu</string> <string name="hide_has_media_label">Obsahuje média</string> <string name="filtered_label">Filtrované</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Poslední aktualizace selhala</string> <string name="open_podcast">Otevřít podcast</string> <string name="please_wait_for_data">Počkejte prosím na dokončení načítání</string> <!--actions on feeditems--> @@ -223,16 +228,12 @@ <string name="download_error_details">Detaily</string> <string name="download_error_details_message">%1$s \n\nURL souboru:\n%2$s</string> <string name="download_error_device_not_found">Úložné zařízení nenalezeno</string> - <string name="download_error_insufficient_space">Nedostatek volného místa</string> <string name="download_error_http_data_error">HTTP chyba</string> <string name="download_error_error_unknown">Neznámá chyba</string> - <string name="download_error_parser_exception">Výjimka parseru</string> <string name="download_error_unsupported_type">Nepodporovaný typ kanálu</string> <string name="download_error_connection_error">Chyba spojení</string> - <string name="download_error_unknown_host">Neznámý host</string> <string name="download_error_unauthorized">Chyba přihlášení</string> <string name="download_error_file_type_type">Chyba typu souboru</string> - <string name="download_error_forbidden">Zákázáno</string> <string name="download_canceled_msg">Stahování zrušeno</string> <string name="download_canceled_autodownload_enabled_msg">Stahování zrušeno\nVypnuto <i>automatické stahování</i> této položky</string> <string name="download_report_title">Stahování dokončeno s chybou</string> @@ -248,14 +249,7 @@ <item quantity="many">%d čekajících na stažení</item> <item quantity="other">%d čekajících na stažení</item> </plurals> - <string name="downloads_processing">Probíhá stahování</string> <string name="download_notification_title">Stahuji podcast data</string> - <plurals name="download_report_content"> - <item quantity="one">%d úspěšné stažení, %d selhalo</item> - <item quantity="few">%d úspěšná stažení, %d selhala</item> - <item quantity="many">%d úspěšných stažení, %d selhalo</item> - <item quantity="other">%d úspěšných stažení, %d selhalo</item> - </plurals> <string name="download_log_title_unknown">Neznámý název</string> <string name="download_type_feed">Kanál</string> <string name="download_type_media">Soubor</string> @@ -338,13 +332,12 @@ <string name="no_fav_episodes_label">Epizody si můžete přidat mezi oblíbené dlouhým dotykem.</string> <string name="no_chapters_head_label">Žádné kapitoly</string> <string name="no_chapters_label">Tato epizoda nemá žádné kapitoly.</string> - <string name="no_subscriptions_head_label">Žádné sbírky</string> - <string name="no_subscriptions_label">Pro přidání podcastu do sbírky se dotkněte ikonky plus níže.</string> + <string name="no_subscriptions_head_label">Žádné odběry</string> + <string name="no_subscriptions_label">Pro přihlášení odběru podcastu stiskněte ikonu plus níže.</string> <!--Preferences--> <string name="storage_pref">Úložiště</string> <string name="storage_sum">Automatické mazání epizod, Import, Export</string> <string name="project_pref">Projekt</string> - <string name="queue_label">Fronta</string> <string name="synchronization_pref">Synchronizace</string> <string name="synchronization_sum">Synchronizace s dalšími zařízeními pomocí služby gpodder.net</string> <string name="automation">Automatizace</string> @@ -360,14 +353,17 @@ <string name="preference_search_clear_history">Vymazat historii</string> <string name="media_player">Přehrávač médií</string> <string name="pref_episode_cleanup_title">Vyčistit epizody</string> - <string name="pref_episode_cleanup_summary">Epizody, které nejsou ve frontě a nejsou označeny za oblíbené by mělo být možné smazat, pokud bude funkce automatického stahování potřebovat místo pro nové epizody</string> <string name="pref_pauseOnDisconnect_sum">Při odpojení sluchátek nebo bluetooth připojení pozastavit přehrávání.</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Pokračovat v přehrávání po připojení sluchátek</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Pokračovat v přehrávání po připojení bluetooth</string> - <string name="pref_hardwareForwardButtonSkips_title">Tlačítko rychle vpřed přeskakuje</string> - <string name="pref_hardwareForwardButtonSkips_sum">Stisk tlačítka rychle vpřed (FF) na připojeném zařízení Bluetooth přeskočí na další epizodu místo rychlého přetočení vpřed.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Tlačítko zpět restartuje</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Po stlačení hardwarového tlačítka pro posun zpět místo přetočení vpřed restartovat přehrávání aktuální epizody</string> + <string name="pref_hardware_forward_button_title">Tlačítno vpřed</string> + <string name="pref_hardware_forward_button_summary">Přizpůsobit tlačítko vpřed</string> + <string name="pref_hardware_previous_button_title">Tlačítko předchozí</string> + <string name="pref_hardware_previous_button_summary">Přizpůsobit tlačítko předchozí</string> + <string name="button_action_fast_forward">Posunout vpřed</string> + <string name="button_action_rewind">Posunout zpět</string> + <string name="button_action_skip_episode">Přeskočit epizodu</string> + <string name="button_action_restart_episode">Znovu spustit epizodu</string> <string name="pref_followQueue_sum">Po přehrání položky z fronty přejít automaticky na další</string> <string name="pref_auto_delete_sum">Smazat díl po jeho přehrání</string> <string name="pref_auto_delete_title">Automatické mazání</string> @@ -387,7 +383,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Vypnout</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Nastavit interval</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Nastavit čas v průběhu dne</string> - <string name="pref_autoUpdateIntervallOrTime_every">každých %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">v %1$s</string> <string name="pref_followQueue_title">Kontinuální přehrávání</string> <string name="pref_pauseOnHeadsetDisconnect_title">Sluchátka nebo Bluetooth odpojeno</string> @@ -407,10 +402,10 @@ <string name="pref_set_theme_title">Vybrat motiv</string> <string name="pref_nav_drawer_items_title">Změnit navigační panel</string> <string name="pref_nav_drawer_items_sum">Upravit zobrazení položek v navigačním panelu.</string> - <string name="pref_nav_drawer_feed_order_title">Nastavit pořadí sbírek</string> - <string name="pref_nav_drawer_feed_order_sum">Upravit pořadí vašich sbírek</string> - <string name="pref_nav_drawer_feed_counter_title">Nastavit čítač sbírek</string> - <string name="pref_nav_drawer_feed_counter_sum">Změnit informaci zobrazenou čítačem sbírek. Též ovlivňuje řazení, je-li nastaveno na „podle čítače“.</string> + <string name="pref_nav_drawer_feed_order_title">Nastavit pořadí odběrů</string> + <string name="pref_nav_drawer_feed_order_sum">Upravit pořadí vašich odběrů</string> + <string name="pref_nav_drawer_feed_counter_title">Nastavit čítač odběrů</string> + <string name="pref_nav_drawer_feed_counter_sum">Změnit informaci zobrazenou čítačem odběrů. Též ovlivňuje řazení, je-li nastaveno na „podle čítače“.</string> <string name="pref_set_theme_sum">Změnit vzhled AntennaPod.</string> <string name="pref_automatic_download_title">Automatické stahování</string> <string name="pref_automatic_download_sum">Nastavení automatického stahování epizod.</string> @@ -418,11 +413,12 @@ <string name="pref_autodl_wifi_filter_sum">Povolit automatické stahování pouze pomocí vybraných Wi-Fi sítí.</string> <string name="pref_automatic_download_on_battery_title">Stahovat, pokud neprobíhá nabíjení</string> <string name="pref_automatic_download_on_battery_sum">Povolit automatické stahování i pokud není baterie nabíjena</string> - <string name="pref_parallel_downloads_title">Paralelní stahování</string> + <string name="pref_parallel_downloads_title">Souběžné stahování</string> <string name="pref_episode_cache_title">Historie epizod</string> <string name="pref_episode_cache_summary">Celkový počet epizod stažených na zařízení. Automatické stahování se zastaví při dosažení této hodnoty.</string> <string name="pref_episode_cover_title">Použít obrázek epizody</string> - <string name="pref_episode_cover_summary">Použít obrázek přímo z epizody, pokud je k dispozici. Není-li tato možnost zaškrtnuta, tak se vždy použije obrázek podcastu.</string> + <string name="pref_show_remain_time_title">Zobrazit zbývající čas</string> + <string name="pref_show_remain_time_summary">Pokud je vybráno, zobrazí se zbývající čas epizody. Jinak se zobrazí celková délka epizody.</string> <string name="pref_theme_title_use_system">Použít systémové téma</string> <string name="pref_theme_title_light">Světlý</string> <string name="pref_theme_title_dark">Tmavý</string> @@ -442,8 +438,6 @@ <string name="pref_gpodnet_full_sync_title">Synchronizovat vše ihned</string> <string name="pref_gpodnet_full_sync_sum">Synchronizovat všechny odběry a stav epizod s gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Přihlášen jako <i>%1$s</i> z přístroje <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Synchronizace selhala</string> - <string name="pref_gpodnet_notifications_sum">Toto nastavení se netýká chyb přihlášení.</string> <string name="pref_playback_speed_sum">Upravit předvybrané možnosti pro přehrávání zvuku různými rychlostmi</string> <string name="pref_feed_playback_speed_sum">Rychlost, která bude použita při zahájení přehrávání epizod tohoto podcastu</string> <string name="pref_feed_skip">Automatické přeskočení</string> @@ -458,8 +452,6 @@ <string name="pref_fast_forward_sum">Upravit o kolik sekund se přeskočí dopředu při stisku tlačítka rychle vpřed (FF).</string> <string name="pref_rewind">Délka času posunu zpět</string> <string name="pref_rewind_sum">Upravit o kolik sekund se přeskočí zpět při stisku tlačítka přetočit zpět (RW).</string> - <string name="pref_gpodnet_sethostname_title">Nastavit hostname</string> - <string name="pref_gpodnet_sethostname_use_default_host">Použít přednastaveného hosta</string> <string name="pref_expandNotify_title">Vysoká priorita pro oznámení</string> <string name="pref_expandNotify_sum">Toto obvykle přidá tlačítka ovládání přehrávání do zpráv upozornění</string> <string name="pref_persistNotify_title">Pevné ovládání přehrávání</string> @@ -470,10 +462,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Lze vybrat maximálně %1$d položek.</string> <string name="pref_lockscreen_background_title">Nastavit pozadí uzamčené obrazovky</string> <string name="pref_lockscreen_background_sum">Nastavit pozadí uzamčené obrazovky na obrázek aktuální epizody. Jako vedlejší efekt zobrazí toto nastavení obrázek i v aplikacích třetích stran. </string> - <string name="pref_showDownloadReport_title">Stahování selhalo</string> - <string name="pref_showDownloadReport_sum">Pokud selže stahování, vygenerovat report zobrazující detaily o chybě.</string> - <string name="pref_showAutoDownloadReport_title">Automatické stahování dokončeno</string> - <string name="pref_showAutoDownloadReport_sum">Zobrazovat oznámení o automaticky stažených epizodách.</string> <string name="pref_expand_notify_unsupport_toast">Verze Androidu nižší než 4.1 nepodporují rozšířená oznámení.</string> <string name="pref_enqueue_location_title">Pozice přidávání do fronty</string> <string name="pref_enqueue_location_sum">Přidávat epizody na: %1$s</string> @@ -494,14 +482,14 @@ <string name="pref_current_value">Aktuální hodnota: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Nastavit síťovou proxy</string> - <string name="pref_faq">Často kladené otázky</string> <string name="pref_no_browser_found">Webový prohlížeč nenalezen.</string> <string name="pref_cast_title">Chromecast podpora</string> <string name="pref_cast_message_play_flavor">Povolit podporu vzdáleného přehrávání médií na Cast přístrojích (jako třeba Chromecast, Audio Speakers nebo Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast vyžaduje proprietární knihovny třetích stran, které jsou vypnuty v této verzi AntennaPod</string> <string name="pref_enqueue_downloaded_title">Zařadit stažené</string> <string name="pref_enqueue_downloaded_summary">Přidat stažené epizody do fronty</string> - <string name="media_player_builtin">Vestavěný přehrávač Androidu</string> + <string name="media_player_builtin">Vestavěný přehrávač Androidu (zastaralé)</string> + <string name="media_player_sonic">Přehrávač médií Sonic (zastaralé)</string> <string name="media_player_exoplayer_recommended">ExoPlayer (doporučen)</string> <string name="media_player_switch_to_exoplayer">Přepnout na ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Přepnuto na ExoPlayer</string> @@ -523,10 +511,10 @@ <string name="back_button_go_to_page_title">Vybrat stránku</string> <string name="pref_delete_removes_from_queue_title">Mazání také odstraňuje epizody z fronty</string> <string name="pref_delete_removes_from_queue_sum">Automaticky odstraní epizodu z fronty poté, co je smazána.</string> - <string name="pref_filter_feed_title">Filtr sbírek</string> - <string name="pref_filter_feed_sum">Filtrujte svoje sbírky v navigačním panelu a na obrazovce odebíraných kanálů.</string> + <string name="pref_filter_feed_title">Filtr odběrů</string> + <string name="pref_filter_feed_sum">Filtrujte svoje odběry v navigačním panelu a na obrazovce odběrů.</string> <string name="no_filter_label">Žádné</string> - <string name="subscriptions_are_filtered">Odebírané sbírky jsou filtrovány.</string> + <string name="subscriptions_are_filtered">Odběry jsou filtrovány.</string> <string name="subscriptions_counter_greater_zero">Počet vyšší než nula</string> <string name="auto_downloaded">Automaticky stahováno</string> <string name="not_auto_downloaded">Nebylo automaticky staženo</string> @@ -552,18 +540,18 @@ <string name="sync_status_episodes_upload">Nahrávají se změny epizod…</string> <string name="sync_status_episodes_download">Stahují se změny epizod…</string> <string name="sync_status_upload_played">Nahrává se stav poslechnutí…</string> - <string name="sync_status_subscriptions">Synchronizují se sbírky…</string> + <string name="sync_status_subscriptions">Synchronizuji odběry…</string> <string name="sync_status_success">Synchronizace proběhla úspěšně</string> <string name="sync_status_error">Synchronizace selhala</string> <!--import and export--> - <string name="import_export_summary">Přesunout sbírky a frontu do jiného zařízení</string> + <string name="import_export_summary">Přesunout odběry a frontu do jiného zařízení</string> <string name="database">Databáze</string> <string name="opml">OPML</string> <string name="html">HTML</string> - <string name="html_export_summary">Ukažte své sbírky přátelům</string> - <string name="opml_export_summary">Přenést sbírky do jiné podcastové aplikace</string> - <string name="opml_import_summary">Importovat vaše sbírky z jiné podcastové aplikace</string> - <string name="database_export_summary">Přenést sbírky, poslechnuté epizody a frontu do aplikace AntennaPod na jiném zařízení</string> + <string name="html_export_summary">Ukažte své odběry přátelům</string> + <string name="opml_export_summary">Přenést odběry do jiné podcastové aplikace</string> + <string name="opml_import_summary">Importovat vaše odběry z jiné podcastové aplikace</string> + <string name="database_export_summary">Přenést odběry, poslechnuté epizody a frontu do aplikace AntennaPod na jiném zařízení</string> <string name="database_import_summary">Importovat AntennaPod databázi z jiného zařízení</string> <string name="opml_import_label">OPML import</string> <string name="opml_add_podcast_label">Importovat podcasty (OPML)</string> @@ -575,7 +563,7 @@ <string name="html_export_label">HTML export</string> <string name="database_export_label">Export databáze</string> <string name="database_import_label">Import databáze</string> - <string name="database_import_warning">Importem databáze nahradíte všechny svoje sbírky a historii poslechu. Doporučujeme nejdřív zvážit exportování současné databáze pro případnou obnovu. Vážně chcete databázi nahradit?</string> + <string name="database_import_warning">Importem databáze nahradíte všechny své odběry a historii poslechu. Doporučujeme nejdříve exportovat současnou databázi pro případnou obnovu. Opravdu chcete databázi nahradit?</string> <string name="please_wait">Čekejte prosím…</string> <string name="export_error_label">Chyba exportu</string> <string name="export_success_title">Export úspěšný</string> @@ -590,6 +578,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Nastavit časovač vypnutí</string> <string name="disable_sleeptimer_label">Deaktivovat časovač vypnutí</string> + <string name="extend_sleep_timer_label">+%d minut</string> <string name="sleep_timer_label">Časovač vypnutí</string> <string name="time_dialog_invalid_input">Neplatný vstup, musí být zadáno celé číslo</string> <string name="shake_to_reset_label">Restartujte zatřesením</string> @@ -623,22 +612,22 @@ <string name="gpodnet_suggestions_header">DOPORUČENÉ</string> <string name="gpodnet_search_hint">Prohledat gpodder.net</string> <string name="gpodnetauth_login_title">Přihlásit</string> - <string name="gpodnetauth_login_descr">Vítejte do průvodce přihlášením ke gpodder.net účtu. Zadejte vaše přihlašovací údaje:</string> <string name="gpodnetauth_login_butLabel">Přihlásit</string> - <string name="gpodnetauth_login_register">Pokud ještě nemáte účet, můžete ho vytvořit zde:\nhttps://gpodder.net/register/</string> + <string name="create_account">Vytvořit účet</string> <string name="username_label">Uživatelské jméno</string> <string name="password_label">Heslo</string> - <string name="gpodnetauth_device_title">Výběr zařízení</string> + <string name="gpodnet_description">Gpodder.net je open-source služba na synchronizaci podcástů nezávislá na AntennaPod projektu.</string> + <string name="gpodnetauth_server_official">Oficiální server gpodder.net</string> + <string name="gpodnetauth_server_custom">Vlastní server</string> + <string name="gpodnetauth_host">Hostname</string> + <string name="gpodnetauth_select_server">Vybrat server</string> <string name="gpodnetauth_device_descr">Vytvořte nové nebo vyberte již existující zařízení pro použití s vašim gpodder.net účtem.</string> - <string name="gpodnetauth_device_deviceID">ID zařízení:\u0020</string> - <string name="gpodnetauth_device_caption">Nadpis</string> - <string name="gpodnetauth_device_butCreateNewDevice">Vytvořit nové zařízení</string> - <string name="gpodnetauth_device_chooseExistingDevice">Vybrat existující zařízení:</string> - <string name="gpodnetauth_device_errorEmpty">ID zařízení nesmí být prázdné</string> - <string name="gpodnetauth_device_errorAlreadyUsed">ID zařízení je již obsazeno</string> + <string name="gpodnetauth_device_name">Název zařízení</string> + <string name="gpodnetauth_device_name_default">AntennaPod na %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Titulek nesmí být prázdný</string> + <string name="gpodnetauth_existing_devices">Existující zařízení</string> + <string name="gpodnetauth_create_device">Vytvořit zařízení</string> <string name="gpodnetauth_device_butChoose">Vybrat</string> - <string name="gpodnetauth_finish_title">Úspěšně přihlášeno!</string> <string name="gpodnetauth_finish_descr">Gratulujeme! Váš gpodder.net účet je nyní úspěšně propojen s vaším zařízením. AntennaPod bude automaticky synchronizovat odebírané podcasty s nastaveným účtem na gpodder.net.</string> <string name="gpodnetauth_finish_butsyncnow">Synchronizovat nyní</string> <string name="gpodnetauth_finish_butgomainscreen">Přejít na hlavní obrazovku</string> @@ -674,7 +663,7 @@ <string name="pref_restart_required">Pro aktivování změn nastavení bylo třeba restartovat aplikaci AntennaPod.</string> <!--Online feed view--> <string name="subscribe_label">Odebírat</string> - <string name="subscribing_label">Přidává se do sbírky…</string> + <string name="subscribing_label">Přidává se do odběrů…</string> <string name="preview_episode">Spustit ukázku</string> <string name="stop_preview">Zastavit ukázku</string> <!--Content descriptions for image buttons--> @@ -692,6 +681,7 @@ <string name="switch_pages">Přehodit stránku</string> <string name="position">Pozice: %1$s</string> <string name="apply_action">Vykonat</string> + <string name="play_chapter">Přehrát kapitolu</string> <!--Feed information screen--> <string name="authentication_label">Ověření</string> <string name="authentication_descr">Změnit uživatelské jméno a heslo pro tento podcast a jeho epizody.</string> @@ -826,18 +816,22 @@ <string name="cast_failed_receiver_player_error">Přijímač zaznamenal závažnou chybu</string> <string name="cast_failed_media_error_skipping">Chyba přehrávání médií. Přeskakuji...</string> <!--Notification channels--> + <string name="notification_group_errors">Chyby</string> + <string name="notification_group_news">Novinky</string> <string name="notification_channel_user_action">Je vyžadována činnost z vaší strany</string> <string name="notification_channel_user_action_description">Zobrazuje se, pokud je požadována činnost z vaší strany. Například je-li potřeba zadat heslo.</string> <string name="notification_channel_downloading">Stahuji</string> <string name="notification_channel_downloading_description">Zobrazuje se v průběhu stahování.</string> <string name="notification_channel_playing">Přehrává se</string> <string name="notification_channel_playing_description">Umožňuje ovládat přehrávání. Toto je to hlavní oznámení, které uvidité při přehrávání podcastu.</string> - <string name="notification_channel_error">Chyby</string> - <string name="notification_channel_error_description">Upozorňovat pokud něco selže, například stahování či aktualizace odebíraného kanálu.</string> - <string name="notification_channel_sync_error">Chyby synchronizace</string> + <string name="notification_channel_download_error">Stahování selhalo</string> + <string name="notification_channel_download_error_description">Zobrazeno pokud selže stahování či aktualizace odebíraného kanálu.</string> + <string name="notification_channel_sync_error">Synchronizace selhala</string> <string name="notification_channel_sync_error_description">Zobrazovat chybu synchronizace s gpodder.</string> - <string name="notification_channel_auto_download">Automatické stahování</string> + <string name="notification_channel_auto_download">Automatické stahování dokončeno</string> <string name="notification_channel_episode_auto_download">Zobrazuje se po automatickém stažení epizod.</string> + <string name="notification_channel_new_episode">Nová epizoda</string> + <string name="notification_channel_new_episode_description">Zobrazeno pokud bude nalezena nová epizoda podcastu a upozornění jsou zapnuta.</string> <!--Widget settings--> <string name="widget_settings">Nastavení widgetu</string> <string name="widget_create_button">Vytvořit widget</string> diff --git a/core/src/main/res/values-da/strings.xml b/core/src/main/res/values-da/strings.xml index 1fc7e1d69..cd8f66578 100644 --- a/core/src/main/res/values-da/strings.xml +++ b/core/src/main/res/values-da/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statistik</string> <string name="add_feed_label">Tilføj podcast</string> <string name="episodes_label">Udsendelser</string> + <string name="queue_label">Kø</string> <string name="all_episodes_short_label">Alle</string> <string name="new_episodes_label">Nye</string> <string name="favorite_episodes_label">Foretrukne</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Log</string> <string name="subscriptions_label">Abonnementer</string> <string name="subscriptions_list_label">Liste over abonnementer</string> - <string name="cancel_download_label">Annuller\noverførsel</string> + <string name="cancel_download_label">Annuller overførsel</string> <string name="playback_history_label">Afspilningshistorik</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">Login til gpodder.net</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Afspilning</string> <string name="download_statistics_label">Overførsler</string> <string name="notification_pref_fragment">Påmindelser</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" ikke fundet</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Samlet tid for afspillede udsendelser:</string> <string name="statistics_details_dialog">%1$d af %2$d udsendelser startet.\n\nAfspillet %3$s af %4$s.</string> @@ -53,6 +56,7 @@ <string name="drawer_feed_counter_none">Ingen</string> <!--Bug report activity--> <string name="log_file_share_exception">Ingen kompatible apper fundet.</string> + <string name="export_logs_menu_title">Eksporter detaljeret log</string> <!--Webview actions--> <string name="open_in_browser_label">Åbn i browser</string> <string name="copy_url_label">Kopier webadresse</string> @@ -81,7 +85,6 @@ <string name="description_label">Beskrivelse</string> <string name="episodes_suffix">\u0020udsendelser</string> <string name="processing_label">Behandler</string> - <string name="save_username_password_label">Gem brugernavn og adgangskode</string> <string name="close_label">Luk</string> <string name="retry_label">Prøv igen</string> <string name="auto_download_label">Inkludér i automatiske overførsler</string> @@ -93,7 +96,7 @@ <string name="feed_volume_reduction_off">Fra</string> <string name="feed_volume_reduction_light">Lidt</string> <string name="feed_volume_reduction_heavy">Meget</string> - <string name="parallel_downloads_suffix">\u0020parallelle overførsler</string> + <string name="parallel_downloads">%1$d parallelle overførsler</string> <string name="feed_auto_download_global">Global standard</string> <string name="feed_auto_download_always">Altid</string> <string name="feed_auto_download_never">Aldrig</string> @@ -113,7 +116,22 @@ <item quantity="one">%d valgt</item> <item quantity="other">%d valgte</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d episode</item> + <item quantity="other">%d episoder</item> + </plurals> <string name="loading_more">Indlæser mere ...</string> + <string name="episode_notification">Påmindelser om episoder</string> + <string name="episode_notification_summary">Vis en notifikation når der er nye episoder</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s har en ny episode</item> + <item quantity="other">%2$s har %1$d nye udsendelser</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Nye episoder</item> + <item quantity="other">Nye episoder</item> + </plurals> + <string name="new_episode_notification_group_text">Dine abonnementer har nye udsendelser</string> <!--Actions on feeds--> <string name="mark_all_read_label">Marker alle som afspillet</string> <string name="mark_all_read_msg">Marker alle udsendelser som afspillet</string> @@ -131,7 +149,7 @@ <string name="share_label">Del</string> <string name="share_label_with_ellipses">Del...</string> <string name="share_file_label">Del fil</string> - <string name="share_website_url_label">Netsted adresse</string> + <string name="share_website_url_label">Adresse på netsted</string> <string name="share_feed_url_label">Podcast-adresse</string> <string name="feed_delete_confirmation_msg">Bekræft venligst at du ønsker at slette podcasten \"%1$s\" og ALLE dens udsendelser (inklusive overførte udsendelser)</string> <string name="feed_delete_confirmation_local_msg">Bekræft venligst at du vil fjerne podcasten \"%1$s\". Filerne i den lokale kildemappe vil ikke blive slettet.</string> @@ -145,7 +163,6 @@ <string name="hide_not_queued_episodes_label">Ikke sat i kø</string> <string name="hide_has_media_label">Har medier</string> <string name="filtered_label">Filtrerede</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Sidste opdatering fejlede</string> <string name="open_podcast">Åbn podcast</string> <string name="please_wait_for_data">Vær venlig og vent til data\'et er indlæst</string> <!--actions on feeditems--> @@ -160,6 +177,10 @@ <string name="delete_label">Slet</string> <string name="delete_failed">Kan ikke slette fil. En genstart af enheden vil sandsynligvis hjælpe.</string> <string name="delete_episode_label">Slet udsendelse</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d udsendelse valgt, %d hentet udesendelse slettet.</item> + <item quantity="other">%dudsendelser valgt, %d hentede udsendelser slettet.</item> + </plurals> <string name="remove_new_flag_label">Fjern \"ny\"-markering</string> <string name="removed_new_flag_label">Fjernet \"ny\"-markering</string> <string name="mark_read_label">Marker som afspillet</string> @@ -192,7 +213,7 @@ <string name="added_to_favorites">Føjet til foretrukne</string> <string name="remove_from_favorite_label">Fjern fra foretrukne</string> <string name="removed_from_favorites">Fjernet fra foretrukne</string> - <string name="visit_website_label">Besøg webside</string> + <string name="visit_website_label">Besøg netsted</string> <string name="skip_episode_label">Spring udsendelse over</string> <string name="activate_auto_download">Slå Automatisk overførsel til</string> <string name="deactivate_auto_download">Slå Automatisk overførsel fra</string> @@ -206,16 +227,12 @@ <string name="download_error_details">Detaljer</string> <string name="download_error_details_message">%1$s \n\nFil-URL:\n%2$s</string> <string name="download_error_device_not_found">Kan ikke finde lagerenhed</string> - <string name="download_error_insufficient_space">Ikke nok plads</string> <string name="download_error_http_data_error">HTTP-datafejl</string> <string name="download_error_error_unknown">Ukendt fejl</string> - <string name="download_error_parser_exception">Parser-undtagelse</string> <string name="download_error_unsupported_type">Feedets type er ikke understøttet</string> <string name="download_error_connection_error">Forbindelsesfejl</string> - <string name="download_error_unknown_host">Ukendt vært</string> <string name="download_error_unauthorized">Godkendelsesfejl</string> <string name="download_error_file_type_type">Filtypefejl</string> - <string name="download_error_forbidden">Adgang nægtet</string> <string name="download_canceled_msg">Overførsel annulleret</string> <string name="download_canceled_autodownload_enabled_msg">Overførsel annulleret\n<i>Automatisk overførsel</i> blev slået fra for dette element</string> <string name="download_report_title">Overførsler afsluttet med fejl</string> @@ -229,12 +246,7 @@ <item quantity="one">%d overførsel mangler</item> <item quantity="other">%d overførsler mangler</item> </plurals> - <string name="downloads_processing">Bearbejder overførte data</string> <string name="download_notification_title">Henter podcast-data</string> - <plurals name="download_report_content"> - <item quantity="one">%d overførsel lykkedes, %d fejlede</item> - <item quantity="other">%d overførsler lykkedes, %d fejlede</item> - </plurals> <string name="download_log_title_unknown">Ukendt titel</string> <string name="download_type_feed">Feed</string> <string name="download_type_media">Mediefil</string> @@ -267,6 +279,7 @@ <string name="player_go_to_picture_in_picture">Billed-i-billed-tilstand</string> <string name="unknown_media_key">AntennaPod - Ukendt medienøgle: %1$d</string> <string name="error_file_not_found">Fil ikke fundet</string> + <string name="no_media_label">Element indeholder ikke en mediefil.</string> <!--Queue operations--> <string name="lock_queue">Lås kø</string> <string name="unlock_queue">Lås kø op</string> @@ -321,9 +334,8 @@ <string name="no_subscriptions_label">For at abonnere på en podcast, klik plus ikonet nedenfor</string> <!--Preferences--> <string name="storage_pref">Lagring</string> - <string name="storage_sum">Automatisk sletning, Importer, Exporter af episoder</string> + <string name="storage_sum">Automatisk sletning af udsendelser, import, eksport</string> <string name="project_pref">Projekt</string> - <string name="queue_label">Kø</string> <string name="synchronization_pref">Synkronisering</string> <string name="synchronization_sum">Synkroniser med andre enheder ved hjælp af gpodder.net</string> <string name="automation">Automatisering</string> @@ -339,14 +351,17 @@ <string name="preference_search_clear_history">Slet historik</string> <string name="media_player">Medieafspiller</string> <string name="pref_episode_cleanup_title">Oprydning i udsendelser</string> - <string name="pref_episode_cleanup_summary">Tillad at udsendelser, som ikke er i køen og som ikke er markeret som foretrukne, kan fjernes, hvis Automatisk overførsel har brug for plads til nye udsendelser</string> <string name="pref_pauseOnDisconnect_sum">Sæt afspilning på pause, hvis hovedtelefoner eller bluetooth afkobles</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Genoptag afspilning når hovedtelefonerne tilsluttes igen</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Genoptag afspilning når bluetooth forbinder igen</string> - <string name="pref_hardwareForwardButtonSkips_title">Fremadknap springer over</string> - <string name="pref_hardwareForwardButtonSkips_sum">Når der trykkes på næste knappen på de tilsluttede høretelefoner, skift til næste episode istedet for at springe frem.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Tilbageknap genstarter</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Når der trykkes på en fysisk tilbageknap, skal den aktuelle udsendelse afspilles forfra i stedet for at der spoles tilbage.</string> + <string name="pref_hardware_forward_button_title">Fremadknap</string> + <string name="pref_hardware_forward_button_summary">Konfigurer fremadknap</string> + <string name="pref_hardware_previous_button_title">Tilbageknap</string> + <string name="pref_hardware_previous_button_summary">Konfigurer tilbageknap</string> + <string name="button_action_fast_forward">Spol frem</string> + <string name="button_action_rewind">Spol tilbage</string> + <string name="button_action_skip_episode">Spring episode over</string> + <string name="button_action_restart_episode">Genstart udsendelse</string> <string name="pref_followQueue_sum">Gå til næste element i køen når afspilningen er færdig</string> <string name="pref_auto_delete_sum">Slet udsendelsen når afspilningen er færdig</string> <string name="pref_auto_delete_title">Slet automatisk</string> @@ -357,7 +372,7 @@ <string name="pref_favorite_keeps_episodes_sum">Behold udsendelser, som er markeret som foretrukne</string> <string name="pref_favorite_keeps_episodes_title">Behold foretrukne udsendelser</string> <string name="playback_pref">Afspilning</string> - <string name="playback_pref_sum">Hovedtelefon kontrol, Overspring intervaller, Kø</string> + <string name="playback_pref_sum">Hovedtelefonstyring, overspringsintervaller, kø</string> <string name="network_pref">Netværk</string> <string name="network_pref_sum">Opdateringsinterval, overførselsindstillinger, mobildata</string> <string name="pref_autoUpdateIntervallOrTime_title">Opdateringsinterval eller -klokkeslæt</string> @@ -366,8 +381,11 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Slå fra</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Indstil interval</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Indstil klokkeslæt</string> - <string name="pref_autoUpdateIntervallOrTime_every">hver %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">kl. %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Hver time</item> + <item quantity="other">Hver %d. time</item> + </plurals> <string name="pref_followQueue_title">Kontinuerlig afspilning</string> <string name="pref_pauseOnHeadsetDisconnect_title">Hovedtelefoner eller bluetooth afbrudt</string> <string name="pref_unpauseOnHeadsetReconnect_title">Tilslutning af hovedtelefoner igen</string> @@ -401,7 +419,7 @@ <string name="pref_episode_cache_title">Mellemlager for udsendelser</string> <string name="pref_episode_cache_summary">Total nummer af episoder downloades på din enhed. Stop automatisk download, når dette nummer nåes.</string> <string name="pref_episode_cover_title">Brug udsendelsesbillede</string> - <string name="pref_episode_cover_summary">Brug det udsendelsesspecifikke billede når muligt. Hvis dette slås fra, vil appen altid bruge podcastens billede.</string> + <string name="pref_show_remain_time_title">Hvis tilbageværende tid</string> <string name="pref_theme_title_use_system">Brug systemtema</string> <string name="pref_theme_title_light">Lys</string> <string name="pref_theme_title_dark">Mørk</string> @@ -421,10 +439,8 @@ <string name="pref_gpodnet_full_sync_title">Tving fuld synkronisering</string> <string name="pref_gpodnet_full_sync_sum">Synkroniser tilstande for alle abonnementer og udsendelser med gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Logget ind som <i>%1$s</i> med enheden <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Synkronisering mislykkedes</string> - <string name="pref_gpodnet_notifications_sum">Denne indstilling vedrører ikke godkendelsesfejl.</string> <string name="pref_playback_speed_sum">Tilpas de tilgængelige hastigheder for afspilning med variabel hastighed</string> - <string name="pref_feed_playback_speed_sum">Hastighed, der skal bruges, når lydafspilning startes til afsnit i denne podcast</string> + <string name="pref_feed_playback_speed_sum">Hastighed, der skal bruges, når lydafspilning startes for udsendelser i denne podcast</string> <string name="pref_feed_skip">Overspring automatisk</string> <string name="pref_feed_skip_sum">Overspring introduktioner og slut kreditter.</string> <string name="pref_feed_skip_ending">Overspring sidste</string> @@ -437,8 +453,6 @@ <string name="pref_fast_forward_sum">Indstil antallet af sekunder, der skal springes fremad, når der trykkes på fremadspolingsknappen</string> <string name="pref_rewind">Tidshop for tilbagespoling</string> <string name="pref_rewind_sum">Indstil antallet af sekunder, der skal springes tilbage, når der trykkes på tilbagespolingsknappen</string> - <string name="pref_gpodnet_sethostname_title">Indstil værtsnavn</string> - <string name="pref_gpodnet_sethostname_use_default_host">Brug standardvært</string> <string name="pref_expandNotify_title">Høj prioritet for notifikation</string> <string name="pref_expandNotify_sum">Dette udvider normalt notifikationen til at vise afspilningsknapper.</string> <string name="pref_persistNotify_title">Vedholdende afspilningsknapper</string> @@ -449,10 +463,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Du kan højst vælge %1$d knapper.</string> <string name="pref_lockscreen_background_title">Indstil baggrund på låseskærmen</string> <string name="pref_lockscreen_background_sum">Sæt baggrunden på låseskærmen til billedet for den aktuelle udsendelse. Som en sidevirkning vil det også vise billedet i tredjepartsapps.</string> - <string name="pref_showDownloadReport_title">Overførsel mislykkedes</string> - <string name="pref_showDownloadReport_sum">Lav en rapport, som viser detaljer om fejlene, hvis overførsler fejler</string> - <string name="pref_showAutoDownloadReport_title">Automatisk overførsel fuldført</string> - <string name="pref_showAutoDownloadReport_sum">Vis en notifikation for automatisk overførte udsendelser</string> <string name="pref_expand_notify_unsupport_toast">Android-versioner før 4.1 understøtter ikke udvidede notifikationer.</string> <string name="pref_enqueue_location_title">Placering i kø</string> <string name="pref_enqueue_location_sum">Føj udsendelser til: %1$s</string> @@ -462,10 +472,11 @@ <string name="pref_smart_mark_as_played_disabled">Slået fra</string> <string name="pref_image_cache_size_title">Størrelse på mellemlager (cache) for billeder</string> <string name="pref_image_cache_size_sum">Størrelse på diskmellemlageret (disk cache) for billeder</string> - <string name="visit_user_forum">Bruger froum</string> + <string name="documentation_support">Dokumentation & Support</string> + <string name="visit_user_forum">Brugerforum</string> <string name="bug_report_title">Rapportér fejl i appen</string> <string name="open_bug_tracker">Åbn programfejlsdatabase</string> - <string name="export_logs">Eksportere log</string> + <string name="export_logs">Eksportér logge</string> <string name="copy_to_clipboard">Kopier til udklipsholder</string> <string name="copied_to_clipboard">Kopieret til udklipsholder</string> <string name="experimental_pref">Eksperimentelt</string> @@ -473,14 +484,14 @@ <string name="pref_current_value">Nuværende værdi: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Indstil en netværksproxy</string> - <string name="pref_faq">Ofte Stillede Spørgsmål</string> <string name="pref_no_browser_found">Ingen webbrowser fundet</string> <string name="pref_cast_title">Chromecast-understøttelse</string> <string name="pref_cast_message_play_flavor">Aktiver understøttelse af fjernafspilning på Cast-enheder (såsom Chromecast, højttalere med indbygget Chromecast, eller Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast kræver tredjeparts proprietære biblioteker, som er slået fra i denne version af AntennaPod</string> <string name="pref_enqueue_downloaded_title">Sæt overførte udsendelser i kø</string> <string name="pref_enqueue_downloaded_summary">Føj downloadede udsendelser til køen</string> - <string name="media_player_builtin">Indbygget Android-afspiller</string> + <string name="media_player_builtin">Indbygget Android-afspiller (deprekeret)</string> + <string name="media_player_sonic">Sonic-medieafspiller (deprekeret)</string> <string name="media_player_exoplayer_recommended">ExoPlayer (anbefalet)</string> <string name="media_player_switch_to_exoplayer">Skift til ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Skiftet til ExoPlayer.</string> @@ -501,7 +512,7 @@ <string name="back_button_go_to_page">Gå til side ...</string> <string name="back_button_go_to_page_title">Vælg side</string> <string name="pref_delete_removes_from_queue_title">Slet fjernet fra kø</string> - <string name="pref_delete_removes_from_queue_sum">Fjern automatisk afsnit fra køen, når den slettes.</string> + <string name="pref_delete_removes_from_queue_sum">Fjern automatisk udsendelse fra køen, når den slettes.</string> <string name="pref_filter_feed_title">Abonnement filter</string> <string name="pref_filter_feed_sum">Filtrer dine abonnementer i navigationspanelet og på abonnementsoversigten.</string> <string name="no_filter_label">Ingen</string> @@ -513,9 +524,9 @@ <string name="not_kept_updated">Ikke holdt opdateret</string> <!--About screen--> <string name="about_pref">Om</string> - <string name="antennapod_version">AntennaPod version</string> - <string name="contributors">Bidragere</string> - <string name="contributors_summary">Alle kan hjælpe med at lave AntennaPod bedre - med kode, oversættelse eller hjælpe brugere i vores forum</string> + <string name="antennapod_version">AntennaPod-version</string> + <string name="contributors">Bidragydere</string> + <string name="contributors_summary">Alle kan hjælpe med at lave AntennaPod bedre - med kode, oversættelse eller ved at hjælpe brugere i vores forum</string> <string name="developers">Udviklere</string> <string name="translators">Oversættere</string> <string name="special_thanks">Særlig tak</string> @@ -541,34 +552,35 @@ <string name="html">HTML</string> <string name="html_export_summary">Vis abonnementer</string> <string name="opml_export_summary">Flyt abonnementer til anden podcast program</string> - <string name="opml_import_summary">Importerer abonnementer fra anden podcast program</string> - <string name="database_export_summary">Flyt abonnementer, aflyttede afsnit og kø til AntennaPod på en anden enhed</string> - <string name="database_import_summary">Importere AntennaPod database fra anden enhed</string> + <string name="opml_import_summary">Importér dine abonnementer fra et andet podcast-program</string> + <string name="database_export_summary">Flyt abonnementer, aflyttede udsendelser og kø til AntennaPod på en anden enhed</string> + <string name="database_import_summary">Importér AntennaPod-database fra en anden enhed</string> <string name="opml_import_label">OPML-import</string> - <string name="opml_add_podcast_label">Importere podcast liste (OPML)</string> + <string name="opml_add_podcast_label">Importér podcastliste (OPML)</string> <string name="opml_reader_error">Der opstod en fejl, da OPML-dokumentet blev forsøgt indlæst:</string> <string name="opml_import_error_no_file">Ingen fil valgt!</string> <string name="select_all_label">Vælg alle</string> <string name="deselect_all_label">Fravælg alle</string> <string name="opml_export_label">OPML-eksport</string> <string name="html_export_label">HTML-eksport</string> - <string name="database_export_label">Eksportere database</string> - <string name="database_import_label">Importere database</string> - <string name="database_import_warning">Importerong af ny database vil overskrive alle dine nuværende abonnenter og lytte historik. Du burde exportere din nuværende database som en backup. Vil du overskrive?</string> + <string name="database_export_label">Eksport af database</string> + <string name="database_import_label">Importér database</string> + <string name="database_import_warning">Import af database vil overskrive alle dine nuværende abonnementer og din afspilningshistorik. Du bør eksportere din nuværende database som en sikkerhedskopi først. Vil du overskrive?</string> <string name="please_wait">Vent...</string> <string name="export_error_label">Eksportfejl</string> <string name="export_success_title">Eksport lykkedes</string> <string name="export_success_sum">Den eksporterede fil blev skrevet til:\n\n%1$s</string> <string name="opml_import_ask_read_permission">Adgang til eksternt lager er påkrævet for at læse OPML-filen</string> - <string name="import_select_file">Vælg fil til import</string> + <string name="import_select_file">Vælg fil der skal importeres</string> <string name="successful_import_label">Importeret</string> <string name="import_ok">Tryk venligst OK for at genstarte AntennaPod</string> - <string name="import_no_downgrade">Databasen var exporteret af en nyere version af AntennaPod. Din nuværende installation ved ikke endnu hvordan den skal håndtere denne fil.</string> - <string name="favorites_export_label">Foretrukne eksport</string> - <string name="favorites_export_summary">Eksportere gemte foretrukne til fil</string> + <string name="import_no_downgrade">Databasen blev eksporteret fra en nyere version af AntennaPod. Din nuværende installation ved endnu ikke hvordan den skal håndtere denne fil.</string> + <string name="favorites_export_label">Eksport af foretrukne</string> + <string name="favorites_export_summary">Eksportér gemte foretrukne til fil</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Indstil søvntimer</string> <string name="disable_sleeptimer_label">Slå søvntimer fra</string> + <string name="extend_sleep_timer_label">+%d minutter</string> <string name="sleep_timer_label">Søvntimer</string> <string name="time_dialog_invalid_input">Ugyldig indtastning: tid skal være et heltal</string> <string name="shake_to_reset_label">Ryst for at nulstille</string> @@ -596,22 +608,20 @@ <string name="gpodnet_suggestions_header">FORSLAG</string> <string name="gpodnet_search_hint">Søg på gpodder.net</string> <string name="gpodnetauth_login_title">Log ind</string> - <string name="gpodnetauth_login_descr">Velkommen til gpodder.nets loginproces. Skriv først dine loginoplysninger:</string> <string name="gpodnetauth_login_butLabel">Log ind</string> - <string name="gpodnetauth_login_register">Hvis du ikke har en konto endnu, kan du oprette en her:\nhttps://gpodder.net/register/</string> + <string name="create_account">Opret konto</string> <string name="username_label">Brugernavn</string> <string name="password_label">Adgangskode</string> - <string name="gpodnetauth_device_title">Valg af enhed</string> + <string name="gpodnetauth_server_official">Officiel gpodder.net server</string> + <string name="gpodnetauth_host">Værtsnavn</string> + <string name="gpodnetauth_select_server">Vælg server</string> <string name="gpodnetauth_device_descr">Opret en ny enhed til at bruge med din gpodder.net-konto eller vælg en eksisterende:</string> - <string name="gpodnetauth_device_deviceID">Enheds-id:\u0020</string> - <string name="gpodnetauth_device_caption">Enhedsnavn</string> - <string name="gpodnetauth_device_butCreateNewDevice">Opret ny enhed</string> - <string name="gpodnetauth_device_chooseExistingDevice">Vælg en eksisterende enhed:</string> - <string name="gpodnetauth_device_errorEmpty">Enheds-id må ikke være tomt</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Enheds-id er allerede i brug</string> + <string name="gpodnetauth_device_name">Enhedsnavn</string> + <string name="gpodnetauth_device_name_default">AntennaPod på %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Enhedsnavn må ikke være tomt</string> + <string name="gpodnetauth_existing_devices">Eksisterende enheder</string> + <string name="gpodnetauth_create_device">Opret ny enhed</string> <string name="gpodnetauth_device_butChoose">Vælg</string> - <string name="gpodnetauth_finish_title">Login lykkedes!</string> <string name="gpodnetauth_finish_descr">Tillykke! Din gpodder.net-konto er nu forbundet med din enhed. AntennaPod vil fra nu af automatisk synkronisere dine abonnementer på din enhed med din gpodder.net-konto.</string> <string name="gpodnetauth_finish_butsyncnow">Start synkronisering nu</string> <string name="gpodnetauth_finish_butgomainscreen">Gå til hovedskærmen</string> @@ -659,7 +669,7 @@ <string name="media_type_video_label">Video</string> <string name="status_downloading_label">Udsendelse overføres</string> <string name="in_queue_label">Udsendelse er i køen</string> - <string name="is_favorite_label">Afsnit er markeret som favorit</string> + <string name="is_favorite_label">Udsendelse er markeret som foretrukken</string> <string name="drag_handle_content_description">Træk for at ændre dette elements placering</string> <string name="load_next_page_label">Indlæs næste side</string> <string name="switch_pages">Skift sider</string> @@ -677,8 +687,8 @@ <string name="keep_updated">Hold opdateret</string> <string name="keep_updated_summary">Inkludere denne podcast når alle podcast genindlæses</string> <string name="auto_download_disabled_globally">Automatisk download slået fra i de generelle AntennaPod indstilinger</string> - <string name="statistics_listened_for">Lytter efter:</string> - <string name="statistics_episodes_on_device">Afsnit på enhed:</string> + <string name="statistics_listened_for">Lyttet i:</string> + <string name="statistics_episodes_on_device">Udsendelser på enheden:</string> <string name="statistics_space_used">Plads brugt:</string> <string name="statistics_view_all">Vis for alle podcasts »</string> <!--Progress information--> @@ -691,7 +701,7 @@ <string name="search_podcastindex_label">Søg på Podcastindex.org</string> <string name="search_fyyd_label">Søg i fyyd</string> <string name="advanced">Avanceret</string> - <string name="add_podcast_by_url">Tilføj podcast via RSS adresse</string> + <string name="add_podcast_by_url">Tilføj podcast via RSS-adresse</string> <string name="browse_gpoddernet_label">Gennemse gpodder.net</string> <string name="discover">Opdag</string> <string name="discover_hide">Gem</string> @@ -758,7 +768,7 @@ <string name="share_dialog_include_label">Medtag:</string> <string name="share_playback_position_dialog_label">Afspilningsposition</string> <string name="share_dialog_media_file_url_label">Adresse på mediefil</string> - <string name="share_dialog_episode_website_label">Afsnit netsted</string> + <string name="share_dialog_episode_website_label">Adresse på udsendelse</string> <string name="share_dialog_media_file_label">Mediefil</string> <!--Audio controls--> <string name="audio_controls">Lydknapper</string> @@ -799,18 +809,20 @@ <string name="cast_failed_receiver_player_error">Modtagerafspilleren er stødt på en alvorlig fejl</string> <string name="cast_failed_media_error_skipping">Fejl ved afspilning af medie. Springer over…</string> <!--Notification channels--> + <string name="notification_group_errors">Fejl</string> + <string name="notification_group_news">Nyheder</string> <string name="notification_channel_user_action">Handling påkrævet</string> <string name="notification_channel_user_action_description">Vist hvis din handling er nødvendig, for eksempel hvis du skal skrive et kodeord.</string> <string name="notification_channel_downloading">Henter</string> <string name="notification_channel_downloading_description">Vises samtidig med den hentes.</string> <string name="notification_channel_playing">Spiller nu</string> <string name="notification_channel_playing_description">Giver adgang til at kontrollere afspilning. Dette er den mest normale notifikation du vil se, mens du afspiller en podcast.</string> - <string name="notification_channel_error">Fejl</string> - <string name="notification_channel_error_description">Vises hvis noget gik galt, for eksempel hvis en overførsel eller feed-opdatering fejlede.</string> - <string name="notification_channel_sync_error">Synkroniseringsfejl</string> + <string name="notification_channel_download_error">Overførsel mislykkedes</string> + <string name="notification_channel_sync_error">Synkronisering mislykkedes</string> <string name="notification_channel_sync_error_description">Vises når gpodder-synkronisering fejler.</string> - <string name="notification_channel_auto_download">Automatisk hentninger</string> + <string name="notification_channel_auto_download">Automatisk overførsel fuldført</string> <string name="notification_channel_episode_auto_download">Vist når episoder automatisk var blevet downloaded.</string> + <string name="notification_channel_new_episode">Ny episode</string> <!--Widget settings--> <string name="widget_settings">Kontrol opsætning</string> <string name="widget_create_button">Opret kontrol</string> diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 1beeeb0af..6eb7bbec2 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statistiken</string> <string name="add_feed_label">Podcast hinzufügen</string> <string name="episodes_label">Episoden</string> + <string name="queue_label">Warteschlange</string> <string name="all_episodes_short_label">Alle</string> <string name="new_episodes_label">Neu</string> <string name="favorite_episodes_label">Favoriten</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Wiedergabe</string> <string name="download_statistics_label">Downloads</string> <string name="notification_pref_fragment">Benachrichtigungen</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" nicht gefunden</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Gesamtzeit aller gespielten Episoden</string> <string name="statistics_details_dialog">%1$d von %2$d Episoden gestartet.\n\n%3$s von %4$s Episoden gespielt.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Keine</string> <!--Bug report activity--> <string name="log_file_share_exception">Keine kompatiblen Apps gefunden</string> + <string name="export_logs_menu_title">Detaillierte Logs exportieren</string> + <string name="confirm_export_log_dialog_message">Detaillierte Logs können persönliche Informationen enthalten, wie zum Beispiel die Liste deiner Abonnements</string> <!--Webview actions--> <string name="open_in_browser_label">Im Browser öffnen</string> <string name="copy_url_label">URL kopieren</string> @@ -81,7 +86,6 @@ <string name="description_label">Beschreibung</string> <string name="episodes_suffix">\u0020Episoden</string> <string name="processing_label">Verarbeite</string> - <string name="save_username_password_label">Benutzername und Password merken</string> <string name="close_label">Schließen</string> <string name="retry_label">Erneut versuchen</string> <string name="auto_download_label">Automatisch herunterladen</string> @@ -93,7 +97,7 @@ <string name="feed_volume_reduction_off">Aus</string> <string name="feed_volume_reduction_light">Schwach</string> <string name="feed_volume_reduction_heavy">Stark</string> - <string name="parallel_downloads_suffix">\u0020gleichzeitige Downloads</string> + <string name="parallel_downloads">%1$d parallele Downloads</string> <string name="feed_auto_download_global">Standardwert</string> <string name="feed_auto_download_always">Immer</string> <string name="feed_auto_download_never">Nie</string> @@ -113,7 +117,22 @@ <item quantity="one">%d ausgewählt</item> <item quantity="other">%d ausgewählt</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d Episode</item> + <item quantity="other">%d Episoden</item> + </plurals> <string name="loading_more">Lade mehr...</string> + <string name="episode_notification">Episodenbenachrichtigung</string> + <string name="episode_notification_summary">Benachrichtigung beim Erscheinen einer neuen Episode anzeigen.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">Neue Episode bei %2$s</item> + <item quantity="other">%1$d neue Episoden bei %2$s</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Neue Episode</item> + <item quantity="other">Neue Episoden</item> + </plurals> + <string name="new_episode_notification_group_text">Deine Abonnements haben neue Episoden.</string> <!--Actions on feeds--> <string name="mark_all_read_label">Alle als gespielt markieren</string> <string name="mark_all_read_msg">Alle Episoden als gespielt markiert</string> @@ -145,7 +164,6 @@ <string name="hide_not_queued_episodes_label">Nicht in Warteschlange</string> <string name="hide_has_media_label">Hat Medien</string> <string name="filtered_label">Gefiltert</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Aktualisierung fehlgeschlagen</string> <string name="open_podcast">Podcast öffnen</string> <string name="please_wait_for_data">Bitte warte, bis die Daten geladen sind</string> <!--actions on feeditems--> @@ -160,6 +178,10 @@ <string name="delete_label">Löschen</string> <string name="delete_failed">Die Datei kann nicht gelöscht werden. Eventuell hilft es, das Gerät neu zu starten.</string> <string name="delete_episode_label">Episode löschen</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d Episode ausgewählt, %d Download gelöscht.</item> + <item quantity="other">%d Episode ausgewählt, %d Download(s) gelöscht.</item> + </plurals> <string name="remove_new_flag_label">\"Neu\"-Markierung entfernen</string> <string name="removed_new_flag_label">\"Neu\"-Markierung entfernt</string> <string name="mark_read_label">Als gespielt markieren</string> @@ -206,16 +228,12 @@ <string name="download_error_details">Details</string> <string name="download_error_details_message">%1$s \n\nDatei-URL:\n%2$s</string> <string name="download_error_device_not_found">Speichermedium nicht gefunden</string> - <string name="download_error_insufficient_space">Zu wenig Speicherplatz</string> <string name="download_error_http_data_error">HTTP Datenfehler</string> <string name="download_error_error_unknown">Unbekannter Fehler</string> - <string name="download_error_parser_exception">Parserfehler</string> <string name="download_error_unsupported_type">Nicht unterstützter Feed-Typ</string> <string name="download_error_connection_error">Verbindungsfehler</string> - <string name="download_error_unknown_host">Unbekannter Host</string> <string name="download_error_unauthorized">Authentifizierungsfehler</string> <string name="download_error_file_type_type">Dateityp-Fehler</string> - <string name="download_error_forbidden">Verboten</string> <string name="download_canceled_msg">Download abgebrochen</string> <string name="download_canceled_autodownload_enabled_msg">Download abgebrochen\n<i>Automatischen Download</i> für diese Episode deaktiviert</string> <string name="download_report_title">Downloads endeten mit Fehler(n)</string> @@ -229,12 +247,7 @@ <item quantity="one">%d Download übrig</item> <item quantity="other">%d Downloads übrig</item> </plurals> - <string name="downloads_processing">Verarbeite Downloads</string> <string name="download_notification_title">Lade Podcast-Daten</string> - <plurals name="download_report_content"> - <item quantity="one">%d Download erfolgreich, %d fehlgeschlagen</item> - <item quantity="other">%d Downloads erfolgreich, %d fehlgeschlagen</item> - </plurals> <string name="download_log_title_unknown">Unbekannter Titel</string> <string name="download_type_feed">Feed</string> <string name="download_type_media">Mediendatei</string> @@ -323,7 +336,6 @@ <string name="storage_pref">Speicher</string> <string name="storage_sum">Automatisches Löschen von Episoden, Importieren, Exportieren</string> <string name="project_pref">Projekt</string> - <string name="queue_label">Warteschlange</string> <string name="synchronization_pref">Synchronisation</string> <string name="synchronization_sum">Synchronisiere über gpodder.net mit anderen Geräten</string> <string name="automation">Automatisierung</string> @@ -339,14 +351,14 @@ <string name="preference_search_clear_history">Verlauf leeren</string> <string name="media_player">Medienabspieler</string> <string name="pref_episode_cleanup_title">Automatisches Löschen</string> - <string name="pref_episode_cleanup_summary">Episoden, die weder in der Warteschlange noch Favoriten sind, können gelöscht werden, wenn beim automatischen Herunterladen Speicherplatz für neue Episoden gebraucht wird</string> + <string name="pref_episode_cleanup_summary">Episoden, die gelöscht werden können, wenn beim automatischen Herunterladen Platz für neue Episoden benötigt wird</string> <string name="pref_pauseOnDisconnect_sum">Wiedergabe pausieren, wenn Kopfhörer ausgesteckt oder Bluetooth getrennt wird</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Wiedergabe fortsetzen, wenn Kopfhörer wieder eingesteckt werden</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Wiedergabe fortsetzen, wenn Bluetooth wieder verbunden ist</string> - <string name="pref_hardwareForwardButtonSkips_title">\"Nächster\"-Taste springt zur nächsten Episode</string> - <string name="pref_hardwareForwardButtonSkips_sum">Wenn ein Vorwärts Button auf einem Bluetooth Gerät gedrückt wird, überspringe die nächste Episode anstatt vorzuspulen.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Vorheriger-Taste startet neu</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Die Wiedergabe der aktuellen Episode neu starten, wenn die \"Vorheriger\"-Taste gedrückt wird (statt zurückzuspulen)</string> + <string name="button_action_fast_forward">Vorspulen</string> + <string name="button_action_rewind">Zurückspulen</string> + <string name="button_action_skip_episode">Episode überspringen</string> + <string name="button_action_restart_episode">Episode erneut wiedergeben</string> <string name="pref_followQueue_sum">Springe zur nächsten Episode in der Warteschlange, wenn die Wiedergabe endet</string> <string name="pref_auto_delete_sum">Episode löschen, wenn die Wiedergabe endet</string> <string name="pref_auto_delete_title">Automatisches Löschen</string> @@ -366,8 +378,11 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Deaktivieren</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Intervall einstellen</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Tageszeit festlegen</string> - <string name="pref_autoUpdateIntervallOrTime_every">jede %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">um %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Jede Stunde</item> + <item quantity="other">Alle %d Stunden</item> + </plurals> <string name="pref_followQueue_title">Durchgehendes Abspielen</string> <string name="pref_pauseOnHeadsetDisconnect_title">Kopfhörer oder Bluetooth getrennt</string> <string name="pref_unpauseOnHeadsetReconnect_title">Kopfhörer wieder eingesteckt</string> @@ -401,7 +416,9 @@ <string name="pref_episode_cache_title">Episodenspeicher</string> <string name="pref_episode_cache_summary">Gesamtzahl an Episoden, die auf dem Gerät gespeichert werden. Automatisches Herunterladen wird pausiert, wenn diese Anzahl erreicht ist.</string> <string name="pref_episode_cover_title">Episoden-Bilder verwenden</string> - <string name="pref_episode_cover_summary">Benutze Episoden-Bilder, wenn verfügbar. Ist die Option deaktiviert, wird immer das Podcast-Bild angezeigt.</string> + <string name="pref_episode_cover_summary">Falls verfügbar episodenspezifische Titelbilder in Listen verwenden. Falls nicht ausgewählt wird immer das Titelbild des Podcasts verwendet.</string> + <string name="pref_show_remain_time_title">Verbleibende Zeit anzeigen</string> + <string name="pref_show_remain_time_summary">Zeigt, falls ausgewählt, die verbleibende Zeit für die Episode, andernfalls die gesamte verbleibende Zeit aller Episoden.</string> <string name="pref_theme_title_use_system">System-Design verwenden</string> <string name="pref_theme_title_light">Hell</string> <string name="pref_theme_title_dark">Dunkel</string> @@ -421,8 +438,6 @@ <string name="pref_gpodnet_full_sync_title">Komplette Synchronisation erzwingen</string> <string name="pref_gpodnet_full_sync_sum">Kompletten Abonnement- und Episoden-Status mit gpodder.net synchronisieren.</string> <string name="pref_gpodnet_login_status"><![CDATA[Eingeloggt als <i>%1$s</i> mit dem Gerät <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Synchronisation fehlgeschlagen</string> - <string name="pref_gpodnet_notifications_sum">Diese Einstellung gilt nicht für Authentifizierungsfehler.</string> <string name="pref_playback_speed_sum">Anpassen der verfügbaren Geschwindigkeiten für die Wiedergabe mit variabler Geschwindigkeit</string> <string name="pref_feed_playback_speed_sum">Abspielgeschwindigkeit für Episoden dieses Podcasts</string> <string name="pref_feed_skip">Automatisches Überspringen</string> @@ -437,8 +452,6 @@ <string name="pref_fast_forward_sum">Passe an, wie viele Sekunden vorgespult wird, wenn die entsprechende Hardware-Taste gedrückt wird</string> <string name="pref_rewind">Rückspulzeit</string> <string name="pref_rewind_sum">Passe an, wie viele Sekunden zurückgespult wird, wenn die entsprechende Hardware-Taste gedrückt wird</string> - <string name="pref_gpodnet_sethostname_title">Hostname ändern</string> - <string name="pref_gpodnet_sethostname_use_default_host">Standard-Host verwenden</string> <string name="pref_expandNotify_title">Hohe Benachrichtigungspriorität</string> <string name="pref_expandNotify_sum">Dies erweitert normalerweise die Benachrichtigung und zeigt so die Wiedergabe-Buttons an.</string> <string name="pref_persistNotify_title">Persistente Wiedergabesteuerung</string> @@ -449,10 +462,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Du kannst maximal %1$d Elemente auswählen.</string> <string name="pref_lockscreen_background_title">Lockscreen-Hintergrund einstellen</string> <string name="pref_lockscreen_background_sum">Verwende das aktuelle Episodenbild als Lockscreen-Hintergrund. Es wird als Nebeneffekt auch in anderen Apps gezeigt.</string> - <string name="pref_showDownloadReport_title">Download fehlgeschlagen</string> - <string name="pref_showDownloadReport_sum">Wenn Downloads fehlschlagen, erstelle einen Bericht, der die Details des Fehlschlages beschreibt.</string> - <string name="pref_showAutoDownloadReport_title">Automatischer Download abgeschlossen</string> - <string name="pref_showAutoDownloadReport_sum">Zeige eine Benachrichtigung für automatisch heruntergeladene Episoden.</string> <string name="pref_expand_notify_unsupport_toast">Android-Versionen vor 4.1 unterstützen keine erweiterten Benachrichtigungen.</string> <string name="pref_enqueue_location_title">Position beim Einreihen</string> <string name="pref_enqueue_location_sum">Füge Episoden %1$s hinzu</string> @@ -462,6 +471,7 @@ <string name="pref_smart_mark_as_played_disabled">Deaktiviert</string> <string name="pref_image_cache_size_title">Größe des Bilder-Zwischenspeichers</string> <string name="pref_image_cache_size_sum">Größe des Zwischenspeichers für Bilder</string> + <string name="documentation_support">Dokumentation + Support</string> <string name="visit_user_forum">Benutzerforum</string> <string name="bug_report_title">Fehler melden</string> <string name="open_bug_tracker">Bug-Tracker öffnen</string> @@ -473,14 +483,14 @@ <string name="pref_current_value">Aktueller Wert: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Richte einen Netzwerk-Proxy ein</string> - <string name="pref_faq">Häufig gestellte Fragen</string> <string name="pref_no_browser_found">Kein Browser gefunden.</string> <string name="pref_cast_title">Chromecast-Unterstützung</string> <string name="pref_cast_message_play_flavor">Aktiviere die Unterstützung von Cast-Geräten (Chromecast, Lautsprecher oder Android TV) zum entfernten Abspielen</string> <string name="pref_cast_message_free_flavor">Chromecast benötigt proprietäre Bibliotheken von Drittanbietern, die in dieser Version von AntennaPod deaktiviert sind</string> <string name="pref_enqueue_downloaded_title">Downloads einreihen</string> <string name="pref_enqueue_downloaded_summary">Füge heruntergeladene Episoden zur Warteschlange hinzu</string> - <string name="media_player_builtin">Androids eingebauter Abspieler</string> + <string name="media_player_builtin">Integrierter Android Player (veraltet) </string> + <string name="media_player_sonic">Sonic Media Player (veraltet) </string> <string name="media_player_exoplayer_recommended">ExoPlayer (empfohlen)</string> <string name="media_player_switch_to_exoplayer">Zu ExoPlayer wechseln</string> <string name="media_player_switched_to_exoplayer">Zu ExoPlayer gewechselt.</string> @@ -569,6 +579,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Timer einstellen</string> <string name="disable_sleeptimer_label">Schlummerfunktion deaktivieren</string> + <string name="extend_sleep_timer_label">+%d min</string> <string name="sleep_timer_label">Schlummerfunktion</string> <string name="time_dialog_invalid_input">Ungültige Eingabe, Zeit muss eine Ganzzahl sein</string> <string name="shake_to_reset_label">Durch Schütteln zurücksetzen</string> @@ -596,22 +607,22 @@ <string name="gpodnet_suggestions_header">VORSCHLÄGE</string> <string name="gpodnet_search_hint">gpodder.net durchsuchen</string> <string name="gpodnetauth_login_title">Anmeldung</string> - <string name="gpodnetauth_login_descr">Willkommen beim gpodder.net Anmeldeprozess. Gib zuerst deine Anmeldeinformationen ein:</string> <string name="gpodnetauth_login_butLabel">Anmelden</string> - <string name="gpodnetauth_login_register">Falls du noch kein gpodder.net Profil hast, kannst du hier eines erstellen: https://gpodder.net/register/</string> + <string name="create_account">Konto anlegen</string> <string name="username_label">Benutzername</string> <string name="password_label">Passwort</string> - <string name="gpodnetauth_device_title">Geräte-Auswahl</string> + <string name="gpodnet_description">Gpodder.net ist ein Open-Source-Dienst zur Synchronisation von Podcasts und unabhängig vom AntennaPod-Projekt.</string> + <string name="gpodnetauth_server_official">Offizieller Server gpodder.net</string> + <string name="gpodnetauth_server_custom">Anderer Server</string> + <string name="gpodnetauth_host">Hostname</string> + <string name="gpodnetauth_select_server">Server auswählen</string> <string name="gpodnetauth_device_descr">Erstelle ein neues Gerät für dein gpodder.net Profil oder wähle ein bereits vorhandenes:</string> - <string name="gpodnetauth_device_deviceID">Geräte-ID:\u0020</string> - <string name="gpodnetauth_device_caption">Beschreibung</string> - <string name="gpodnetauth_device_butCreateNewDevice">Neues Gerät erstellen</string> - <string name="gpodnetauth_device_chooseExistingDevice">Vorhandenes Gerät auswählen</string> - <string name="gpodnetauth_device_errorEmpty">Geräte-ID darf nicht leer sein</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Geräte-ID wird bereits verwendet</string> + <string name="gpodnetauth_device_name">Gerätename</string> + <string name="gpodnetauth_device_name_default">AntennaPod auf %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Beschreibung darf nicht leer sein</string> + <string name="gpodnetauth_existing_devices">Bestehende Geräte</string> + <string name="gpodnetauth_create_device">Gerät anlegen</string> <string name="gpodnetauth_device_butChoose">Auswählen</string> - <string name="gpodnetauth_finish_title">Anmeldung erfolgreich!</string> <string name="gpodnetauth_finish_descr">Glückwunsch! Dein gpodder.net Profil ist jetzt mit deinem Gerät verbunden. Von nun an wird AntennaPod automatisch deine Abonnements mit deinem gpodder.net Profil synchronisieren.</string> <string name="gpodnetauth_finish_butsyncnow">Jetzt synchronisieren</string> <string name="gpodnetauth_finish_butgomainscreen">Zum Hauptbildschirm zurückkehren</string> @@ -665,6 +676,7 @@ <string name="switch_pages">Seiten wechseln</string> <string name="position">Position: %1$s</string> <string name="apply_action">Aktion anwenden</string> + <string name="play_chapter">Kapitel abspielen</string> <!--Feed information screen--> <string name="authentication_label">Authentifizierung</string> <string name="authentication_descr">Ändere den Benutzernamen und das Passwort für diesen Podcast und dessen Episoden.</string> @@ -799,18 +811,20 @@ <string name="cast_failed_receiver_player_error">Es wurde ein schwerer Fehler beim Empfangsgerät festgestellt</string> <string name="cast_failed_media_error_skipping">Fehler bei Wiedergabe. Überspringe...</string> <!--Notification channels--> + <string name="notification_group_errors">Fehler</string> + <string name="notification_group_news">Nachrichten</string> <string name="notification_channel_user_action">Handlung notwendig</string> <string name="notification_channel_user_action_description">Wird gezeigt, wenn deine Handlung notwendig ist, zum Beispiel wenn du ein Passwort eingeben musst.</string> <string name="notification_channel_downloading">Herunterladen</string> <string name="notification_channel_downloading_description">Wird gezeigt beim Herunterladen.</string> <string name="notification_channel_playing">Jetzt spielt</string> <string name="notification_channel_playing_description">Erlaubt es, die Wiedergabe zu steuern. Dies ist die Hauptbenachrichtigung, die du siehst, während ein Podcast abgespielt wird.</string> - <string name="notification_channel_error">Fehler</string> - <string name="notification_channel_error_description">Wird bei einem Problem angezeigt, wenn zum Beispiel ein Download oder die Aktualisierung eines Feed fehlschlägt.</string> - <string name="notification_channel_sync_error">Fehler bei der Synchronisation</string> + <string name="notification_channel_download_error">Download fehlgeschlagen</string> + <string name="notification_channel_sync_error">Synchronisation fehlgeschlagen</string> <string name="notification_channel_sync_error_description">Wird angezeigt, wenn die gpodder-Synchronisierung fehlschlägt.</string> - <string name="notification_channel_auto_download">Automatische Downloads</string> + <string name="notification_channel_auto_download">Automatischer Download abgeschlossen</string> <string name="notification_channel_episode_auto_download">Wird angezeigt, wenn Episoden automatisch heruntergeladen worden sind.</string> + <string name="notification_channel_new_episode">Neue Episode</string> <!--Widget settings--> <string name="widget_settings">Widget-Einstellungen</string> <string name="widget_create_button">Widget erstellen</string> diff --git a/core/src/main/res/values-es/strings.xml b/core/src/main/res/values-es/strings.xml index 433fad64e..b53359b2b 100644 --- a/core/src/main/res/values-es/strings.xml +++ b/core/src/main/res/values-es/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Estadísticas</string> <string name="add_feed_label">Añadir pódcast</string> <string name="episodes_label">Episodios</string> + <string name="queue_label">Cola</string> <string name="all_episodes_short_label">Todos</string> <string name="new_episodes_label">Nuevos</string> <string name="favorite_episodes_label">Favoritos</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Registro</string> <string name="subscriptions_label">Suscripciones</string> <string name="subscriptions_list_label">Lista de suscripciones</string> - <string name="cancel_download_label">Cancelar\ndescarga</string> + <string name="cancel_download_label">Cancelar Descarga</string> <string name="playback_history_label">Historial de reproducciones</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">Iniciar sesión en gpodder.net</string> @@ -26,6 +27,7 @@ <string name="playback_statistics_label">Reproducción</string> <string name="download_statistics_label">Descargas</string> <string name="notification_pref_fragment">Notificaciones</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Tiempo total de reproducción de episodios:</string> <string name="statistics_details_dialog">%1$d episodios iniciados de %2$d.\n\nReproducidos %3$s de %4$s.</string> @@ -53,6 +55,8 @@ <string name="drawer_feed_counter_none">Ninguno</string> <!--Bug report activity--> <string name="log_file_share_exception">No se encontraro apps compatibles</string> + <string name="export_logs_menu_title">Exportar registros detallados</string> + <string name="confirm_export_log_dialog_message">Los registros detallados pueden contener información sensible, como su lista de suscripciones</string> <!--Webview actions--> <string name="open_in_browser_label">Abrir en el navegador</string> <string name="copy_url_label">Copiar URL</string> @@ -81,7 +85,6 @@ <string name="description_label">Descripción</string> <string name="episodes_suffix">\u0020episodios</string> <string name="processing_label">Procesando</string> - <string name="save_username_password_label">Guardar usuario y contraseña</string> <string name="close_label">Cerrar</string> <string name="retry_label">Reintentar</string> <string name="auto_download_label">Incluir en descargas automáticas</string> @@ -89,16 +92,17 @@ <string name="auto_download_apply_to_items_message">La nueva opción <i>descarga automática</i> se aplicará automáticamente a episodios nuevos.\n¿También desea aplicarlo a episodios anteriores?</string> <string name="auto_delete_label">Borrar episodio automáticamente</string> <string name="feed_volume_reduction">Reducción de volumen</string> - <string name="feed_volume_reduction_summary">Bajar el volumen para episodios de este feed: %1$s</string> + <string name="feed_volume_reduction_summary">Bajar el volumen para episodios de este canal: %1$s</string> <string name="feed_volume_reduction_off">Apagado</string> <string name="feed_volume_reduction_light">Ligero</string> <string name="feed_volume_reduction_heavy">Fuerte</string> - <string name="parallel_downloads_suffix">\u0020descargas paralelas</string> + <string name="parallel_downloads">%1$d descargas paralelas</string> <string name="feed_auto_download_global">Global por defecto</string> <string name="feed_auto_download_always">Siempre</string> <string name="feed_auto_download_never">Nunca</string> <string name="send_label">Enviar…</string> <string name="episode_cleanup_never">Nunca</string> + <string name="episode_cleanup_except_favorite_removal">Cuando no esté en favoritos</string> <string name="episode_cleanup_queue_removal">Cuando no esté en la cola</string> <string name="episode_cleanup_after_listening">Después de acabar</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -113,7 +117,22 @@ <item quantity="one">1%d seleccionado</item> <item quantity="other">%d seleccionado</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d episodio</item> + <item quantity="other">%d episodios</item> + </plurals> <string name="loading_more">Cargando mas...</string> + <string name="episode_notification">Notificaciones de Episodios</string> + <string name="episode_notification_summary">Mostrar una notificación cuando se estrene un nuevo episodio.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s tiene un nuevo episodio</item> + <item quantity="other">%2$s tiene %1$d episodios nuevos</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Nuevo Episodio</item> + <item quantity="other">Nuevos Episodios</item> + </plurals> + <string name="new_episode_notification_group_text">Sus suscripciones tienen nuevos episodios.</string> <!--Actions on feeds--> <string name="mark_all_read_label">Marcar todos como reproducidos</string> <string name="mark_all_read_msg">Marcados todos los episodios como reproducidos</string> @@ -122,7 +141,7 @@ <string name="remove_all_new_flags_label">Eliminar todas las marcas \"nuevo\"</string> <string name="removed_all_new_flags_msg">Eliminadas todas las marcas \"nuevo\"</string> <string name="remove_all_new_flags_confirmation_msg">Por favor, confirma que quieres eliminar las marcas \"nuevo\" de todos los episodios.</string> - <string name="show_info_label">Información del programa</string> + <string name="show_info_label">Mostrar información</string> <string name="show_feed_settings_label">Mostrar ajustes del pódcast</string> <string name="feed_info_label">Información del pódcast</string> <string name="feed_settings_label">Ajustes del pódcast</string> @@ -132,7 +151,7 @@ <string name="share_label_with_ellipses">Compartir…</string> <string name="share_file_label">Compartir el archivo</string> <string name="share_website_url_label">Dirección web</string> - <string name="share_feed_url_label">URL del feed del podcast</string> + <string name="share_feed_url_label">URL del canal del podcast</string> <string name="feed_delete_confirmation_msg">Confirme que quiere borrar el pódcast \"%1$s\" y TODOS los episodios (incluidos los descargados).</string> <string name="feed_delete_confirmation_local_msg">Confirme que quiere borrar el podcast \"%1$s\". Los archivos en la carpeta origen local no serán borrados.</string> <string name="feed_remover_msg">Eliminando el pódcast</string> @@ -145,7 +164,6 @@ <string name="hide_not_queued_episodes_label">No en cola</string> <string name="hide_has_media_label">Tiene multimedia</string> <string name="filtered_label">Filtrados</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Error en la última actualización</string> <string name="open_podcast">Abrir pódcast</string> <string name="please_wait_for_data">Esperando a que los datos carguen</string> <!--actions on feeditems--> @@ -160,6 +178,10 @@ <string name="delete_label">Borrar</string> <string name="delete_failed">No se puede borrar el fichero. Reiniciar el dispositivo podría ayudar.</string> <string name="delete_episode_label">Borrar Episodio</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d episodio seleccionado, %d descarga eliminada.</item> + <item quantity="other">%d episodios seleccionados, %d descarga(s) eliminada(s).</item> + </plurals> <string name="remove_new_flag_label">Eliminar marca \"nuevo\"</string> <string name="removed_new_flag_label">Eliminada marca \"nuevo\"</string> <string name="mark_read_label">Marcar como reproducido</string> @@ -206,20 +228,16 @@ <string name="download_error_details">Detalles</string> <string name="download_error_details_message">%1$s \n\nURL de archivo:\n%2$s</string> <string name="download_error_device_not_found">No se ha encontrado un dispositivo de almacenamiento</string> - <string name="download_error_insufficient_space">Espacio insuficiente</string> <string name="download_error_http_data_error">Error de datos HTTP</string> <string name="download_error_error_unknown">Error desconocido</string> - <string name="download_error_parser_exception">Excepción del analizador</string> <string name="download_error_unsupported_type">Tipo de canal no admitido</string> <string name="download_error_connection_error">Error de conexión</string> - <string name="download_error_unknown_host">Host desconocido</string> <string name="download_error_unauthorized">Error de autenticación</string> <string name="download_error_file_type_type">Tipo de archivo erróneo</string> - <string name="download_error_forbidden">Prohibido</string> <string name="download_canceled_msg">Descarga cancelada</string> <string name="download_canceled_autodownload_enabled_msg">Descarga cancelada\nSe desactivó la <i>descarga automática</i> en este elemento</string> <string name="download_report_title">Descargas completadas con error(es)</string> - <string name="auto_download_report_title">Auto-descargas completadas</string> + <string name="auto_download_report_title">Descargas automáticas completadas</string> <string name="download_report_content_title">Informe de descargas</string> <string name="download_error_malformed_url">URL con formato incorrecto</string> <string name="download_error_io_error">Error de E/S</string> @@ -229,12 +247,7 @@ <item quantity="one">Queda %d descarga</item> <item quantity="other">Quedan %d descargas</item> </plurals> - <string name="downloads_processing">Procesando descargas</string> <string name="download_notification_title">Descargando datos del pódcast</string> - <plurals name="download_report_content"> - <item quantity="one">%d descarga exitosa, %d fallidas</item> - <item quantity="other">%d descargas exitosas, %d fallidas</item> - </plurals> <string name="download_log_title_unknown">Título desconocido</string> <string name="download_type_feed">Canal</string> <string name="download_type_media">Archivo multimedia</string> @@ -242,11 +255,11 @@ <string name="null_value_podcast_error">No se proporcionó ningún pódcast que pudiera mostrarse.</string> <string name="authentication_notification_title">Autenticación requerida</string> <string name="authentication_notification_msg">El recurso solicitado requiere un usuario y contraseña</string> - <string name="confirm_mobile_download_dialog_title">Confirmar descarga por red móvil</string> + <string name="confirm_mobile_download_dialog_title">Confirmar descarga vía datos móviles</string> <string name="confirm_mobile_download_dialog_message_not_in_queue">Se desactivaron las descargas por red de datos móviles en la configuración.\n\nPuede elegir entre añadir el episodio a la cola o permitir las descargas temporalmente.\n\n<small>Se recordará su elección durante 10 minutos.</small></string> <string name="confirm_mobile_download_dialog_message">Se desactivaron las descargas por red de datos móviles en la configuración.\n\n¿Quiere permitir las descargas temporalmente?\n\n<small>Se recordará su elección durante 10 minutos.</small></string> - <string name="confirm_mobile_streaming_notification_title">Confirmar streaming por red móvil</string> - <string name="confirm_mobile_streaming_notification_message">El streaming sobre datos móviles está deshabilitado en los ajustes. Toca para hacer el streaming de todas formas.</string> + <string name="confirm_mobile_streaming_notification_title">Confirmar reproducción vía datos móviles</string> + <string name="confirm_mobile_streaming_notification_message">La reproducción vía datos móviles está deshabilitada en los ajustes. Toca para escuchar en directo todas formas.</string> <string name="confirm_mobile_streaming_button_always">Siempre</string> <string name="confirm_mobile_streaming_button_once">Una vez</string> <string name="confirm_mobile_download_dialog_only_add_to_queue">Añadir a la cola</string> @@ -267,6 +280,7 @@ <string name="player_go_to_picture_in_picture">Modo picture-in-picture</string> <string name="unknown_media_key">AntennaPod - Tecla multimedia desconocida: %1$d</string> <string name="error_file_not_found">Archivo no encontrado</string> + <string name="no_media_label">El elemento no contiene un archivo multimedia</string> <!--Queue operations--> <string name="lock_queue">Bloquear cola</string> <string name="unlock_queue">Desbloquear cola</string> @@ -317,13 +331,12 @@ <string name="no_fav_episodes_label">Puede añadir episodios a los favoritos presionándolos durante un tiempo prolongado.</string> <string name="no_chapters_head_label">Sin capítulos</string> <string name="no_chapters_label">Este episodio no tiene capítulos.</string> - <string name="no_subscriptions_head_label">No hay subscripciones</string> + <string name="no_subscriptions_head_label">No hay suscripciones</string> <string name="no_subscriptions_label">Para suscribirse a un podcast, pulsa el icono \"más\" de abajo.</string> <!--Preferences--> <string name="storage_pref">Almacenamiento</string> - <string name="storage_sum">Auto borrar espisodio, Importar, Exportar</string> + <string name="storage_sum">Borrado automático, Importar, Exportar</string> <string name="project_pref">Proyecto</string> - <string name="queue_label">Cola</string> <string name="synchronization_pref">Sincronización</string> <string name="synchronization_sum">Sincronizar con otros dispositivos usando gpodder.net</string> <string name="automation">Automatización</string> @@ -334,19 +347,24 @@ <string name="external_elements">Elementos externos</string> <string name="interruptions">Interrupciones</string> <string name="playback_control">Control de reproducción</string> + <string name="reassign_hardware_buttons">Reasignar botones físicos</string> <string name="preference_search_hint">Buscar...</string> <string name="preference_search_no_results">Sin resultados</string> <string name="preference_search_clear_history">Borrar historial</string> <string name="media_player">Reproductor multimedia</string> <string name="pref_episode_cleanup_title">Limpieza de episodios</string> - <string name="pref_episode_cleanup_summary">Los episodios que no estén en la cola ni en favoritos pueden eliminarse si la descarga automática necesita espacio para nuevos episodios</string> + <string name="pref_episode_cleanup_summary">Episodios que pueden ser eliminados si la Descarga Automática necesita espacio para nuevos episodios</string> <string name="pref_pauseOnDisconnect_sum">Pausar la reproducción al desconectar los auriculares o el bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Reanudar la reproducción cuando se reconecten los auriculares</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Reanudar la reproducción cuando se reconecte el bluetooth</string> - <string name="pref_hardwareForwardButtonSkips_title">Botón avance: Saltar</string> - <string name="pref_hardwareForwardButtonSkips_sum">Al pulsar el botón de avance en un dispositivo conectado por bluetooth, salta al siguiente episodio en lugar de avanzar</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Botón retroceso: Reiniciar</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Al pulsar el botón físico de retroceso se comenzará el episodio de nuevo en lugar de retroceder</string> + <string name="pref_hardware_forward_button_title">Botón de Avance</string> + <string name="pref_hardware_forward_button_summary">Personalizar el comportamiento del botón de avance</string> + <string name="pref_hardware_previous_button_title">Botón de Retroceso</string> + <string name="pref_hardware_previous_button_summary">Personalizar el comportamiento del botón de retroceso</string> + <string name="button_action_fast_forward">Avance Rápido</string> + <string name="button_action_rewind">Rebobinar</string> + <string name="button_action_skip_episode">Omitir episodio</string> + <string name="button_action_restart_episode">Reiniciar Episodio</string> <string name="pref_followQueue_sum">Saltar al siguiente elemento de la cola al acabar la reproducción</string> <string name="pref_auto_delete_sum">Borrar el episodio cuando finalice la reproducción</string> <string name="pref_auto_delete_title">Eliminar automáticamente</string> @@ -366,21 +384,24 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Deshabilitar</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Ajustar intervalo</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Ajustar hora del día</string> - <string name="pref_autoUpdateIntervallOrTime_every">todos los %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">a las %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Cada hora</item> + <item quantity="other">Cada %d horas</item> + </plurals> <string name="pref_followQueue_title">Reproducción continua</string> <string name="pref_pauseOnHeadsetDisconnect_title">Desconexión de los auricuales o Bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_title">Reconectar con los auriculares</string> <string name="pref_unpauseOnBluetoothReconnect_title">Reconectar con Bluetooth</string> - <string name="pref_stream_over_download_title">Preferir Streaming</string> - <string name="pref_stream_over_download_sum">Muestra el botón de stream en lugar del botón de descargar en las listas.</string> - <string name="pref_mobileUpdate_title">Actualizaciones por red móvil</string> + <string name="pref_stream_over_download_title">Preferir escuchar en directo</string> + <string name="pref_stream_over_download_sum">Muestra el botón de escuchar en directo en lugar del botón de descargar en las listas.</string> + <string name="pref_mobileUpdate_title">Descargas vía datos móviles</string> <string name="pref_mobileUpdate_sum">Seleccionar lo que se debe permitir descargar con datos móviles</string> - <string name="pref_mobileUpdate_refresh">Actualización de podcast</string> + <string name="pref_mobileUpdate_refresh">Actualizar podcast</string> <string name="pref_mobileUpdate_images">Imágenes de portada</string> - <string name="pref_mobileUpdate_auto_download">Auto descargar</string> + <string name="pref_mobileUpdate_auto_download">Descarga automática</string> <string name="pref_mobileUpdate_episode_download">Descarga de episodio</string> - <string name="pref_mobileUpdate_streaming">Streaming</string> + <string name="pref_mobileUpdate_streaming">Escuchar en directo</string> <string name="user_interface_label">Interfaz de usuario</string> <string name="user_interface_sum">Apariencia, Suscripción, Pantalla de bloqueo</string> <string name="pref_set_theme_title">Elegir un tema</string> @@ -401,7 +422,9 @@ <string name="pref_episode_cache_title">Almacenamiento de episodios</string> <string name="pref_episode_cache_summary">Número total de episodios cacheados en el dispositivo. La descarga automática se suspenderá si se alcanza este número.</string> <string name="pref_episode_cover_title">Usar portada del episodio</string> - <string name="pref_episode_cover_summary">Usar la portada del episodio cuando sea posible. Si se desactiva, la aplicación siempre usará la portada del podcast.</string> + <string name="pref_episode_cover_summary">Usar la portada de cada episodio en las listas cuando sea posible. Si se desactiva, la aplicación siempre usará la portada del podcast.</string> + <string name="pref_show_remain_time_title">Mostrar Tiempo Restante</string> + <string name="pref_show_remain_time_summary">Muestra el tiempo restante de los episodios si está activado. Si se desactiva, muestra la duración total de los episodios.</string> <string name="pref_theme_title_use_system">Usar tema del sistema</string> <string name="pref_theme_title_light">Claro</string> <string name="pref_theme_title_dark">Oscuro</string> @@ -421,8 +444,6 @@ <string name="pref_gpodnet_full_sync_title">Forzar la sincronización completa</string> <string name="pref_gpodnet_full_sync_sum">Sincronizar todas las suscripciones y episodios con gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Identificado como <i>%1$s</i> con dispositivo <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Error en la sincronización</string> - <string name="pref_gpodnet_notifications_sum">Este ajuste no afecta a los errores de autenticación.</string> <string name="pref_playback_speed_sum">Personalice las velocidades disponibles en la reproducción a velocidad variable</string> <string name="pref_feed_playback_speed_sum">La velocidad a la que comenzarán los episodios de este podcast</string> <string name="pref_feed_skip">Saltar automático</string> @@ -437,8 +458,6 @@ <string name="pref_fast_forward_sum">Personalice el número de segundos que avanzará cuando se pulsa el botón de avance</string> <string name="pref_rewind">Intervalo de retroceso</string> <string name="pref_rewind_sum">Personalice el número de segundos que retrocederá cuando se pulsa el botón de retrocedeso</string> - <string name="pref_gpodnet_sethostname_title">Establecer nombre del dispositivo</string> - <string name="pref_gpodnet_sethostname_use_default_host">Usar el nombre predeterminado</string> <string name="pref_expandNotify_title">Alta prioridad de las notificaciones</string> <string name="pref_expandNotify_sum">Esto suele expandir las notificaciones para mostrar los botones de reproducción.</string> <string name="pref_persistNotify_title">Controles de reproducción persistentes</string> @@ -449,10 +468,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Sólo puede seleccionar un máximo de %1$d elementos.</string> <string name="pref_lockscreen_background_title">Establecer fondo de pantalla de bloqueo</string> <string name="pref_lockscreen_background_sum">Establecer el fondo de pantalla de bloqueo desde la imagen del episodio. Como efecto secundario, esto también mostrarán las imagen de aplicaciones de terceros.</string> - <string name="pref_showDownloadReport_title">Descarga fallida</string> - <string name="pref_showDownloadReport_sum">Si la descarga falla, generar un informe con los detalles del fallo</string> - <string name="pref_showAutoDownloadReport_title">Auto-descarga completada</string> - <string name="pref_showAutoDownloadReport_sum">Mostrar una notificación de los episodios descargados automáticamente.</string> <string name="pref_expand_notify_unsupport_toast">Las versiones de Android anteriores a la 4.1 no soportan notificaciones expandidas</string> <string name="pref_enqueue_location_title">Añadir a la cola en cierta ubicación</string> <string name="pref_enqueue_location_sum">Añadir episodios a: %1$s</string> @@ -473,14 +488,15 @@ <string name="pref_current_value">Valor actual: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Configurar proxy de red</string> - <string name="pref_faq">Preguntas de Uso Frecuente (FAQ)</string> <string name="pref_no_browser_found">No se ha encontrado un navegador web.</string> <string name="pref_cast_title">Soporte para Chromecast</string> <string name="pref_cast_message_play_flavor">Habilitar soporte para reproducción remota en dispositivos Cast (como Chromecast, altavoces o Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast requiere librerías propietarias de terceros que están deshabilitadas en esta versión de AntennaPod</string> <string name="pref_enqueue_downloaded_title">Añadir descargados a la cola</string> <string name="pref_enqueue_downloaded_summary">Añadir episodios descargados a la cola</string> - <string name="media_player_builtin">Reproductor Android integrado</string> + <string name="media_player_builtin">Reproductor integrado de Android (obsoleto)</string> + <string name="media_player_sonic">Sonic Media Player (obsoleto)</string> + <string name="media_player_exoplayer_recommended">ExoPlayer (recomendado)</string> <string name="media_player_switch_to_exoplayer">Cambiar a ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Cambiado a ExoPlayer</string> <string name="pref_skip_silence_title">Saltar silencio en audio</string> @@ -505,7 +521,8 @@ <string name="pref_filter_feed_sum">Filtra tus suscripciones en el cajón de navegación y pantallas de suscripción.</string> <string name="no_filter_label">Ninguno</string> <string name="subscriptions_are_filtered">Las suscripciones están filtradas.</string> - <string name="auto_downloaded">Auto-descargado</string> + <string name="subscriptions_counter_greater_zero">Contador mayor que cero</string> + <string name="auto_downloaded">Descargado automáticamente</string> <string name="not_auto_downloaded"> No auto-descargado</string> <string name="kept_updated">Mantenido actualizado</string> <string name="not_kept_updated">No mantenido actualizado</string> @@ -567,6 +584,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Establecer un temporizador</string> <string name="disable_sleeptimer_label">Desactivar el temporizador</string> + <string name="extend_sleep_timer_label">+%d min</string> <string name="sleep_timer_label">Temporizador</string> <string name="time_dialog_invalid_input">Entrada no válida, el tiempo debe ser un número entero</string> <string name="shake_to_reset_label">Agita para reiniciar</string> @@ -594,22 +612,21 @@ <string name="gpodnet_suggestions_header">SUGERENCIAS</string> <string name="gpodnet_search_hint">Buscar en gpodder.net</string> <string name="gpodnetauth_login_title">Iniciar sesión</string> - <string name="gpodnetauth_login_descr">Bienvenido al inicio de sesión de gpodder.net. Primero, escriba sus datos de inicio de sesión:</string> <string name="gpodnetauth_login_butLabel">Iniciar sesión</string> - <string name="gpodnetauth_login_register">Si aún no tiene una cuenta, puede crearla en:\nhttps://gpodder.net/register/</string> + <string name="create_account">Crear cuenta</string> <string name="username_label">Usuario</string> <string name="password_label">Contraseña</string> - <string name="gpodnetauth_device_title">Selección del dispositivo</string> + <string name="gpodnet_description">Gpodder.net es un servicio de sincronización de podcasts de código abierto que es independiente del proyecto AntennaPod.</string> + <string name="gpodnetauth_server_official">Servidor oficial de gpodder.net</string> + <string name="gpodnetauth_server_custom">Servidor personalizado</string> + <string name="gpodnetauth_host">Nombre del host</string> + <string name="gpodnetauth_select_server">Seleccione el servidor</string> <string name="gpodnetauth_device_descr">Cree un nuevo dispositivo para usar en su cuenta de gpodder.net o elija uno existente:</string> - <string name="gpodnetauth_device_deviceID">Id. de dispositivo:\u0020</string> - <string name="gpodnetauth_device_caption">Descripción</string> - <string name="gpodnetauth_device_butCreateNewDevice">Crear dispositivo nuevo</string> - <string name="gpodnetauth_device_chooseExistingDevice">Elegir dispositivo existente:</string> - <string name="gpodnetauth_device_errorEmpty">El id. de dispositivo no puede estar vacío</string> - <string name="gpodnetauth_device_errorAlreadyUsed">El id. de dispositivo ya está en uso</string> + <string name="gpodnetauth_device_name">Nombre del dispositivo</string> <string name="gpodnetauth_device_caption_errorEmpty">El texto no puede estar en blanco</string> + <string name="gpodnetauth_existing_devices">Dispositivos existentes</string> + <string name="gpodnetauth_create_device">Crear dispositivo</string> <string name="gpodnetauth_device_butChoose">Elegir</string> - <string name="gpodnetauth_finish_title">¡Inicio de sesión correcto!</string> <string name="gpodnetauth_finish_descr">¡Enhorabuena! Su cuenta de gpodder.net está ahora asociada con su dispositivo. A partir de ahora AntennaPod sincronizará automáticamente las suscripciones de su dispositivo con su cuenta de gpodder.net.</string> <string name="gpodnetauth_finish_butsyncnow">Comenzar sincronización ahora</string> <string name="gpodnetauth_finish_butgomainscreen">Ir a la pantalla principal</string> @@ -663,6 +680,7 @@ <string name="switch_pages">Cambiar páginas</string> <string name="position">Posición: %1$s</string> <string name="apply_action">Aplicar acción</string> + <string name="play_chapter">Reproducir capítulo</string> <!--Feed information screen--> <string name="authentication_label">Autenticación</string> <string name="authentication_descr">Cambiar nombre y contraseña de este pódcast y sus episodios</string> @@ -699,9 +717,11 @@ <string name="search_powered_by">Resultados de %1$s</string> <!--Local feeds--> <string name="add_local_folder">Añadir carpeta local</string> + <string name="local_folder">Carpeta local</string> <string name="reconnect_local_folder">Re-conectar carpeta local</string> <string name="reconnect_local_folder_warning">En caso de falta de permisos, puedes usar esto para re-conectar la misma carpeta. No selecciones otra carpeta.</string> <string name="local_feed_description">Este podcast virtual fue creado añadiendo una carpeta a AntennaPod.</string> + <string name="unable_to_start_system_file_manager">No se puede iniciar el administrador de archivos del sistema</string> <string name="filter">Filtro</string> <!--Episodes apply actions--> <string name="all_label">Todos</string> @@ -795,24 +815,28 @@ <string name="cast_failed_receiver_player_error">El reproductor ha encontrado un error grave</string> <string name="cast_failed_media_error_skipping">Error reproduciendo medio. Saltando…</string> <!--Notification channels--> + <string name="notification_group_errors">Errores</string> + <string name="notification_group_news">Nuevos</string> <string name="notification_channel_user_action">Acción necesaria</string> <string name="notification_channel_user_action_description">Se muestra si su acción es necesaria, por ejemplo, si necesita introducir una contraseña.</string> <string name="notification_channel_downloading">Descargando</string> <string name="notification_channel_downloading_description">Se muestra mientras se está descargando.</string> <string name="notification_channel_playing">Reproduciendo</string> <string name="notification_channel_playing_description">Permite controlar la reproducción. Es la notificación principal que se ve mientras se reproduce un pódcast.</string> - <string name="notification_channel_error">Errores</string> - <string name="notification_channel_error_description">Muestra si algo salió mal, por ejemplo, si falla la descarga o la actualización del feed.</string> - <string name="notification_channel_sync_error">Errores de sincronización</string> + <string name="notification_channel_download_error">Descarga fallida</string> + <string name="notification_channel_download_error_description">Muestra cuando falla la descarga o la actualización del canal.</string> + <string name="notification_channel_sync_error">Sincronización fallida</string> <string name="notification_channel_sync_error_description">Mostrar cuando falle la sincronización de gpodder.</string> - <string name="notification_channel_auto_download">Descargas automáticas</string> + <string name="notification_channel_auto_download">Descarga automática completada</string> <string name="notification_channel_episode_auto_download">Mostrar cuándo los episodios se han descargado automáticamente.</string> + <string name="notification_channel_new_episode">Nuevo Episodio</string> + <string name="notification_channel_new_episode_description">Se muestra al encontrar un nuevo episodio de un podcast, si las notificaciones están activadas</string> <!--Widget settings--> <string name="widget_settings">Configuraciones del Widget</string> <string name="widget_create_button">Crear widget</string> <string name="widget_opacity">Opacidad</string> <!--On-Demand configuration--> <string name="on_demand_config_setting_changed">Configuración actualizada satisfactoriamente.</string> - <string name="on_demand_config_stream_text">Parece que usas mucho el stream. ¿Quieres mostrar el botón de stream en la lista de episodios?</string> + <string name="on_demand_config_stream_text">Parece que usas mucho la reproducción en directo. ¿Quieres que las listas de episodios muestren los botones de escuchar en directo?</string> <string name="on_demand_config_download_text">Parece que usas mucho las descargas. ¿Quieres mostrar el botón de descargar en la lista de episodios?</string> </resources> diff --git a/core/src/main/res/values-et/strings.xml b/core/src/main/res/values-et/strings.xml index 65e20126b..5a5c8c804 100644 --- a/core/src/main/res/values-et/strings.xml +++ b/core/src/main/res/values-et/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statistika</string> <string name="add_feed_label">Lisa taskuhääling</string> <string name="episodes_label">Saated</string> + <string name="queue_label">Järjekord</string> <string name="all_episodes_short_label">Kõik</string> <string name="new_episodes_label">Uued</string> <string name="favorite_episodes_label">Lemmikud</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Logi</string> <string name="subscriptions_label">Tellimused</string> <string name="subscriptions_list_label">Tellimuste nimekiri</string> - <string name="cancel_download_label">Tühista\nLaadi alla</string> + <string name="cancel_download_label">Katkesta allalaadimine</string> <string name="playback_history_label">Esitamise ajalugu</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net kasutajanimi</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Esitamine</string> <string name="download_statistics_label">Allalaadimised</string> <string name="notification_pref_fragment">Teavitused</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" ei leitud</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Saadete kogupikkus:</string> <string name="statistics_details_dialog">%1$d %2$d-st saatest on alustatud.\n\nKuulatud on %3$s saadet %4$s-st.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Pole</string> <!--Bug report activity--> <string name="log_file_share_exception">Ühtegi ühilduvat rakendust ei leitud</string> + <string name="export_logs_menu_title">Ekspordi täpne logi</string> + <string name="confirm_export_log_dialog_message">Täpne logi võib sisaldada tundlikku infot nagu sinu tellimuste nimekiri</string> <!--Webview actions--> <string name="open_in_browser_label">Ava veebisirvijas</string> <string name="copy_url_label">Kopeeri URL</string> @@ -81,7 +86,6 @@ <string name="description_label">Kirjeldus</string> <string name="episodes_suffix">\u0020saadet</string> <string name="processing_label">Töötlemine</string> - <string name="save_username_password_label">Salvesta kasutajanimi ja parool</string> <string name="close_label">Sulge</string> <string name="retry_label">Proovi uuesti</string> <string name="auto_download_label">Lisa automaatsetesse allalaadimistesse</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">Väljas</string> <string name="feed_volume_reduction_light">Kerge</string> <string name="feed_volume_reduction_heavy">Tugev</string> - <string name="parallel_downloads_suffix">\u0020samaaegset allalaadimist</string> + <string name="parallel_downloads">%1$d paralleelset allalaadimist</string> <string name="feed_auto_download_global">Üldine vaikeväärtus</string> <string name="feed_auto_download_always">Alati</string> <string name="feed_auto_download_never">Mitte kunagi</string> <string name="send_label">Saada...</string> <string name="episode_cleanup_never">Mitte kunagi</string> + <string name="episode_cleanup_except_favorite_removal">Kui pole lemmik</string> <string name="episode_cleanup_queue_removal">Kui pole järjekorras</string> <string name="episode_cleanup_after_listening">Pärast lõpetamist</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -113,7 +118,17 @@ <item quantity="one">%d valitud</item> <item quantity="other">%d valitud</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d saade</item> + <item quantity="other">%d saadet</item> + </plurals> <string name="loading_more">Laadimine…</string> + <string name="episode_notification">Saadete teavitused</string> + <string name="episode_notification_summary">Teate kuvamine, kui avaldatakse uus saade.</string> + <plurals name="new_episode_notification_title"> + <item quantity="one">Uus saade</item> + <item quantity="other">Uued saated</item> + </plurals> <!--Actions on feeds--> <string name="mark_all_read_label">Märgi kuulatuks</string> <string name="mark_all_read_msg">Märgi kõik saated kuulatuks</string> @@ -145,7 +160,6 @@ <string name="hide_not_queued_episodes_label">Pole järjekorras</string> <string name="hide_has_media_label">On meediafaile</string> <string name="filtered_label">Filtreeritud</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Viimane värskendamine ebaõnnestus</string> <string name="open_podcast">Ava taskuhääling</string> <string name="please_wait_for_data">Palun oota andmete laadimist</string> <!--actions on feeditems--> @@ -206,16 +220,12 @@ <string name="download_error_details">Üksikasjad</string> <string name="download_error_details_message">%1$s \n\nFaili URL:\n%2$s</string> <string name="download_error_device_not_found">Salvestuskohta ei leitud</string> - <string name="download_error_insufficient_space">Pole piisavalt ruumi</string> <string name="download_error_http_data_error">HTTP andmete viga</string> <string name="download_error_error_unknown">Tundmatu tõrge</string> - <string name="download_error_parser_exception">Parsimise järjekord</string> <string name="download_error_unsupported_type">Toetamata uudisvoo tüüp</string> <string name="download_error_connection_error">Ühenduse viga</string> - <string name="download_error_unknown_host">Tundmatu host</string> <string name="download_error_unauthorized">Autentimise viga</string> <string name="download_error_file_type_type">Failitüübi viga</string> - <string name="download_error_forbidden">Keelatud</string> <string name="download_canceled_msg">Allalaadimine on tühistatud</string> <string name="download_canceled_autodownload_enabled_msg">Allalaadimine tühistati\nKeelati selle saate <i>automaatne allalaadimine</i></string> <string name="download_report_title">Allalaadimised lõpetati veaga (vigadega)</string> @@ -229,12 +239,7 @@ <item quantity="one">%d allalaadimine jäänud</item> <item quantity="other">%d allalaadimist jäänud</item> </plurals> - <string name="downloads_processing">Allalaadimiste töötlemine</string> <string name="download_notification_title">Taskuhäälingu andmete allalaadimine</string> - <plurals name="download_report_content"> - <item quantity="one">%d allalaadimine õnnestus, %d ebaõnnestus</item> - <item quantity="other">%d allalaadimist õnnestus, %d ebaõnnestus</item> - </plurals> <string name="download_log_title_unknown">Tundmatu pealkiri</string> <string name="download_type_feed">Uudisvoog</string> <string name="download_type_media">Meediafail</string> @@ -323,7 +328,6 @@ <string name="storage_pref">Salvestusruum</string> <string name="storage_sum">Saate automaatne kustutamine, importimine, eksportimine</string> <string name="project_pref">Projekt</string> - <string name="queue_label">Järjekord</string> <string name="synchronization_pref">Sünkroonimine</string> <string name="synchronization_sum">Sünkrooni teiste seadmetega gpodder.net abil</string> <string name="automation">Automaatika</string> @@ -339,14 +343,9 @@ <string name="preference_search_clear_history">Puhasta ajalugu</string> <string name="media_player">Meediaesitaja</string> <string name="pref_episode_cleanup_title">Saadete kustutamine</string> - <string name="pref_episode_cleanup_summary">Saated, mis ei ole järjekorras ega lemmikud, on eemaldamise kandidaadid, kui automaatselt allalaaditavate saadete jaoks on vaja rohkem ruumi</string> <string name="pref_pauseOnDisconnect_sum">Esitus pausitakse, kui kõrvaklapid või bluetooth eemaldatakse</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Esitus jätkub, kui kõrvaklapid uuesti ühendatakse</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Esitus jätkub kui bluetooth uuesti ühendub</string> - <string name="pref_hardwareForwardButtonSkips_title">Edasi nupp jätab vahele</string> - <string name="pref_hardwareForwardButtonSkips_sum">Edasi nupu vajutamine bluetoothi seadmel edasi kerimise asemel hüppab järgmisele saatele</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Tagasi nupp alustab uuesti</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Riistvaralise tagasi nupu vajutamine alustab tagasi kerimise asemel praegust saadet algusest</string> <string name="pref_followQueue_sum">Kui saade lõpeb, siis esita kohe järgmine järjekorras olev saade.</string> <string name="pref_auto_delete_sum">Kustuta saated, kui need on kuulatud</string> <string name="pref_auto_delete_title">Automaatne kustutamine</string> @@ -366,7 +365,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Lülita välja</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Määra intervall</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Määra aeg päevades</string> - <string name="pref_autoUpdateIntervallOrTime_every">iga %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">kell %1$s</string> <string name="pref_followQueue_title">Pidev esitamine</string> <string name="pref_pauseOnHeadsetDisconnect_title">Peakomplekti või Bluetoothi lahti ühendamisel</string> @@ -401,7 +399,6 @@ <string name="pref_episode_cache_title">Saadete vahemälu</string> <string name="pref_episode_cache_summary">Seadme puhvrisse allalaaditud saadete koguarv. Automaatne allalaadimine peatub, kui selle numbrini jõutakse.</string> <string name="pref_episode_cover_title">Kasuta saate kaanepilti</string> - <string name="pref_episode_cover_summary">Kasuta saate kaanepilti alati, kui see on olemas. Kui see pole märgitud, kasutab äpp alati taskuhäälingu kaanepilti.</string> <string name="pref_theme_title_use_system">Kasuta süsteemi kujundust</string> <string name="pref_theme_title_light">Hele</string> <string name="pref_theme_title_dark">Tume</string> @@ -421,8 +418,6 @@ <string name="pref_gpodnet_full_sync_title">Nõua täielikku sünkroonimist</string> <string name="pref_gpodnet_full_sync_sum">Sünkroniseeri kõiki tellimusi ja saate olekuid gpodder.net-iga.</string> <string name="pref_gpodnet_login_status"><![CDATA[Sisseloginud kui <i>%1$s</i> seadmega <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Sünkroniseerimine ebaõnnestus</string> - <string name="pref_gpodnet_notifications_sum">See seadistus ei rakendu autentimise vigadele.</string> <string name="pref_playback_speed_sum">Kohanda kiiruseid, mis on esitamisel saadaval</string> <string name="pref_feed_playback_speed_sum">Millise kiirusega esitatakse selle tellimuse saadete heli</string> <string name="pref_feed_skip">Automaatne vahelejätmine</string> @@ -437,8 +432,6 @@ <string name="pref_fast_forward_sum">Määra, mitu sekundit edasi hüpatakse, kui vajutatakse edasi kerimise nuppu</string> <string name="pref_rewind">Tagasi kerimise hüpe</string> <string name="pref_rewind_sum">Määra, mitu sekundit tagasi hüpatakse, kui vajutatakse tagasi kerimise nuppu</string> - <string name="pref_gpodnet_sethostname_title">Määra hostinimi</string> - <string name="pref_gpodnet_sethostname_use_default_host">Kasuta vaikimisi hosti</string> <string name="pref_expandNotify_title">Kõrge teate prioriteet</string> <string name="pref_expandNotify_sum">See tavaliselt kuvab teadet laiemana ning näha on esitusnupud.</string> <string name="pref_persistNotify_title">Püsivad taasesitamise nupud</string> @@ -449,10 +442,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Maksimaalselt saab valida %1$d kirjet.</string> <string name="pref_lockscreen_background_title">Määra lukustusekraani taustapilt</string> <string name="pref_lockscreen_background_sum">Määra lukuekraani taustaks selle saate pilt. Kõrvalmõjuna kuvab see pilti ka teistes rakendustes.</string> - <string name="pref_showDownloadReport_title">Allalaadimine ebaõnnestus</string> - <string name="pref_showDownloadReport_sum">Kui allalaadimised nurjuvad, genereeri raport, mis kuvab vea üksikasju.</string> - <string name="pref_showAutoDownloadReport_title">Automaatne allalaadimine on lõpetatud</string> - <string name="pref_showAutoDownloadReport_sum">Teate kuvamine automaatselt allalaaditud saadete kohta.</string> <string name="pref_expand_notify_unsupport_toast">Vanemad Androidi versioonid kui 4.1 ei toeta laiendatud teavitusi.</string> <string name="pref_enqueue_location_title">Järjekorra asukoht</string> <string name="pref_enqueue_location_sum">Saated lisatakse: %1$s</string> @@ -473,14 +462,12 @@ <string name="pref_current_value">Praegune väärtus: %1$s</string> <string name="pref_proxy_title">Vaheserver</string> <string name="pref_proxy_sum">Määra võrgu vaheserver</string> - <string name="pref_faq">Korduma kippuvad küsimused</string> <string name="pref_no_browser_found">Veebilehitsejat ei leitud.</string> <string name="pref_cast_title">Chromecasti tugi</string> <string name="pref_cast_message_play_flavor">Luba meedia esitamine kaugseadmetest (Chromecast, kõlarid või Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast vajab kolmanda osapoole omanduslikke teeke, mis on selles AntennaPodi versioonis välja lülitatud</string> <string name="pref_enqueue_downloaded_title">Järjekord allalaaditud</string> <string name="pref_enqueue_downloaded_summary">Allalaaditud saadete lisamine järjekorda</string> - <string name="media_player_builtin">Sisseehitatud Androidi esitaja</string> <string name="media_player_exoplayer_recommended">ExoPlayer (soovitatud)</string> <string name="media_player_switch_to_exoplayer">Vaheta ExoPlayerile</string> <string name="media_player_switched_to_exoplayer">Vahetati ExoPlayerile.</string> @@ -596,22 +583,12 @@ <string name="gpodnet_suggestions_header">SOOVITUSED</string> <string name="gpodnet_search_hint">Otsi gpodder.net-ist</string> <string name="gpodnetauth_login_title">Logi sisse</string> - <string name="gpodnetauth_login_descr">Tere tulemast gpodder.net-i sisse logima. Kõigepealt sisesta sisselogimise andmed:</string> <string name="gpodnetauth_login_butLabel">Logi sisse</string> - <string name="gpodnetauth_login_register">Kui sul pole veel kontot, siis sa saad selle endale registreerida siin:\nhttps://gpodder.net/register/</string> <string name="username_label">Kasutajanimi</string> <string name="password_label">Parool</string> - <string name="gpodnetauth_device_title">Seadme valimine</string> <string name="gpodnetauth_device_descr">Loo oma gpodder.net konto jaoks uus seade või vali olemasolev:</string> - <string name="gpodnetauth_device_deviceID">Seadme ID:\u0020</string> - <string name="gpodnetauth_device_caption">Pealkiri</string> - <string name="gpodnetauth_device_butCreateNewDevice">Loo uus seade</string> - <string name="gpodnetauth_device_chooseExistingDevice">Vali olemasolev seade:</string> - <string name="gpodnetauth_device_errorEmpty">Seadme ID ei tohi olla tühi</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Seadme ID on juba kasutuses</string> <string name="gpodnetauth_device_caption_errorEmpty">Pealkiri ei tohi olla tühi</string> <string name="gpodnetauth_device_butChoose">Vali</string> - <string name="gpodnetauth_finish_title">Sisse logitud!</string> <string name="gpodnetauth_finish_descr">Palju õnne! Sinu gpodder.net konto on nüüd lingitud sinu seadmega. AntennaPod süngib nüüdsest tellimused sinu seadmes gpodder.net-i kontoga.</string> <string name="gpodnetauth_finish_butsyncnow">Alusta kohe sünkroonimist</string> <string name="gpodnetauth_finish_butgomainscreen">Mine peaekraanile</string> @@ -805,11 +782,7 @@ <string name="notification_channel_downloading_description">Näidatakse allalaadimise ajal.</string> <string name="notification_channel_playing">Praegu esitatakse</string> <string name="notification_channel_playing_description">Võimaldab esitust juhtida. See on saate kuulamise ajal peamine teade.</string> - <string name="notification_channel_error">Vead</string> - <string name="notification_channel_error_description">Näita, kui midagi läks valest. Näiteks, kui allalaadimine või uudivoo uuendamine ebaõnnestus.</string> - <string name="notification_channel_sync_error">Sünkroniseerimise tõrked</string> <string name="notification_channel_sync_error_description">Näidatakse, kui gpodder sünkroniseerimine ebaõnnestub.</string> - <string name="notification_channel_auto_download">Automaatsed allalaadimised</string> <string name="notification_channel_episode_auto_download">Näita, kui saateid laaditi automaatselt alla.</string> <!--Widget settings--> <string name="widget_settings">Vidina seaded</string> diff --git a/core/src/main/res/values-eu/strings.xml b/core/src/main/res/values-eu/strings.xml index 6bc7b3c9e..1c85c76f2 100644 --- a/core/src/main/res/values-eu/strings.xml +++ b/core/src/main/res/values-eu/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Estatistikak</string> <string name="add_feed_label">Gehitu podcasta</string> <string name="episodes_label">Saioak</string> + <string name="queue_label">Ilara</string> <string name="all_episodes_short_label">Denak</string> <string name="new_episodes_label">Berria</string> <string name="favorite_episodes_label">Gogokoak</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Erreprodukzioa</string> <string name="download_statistics_label">Deskargak</string> <string name="notification_pref_fragment">Jakinarazpenak</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" ezin da aurkitu</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Ikusitako saio denen denbora:</string> <string name="statistics_details_dialog">%1$d kanpo %2$d hasitako saioetatik. %3$s \n\nErreproduzituak %4$setatik.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Bat ere ez</string> <!--Bug report activity--> <string name="log_file_share_exception">Ez dago app bateragarririk</string> + <string name="export_logs_menu_title">Esportatu log zehatzak</string> + <string name="confirm_export_log_dialog_message">Erregistro zehatzek informazio sentikorra izan dezakete, hala nola zure harpidetza-zerrendak.</string> <!--Webview actions--> <string name="open_in_browser_label">Nabigatzailean ireki</string> <string name="copy_url_label">URLa kopiatu</string> @@ -81,7 +86,6 @@ <string name="description_label">Deskribapena</string> <string name="episodes_suffix">\u0020saio</string> <string name="processing_label">Prozesatzen</string> - <string name="save_username_password_label">Gorde erabiltzailea eta pasahitza</string> <string name="close_label">Itxi</string> <string name="retry_label">Saiatu berriro</string> <string name="auto_download_label">Deskarga automatikoetan sartu</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">Itzalita</string> <string name="feed_volume_reduction_light">Gozoa</string> <string name="feed_volume_reduction_heavy">Indartsua</string> - <string name="parallel_downloads_suffix">\u0020deskarga paraleloak</string> + <string name="parallel_downloads">%1$d deskarga paraleloak</string> <string name="feed_auto_download_global">Globala aurrez zehaztua</string> <string name="feed_auto_download_always">Beti</string> <string name="feed_auto_download_never">Inoiz ez</string> <string name="send_label">Bidali...</string> <string name="episode_cleanup_never">Inoiz ez</string> + <string name="episode_cleanup_except_favorite_removal">Gogoko ez denean</string> <string name="episode_cleanup_queue_removal">Ilaran ez dagoenean</string> <string name="episode_cleanup_after_listening">Bukatu ondoren</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -113,7 +118,22 @@ <item quantity="one">%d aukeratua</item> <item quantity="other">%d aukeratua</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d saio</item> + <item quantity="other">%d saioak</item> + </plurals> <string name="loading_more">Kargatzen...</string> + <string name="episode_notification">Saio jakinarazpenak</string> + <string name="episode_notification_summary">Erakutsi jakinarazpena saio berri bat kaleratzen denean.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s (e)k saio berri bat du</item> + <item quantity="other">%2$s (e)k %1$d saio berri ditu</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Saio berria</item> + <item quantity="other">Saio berriak</item> + </plurals> + <string name="new_episode_notification_group_text">Zure harpidetzak saio berriak ditu.</string> <!--Actions on feeds--> <string name="mark_all_read_label">Markatu denak ikusita bezala</string> <string name="mark_all_read_msg">Markatu saio denak ikusita bezala</string> @@ -145,7 +165,6 @@ <string name="hide_not_queued_episodes_label">Ez dago ilaran</string> <string name="hide_has_media_label">Media du</string> <string name="filtered_label">Iragaziak</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Errorea azken eguneraketan</string> <string name="open_podcast">Ireki podcasta</string> <string name="please_wait_for_data">Mesedez, itxaron datuak kargatu arte</string> <!--actions on feeditems--> @@ -160,6 +179,10 @@ <string name="delete_label">Ezabatu</string> <string name="delete_failed">Ezin da fitxategia ezabatu. Gailua berrabiarazteak lagun dezake.</string> <string name="delete_episode_label">Saioa ezabatu</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%dsaio aukeratuta, jaitsiera %d ezabatuta.</item> + <item quantity="other">%d saio aukeratuta, %d jaitsiera ezabatuta.</item> + </plurals> <string name="remove_new_flag_label">Kendu \"berria\" ikurra</string> <string name="removed_new_flag_label">\"Berria\" ikurra kendu da</string> <string name="mark_read_label">Markatu ikusita bezala</string> @@ -206,16 +229,12 @@ <string name="download_error_details">Xehetasunak</string> <string name="download_error_details_message">%1$s \n\nartxibategiaren URL:\n%2$s</string> <string name="download_error_device_not_found">Ez da biltegiratze gailurik aurkitu</string> - <string name="download_error_insufficient_space">Ez dago nahiko tokirik</string> <string name="download_error_http_data_error">HTTP datuen errorea</string> <string name="download_error_error_unknown">Errore ezezaguna</string> - <string name="download_error_parser_exception">Analizatzailearen salbuespena</string> <string name="download_error_unsupported_type">Kanal mota ez onartua</string> <string name="download_error_connection_error">Konexio errorea</string> - <string name="download_error_unknown_host">Ostalari ezezaguna</string> <string name="download_error_unauthorized">Egiaztatze errorea</string> <string name="download_error_file_type_type">Artxibategi motaren errorea</string> - <string name="download_error_forbidden">Debekaturik</string> <string name="download_canceled_msg">Deskarga ezeztatua</string> <string name="download_canceled_autodownload_enabled_msg">Deskarga ezeztatua\aktibatu da <i>Auto deskarga</i> elementu honetan</string> <string name="download_report_title">Deskarga(k) osatu d(ir)a errorea(k) d(it)uela</string> @@ -229,12 +248,7 @@ <item quantity="one">%d deskarga zain</item> <item quantity="other">%d deskarga zain</item> </plurals> - <string name="downloads_processing">Deskargak prozesatzen</string> <string name="download_notification_title">Podcastaren datuak deskargatzen</string> - <plurals name="download_report_content"> - <item quantity="one">Deskarga arrakastatsu %d , %d (e)k huts egin du(te)</item> - <item quantity="other">%d deskarga arrakastasuak, %d (e)k huts egin du(te)</item> - </plurals> <string name="download_log_title_unknown">Izenburu ezezaguna</string> <string name="download_type_feed">Kanala</string> <string name="download_type_media">Media artxibategia</string> @@ -267,6 +281,7 @@ <string name="player_go_to_picture_in_picture">Picture-in-picture modua</string> <string name="unknown_media_key">AntennaPod - Media tekla ezezaguna: %1$d</string> <string name="error_file_not_found">Ez da artxibategirik aurkitu</string> + <string name="no_media_label">Elementuak ez du multimedia fitxategirik</string> <!--Queue operations--> <string name="lock_queue">Blokeatu ilara</string> <string name="unlock_queue">Desblokeatu ilara</string> @@ -323,7 +338,6 @@ <string name="storage_pref">Biltegia</string> <string name="storage_sum">Ezabatze automatikoa, jaso, bidali</string> <string name="project_pref">Proiektua</string> - <string name="queue_label">Ilara</string> <string name="synchronization_pref">Sinkronizazioa</string> <string name="synchronization_sum">Sinkronizatzen gpodder.net erabiltzen duten beste gailu batzuekin</string> <string name="automation">Automatizazioa</string> @@ -334,19 +348,24 @@ <string name="external_elements">Kanpo elementuak</string> <string name="interruptions">Etenaldiak</string> <string name="playback_control">Erreprodukzioaren kontrola</string> + <string name="reassign_hardware_buttons">Birjarri hardware botoiak</string> <string name="preference_search_hint">Bilatu...</string> <string name="preference_search_no_results">Emaitzarik ez</string> <string name="preference_search_clear_history">Historia ezabatu</string> <string name="media_player">Media erreproduzigailua</string> <string name="pref_episode_cleanup_title">Saioen garbitzea</string> - <string name="pref_episode_cleanup_summary">Ilaran edo gogokoetan ez dauden gertakariak ezabatu egin daitezke deskarga automatikoak gertakari berrietarako lekua behar badu.</string> + <string name="pref_episode_cleanup_summary">Deskarga automatikoak saio berrietarako lekua behar badu kentzeko eskubidea izan beharko luketen saioak</string> <string name="pref_pauseOnDisconnect_sum">Erreprodukzioa gelditu entzungailu edo bluetootha kentzean</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Erreprodukzioa jarraitu entzungailu edo bluettota berriz konektatzean</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Erreprodukzioa jarraitu bluetootha berriz konektatzean</string> - <string name="pref_hardwareForwardButtonSkips_title">Aurrera botoia: jauzi</string> - <string name="pref_hardwareForwardButtonSkips_sum">Bluetooth bidez konektatutako gailuan aurrera botoia sakatzean, hurrengo saiora egingo du jauzi aurrera egin beharrean</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Atzera botoia: Berrabiarazi</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Atzera botoia sakatzean saioa berriz hasiko da atzera egin beharrean</string> + <string name="pref_hardware_forward_button_title">Aurrera botoia</string> + <string name="pref_hardware_forward_button_summary">Pertsonalizatu aurrera botoiaren portaera</string> + <string name="pref_hardware_previous_button_title">Atzera botoia</string> + <string name="pref_hardware_previous_button_summary">Pertsonalizatu atzera botoiaren portaera</string> + <string name="button_action_fast_forward">Aurrera egin</string> + <string name="button_action_rewind">Atzera egin</string> + <string name="button_action_skip_episode">Baztertu saioa</string> + <string name="button_action_restart_episode">Berrekarri saioa</string> <string name="pref_followQueue_sum">Erreprodukzioa amaitzean ilarako hurrengo elementura jauzi</string> <string name="pref_auto_delete_sum">Saioa ezabatu erreprodukzioa amaitzean</string> <string name="pref_auto_delete_title">Automatikoki ezabatu</string> @@ -362,12 +381,15 @@ <string name="network_pref_sum">Eguneratze tartea, deskarga kontrolak, mugikorraren datuak</string> <string name="pref_autoUpdateIntervallOrTime_title">Eguneratzeko tartea edo ordua</string> <string name="pref_autoUpdateIntervallOrTime_sum">Zehaztu tarte bat edo eguneko ordu jakin bat podcastak automatikoki freskatzeko</string> - <string name="pref_autoUpdateIntervallOrTime_message">Zuk ahal duzu <i>tartea</i> gustuko \"2orduro\" <i>eguneko ordua </i> \"7:00 AM\" adibidez edo <i>desgaitu</i> eguneraketa automatikoak\n\n<small>Oharra: Eguneraketa orduak ez dira zehatzak. Atzerapen txiki bat eman daiteke.</small></string> + <string name="pref_autoUpdateIntervallOrTime_message">Zuk ahal duzu <i>tartea</i> gustuko \"2orduro\" ezarri, eta <i>eguneko ordua </i> \"7:00 AM\" adibidez edo <i>desgaitu</i> eguneraketa automatikoak\n\n<small>Oharra: Eguneraketa orduak ez dira zehatzak. Atzerapen txiki bat eman daiteke.</small></string> <string name="pref_autoUpdateIntervallOrTime_Disable">Desgaitu</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Zehaztu tartea</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Zehaztu eguneko ordua</string> - <string name="pref_autoUpdateIntervallOrTime_every">%1$sdenak</string> <string name="pref_autoUpdateIntervallOrTime_at">%1$setan</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Orduro</item> + <item quantity="other">%d orduro</item> + </plurals> <string name="pref_followQueue_title">Etengabeko erreprodukzioa</string> <string name="pref_pauseOnHeadsetDisconnect_title">Entzungailu edo Bluetootharen deskonexioa</string> <string name="pref_unpauseOnHeadsetReconnect_title">Birkonektatu entzungailuez</string> @@ -401,7 +423,9 @@ <string name="pref_episode_cache_title">Saioak gordetzea</string> <string name="pref_episode_cache_summary">Gailuan katxeatutako saioen zenbatekoa. Deskarga automatikoa bertan behera utziko da zenbaki honetara heltzean.</string> <string name="pref_episode_cover_title">Saioaren azala erabili</string> - <string name="pref_episode_cover_summary">Erabili atalaren azala ahal denean. Desaktibatzen bada, aplikazioak podcastaren azala erabiliko du beti.</string> + <string name="pref_episode_cover_summary">Erabili atalaren azal espezifikoa zerrendetan eskuragarri dagoenean. Desaktibatzen bada, aplikazioak podcastaren azala erabiliko du beti.</string> + <string name="pref_show_remain_time_title">Erakutsi geratzen den denbora</string> + <string name="pref_show_remain_time_summary">Erakutsi saioen gelditzen denbora markatuta daudenean. Markatu gabe badaude, erakutsi saioen iraupen osoa.</string> <string name="pref_theme_title_use_system">Sistemaren gaia erabili</string> <string name="pref_theme_title_light">Argia</string> <string name="pref_theme_title_dark">Iluna</string> @@ -421,8 +445,6 @@ <string name="pref_gpodnet_full_sync_title">Sinkronizazio osoa behartu</string> <string name="pref_gpodnet_full_sync_sum">Sinkronizatu harpidetza denak eta saioak gpodder.net-ekin</string> <string name="pref_gpodnet_login_status"><![CDATA[Horrela hasi du saioa <i>%1$s</i> gailu honekin <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Akatsa sinkronizazioan </string> - <string name="pref_gpodnet_notifications_sum">Ezarpen honek ez du eraginik saio hasierako erroretan eraginik.</string> <string name="pref_playback_speed_sum">Pertsonalizatu abiadura aldakorreko erreprodukzioan dauden abiadurak</string> <string name="pref_feed_playback_speed_sum">Podcast hauen berezko irakurtze abiadura</string> <string name="pref_feed_skip">Salto automatikoa</string> @@ -439,8 +461,6 @@ <string name="pref_rewind">Atzera egiteko tartea</string> <string name="pref_rewind_sum">Pertsonalizatu zenbat segundu egingo duen atzera atzera botoia sakatzean </string> - <string name="pref_gpodnet_sethostname_title">Ezarri gailuaren izena</string> - <string name="pref_gpodnet_sethostname_use_default_host">Erabili lehenetsitako izena</string> <string name="pref_expandNotify_title">Jakinarazpenen lehentasuna</string> <string name="pref_expandNotify_sum">Honek jakinarazpenak zabaltzen ditu erreprodukzio botoiak erakusteko</string> <string name="pref_persistNotify_title">Erreprodukzio kontrol iraunkorrak</string> @@ -451,10 +471,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Ezingo duzu %1$d elementu baino gehiago aukeratu.</string> <string name="pref_lockscreen_background_title">Ezarri blokeo pantailaren atzealdea</string> <string name="pref_lockscreen_background_sum">Ezarri saioaren irudia blokeo pantailarako atzealdea moduan. Horren eraginez, hirugarrenen aplikazio irudiak ere azalduko dira.</string> - <string name="pref_showDownloadReport_title">Deskargak huts egin du</string> - <string name="pref_showDownloadReport_sum">Deskargak huts egiten badu, sortu txostena akatsaren xehetasunekin.</string> - <string name="pref_showAutoDownloadReport_title">Deskarga automatikoak osatuta</string> - <string name="pref_showAutoDownloadReport_sum">Erakutsi automatikoki deskargatutako saioen jakinarazpen bat.</string> <string name="pref_expand_notify_unsupport_toast">Android 4.1 aurreko bertsioek ez dituzte zabaldutako jakinarazpenak jasaten</string> <string name="pref_enqueue_location_title">Ilaran gehitu kokalekuan</string> <string name="pref_enqueue_location_sum">Saioak hemen gehitu: %1$s</string> @@ -464,6 +480,7 @@ <string name="pref_smart_mark_as_played_disabled">Desgaitua</string> <string name="pref_image_cache_size_title">Irudiak biltegiratzeko tamaina</string> <string name="pref_image_cache_size_sum">Diskoko irudien biltegiratze tamaina</string> + <string name="documentation_support">Dokumentazioa & Laguntza</string> <string name="visit_user_forum">Erabiltzaileen foroa</string> <string name="bug_report_title">Errorearen berri eman</string> <string name="open_bug_tracker">Erroreen bilatzailea irekirik</string> @@ -475,14 +492,14 @@ <string name="pref_current_value">Egungo balioa: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Konfiguratu sareko proxya</string> - <string name="pref_faq">Ohiko galderak</string> <string name="pref_no_browser_found">Ez da web nabigatzailea aurkitu</string> <string name="pref_cast_title">Chromecasterako euskarria</string> <string name="pref_cast_message_play_flavor">Gaitu Cast gailuetan urrutira erreproduzitzeko euskarria (chromecast, altabozak edo Android TB modukoak)</string> <string name="pref_cast_message_free_flavor">Chromecastek AntennaPod bertsio honetan desgaiturik dauden hirugarrenen liburutegiak behar ditu</string> <string name="pref_enqueue_downloaded_title">Gehitu deskargatutakoak ilarara</string> <string name="pref_enqueue_downloaded_summary">Gehitu deskargatutako saioak ilarara</string> - <string name="media_player_builtin">Integratutako Android erreproduzitzailea</string> + <string name="media_player_builtin">Integratutako Android erreproduzigailua (zaharkituta)</string> + <string name="media_player_sonic">Sonic Media Player (zaharkituta) </string> <string name="media_player_exoplayer_recommended">ExoPlayer (gomendatua)</string> <string name="media_player_switch_to_exoplayer">Aldatu ExoPlayer-era</string> <string name="media_player_switched_to_exoplayer">ExoPlayer-era aldatu da.</string> @@ -571,6 +588,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Ezarri tenporizadore bat</string> <string name="disable_sleeptimer_label">Desgaitu tenporizadorea</string> + <string name="extend_sleep_timer_label">+%d minutu</string> <string name="sleep_timer_label">Tenporizadorea</string> <string name="time_dialog_invalid_input">Baliogabeko sarrera, denbora zenbaki osoa izan behar du</string> <string name="shake_to_reset_label">Astindu berrezarteko</string> @@ -598,22 +616,22 @@ <string name="gpodnet_suggestions_header">IRADOKIZUNAK</string> <string name="gpodnet_search_hint">Bilatu gpodder.net-en</string> <string name="gpodnetauth_login_title">Hasi saioa</string> - <string name="gpodnetauth_login_descr">Ongi etorri gpodder.net saio hasierara. Hasteko zure saio hasierako datuak sartu:</string> <string name="gpodnetauth_login_butLabel">Hasi saioa</string> - <string name="gpodnetauth_login_register">Oraindik konturik ez baduzu hemen sortu dezakezu:\nhttps://gpodder.net/register/</string> + <string name="create_account">Kontua sortu</string> <string name="username_label">Erabioltzailea</string> <string name="password_label">Pasahitza</string> - <string name="gpodnetauth_device_title">Gailua aukeratzea</string> + <string name="gpodnet_description">Gpodder.net AntennaPod proiektutik independentea den sinkronizazio zerbitzu bat da.</string> + <string name="gpodnetauth_server_official">gpodder.net zerbitzari ofiziala</string> + <string name="gpodnetauth_server_custom">Zerbitzari pertsonalizatua</string> + <string name="gpodnetauth_host">Ostalari izena</string> + <string name="gpodnetauth_select_server">Aukeratu zerbitzaria</string> <string name="gpodnetauth_device_descr">Sortu gailu bat zure gpodder.net kontua erabiltzeko edo dagoenetako bat aukeratu:</string> - <string name="gpodnetauth_device_deviceID">Gailuaren ID:\u0020</string> - <string name="gpodnetauth_device_caption">Deskribapena</string> - <string name="gpodnetauth_device_butCreateNewDevice">Sortu gailu berria</string> - <string name="gpodnetauth_device_chooseExistingDevice">Dagoen gailuetako bat aukeratu:</string> - <string name="gpodnetauth_device_errorEmpty">Gailuaren ID ezin du hutsik egon</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Gailuaren ID dagoeneko erabiltzen da</string> + <string name="gpodnetauth_device_name">Gailuaren izena</string> + <string name="gpodnetauth_device_name_default">AntennaPod aktibatuta %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Testuak ezin du hutsik egon</string> + <string name="gpodnetauth_existing_devices">Dauden gailuak</string> + <string name="gpodnetauth_create_device">Sortu gailua</string> <string name="gpodnetauth_device_butChoose">Aukeratu</string> - <string name="gpodnetauth_finish_title">Saioa ondo hasi duzu!</string> <string name="gpodnetauth_finish_descr">Zorionak! Zure gpodder.net kontua zure gailuarekin lotuta dago orain. Hemendik aurrera, AntennaPodek automatikoki sinkronizatuko ditu zure gailuaren harpidetzak bzure gpodder.net kontuarekin.</string> <string name="gpodnetauth_finish_butsyncnow">Hasi sinkronizazioa orain</string> <string name="gpodnetauth_finish_butgomainscreen">Joan pantaila nagusira</string> @@ -648,7 +666,7 @@ <string name="pref_resumeAfterCall_title">Berrekin dei baten ondoren</string> <string name="pref_restart_required">Beharrezkoa da AntennaPod berrabiaraztea aldaketak gauzatzeko.</string> <!--Online feed view--> - <string name="subscribe_label">Eman izena</string> + <string name="subscribe_label">Harpidetu</string> <string name="subscribing_label">Harpidetzen...</string> <string name="preview_episode">Aurretiko ikuspegia</string> <string name="stop_preview">Gelditu aurretiko ikuspegia</string> @@ -667,6 +685,7 @@ <string name="switch_pages">Aldatu orriak</string> <string name="position">Kokalekua: %1$s</string> <string name="apply_action">Aplikatu ekintza</string> + <string name="play_chapter">Kapitulua abiarazi</string> <!--Feed information screen--> <string name="authentication_label">Egiaztatzea</string> <string name="authentication_descr">Aldatu podcast honen eta bere saioen erabiltzaile izena eta pasahitza.</string> @@ -801,18 +820,22 @@ <string name="cast_failed_receiver_player_error">Erreproduzigailuak akats larria aurkitu du</string> <string name="cast_failed_media_error_skipping">Errorea medioa erreproduzitzean. Jauzi egiten …</string> <!--Notification channels--> + <string name="notification_group_errors">Erroreak</string> + <string name="notification_group_news">Berriak</string> <string name="notification_channel_user_action">Beharrezko ekintza</string> <string name="notification_channel_user_action_description">Zure ekintza beharrezkoa den erakusten da, adibidez, pasahitz bat sartu behar duzun edo ez.</string> <string name="notification_channel_downloading">Deskargatzen</string> <string name="notification_channel_downloading_description">Deskargatu bitartean erakusten da.</string> <string name="notification_channel_playing">Erreproduzitzen</string> <string name="notification_channel_playing_description">Erreprodukzioa kontrolatzeko aukera ematen du. Podcast bat erreproduzitzen den bitartean ikusten den jakinarazpen nagusia da.</string> - <string name="notification_channel_error">Erroreak</string> - <string name="notification_channel_error_description">Zerbait gaizki irten bada erakusten du, adibidez, jeitsi edo feed-eguneraketak huts egiten badute.</string> - <string name="notification_channel_sync_error">Akatsak sinkronizazioan </string> + <string name="notification_channel_download_error">Deskargak huts egin du</string> + <string name="notification_channel_download_error_description">Deskargak edo jarioen eguneratzeak huts egiten duenean erakusten da.</string> + <string name="notification_channel_sync_error">Sinkronizazioak huts egin du</string> <string name="notification_channel_sync_error_description">Erakutsi gpoder -en sinkronizazioak huts egitean.</string> - <string name="notification_channel_auto_download">Deskarga automatikoak</string> + <string name="notification_channel_auto_download">Deskarga automatikoak osatuta</string> <string name="notification_channel_episode_auto_download">Pasarteak automatikoki deskargatu direnean erakusten da.</string> + <string name="notification_channel_new_episode">Saio berria</string> + <string name="notification_channel_new_episode_description">Saio berriak eskuragarri daudenean eta jakinarazpenak aktibatu direnean erakusten da</string> <!--Widget settings--> <string name="widget_settings">Widget ezarpenak</string> <string name="widget_create_button">widget-a sortu</string> diff --git a/core/src/main/res/values-fa/strings.xml b/core/src/main/res/values-fa/strings.xml index d4bb6a29c..0ac22655c 100644 --- a/core/src/main/res/values-fa/strings.xml +++ b/core/src/main/res/values-fa/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">آمار</string> <string name="add_feed_label">افزودن پادکست</string> <string name="episodes_label">قسمتها</string> + <string name="queue_label">صف</string> <string name="all_episodes_short_label">همه</string> <string name="new_episodes_label">جدید</string> <string name="favorite_episodes_label">محبوبها</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">گزارش</string> <string name="subscriptions_label">اشتراکها</string> <string name="subscriptions_list_label">فهرست اشتراکها</string> - <string name="cancel_download_label">لغو\nبارگیری</string> <string name="playback_history_label">تاریخچه پخش</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net ورود</string> @@ -26,6 +26,7 @@ <string name="playback_statistics_label">پخش</string> <string name="download_statistics_label">بارگیریها</string> <string name="notification_pref_fragment">اعلان ها</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">مجموع زمان پادکستهای پخششده:</string> <string name="statistics_details_dialog">%1$d از %2$d قسمتها شروع شده است.\n\n%3$s از %4$s پخش.</string> @@ -81,7 +82,6 @@ <string name="description_label">شرح</string> <string name="episodes_suffix">\u0020قسمت</string> <string name="processing_label">در حال پردازش</string> - <string name="save_username_password_label">ذخیرهٔ نام کاربری و رمز عبور</string> <string name="close_label">بستن</string> <string name="retry_label">تلاش مجدد</string> <string name="auto_download_label">شامل بارگیری خودکار شود</string> @@ -93,7 +93,6 @@ <string name="feed_volume_reduction_off">خاموش</string> <string name="feed_volume_reduction_light">سبک</string> <string name="feed_volume_reduction_heavy">سنگین</string> - <string name="parallel_downloads_suffix">\u0020بارگیری همزمان</string> <string name="feed_auto_download_global">پیشفرض جهانی</string> <string name="feed_auto_download_always">همیشه</string> <string name="feed_auto_download_never">هیچگاه</string> @@ -145,7 +144,6 @@ <string name="hide_not_queued_episodes_label">خارج از صف</string> <string name="hide_has_media_label">دارای رسانه</string> <string name="filtered_label">فیلتر شده</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} آخرین تازهسازی ناموفق بود</string> <string name="open_podcast">باز کردن پادکست</string> <string name="please_wait_for_data">لطفا تا زمان دریافت اطلاعات منتظر بمانید</string> <!--actions on feeditems--> @@ -206,16 +204,12 @@ <string name="download_error_details">جزئیات</string> <string name="download_error_details_message">%1$s\n\nنشانی پرونده:\n%2$s</string> <string name="download_error_device_not_found">حافظهٔ خارجی یافت نشد</string> - <string name="download_error_insufficient_space">فضای ناکافی</string> <string name="download_error_http_data_error">خطای HTTP Data </string> <string name="download_error_error_unknown">خطای ناشناخته</string> - <string name="download_error_parser_exception">خطای تجزیه</string> <string name="download_error_unsupported_type"> عدم پشتیبانی از این نوع خوراک</string> <string name="download_error_connection_error">خطای اتصال</string> - <string name="download_error_unknown_host">میزبان ناشناس</string> <string name="download_error_unauthorized">خطای احراز هویت</string> <string name="download_error_file_type_type">خطای نوع پرونده</string> - <string name="download_error_forbidden">ممنوع</string> <string name="download_canceled_msg">بارگیری لغو شد</string> <string name="download_canceled_autodownload_enabled_msg">بارگیری لغو شد\nغیر فعال کردن <i>بارگیری خودکار</i> برای این مورد </string> <string name="download_report_title">بارگیریها با خطا(ها) کامل شد</string> @@ -229,7 +223,6 @@ <item quantity="one">%d بارگیری باقی مانده است</item> <item quantity="other">%d بارگیری باقی مانده است</item> </plurals> - <string name="downloads_processing">پردازش بارگیریها</string> <string name="download_notification_title">بارگیری داده پادکست</string> <string name="download_log_title_unknown">عنوان ناشناخته</string> <string name="download_type_feed">خوراک</string> @@ -319,7 +312,6 @@ <string name="storage_pref">حافظه</string> <string name="storage_sum">حذف خودکار قسمت، درونریزی، برونریزی</string> <string name="project_pref">پروژه</string> - <string name="queue_label">صف</string> <string name="synchronization_pref">همگامسازی</string> <string name="synchronization_sum">با کمک gpodder.net با دستگاههای دیگر همگام کنید</string> <string name="automation">اتوماسیون</string> @@ -335,14 +327,9 @@ <string name="preference_search_clear_history">پاک کردن تاریخچه</string> <string name="media_player">پخشکننده رسانه</string> <string name="pref_episode_cleanup_title">پاکسازی قسمت</string> - <string name="pref_episode_cleanup_summary">قسمتهایی که در صف موجود نیستند و در مورد علاقه هم نیستند باید واجد شرایط برای حذف باشد اگر بارگیری خودکار فضای بیشتری برای قسمتها جدید میخواهد</string> <string name="pref_pauseOnDisconnect_sum">متوقفسازی پخش هنگامی که هدفنها یا بلوتوث قطع شود</string> <string name="pref_unpauseOnHeadsetReconnect_sum">ادامه پخش وقتی که هدفنها دوباره متصل شود</string> <string name="pref_unpauseOnBluetoothReconnect_sum">ادامه پخش هنگامی که بلوتوث دوباره متصل شود</string> - <string name="pref_hardwareForwardButtonSkips_title">دکمه پرشهای به جلو</string> - <string name="pref_hardwareForwardButtonSkips_sum">هنگام فشار دادن دکمه جلو بر روی دستگاه متصل به بلوتوث ، به جای فوروارد ، به قسمت بعدی بروید</string> - <string name="pref_hardwarePreviousButtonRestarts_title">دکمه «قبلی» دوباره راه اندازی شد</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">هنگام فشار دادن دکمه فیزیکی «قبلی» ، قسمت فعلی را دوباره شروع کنید بجای عقب رفتن</string> <string name="pref_followQueue_sum">پس از پایان پخش ، به مورد صف بعدی بروید</string> <string name="pref_auto_delete_sum">با پایان پخش ، قسمت حذف شود</string> <string name="pref_auto_delete_title">حذف خودکار</string> @@ -362,7 +349,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">از کار انداختن</string> <string name="pref_autoUpdateIntervallOrTime_Interval">تنظیم بازه</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">تنظیم زمان روز</string> - <string name="pref_autoUpdateIntervallOrTime_every">هر %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">در %1$s</string> <string name="pref_followQueue_title">پخش مداوم</string> <string name="pref_pauseOnHeadsetDisconnect_title">قطع اتصال هدفون یا بلوتوث</string> @@ -397,7 +383,6 @@ <string name="pref_episode_cache_title">قسمت های ذخیره شده</string> <string name="pref_episode_cache_summary">تعداد کل قسمتهای قابل بارگیری در ذخیره گاه دستگاه. در صورت رسیدن به این تعداد بارگیری خودکار به حالت تعلیق در می آید.</string> <string name="pref_episode_cover_title">استفاده از عکس قسمت</string> - <string name="pref_episode_cover_summary">هر زمان که جلد مخصوص قسمت در دسترس بود از آن استفاده کن. در صورت لغو انتخاب ، برنامه همیشه از تصویر جلد پادکست استفاده می کند.</string> <string name="pref_theme_title_use_system">از طرح زمینه سیستم استفاده کنید</string> <string name="pref_theme_title_light">روشن</string> <string name="pref_theme_title_dark">تاریک</string> @@ -417,8 +402,6 @@ <string name="pref_gpodnet_full_sync_title">همگام سازی کامل را اجبار کن</string> <string name="pref_gpodnet_full_sync_sum">همه اشتراک ها و حالت های قسمت را با gpodder.net همگام سازی کنید.</string> <string name="pref_gpodnet_login_status"><![CDATA[وارد شد به عنوان <i>%1$s</i> با وسیله <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">همگام سازی انجام نشد</string> - <string name="pref_gpodnet_notifications_sum">این تنظیمات در مورد خطاهای احراز هویت اعمال نمی شود.</string> <string name="pref_playback_speed_sum">سرعتهای موجود برای بازپخش با سرعت متغیر را سفارشی کنید</string> <string name="pref_feed_playback_speed_sum">سرعت شروع بازپخش صدا برای قسمت های این پادکست</string> <string name="pref_feed_skip">پرش خودکار</string> @@ -433,8 +416,6 @@ <string name="pref_fast_forward_sum">با کلیک بر روی دکمه سریع جلو ، تعداد ثانیه ها را برای پرش به جلو تنظیم کنید</string> <string name="pref_rewind">زمان پرش برای عقب رفتن سریع</string> <string name="pref_rewind_sum">با کلیک روی دکمه برگشت تعداد ثانیه ها را برای پرش به عقب تنظیم کنید</string> - <string name="pref_gpodnet_sethostname_title">تنظیم نام میزبان</string> - <string name="pref_gpodnet_sethostname_use_default_host">استفاده از میزبان پیش فرض</string> <string name="pref_expandNotify_title">اعلان با الویت بالا</string> <string name="pref_expandNotify_sum">این معمولاً اعلان را برای نمایش دکمه های پخش گسترش می دهد.</string> <string name="pref_persistNotify_title">کنترلهای پخش مداوم</string> @@ -445,10 +426,6 @@ <string name="pref_compact_notification_buttons_dialog_error">شما فقط می توانید حداکثر %1$d مورد را انتخاب کنید.</string> <string name="pref_lockscreen_background_title">تنظیم پسزمینه صفحهٔ قفل</string> <string name="pref_lockscreen_background_sum">پس زمینه صفحه قفل را روی تصویر جلد قسمت فعلی تنظیم کن. به عنوان اثر جانبی ، این تصویر در برنامه های شخص ثالث نیز نشان داده خواهد شد.</string> - <string name="pref_showDownloadReport_title">دانلود ناموفق</string> - <string name="pref_showDownloadReport_sum">در صورت شکست در بارگیری، گزارشی تولید شود که جزئیات شکست را نشان دهد.</string> - <string name="pref_showAutoDownloadReport_title">بارگیری خودکار انجام شد</string> - <string name="pref_showAutoDownloadReport_sum">اعلانی را برای قسمتهای بارگیری خودکار نشان دهید.</string> <string name="pref_expand_notify_unsupport_toast">اندرویدهای قدیمیتر از نسخه ۴٫۱ از اعلانهای بسطیافته پشتیبانی نمیکنند.</string> <string name="pref_enqueue_location_title">مکان را از صف در آور</string> <string name="pref_enqueue_location_sum">قسمتها را به اضافه کنید به : %1$s</string> @@ -469,14 +446,12 @@ <string name="pref_current_value">مقدار فعلی: %1$s</string> <string name="pref_proxy_title">پروکسی</string> <string name="pref_proxy_sum">تنظیم پروکسی شبکه</string> - <string name="pref_faq">سوالات متداول</string> <string name="pref_no_browser_found">مرورگر وب پیدا نشد.</string> <string name="pref_cast_title">پشتیبانی از کرومکست</string> <string name="pref_cast_message_play_flavor">پشتیبانی از پخش از راه دور رسانه را در دستگاه های Cast فعال کنید (مانند Chromecast ، بلندگوهای صوتی یا Android TV)</string> <string name="pref_cast_message_free_flavor">کرومکست نیازمند کتابخانههای غیرآزاد است که در این نسخه از AntennaPod غیر فعال هستند</string> <string name="pref_enqueue_downloaded_title">در صف نهادن بارگیریشدهها</string> <string name="pref_enqueue_downloaded_summary">قسمتهای بارگیری شده را به صف اضافه کنید</string> - <string name="media_player_builtin">پخشکننده پیشفرض اندروید</string> <string name="media_player_exoplayer_recommended">ExoPlayer (توصیه می شود)</string> <string name="media_player_switch_to_exoplayer">به ExoPlayer بروید</string> <string name="media_player_switched_to_exoplayer">به ExoPlayer تغییر وضعیت داده شد.</string> @@ -592,22 +567,12 @@ <string name="gpodnet_suggestions_header">پیشنهادها</string> <string name="gpodnet_search_hint">جستوجوی gpodder.net</string> <string name="gpodnetauth_login_title">ورود</string> - <string name="gpodnetauth_login_descr">به فرایند ورود gpodder.net خوش آمدید. نحست اطّلاعات ورودتان را بنویسید:</string> <string name="gpodnetauth_login_butLabel">ورود</string> - <string name="gpodnetauth_login_register">اگر هنوز حسابی ندارید، میتوانید اینجا یکی بسازید:\https://gpodder.net/register/n</string> <string name="username_label">نام کاربری</string> <string name="password_label">گذرواژه</string> - <string name="gpodnetauth_device_title">گزینش افزاره</string> <string name="gpodnetauth_device_descr">برای استفادهٔ حساب gpodder,netتان حسابی جدید ساخته یا حسابی موجود را برگزینید:</string> - <string name="gpodnetauth_device_deviceID">شناسهٔ افزاره:\u0020</string> - <string name="gpodnetauth_device_caption">عنوان</string> - <string name="gpodnetauth_device_butCreateNewDevice">ایجاد افزارهٔ جدید</string> - <string name="gpodnetauth_device_chooseExistingDevice">گزینش موجود</string> - <string name="gpodnetauth_device_errorEmpty">شناسهٔ افزاره نباید خلی باشد</string> - <string name="gpodnetauth_device_errorAlreadyUsed">شناسهٔ افزاره از پیش در حال استفاده است</string> <string name="gpodnetauth_device_caption_errorEmpty">عنوان نباید خالی باشد</string> <string name="gpodnetauth_device_butChoose">گزینش</string> - <string name="gpodnetauth_finish_title">ورود موفق!</string> <string name="gpodnetauth_finish_descr">تبریک می گویم! حساب gpodder.net شما اکنون با دستگاه شما پیوند داده شده است. آنتن پاد از این پس اشتراک های دستگاه شما را با حساب gpodder.net خود به طور خودکار همگام سازی می کند.</string> <string name="gpodnetauth_finish_butsyncnow">شروع همگامسازی</string> <string name="gpodnetauth_finish_butgomainscreen">رفتن به صفحهٔ اصلی</string> @@ -801,11 +766,7 @@ <string name="notification_channel_downloading_description">هنگام بارگیری نشان داده می شود.</string> <string name="notification_channel_playing">در حال اجرا</string> <string name="notification_channel_playing_description">امکان کنترل پخش را فراهم می کند. این اعلان اصلی است که هنگام پخش پادکست مشاهده می کنید.</string> - <string name="notification_channel_error">خطاها</string> - <string name="notification_channel_error_description">اگر مشکلی پیش آمد ، به عنوان مثال اگر در بارگیری یا به روزرسانی خبره مشکلی پیش آمد ، نشان داده می شود.</string> - <string name="notification_channel_sync_error">خطاهای همگام سازی</string> <string name="notification_channel_sync_error_description">هنگامی که همگام سازی gpodder انجام نشد نشان داده می شود.</string> - <string name="notification_channel_auto_download">بارگیریهای خودکار</string> <string name="notification_channel_episode_auto_download">وقتی قسمتها به طور خودکار بارگیری می شوند ، نشان داده می شوند.</string> <!--Widget settings--> <string name="widget_settings">تنظیمات ویجت</string> diff --git a/core/src/main/res/values-fi/strings.xml b/core/src/main/res/values-fi/strings.xml index ff6c9c773..4f4ebdf18 100644 --- a/core/src/main/res/values-fi/strings.xml +++ b/core/src/main/res/values-fi/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Tilastot</string> <string name="add_feed_label">Lisää Podcast</string> <string name="episodes_label">Jaksot</string> + <string name="queue_label">Jono</string> <string name="all_episodes_short_label">Kaikki</string> <string name="new_episodes_label">Uusi</string> <string name="favorite_episodes_label">Suosikit</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">Loki</string> <string name="subscriptions_label">Tilaukset</string> <string name="subscriptions_list_label">Tilauslista</string> - <string name="cancel_download_label">Peruuta\nLataus</string> <string name="playback_history_label">Soittohistoria</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net Kirjautuminen</string> @@ -25,6 +25,7 @@ <string name="episode_cache_full_message">Jaksojen välimuistin rajoitus on ylitetty. Voit lisätä välimuistin kokoa Asetuksissa.</string> <string name="playback_statistics_label">Toisto</string> <string name="download_statistics_label">Lataukset</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="statistics_details_dialog">%1$d jakso %2$d:sta aloitettu.\n\nSoitettu %3$s jaksoa %4$s:sta.</string> <string name="statistics_mode">Tilastointitila</string> @@ -75,7 +76,6 @@ <string name="description_label">Kuvaus</string> <string name="episodes_suffix">\u0020jaksoa</string> <string name="processing_label">Prosessoi</string> - <string name="save_username_password_label">Tallenna käyttäjätunnus ja salasana</string> <string name="close_label">Sulje</string> <string name="retry_label">Yritä uudelleen</string> <string name="auto_download_label">Lataa automaattisesti</string> @@ -87,7 +87,6 @@ <string name="feed_volume_reduction_off">Pois käytöstä</string> <string name="feed_volume_reduction_light">Kevyt</string> <string name="feed_volume_reduction_heavy">Voimakas</string> - <string name="parallel_downloads_suffix">\u0020yhtäaikaiset lataukset</string> <string name="feed_auto_download_global">Globaali oletus</string> <string name="feed_auto_download_always">Aina</string> <string name="feed_auto_download_never">Ei koskaan</string> @@ -134,7 +133,6 @@ <string name="hide_not_queued_episodes_label">Ei jonossa</string> <string name="hide_has_media_label">Sisältää mediaa</string> <string name="filtered_label">Suodatettu</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Viimeisin päivitys epäonnistui</string> <string name="open_podcast">Avaa podcast</string> <string name="please_wait_for_data">Odota kunnes tiedot ovat ladattu</string> <!--actions on feeditems--> @@ -195,16 +193,12 @@ <string name="download_error_details">Tiedot</string> <string name="download_error_details_message">%1$s \n\nTiedoston URL:\n%2$s</string> <string name="download_error_device_not_found">Tallennuslaitetta ei löytynyt</string> - <string name="download_error_insufficient_space">Ei tarpeeksi tilaa</string> <string name="download_error_http_data_error">HTTP Data virhe</string> <string name="download_error_error_unknown">Tuntematon virhe</string> - <string name="download_error_parser_exception">Jäsenninpoikkeus</string> <string name="download_error_unsupported_type">Ei tuettu syötetyyppi</string> <string name="download_error_connection_error">Yhteysvirhe</string> - <string name="download_error_unknown_host">Tuntematon isäntä</string> <string name="download_error_unauthorized">Todentamisvirhe</string> <string name="download_error_file_type_type">Tiedostotyyppivirhe</string> - <string name="download_error_forbidden">Ei sallittu</string> <string name="download_canceled_msg">Lataus peruutettu</string> <string name="download_canceled_autodownload_enabled_msg">Lataus peruutettu\nPoistettu <i>Automaattinen lataus</i> tälle tiedolle </string> <string name="download_report_title">Lataukset valmistuivat virhe(id)en kanssa</string> @@ -218,7 +212,6 @@ <item quantity="one">%d lataus jäljellä</item> <item quantity="other">%d latausta jäljellä</item> </plurals> - <string name="downloads_processing">Käsitellään latauksia</string> <string name="download_notification_title">Ladataan podcastin tietoja</string> <string name="download_log_title_unknown">Tuntematon otsikko</string> <string name="download_type_feed">Syöte</string> @@ -304,7 +297,6 @@ <string name="storage_pref">Tallennus</string> <string name="storage_sum">Jakson automaattinen poisto, tuonti, vienti</string> <string name="project_pref">Projekti</string> - <string name="queue_label">Jono</string> <string name="synchronization_pref">Synkronointi</string> <string name="synchronization_sum">Synkronoi muiden laitteiden kanssa käyttäen gpodder.net-palvelua</string> <string name="automation">Automaatio</string> @@ -320,14 +312,9 @@ <string name="preference_search_clear_history">Tyhjennä historia</string> <string name="media_player">Mediasoitin</string> <string name="pref_episode_cleanup_title">Jakson siivous</string> - <string name="pref_episode_cleanup_summary">Jaksot, jotka eivät ole jonossa ja eivät ole suosikkeja tulisi olla valmiita poistoon jos Automaattiinen lataus tarvitsee tilaa uusille jaksoille</string> <string name="pref_pauseOnDisconnect_sum">Pysäytä soitto kun kuulokkeet tai bluetooth katkaistaan</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Jatka soittoa kun kuulokkeet yhdistetään uudestaan</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Jatka soittoa kun bluetooth yhdistyy uudestaan</string> - <string name="pref_hardwareForwardButtonSkips_title">Seuraava nappi ohittaa</string> - <string name="pref_hardwareForwardButtonSkips_sum">Kun painetaan seuraava-nappia bluetooth-laitteessa, hyppää seuraavaan jaksoon eteenpäin siirtymisen sijasta.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Edellinen-nappi aloittaa alusta</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Kun painetaan edellinen-nappia, aloita nykyinen jakso alusta taaksepäin siirtymisen sijasta</string> <string name="pref_followQueue_sum">Hyppää seuraavaan jonossa kun soitto valmistuu</string> <string name="pref_auto_delete_sum">Poista jakso toiston loputtua</string> <string name="pref_auto_delete_title">Automaattinen poisto</string> @@ -347,7 +334,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Poista käytöstä</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Aseta aikaväli</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Aseta ajankohta</string> - <string name="pref_autoUpdateIntervallOrTime_every">joka %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">aika %1$s</string> <string name="pref_followQueue_title">Jatkuva toisto</string> <string name="pref_unpauseOnHeadsetReconnect_title">Kuulokkeiden uudelleenyhdistyminen</string> @@ -380,7 +366,6 @@ <string name="pref_episode_cache_title">Jaksojen välimuisti</string> <string name="pref_episode_cache_summary">Ladattuja jaksoja yhteensä välimuistissa tällä laitteella. Automaattinen lataaminen pysäytetään, jos tämä raja ylittyy.</string> <string name="pref_episode_cover_title">Käytä jakson kansikuvaa</string> - <string name="pref_episode_cover_summary">Käytä jaksokohtaista kansikuvaa, kun se on saatavilla. Jos tämä ei ole valittuna, sovellus käyttää aina podcastin kansikuvaa.</string> <string name="pref_theme_title_light">Vaalea</string> <string name="pref_theme_title_dark">Tumma</string> <string name="pref_theme_title_trueblack">Musta (AMOLED valmis)</string> @@ -397,15 +382,12 @@ <string name="pref_gpodnet_sync_changes_sum">Synkronoi tilaukset ja jaksojen tilojen muutokset gpodder.net.</string> <string name="pref_gpodnet_full_sync_sum">Synkronoi kaikki tilaukset ja jaksojen tilastot gpodder.net</string> <string name="pref_gpodnet_login_status"><![CDATA[Kirjauduttu <i>%1$s</i> laitteella <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_sum">Tämä asetus ei vaikuta autentikointivirheisiin.</string> <string name="pref_playback_time_respects_speed_title">Mukauta mediatietoja soiton nopeuteen</string> <string name="pref_playback_time_respects_speed_sum">Näytetty aika ja kesto mukautuvat soiton nopeuteen</string> <string name="pref_fast_forward">Seuraava skippaa aikaa</string> <string name="pref_fast_forward_sum">Kustomoi sekunnit jonka verran skipataaan kun painat Seuraava nappia.</string> <string name="pref_rewind">Edellinen skippaa aikaa</string> <string name="pref_rewind_sum">kustomoi sekunnit jonka verran hypätään takaisin kun painat Edellinen nappia.</string> - <string name="pref_gpodnet_sethostname_title">Aseta hostname</string> - <string name="pref_gpodnet_sethostname_use_default_host">Käytä oletusporttia</string> <string name="pref_expandNotify_title">Korkea ilmoitusprioriteetti</string> <string name="pref_expandNotify_sum">Tämä laajentaa ilmoituksen näyttämään soittonapit</string> <string name="pref_persistNotify_title">Pysyvät soittokontrollit</string> @@ -414,7 +396,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Voit valita vain maksimissaan %1$d asioita.</string> <string name="pref_lockscreen_background_title">Aseta lukitusruudun taustakuva</string> <string name="pref_lockscreen_background_sum">Aseta lukitusruudun taustakuva nykyisen jakson kuvaan. Tämä kuva näkyy kolmannen osapuolen sovelluksissa.</string> - <string name="pref_showDownloadReport_sum">Jos lataus epäonnistuu, generoi raportti joka näyttää virheen tiedot.</string> <string name="pref_expand_notify_unsupport_toast">Android versiot ennen 4.1 eivät tue laajenettuja ilmoituksia.</string> <string name="pref_smart_mark_as_played_disabled">Poissa käytöstä</string> <string name="pref_image_cache_size_title">Kuvan välimuistin koko</string> @@ -430,7 +411,6 @@ <string name="pref_cast_message_free_flavor">Chromecast tarvitsee kolmannen osapuolen suljettuja kirjastoja, jotka on poistettu tässä AntennaPodissa.</string> <string name="pref_enqueue_downloaded_title">Lisää ladatut jonoon</string> <string name="pref_enqueue_downloaded_summary">Lisää ladatut jaksot jonoon</string> - <string name="media_player_builtin">Sisäänrakennettu Android-soitto</string> <string name="pref_skip_silence_title">Skippaa tyhjät kohdat audiossa</string> <string name="pref_videoBehavior_title">Video sulkiessa</string> <string name="pref_videoBehavior_sum">Kun suljetaan videon soitto</string> @@ -497,22 +477,12 @@ <string name="gpodnet_suggestions_header">EHDOTUKSET</string> <string name="gpodnet_search_hint">Etsi gpodder.net</string> <string name="gpodnetauth_login_title">Sisäänkirjautuminen</string> - <string name="gpodnetauth_login_descr">Tervetuloa gpodder.net sisäänkirjautumiseen. Kirjoita kirjautumistiedot:</string> <string name="gpodnetauth_login_butLabel">Sisäänkirjautuminen</string> - <string name="gpodnetauth_login_register">Jos sinulla ei ole vielä tiliä, voit luoda sen täällä:\nhttps://gpodder.net/register/</string> <string name="username_label">Käyttäjätunnus</string> <string name="password_label">Salasana</string> - <string name="gpodnetauth_device_title">Laitevalinta</string> <string name="gpodnetauth_device_descr">Luo uusi laite gpodder.net tiliin tai valitse olemassaoleva laite:</string> - <string name="gpodnetauth_device_deviceID">Laite ID:\u0020</string> - <string name="gpodnetauth_device_caption">Kuvaus</string> - <string name="gpodnetauth_device_butCreateNewDevice">Luo uusi laite</string> - <string name="gpodnetauth_device_chooseExistingDevice">Valitse olemassaoleva laite:</string> - <string name="gpodnetauth_device_errorEmpty">Laite ID ei saa olla tyhjä</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Laite ID on jo käytössä</string> <string name="gpodnetauth_device_caption_errorEmpty">Kuvaus ei saa olla tyhjä</string> <string name="gpodnetauth_device_butChoose">Valitse</string> - <string name="gpodnetauth_finish_title">Sisäänkirjautuminen onnistui</string> <string name="gpodnetauth_finish_descr">Onneksi olkoon! Sinun gpodder.net tili on liitetty laitteeseesi. AntennaPod alkaa automaattisesti synkronoimaan gpodder.net tiliisi.</string> <string name="gpodnetauth_finish_butsyncnow">Aloita synkrointi nyt</string> <string name="gpodnetauth_finish_butgomainscreen">Mene pääsivulle</string> @@ -663,7 +633,6 @@ <string name="notification_channel_downloading_description">Näytetään kun ladataan.</string> <string name="notification_channel_playing">Soittaa nyt</string> <string name="notification_channel_playing_description">Sallii soiton kontrollit. Tämä on pääilmoitus, jonka näet kun podcast soitetaan.</string> - <string name="notification_channel_error">Virheet</string> <!--Widget settings--> <!--On-Demand configuration--> </resources> diff --git a/core/src/main/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml index a0d86ea91..15d38f963 100644 --- a/core/src/main/res/values-fr/strings.xml +++ b/core/src/main/res/values-fr/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statistiques</string> <string name="add_feed_label">Ajouter un podcast</string> <string name="episodes_label">Episodes</string> + <string name="queue_label">Liste de lecture</string> <string name="all_episodes_short_label">Tous</string> <string name="new_episodes_label">Nouveaux</string> <string name="favorite_episodes_label">Favoris</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Lecture</string> <string name="download_statistics_label">Téléchargements</string> <string name="notification_pref_fragment">Notifications</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" non trouvé</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Durée totale d\'écoute :</string> <string name="statistics_details_dialog">%1$d épisodes démarrés sur %2$d\nsoit %3$s de lues sur %4$s.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Aucun</string> <!--Bug report activity--> <string name="log_file_share_exception">Aucune application compatible trouvée</string> + <string name="export_logs_menu_title">Export détaillé des logs</string> + <string name="confirm_export_log_dialog_message">Les logs détaillés peuvent contenir des données sensibles, par exemple la liste des abonnements</string> <!--Webview actions--> <string name="open_in_browser_label">Ouvrir dans le navigateur</string> <string name="copy_url_label">Copier le lien</string> @@ -81,7 +86,6 @@ <string name="description_label">Description</string> <string name="episodes_suffix">\u0020épisodes</string> <string name="processing_label">Traitement en cours</string> - <string name="save_username_password_label">Sauvegarder votre identifiant et votre mot de passe</string> <string name="close_label">Fermer</string> <string name="retry_label">Réessayer</string> <string name="auto_download_label">Télécharger automatiquement</string> @@ -93,13 +97,14 @@ <string name="feed_volume_reduction_off">aucune</string> <string name="feed_volume_reduction_light">faible</string> <string name="feed_volume_reduction_heavy">importante</string> - <string name="parallel_downloads_suffix">\u0020téléchargements parallèles</string> + <string name="parallel_downloads">%1$d téléchargements simultanés</string> <string name="feed_auto_download_global">Option par défaut</string> <string name="feed_auto_download_always">Toujours</string> <string name="feed_auto_download_never">Jamais</string> <string name="send_label">Envoyer...</string> <string name="episode_cleanup_never">Jamais</string> - <string name="episode_cleanup_queue_removal">Quand l’épisode n\'est pas dans la liste de lecture</string> + <string name="episode_cleanup_except_favorite_removal">Quand pas un favori</string> + <string name="episode_cleanup_queue_removal">Quand pas dans la liste de lecture</string> <string name="episode_cleanup_after_listening">Après avoir terminé</string> <plurals name="episode_cleanup_hours_after_listening"> <item quantity="one">1 heure après avoir été écouté</item> @@ -113,7 +118,22 @@ <item quantity="one">%d sélectionné</item> <item quantity="other">%d sélectionnés</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d épisode</item> + <item quantity="other">%d épisodes</item> + </plurals> <string name="loading_more">Chargement...</string> + <string name="episode_notification">Notification des épisodes</string> + <string name="episode_notification_summary">Affiche une notification quand de nouveaux épisodes sont disponibles.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s a un nouvel épisode</item> + <item quantity="other">%2$s a %1$d nouveaux épisodes</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Nouvel épisode</item> + <item quantity="other">Nouveaux épisodes</item> + </plurals> + <string name="new_episode_notification_group_text">Vos abonnements disposent de nouveaux épisodes.</string> <!--Actions on feeds--> <string name="mark_all_read_label">Marquer tous les épisodes comme lus</string> <string name="mark_all_read_msg">Tous les épisodes ont été marqués comme lus</string> @@ -145,7 +165,6 @@ <string name="hide_not_queued_episodes_label">Pas dans la liste de lecture</string> <string name="hide_has_media_label">Avec média</string> <string name="filtered_label">Filtré</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} La dernière mise à jour a échoué</string> <string name="open_podcast">Ouvrir le podcast</string> <string name="please_wait_for_data">Merci d\'attendre la fin du téléchargement des données</string> <!--actions on feeditems--> @@ -160,6 +179,10 @@ <string name="delete_label">Supprimer</string> <string name="delete_failed">Suppression du fichier impossible. Redémarrer pourrait aider.</string> <string name="delete_episode_label">Suppression de l\'épisode</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d épisode sélectionné, %d téléchargement supprimé.</item> + <item quantity="other">%d épisodes sélectionnés, %d téléchargement(s) supprimé(s).</item> + </plurals> <string name="remove_new_flag_label">Ne plus considérer nouveau</string> <string name="removed_new_flag_label">Le statut \"nouveau\" a été supprimé</string> <string name="mark_read_label">Marquer comme lu</string> @@ -206,16 +229,12 @@ <string name="download_error_details">Détails</string> <string name="download_error_details_message">%1$s \n\nLien du fichier :\n%2$s</string> <string name="download_error_device_not_found">Volume de stockage non trouvé</string> - <string name="download_error_insufficient_space">Espace insuffisant</string> <string name="download_error_http_data_error">Erreur de données HTTP</string> <string name="download_error_error_unknown">Erreur inconnue</string> - <string name="download_error_parser_exception">Message d\'erreur</string> <string name="download_error_unsupported_type">Type de flux non géré</string> <string name="download_error_connection_error">Erreur de connexion</string> - <string name="download_error_unknown_host">Hôte inconnu</string> <string name="download_error_unauthorized">Erreur d\'authentification</string> <string name="download_error_file_type_type">Erreur format de fichier</string> - <string name="download_error_forbidden">Interdit</string> <string name="download_canceled_msg">Téléchargement annulé</string> <string name="download_canceled_autodownload_enabled_msg">Téléchargement annulé\n <i>Téléchargement Automatique</i> désactivé pour cet élément</string> <string name="download_report_title">Téléchargements terminés avec des erreurs</string> @@ -229,12 +248,7 @@ <item quantity="one">%d téléchargement restant</item> <item quantity="other">%d téléchargements restants</item> </plurals> - <string name="downloads_processing">Traitement des téléchargements</string> <string name="download_notification_title">Téléchargement des données du podcast</string> - <plurals name="download_report_content"> - <item quantity="one">%1$d téléchargement réussi, %2$d échoué</item> - <item quantity="other">%1$d téléchargements réussis, %2$d échoués</item> - </plurals> <string name="download_log_title_unknown">Titre inconnu</string> <string name="download_type_feed">Flux</string> <string name="download_type_media">Fichier média</string> @@ -267,6 +281,7 @@ <string name="player_go_to_picture_in_picture">Mode Picture-in-Picture</string> <string name="unknown_media_key">AntennaPod - Touche média inconnue : %1$d</string> <string name="error_file_not_found">Fichier non trouvé</string> + <string name="no_media_label">Aucun fichier média disponible</string> <!--Queue operations--> <string name="lock_queue">Verrouiller la liste de lecture</string> <string name="unlock_queue">Déverrouiller la liste de lecture</string> @@ -323,7 +338,6 @@ <string name="storage_pref">Stockage</string> <string name="storage_sum">Suppression automatique, importation, exportation</string> <string name="project_pref">Projet</string> - <string name="queue_label">Liste de lecture</string> <string name="synchronization_pref">Synchronisation</string> <string name="synchronization_sum">Utiliser gpodder.net pour synchroniser avec d\'autres appareils</string> <string name="automation">Automatisation</string> @@ -334,19 +348,24 @@ <string name="external_elements">Eléments externes</string> <string name="interruptions">Interruptions</string> <string name="playback_control">Contrôle de lecture</string> + <string name="reassign_hardware_buttons">Réaffectation des boutons</string> <string name="preference_search_hint">Chercher...</string> <string name="preference_search_no_results">Aucun résultat</string> <string name="preference_search_clear_history">Effacer l\'historique</string> <string name="media_player">Lecteur multimédia</string> <string name="pref_episode_cleanup_title">Nettoyage des épisodes</string> - <string name="pref_episode_cleanup_summary">Les épisodes qui ne sont pas dans la liste de lecture et qui ne sont pas marqués comme favoris peuvent être supprimés si l\'espace est insuffisant pour le téléchargement automatique de nouveaux épisodes</string> + <string name="pref_episode_cleanup_summary">Episodes pouvant être supprimés si le téléchargement automatique a besoin de plus de place.</string> <string name="pref_pauseOnDisconnect_sum">Interrompre la lecture quand les écouteurs ou le bluetooth sont déconnectés</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Reprendre la lecture quand des écouteurs sont branchés</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Reprendre la lecture quand le Bluetooth se reconnecte</string> - <string name="pref_hardwareForwardButtonSkips_title">Le bouton \"saut avant\" saute l\'épisode</string> - <string name="pref_hardwareForwardButtonSkips_sum">Passer à l\'épisode suivant au lieu de faire un saut avant quand \"saut avant\" est pressé sur un périphérique bluetooth</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Le bouton \"saut arrière\" redémarre l\'épisode</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Repartir de zéro au lieu de faire un saut arrière quand un bouton physique \"saut arrière\" est pressé</string> + <string name="pref_hardware_forward_button_title">Bouton Episode Suivant</string> + <string name="pref_hardware_forward_button_summary">Définir le comportement du bouton \"épisode suivant\"</string> + <string name="pref_hardware_previous_button_title">Bouton Episode Précédent</string> + <string name="pref_hardware_previous_button_summary">Définir le comportement du bouton \"épisode précédent\"</string> + <string name="button_action_fast_forward">Saut Avant</string> + <string name="button_action_rewind">Saut Arrière</string> + <string name="button_action_skip_episode">Sauter l\'épisode</string> + <string name="button_action_restart_episode">Redémarrer l\'épisode</string> <string name="pref_followQueue_sum">Après la fin d\'un épisode, passer au suivant</string> <string name="pref_auto_delete_sum">Supprimer l\'épisode quand la lecture est finie</string> <string name="pref_auto_delete_title">Suppression automatique</string> @@ -366,8 +385,11 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Désactiver</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Intervalle</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Heure</string> - <string name="pref_autoUpdateIntervallOrTime_every">toutes les %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">à %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Toutes les heures</item> + <item quantity="other">Toutes les %d heures</item> + </plurals> <string name="pref_followQueue_title">Lecture continue</string> <string name="pref_pauseOnHeadsetDisconnect_title">Déconnexion des écouteurs ou du Bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_title">Connexion des écouteurs</string> @@ -401,7 +423,9 @@ <string name="pref_episode_cache_title">Nombre d\'épisodes stockés</string> <string name="pref_episode_cache_summary">Nombre maximum d\'épisodes stockés sur l\'appareil. Le téléchargement automatique sera suspendu si ce nombre est atteint.</string> <string name="pref_episode_cover_title">Image des épisodes</string> - <string name="pref_episode_cover_summary">Lorsqu\'elles existent, utiliser les images propres aux épisodes au lieu de celle du podcast.</string> + <string name="pref_episode_cover_summary">Lorsqu\'elles existent, utiliser pour les listes les images propres aux épisodes au lieu de celle du podcast. Sinon l\'image du podcast sera toujours utilisé.</string> + <string name="pref_show_remain_time_title">Afficher la durée restante</string> + <string name="pref_show_remain_time_summary">Pour les épisodes, montrer la durée restant à lire à la place de la durée totale.</string> <string name="pref_theme_title_use_system">Thème du système</string> <string name="pref_theme_title_light">Clair</string> <string name="pref_theme_title_dark">Sombre</string> @@ -421,8 +445,6 @@ <string name="pref_gpodnet_full_sync_title">Forcer une synchronisation totale</string> <string name="pref_gpodnet_full_sync_sum">Synchroniser tous les abonnements et tous les états des épisodes avec gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Connecté comme <i>%1$s</i> avec l\'appareil <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">La synchronisation a échoué</string> - <string name="pref_gpodnet_notifications_sum">Ce paramètre ne s\'applique pas aux erreurs d\'authentification.</string> <string name="pref_playback_speed_sum">Définir les vitesses disponibles lors de la lecture</string> <string name="pref_feed_playback_speed_sum">Vitesse de lecture par défaut des épisodes</string> <string name="pref_feed_skip">Saut automatique</string> @@ -437,8 +459,6 @@ <string name="pref_fast_forward_sum">Nombre de secondes à sauter quand le bouton \"saut avant\" est pressé</string> <string name="pref_rewind">Durée du saut arrière</string> <string name="pref_rewind_sum">Nombre de secondes à sauter quand le bouton \"saut arrière\" est pressé</string> - <string name="pref_gpodnet_sethostname_title">Choisir un nom de domaine</string> - <string name="pref_gpodnet_sethostname_use_default_host">Utiliser le nom de domaine par défaut</string> <string name="pref_expandNotify_title">Priorité haute de notification</string> <string name="pref_expandNotify_sum">Permet, généralement, d\'étendre la notification pour montrer les boutons de lecture</string> <string name="pref_persistNotify_title">Boutons de lecture permanents</string> @@ -449,10 +469,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Vous ne pouvez pas choisir plus de %1$d éléments.</string> <string name="pref_lockscreen_background_title">Changer l’arrière plan de l\'écran de verrouillage</string> <string name="pref_lockscreen_background_sum">Placer l\'image de l’épisode en arrière plan de l\'écran de verrouillage. Cela aura aussi pour effet de montrer l\'image dans les autres applications.</string> - <string name="pref_showDownloadReport_title">Le téléchargement a échoué</string> - <string name="pref_showDownloadReport_sum">Si les téléchargements échouent, générer un rapport détaillé des échecs.</string> - <string name="pref_showAutoDownloadReport_title">Le téléchargement automatique est terminé</string> - <string name="pref_showAutoDownloadReport_sum">Afficher une notification pour les épisodes téléchargés automatiquement.</string> <string name="pref_expand_notify_unsupport_toast">Les versions d\'Android antérieures à 4.1 ne sont pas compatibles avec les notifications élargies</string> <string name="pref_enqueue_location_title">Emplacement des épisodes téléchargés</string> <string name="pref_enqueue_location_sum">Ajouter les épisodes : %1$s</string> @@ -462,6 +478,7 @@ <string name="pref_smart_mark_as_played_disabled">Désactivé</string> <string name="pref_image_cache_size_title">Taille du cache de l\'image</string> <string name="pref_image_cache_size_sum">Taille de l’espace de stockage temporaire des images.</string> + <string name="documentation_support">Documentation & Support</string> <string name="visit_user_forum">Forum des utilisateurs</string> <string name="bug_report_title">Signaler un bug</string> <string name="open_bug_tracker">Ouvrir le suivi des bugs</string> @@ -473,15 +490,15 @@ <string name="pref_current_value">Valeur actuelle : %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Paramétrer un réseau proxy</string> - <string name="pref_faq">Foire aux questions</string> <string name="pref_no_browser_found">Aucun navigateur trouvé.</string> <string name="pref_cast_title">Support Chromecast</string> <string name="pref_cast_message_play_flavor">Activer la lecture à distance sur les appareils Cast (comme Chromecast, Audio Speaker ou Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast nécessite des bibliothèques tierces qui sont désactivées dans cette version d\'AntennaPod</string> <string name="pref_enqueue_downloaded_title">Ajouter à la liste après téléchargement</string> <string name="pref_enqueue_downloaded_summary">Mettre les épisodes dans la la liste de lecture après téléchargement</string> - <string name="media_player_builtin">Lecteur natif d\'Android</string> - <string name="media_player_exoplayer_recommended">ExoPlayer (recommendé)</string> + <string name="media_player_builtin">Lecteur natif d\'Android (plus maintenu)</string> + <string name="media_player_sonic">Sonic Media Player (plus maintenu) </string> + <string name="media_player_exoplayer_recommended">ExoPlayer (recommandé)</string> <string name="media_player_switch_to_exoplayer">Utiliser ExoPlayer pour la lecture</string> <string name="media_player_switched_to_exoplayer">Lecteur changé pour ExoPlayer</string> <string name="pref_skip_silence_title">Supprimer les silences audios</string> @@ -569,6 +586,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Activer le minuteur</string> <string name="disable_sleeptimer_label">Désactiver le minuteur</string> + <string name="extend_sleep_timer_label">+%d min</string> <string name="sleep_timer_label">Minuteur d\'arrêt</string> <string name="time_dialog_invalid_input">Entrée invalide, la durée doit être un nombre entier</string> <string name="shake_to_reset_label">Secouer pour redémarrer</string> @@ -596,22 +614,22 @@ <string name="gpodnet_suggestions_header">SUGGESTIONS</string> <string name="gpodnet_search_hint">Chercher sur gpodder.net</string> <string name="gpodnetauth_login_title">Se connecter</string> - <string name="gpodnetauth_login_descr">Bienvenue dans le processus de connexion à gpodder.net. Premièrement, veuillez entrer vos informations de connexion :</string> <string name="gpodnetauth_login_butLabel">Connexion</string> - <string name="gpodnetauth_login_register">SI vous n\'avez pas encore de compte, vous pouvez en créer un ici:\https://gpodder.net/register/</string> + <string name="create_account">Créer un compte</string> <string name="username_label">Identifiant</string> <string name="password_label">Mot de passe</string> - <string name="gpodnetauth_device_title">Choix de l\'appareil</string> + <string name="gpodnet_description">Gpodder.net est un service de synchronisation open-source indépendant du projet AntennaPod.</string> + <string name="gpodnetauth_server_official">Serveur officiel de gpodder.net</string> + <string name="gpodnetauth_server_custom">Serveur personnalisé</string> + <string name="gpodnetauth_host">Nom d\'hôte</string> + <string name="gpodnetauth_select_server">Sélectionner le serveur</string> <string name="gpodnetauth_device_descr">Créer un nouvel appareil pour votre compte gpodder.net ou choisir un appareil existant :</string> - <string name="gpodnetauth_device_deviceID">ID de l\'appareil :\u0020</string> - <string name="gpodnetauth_device_caption">Légende</string> - <string name="gpodnetauth_device_butCreateNewDevice">Créer un nouvel appareil</string> - <string name="gpodnetauth_device_chooseExistingDevice">Choisir un appareil existant :</string> - <string name="gpodnetauth_device_errorEmpty">L\'ID de l\'appareil ne peut pas être vide</string> - <string name="gpodnetauth_device_errorAlreadyUsed">L\'ID de cet appareil est déjà en cours d\'utilisation</string> + <string name="gpodnetauth_device_name">Nom de l\'appareil</string> + <string name="gpodnetauth_device_name_default">AntennaPod sur %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Le nom ne peut pas être vide</string> + <string name="gpodnetauth_existing_devices">Appareils déjà existants</string> + <string name="gpodnetauth_create_device">Créer un appareil</string> <string name="gpodnetauth_device_butChoose">Choisir</string> - <string name="gpodnetauth_finish_title">Connexion réussie !</string> <string name="gpodnetauth_finish_descr">Félicitations ! Votre compte gpodder.net est maintenant lié à votre appareil. AntennaPod va désormais automatiquement synchroniser vos podcasts sur votre appareil avec votre compte gpodder.</string> <string name="gpodnetauth_finish_butsyncnow">Commencer la synchronisation</string> <string name="gpodnetauth_finish_butgomainscreen">Aller à l\'écran d\'accueil</string> @@ -665,6 +683,7 @@ <string name="switch_pages">Changer les pages</string> <string name="position">Position : %1$s</string> <string name="apply_action">Appliquer l\'action</string> + <string name="play_chapter">Lire le chapitre</string> <!--Feed information screen--> <string name="authentication_label">Authentification</string> <string name="authentication_descr">Identifiant et mot de passe pour ce podcast.</string> @@ -679,7 +698,7 @@ <string name="auto_download_disabled_globally">Le téléchargement automatique n\'est pas activé dans les préférences</string> <string name="statistics_listened_for">Temps d\'écoute :</string> <string name="statistics_episodes_on_device">Episodes sur l\'appareil :</string> - <string name="statistics_space_used">Place utilisée :</string> + <string name="statistics_space_used">Espace utilisé :</string> <string name="statistics_view_all">Voir les statistiques pour tous les podcasts »</string> <!--Progress information--> <string name="progress_upgrading_database">Mise à jour de la base de données</string> @@ -719,8 +738,8 @@ <string name="downloaded_label">Téléchargés</string> <string name="selected_downloaded_label">Episodes téléchargés sélectionnés</string> <string name="not_downloaded_label">Non téléchargés</string> - <string name="selected_not_downloaded_label">Épisodes non téléchargés sélectionnés</string> - <string name="selected_queued_label">Épisodes présents dans la liste de lecture sélectionnés</string> + <string name="selected_not_downloaded_label">Episodes non téléchargés sélectionnés</string> + <string name="selected_queued_label">Episodes présents dans la liste de lecture sélectionnés</string> <string name="selected_not_queued_label">Episodes absents de la liste de lecture sélectionnés</string> <string name="selected_has_media_label">Sélectionner les épisodes avec des médias</string> <string name="hide_is_favorite_label">Est un favori</string> @@ -799,18 +818,22 @@ <string name="cast_failed_receiver_player_error">Le lecteur de réception a rencontré une grave erreur</string> <string name="cast_failed_media_error_skipping">Erreur de lecture du média. Passage au suivant...</string> <!--Notification channels--> + <string name="notification_group_errors">Erreurs</string> + <string name="notification_group_news">Informations</string> <string name="notification_channel_user_action">Action requise</string> <string name="notification_channel_user_action_description">S\'affiche si une action est requise. Par exemple, un mot de passe à saisir.</string> <string name="notification_channel_downloading">Téléchargement en cours</string> <string name="notification_channel_downloading_description">S\'affiche lorsqu\'un téléchargement est en cours.</string> <string name="notification_channel_playing">Lecture en cours</string> <string name="notification_channel_playing_description">Permet de contrôler la lecture. C\'est la notification principale pendant la lecture d\'un podcast.</string> - <string name="notification_channel_error">Erreurs</string> - <string name="notification_channel_error_description">S\'affiche quand quelque chose c\'est mal passé. Par exemple, un téléchargement ou une mise à jour de flux qui échoue.</string> - <string name="notification_channel_sync_error">Erreurs de synchronisation</string> + <string name="notification_channel_download_error">Erreur de téléchargement</string> + <string name="notification_channel_download_error_description">S\'affiche quand un téléchargement ou la mise un jour d\'un abonnement échoue.</string> + <string name="notification_channel_sync_error">Echec de la synchronisation</string> <string name="notification_channel_sync_error_description">S\'affiche quand la synchronisation avec gpodder échoue.</string> - <string name="notification_channel_auto_download">Téléchargement automatique</string> + <string name="notification_channel_auto_download">Fin du téléchargement automatique</string> <string name="notification_channel_episode_auto_download">S\'affiche lorsque des épisodes ont été téléchargés automatiquement.</string> + <string name="notification_channel_new_episode">Nouveaux épisodes</string> + <string name="notification_channel_new_episode_description">S\'affiche quand de nouveaux épisodes sont disponible et que les notifications ont été activées</string> <!--Widget settings--> <string name="widget_settings">Préférences des widgets</string> <string name="widget_create_button">Créer un widget</string> diff --git a/core/src/main/res/values-gl/strings.xml b/core/src/main/res/values-gl/strings.xml index 4f3d385bc..4bd82858c 100644 --- a/core/src/main/res/values-gl/strings.xml +++ b/core/src/main/res/values-gl/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Estatísticas</string> <string name="add_feed_label">Engadir Podcast</string> <string name="episodes_label">Episodios</string> + <string name="queue_label">Cola</string> <string name="all_episodes_short_label">Todo</string> <string name="new_episodes_label">Novo</string> <string name="favorite_episodes_label">Favoritos</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Rexistro</string> <string name="subscriptions_label">Subscricións</string> <string name="subscriptions_list_label">Lista de subscricións</string> - <string name="cancel_download_label">Cancelar\nDescarga</string> + <string name="cancel_download_label">Cancelar descarga</string> <string name="playback_history_label">Historial de reprodución</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net Conexión</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Reprodución</string> <string name="download_statistics_label">Descargas</string> <string name="notification_pref_fragment">Notificacións</string> + <!--Google Assistant--> + <string name="app_action_not_found">Non se atopa \"%1$s\"</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Duración total dos episodios reproducidos:</string> <string name="statistics_details_dialog">%1$d de %2$d episodios iniciados.\n\nReproducidos %3$s de %4$s.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Ningún</string> <!--Bug report activity--> <string name="log_file_share_exception">Non se atopan apps compatibles</string> + <string name="export_logs_menu_title">Exportar rexistro detallado</string> + <string name="confirm_export_log_dialog_message">O rexistro detallado podería conter información sensible, como a túa lista de subscricións</string> <!--Webview actions--> <string name="open_in_browser_label">Abrir no navegador</string> <string name="copy_url_label">Copiar URL</string> @@ -81,7 +86,6 @@ <string name="description_label">Descrición</string> <string name="episodes_suffix">\u0020episodios</string> <string name="processing_label">Procesando</string> - <string name="save_username_password_label">Gardar nome de usuario e contrasinal</string> <string name="close_label">Pechar</string> <string name="retry_label">Reintentar</string> <string name="auto_download_label">Incluír en descargas automáticas</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">Apagado</string> <string name="feed_volume_reduction_light">Claro</string> <string name="feed_volume_reduction_heavy">Forte</string> - <string name="parallel_downloads_suffix">\u0020descargas paralelas</string> - <string name="feed_auto_download_global">Valor xeral por omisión</string> + <string name="parallel_downloads">%1$d descargas en paralelo</string> + <string name="feed_auto_download_global">Valor xeral por defecto</string> <string name="feed_auto_download_always">Sempre</string> <string name="feed_auto_download_never">Nunca</string> <string name="send_label">Enviar...</string> <string name="episode_cleanup_never">Nunca</string> + <string name="episode_cleanup_except_favorite_removal">Cando non favorito</string> <string name="episode_cleanup_queue_removal">Cando non esté na cola</string> <string name="episode_cleanup_after_listening">Tras rematar</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -113,7 +118,22 @@ <item quantity="one">%d seleccionado</item> <item quantity="other">%d seleccionados</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d episodio</item> + <item quantity="other">%d episodios</item> + </plurals> <string name="loading_more">Cargando máis...</string> + <string name="episode_notification">Notificación de episodios</string> + <string name="episode_notification_summary">Mostra unha notificación cando se publica un novo episodio.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s ten un novo episodio</item> + <item quantity="other">%2$s ten %1$d novos episodios</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Novo episodio</item> + <item quantity="other">Novos episodios</item> + </plurals> + <string name="new_episode_notification_group_text">As túas subscricións teñen novos episodios.</string> <!--Actions on feeds--> <string name="mark_all_read_label">Marcar todo como reproducido</string> <string name="mark_all_read_msg">Marcáronse todos como reproducidos</string> @@ -145,7 +165,6 @@ <string name="hide_not_queued_episodes_label">Fora da cola</string> <string name="hide_has_media_label">Ten multimedia</string> <string name="filtered_label">Filtrado</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Erro na última actualización</string> <string name="open_podcast">Abrir podcast</string> <string name="please_wait_for_data">Agarda ata que se carguen os datos</string> <!--actions on feeditems--> @@ -160,6 +179,10 @@ <string name="delete_label">Borrar</string> <string name="delete_failed">Non se puido eliminar o ficheiro. Reiniciar o dispositivo podería axudar.</string> <string name="delete_episode_label">Eliminar episodio</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d episodio seleccionado, %d descarga elimnada.</item> + <item quantity="other">%d episodios seleccionados, %d descarga(s) eliminadas.</item> + </plurals> <string name="remove_new_flag_label">Quitar marca \"novo\"</string> <string name="removed_new_flag_label">Eliminouse marca \"novo\"</string> <string name="mark_read_label">Marcar como reproducido</string> @@ -206,16 +229,12 @@ <string name="download_error_details">Detalles</string> <string name="download_error_details_message">%1$s\n\nURL do ficheiro:\n %2$s </string> <string name="download_error_device_not_found">Non se atopou dispositivo de almacenamento</string> - <string name="download_error_insufficient_space">Non hai suficiente espacio</string> <string name="download_error_http_data_error">Fallo de datos HTTP</string> <string name="download_error_error_unknown">Fallo descoñecido</string> - <string name="download_error_parser_exception">Excepción no procesador</string> <string name="download_error_unsupported_type">Tipo de fonte non admitida</string> <string name="download_error_connection_error">Fallo na conexión</string> - <string name="download_error_unknown_host">Servidor descoñecido</string> <string name="download_error_unauthorized">Fallo na autenticación</string> <string name="download_error_file_type_type">Fallo no tipo de ficheiro</string> - <string name="download_error_forbidden">Non admitido</string> <string name="download_canceled_msg">Descarga cancelada</string> <string name="download_canceled_autodownload_enabled_msg">Descarga cancelada\nDesactivouse a <i>Descarga Automática</i> para este elemento</string> <string name="download_report_title">Descargas completadas con erro(s)</string> @@ -229,12 +248,7 @@ <item quantity="one">%d descarga restante</item> <item quantity="other">%d descargas restantes</item> </plurals> - <string name="downloads_processing">Procesando as descargas</string> <string name="download_notification_title">Descargando datos do podcast</string> - <plurals name="download_report_content"> - <item quantity="one">%d descarga correcta, %d fallou</item> - <item quantity="other">%d descargas correctas, %d fallaron</item> - </plurals> <string name="download_log_title_unknown">Título descoñecido</string> <string name="download_type_feed">Fonte</string> <string name="download_type_media">Ficheiro de medios</string> @@ -267,6 +281,7 @@ <string name="player_go_to_picture_in_picture">Modo imaxe-en-imaxe</string> <string name="unknown_media_key">AntennaPod - chave de medios descoñecida: %1$d</string> <string name="error_file_not_found">Non se atopa o ficheiro</string> + <string name="no_media_label">O elemento non conten un ficheiro multimedia</string> <!--Queue operations--> <string name="lock_queue">Bloquear a cola</string> <string name="unlock_queue">Desbloquear a cola</string> @@ -323,7 +338,6 @@ <string name="storage_pref">Almacenamento</string> <string name="storage_sum">Borrado automático do episodio, Importar, Exportar</string> <string name="project_pref">Proxecto</string> - <string name="queue_label">Cola</string> <string name="synchronization_pref">Sincronización</string> <string name="synchronization_sum">Sincronizar con outros dispositivos usando gpodder.net</string> <string name="automation">Automatizado</string> @@ -334,19 +348,24 @@ <string name="external_elements">Elementos externos</string> <string name="interruptions">Interrupcións</string> <string name="playback_control">Control de reprodución</string> + <string name="reassign_hardware_buttons">Reasignar botóns físicos</string> <string name="preference_search_hint">Busca....</string> <string name="preference_search_no_results">Sen resultados</string> <string name="preference_search_clear_history">Limpar historial</string> <string name="media_player">Reprodutor de medios</string> <string name="pref_episode_cleanup_title">Limpeza de episodios</string> - <string name="pref_episode_cleanup_summary">Os episodios que non están na cola e tampouco son favoritos deberían poder ser candidatos a ser eliminados se a función Descarga Automática precisa espazo para novos episodios.</string> + <string name="pref_episode_cleanup_summary">Episodios que se poden eliminar se a Descarga Automática precisa espazo para novos episodios</string> <string name="pref_pauseOnDisconnect_sum">Deter a reprodución cando se desconectan os auriculares ou bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Retomar a reprodución cando se conectan os auriculares</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Retomar a reprodución cando se reconecta o bluetooth</string> - <string name="pref_hardwareForwardButtonSkips_title">O botón Adiante salta</string> - <string name="pref_hardwareForwardButtonSkips_sum">A premer no botón de adiante nun dispositivo conectado por bluetooth ir ao episodio seguinte no lugar de avance rápido</string> - <string name="pref_hardwarePreviousButtonRestarts_title">O botón Anterior reinicia</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Cando se presiona Anterior no dispositivo reinicia o episodio no lugar de ir cara atrás</string> + <string name="pref_hardware_forward_button_title">Botón Seguinte</string> + <string name="pref_hardware_forward_button_summary">Personalizar o comportamento do botón Seguinte</string> + <string name="pref_hardware_previous_button_title">Botón Anterior</string> + <string name="pref_hardware_previous_button_summary">Personalizar o comportamento do botón Anterior</string> + <string name="button_action_fast_forward">Avance rápido</string> + <string name="button_action_rewind">Rebobinar</string> + <string name="button_action_skip_episode">Saltar episodio</string> + <string name="button_action_restart_episode">Reiniciar episodio</string> <string name="pref_followQueue_sum">Saltar ao seguinte elemento na cola cando remata o episodio</string> <string name="pref_auto_delete_sum">Eliminar o episodio cando remata a súa reprodución</string> <string name="pref_auto_delete_title">Borrado automático</string> @@ -366,8 +385,11 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Desactivar</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Establecer intervalo</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Establecer hora do día</string> - <string name="pref_autoUpdateIntervallOrTime_every">cada %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">as %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Cada hora</item> + <item quantity="other">Cada %d horas</item> + </plurals> <string name="pref_followQueue_title">Reprodución continua</string> <string name="pref_pauseOnHeadsetDisconnect_title">Auriculares ou Bluetooth desconectados</string> <string name="pref_unpauseOnHeadsetReconnect_title">Reconexión de auriculares</string> @@ -401,7 +423,9 @@ <string name="pref_episode_cache_title">Caché de episodios</string> <string name="pref_episode_cache_summary">O número total de episodios descargados na caché do dispositivo. A descarga automática suspenderase se se alcanza este número.</string> <string name="pref_episode_cover_title">Utilizar Portada do episodio</string> - <string name="pref_episode_cover_summary">Utilizar a portada específica do episodio cando sexa posible. Se non o marcas, a app mostrará sempre a portada do podcast.</string> + <string name="pref_episode_cover_summary">Usar a capa específica do episodio nas listas cando estivese dispoñible. Sen marcar, a app sempre usará a imaxe de cuberta do podcast.</string> + <string name="pref_show_remain_time_title">Mostrar tempo restante</string> + <string name="pref_show_remain_time_summary">Se o seleccionas mostrará o tempo restante dos episodios. Se non, mostrará a duración total dos episodios.</string> <string name="pref_theme_title_use_system">Utilizar o decorado do sistema</string> <string name="pref_theme_title_light">Claro</string> <string name="pref_theme_title_dark">Oscuro</string> @@ -421,8 +445,6 @@ <string name="pref_gpodnet_full_sync_title">Forzar sincronización completa</string> <string name="pref_gpodnet_full_sync_sum">Sincronizar todas as subscricións e estados de episodios con gpodder.net</string> <string name="pref_gpodnet_login_status"><![CDATA[Conectada como <i>%1$s</i> co dispositivo <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Fallou a sincronización</string> - <string name="pref_gpodnet_notifications_sum">Esta preferencia non se aplica a fallos na autenticación.</string> <string name="pref_playback_speed_sum">Personaliza as velocidades dispoñibles para reprodución de velocidade variable</string> <string name="pref_feed_playback_speed_sum">A velocidade para reproducir o contido dos episodios deste podcast</string> <string name="pref_feed_skip">Salto automático</string> @@ -437,8 +459,6 @@ <string name="pref_fast_forward_sum">Personaliza o número de segundos a avanzar cando o se preme o botón de avance rápido</string> <string name="pref_rewind">Retroceso Salta tempo</string> <string name="pref_rewind_sum">Personaliza o número de segundos que se retrocede na reprodución cando se preme o botón retroceso</string> - <string name="pref_gpodnet_sethostname_title">Establecer servidor</string> - <string name="pref_gpodnet_sethostname_use_default_host">Utilizar servidor por omisión</string> <string name="pref_expandNotify_title">Alta prioridade nas notificacións</string> <string name="pref_expandNotify_sum">Isto expande as notificacións para mostrar os botóns de reprodución</string> <string name="pref_persistNotify_title">Controles persistentes de reprodución</string> @@ -449,10 +469,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Só podes selecionar un máximo de %1$d elementos.</string> <string name="pref_lockscreen_background_title">Establecer fondo da pantalla de bloqueo</string> <string name="pref_lockscreen_background_sum">Establecer o fondo de pantalla de bloqueo coa imaxe do episodio actual. Como consecuencia, esto tamén mostrará a imaxe en aplicacións de terceiros.</string> - <string name="pref_showDownloadReport_title">Fallou a descarga</string> - <string name="pref_showDownloadReport_sum">Si falla a descarga, xerar un informe que informe dos detalles do fallo.</string> - <string name="pref_showAutoDownloadReport_title">Descarga automática completada</string> - <string name="pref_showAutoDownloadReport_sum">Mostra unha notificación para os episodios descargados automáticamente.</string> <string name="pref_expand_notify_unsupport_toast">As versións de Android anteriores a 4.1 non teñen soporte para notificacións expandidas.</string> <string name="pref_enqueue_location_title">Situación na cola</string> <string name="pref_enqueue_location_sum">Engadir episodios a: %1$s</string> @@ -462,6 +478,7 @@ <string name="pref_smart_mark_as_played_disabled">Desactivado</string> <string name="pref_image_cache_size_title">Tamaño da caché de imaxes</string> <string name="pref_image_cache_size_sum">Tamaño da caché en disco para as imaxes.</string> + <string name="documentation_support">Documentación & Axuda</string> <string name="visit_user_forum">Foro de usuarias</string> <string name="bug_report_title">Informar de fallo</string> <string name="open_bug_tracker">Abrir seguimento de fallos</string> @@ -473,14 +490,14 @@ <string name="pref_current_value">Valor actual: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Establecer un proxy para a rede</string> - <string name="pref_faq">Preguntas Máis Frecuentes</string> <string name="pref_no_browser_found">Non se atopou un navegador web</string> <string name="pref_cast_title">Soporte Chromecast</string> <string name="pref_cast_message_play_flavor">Habilitar o soporte de reprodución remota nun dispositivo Cast (como o Chromecast, Altofalantes ou Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast precisa software propietario de terceiras partes que están desactivadas en esta versión de AntennaPod</string> <string name="pref_enqueue_downloaded_title">Foron descargados os elementos da cola</string> <string name="pref_enqueue_downloaded_summary">Engadir os episodios descargados a cola</string> - <string name="media_player_builtin">Reprodutor android nativo</string> + <string name="media_player_builtin">Reprodutor incluído de Android (relegado)</string> + <string name="media_player_sonic">Reprodutor Sonic Media (relegado)</string> <string name="media_player_exoplayer_recommended">ExoPlayer (recomendado)</string> <string name="media_player_switch_to_exoplayer">Cambiar a ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Cambiaches a ExoPlayer.</string> @@ -569,6 +586,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Establecer apagado automático</string> <string name="disable_sleeptimer_label">Desactivar o apagado automático</string> + <string name="extend_sleep_timer_label">+ %d min</string> <string name="sleep_timer_label">Apagado automático</string> <string name="time_dialog_invalid_input">Entrada incorrecta, o tempo ten que ser un número enteiro</string> <string name="shake_to_reset_label">Axita para restablecer</string> @@ -596,22 +614,22 @@ <string name="gpodnet_suggestions_header">SUXESTIÓNS</string> <string name="gpodnet_search_hint">Buscar en gpodder.net</string> <string name="gpodnetauth_login_title">Conexión</string> - <string name="gpodnetauth_login_descr">Benvida ao proceso de conexión a gpodder.net. Primeiro, escriba os seus datos de conexión:</string> <string name="gpodnetauth_login_butLabel">Conexión</string> - <string name="gpodnetauth_login_register">Se aínda non tes unha conta, podes crear unha aquí:\nhttps://gpodder.net/register/</string> + <string name="create_account">Crear conta</string> <string name="username_label">Nome de usuaria</string> <string name="password_label">Contrasinal</string> - <string name="gpodnetauth_device_title">Selección de dispositivo</string> + <string name="gpodnet_description">Gpodder.net é un servizo de sincronización de podcast de código aberto que é independente do proxecto AntennaPod.</string> + <string name="gpodnetauth_server_official">Servidor oficial gpodder.net</string> + <string name="gpodnetauth_server_custom">Servidor personalizado</string> + <string name="gpodnetauth_host">Servidor</string> + <string name="gpodnetauth_select_server">Elixe servidor</string> <string name="gpodnetauth_device_descr">Crear un novo dispositivo para usar coa túa conta gpodder.net ou escoller un existente:</string> - <string name="gpodnetauth_device_deviceID">ID de dispositivo:\u0020</string> - <string name="gpodnetauth_device_caption">Título</string> - <string name="gpodnetauth_device_butCreateNewDevice">Crear un novo dispositivo</string> - <string name="gpodnetauth_device_chooseExistingDevice">Escolle un novo dispositivo:</string> - <string name="gpodnetauth_device_errorEmpty">O ID do dispositivo non pode quedar baleiro</string> - <string name="gpodnetauth_device_errorAlreadyUsed">ID de dispositivo xa en uso</string> + <string name="gpodnetauth_device_name">Nome do dispositivo</string> + <string name="gpodnetauth_device_name_default">AntennaPod en %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">O título non pode estar baleiro</string> + <string name="gpodnetauth_existing_devices">Dispositivos existentes</string> + <string name="gpodnetauth_create_device">Crear dispositivo</string> <string name="gpodnetauth_device_butChoose">Escoller</string> - <string name="gpodnetauth_finish_title">Conexión correcta!</string> <string name="gpodnetauth_finish_descr">Parabéns! A túa conta gpodder.net está conectada ao dispositivo. AntennaPod poderá agora sincronizar automaticamente as túas subscricións no dispositivo na conta de gpodder.net</string> <string name="gpodnetauth_finish_butsyncnow">Iniciar a sincronización</string> <string name="gpodnetauth_finish_butgomainscreen">Ir a pantalla principal</string> @@ -639,7 +657,7 @@ <string name="folder_not_writable_error">non se pode escribir en \"%1$s\"</string> <string name="folder_not_empty_dialog_title">O cartafol non está baleiro</string> <string name="folder_not_empty_dialog_msg">O cartafol escollido non está baleiro. As descargas de medios e outros ficheiros situaranse directamente neste cartafol. Utilizalo igualmente?</string> - <string name="set_to_default_folder">Escolle o cartafol por omisión</string> + <string name="set_to_default_folder">Escolle o cartafol por defecto</string> <string name="pref_pausePlaybackForFocusLoss_sum">Pausar a reprodución en lugar de baixar o volume cando outra aplicación quere reproducir un son.</string> <string name="pref_pausePlaybackForFocusLoss_title">Pausa para interrupcións</string> <string name="pref_resumeAfterCall_sum">Retomar a reprodución despois de rematar a chamada telefónica</string> @@ -665,6 +683,7 @@ <string name="switch_pages">Cambiar de páxina</string> <string name="position">Posición: %1$s</string> <string name="apply_action">Aplicar acción</string> + <string name="play_chapter">Reproducir capítulo</string> <!--Feed information screen--> <string name="authentication_label">Autenticación</string> <string name="authentication_descr">Cambiar o seu nome de usuaria e contrasinal para este podcast e os seus episodios.</string> @@ -799,18 +818,22 @@ <string name="cast_failed_receiver_player_error">O reprodutor receptor atopou un fallo grave</string> <string name="cast_failed_media_error_skipping">Fallo na reprodución de medios. Saltando...</string> <!--Notification channels--> + <string name="notification_group_errors">Erros</string> + <string name="notification_group_news">Novas</string> <string name="notification_channel_user_action">Acción requerida</string> <string name="notification_channel_user_action_description">Mostrado si a súa acción é requerida, por exemplo si precisa introducir o contrasinal.</string> <string name="notification_channel_downloading">Descargando</string> <string name="notification_channel_downloading_description">Mostrado durante a descarga actual.</string> <string name="notification_channel_playing">Soando agora</string> <string name="notification_channel_playing_description">Permite controlar a reprodución. Esta é a notificación principal que verá mentras reproduce un podcast.</string> - <string name="notification_channel_error">Fallos</string> - <string name="notification_channel_error_description">Mostrar se algo fallou, por exemplo se a descarga ou a actualización da fonte fallaron.</string> - <string name="notification_channel_sync_error">Erros durante a sincronización</string> + <string name="notification_channel_download_error">Fallou a descarga</string> + <string name="notification_channel_download_error_description">Mostrado cando a descarga ou actualización da fonte fallou.</string> + <string name="notification_channel_sync_error">Fallou a sincronización</string> <string name="notification_channel_sync_error_description">Mostrado cando falla a sincronización con gpodder.</string> - <string name="notification_channel_auto_download">Descargas Automáticas</string> + <string name="notification_channel_auto_download">Completouse a descarga automática</string> <string name="notification_channel_episode_auto_download">Mostrado cando os episodios se descargaron automáticamente.</string> + <string name="notification_channel_new_episode">Novo episodio</string> + <string name="notification_channel_new_episode_description">Móstrase cando se atopa un novo episodio dun podcast, se están activas as notificacións</string> <!--Widget settings--> <string name="widget_settings">Axustes do Widget</string> <string name="widget_create_button">Crear widget</string> diff --git a/core/src/main/res/values-hu/strings.xml b/core/src/main/res/values-hu/strings.xml index 5f90546e3..3333c5ae3 100644 --- a/core/src/main/res/values-hu/strings.xml +++ b/core/src/main/res/values-hu/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statisztika</string> <string name="add_feed_label">Podcast hozzáadása</string> <string name="episodes_label">Epizódok</string> + <string name="queue_label">Lejátszási sor</string> <string name="all_episodes_short_label">Mind</string> <string name="new_episodes_label">Újak</string> <string name="favorite_episodes_label">Kedvencek</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Napló</string> <string name="subscriptions_label">Feliratkozások</string> <string name="subscriptions_list_label">Feliratkozások listája</string> - <string name="cancel_download_label">Letöltés\nmegszakítása</string> + <string name="cancel_download_label">Letöltés megszakítása</string> <string name="playback_history_label">Lejátszási napló</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net bejelentkezés</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Lejátszás</string> <string name="download_statistics_label">Letöltések</string> <string name="notification_pref_fragment">Értesítések</string> + <!--Google Assistant--> + <string name="app_action_not_found">A(z) „%1$s” nem található</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">A lejátszott epizódok összideje:</string> <string name="statistics_details_dialog">%1$d/%2$d epizód elindítva.\n\n%3$s/%4$s lejátszva.</string> @@ -79,7 +82,6 @@ <string name="description_label">Leírás</string> <string name="episodes_suffix">\u0020epizód</string> <string name="processing_label">Feldolgozás</string> - <string name="save_username_password_label">Felhasználónév és jelszó mentése</string> <string name="close_label">Bezárás</string> <string name="retry_label">Újra</string> <string name="auto_download_label">Hozzáadás az automatikus letöltésekhez</string> @@ -91,7 +93,6 @@ <string name="feed_volume_reduction_off">Ki</string> <string name="feed_volume_reduction_light">Enyhe</string> <string name="feed_volume_reduction_heavy">Erős</string> - <string name="parallel_downloads_suffix">\u0020párhuzamos letöltés</string> <string name="feed_auto_download_global">Globális alapértelmezett</string> <string name="feed_auto_download_always">Mindig</string> <string name="feed_auto_download_never">Soha</string> @@ -130,6 +131,7 @@ <string name="share_label_with_ellipses">Megosztás…</string> <string name="share_file_label">Fájl megosztása</string> <string name="share_website_url_label">Webcím</string> + <string name="share_feed_url_label">Podcast csatorna URL</string> <string name="feed_delete_confirmation_msg">Erősítse meg, hogy törli a(z) „%1$s” podcastot, és az ÖSSZES epizódját (a letöltött epizódokat is beleértve).</string> <string name="feed_delete_confirmation_local_msg">Erősítse meg, hogy törli a(z) „%1$s” podcastot. A helyi forrásmappában lévő fájlok nem törlődnek.</string> <string name="feed_remover_msg">Podcast eltávolítása</string> @@ -142,7 +144,6 @@ <string name="hide_not_queued_episodes_label">Nem sorbaállított</string> <string name="hide_has_media_label">Médiát tartalmaz</string> <string name="filtered_label">Szűrt</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} A legutóbbi frissítés sikertelen</string> <string name="open_podcast">Podcast megnyitása</string> <string name="please_wait_for_data">Várjon az adatok betöltésére</string> <!--actions on feeditems--> @@ -203,16 +204,12 @@ <string name="download_error_details">Részletek</string> <string name="download_error_details_message">%1$s \n\nFájl URL:\n%2$s</string> <string name="download_error_device_not_found">Tárolóeszköz nem található</string> - <string name="download_error_insufficient_space">Túl kevés tárhely</string> <string name="download_error_http_data_error">HTTP adathiba</string> <string name="download_error_error_unknown">Ismeretlen hiba</string> - <string name="download_error_parser_exception">Értelmező által dobott kivétel</string> <string name="download_error_unsupported_type">Nem támogatott csatornatípus</string> <string name="download_error_connection_error">Kapcsolódási hiba</string> - <string name="download_error_unknown_host">Ismeretlen kiszolgál</string> <string name="download_error_unauthorized">Hitelesítési hiba</string> <string name="download_error_file_type_type">Fájltípus-hiba</string> - <string name="download_error_forbidden">Megtiltva</string> <string name="download_canceled_msg">Letöltés megszakítva</string> <string name="download_canceled_autodownload_enabled_msg">Letöltés megszakítva\n<i>Automatikus letöltése</i> letiltva az elemnél</string> <string name="download_report_title">A letöltések hibákkal fejeződtek be</string> @@ -226,12 +223,7 @@ <item quantity="one">%d letöltés van hátra</item> <item quantity="other">%d letöltés van hátra</item> </plurals> - <string name="downloads_processing">Letöltések feldolgozása</string> <string name="download_notification_title">Podcast adatok letöltése</string> - <plurals name="download_report_content"> - <item quantity="one">%d letöltés sikeres, %d sikertelen</item> - <item quantity="other">%d letöltés sikeres, %d sikertelen</item> - </plurals> <string name="download_log_title_unknown">Ismeretlen cím</string> <string name="download_type_feed">Csatorna</string> <string name="download_type_media">Médiafájl</string> @@ -264,6 +256,7 @@ <string name="player_go_to_picture_in_picture">Kép a képben mód</string> <string name="unknown_media_key">AntennaPod – Ismeretlen médiabillentyű: %1$d</string> <string name="error_file_not_found">A fájl nem találhat</string> + <string name="no_media_label">Az elem nem tartalmaz médiafájlt</string> <!--Queue operations--> <string name="lock_queue">Lejátszási sor zárolása</string> <string name="unlock_queue">Lejátszási sor feloldása</string> @@ -292,6 +285,8 @@ <string name="no_playback_plugin_title">A bővítmény nincs telepítve</string> <string name="no_playback_plugin_or_sonic_msg">A változó lejátszási sebesség működéséhez azt javasoljuk, hogy engedélyezze a beépített Sonic médialejátszót.</string> <string name="enable_sonic">Sonic engedélyezése</string> + <string name="speed_presets">Előbeállítások</string> + <string name="preset_already_exists">Már mentve lett előbeállításként: %1$.2fx.</string> <!--Empty list labels--> <string name="no_items_header_label">Nincsenek epizódok a sorban</string> <string name="no_items_label">Adjon hozzá egy epizód azzal, hogy letölti, vagy nyomjon meg hosszan egy epizódot, és válassza a „Hozzáadás a sorhoz” lehetőséget.</string> @@ -318,7 +313,6 @@ <string name="storage_pref">Tároló</string> <string name="storage_sum">Epizódok automatikus törlése, importálása, exportálása</string> <string name="project_pref">Projekt</string> - <string name="queue_label">Lejátszási sor</string> <string name="synchronization_pref">Szinkronizálás</string> <string name="synchronization_sum">Szinkronizáció más eszközökkel a gpodder.net segítségével</string> <string name="automation">Automatizálás</string> @@ -329,19 +323,24 @@ <string name="external_elements">Külső elemek</string> <string name="interruptions">Megszakítások</string> <string name="playback_control">Lejátszásvezérlés </string> + <string name="reassign_hardware_buttons">Hardvergomb-társítások átrendezése</string> <string name="preference_search_hint">Keresés…</string> <string name="preference_search_no_results">Nincsenek találatok</string> <string name="preference_search_clear_history">Napló törlése</string> <string name="media_player">Médialejátszó</string> <string name="pref_episode_cleanup_title">Epizódok tisztítása</string> - <string name="pref_episode_cleanup_summary">Azon epizódok, melyek nincsenek a sorban és nem kedvencek, azok törölhetőek, ha az Automatikus letöltéshez helyre van szüksége az új epizódok miatt.</string> + <string name="pref_episode_cleanup_summary">Azon epizódok, melyek törölhetők, ha az Automatikus letöltésnek helyre van szüksége az új epizódok miatt.</string> <string name="pref_pauseOnDisconnect_sum">Lejátszás szüneteltetése fejhallgató vagy bluetooth leválasztásakor</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Lejátszás folytatása a fejhallgatók újracsatlakoztatásakor</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Lejátszás folytatása a bluetooth újracsatlakozásakor</string> - <string name="pref_hardwareForwardButtonSkips_title">Az előre gomb átugorja</string> - <string name="pref_hardwareForwardButtonSkips_sum">Az bluetooth kapcsolaton csatlakozó eszköz előre gombjának megnyomásakor előretekerés helyett a következő számra ugrik</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Az előző gomb újraindítja</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">A hardveres előző gomb megnyomásakor visszatekerés helyett újraindítja a jelenlegi epizódot</string> + <string name="pref_hardware_forward_button_title">Tovább gomb</string> + <string name="pref_hardware_forward_button_summary">A tovább gomb viselkedésének testreszabása</string> + <string name="pref_hardware_previous_button_title">Előző gomb</string> + <string name="pref_hardware_previous_button_summary">Az előző gomb viselkedésének testreszabása</string> + <string name="button_action_fast_forward">Gyors előretekerés</string> + <string name="button_action_rewind">Visszatekerés</string> + <string name="button_action_skip_episode">Epizód kihagyása</string> + <string name="button_action_restart_episode">Epizód újraindítása</string> <string name="pref_followQueue_sum">A lejátszás befejeztével ugrás a sor következő elemére</string> <string name="pref_auto_delete_sum">Az epizód törlése, ha a lejátszás véget ért</string> <string name="pref_auto_delete_title">Automatikus törlés</string> @@ -361,9 +360,13 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Letiltás</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Intervallum megadása</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Időpont megadása</string> - <string name="pref_autoUpdateIntervallOrTime_every">minden %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">ekkor: %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Óránként</item> + <item quantity="other">%d óránként</item> + </plurals> <string name="pref_followQueue_title">Folyamatos lejátszás</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Fejhallgató vagy Bluetooth leválasztása</string> <string name="pref_unpauseOnHeadsetReconnect_title">Fejhallgató újracsatlakoztatása</string> <string name="pref_unpauseOnBluetoothReconnect_title">Bluetooth újracsatlakoztatás</string> <string name="pref_stream_over_download_title">Adatátvitel előnyben részesítése</string> @@ -396,6 +399,8 @@ <string name="pref_episode_cache_summary">Az eszközön tárolt letöltött epizódok száma. Az automatikus letöltés felfüggesztésre kerül, ha eléri ezt a számot.</string> <string name="pref_episode_cover_title">Epizód borítókép használata</string> <string name="pref_episode_cover_summary">Epizódspecifikus borító használata, ha lehetséges. Ha nincs bekapcsolva, akkor az alkalmazás mindig a podcast borítóját fogja használni.</string> + <string name="pref_show_remain_time_title">Hátralévő idő megjelenítése</string> + <string name="pref_show_remain_time_summary">Ha be van jelölve, akkor megjeleníti az epizódból hátralévő időt. Ha nincs bejelölve, akkor az epizód hosszát jeleníti meg.</string> <string name="pref_theme_title_use_system">Rendszertéma használata</string> <string name="pref_theme_title_light">Világos</string> <string name="pref_theme_title_dark">Sötét</string> @@ -415,8 +420,6 @@ <string name="pref_gpodnet_full_sync_title">Teljes szinkronizáció kényszerítése</string> <string name="pref_gpodnet_full_sync_sum">Az összes feliratkozásának és epizódállapotainak szinkronizálása a gpodder.nettel.</string> <string name="pref_gpodnet_login_status"><![CDATA[Bejelentkezve mint <i>%1$s</i>, a(z) <i>%2$s</i> eszközzel]]></string> - <string name="pref_gpodnet_notifications_title">Szinkronizálás sikertelen</string> - <string name="pref_gpodnet_notifications_sum">Ez a beállítás a hitelesítési hibákra nem érvényes.</string> <string name="pref_playback_speed_sum">A változó sebességű lejátszáshoz elérhető sebességek testreszabása</string> <string name="pref_feed_playback_speed_sum">A podcast epizódjainak indításakor használandó lejátszási sebesség</string> <string name="pref_feed_skip">Automatikus kihagyás</string> @@ -431,20 +434,16 @@ <string name="pref_fast_forward_sum">Szabja testre, hogy hány másodperccel ugorjon előre az előretekerés gomb megnyomásakor</string> <string name="pref_rewind">Visszatekerés mértéke</string> <string name="pref_rewind_sum">Szabja testre, hogy hány másodperccel ugorjon vissza a visszatekerés gomb megnyomásakor</string> - <string name="pref_gpodnet_sethostname_title">Gépnév megadása</string> - <string name="pref_gpodnet_sethostname_use_default_host">Alapértelmezett gépnév használata</string> <string name="pref_expandNotify_title">Magas értesítési prioritás</string> <string name="pref_expandNotify_sum">Ez általában kibővíti az értesítést, hogy megjelenítse a lejátszási gombokat.</string> <string name="pref_persistNotify_title">Állandó lejátszásvezérlők</string> <string name="pref_persistNotify_sum">Az értesítés és a zárképernyőn megjelenő vezérlők megtartása a lejátszás szüneteltetésekor.</string> + <string name="pref_compact_notification_buttons_title">Kompakt értesítési gombok beállítása</string> + <string name="pref_compact_notification_buttons_sum">A lejátszási gombok módosítása az értesítés összecsukása esetén. A lejátszás/szüneteltetés gombok mindig szerepelnek.</string> <string name="pref_compact_notification_buttons_dialog_title">Válasszon legfeljebb %1$d elemet</string> <string name="pref_compact_notification_buttons_dialog_error">Legfeljebb %1$d elemet választhat.</string> <string name="pref_lockscreen_background_title">Képernyőzár háttérkép beállítása</string> <string name="pref_lockscreen_background_sum">A képernyőzár háttérképének beállítása a jelenlegi epizód képére. Mellékhatásként, ez a harmadik féltől származó alkalmazásokban is megjeleníti a képet.</string> - <string name="pref_showDownloadReport_title">Letöltés sikertelen</string> - <string name="pref_showDownloadReport_sum">Ha a letöltések sikertelenek, előállít egy jelentést, amely részletezi a hibát</string> - <string name="pref_showAutoDownloadReport_title">Elkészült az automatikus letöltés</string> - <string name="pref_showAutoDownloadReport_sum">Értesítés megjelenítése az automatikusan letöltött epizódokhoz.</string> <string name="pref_expand_notify_unsupport_toast">A 4.1 előtti Android verziók nem támogatják a bővített értesítéseket.</string> <string name="pref_enqueue_location_title">Hely sorbaállítása</string> <string name="pref_enqueue_location_sum">Epizódok hozzáadása ehhez: %1$s</string> @@ -454,6 +453,7 @@ <string name="pref_smart_mark_as_played_disabled">Letiltva</string> <string name="pref_image_cache_size_title">Kép gyorsítótár mérete</string> <string name="pref_image_cache_size_sum">Kép gyorsítótár mérete a lemezen.</string> + <string name="documentation_support">Dokumentációs és támogatása</string> <string name="visit_user_forum">Felhasználói fórum</string> <string name="bug_report_title">Hibajelentés</string> <string name="open_bug_tracker">Hibakövető megnyitása</string> @@ -465,14 +465,12 @@ <string name="pref_current_value">Jelenlegi érték: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Hálózati proxy beállítása</string> - <string name="pref_faq">Gyakran ismételt kérdések</string> <string name="pref_no_browser_found">Nem található webböngésző.</string> <string name="pref_cast_title">Chromecast támogatás</string> <string name="pref_cast_message_play_flavor">A távoli médialejátszás engedélyezése a Cast eszközökön (mint a Chromecast, hangfalak vagy Android TV)</string> <string name="pref_cast_message_free_flavor">A Chromecast harmadik féltől származó zárt programkönyvtárakat igényel, amelyek le vannak tiltva az AntennaPod jelen verziójában.</string> <string name="pref_enqueue_downloaded_title">Letöltött elemek sorbaállítása</string> <string name="pref_enqueue_downloaded_summary">Letöltött epizódok sorhoz adása</string> - <string name="media_player_builtin">Beépített androidos lejátszó</string> <string name="media_player_exoplayer_recommended">ExoPlayer (javasolt)</string> <string name="media_player_switch_to_exoplayer">Váltás az ExoPlayerre</string> <string name="media_player_switched_to_exoplayer">Átváltva az ExoPlayerre.</string> @@ -580,22 +578,12 @@ <string name="gpodnet_suggestions_header">JAVASLATOK</string> <string name="gpodnet_search_hint">Keresés a gpodder.neten</string> <string name="gpodnetauth_login_title">Bejelentkezés</string> - <string name="gpodnetauth_login_descr">Üdvözli a gpodder.net bejelentkezési folyamat. Először írja be a bejelentkezési információit:</string> <string name="gpodnetauth_login_butLabel">Bejelentkezés</string> - <string name="gpodnetauth_login_register">Ha még nincs fiókja, itt létrehozhat egyet:\nhttps://gpodder.net/register/</string> <string name="username_label">Felhasználónév</string> <string name="password_label">Jelszó</string> - <string name="gpodnetauth_device_title">Eszköz kiválasztása</string> <string name="gpodnetauth_device_descr">Hozzon létre egy új eszközt a gpodder.net fiókjához, vagy válasszon egy meglévőt:</string> - <string name="gpodnetauth_device_deviceID">Eszközazonosító:\u0020</string> - <string name="gpodnetauth_device_caption">Felirat</string> - <string name="gpodnetauth_device_butCreateNewDevice">Új eszköz létrehozása</string> - <string name="gpodnetauth_device_chooseExistingDevice">Létező eszköz kiválasztása:</string> - <string name="gpodnetauth_device_errorEmpty">Az eszközazonosító nem lehet üres</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Az eszközazonosító már használatban van</string> <string name="gpodnetauth_device_caption_errorEmpty">A felirat nem lehet üres</string> <string name="gpodnetauth_device_butChoose">Kiválasztás</string> - <string name="gpodnetauth_finish_title">Bejelentkezés sikeres!</string> <string name="gpodnetauth_finish_descr">Gratulálunk! A gpodder.net fiókja most már össze van kapcsolva az eszközével. Az AntennaPod automatikusan szinkronizálja az eszközén lévő feliratkozásait a gpodder.net fiókjával.</string> <string name="gpodnetauth_finish_butsyncnow">Szinkronizálás indítása most</string> <string name="gpodnetauth_finish_butgomainscreen">Ugrás a főképernyőre</string> @@ -769,9 +757,6 @@ <string name="notification_channel_downloading_description">Letöltés közben jelenik meg</string> <string name="notification_channel_playing">Most játszott</string> <string name="notification_channel_playing_description">Lehetővé teszi a lejátszás vezérlését. Ez a fő értesítés, amit a podcast lejátszásakor lát.</string> - <string name="notification_channel_error">Hibák</string> - <string name="notification_channel_sync_error">Szinkronizálási hibák</string> - <string name="notification_channel_auto_download">Automatikus letöltések</string> <string name="notification_channel_episode_auto_download">Akkor jelenik meg, ha az epizódok automatikusan letöltésre kerültek.</string> <!--Widget settings--> <string name="widget_settings">Widget beállítások</string> diff --git a/core/src/main/res/values-it/strings.xml b/core/src/main/res/values-it/strings.xml index 65ad90f28..730623a0d 100644 --- a/core/src/main/res/values-it/strings.xml +++ b/core/src/main/res/values-it/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statistiche</string> <string name="add_feed_label">Aggiungi podcast</string> <string name="episodes_label">Episodi</string> + <string name="queue_label">Coda</string> <string name="all_episodes_short_label">Tutti</string> <string name="new_episodes_label">Novità</string> <string name="favorite_episodes_label">Preferiti</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Registro</string> <string name="subscriptions_label">Iscrizioni</string> <string name="subscriptions_list_label">Elenco iscrizioni</string> - <string name="cancel_download_label">Annulla\nil download</string> + <string name="cancel_download_label">Annulla download</string> <string name="playback_history_label">Cronologia riproduzioni</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">Accesso a gpodder.net</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Riproduzione</string> <string name="download_statistics_label">Download</string> <string name="notification_pref_fragment">Notifiche</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" non trovato</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Tempo totale episodi riprodotti:</string> <string name="statistics_details_dialog">%1$d di %2$d episodi iniziati.\n\nRiprodotti %3$s di %4$s.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Nessuno</string> <!--Bug report activity--> <string name="log_file_share_exception">Nessuna applicazione compatibile trovata</string> + <string name="export_logs_menu_title">Esporta log dettagliati</string> + <string name="confirm_export_log_dialog_message">I log dettagliati possono contenere informazioni sensibili come la lista delle iscrizioni</string> <!--Webview actions--> <string name="open_in_browser_label">Apri nel browser</string> <string name="copy_url_label">Copia URL</string> @@ -81,7 +86,6 @@ <string name="description_label">Descrizione</string> <string name="episodes_suffix">\u0020episodi</string> <string name="processing_label">Elaborazione in corso</string> - <string name="save_username_password_label">Salva nome utente e password</string> <string name="close_label">Chiudi</string> <string name="retry_label">Riprova</string> <string name="auto_download_label">Includi nei download automatici</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">Spento</string> <string name="feed_volume_reduction_light">Leggero</string> <string name="feed_volume_reduction_heavy">Deciso</string> - <string name="parallel_downloads_suffix">\u0020download paralleli</string> + <string name="parallel_downloads">%1$d download paralleli</string> <string name="feed_auto_download_global">Predefinita globale</string> <string name="feed_auto_download_always">Sempre</string> <string name="feed_auto_download_never">Mai</string> <string name="send_label">Invia...</string> <string name="episode_cleanup_never">Mai</string> + <string name="episode_cleanup_except_favorite_removal">Quando non Preferito</string> <string name="episode_cleanup_queue_removal">Quando non è in coda</string> <string name="episode_cleanup_after_listening">Dopo il completamento</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -113,7 +118,22 @@ <item quantity="one">%d selezionato</item> <item quantity="other">%d selezionati</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d episodio</item> + <item quantity="other">%d episodi</item> + </plurals> <string name="loading_more">Caricamento successivi...</string> + <string name="episode_notification">Notifiche episodi</string> + <string name="episode_notification_summary">Mostra una notifica quando viene pubblicato un episodio.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s ha un nuovo episodio</item> + <item quantity="other">%2$s ha %1$d nuovi episodi</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Nuovo episodio</item> + <item quantity="other">Nuovi episodi</item> + </plurals> + <string name="new_episode_notification_group_text">Le tue sottoscrizioni hanno nuovi episodi.</string> <!--Actions on feeds--> <string name="mark_all_read_label">Segna tutti come riprodotti</string> <string name="mark_all_read_msg">Segnati tutti gli episodi come riprodotti</string> @@ -145,7 +165,6 @@ <string name="hide_not_queued_episodes_label">Non in coda</string> <string name="hide_has_media_label">Con media</string> <string name="filtered_label">Filtrati</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Ultimo aggiornamento fallito</string> <string name="open_podcast">Apri podcast</string> <string name="please_wait_for_data">Attendi il caricamento dei dati</string> <!--actions on feeditems--> @@ -160,6 +179,10 @@ <string name="delete_label">Elimina</string> <string name="delete_failed">Impossibile eliminare il file. Prova a riavviare il dispositivo.</string> <string name="delete_episode_label">Elimina episodio</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d episodio selezionato, %d download eliminato.</item> + <item quantity="other">%d episodi selezionati, %d download eliminato(i).</item> + </plurals> <string name="remove_new_flag_label">Rimuovi flag \"nuovo\"</string> <string name="removed_new_flag_label">Flag \"nuovo\" rimosso</string> <string name="mark_read_label">Segna come riprodotto</string> @@ -206,16 +229,12 @@ <string name="download_error_details">Dettagli</string> <string name="download_error_details_message">%1$s \n\nURL file:\n%2$s</string> <string name="download_error_device_not_found">Spazio di archiviazione non trovato</string> - <string name="download_error_insufficient_space">Spazio insufficiente</string> <string name="download_error_http_data_error">Errore dei dati HTTP</string> <string name="download_error_error_unknown">Errore sconosciuto</string> - <string name="download_error_parser_exception">Eccezione del decodificatore</string> <string name="download_error_unsupported_type">Tipo di feed non supportato</string> <string name="download_error_connection_error">Errore di connessione</string> - <string name="download_error_unknown_host">Host sconosciuto</string> <string name="download_error_unauthorized">Errore di autenticazione</string> <string name="download_error_file_type_type">Errore del tipo di file</string> - <string name="download_error_forbidden">Proibito</string> <string name="download_canceled_msg">Download annullato</string> <string name="download_canceled_autodownload_enabled_msg">Download annullato\n<i>Download automatico</i> disabilitato per questo elemento</string> <string name="download_report_title">Download completato con un errore (o errori)</string> @@ -229,12 +248,7 @@ <item quantity="one">%d download rimanente</item> <item quantity="other">%d download rimanenti</item> </plurals> - <string name="downloads_processing">Elaborazione dei download in corso</string> <string name="download_notification_title">Scaricamento podcast in corso</string> - <plurals name="download_report_content"> - <item quantity="one">%d scaricamento completato, %d non riuscito.</item> - <item quantity="other">%d scaricamenti completati, %d non riusciti.</item> - </plurals> <string name="download_log_title_unknown">Titolo sconosciuto</string> <string name="download_type_feed">Feed</string> <string name="download_type_media">File multimediali</string> @@ -267,6 +281,7 @@ <string name="player_go_to_picture_in_picture">Modalità picture-in-picture</string> <string name="unknown_media_key">AntennaPod - Chiave dell\'elemento multimediale sconosciuta: %1$d</string> <string name="error_file_not_found">File non trovato</string> + <string name="no_media_label">L\'oggetto non contiene un file multimediale</string> <!--Queue operations--> <string name="lock_queue">Blocca la coda</string> <string name="unlock_queue">Sblocca la coda</string> @@ -323,7 +338,6 @@ <string name="storage_pref">Memoria</string> <string name="storage_sum">Eliminazione episodi, importazione, esportazione</string> <string name="project_pref">Progetto</string> - <string name="queue_label">Coda</string> <string name="synchronization_pref">Sincronizzazione</string> <string name="synchronization_sum">Sincronizza con altri dispositivi tramite gpodder.net</string> <string name="automation">Automazione</string> @@ -334,19 +348,24 @@ <string name="external_elements">Elementi esterni</string> <string name="interruptions">Interruzioni</string> <string name="playback_control">Controllo riproduzione</string> + <string name="reassign_hardware_buttons">Riassegna pulsanti hardware</string> <string name="preference_search_hint">Cerca...</string> <string name="preference_search_no_results">Nessun risultato</string> <string name="preference_search_clear_history">Svuota cronologia</string> <string name="media_player">Riproduttore multimediale</string> <string name="pref_episode_cleanup_title">Pulizia episodi</string> - <string name="pref_episode_cleanup_summary">Gli episodi non in coda e che non sono tra i preferiti potrebbero essere rimossi se i Download automatici richiedono altro spazio.</string> + <string name="pref_episode_cleanup_summary">Episodi cancellabili se il download automatico richiede altro spazio per nuovi episodi</string> <string name="pref_pauseOnDisconnect_sum">Sospende la riproduzione quando le cuffie o il bluetooth vengono disconnessi</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Riprendi la riproduzione quando vengono riconnesse le cuffie</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Riprende la riproduzione quando il Bluetooth si riconnette</string> - <string name="pref_hardwareForwardButtonSkips_title">Il tasto Avanti salta la traccia</string> - <string name="pref_hardwareForwardButtonSkips_sum">Premendo il tasto Avanti sul dispositivo Bluetooth connesso, passa all\'episodio successivo invece di andare avanti veloce</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Il tasto Indietro riavvia la traccia</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Premendo il tasto fisico Indietro, viene riavviata la traccia invece riavvolgere alcuni secondi</string> + <string name="pref_hardware_forward_button_title">Tasto Avanti</string> + <string name="pref_hardware_forward_button_summary">Personalizza l\'azione del tasto Avanti</string> + <string name="pref_hardware_previous_button_title">Tasto Indietro</string> + <string name="pref_hardware_previous_button_summary">Personalizza l\'azione del tasto Indietro</string> + <string name="button_action_fast_forward">Avanti veloce</string> + <string name="button_action_rewind">Riavvolgi</string> + <string name="button_action_skip_episode">Salta episodio</string> + <string name="button_action_restart_episode">Riavvia episodio</string> <string name="pref_followQueue_sum">Passa al successivo episodio della coda quando viene completata la riproduzione</string> <string name="pref_auto_delete_sum">Elimina l\'episodio quando viene completata la riproduzione</string> <string name="pref_auto_delete_title">Elimina automaticamente</string> @@ -366,8 +385,11 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Disabilita</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Imposta Intervallo</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Imposta orario</string> - <string name="pref_autoUpdateIntervallOrTime_every">ogni %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">alle %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Ogni ora</item> + <item quantity="other">Ogni %d ore</item> + </plurals> <string name="pref_followQueue_title">Riproduzione continua</string> <string name="pref_pauseOnHeadsetDisconnect_title">Disconnessione cuffie o Bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_title">Riconnessione cuffie</string> @@ -401,7 +423,9 @@ <string name="pref_episode_cache_title">Cache degli episodi</string> <string name="pref_episode_cache_summary">Numero di episodi scaricati memorizzabili sul dispositivo. I download automatici vengono interrotti se si raggiunge questo valore.</string> <string name="pref_episode_cover_title">Usa immagine episodio</string> - <string name="pref_episode_cover_summary">Visualizza l\'immagine dell\'episodio se disponibile. Se disattivata, verrà usata sempre l\'immagine del podcast.</string> + <string name="pref_episode_cover_summary">Usa l\'immagine dell\'episodio, se disponibile. Se non selezionato, l\'app userà sempre l\'immagine di copertina del podcast.</string> + <string name="pref_show_remain_time_title">Mostra tempo residuo</string> + <string name="pref_show_remain_time_summary">Mostra il tempo rimanente degli episodi. Se non selezionato, mostra la durata totale degli episodi.</string> <string name="pref_theme_title_use_system">Usa tema di sistema</string> <string name="pref_theme_title_light">Chiaro</string> <string name="pref_theme_title_dark">Scuro</string> @@ -421,8 +445,6 @@ <string name="pref_gpodnet_full_sync_title">Forza sincronizzazione completa</string> <string name="pref_gpodnet_full_sync_sum">Sincronizza le iscrizioni e lo stato di tutti gli episodi con gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Accesso come <i>%1$s</i> con il dispositivo <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Sincronizzazione fallita</string> - <string name="pref_gpodnet_notifications_sum">Non si applica agli errori di autenticazione.</string> <string name="pref_playback_speed_sum">Personalizzare le velocità disponibili per le varie velocità di riproduzione.</string> <string name="pref_feed_playback_speed_sum">Velocità da usare per la riproduzione degli episodi di questo podcast</string> <string name="pref_feed_skip">Salta automaticamente</string> @@ -437,8 +459,6 @@ <string name="pref_fast_forward_sum">Personalizza il numero di secondi da saltare in avanti quando si preme il tasto Avanti veloce</string> <string name="pref_rewind">Tempo di salto indietro</string> <string name="pref_rewind_sum">Personalizza il numero di secondi da saltare indietro quando si preme il tasto Riavvolgi</string> - <string name="pref_gpodnet_sethostname_title">Imposta l\'hostname</string> - <string name="pref_gpodnet_sethostname_use_default_host">Usa l\'host di default</string> <string name="pref_expandNotify_title">Priorità notifiche superiori</string> <string name="pref_expandNotify_sum">Di solito espande la notifica per mostrare i tasti di riproduzione.</string> <string name="pref_persistNotify_title">Controlli di riproduzione persistenti</string> @@ -449,10 +469,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Puoi selezionare al massimo %1$d voci.</string> <string name="pref_lockscreen_background_title">Cambia sfondo della schermata di blocco</string> <string name="pref_lockscreen_background_sum">Sostituisce l\'immagine della schermata di blocco con quella dell\'episodio in riproduzione. Mostrerà l\'immagine anche in app di terze parti.</string> - <string name="pref_showDownloadReport_title">Download fallito</string> - <string name="pref_showDownloadReport_sum">Se il download fallisce, genera un report che mostra i dettagli dell\'errore.</string> - <string name="pref_showAutoDownloadReport_title">Download automatico completato</string> - <string name="pref_showAutoDownloadReport_sum">Mostra una notifica per gli episodi scaricati automaticamente.</string> <string name="pref_expand_notify_unsupport_toast">Le versioni Android precedenti alla 4.1 non supportano le notifiche estese.</string> <string name="pref_enqueue_location_title">Posizione in coda</string> <string name="pref_enqueue_location_sum">Posizione nuovi episodi: %1$s</string> @@ -462,6 +478,7 @@ <string name="pref_smart_mark_as_played_disabled">Disabilitato</string> <string name="pref_image_cache_size_title">Dimensione cache delle immagini</string> <string name="pref_image_cache_size_sum">Spazio su disco usato per la cache delle immagini.</string> + <string name="documentation_support">Documentazione & supporto</string> <string name="visit_user_forum">Forum utenti</string> <string name="bug_report_title">Segnala un problema</string> <string name="open_bug_tracker">Apri il bug tracker</string> @@ -473,14 +490,14 @@ <string name="pref_current_value">Impostazione attuale: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Imposta proxy di rete</string> - <string name="pref_faq">Domande frequenti - FAQ</string> <string name="pref_no_browser_found">Nessun browser web trovato.</string> <string name="pref_cast_title">Supporto a Chromecast</string> <string name="pref_cast_message_play_flavor">Abilita il supporto per la riproduzione multimediale remota su dispositivi Cast (Chromecast, casse esterne o Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast richiede librerie proprietarie di terze parti che sono disabilitate in questa versione di AntennaPod</string> <string name="pref_enqueue_downloaded_title">Aggiungi i download alla coda</string> <string name="pref_enqueue_downloaded_summary">Aggiunge gli episodi alla coda quando vengono scaricati</string> - <string name="media_player_builtin">Player Android integrato</string> + <string name="media_player_builtin">Player Android integrato (obsoleto)</string> + <string name="media_player_sonic">Sonic Media Player (obsoleto)</string> <string name="media_player_exoplayer_recommended">ExoPlayer (consigliato)</string> <string name="media_player_switch_to_exoplayer">Passa ad ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Passaggio ad ExoPlayer eseguito.</string> @@ -569,6 +586,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Imposta timer</string> <string name="disable_sleeptimer_label">Disabilita il timer di spegnimento</string> + <string name="extend_sleep_timer_label">+%d min</string> <string name="sleep_timer_label">Timer di spegnimento</string> <string name="time_dialog_invalid_input">Input non valido, il campo deve essere un numero intero.</string> <string name="shake_to_reset_label">Scuoti per resettare</string> @@ -596,22 +614,22 @@ <string name="gpodnet_suggestions_header">SUGGERIMENTI</string> <string name="gpodnet_search_hint">Cerca su gpodder.net</string> <string name="gpodnetauth_login_title">Login</string> - <string name="gpodnetauth_login_descr">Benvenuto alla procedura di login di gpodder.net. Come prima cosa, inserisci le credenziali:</string> <string name="gpodnetauth_login_butLabel">Login</string> - <string name="gpodnetauth_login_register">Se non hai ancora un account, puoi crearne uno qui:\nhttps://gpodder.net/register/</string> + <string name="create_account">Crea account</string> <string name="username_label">Username</string> <string name="password_label">Password</string> - <string name="gpodnetauth_device_title">Scelta del dispositivo</string> + <string name="gpodnet_description">Gpodder.net è un servizio open source di sincronizzazione dei podcast indipendente dal progetto AntennaPod.</string> + <string name="gpodnetauth_server_official">Server gpodder.net ufficiale</string> + <string name="gpodnetauth_server_custom">Server alternativo</string> + <string name="gpodnetauth_host">Hostname</string> + <string name="gpodnetauth_select_server">Seleziona server</string> <string name="gpodnetauth_device_descr">Crea un nuovo dispositivo per utilizzare il tuo account gpodder.net o scegline uno esistente:</string> - <string name="gpodnetauth_device_deviceID">ID del dispositivo:\u0020</string> - <string name="gpodnetauth_device_caption">Caption</string> - <string name="gpodnetauth_device_butCreateNewDevice">Crea un nuovo dispositivo</string> - <string name="gpodnetauth_device_chooseExistingDevice">Scegli un dispositivo esistente:</string> - <string name="gpodnetauth_device_errorEmpty">L\'ID del dispositivo non può essere vuoto</string> - <string name="gpodnetauth_device_errorAlreadyUsed">ID del dispositivo già in uso</string> + <string name="gpodnetauth_device_name">Nome dispositivo</string> + <string name="gpodnetauth_device_name_default">AntennaPod su %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">La didascalia non può essere vuota</string> + <string name="gpodnetauth_existing_devices">Dispositivi esistenti</string> + <string name="gpodnetauth_create_device">Crea dispositivo</string> <string name="gpodnetauth_device_butChoose">Scegli</string> - <string name="gpodnetauth_finish_title">Login effettuato!</string> <string name="gpodnetauth_finish_descr">Congraturazioni! Il tuo account gpodder.net è stato collegato con il dispositivo. Ora AntennaPod sincronizzerà automaticamente le sottoscrizioni sul dispositivo con il tuo account gpodder.net.</string> <string name="gpodnetauth_finish_butsyncnow">Avvia la sincronizzazione</string> <string name="gpodnetauth_finish_butgomainscreen">Vai alla schermata principale</string> @@ -665,6 +683,7 @@ <string name="switch_pages">Cambia schermata</string> <string name="position">Posizione: %1$s</string> <string name="apply_action">Applica la scelta</string> + <string name="play_chapter">Riproduci capitolo</string> <!--Feed information screen--> <string name="authentication_label">Autenticazione</string> <string name="authentication_descr">Cambia il nome utente e la password per questo podcast e i suoi episodi.</string> @@ -799,18 +818,22 @@ <string name="cast_failed_receiver_player_error">Il dispositivo ricevente ha restituito un errore grave</string> <string name="cast_failed_media_error_skipping">Errore nella riproduzione. Salto...</string> <!--Notification channels--> + <string name="notification_group_errors">Errori</string> + <string name="notification_group_news">Novità</string> <string name="notification_channel_user_action">Azione richesta</string> <string name="notification_channel_user_action_description">Visualizzato se è richiesto un intervento, ad esempio se è necessario inserire la password.</string> <string name="notification_channel_downloading">Scaricamento</string> <string name="notification_channel_downloading_description">Visualizzato mentre il download è in corso</string> <string name="notification_channel_playing">In riproduzione</string> <string name="notification_channel_playing_description">Permette di controllare la riproduzione. E\' la principale notifica visualizzata quando un podcast è in riproduzione.</string> - <string name="notification_channel_error">Errori</string> - <string name="notification_channel_error_description">Viene mostrato se qualcosa fallisce, ad esempio il download o l\'aggiornamento del feed.</string> - <string name="notification_channel_sync_error">Errori di sincronizzazione</string> + <string name="notification_channel_download_error">Download fallito</string> + <string name="notification_channel_download_error_description">Mostrato quando fallisce un download o aggiornamento del feed.</string> + <string name="notification_channel_sync_error">Sincronizzazione fallita</string> <string name="notification_channel_sync_error_description">Mostrati quando la sincronizzazione con gpodder fallisce.</string> - <string name="notification_channel_auto_download">Download automatici</string> + <string name="notification_channel_auto_download">Download automatico completato</string> <string name="notification_channel_episode_auto_download">Viene mostrato quando un episodio è stato scaricato automaticamente.</string> + <string name="notification_channel_new_episode">Nuovo episodio</string> + <string name="notification_channel_new_episode_description">Mostrato quando viene trovato un nuovo episodio di un podcast, se le notifiche sono attive.</string> <!--Widget settings--> <string name="widget_settings">Impostazioni widget</string> <string name="widget_create_button">Crea widget</string> diff --git a/core/src/main/res/values-iw/strings.xml b/core/src/main/res/values-iw/strings.xml index f9016ec3f..b1857dabf 100644 --- a/core/src/main/res/values-iw/strings.xml +++ b/core/src/main/res/values-iw/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">סטטיסטיקה</string> <string name="add_feed_label">הוספת פודקאסט</string> <string name="episodes_label">פרקים</string> + <string name="queue_label">תור</string> <string name="all_episodes_short_label">הכול</string> <string name="new_episodes_label">חדש</string> <string name="favorite_episodes_label">מועדפים</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">יומן</string> <string name="subscriptions_label">פודקאסטים</string> <string name="subscriptions_list_label">רשימת פודקאסטים</string> - <string name="cancel_download_label">ביטול\nהורדה</string> + <string name="cancel_download_label">ביטול הורדה</string> <string name="playback_history_label">היסטוריית ניגון</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">כניסה אל gpodder.net</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">נגינה</string> <string name="download_statistics_label">הורדות</string> <string name="notification_pref_fragment">התראות</string> + <!--Google Assistant--> + <string name="app_action_not_found">„%1$s” לא נמצא</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">זמן הנגינה הכולל של הפרקים:</string> <string name="statistics_details_dialog">%1$d מתוך %2$d פרקים החלו.\n\nנוגנו %3$s מתוך %4$s.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">ללא</string> <!--Bug report activity--> <string name="log_file_share_exception">לא נמצאו יישומונים תואמים</string> + <string name="export_logs_menu_title">ייצוא יומנים מפורטים</string> + <string name="confirm_export_log_dialog_message">יומנים מפורטים עשויים להכיל מידע רגיש כגון רשימת המינויים שלך</string> <!--Webview actions--> <string name="open_in_browser_label">פתיחה בדפדפן</string> <string name="copy_url_label">העתקת כתובת</string> @@ -81,7 +86,6 @@ <string name="description_label">תיאור</string> <string name="episodes_suffix">\u0020פרקים</string> <string name="processing_label">מתבצע עיבוד</string> - <string name="save_username_password_label">שמירת שם משתמש וססמה</string> <string name="close_label">סגירה</string> <string name="retry_label">לנסות שוב</string> <string name="auto_download_label">לכלול בהורדות אוטומטיות</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">כבוי</string> <string name="feed_volume_reduction_light">טיפה</string> <string name="feed_volume_reduction_heavy">מאוד</string> - <string name="parallel_downloads_suffix">\u0020הורדות במקביל</string> + <string name="parallel_downloads">%1$d הורדות במקביל</string> <string name="feed_auto_download_global">בררת מחדל גלובלית</string> <string name="feed_auto_download_always">תמיד</string> <string name="feed_auto_download_never">אף פעם</string> <string name="send_label">שליחה…</string> <string name="episode_cleanup_never">אף פעם</string> + <string name="episode_cleanup_except_favorite_removal">כאשר לא במועדפים</string> <string name="episode_cleanup_queue_removal">כאשר לא בתור</string> <string name="episode_cleanup_after_listening">אחרי סיום</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -119,7 +124,28 @@ <item quantity="many">נבחרו %d</item> <item quantity="other">נבחרו %d</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">פרק אחד</item> + <item quantity="two">%d פרקים</item> + <item quantity="many">%d פרקים</item> + <item quantity="other">%d פרקים</item> + </plurals> <string name="loading_more">נטענים עוד…</string> + <string name="episode_notification">התראות פרקים</string> + <string name="episode_notification_summary">להציג התראה כאשר יוצא פרק חדש.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">אצל %2$s יש פרק אחד חדש</item> + <item quantity="two">אצל %2$s יש %1$d פרקים חדשים</item> + <item quantity="many">אצל %2$s יש %1$d פרקים חדשים</item> + <item quantity="other">אצל %2$s יש %1$d פרקים חדשים</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">פרק חדש</item> + <item quantity="two">פרקים חדשים</item> + <item quantity="many">פרקים חדשים</item> + <item quantity="other">פרקים חדשים</item> + </plurals> + <string name="new_episode_notification_group_text">למינוי שלך יש פרקים חדשים.</string> <!--Actions on feeds--> <string name="mark_all_read_label">לסמן הכול כנוגנו</string> <string name="mark_all_read_msg">לסמן את כל הפרקים כנוגנו</string> @@ -151,7 +177,6 @@ <string name="hide_not_queued_episodes_label">לא בתור</string> <string name="hide_has_media_label">יש מדיה</string> <string name="filtered_label">מסונן</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} הרענון האחרון נכשל</string> <string name="open_podcast">פתיחת פודקאסט</string> <string name="please_wait_for_data">נא להמתין לסיום טעינת הנתונים</string> <!--actions on feeditems--> @@ -168,6 +193,12 @@ <string name="delete_label">מחיקה</string> <string name="delete_failed">לא ניתן למחוק קובץ. הפעלת המכשיר מחדש עשויה לסייע.</string> <string name="delete_episode_label">מחיקת פרק</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">פרק אחד נבחר, הורדה אחת נמחקה.</item> + <item quantity="two">%d פרקים נבחרו, %d הורדה/ות נמחקה/ו.</item> + <item quantity="many">%d פרקים נבחרו, %d הורדה/ות נמחקה/ו.</item> + <item quantity="other">%d פרקים נבחרו, %d הורדה/ות נמחקה/ו.</item> + </plurals> <string name="remove_new_flag_label">הסרת הסימון „חדש”</string> <string name="removed_new_flag_label">הוסר הסימון „חדש”</string> <string name="mark_read_label">סימון כנצפה</string> @@ -222,16 +253,12 @@ <string name="download_error_details">פרטים</string> <string name="download_error_details_message">%1$s \n\nכתובת הקובץ:\n%2$s</string> <string name="download_error_device_not_found">התקן האחסון לא נמצא</string> - <string name="download_error_insufficient_space">אין די שטח אחסון</string> <string name="download_error_http_data_error">שגיאת נתוני HTTP</string> <string name="download_error_error_unknown">שגיאה לא ידועה</string> - <string name="download_error_parser_exception">שגיאת מפענח</string> <string name="download_error_unsupported_type">סוג ההזנה אינו נתמך</string> <string name="download_error_connection_error">שגיאת חיבור</string> - <string name="download_error_unknown_host">שרת לא ידוע</string> <string name="download_error_unauthorized">שגיאת אימות</string> <string name="download_error_file_type_type">שגיאת סוג קובץ</string> - <string name="download_error_forbidden">אסור</string> <string name="download_canceled_msg">הורדה בוטלה</string> <string name="download_canceled_autodownload_enabled_msg">ההורדה בוטלה\nה<i>הורדה האוטומטית</i> הושבתה עבור פריט זה</string> <string name="download_report_title">הורדות הושלמו עם שגיאה אחת או יותר</string> @@ -247,14 +274,7 @@ <item quantity="many">נותרו %d הורדות</item> <item quantity="other">נותרו %d הורדות</item> </plurals> - <string name="downloads_processing">ההורדות בהליכי עיבוד</string> <string name="download_notification_title">נתוני הפודקאסט מתקבלים</string> - <plurals name="download_report_content"> - <item quantity="one">הורדה %d הצליחה, %d נכשלו</item> - <item quantity="two">%d הורדות הצליחו, %d נכשלו</item> - <item quantity="many">%d הורדות הצליחו, %d נכשלו</item> - <item quantity="other">%d הורדות הצליחו, %d נכשלו</item> - </plurals> <string name="download_log_title_unknown">כותרת לא ידועה</string> <string name="download_type_feed">הזנה</string> <string name="download_type_media">קובץ מדיה</string> @@ -287,6 +307,7 @@ <string name="player_go_to_picture_in_picture">מצב תמונה בתוך תמונה</string> <string name="unknown_media_key">אנטנה־פּוֹד - מפתח מדיה לא ידוע: %1$d</string> <string name="error_file_not_found">הקובץ לא נמצא</string> + <string name="no_media_label">הפריט אינו מכיל קובץ מדיה</string> <!--Queue operations--> <string name="lock_queue">נעילת תור</string> <string name="unlock_queue">שחרור תור</string> @@ -343,7 +364,6 @@ <string name="storage_pref">אחסון</string> <string name="storage_sum">מחיקה אוטומטית של פרקים, ייבוא, ייצוא</string> <string name="project_pref">מיזם</string> - <string name="queue_label">תור</string> <string name="synchronization_pref">סנכרון</string> <string name="synchronization_sum">סנכרון עם מכשירים אחרים דרך gpodder.net</string> <string name="automation">אוטומציה</string> @@ -354,19 +374,24 @@ <string name="external_elements">רכיבים חיצוניים</string> <string name="interruptions">הפרעות</string> <string name="playback_control">בקרת נגינה</string> + <string name="reassign_hardware_buttons">הקצאת כפתורי חומרה מחדש</string> <string name="preference_search_hint">חיפוש…</string> <string name="preference_search_no_results">אין תוצאות</string> <string name="preference_search_clear_history">פינוי ההיסטוריה</string> <string name="media_player">נגן מדיה</string> <string name="pref_episode_cleanup_title">ניקוי פרקים</string> - <string name="pref_episode_cleanup_summary">פרקים שאינם בתור ואינם במועדפים אמורים לענות לתנאים של הסרה במקרה שההורדה האוטומטית זקוקה למקום לפרקים חדשים</string> + <string name="pref_episode_cleanup_summary">פרקים שמועמדים להסרה אם ההורדה האוטומטית צריכה מקום לפרקים חדשים</string> <string name="pref_pauseOnDisconnect_sum">השהיית הנגינה כאשר האוזניות או ה־Bluetooth מנותקים</string> <string name="pref_unpauseOnHeadsetReconnect_sum">להמשיך את הניגון כשהאוזניות מחוברות מחדש</string> <string name="pref_unpauseOnBluetoothReconnect_sum">להמשיך את הנגינה עם חיבור מחדש של ה־Bluetooth</string> - <string name="pref_hardwareForwardButtonSkips_title">כפתור קדימה מדלג</string> - <string name="pref_hardwareForwardButtonSkips_sum">בעת לחיצה על כפתור הבא במכשיר bluetooth מחובר יש לדלג לפרק הבא במקום להריץ קדימה</string> - <string name="pref_hardwarePreviousButtonRestarts_title">כפתור אחורה מתחיל מחדש</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">לחיצה על כפתור החומרה אחורה מדלג מתחיל מחדש את נגינת הפרק הנוכחי במקום לחזור אחורה בפרק</string> + <string name="pref_hardware_forward_button_title">כפתור קדימה</string> + <string name="pref_hardware_forward_button_summary">להתאים את התנהגות הכפתור קדימה</string> + <string name="pref_hardware_previous_button_title">כפתור אחורה</string> + <string name="pref_hardware_previous_button_summary">להתאים את התנהגות הכפתור אחורה</string> + <string name="button_action_fast_forward">האצה קדימה</string> + <string name="button_action_rewind">חזרה לאחור</string> + <string name="button_action_skip_episode">דילוג על פרק</string> + <string name="button_action_restart_episode">התחלת הפרק מחדש</string> <string name="pref_followQueue_sum">לעבור לפריט הבא בתור כאשר הניגון מסתיים</string> <string name="pref_auto_delete_sum">מחיקת פרק כשהניגון מסתיים</string> <string name="pref_auto_delete_title">מחיקה אוטומטית</string> @@ -386,8 +411,13 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">השבתה</string> <string name="pref_autoUpdateIntervallOrTime_Interval">הגדרת הפרש זמן</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">הגדרת הזמן ביום</string> - <string name="pref_autoUpdateIntervallOrTime_every">כל %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">ב־%1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">כל שעה</item> + <item quantity="two">כל שעתיים</item> + <item quantity="many">כל %d שעות</item> + <item quantity="other">כל %d שעות</item> + </plurals> <string name="pref_followQueue_title">ניגון מתמשך</string> <string name="pref_pauseOnHeadsetDisconnect_title">ניתוק אוזניות או Bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_title">חיבור אוזניות מחדש</string> @@ -421,7 +451,9 @@ <string name="pref_episode_cache_title">מטמון פרקים</string> <string name="pref_episode_cache_summary">המספר הכולל של פרקים שהורדו ונשמרים במכשיר. הורדה אוטומטית תושבת אם הכמות הזאת הושגה.</string> <string name="pref_episode_cover_title">להשתמש בעטיפת הפרק</string> - <string name="pref_episode_cover_summary">להשתמש בעטיפת הפרק כאשר ניתן. אם האפשרות לא סומנה היישומון ישתמש בתמונת העטיפה של הפודקאסט.</string> + <string name="pref_episode_cover_summary">להשתמש בעטיפת הפרק ברשימות כאשר ניתן. אם האפשרות לא סומנה היישומון ישתמש בתמונת העטיפה של הפודקאסט.</string> + <string name="pref_show_remain_time_title">הצגת הזמן שנותר</string> + <string name="pref_show_remain_time_summary">הצגת הזמן שנותר לפרקים כאשר האפשרות מסומנת. אם אינה מסומנת, להציג את סך אורכם של הפרקים.</string> <string name="pref_theme_title_use_system">להשתמש בערכת העיצוב של המערכת</string> <string name="pref_theme_title_light">בהיר</string> <string name="pref_theme_title_dark">כהה</string> @@ -441,8 +473,6 @@ <string name="pref_gpodnet_full_sync_title">לכפות סנכרון מלא</string> <string name="pref_gpodnet_full_sync_sum">סנכרון כל המינויים ומצבי הפרקים עם gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[נכנסת בשם <i>%1$s</i> עם ההתקן <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">הסנכרון נכשל</string> - <string name="pref_gpodnet_notifications_sum">הגדרה זו אינה חלה על שגיאות אימות.</string> <string name="pref_playback_speed_sum">התאמת המהירויות הזמינות למהירות נגינה משתנה</string> <string name="pref_feed_playback_speed_sum">המהירות שתחול על פרקים בפודקאסט זה כשאלו מתחילים להתנגן</string> <string name="pref_feed_skip">לדלג אוטומטית</string> @@ -457,8 +487,6 @@ <string name="pref_fast_forward_sum">התאמת מספר השניות של הקפיצה קדימה בעת לחיצה על כפתור ההאצה</string> <string name="pref_rewind">זמן בקפיצה אחורה</string> <string name="pref_rewind_sum">התאמת מספר השניות של הקפיצה אחורה בעת לחיצה על כפתור החזרה</string> - <string name="pref_gpodnet_sethostname_title">הגדרת שם מארח</string> - <string name="pref_gpodnet_sethostname_use_default_host">שימוש בשם מארח כבררת מחדל</string> <string name="pref_expandNotify_title">עדיפות גבוהה להתראה</string> <string name="pref_expandNotify_sum">הגדרה זו מרחיבה את ההתראה כדי שתציג גם כפתורי נגינה.</string> <string name="pref_persistNotify_title">פקדי נגינה קבועים</string> @@ -469,10 +497,6 @@ <string name="pref_compact_notification_buttons_dialog_error">ניתן לבחור עד %1$d פריטים.</string> <string name="pref_lockscreen_background_title">הגדרת רקע מסך הנעילה</string> <string name="pref_lockscreen_background_sum">הגדרת רקע מסך הנעילה לתמונה של הפרק שמתנגן כעת. כתופעת לוואי, התמונה תופיע גם ביישומי צד שלישי.</string> - <string name="pref_showDownloadReport_title">ההורדה נכשלה</string> - <string name="pref_showDownloadReport_sum">אם ההורדה נכשלת, יש ליצר דוח שמציג את פרטי הכשל.</string> - <string name="pref_showAutoDownloadReport_title">הורדה אוטומטית הושלמה</string> - <string name="pref_showAutoDownloadReport_sum">להציג הודעה לפרקים שהתקבלו אוטומטית.</string> <string name="pref_expand_notify_unsupport_toast">גרסאות Android שקדמו ל־4.1 אינן תומכות בהתרעות מתרחבות.</string> <string name="pref_enqueue_location_title">הוספת המיקום לתור</string> <string name="pref_enqueue_location_sum">הוספת פרקים אל: %1$s</string> @@ -482,6 +506,7 @@ <string name="pref_smart_mark_as_played_disabled">מושבת</string> <string name="pref_image_cache_size_title">גודל מטמון התמונות</string> <string name="pref_image_cache_size_sum">הנפח בכונן שעשוי לשמש למטמון של תמונות.</string> + <string name="documentation_support">תיעוד ותמיכה</string> <string name="visit_user_forum">פורום המשתמשים</string> <string name="bug_report_title">דיווח על תקלה</string> <string name="open_bug_tracker">פתיחת מערכת מעקב התקלות</string> @@ -493,14 +518,14 @@ <string name="pref_current_value">ערך נוכחי: %1$s</string> <string name="pref_proxy_title">מתווך</string> <string name="pref_proxy_sum">הגדרת מתווך רשת</string> - <string name="pref_faq">שאלות נפוצות</string> <string name="pref_no_browser_found">לא נמצא דפדפן.</string> <string name="pref_cast_title">תמיכה ב־Chromecast</string> <string name="pref_cast_message_play_flavor">הפעלת תמיכה בנגינת מדיה על התקני שידור מרוחקים (כגון Chromecast, רמקולים דיגיטליים או Android TV)</string> <string name="pref_cast_message_free_flavor">לתמיכה ב־Chromecast נדרשות ספריות קנייניות מאת צד־שלישי שמושבתות בגרסה זו של אנטנה־פּוֹד</string> <string name="pref_enqueue_downloaded_title">הוספת הורדות לתור</string> <string name="pref_enqueue_downloaded_summary">הוספת פרקים שהתקבלו לתור</string> - <string name="media_player_builtin">הנגן המובנה ב־Android</string> + <string name="media_player_builtin">הנגן המובנה ב־Android (לא זמין עוד)</string> + <string name="media_player_sonic">נגן המדיה Sonic (לא זמין עוד)</string> <string name="media_player_exoplayer_recommended">ExoPlayer (מומלץ)</string> <string name="media_player_switch_to_exoplayer">מעבר ל־ExoPlayer</string> <string name="media_player_switched_to_exoplayer">הועבר ל־ExoPlayer.</string> @@ -589,6 +614,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">הגדרת מתזמן שינה</string> <string name="disable_sleeptimer_label">השבתת מתזמן שינה</string> + <string name="extend_sleep_timer_label">+%d דק׳</string> <string name="sleep_timer_label">מתזמן שינה</string> <string name="time_dialog_invalid_input">קלט שגוי, השעה חייב להיות מספר שלם וחיובי</string> <string name="shake_to_reset_label">יש לנער כדי לאפס</string> @@ -622,22 +648,22 @@ <string name="gpodnet_suggestions_header">המלצות</string> <string name="gpodnet_search_hint">חיפוש ב־gpodder.net</string> <string name="gpodnetauth_login_title">כניסה</string> - <string name="gpodnetauth_login_descr">ברוך בואך לתהליך הכניסה ל־gpodder.net. ראשית, עליך להקליד את פרטי הכניסה שלך:</string> <string name="gpodnetauth_login_butLabel">כניסה</string> - <string name="gpodnetauth_login_register">אם עדיין אין לך חשבון, ניתן ליצור אחד כאן:\nhttps://gpodder.net/register/</string> + <string name="create_account">יצירת חשבון</string> <string name="username_label">שם משתמש</string> <string name="password_label">ססמה</string> - <string name="gpodnetauth_device_title">בחירת מכשיר</string> + <string name="gpodnet_description">Gpodder.net הוא שירות סנכרון פודקאסטים בקוד פתוח שאין לו קשר למיזם אנטנה־פּוֹד.</string> + <string name="gpodnetauth_server_official">שרת gpodder.net רשמי</string> + <string name="gpodnetauth_server_custom">שרת בהתאמה אישית</string> + <string name="gpodnetauth_host">שם מארח</string> + <string name="gpodnetauth_select_server">בחירת שרת</string> <string name="gpodnetauth_device_descr">ניתן ליצור התקן חדש לשימוש עם החשבון שלך ב־gpodder.net או לבחור בהתקן חדש:</string> - <string name="gpodnetauth_device_deviceID">מזהה מכשיר:\u0020</string> - <string name="gpodnetauth_device_caption">כותרת</string> - <string name="gpodnetauth_device_butCreateNewDevice">יצירת מכשיר חדש</string> - <string name="gpodnetauth_device_chooseExistingDevice">בחירת מכשיר קיים:</string> - <string name="gpodnetauth_device_errorEmpty">מזהה המכשיר לא יכול להישאר ריק</string> - <string name="gpodnetauth_device_errorAlreadyUsed">מזהה המכשיר כבר בשימוש</string> + <string name="gpodnetauth_device_name">שם המכשיר</string> + <string name="gpodnetauth_device_name_default">אנטנה־פּוֹד על %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">הכותרת לא יכולה להישאר ריקה</string> + <string name="gpodnetauth_existing_devices">מכשירים קיימים</string> + <string name="gpodnetauth_create_device">יצירת מכשיר</string> <string name="gpodnetauth_device_butChoose">בחירה</string> - <string name="gpodnetauth_finish_title">נכנסת בהצלחה!</string> <string name="gpodnetauth_finish_descr">מזל טוב! חשבון ה־gpodder.net שלך מקושר כעת עם המכשיר שלך. מעתה כל המינויים שלך יסונכרנו אוטומטית על ידי אנטנה־פּוֹד מהמכשיר שלך לחשבון ה־gpodder.net שלך.</string> <string name="gpodnetauth_finish_butsyncnow">התחלת סנכרון כעת</string> <string name="gpodnetauth_finish_butgomainscreen">מעבר למסך הראשי</string> @@ -691,6 +717,7 @@ <string name="switch_pages">החלפת עמודים</string> <string name="position">מיקום: %1$s</string> <string name="apply_action">החלת פעולה</string> + <string name="play_chapter">לנגן פרק</string> <!--Feed information screen--> <string name="authentication_label">אימות</string> <string name="authentication_descr">שינוי שם המשתמש והססמה שלך לפודקאסט הזה ולפרקים שלו.</string> @@ -825,18 +852,22 @@ <string name="cast_failed_receiver_player_error">הנגן המקבל נתקל בשגיאה חמורה</string> <string name="cast_failed_media_error_skipping">שגיאה בנגינת המדיה. מתבצע דילוג…</string> <!--Notification channels--> + <string name="notification_group_errors">שגיאות</string> + <string name="notification_group_news">חדשות</string> <string name="notification_channel_user_action">נדרשת פעולה</string> <string name="notification_channel_user_action_description">מופיע אם נדרשת פעולה מצדך, למשך אם עליך להקליד ססמה.</string> <string name="notification_channel_downloading">הורדה</string> <string name="notification_channel_downloading_description">מופיע בזמן שמתרחשת הורדה.</string> <string name="notification_channel_playing">מתנגן כעת</string> <string name="notification_channel_playing_description">מאשר לשלוט בנגינה. זאת ההתראה הראשית שמופיעה בעת נגינת פודקאסט.</string> - <string name="notification_channel_error">שגיאות</string> - <string name="notification_channel_error_description">מופיע אם משהו משתבש, למשל אם הורדה או עדכון הזנה נכשלים.</string> - <string name="notification_channel_sync_error">שגיאות סנכרון</string> + <string name="notification_channel_download_error">ההורדה נכשלה</string> + <string name="notification_channel_download_error_description">מופיע כאשר הורדה או עדכון הזנה נכשלים.</string> + <string name="notification_channel_sync_error">הסנכרון נכשל</string> <string name="notification_channel_sync_error_description">מופיע כאשר הסנכרון מול gpodder נכשל.</string> - <string name="notification_channel_auto_download">הורדות אוטומטיות</string> + <string name="notification_channel_auto_download">ההורדה האוטומטית הושלמה</string> <string name="notification_channel_episode_auto_download">מופיע כאשר פרקים התקבלו אוטומטית.</string> + <string name="notification_channel_new_episode">פרק חדש</string> + <string name="notification_channel_new_episode_description">מופיע כאשר נמצא פרק חדש בפודקאסט, כאשר התראות פעילות</string> <!--Widget settings--> <string name="widget_settings">הגדרות וידג׳ט</string> <string name="widget_create_button">יצירת וידג׳</string> diff --git a/core/src/main/res/values-ja/strings.xml b/core/src/main/res/values-ja/strings.xml index 7a38857a2..f052d8f02 100644 --- a/core/src/main/res/values-ja/strings.xml +++ b/core/src/main/res/values-ja/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">統計情報</string> <string name="add_feed_label">フィードを追加</string> <string name="episodes_label">エピソード</string> + <string name="queue_label">キュー</string> <string name="all_episodes_short_label">すべて</string> <string name="new_episodes_label">新規</string> <string name="favorite_episodes_label">お気に入り</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">ログ</string> <string name="subscriptions_label">購読</string> <string name="subscriptions_list_label">購読リスト</string> - <string name="cancel_download_label">ダウンロードをキャンセル</string> <string name="playback_history_label">再生履歴</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net ログイン</string> @@ -25,6 +25,7 @@ <string name="episode_cache_full_message">エピソードキャッシュが制限に達しました。設定でキャッシュサイズを増やすことができます。</string> <string name="playback_statistics_label">再生</string> <string name="download_statistics_label">ダウンロード</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="statistics_details_dialog">%2$d から %1$d のエピソードが開始しました。\n\n%4$s から%3$s を再生しました。</string> <string name="statistics_mode">統計情報モード</string> @@ -75,7 +76,6 @@ <string name="description_label">説明</string> <string name="episodes_suffix">\u0020エピソード</string> <string name="processing_label">処理中</string> - <string name="save_username_password_label">ユーザ名とパスワードを保存する</string> <string name="close_label">閉じる</string> <string name="retry_label">再試行</string> <string name="auto_download_label">自動ダウンロードに含む</string> @@ -86,7 +86,6 @@ <string name="feed_volume_reduction_off">オフ</string> <string name="feed_volume_reduction_light">ライト</string> <string name="feed_volume_reduction_heavy">ヘビー</string> - <string name="parallel_downloads_suffix">\u0020パラレル ダウンロード</string> <string name="feed_auto_download_global">全般のデフォルト</string> <string name="feed_auto_download_always">常に</string> <string name="feed_auto_download_never">しない</string> @@ -127,7 +126,6 @@ <string name="hide_not_queued_episodes_label">キューに未追加</string> <string name="hide_has_media_label">メディアあり</string> <string name="filtered_label">フィルターしました</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} 前回更新に失敗しました</string> <string name="open_podcast">ポッドキャストを開く</string> <string name="please_wait_for_data">データが読み込まれるまでしばらくお待ちください</string> <!--actions on feeditems--> @@ -182,16 +180,12 @@ <string name="download_error_details">詳細</string> <string name="download_error_details_message">%1$s \n\nファイル URL:\n%2$s</string> <string name="download_error_device_not_found">ストレージ デバイスが見つかりません</string> - <string name="download_error_insufficient_space">スペースが不足しています</string> <string name="download_error_http_data_error">HTTPデータエラー</string> <string name="download_error_error_unknown">不明なエラー</string> - <string name="download_error_parser_exception">解析エラー</string> <string name="download_error_unsupported_type">サポートしないフィードタイプ</string> <string name="download_error_connection_error">接続エラー</string> - <string name="download_error_unknown_host">ホスト不明</string> <string name="download_error_unauthorized">認証エラー</string> <string name="download_error_file_type_type">ファイルタイプ エラー</string> - <string name="download_error_forbidden">禁止</string> <string name="download_canceled_msg">ダウンロードをキャンセルしました</string> <string name="download_canceled_autodownload_enabled_msg">ダウンロードをキャンセルしました\nこのアイテムの <i>自動ダウンロード</i> を無効にしました</string> <string name="download_report_title">ダウンロードがエラーで完了しました</string> @@ -204,7 +198,6 @@ <plurals name="downloads_left"> <item quantity="other">%d ダウンロード残</item> </plurals> - <string name="downloads_processing">ダウンロード処理中</string> <string name="download_notification_title">ポッドキャストデータをダウンロード中</string> <string name="download_log_title_unknown">タイトル不明</string> <string name="download_type_feed">フィード</string> @@ -290,7 +283,6 @@ <string name="storage_pref">ストレージ</string> <string name="storage_sum">エピソードの自動削除、インポート、エクスポート</string> <string name="project_pref">プロジェクト</string> - <string name="queue_label">キュー</string> <string name="automation">自動</string> <string name="download_pref_details">詳細</string> <string name="import_export_pref">インポート/エクスポート</string> @@ -304,14 +296,9 @@ <string name="preference_search_clear_history">履歴をクリア</string> <string name="media_player">メディアプレーヤー</string> <string name="pref_episode_cleanup_title">エピソード クリーンアップ</string> - <string name="pref_episode_cleanup_summary">キューに含まれておらず、お気に入りではないエピソードは、自動ダウンロードで新しいエピソードのためにスペースが必要な場合、除去の対象になります</string> <string name="pref_pauseOnDisconnect_sum">ヘッドフォンまたはBluetoothの接続が切断された時、再生を一時停止します</string> <string name="pref_unpauseOnHeadsetReconnect_sum">ヘッドフォンが再接続された時に再生を再開します</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Bluetoothが再接続された時に再生を再開します</string> - <string name="pref_hardwareForwardButtonSkips_title">早送りボタンでスキップ</string> - <string name="pref_hardwareForwardButtonSkips_sum">Bluetoothで接続されたデバイスの早送りボタンを押したときに、早送りの代わりに次のエピソードにスキップします</string> - <string name="pref_hardwarePreviousButtonRestarts_title">戻るボタンで再開</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">ハードウェアの戻るボタンを押したときに、巻き戻しの代わりに現在のエピソードの再生を再開します</string> <string name="pref_followQueue_sum">再生が完了した時に次のキューのアイテムに移動します</string> <string name="pref_auto_delete_sum">再生が完了した時にエピソードを削除します</string> <string name="pref_auto_delete_title">自動削除</string> @@ -331,7 +318,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">無効</string> <string name="pref_autoUpdateIntervallOrTime_Interval">間隔をセット</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">時間をセット</string> - <string name="pref_autoUpdateIntervallOrTime_every">%1$s ごと</string> <string name="pref_autoUpdateIntervallOrTime_at">%1$s に</string> <string name="pref_followQueue_title">連続再生</string> <string name="pref_unpauseOnHeadsetReconnect_title">ヘッドフォン再接続</string> @@ -364,7 +350,6 @@ <string name="pref_episode_cache_title">エピソードキャッシュ</string> <string name="pref_episode_cache_summary">デバイスにキャッシュされたダウンロード済エピソードの合計数。この数に達すると自動ダウンロードが抑制されます。</string> <string name="pref_episode_cover_title">エピソードカバーを使用する</string> - <string name="pref_episode_cover_summary">エピソード固有のカバーが利用できる場合は常に使用します。 チェックされていない場合、アプリは常にポッドキャストのカバー画像を使用します。</string> <string name="pref_theme_title_use_system">システムのテーマを使用する</string> <string name="pref_theme_title_light">ライト</string> <string name="pref_theme_title_dark">ダーク</string> @@ -382,7 +367,6 @@ <string name="pref_gpodnet_sync_changes_sum">購読とエピソードの状態の変更を gpodder.net で同期します。</string> <string name="pref_gpodnet_full_sync_sum">すべての購読とエピソードの状態を gpodder.net で同期します。</string> <string name="pref_gpodnet_login_status"><![CDATA[<i>%1$s</i> としてデバイス <i>%2$s</i> でログインしました]]></string> - <string name="pref_gpodnet_notifications_sum">この設定は、認証エラーには適用されません。</string> <string name="pref_feed_playback_speed_sum">このポッドキャストでエピソードのオーディオ再生を開始するときに使用する速度</string> <string name="pref_playback_time_respects_speed_title">メディア情報を再生速度に調整</string> <string name="pref_playback_time_respects_speed_sum">表示される位置と時間が再生速度に調整されます</string> @@ -390,8 +374,6 @@ <string name="pref_fast_forward_sum">早送りボタンがクリックされたときにジャンプする秒数をカスタマイズします</string> <string name="pref_rewind">巻き戻しスキップ時間</string> <string name="pref_rewind_sum">巻き戻しボタンがクリックされたときに後方にジャンプする秒数をカスタマイズします</string> - <string name="pref_gpodnet_sethostname_title">ホスト名をセット</string> - <string name="pref_gpodnet_sethostname_use_default_host">デフォルトホストを使用</string> <string name="pref_expandNotify_title">優先度の高い通知</string> <string name="pref_expandNotify_sum">これは通常再生ボタンを表示するように通知を展開します。</string> <string name="pref_persistNotify_title">永続再生コントロール</string> @@ -400,8 +382,6 @@ <string name="pref_compact_notification_buttons_dialog_error">最大 %1$d のアイテムのみを選択できます。</string> <string name="pref_lockscreen_background_title">ロック画面の背景を設定</string> <string name="pref_lockscreen_background_sum">ロック画面の背景を、現在のエピソードの画像に設定します。副作用として、これはサードパーティのアプリケーションでも画像を表示します。</string> - <string name="pref_showDownloadReport_sum">ダウンロードが失敗した場合、失敗の詳細を表示するレポートを生成します。</string> - <string name="pref_showAutoDownloadReport_sum">自動ダウンロードされたエピソードの通知を表示します。</string> <string name="pref_expand_notify_unsupport_toast">Androidバージョン4.1以前では、拡張通知をサポートしていません。</string> <string name="pref_enqueue_location_title">キューに入れる場所</string> <string name="pref_enqueue_location_sum">エピソードを追加: %1$s</string> @@ -422,14 +402,12 @@ <string name="pref_current_value">現在の値: %1$s</string> <string name="pref_proxy_title">プロキシ</string> <string name="pref_proxy_sum">ネットワーク プロキシの設定</string> - <string name="pref_faq">よくある質問と答え</string> <string name="pref_no_browser_found">Webブラウザーが見つかりません。</string> <string name="pref_cast_title">Chromecast サポート</string> <string name="pref_cast_message_play_flavor">(Chromecast、オーディオスピーカー、Android TV など) キャストデバイス上でリモートメディア再生のサポートを有効にします</string> <string name="pref_cast_message_free_flavor">Chromecast は AntennaPod のこのバージョンで無効になっているサードパーティ独自のライブラリーが必要です</string> <string name="pref_enqueue_downloaded_title">ダウンロードのキューに入れる</string> <string name="pref_enqueue_downloaded_summary">ダウンロードしたエピソードをキューに追加します</string> - <string name="media_player_builtin">ビルトイン Android プレーヤー</string> <string name="pref_skip_silence_title">音声の無音をスキップ</string> <string name="pref_videoBehavior_title">ビデオ終了時</string> <string name="pref_videoBehavior_sum">ビデオ再生から遷移時の動作</string> @@ -514,22 +492,12 @@ <string name="gpodnet_suggestions_header">おススメ</string> <string name="gpodnet_search_hint">gpodder.netを検索</string> <string name="gpodnetauth_login_title">ログイン</string> - <string name="gpodnetauth_login_descr">gpodder.netログインへようこそ。まずログイン情報を入力してください。</string> <string name="gpodnetauth_login_butLabel">ログイン</string> - <string name="gpodnetauth_login_register">まだアカウントをお持ちでなければ、ここで作成することができます。\nhttps://gpodder.net/register/</string> <string name="username_label">ユーザー名</string> <string name="password_label">パスワード</string> - <string name="gpodnetauth_device_title">端末選択</string> <string name="gpodnetauth_device_descr">gpodder.net アカウントで使用する新しい端末を作成するか、既存のものを選択してください。</string> - <string name="gpodnetauth_device_deviceID">端末ID:\u0020</string> - <string name="gpodnetauth_device_caption">キャプション</string> - <string name="gpodnetauth_device_butCreateNewDevice">新しい端末を作成</string> - <string name="gpodnetauth_device_chooseExistingDevice">既存の端末を選択:</string> - <string name="gpodnetauth_device_errorEmpty">端末IDは空にできません</string> - <string name="gpodnetauth_device_errorAlreadyUsed">端末IDは既に使用しています</string> <string name="gpodnetauth_device_caption_errorEmpty">キャプションは空にできません</string> <string name="gpodnetauth_device_butChoose">選択</string> - <string name="gpodnetauth_finish_title">ログインされました!</string> <string name="gpodnetauth_finish_descr">おめでとうございます! あなたのgpodder.netアカウントが今お使いの端末とリンクされました。 AntennaPodは今から自動的にgpodder.netアカウントを使用して端末の購読を同期します。</string> <string name="gpodnetauth_finish_butsyncnow">今すぐ同期を開始</string> <string name="gpodnetauth_finish_butgomainscreen">メイン画面に移動</string> @@ -684,8 +652,6 @@ <string name="notification_channel_downloading_description">現在のダウンロードが表示されます。</string> <string name="notification_channel_playing">現在再生中</string> <string name="notification_channel_playing_description">再生をコントロールできます。これはポッドキャスト再生中のメイン通知です。</string> - <string name="notification_channel_error">エラー</string> - <string name="notification_channel_auto_download">自動ダウンロード</string> <string name="notification_channel_episode_auto_download">エピソードが自動ダウンロードされた時に表示します。</string> <!--Widget settings--> <string name="widget_settings">ウィジェット設定</string> diff --git a/core/src/main/res/values-ko/strings.xml b/core/src/main/res/values-ko/strings.xml index 538aa2629..569877cf3 100644 --- a/core/src/main/res/values-ko/strings.xml +++ b/core/src/main/res/values-ko/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">통계</string> <string name="add_feed_label">팟캐스트 추가</string> <string name="episodes_label">에피소드</string> + <string name="queue_label">대기열</string> <string name="all_episodes_short_label">전체</string> <string name="new_episodes_label">신규</string> <string name="favorite_episodes_label">즐겨찾기</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">기록</string> <string name="subscriptions_label">구독</string> <string name="subscriptions_list_label">구독 목록</string> - <string name="cancel_download_label">다운로드\n취소</string> + <string name="cancel_download_label">다운로드 취소</string> <string name="playback_history_label">재생 기록</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net 로그인</string> @@ -25,14 +26,21 @@ <string name="episode_cache_full_message">에피소드 캐시 한계값에 도달했습니다. 설정에서 캐시 크기를 늘릴 수 있습니다.</string> <string name="playback_statistics_label">재생</string> <string name="download_statistics_label">다운로드</string> + <string name="notification_pref_fragment">알림</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" 없음</string> <!--Statistics fragment--> + <string name="total_time_listened_to_podcasts">재생한 에피소드 총 시간:</string> <string name="statistics_details_dialog">에피소드 %1$d개 (전체 %2$d개) 시작.\n\n%3$s개 재생 (전체 %4$s개).</string> <string name="statistics_mode">통계 모드</string> <string name="statistics_mode_normal">실제 재생한 시간을 계산합니다. 두 번 재생하면 두 번 계산되고, 재생한 것으로 표시하면 계산에 안 들어갑니다.</string> + <string name="statistics_mode_count_all">재생했다고 표시한 모든 에피소드의 합</string> <string name="statistics_speed_not_counted">주의: 재생 속도는 고려하지 않습니다</string> <string name="statistics_reset_data">통계 데이터 초기화</string> <string name="statistics_reset_data_msg">모든 에피소드의 재생 시간 기록을 지웁니다. 정말로 계속 하시겠습니까?</string> + <string name="statistics_counting_since">%s 이후,\n재생한 항목:</string> <!--Download Statistics fragment--> + <string name="total_size_downloaded_podcasts">장치의 에피소드 총 크기:</string> <!--Main activity--> <string name="drawer_open">메뉴 열기</string> <string name="drawer_close">메뉴 닫기</string> @@ -47,6 +55,9 @@ <string name="drawer_feed_counter_downloaded">다운로드한 에피소드 수</string> <string name="drawer_feed_counter_none">없음</string> <!--Bug report activity--> + <string name="log_file_share_exception">호환되는 앱이 없습니다</string> + <string name="export_logs_menu_title">자세한 기록 내보내기</string> + <string name="confirm_export_log_dialog_message">자세한 기록에는 구독 목록과 같은 민감한 정보가 들어 있을 수도 있습니다</string> <!--Webview actions--> <string name="open_in_browser_label">브라우저에서 열기</string> <string name="copy_url_label">URL 복사</string> @@ -75,7 +86,6 @@ <string name="description_label">설명</string> <string name="episodes_suffix">\u0020에피소드</string> <string name="processing_label">처리 중</string> - <string name="save_username_password_label">사용자 이름 및 암호 저장</string> <string name="close_label">닫기</string> <string name="retry_label">다시 시도</string> <string name="auto_download_label">자동 다운로드에 포함</string> @@ -87,12 +97,13 @@ <string name="feed_volume_reduction_off">끄기</string> <string name="feed_volume_reduction_light">가볍게</string> <string name="feed_volume_reduction_heavy">무겁게</string> - <string name="parallel_downloads_suffix">\u0020동시 다운로드</string> + <string name="parallel_downloads">%1$d개 병렬 다운로드</string> <string name="feed_auto_download_global">전체 기본값</string> <string name="feed_auto_download_always">항상</string> <string name="feed_auto_download_never">안 함</string> <string name="send_label">보내기…</string> <string name="episode_cleanup_never">안 함</string> + <string name="episode_cleanup_except_favorite_removal">즐겨찾기 아닐 때</string> <string name="episode_cleanup_queue_removal">대기열에 없을 때</string> <string name="episode_cleanup_after_listening">끝나고 나서</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -104,7 +115,19 @@ <plurals name="num_selected_label"> <item quantity="other">%d개 선택</item> </plurals> + <plurals name="num_episodes"> + <item quantity="other">%d개 에피소드</item> + </plurals> <string name="loading_more">더 읽어들이기…</string> + <string name="episode_notification">에피소드 알림</string> + <string name="episode_notification_summary">새 에피소드가 나오면 알림을 표시합니다.</string> + <plurals name="new_episode_notification_message"> + <item quantity="other">%2$s에 새 에피소드 %1$d개</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="other">새 에피소드</item> + </plurals> + <string name="new_episode_notification_group_text">구독에 새 에피소드가 있습니다.</string> <!--Actions on feeds--> <string name="mark_all_read_label">모두 재생했다고 표시</string> <string name="mark_all_read_msg">모든 에피소드를 재생했다고 표시했습니다</string> @@ -119,8 +142,13 @@ <string name="feed_settings_label">팟캐스트 설정</string> <string name="rename_feed_label">팟캐스트 이름 바꾸기</string> <string name="remove_feed_label">팟캐스트 제거</string> + <string name="share_label">공유</string> + <string name="share_label_with_ellipses">공유…</string> <string name="share_file_label">파일 공유</string> + <string name="share_website_url_label">웹사이트 주소</string> + <string name="share_feed_url_label">팟캐스트 피드 URL</string> <string name="feed_delete_confirmation_msg">확인하면 \"%1$s\" 피드를 삭제하고 이 피드에서 다운로드한 모든 에피소드를 삭제합니다.</string> + <string name="feed_delete_confirmation_local_msg">정말로 \"%1$s\" 팟캐스트를 지울지 확인하십시오. 로컬 소스 폴더에 있는 파일은 삭제하지 않을 것입니다.</string> <string name="feed_remover_msg">팟캐스트 삭제하는 중</string> <string name="load_complete_feed">전체 팟캐스트 새로 고침</string> <string name="multi_select">다중 선택</string> @@ -131,7 +159,6 @@ <string name="hide_not_queued_episodes_label">대기열 추가 안 함</string> <string name="hide_has_media_label">미디어 있음</string> <string name="filtered_label">필터링함</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} 최근 새로 고침 실패</string> <string name="open_podcast">팟캐스트 열기</string> <string name="please_wait_for_data">데이터를 읽어 들일 때까지 기다리십시오</string> <!--actions on feeditems--> @@ -145,6 +172,9 @@ <string name="delete_label">삭제</string> <string name="delete_failed">파일을 삭제할 수 없습니다. 장치를 재부팅하면 동작할 수도 있습니다.</string> <string name="delete_episode_label">에피소드 삭제</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="other">%d개 에피소드 선택. %d개 다운로드 삭제.</item> + </plurals> <string name="remove_new_flag_label">\"신규\" 플래그 제거</string> <string name="removed_new_flag_label">\"신규\" 플래그 제거함</string> <string name="mark_read_label">재생했다고 표시</string> @@ -187,16 +217,12 @@ <string name="download_error_details">자세히</string> <string name="download_error_details_message">%1$s \n\n파일 URL:\n%2$s</string> <string name="download_error_device_not_found">저장 장치가 없습니다</string> - <string name="download_error_insufficient_space">저장 공간이 부족합니다</string> <string name="download_error_http_data_error">HTTP 데이터 오류</string> <string name="download_error_error_unknown">알 수 없는 오류</string> - <string name="download_error_parser_exception">파서 프로그램 예외</string> <string name="download_error_unsupported_type">지원하지 않는 피드 종류</string> <string name="download_error_connection_error">연결 오류</string> - <string name="download_error_unknown_host">알 수 없는 호스트</string> <string name="download_error_unauthorized">인증 오류</string> <string name="download_error_file_type_type">파일 종류 오류</string> - <string name="download_error_forbidden">금지됨</string> <string name="download_canceled_msg">다운로드 취소함</string> <string name="download_canceled_autodownload_enabled_msg">다운로드 취소함\n이 항목에 <i>자동 다운로드</i>를 해제합니다</string> <string name="download_report_title">다운로드 마침 (오류 있음)</string> @@ -209,7 +235,6 @@ <plurals name="downloads_left"> <item quantity="other">다운로드 %d개 남음</item> </plurals> - <string name="downloads_processing">다운로드 처리 중</string> <string name="download_notification_title">팟캐스트 데이터 다운로드 중</string> <string name="download_log_title_unknown">알 수 없는 제목</string> <string name="download_type_feed">피드</string> @@ -223,6 +248,8 @@ <string name="confirm_mobile_download_dialog_message">휴대전화망 데이터 연결을 통한 다운로드는 설정에서 막혀 있습니다.\n\n임시로 다운로드를 열 수 있습니다\n\n<small>여기서 선택한 사항은 10분 동안 유지됩니다.</small></string> <string name="confirm_mobile_streaming_notification_title">휴대전화망 스트리밍 확인</string> <string name="confirm_mobile_streaming_notification_message">휴대전화 데이터 연결을 통한 스트리밍은 사용하지 않게 설정되어 있습니다. 그래도 스트리밍을 하려면 누르십시오.</string> + <string name="confirm_mobile_streaming_button_always">항상</string> + <string name="confirm_mobile_streaming_button_once">한번만</string> <string name="confirm_mobile_download_dialog_only_add_to_queue">대기열에 추가</string> <string name="confirm_mobile_download_dialog_enable_temporarily">임시로 허용</string> <!--Mediaplayer messages--> @@ -241,6 +268,7 @@ <string name="player_go_to_picture_in_picture">화면 속 화면 모드</string> <string name="unknown_media_key">안테나팟 - 알 수 없는 미디어 키: %1$d</string> <string name="error_file_not_found">파일이 없습니다</string> + <string name="no_media_label">항목에 미디어 파일이 들어있지 않습니다</string> <!--Queue operations--> <string name="lock_queue">대기열 잠그기</string> <string name="unlock_queue">대기열 잠금 해제</string> @@ -269,6 +297,8 @@ <string name="no_playback_plugin_title">플러그인을 설치하지 않았습니다</string> <string name="no_playback_plugin_or_sonic_msg">여러가지 속도로 재생이 동작하려면 내부의 소닉 미디어 플레이어 사용을 추천합니다.</string> <string name="enable_sonic">소닉 사용</string> + <string name="speed_presets">프리셋</string> + <string name="preset_already_exists">%1$.2f배속이 이미 프리셋으로 저장되었습니다.</string> <!--Empty list labels--> <string name="no_items_header_label">대기열의 에피소드 없음</string> <string name="no_items_label">에피소드를 다운로드하면 추가합니다. 또는 에피소드를 길게 눌러 \"대기열에 추가\"를 선택합니다.</string> @@ -295,7 +325,6 @@ <string name="storage_pref">저장소</string> <string name="storage_sum">에피소드 자동 삭제, 가져오기, 내보내기</string> <string name="project_pref">프로젝트</string> - <string name="queue_label">대기열</string> <string name="synchronization_pref">동기화</string> <string name="synchronization_sum">gpodder.net 사용하는 다른 장비와 동기화</string> <string name="automation">자동</string> @@ -306,19 +335,24 @@ <string name="external_elements">외부 항목</string> <string name="interruptions">끼어들기</string> <string name="playback_control">재생 조작</string> + <string name="reassign_hardware_buttons">하드웨어 버튼 재할당</string> <string name="preference_search_hint">검색…</string> <string name="preference_search_no_results">결과 없음</string> <string name="preference_search_clear_history">기록 지우기</string> <string name="media_player">미디어 플레이어</string> <string name="pref_episode_cleanup_title">에피소드 정리</string> - <string name="pref_episode_cleanup_summary">대기열에 없고 즐겨찾기에 없는 에피소드는 자동 다운로드에서 새 에피소드에 공간이 필요할 경우 제거될 수 있습니다.</string> + <string name="pref_episode_cleanup_summary">자동 다운로드에서 새 에피소드에 공간이 더 필요할 때 삭제될 수 있는 에피소드</string> <string name="pref_pauseOnDisconnect_sum">헤드폰이나 블루투스가 연결 해제되었을 경우 일시정지합니다.</string> <string name="pref_unpauseOnHeadsetReconnect_sum">헤드폰 다시 연결할 때 재생을 계속합니다.</string> <string name="pref_unpauseOnBluetoothReconnect_sum">블루투스가 다시 연결되면 재생을 계속합니다.</string> - <string name="pref_hardwareForwardButtonSkips_title">앞으로 가기 버튼 건너뛰기</string> - <string name="pref_hardwareForwardButtonSkips_sum">앞으로 가기 하드웨어 버튼을 눌렀을 경우 빨리감기 대신 다음 에피소드로 넘깁니다.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">이전 버튼을 누르면 재시작</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">하드웨어 이전 버튼을 누르면 뒤로감기 대신 현재 에피소드를 재시작합니다</string> + <string name="pref_hardware_forward_button_title">다음 버튼</string> + <string name="pref_hardware_forward_button_summary">다음 버튼의 동작을 직접 설정합니다</string> + <string name="pref_hardware_previous_button_title">이전 버튼</string> + <string name="pref_hardware_previous_button_summary">이전 버튼의 동작을 직접 설정합니다</string> + <string name="button_action_fast_forward">빨리 감기</string> + <string name="button_action_rewind">뒤로 감기</string> + <string name="button_action_skip_episode">에피소드 건너뛰기</string> + <string name="button_action_restart_episode">에피소드 다시 시작</string> <string name="pref_followQueue_sum">재생을 마쳤을 때 다음 대기열로 이동</string> <string name="pref_auto_delete_sum">재생이 끝나면 에피소드 삭제</string> <string name="pref_auto_delete_title">자동 삭제</string> @@ -338,9 +372,12 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">사용 안 함</string> <string name="pref_autoUpdateIntervallOrTime_Interval">주기 지정</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">하루 중 시각 지정</string> - <string name="pref_autoUpdateIntervallOrTime_every">매 %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">%1$s에서</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="other">매 %d시간</item> + </plurals> <string name="pref_followQueue_title">연속 재생</string> + <string name="pref_pauseOnHeadsetDisconnect_title">헤드폰 또는 블루투스 연결 끊김</string> <string name="pref_unpauseOnHeadsetReconnect_title">헤드폰 다시 연결</string> <string name="pref_unpauseOnBluetoothReconnect_title">블루투스 다시 연결</string> <string name="pref_stream_over_download_title">스트리밍 우선</string> @@ -353,6 +390,7 @@ <string name="pref_mobileUpdate_episode_download">에피소드 다운로드</string> <string name="pref_mobileUpdate_streaming">스트리밍</string> <string name="user_interface_label">사용자 인터페이스</string> + <string name="user_interface_sum">모양, 구독, 잠금 화면</string> <string name="pref_set_theme_title">테마 선택</string> <string name="pref_nav_drawer_items_title">네비게이션 드로어 항목 설정</string> <string name="pref_nav_drawer_items_sum">네비게이션 드로어에 어떤 항목을 표시할지 바꿉니다.</string> @@ -372,6 +410,8 @@ <string name="pref_episode_cache_summary">장치에 임시 저장한 다운로드한 에피소드의 전체 개수. 이 숫자에 도달하면 자동 다운로드가 지연됩니다.</string> <string name="pref_episode_cover_title">에피소드 커버 사용</string> <string name="pref_episode_cover_summary">에피소드마다 설정된 커버가 있으면 그 커버를 사용합니다. 사용하지 않으면, 앱에서는 항상 팟캐스트 커버 이미지를 사용합니다.</string> + <string name="pref_show_remain_time_title">남은 시간 표시</string> + <string name="pref_show_remain_time_summary">체크하면 에피소드 남은 시간을 표시합니다. 체크하지 않으면 에피소드의 전체 시간을 표시합니다.</string> <string name="pref_theme_title_use_system">시스템 테마 사용</string> <string name="pref_theme_title_light">밝게</string> <string name="pref_theme_title_dark">어둡게</string> @@ -391,7 +431,7 @@ <string name="pref_gpodnet_full_sync_title">전체 동기화 강제</string> <string name="pref_gpodnet_full_sync_sum">gpodder.net의 모든 구독과 에피소드 상태를 동기화합니다.</string> <string name="pref_gpodnet_login_status"><![CDATA[<i>%1$s</i> 사용자로 로그인, <i>%2$s</i> 장치]]></string> - <string name="pref_gpodnet_notifications_sum">이 설정은 인증 오류에는 적용되지 않습니다.</string> + <string name="pref_playback_speed_sum">재생 속도를 다양하게 할 경우 속도를 직접 설정</string> <string name="pref_feed_playback_speed_sum">이 팟캐스트의 에피소드를 재생할 때 사용할 오디오 재생 속도</string> <string name="pref_feed_skip">자동 건너뛰기</string> <string name="pref_feed_skip_sum">소개 및 마지막 크레디트 건너뛰기</string> @@ -405,18 +445,16 @@ <string name="pref_fast_forward_sum">앞으로 감기 버튼을 눌렀을 때 앞으로 넘어갈 초를 지정합니다.</string> <string name="pref_rewind">뒤로 건너뛰기 시간</string> <string name="pref_rewind_sum">뒤로 감기 버튼을 눌렀을 때 뒤로 넘어갈 초를 지정합니다.</string> - <string name="pref_gpodnet_sethostname_title">호스트 이름 설정</string> - <string name="pref_gpodnet_sethostname_use_default_host">기본 호스트 사용</string> <string name="pref_expandNotify_title">알림 우선순위 높게</string> <string name="pref_expandNotify_sum">알림에서 재생 버튼이 표시되도록 확장합니다.</string> <string name="pref_persistNotify_title">재생 조작 항상 표시</string> <string name="pref_persistNotify_sum">재생이 일시 중지했을 때에도 알림과 잠금 화면의 조작 기능을 유지합니다.</string> + <string name="pref_compact_notification_buttons_title">간략한 알림 버튼 사용</string> + <string name="pref_compact_notification_buttons_sum">알림이 중첩되었을 경우 재생 버튼을 바꿉니다. 재생/일시정지 단추는 항상 포함됩니다.</string> <string name="pref_compact_notification_buttons_dialog_title">최대 %1$d개 항목 선택</string> <string name="pref_compact_notification_buttons_dialog_error">최대 %1$d개 항목만 선택할 수 있습니다.</string> <string name="pref_lockscreen_background_title">잠금 화면 배경 설정</string> <string name="pref_lockscreen_background_sum">현재 에피소드의 이미지를 잠금 화면의 배경으로 설정합니다. 대신 이는 제3자 앱의 이미지도 표시하게 됩니다.</string> - <string name="pref_showDownloadReport_sum">다운로드가 실패하면, 실패 이유를 자세히 표시하는 보고서를 만듭니다.</string> - <string name="pref_showAutoDownloadReport_sum">자동 다운로드 에피소드 에 대해 알림을 표시합니다.</string> <string name="pref_expand_notify_unsupport_toast">안드로이드 4.1 미만의 버전에서는 확장 알림을 지원하지 않습니다.</string> <string name="pref_enqueue_location_title">대기열 추가 위치</string> <string name="pref_enqueue_location_sum">에피소드 추가 위치: %1$s</string> @@ -426,6 +464,7 @@ <string name="pref_smart_mark_as_played_disabled">사용 안 함</string> <string name="pref_image_cache_size_title">이미지 캐시 크기</string> <string name="pref_image_cache_size_sum">이미지에 사용할 디스크 캐시 크기</string> + <string name="documentation_support">문서 및 지원</string> <string name="visit_user_forum">사용자 포럼</string> <string name="bug_report_title">문제점 보고</string> <string name="open_bug_tracker">버그 추적 사이트 열기</string> @@ -437,14 +476,15 @@ <string name="pref_current_value">현재 값: %1$s</string> <string name="pref_proxy_title">프록시</string> <string name="pref_proxy_sum">네트워크 프록시 설정</string> - <string name="pref_faq">자주 묻는 질문</string> <string name="pref_no_browser_found">웹브라우저가 없습니다.</string> <string name="pref_cast_title">크롬캐스트 지원</string> <string name="pref_cast_message_play_flavor">캐스트 장치의 원격 미디어 재생 기능 사용 (예: 크롬캐스트, 안드로이드 TV의 오디오 스피커)</string> <string name="pref_cast_message_free_flavor">크롬캐스트는 서드파티 라이브러리가 필요하지만, 이 버전의 안테나팟에서는 사용하지 않게 되어 있습니다.</string> <string name="pref_enqueue_downloaded_title">다운로드한 항목 대기열에 추가</string> <string name="pref_enqueue_downloaded_summary">다운로드한 에피소드를 대기열에 추가</string> - <string name="media_player_builtin">내장 안드로이드 플레이어</string> + <string name="media_player_builtin">내장 안드로이드 플레이어 (권장하지 않음) </string> + <string name="media_player_sonic">Sonic Media Player (권장하지 않음) </string> + <string name="media_player_exoplayer_recommended">ExoPlayer (추천)</string> <string name="media_player_switch_to_exoplayer">ExoPlayer로 전환</string> <string name="media_player_switched_to_exoplayer">ExoPlayer로 전환함.</string> <string name="pref_skip_silence_title">오디오에서 묵음 구간 건너뛰기</string> @@ -465,6 +505,15 @@ <string name="back_button_go_to_page_title">페이지 선택</string> <string name="pref_delete_removes_from_queue_title">제거된 항목 대기열에서 삭제</string> <string name="pref_delete_removes_from_queue_sum">삭제된 항목을 대기열에서 자동으로 삭제합니다.</string> + <string name="pref_filter_feed_title">구독 필터</string> + <string name="pref_filter_feed_sum">네비게이션 드로어 및 구독 화면의 구독 내용을 필터링합니다.</string> + <string name="no_filter_label">없음</string> + <string name="subscriptions_are_filtered">구독 필터 적용함.</string> + <string name="subscriptions_counter_greater_zero">0보다 큰 개수</string> + <string name="auto_downloaded">자동 다운로드</string> + <string name="not_auto_downloaded">자동 다운로드 아님</string> + <string name="kept_updated">업데이트 상태로 유지</string> + <string name="not_kept_updated">업데이트 유지 아님</string> <!--About screen--> <string name="about_pref">정보</string> <string name="antennapod_version">안테나팟 버전</string> @@ -499,6 +548,7 @@ <string name="database_export_summary">구독 목록, 재생한 에피소드 목록 및 대기열을 다른 장치의 안테나팟에 옮기기</string> <string name="database_import_summary">안테나팟 데이터베이스를 다른 장치에서 가져오기</string> <string name="opml_import_label">OPML 가져오기</string> + <string name="opml_add_podcast_label">팟캐스트 목록 가져오기 (OPML)</string> <string name="opml_reader_error">OPML 문서를 읽는데 오류가 발생했습니다:</string> <string name="opml_import_error_no_file">파일을 선택하지 않았습니다!</string> <string name="select_all_label">모두 선택</string> @@ -514,12 +564,15 @@ <string name="export_success_sum">내보낸 파일을 다음에 저장했습니다:\n\n%1$s</string> <string name="opml_import_ask_read_permission">OPML 파일을 읽으려면 외부 저장소 접근이 필요합니다</string> <string name="import_select_file">가져올 파일을 선택하십시오</string> + <string name="successful_import_label">가져오기 성공</string> + <string name="import_ok">안테나팟을 다시 시작하려면 확인을 누르십시오</string> <string name="import_no_downgrade">이 데이터베이스는 더 새로운 버전의 안테나팟에서 내보낸 데이터베이스입니다. 현재 설치된 버전의 안테나팟에서는 이 파일을 제대로 처리하지 못할 수도 있습니다.</string> <string name="favorites_export_label">즐겨찾기 내보내기</string> <string name="favorites_export_summary">저장한 즐겨찾기를 파일로 내보내기</string> <!--Sleep timer--> <string name="set_sleeptimer_label">취침 타이머 설정</string> <string name="disable_sleeptimer_label">취침 타이머 사용 않음</string> + <string name="extend_sleep_timer_label">+%d분</string> <string name="sleep_timer_label">취침 타이머</string> <string name="time_dialog_invalid_input">입력이 잘못되었습니다. 시간으로 숫자를 입력해야 합니다.</string> <string name="shake_to_reset_label">흔들어서 타이머 초기화</string> @@ -544,22 +597,22 @@ <string name="gpodnet_suggestions_header">추천</string> <string name="gpodnet_search_hint">gpodder.net 검색</string> <string name="gpodnetauth_login_title">로그인</string> - <string name="gpodnetauth_login_descr">gpodder.net 로그인입니다. 먼저 로그인 정보를 입력하십시오:</string> <string name="gpodnetauth_login_butLabel">로그인</string> - <string name="gpodnetauth_login_register">아직 계정이 없으면 다음에서 만들 수 있습니다:\nhttps://gpodder.net/register/</string> + <string name="create_account">계정 만들기</string> <string name="username_label">사용자 이름</string> <string name="password_label">암호</string> - <string name="gpodnetauth_device_title">장치 선택</string> + <string name="gpodnet_description">gpodder.net은 안테나팟 프로젝트와는 독립적인 오픈소스 팟캐스트 동기화 서비스입니다.</string> + <string name="gpodnetauth_server_official">공식 gpodder.net 서버</string> + <string name="gpodnetauth_server_custom">서버 사용자 지정</string> + <string name="gpodnetauth_host">호스트 이름</string> + <string name="gpodnetauth_select_server">서버 선택</string> <string name="gpodnetauth_device_descr">gpodder.net 계정에서 사용할 장치를 새로 만들거나 기존 장치를 선택하십시오:</string> - <string name="gpodnetauth_device_deviceID">장치 아이디:\u0020</string> - <string name="gpodnetauth_device_caption">설명</string> - <string name="gpodnetauth_device_butCreateNewDevice">새 장치 만들기</string> - <string name="gpodnetauth_device_chooseExistingDevice">기존 장치 선택:</string> - <string name="gpodnetauth_device_errorEmpty">장치 ID는 비어 있으면 안 됩니다</string> - <string name="gpodnetauth_device_errorAlreadyUsed">장치 ID를 이미 사용 중입니다</string> + <string name="gpodnetauth_device_name">장치 이름</string> + <string name="gpodnetauth_device_name_default">안테나팟, %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">자막이 비어 있으면 안 됩니다</string> + <string name="gpodnetauth_existing_devices">기존 장치</string> + <string name="gpodnetauth_create_device">장치 만들기</string> <string name="gpodnetauth_device_butChoose">선택</string> - <string name="gpodnetauth_finish_title">로그인이 성공했습니다!</string> <string name="gpodnetauth_finish_descr">축하합니다! gpodder.net 계정이 장치와 연결되었습니다. 이제 안테나팟에서 gpodder.net 계정의 구독 정보와 자동으로 동기화합니다.</string> <string name="gpodnetauth_finish_butsyncnow">지금 동기화 시작</string> <string name="gpodnetauth_finish_butgomainscreen">메인 화면으로 이동</string> @@ -613,6 +666,7 @@ <string name="switch_pages">페이지 전환</string> <string name="position">위치: %1$s</string> <string name="apply_action">동작 적용</string> + <string name="play_chapter">챕터 재생</string> <!--Feed information screen--> <string name="authentication_label">인증</string> <string name="authentication_descr">이 팟캐스트와 에피소드에 대한 사용자 이름과 비밀번호를 바꿉니다.</string> @@ -625,6 +679,10 @@ <string name="keep_updated">최신 업데이트 유지</string> <string name="keep_updated_summary">모든 팟캐스트를 (자동) 새로 고칠 때 이 팟캐스트 포함</string> <string name="auto_download_disabled_globally">자동 다운로드는 안테나팟 메인 설정에서 꺼져 있습니다</string> + <string name="statistics_listened_for">청취한 시간:</string> + <string name="statistics_episodes_on_device">장치에 들어 있는 에피소드:</string> + <string name="statistics_space_used">사용한 용량:</string> + <string name="statistics_view_all">모든 팟캐스트에 대한 뷰 »</string> <!--Progress information--> <string name="progress_upgrading_database">데이터베이스 업그레이드 중</string> <!--AntennaPodSP--> @@ -632,16 +690,29 @@ <!--Add podcast fragment--> <string name="search_podcast_hint">팟캐스트 검색…</string> <string name="search_itunes_label">iTunes 검색</string> + <string name="search_podcastindex_label">podcastindex.org 검색</string> <string name="search_fyyd_label">fyyd 검색</string> <string name="advanced">고급</string> + <string name="add_podcast_by_url">RSS 주소로 팟캐스트 추가</string> <string name="browse_gpoddernet_label">gpodder.net 둘러보기</string> <string name="discover">발견</string> + <string name="discover_hide">숨기기</string> + <string name="discover_is_hidden">제안 사항을 감추도록 선택했습니다.</string> <string name="discover_more">더 보기 »</string> + <string name="discover_powered_by_itunes">iTunes 추천</string> + <string name="search_powered_by">%1$s 검색 결과</string> <!--Local feeds--> + <string name="add_local_folder">로컬 폴더 추가</string> + <string name="local_folder">로컬 폴더</string> + <string name="reconnect_local_folder">로컬 폴더 다시 연결</string> + <string name="reconnect_local_folder_warning">권한이 거부된 경우, 이 기능을 사용해 정확히 같은 폴더에 다시 연결할 수 있습니다. 다른 폴더를 선택하지 마십시오.</string> + <string name="local_feed_description">이 가상 팟캐스트는 안테나팟에 폴더를 추가해 만들었습니다.</string> + <string name="unable_to_start_system_file_manager">시스템 파일 관리자를 시작할 수 없습니다</string> <string name="filter">필터</string> <!--Episodes apply actions--> <string name="all_label">모두</string> <string name="selected_all_label">모든 에피소드 선택함</string> + <string name="select_none_label">없음</string> <string name="deselected_all_label">모든 에피소드 선택 해제함</string> <string name="played_label">재생함</string> <string name="selected_played_label">재생한 에피소드 선택함</string> @@ -655,13 +726,17 @@ <string name="selected_not_queued_label">대기열에 없는 에피소드 선택함</string> <string name="selected_has_media_label">미디어가 있는 에피소드 선택함</string> <string name="hide_is_favorite_label">즐겨찾기 포함</string> + <string name="not_favorite">즐겨찾기 아님</string> <string name="hide_downloaded_episodes_label">다운로드함</string> <string name="hide_not_downloaded_episodes_label">다운로드 안 함</string> <string name="queued_label">대기열에 있음</string> <string name="not_queued_label">대기열에 없음</string> <string name="has_media">미디어 있음</string> + <string name="no_media">미디어 없음</string> <string name="hide_paused_episodes_label">일시 중지함</string> + <string name="not_paused">일시 중지 아님</string> <string name="hide_played_episodes_label">재생함</string> + <string name="not_played">재생함 아님</string> <!--Sort--> <string name="sort_title_a_z">제목 (A \u2192 Z)</string> <string name="sort_title_z_a">제목 (Z \u2192 A)</string> @@ -682,6 +757,11 @@ <string name="rating_later_label">나중에 알림</string> <string name="rating_now_label">해봅시다!</string> <!--Share episode dialog--> + <string name="share_dialog_include_label">포함:</string> + <string name="share_playback_position_dialog_label">재생 위치</string> + <string name="share_dialog_media_file_url_label">미디어 파일 주소</string> + <string name="share_dialog_episode_website_label">에피소드 웹페이지</string> + <string name="share_dialog_media_file_label">미디어 파일</string> <!--Audio controls--> <string name="audio_controls">오디오 조작</string> <string name="playback_speed">재생 속도</string> @@ -721,15 +801,22 @@ <string name="cast_failed_receiver_player_error">리시버 플레이어에서 심각한 오류가 발생했습니다</string> <string name="cast_failed_media_error_skipping">미디어 재생에 오류. 건너뜁니다...</string> <!--Notification channels--> + <string name="notification_group_errors">오류</string> + <string name="notification_group_news">뉴스</string> <string name="notification_channel_user_action">사용자 조작 필요</string> <string name="notification_channel_user_action_description">사용자 조작이 필요할 때 표시됩니다. 예를 들어 암호를 입력해야 할 때 표시됩니다.</string> <string name="notification_channel_downloading">다운로드 중</string> <string name="notification_channel_downloading_description">현재 다운로드 중일 때 표시됩니다.</string> <string name="notification_channel_playing">현재 재생 중</string> <string name="notification_channel_playing_description">재생을 조작할 수 있습니다 .팟캐스트를 재생할 때 볼 수 있는 메인 알림입니다.</string> - <string name="notification_channel_error">오류</string> - <string name="notification_channel_auto_download">자동 다운로드</string> + <string name="notification_channel_download_error">다운로드 실패</string> + <string name="notification_channel_download_error_description">다운로드 또는 피드 업데이트가 실패할 때 표시.</string> + <string name="notification_channel_sync_error">동기화 실패</string> + <string name="notification_channel_sync_error_description">gpodder 동기화가 실패할 때 표시.</string> + <string name="notification_channel_auto_download">자동 다운로드 마침</string> <string name="notification_channel_episode_auto_download">에피소드를 자동으로 다운로드했을 때 표시됩니다.</string> + <string name="notification_channel_new_episode">새 에피소드</string> + <string name="notification_channel_new_episode_description">알림 기능을 켰을 경우, 팟캐스트의 새 에피소드를 찾으면 표시됩니다.</string> <!--Widget settings--> <string name="widget_settings">위젯 설정</string> <string name="widget_create_button">위젯 만들기</string> diff --git a/core/src/main/res/values-lt/strings.xml b/core/src/main/res/values-lt/strings.xml index fac693b25..a55a193c7 100644 --- a/core/src/main/res/values-lt/strings.xml +++ b/core/src/main/res/values-lt/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statistika</string> <string name="add_feed_label">Pridėti tinklalaidę</string> <string name="episodes_label">Epizodai</string> + <string name="queue_label">Eilė</string> <string name="all_episodes_short_label">Visi</string> <string name="new_episodes_label">Nauji</string> <string name="favorite_episodes_label">Mėgstami</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">Žurnalas</string> <string name="subscriptions_label">Prenumeratos</string> <string name="subscriptions_list_label">Prenumeratų sąrašas</string> - <string name="cancel_download_label">Atšaukti\natsiuntimą</string> <string name="playback_history_label">Atkūrimo istorija</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net prisijungimas</string> @@ -25,6 +25,7 @@ <string name="episode_cache_full_message">Pasiektas epizodų podėlio dydžio limitas. Nustatymuose galite padidinti podėlio dydį.</string> <string name="playback_statistics_label">Atkūrimas</string> <string name="download_statistics_label">Atsiuntimai</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="statistics_details_dialog">Paleisti %1$d iš %2$d epizodų.\n\nPerklausyta %3$s iš %4$s.</string> <string name="statistics_mode">Statistikos režimas</string> @@ -75,7 +76,6 @@ <string name="description_label">Aprašymas</string> <string name="episodes_suffix">\u0020epizodai</string> <string name="processing_label">Apdorojama</string> - <string name="save_username_password_label">Išsaugoti vartotojo vardą ir slaptažodį</string> <string name="close_label">Užverti</string> <string name="retry_label">Bandyti vėl</string> <string name="auto_download_label">Įtraukti į automatinius atsiuntimus</string> @@ -87,7 +87,6 @@ <string name="feed_volume_reduction_off">Išjungta</string> <string name="feed_volume_reduction_light">Švelniai</string> <string name="feed_volume_reduction_heavy">Gausiai</string> - <string name="parallel_downloads_suffix">\u0020lygiagretūs atsiuntimai</string> <string name="feed_auto_download_global">Globali numatytoji</string> <string name="feed_auto_download_always">Visada</string> <string name="feed_auto_download_never">Niekada</string> @@ -140,7 +139,6 @@ <string name="hide_not_queued_episodes_label">Nesantys eilėje</string> <string name="hide_has_media_label">Turintys medijos failų</string> <string name="filtered_label">Filtruota</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Paskutinis atnaujinimas nepavyko</string> <string name="open_podcast">Atverti tinklalaidę</string> <string name="please_wait_for_data">Prašome luktelėti, kol duomenys bus įkelti</string> <!--actions on feeditems--> @@ -211,16 +209,12 @@ <string name="download_error_details">Išsami informacija</string> <string name="download_error_details_message">%1$s \n\nFailo URL:\n%2$s</string> <string name="download_error_device_not_found">Nerastas laikmenos įrenginys</string> - <string name="download_error_insufficient_space">Trūksta laisvos vietos</string> <string name="download_error_http_data_error">HTTP duomenų klaida</string> <string name="download_error_error_unknown">Nežinoma klaida</string> - <string name="download_error_parser_exception">Išimtinė situacija analizatoriuje</string> <string name="download_error_unsupported_type">Nepalaikomas sklaidos kanalo tipas</string> <string name="download_error_connection_error">Susijungimo klaida</string> - <string name="download_error_unknown_host">Nežinomas serveris</string> <string name="download_error_unauthorized">Tapatumo nustatymo klaida</string> <string name="download_error_file_type_type">Failo tipo klaida</string> - <string name="download_error_forbidden">Uždrausta</string> <string name="download_canceled_msg">Atsiuntimas atšauktas</string> <string name="download_canceled_autodownload_enabled_msg">Atsiuntimas atšauktas\n<i>Automatinis atsiuntimas</i> šiam elementui išjungtas</string> <string name="download_report_title">Atsiuntimai užbaigti su klaida (-omis)</string> @@ -236,7 +230,6 @@ <item quantity="many">Liko %d atsiuntimų</item> <item quantity="other">Liko %d atsiuntimų</item> </plurals> - <string name="downloads_processing">Apdorojami atsiuntimai</string> <string name="download_notification_title">Atsiunčiami tinklalaidės duomenys</string> <string name="download_log_title_unknown">Nežinomas pavadinimas</string> <string name="download_type_feed">Sklaidos kanalas</string> @@ -322,7 +315,6 @@ <string name="storage_pref">Laikmena</string> <string name="storage_sum">Automatinis epizodų trynimas, importas, eksportas</string> <string name="project_pref">Projektas</string> - <string name="queue_label">Eilė</string> <string name="synchronization_pref">Sinchronizavimas</string> <string name="synchronization_sum">Sinchronizuoti su kitais įrenginiais naudojantis „gpodder.net“</string> <string name="automation">Automatizacija</string> @@ -338,14 +330,9 @@ <string name="preference_search_clear_history">Išvalyti istoriją</string> <string name="media_player">Medijos grotuvas</string> <string name="pref_episode_cleanup_title">Epizodų valymas</string> - <string name="pref_episode_cleanup_summary">Epizodai, nesantys eilėje ar tarp mėgstamųjų, gali būti ištrinti automatinio atsiuntimo metu pritrūkus laisvos vietos naujiems epizodams </string> <string name="pref_pauseOnDisconnect_sum">Pristabdyti atkūrimą, kai atjungiamos ausinės ar „Bluetooth“</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Pratęsti atkūrimą, kai ausinės pakartotinai prijungiamos</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Pratęsti atkūrimą, kai pakartotinai prisijungiama prie „Bluetooth“</string> - <string name="pref_hardwareForwardButtonSkips_title">Mygtukas „pirmyn“ peršoka epizodą</string> - <string name="pref_hardwareForwardButtonSkips_sum">Paspaudus „Bluetooth“ įrenginio mygtuką „pirmyn“ peršokti į kitą epizodą vietoje greito persukimo į priekį.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Mygtukas „ankstesnis“ paleidžia iš naujo</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Paspaudus aparatinį mygtuką „ankstesnis“ paleisti dabartinį epizodą nuo pradžių vietoj epizodo peršokimo</string> <string name="pref_followQueue_sum">Atkūrimui pasibaigus peršokti į kitą eilės elementą</string> <string name="pref_auto_delete_sum">Ištrinti epizodą pasibaigus atkūrimui</string> <string name="pref_auto_delete_title">Automatinis ištrynimas</string> @@ -365,7 +352,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Išjungti</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Nustatyti intervalą</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Nustatyti dienos metą</string> - <string name="pref_autoUpdateIntervallOrTime_every">kas %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">lygiai %1$s</string> <string name="pref_followQueue_title">Nenutrūkstamas atkūrimas</string> <string name="pref_unpauseOnHeadsetReconnect_title">Pakartotinai prijungus ausines</string> @@ -398,7 +384,6 @@ <string name="pref_episode_cache_title">Epizodų podėlis</string> <string name="pref_episode_cache_summary">Bendras podėlyje atsiųstų epizodų skaičius šiame įrenginyje. Pasiekus šį skaičių automatinis atsiuntimas bus pristabdytas.</string> <string name="pref_episode_cover_title">Naudoti epizodo viršelį</string> - <string name="pref_episode_cover_summary">Naudoti epizodo viršelį, kai prieinama. Nepažymėjus, programėlė visada naudos tinklalaidės viršelio paveikslėlį.</string> <string name="pref_theme_title_use_system">Naudoti sistemos temą</string> <string name="pref_theme_title_light">Šviesi</string> <string name="pref_theme_title_dark">Tamsi</string> @@ -418,7 +403,6 @@ <string name="pref_gpodnet_full_sync_title">Priverstinis pilnas sinchronizavimas</string> <string name="pref_gpodnet_full_sync_sum">Sinchronizuoti visas prenumeratas bei epizodų būsenas su „gpodder.net“.</string> <string name="pref_gpodnet_login_status"><![CDATA[Prisijungta kaip <i>%1$s</i> naudojant įrenginį <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_sum">Šis nustatymas negalioja tapatumo nustatymo klaidoms.</string> <string name="pref_feed_playback_speed_sum">Atkūrimo sparta, naudojama pradedant šios tinklalaidės epizodų atkūrimą</string> <string name="pref_feed_skip">Automatinis praleidimas</string> <string name="pref_feed_skip_sum">Praleisti įvadą ir pabaigos žodį.</string> @@ -432,8 +416,6 @@ <string name="pref_fast_forward_sum">Derinkite, per kiek sekundžių šoktelėti į priekį kai paspaudžiamas persukimo į priekį mygtukas</string> <string name="pref_rewind">Atsukimo atgal trukmė</string> <string name="pref_rewind_sum">Derinkite, per kiek sekundžių šoktelėti atgal kai paspaudžiamas atsukimo atgal mygtukas</string> - <string name="pref_gpodnet_sethostname_title">Nustatyti serverį</string> - <string name="pref_gpodnet_sethostname_use_default_host">Naudoti numatytąjį serverį</string> <string name="pref_expandNotify_title">Aukštas pranešimų prioritetas</string> <string name="pref_expandNotify_sum">Tai dažniausiai išskleidžia pranešimą, kad būtų rodomi atkūrimo valdymo mygtukai.</string> <string name="pref_persistNotify_title">Pastovūs atkūrimo valdikliai</string> @@ -442,8 +424,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Galite pasirinkti daugiausiai %1$d elementus.</string> <string name="pref_lockscreen_background_title">Nustatyti ekrano užrakto foną</string> <string name="pref_lockscreen_background_sum">Atkuriamo epizodo paveikslėlį naudoti kaip ekrano užrakto foną. Paveikslėlis taip pat bus matomas trečiųjų šalių programėlėse.</string> - <string name="pref_showDownloadReport_sum">Atsiuntimui nepavykus, sukurti ataskaitą su išsamiu klaidų aprašymu.</string> - <string name="pref_showAutoDownloadReport_sum">Rodyti pranešimą automatiškai atsiuntus epizodus.</string> <string name="pref_expand_notify_unsupport_toast">Ankstesnės nei 4.1 „Android“ versijos nepalaiko išplėstų programos pranešimų.</string> <string name="pref_enqueue_location_title">Pridėjimo į eilę vieta</string> <string name="pref_enqueue_location_sum">Epizodus pridėti: %1$s</string> @@ -464,14 +444,12 @@ <string name="pref_current_value">Dabartinė reikšmė: %1$s</string> <string name="pref_proxy_title">Įgaliotasis serveris</string> <string name="pref_proxy_sum">Nustatyti įgaliotąjį tinklo serverį</string> - <string name="pref_faq">Dažniausiai užduodami klausimai</string> <string name="pref_no_browser_found">Nerasta jokia interneto naršyklė.</string> <string name="pref_cast_title">„Chromecast“ palaikymas</string> <string name="pref_cast_message_play_flavor">Įjungti nuotolinio medijos atkūrimo „Cast“ įrenginiuose (pvz. „Chromecast“, garso kolonėlės ar „Android TV“) palaikymą</string> <string name="pref_cast_message_free_flavor">„Chromecast“ palaikymui reikalingos nuosavybinės trečiųjų šalių bibliotekos, kurios yra negalimos šioje „AntennaPod“ versijoje.</string> <string name="pref_enqueue_downloaded_title">Atsiuntimus pridėti į eilę</string> <string name="pref_enqueue_downloaded_summary">Parsiuntus epizodus, pridėti juos į eilę</string> - <string name="media_player_builtin">Įtaisytoji „Android“ leistuvė</string> <string name="media_player_switch_to_exoplayer">Perjungti į „ExoPlayer“</string> <string name="media_player_switched_to_exoplayer">Perjungta į „ExoPlayer“.</string> <string name="pref_skip_silence_title">Praleisti tylą</string> @@ -580,22 +558,12 @@ <string name="gpodnet_suggestions_header">PASIŪLYMAI</string> <string name="gpodnet_search_hint">Ieškoti „gpodder.net“ svetainėje</string> <string name="gpodnetauth_login_title">Prisijungti</string> - <string name="gpodnetauth_login_descr">Sveiki! Tai prisijungimo prie „gpodder.net“ vedlys. Visų pirma, įveskite savo prisijungimo duomenis:</string> <string name="gpodnetauth_login_butLabel">Prisijungti</string> - <string name="gpodnetauth_login_register">Jei dar neturite paskyros, galite susikurti ją čia:\nhttps://gpodder.net/register/</string> <string name="username_label">Vartotojo vardas</string> <string name="password_label">Slaptažodis</string> - <string name="gpodnetauth_device_title">Įrenginio pasirinkimas</string> <string name="gpodnetauth_device_descr">Sukurkite naują įrenginį savo „gpodder.net“ paskyrai arba pasirinkite esamą:</string> - <string name="gpodnetauth_device_deviceID">Įrenginio ID:\u0020</string> - <string name="gpodnetauth_device_caption">Pavadinimas</string> - <string name="gpodnetauth_device_butCreateNewDevice">Sukurti naują įrenginį</string> - <string name="gpodnetauth_device_chooseExistingDevice">Pasirinkti esamą įrenginį:</string> - <string name="gpodnetauth_device_errorEmpty">Įrenginio ID laukelis negali būti tuščias</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Toks įrenginio ID jau naudojamas</string> <string name="gpodnetauth_device_caption_errorEmpty">Pavadinimas negali būti tuščias</string> <string name="gpodnetauth_device_butChoose">Pasirinkti</string> - <string name="gpodnetauth_finish_title">Sėkmingai prisijungta!</string> <string name="gpodnetauth_finish_descr">Sveikiname! Jūsų „gpodder.net“ paskyra susieta su šiuo įrenginiu. Nuo šiol „AntennaPod“ automatiškai sinchronizuos prenumeratas šiame įrenginyje su Jūsų „gpodder.net“ paskyra.</string> <string name="gpodnetauth_finish_butsyncnow">Pradėti sinchronizavimą dabar</string> <string name="gpodnetauth_finish_butgomainscreen">Eiti į pagrindinį ekraną</string> @@ -763,8 +731,6 @@ <string name="notification_channel_downloading_description">Rodomas atsiuntimo metu.</string> <string name="notification_channel_playing">Šiuo metu atkuriama</string> <string name="notification_channel_playing_description">Leidžia valdyti atkūrimą. Tai pagrindinis pranešimas matomas tinklalaidės atkūrimo metu.</string> - <string name="notification_channel_error">Klaidos</string> - <string name="notification_channel_auto_download">Automatiniai atsiuntimai</string> <string name="notification_channel_episode_auto_download">Rodomas automatiškai atsiuntus epizodus.</string> <!--Widget settings--> <string name="widget_settings">Valdiklio nustatymai</string> diff --git a/core/src/main/res/values-nb/strings.xml b/core/src/main/res/values-nb/strings.xml index eb653afd2..bf8c9cae6 100644 --- a/core/src/main/res/values-nb/strings.xml +++ b/core/src/main/res/values-nb/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statistikk</string> <string name="add_feed_label">Legg til podkast</string> <string name="episodes_label">Episoder</string> + <string name="queue_label">Kø</string> <string name="all_episodes_short_label">Alle</string> <string name="new_episodes_label">Nye</string> <string name="favorite_episodes_label">Favoritter</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">Logg</string> <string name="subscriptions_label">Abonnementer</string> <string name="subscriptions_list_label">Abonnementliste</string> - <string name="cancel_download_label">Avbryt\nLast ned</string> <string name="playback_history_label">Avspillingshistorikk</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net-innlogging</string> @@ -26,6 +26,7 @@ <string name="playback_statistics_label">Avspilling</string> <string name="download_statistics_label">Nedlastinger</string> <string name="notification_pref_fragment">Varslinger</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Total tid, avspilte episoder</string> <string name="statistics_details_dialog">Startet %1$d av %2$d episoder.\n\nAvspilt %3$s av %4$s.</string> @@ -81,7 +82,6 @@ <string name="description_label">Beskrivelse</string> <string name="episodes_suffix">\u0020episoder</string> <string name="processing_label">Behandler</string> - <string name="save_username_password_label">Lagre brukernavn og passord</string> <string name="close_label">Lukk</string> <string name="retry_label">Prøv igjen</string> <string name="auto_download_label">Inkluder i automatiske nedlastninger</string> @@ -93,7 +93,6 @@ <string name="feed_volume_reduction_off">Av</string> <string name="feed_volume_reduction_light">Lett</string> <string name="feed_volume_reduction_heavy">Tung</string> - <string name="parallel_downloads_suffix">\u0020samtidige nedlastinger</string> <string name="feed_auto_download_global">Global standard</string> <string name="feed_auto_download_always">Alltid</string> <string name="feed_auto_download_never">Aldri</string> @@ -140,7 +139,6 @@ <string name="hide_not_queued_episodes_label">Ikke i kø</string> <string name="hide_has_media_label">Har medier</string> <string name="filtered_label">Filtrert</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Siste oppdatering mislyktes</string> <string name="open_podcast">Åpne podkast</string> <string name="please_wait_for_data">Vent til dataene er lastet inn</string> <!--actions on feeditems--> @@ -201,16 +199,12 @@ <string name="download_error_details">Detaljer</string> <string name="download_error_details_message">%1$s \n\nFil-URL:\n%2$s</string> <string name="download_error_device_not_found">Lagringsenhet ikke funnet</string> - <string name="download_error_insufficient_space">Ikke nok plass</string> <string name="download_error_http_data_error">HTTP-datafeil</string> <string name="download_error_error_unknown">Ukjent feil</string> - <string name="download_error_parser_exception">Parser-unntak</string> <string name="download_error_unsupported_type">Strøm-typen er ikke støttet</string> <string name="download_error_connection_error">Tilkoblingsfeil</string> - <string name="download_error_unknown_host">Ukjent vert</string> <string name="download_error_unauthorized">Autentiseringsfeil</string> <string name="download_error_file_type_type">Filtype-feil</string> - <string name="download_error_forbidden">Ikke tillatt</string> <string name="download_canceled_msg">Nedlasting avbrutt</string> <string name="download_canceled_autodownload_enabled_msg">Nedlasting avbrutt\n<i>Automatisk nedlasting</i> for dette elementet er deaktivert</string> <string name="download_report_title">Nedlasting fullført med feilmeldinger</string> @@ -224,12 +218,7 @@ <item quantity="one">%d nedlasting gjenstår</item> <item quantity="other">%d nedlastinger gjenstår</item> </plurals> - <string name="downloads_processing">Behandler nedlastninger</string> <string name="download_notification_title">Laster ned data til podkast</string> - <plurals name="download_report_content"> - <item quantity="one">%1$d nedlastninger lyktes, %2$d mislyktes</item> - <item quantity="other">%d nedlastinger lyktes, %d mislyktes</item> - </plurals> <string name="download_log_title_unknown">Ukjent tittel</string> <string name="download_type_feed">Strøm</string> <string name="download_type_media">Mediafil</string> @@ -318,7 +307,6 @@ <string name="storage_pref">Lagring</string> <string name="storage_sum">Auto-slett episoder, importer, eksporter</string> <string name="project_pref">Prosjekt</string> - <string name="queue_label">Kø</string> <string name="synchronization_pref">Synkronisering</string> <string name="synchronization_sum">Synkroniser med andre enheter over gpodder.net</string> <string name="automation">Automasjon</string> @@ -334,14 +322,9 @@ <string name="preference_search_clear_history">Slett logg</string> <string name="media_player">Mediespiller</string> <string name="pref_episode_cleanup_title">Episodeopprydding</string> - <string name="pref_episode_cleanup_summary">Episoder som ikke er i køen og ikke er favoritter skal kunne fjernes hvis auto-nedlasting trenger plass til nye episoder</string> <string name="pref_pauseOnDisconnect_sum">Sett playback på pause når hodetelefoner eller bluetooth er frakoblet</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Gjenoppta avspilling når hodetelefoner gjeninnkoples</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Fortsett avspilling når bluetooth er tilkoblet igjen</string> - <string name="pref_hardwareForwardButtonSkips_title">\"Fremover\" hopper over</string> - <string name="pref_hardwareForwardButtonSkips_sum">Hopp til neste episode i stedet for å spole når du trykker på \"Fremover\"-knappen</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Forriv</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Start episoden på nytt i stedet for å spole når du trykker på \"Tilbake\"-knappen</string> <string name="pref_followQueue_sum">Hopp til neste element i køen når avspillingen er ferdig</string> <string name="pref_auto_delete_sum">Slett episode når avspillingen er ferdig</string> <string name="pref_auto_delete_title">Automatisk sletting</string> @@ -361,7 +344,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Skru av</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Sett intervall</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Angi klokkeslett</string> - <string name="pref_autoUpdateIntervallOrTime_every">hver %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">ved %1$s</string> <string name="pref_followQueue_title">Kontinuerlig avspilling</string> <string name="pref_pauseOnHeadsetDisconnect_title">Koblet fra hodetelefoner eller Bluetooth</string> @@ -396,7 +378,6 @@ <string name="pref_episode_cache_title">Mellomlager for episoder</string> <string name="pref_episode_cache_summary">Totalt antall nedlastede episoder bufret på enheten. Automatisk nedlasting vil bli stoppet hvis dette nummeret er nådd.</string> <string name="pref_episode_cover_title">Bruk episode-cover</string> - <string name="pref_episode_cover_summary">Bruk et episode-spesifikt cover hvis det er tilgjengelig. Hvis dette ikke er valgt vil appen alltid bruke podkastens cover-bilde.</string> <string name="pref_theme_title_use_system">Bruk systemets tema</string> <string name="pref_theme_title_light">Lyst</string> <string name="pref_theme_title_dark">Mørkt</string> @@ -416,8 +397,6 @@ <string name="pref_gpodnet_full_sync_title">Tving fram full synkronisering</string> <string name="pref_gpodnet_full_sync_sum">Synkroniser alle abonnement og episoder med gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Logget inn som <i>%1$s</i> med enhet <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Synkronisering feilet</string> - <string name="pref_gpodnet_notifications_sum">Denne instillingen gjelder ikke autentiseringfeil.</string> <string name="pref_feed_playback_speed_sum">Hastigheten som brukes når episoder av denne podkasten spilles av</string> <string name="pref_feed_skip">Automatisk hopp</string> <string name="pref_feed_skip_sum">Hopp over introer og \"rulletekster\"</string> @@ -431,8 +410,6 @@ <string name="pref_fast_forward_sum">Velg hvor mange sekunder som skal hoppes når du trykker på \"Spol fremover\"-knappen</string> <string name="pref_rewind">Hopp tilbake</string> <string name="pref_rewind_sum">Velg hvor mange sekunder som skal hoppes tilbake når \"Spol tilbake\"-knappen trykkes</string> - <string name="pref_gpodnet_sethostname_title">Sett vertsnavn</string> - <string name="pref_gpodnet_sethostname_use_default_host">Bruk standard vert</string> <string name="pref_expandNotify_title">Høy varsling-prioritet</string> <string name="pref_expandNotify_sum">Dette utvider som regel varslingen for å vise kontroller.</string> <string name="pref_persistNotify_title">Vedvarende avspillingskontroller</string> @@ -442,9 +419,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Du kan kun velge opp til %1$d ting.</string> <string name="pref_lockscreen_background_title">Angi som bakgrunn på låseskjermen</string> <string name="pref_lockscreen_background_sum">Angir låseskjermbakgrunnsbildet til å være den nåværende episodens bilde. Som en sideeffekt vil dette også vise bildet i tredjepartsapper.</string> - <string name="pref_showDownloadReport_sum">Generer en rapport som viser detaljer dersom nedlastinger feiler.</string> - <string name="pref_showAutoDownloadReport_title">Automatisk nedlasting fullført</string> - <string name="pref_showAutoDownloadReport_sum">Vis en varsling for automatisk nedlastede episoder</string> <string name="pref_expand_notify_unsupport_toast">Android-versjoner tidligere enn 4.1 støtter ikke utvidede varsler.</string> <string name="pref_enqueue_location_title">Plassering i køen</string> <string name="pref_enqueue_location_sum">Legg episoder til på: %1$s</string> @@ -465,14 +439,12 @@ <string name="pref_current_value">Valgt: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Velg en nettverk-proxy</string> - <string name="pref_faq">FAQ (Ofte stilte spørsmål)</string> <string name="pref_no_browser_found">Ingen nettleser funnet.</string> <string name="pref_cast_title">Chromecast støtte</string> <string name="pref_cast_message_play_flavor">Aktiver støtte for fjern-avspilling på Cast-enheter (som Chromecast, høyttalere eller Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast krever proprietær tredjeparts programvare som er deaktivert i denne utgaven av AntennaPod</string> <string name="pref_enqueue_downloaded_title">Legg til nedlastede i køen</string> <string name="pref_enqueue_downloaded_summary">Legg til nedlastede episoder i køen</string> - <string name="media_player_builtin">Innebygd Android-spiller</string> <string name="media_player_switch_to_exoplayer">Bytt til ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Byttet til ExoPlayer</string> <string name="pref_skip_silence_title">Hopp over stillhet</string> @@ -565,22 +537,12 @@ <string name="gpodnet_suggestions_header">FORSLAG</string> <string name="gpodnet_search_hint">Søk på gpodder.net</string> <string name="gpodnetauth_login_title">Logg inn</string> - <string name="gpodnetauth_login_descr">Velkommen til gpodder.net innlogginsprosess. Først begynner vi med å skrive inn innlogginsinformasjon.</string> <string name="gpodnetauth_login_butLabel">Logg inn</string> - <string name="gpodnetauth_login_register">Dersom du ikke har en konto enda kan du opprette en her:\nhttps://gpodder.net/register/</string> <string name="username_label">Brukernavn</string> <string name="password_label">Passord</string> - <string name="gpodnetauth_device_title">Enhetsvalg</string> <string name="gpodnetauth_device_descr">Lag en ny enhet til å bruke for din gpodder.net konto eller velg en som allerede eksisterer.</string> - <string name="gpodnetauth_device_deviceID">EnhetsID:\u0020</string> - <string name="gpodnetauth_device_caption">Tekst</string> - <string name="gpodnetauth_device_butCreateNewDevice">Lag en ny enhet</string> - <string name="gpodnetauth_device_chooseExistingDevice">Velg eksisterende enhet:</string> - <string name="gpodnetauth_device_errorEmpty">Device ID kan ikke være tom</string> - <string name="gpodnetauth_device_errorAlreadyUsed">EnhetsID er allerede i bruk</string> <string name="gpodnetauth_device_caption_errorEmpty">Tittel kan ikke være tom</string> <string name="gpodnetauth_device_butChoose">Velg</string> - <string name="gpodnetauth_finish_title">Innlogging lyktes.</string> <string name="gpodnetauth_finish_descr">Gratulerer! Din gpodder.net konto er nå linket opp med din enhet. AntennaPod vil nå automatisk synkronisere abonnementer på din enhet med din gpodder.net konto.</string> <string name="gpodnetauth_finish_butsyncnow">Start synkronisering nå.</string> <string name="gpodnetauth_finish_butgomainscreen">Gå til hovedskjermen</string> @@ -751,10 +713,7 @@ <string name="notification_channel_downloading_description">Vises mens nedlasting foregår.</string> <string name="notification_channel_playing">Spilles nå</string> <string name="notification_channel_playing_description">Kan styrre avspilling. Dette er hoved-varslingen du vil se mens en podkast spilles.</string> - <string name="notification_channel_error">Feil</string> - <string name="notification_channel_sync_error">Feil ved synkronisering</string> <string name="notification_channel_sync_error_description">Vises når synkronisering mot gpodder feiler</string> - <string name="notification_channel_auto_download">Automatiske nedlastinger</string> <string name="notification_channel_episode_auto_download">Vises når episoder har blitt lastet ned automatisk</string> <!--Widget settings--> <string name="widget_settings">Widget-innstillinger</string> diff --git a/core/src/main/res/values-nl/strings.xml b/core/src/main/res/values-nl/strings.xml index 6cdd59e77..48ce0c5e2 100644 --- a/core/src/main/res/values-nl/strings.xml +++ b/core/src/main/res/values-nl/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statistieken</string> <string name="add_feed_label">Podcast toevoegen</string> <string name="episodes_label">Afleveringen</string> + <string name="queue_label">Wachtrij</string> <string name="all_episodes_short_label">Alle</string> <string name="new_episodes_label">Nieuw</string> <string name="favorite_episodes_label">Favorieten</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Logboek</string> <string name="subscriptions_label">Abonnementen</string> <string name="subscriptions_list_label">Abonnementenlijst</string> - <string name="cancel_download_label">Download\nafbreken</string> + <string name="cancel_download_label">Download afbreken</string> <string name="playback_history_label">Afspeelgeschiedenis</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">Inloggen op gpodder.net</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Afspelen</string> <string name="download_statistics_label">Downloads</string> <string name="notification_pref_fragment">Meldingen</string> + <!--Google Assistant--> + <string name="app_action_not_found">\'%1$s\' is niet aangetroffen</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Totaalduur van afgespeelde afleveringen:</string> <string name="statistics_details_dialog">%1$d van %2$d afleveringen gestart.\n\n%3$s van %4$s afgespeeld.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Geen</string> <!--Bug report activity--> <string name="log_file_share_exception">Geen compatibele apps aangetroffen</string> + <string name="export_logs_menu_title">Uitgebreide logboeken exporteren</string> + <string name="confirm_export_log_dialog_message">Uitgebreide logboeken kunnen gevoelige informatie bevatten, zoals de abonnementenlijst</string> <!--Webview actions--> <string name="open_in_browser_label">Openen in browser</string> <string name="copy_url_label">URL kopiëren</string> @@ -81,7 +86,6 @@ <string name="description_label">Omschrijving</string> <string name="episodes_suffix">\u0020afleveringen</string> <string name="processing_label">Bezig met verwerken...</string> - <string name="save_username_password_label">Gebruikersnaam en wachtwoord opslaan</string> <string name="close_label">Sluiten</string> <string name="retry_label">Opnieuw</string> <string name="auto_download_label">Meenemen bij automatisch downloaden</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">Uit</string> <string name="feed_volume_reduction_light">Gematigd</string> <string name="feed_volume_reduction_heavy">Aanzienlijk</string> - <string name="parallel_downloads_suffix">\u0020gelijktijdige downloads</string> + <string name="parallel_downloads">%1$d gelijktijdige downloads</string> <string name="feed_auto_download_global">Standaardinstelling</string> <string name="feed_auto_download_always">Altijd</string> <string name="feed_auto_download_never">Nooit</string> <string name="send_label">Versturen...</string> <string name="episode_cleanup_never">Nooit</string> + <string name="episode_cleanup_except_favorite_removal">Indien niet in favorieten</string> <string name="episode_cleanup_queue_removal">Indien niet in wachtrij</string> <string name="episode_cleanup_after_listening">Als aflevering volledig is afgespeeld</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -113,7 +118,22 @@ <item quantity="one">%d geselecteerd</item> <item quantity="other">%d geselecteerd</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d aflevering</item> + <item quantity="other">%d afleveringen</item> + </plurals> <string name="loading_more">Bezig met laden...</string> + <string name="episode_notification">Afleveringsmeldingen</string> + <string name="episode_notification_summary">Toon een melding als er een nieuwe aflevering is uitgebracht.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">Er zijn één nieuwe aflevering van %2$s</item> + <item quantity="other">Er zijn %1$d nieuwe afleveringen van %2$s</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Nieuwe aflevering</item> + <item quantity="other">Nieuwe afleveringen</item> + </plurals> + <string name="new_episode_notification_group_text">Er zijn nieuwe afleveringen beschikbaar.</string> <!--Actions on feeds--> <string name="mark_all_read_label">Alles als afgespeeld markeren</string> <string name="mark_all_read_msg">Alle afleveringen zijn gemarkeerd als afgespeeld</string> @@ -145,7 +165,6 @@ <string name="hide_not_queued_episodes_label">Niet in de wachtrij</string> <string name="hide_has_media_label">Bevat media</string> <string name="filtered_label">Gefilterd</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Vorige verversing mislukt</string> <string name="open_podcast">Podcast openen</string> <string name="please_wait_for_data">Wacht tot de gegevens geladen zijn</string> <!--actions on feeditems--> @@ -160,6 +179,10 @@ <string name="delete_label">Verwijderen</string> <string name="delete_failed">Kan bestand niet verwijderen; start je apparaat opnieuw op.</string> <string name="delete_episode_label">Aflevering verwijderen</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d geselecteerde aflevering - %d download verwijderd</item> + <item quantity="other">%d geselecteerde afleveringen - %d downloads verwijderd</item> + </plurals> <string name="remove_new_flag_label">\'Nieuw\'-label verwijderen</string> <string name="removed_new_flag_label">\'Nieuw\'-label is verwijderd</string> <string name="mark_read_label">Als afgespeeld markeren</string> @@ -206,16 +229,12 @@ <string name="download_error_details">Details</string> <string name="download_error_details_message">%1$s \n\nURL van bestand:\n%2$s</string> <string name="download_error_device_not_found">Opslagmedium niet aangetroffen</string> - <string name="download_error_insufficient_space">Onvoldoende ruimte</string> <string name="download_error_http_data_error">HTTP-gegevensfout</string> <string name="download_error_error_unknown">Onbekende fout</string> - <string name="download_error_parser_exception">Verwerkingsuitzondering</string> <string name="download_error_unsupported_type">Niet-ondersteunde feedsoort</string> <string name="download_error_connection_error">Verbindingsfout</string> - <string name="download_error_unknown_host">Onbekende host</string> <string name="download_error_unauthorized">Authenticatiefout</string> <string name="download_error_file_type_type">Bestandssoortfout</string> - <string name="download_error_forbidden">Niet mogelijk</string> <string name="download_canceled_msg">Download afgebroken</string> <string name="download_canceled_autodownload_enabled_msg">Download afgebroken\n<i>Automatisch downloaden</i> uitgeschakeld voor deze aflevering</string> <string name="download_report_title">Downloads afgerond, maar met fout(en)</string> @@ -229,12 +248,7 @@ <item quantity="one">Nog %d download</item> <item quantity="other">Nog %d downloads</item> </plurals> - <string name="downloads_processing">Bezig met verwerken van downloads</string> <string name="download_notification_title">Bezig met downloaden van podcastgegevens</string> - <plurals name="download_report_content"> - <item quantity="one">%d download voltooid; %d mislukt</item> - <item quantity="other">%d downloads voltooid; %d mislukt</item> - </plurals> <string name="download_log_title_unknown">Onbekende titel</string> <string name="download_type_feed">Feed</string> <string name="download_type_media">Mediabestand</string> @@ -267,6 +281,7 @@ <string name="player_go_to_picture_in_picture">Beeld-in-beeldmodus</string> <string name="unknown_media_key">AntennaPod - onbekende mediatoets: %1$d</string> <string name="error_file_not_found">Bestand niet aangetroffen</string> + <string name="no_media_label">Het item bevat geen mediabestand</string> <!--Queue operations--> <string name="lock_queue">Wachtrij vergrendelen</string> <string name="unlock_queue">Wachtrij ontgrendelen</string> @@ -323,7 +338,6 @@ <string name="storage_pref">Opslag</string> <string name="storage_sum">Automatisch verwijderen, im- en exporteren</string> <string name="project_pref">Project</string> - <string name="queue_label">Wachtrij</string> <string name="synchronization_pref">Synchronisatie</string> <string name="synchronization_sum">Synchroniseer met andere apparaten met behulp van gpodder.net</string> <string name="automation">Automatische acties</string> @@ -334,19 +348,24 @@ <string name="external_elements">Externe elementen</string> <string name="interruptions">Onderbrekingen</string> <string name="playback_control">Afspeelbediening</string> + <string name="reassign_hardware_buttons">Hardwareknoppen opnieuw toewijzen</string> <string name="preference_search_hint">Zoeken...</string> <string name="preference_search_no_results">Geen resultaten</string> <string name="preference_search_clear_history">Geschiedenis wissen</string> <string name="media_player">Mediaspeler</string> <string name="pref_episode_cleanup_title">Automatisch opschonen</string> - <string name="pref_episode_cleanup_summary">Afleveringen die niet in de wachtrij staan én geen favoriet zijn, mogen verwijderd worden als \'Automatisch downloaden\' ruimte nodig heeft voor nieuwe afleveringen</string> + <string name="pref_episode_cleanup_summary">Afleveringen verwijderd mogen worden als \'Automatisch downloaden\' ruimte nodig heeft voor nieuwe afleveringen</string> <string name="pref_pauseOnDisconnect_sum">Afspelen pauzeren als de koptelefoon wordt losgekoppeld of de bluetoothverbinding verbroken</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Afspelen hervatten als de koptelefoon weer wordt aangesloten</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Afspelen hervatten als de bluetoothverbinding hersteld is</string> - <string name="pref_hardwareForwardButtonSkips_title">\'Vooruit\' gebruiken voor overslaan</string> - <string name="pref_hardwareForwardButtonSkips_sum">Als je op de vooruitknop van een via bluetooth verbonden apparaat drukt, wordt de volgende aflevering geladen i.p.v. doorgespoeld</string> - <string name="pref_hardwarePreviousButtonRestarts_title">\'Vorige\' gebruiken voor opnieuw afspelen</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Aflevering afspelen vanaf het begin i.p.v. terugspoelen als er op een fysieke \'vorige\'-knop wordt gedrukt</string> + <string name="pref_hardware_forward_button_title">Vooruitknop</string> + <string name="pref_hardware_forward_button_summary">Pas het gedrag van de vooruitknop aan</string> + <string name="pref_hardware_previous_button_title">Terugknop</string> + <string name="pref_hardware_previous_button_summary">Pas het gedrag van de terugknop aan</string> + <string name="button_action_fast_forward">Vooruitspoelen</string> + <string name="button_action_rewind">Terugspoelen</string> + <string name="button_action_skip_episode">Aflevering overslaan</string> + <string name="button_action_restart_episode">Afleveringen herstarten</string> <string name="pref_followQueue_sum">Volgende item in de wachtrij afspelen als de aflevering voltooid is</string> <string name="pref_auto_delete_sum">Afleveringen verwijderen als ze zijn afgespeeld</string> <string name="pref_auto_delete_title">Automatisch verwijderen</string> @@ -366,8 +385,11 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Uitschakelen</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Tussenpoos instellen</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Tijdstip instellen</string> - <string name="pref_autoUpdateIntervallOrTime_every">elke %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">om %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Elk uur</item> + <item quantity="other">Elke %d uur</item> + </plurals> <string name="pref_followQueue_title">Doorlopend afspelen</string> <string name="pref_pauseOnHeadsetDisconnect_title">Koptelefoon- of bluetoothverbinding verbroken</string> <string name="pref_unpauseOnHeadsetReconnect_title">Opnieuw aansluiten van hoofdtelefoon</string> @@ -402,6 +424,8 @@ <string name="pref_episode_cache_summary">Het totaal aantal gedownloade afleveringen dat moet worden opgeslagen op het apparaat. Automatische downloads worden onderbroken als dit aantal wordt bereikt.</string> <string name="pref_episode_cover_title">Omslag van aflevering gebruiken</string> <string name="pref_episode_cover_summary">Gebruik de bij de aflevering behorende omslag (indien beschikbaar). Als je dit niet inschakelt, dan wordt altijd de omslag van de podcast gebruikt.</string> + <string name="pref_show_remain_time_title">Resterende tijd tonen</string> + <string name="pref_show_remain_time_summary">Schakel in om de resterende tijd van afleveringen te tonen. Schakel uit om de totale duur van afleveringen te tonen.</string> <string name="pref_theme_title_use_system">Systeemthema gebruiken</string> <string name="pref_theme_title_light">Licht</string> <string name="pref_theme_title_dark">Donker</string> @@ -421,8 +445,6 @@ <string name="pref_gpodnet_full_sync_title">Volledige synchronisatie afdwingen</string> <string name="pref_gpodnet_full_sync_sum">Synchroniseer alle abonnementen en afleveringsstatussen met gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Ingelogd als <i>%1$s</i> met apparaat <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Synchronisatie mislukt</string> - <string name="pref_gpodnet_notifications_sum">Deze instelling is niet van toepassing op inlogfouten.</string> <string name="pref_playback_speed_sum">Pas de beschikbare snelheden aan voor de variabele afspeelsnelheid</string> <string name="pref_feed_playback_speed_sum">De te gebruiken snelheid bij het afspelen van afleveringen in deze podcast</string> <string name="pref_feed_skip">Automatisch overslaan</string> @@ -437,8 +459,6 @@ <string name="pref_fast_forward_sum">Pas het aantal seconden aan waarmee wordt vooruitgespoeld per druk op de knop</string> <string name="pref_rewind">Snelheid van terugspoelen</string> <string name="pref_rewind_sum">Pas het aantal seconden aan waarmee wordt teruggespoeld per druk op de knop</string> - <string name="pref_gpodnet_sethostname_title">Hostnaam instellen</string> - <string name="pref_gpodnet_sethostname_use_default_host">Standaardhost gebruiken</string> <string name="pref_expandNotify_title">Melding met hoge prioriteit</string> <string name="pref_expandNotify_sum">Dit klapt meestal de melding uit zodat de bedieningsknoppen kunnen worden getoond.</string> <string name="pref_persistNotify_title">Bedieningsknoppen behouden</string> @@ -449,10 +469,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Je kunt maximaal %1$d knoppen kiezen.</string> <string name="pref_lockscreen_background_title">Achtergrondafbeelding vergrendelscherm</string> <string name="pref_lockscreen_background_sum">Toon de afbeelding van de huidige aflevering op het vergrendelscherm. Hierdoor is de afbeelding ook beschikbaar voor andere apps.</string> - <string name="pref_showDownloadReport_title">Downloaden mislukt</string> - <string name="pref_showDownloadReport_sum">Stel een verslag op met foutdetails als downloads mislukken.</string> - <string name="pref_showAutoDownloadReport_title">Automatisch downloaden voltooid</string> - <string name="pref_showAutoDownloadReport_sum">Toon een melding bij automatisch gedownloade afleveringen.</string> <string name="pref_expand_notify_unsupport_toast">Android-versies lager dan 4.1 ondersteunen geen knoppen op meldingen.</string> <string name="pref_enqueue_location_title">Wachtrijlocatie</string> <string name="pref_enqueue_location_sum">Afleveringen toevoegen aan: %1$s</string> @@ -462,6 +478,7 @@ <string name="pref_smart_mark_as_played_disabled">Uitgeschakeld</string> <string name="pref_image_cache_size_title">Grootte van afbeeldingscache</string> <string name="pref_image_cache_size_sum">Pas de grootte aan van het cachegeheugen voor afbeeldingen.</string> + <string name="documentation_support">Documentatie en ondersteuning</string> <string name="visit_user_forum">Gebruikersforum</string> <string name="bug_report_title">Bug melden</string> <string name="open_bug_tracker">Bugtracker openen</string> @@ -473,14 +490,14 @@ <string name="pref_current_value">Huidige instelling: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Netwerkproxy instellen</string> - <string name="pref_faq">Veelgestelde vragen</string> <string name="pref_no_browser_found">Geen browser aangetroffen.</string> <string name="pref_cast_title">Chromecast-ondersteuning</string> <string name="pref_cast_message_play_flavor">Ondersteuning activeren voor draadloos afspelen via Cast-apparaten (zoals Chromecast, luidsprekers of Android TV)</string> <string name="pref_cast_message_free_flavor">Voor Chromecast is software van derden vereist die niet beschikbaar is in deze versie van AntennaPod</string> <string name="pref_enqueue_downloaded_title">Gedownloade afleveringen in wachtrij</string> <string name="pref_enqueue_downloaded_summary">Voeg gedownloade afleveringen toe aan de wachtrij</string> - <string name="media_player_builtin">Ingebouwde Android-speler</string> + <string name="media_player_builtin">Ingebouwde Android-speler (verouderd)</string> + <string name="media_player_sonic">Sonic-mediaspeler (verouderd)</string> <string name="media_player_exoplayer_recommended">ExoPlayer (aanbevolen)</string> <string name="media_player_switch_to_exoplayer">ExoPlayer gebruiken</string> <string name="media_player_switched_to_exoplayer">Overgeschakeld naar ExoPlayer.</string> @@ -569,6 +586,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Slaaptimer instellen</string> <string name="disable_sleeptimer_label">Slaaptimer uitschakelen</string> + <string name="extend_sleep_timer_label">+%d min.</string> <string name="sleep_timer_label">Slaaptimer</string> <string name="time_dialog_invalid_input">Ongeldige invoer; de tijd moet een geheel getal zijn</string> <string name="shake_to_reset_label">Schudden om opnieuw in te stellen</string> @@ -596,22 +614,22 @@ <string name="gpodnet_suggestions_header">SUGGESTIES</string> <string name="gpodnet_search_hint">gpodder.net doorzoeken</string> <string name="gpodnetauth_login_title">Inloggen</string> - <string name="gpodnetauth_login_descr">Welkom bij het inlogproces van gpodder.net. Typ eerst je inloggegevens:</string> <string name="gpodnetauth_login_butLabel">Inloggen</string> - <string name="gpodnetauth_login_register">Als je nog geen account hebt, dan kun je je hier registreren:\nhttps://gpodder.net/register/</string> + <string name="create_account">Account aanmaken</string> <string name="username_label">Gebruikersnaam</string> <string name="password_label">Wachtwoord</string> - <string name="gpodnetauth_device_title">Apparaatkeuze</string> + <string name="gpodnet_description">Gpodder.net is een open source podcast-synchronisatiedienst die niet betrokken is bij het AntennaPod-project.</string> + <string name="gpodnetauth_server_official">Officiële gpodder.net-server</string> + <string name="gpodnetauth_server_custom">Aangepaste server</string> + <string name="gpodnetauth_host">Hostnaam</string> + <string name="gpodnetauth_select_server">Server kiezen</string> <string name="gpodnetauth_device_descr">Voeg een nieuw apparaat toe aan je gpodder.net-account of kies een bestaand:</string> - <string name="gpodnetauth_device_deviceID">Apparaat-ID:\u0020</string> - <string name="gpodnetauth_device_caption">Omschrijving</string> - <string name="gpodnetauth_device_butCreateNewDevice">Apparaat toevoegen</string> - <string name="gpodnetauth_device_chooseExistingDevice">Bestaand apparaat kiezen:</string> - <string name="gpodnetauth_device_errorEmpty">Apparaat-ID mag niet blanco zijn</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Apparaat-ID wordt al gebruikt</string> + <string name="gpodnetauth_device_name">Apparaatnaam</string> + <string name="gpodnetauth_device_name_default">AntennaPod op %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Apparaatomschrijving mag niet blanco zijn</string> + <string name="gpodnetauth_existing_devices">Bestaande apparaten</string> + <string name="gpodnetauth_create_device">Apparaat toevoegen</string> <string name="gpodnetauth_device_butChoose">Kiezen</string> - <string name="gpodnetauth_finish_title">Ingelogd!</string> <string name="gpodnetauth_finish_descr">Gefeliciteerd! Je gpodder.net-account is nu gekoppeld aan je apparaat. AntennaPod zal voortaan abonnementen automatisch synchroniseren met je gpodder.net-account.</string> <string name="gpodnetauth_finish_butsyncnow">Nu synchroniseren</string> <string name="gpodnetauth_finish_butgomainscreen">Terug naar hoofdscherm</string> @@ -665,6 +683,7 @@ <string name="switch_pages">Van pagina wisselen</string> <string name="position">Positie: %1$s</string> <string name="apply_action">Toepassen</string> + <string name="play_chapter">Hoofdstuk afspelen</string> <!--Feed information screen--> <string name="authentication_label">Authenticatie</string> <string name="authentication_descr">Gebruikersnaam en wachtwoord wijzigen voor deze podcast en bijbehorende afleveringen.</string> @@ -799,18 +818,22 @@ <string name="cast_failed_receiver_player_error">Ernstige fout opgetreden op het afspelende Cast-apparaat</string> <string name="cast_failed_media_error_skipping">Kan media niet afspelen. Bezig met overslaan...</string> <!--Notification channels--> + <string name="notification_group_errors">Foutmeldingen</string> + <string name="notification_group_news">Nieuws</string> <string name="notification_channel_user_action">Actie vereist</string> <string name="notification_channel_user_action_description">Tonen als een actie vereist is, bijvoorbeeld als je een wachtwoord moet invoeren.</string> <string name="notification_channel_downloading">Bezig met downloaden...</string> <string name="notification_channel_downloading_description">Tonen als er iets wordt gedownload.</string> <string name="notification_channel_playing">Nu aan het afspelen</string> <string name="notification_channel_playing_description">Hiermee kun je het afspelen bedienen. Dit is de voornaamste melding tijdens het afspelen van een podcast.</string> - <string name="notification_channel_error">Foutmeldingen</string> - <string name="notification_channel_error_description">Wordt getoond als er iets misgaat, zoals downloaden of het bijwerken van de feed.</string> - <string name="notification_channel_sync_error">Syschronisatiefouten</string> + <string name="notification_channel_download_error">Downloaden mislukt</string> + <string name="notification_channel_download_error_description">Wordt getoond als er iets misgaat, zoals het downloaden of het bijwerken van de feed.</string> + <string name="notification_channel_sync_error">Synchronisatie mislukt</string> <string name="notification_channel_sync_error_description">Worden getoond als gpodder-synchronisatie mislukt.</string> - <string name="notification_channel_auto_download">Automatisch downloaden</string> + <string name="notification_channel_auto_download">Automatisch downloaden voltooid</string> <string name="notification_channel_episode_auto_download">Wordt getoond als afleveringen automatisch zijn gedownload.</string> + <string name="notification_channel_new_episode">Nieuwe aflevering</string> + <string name="notification_channel_new_episode_description">Wordt getoond als er een nieuwe aflevering beschikbaar is en meldingen zijn ingeschakeld.</string> <!--Widget settings--> <string name="widget_settings">Widgetinstellingen</string> <string name="widget_create_button">Widget maken</string> diff --git a/core/src/main/res/values-pl/strings.xml b/core/src/main/res/values-pl/strings.xml index c2b977382..bada1af58 100644 --- a/core/src/main/res/values-pl/strings.xml +++ b/core/src/main/res/values-pl/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statystyki</string> <string name="add_feed_label">Dodaj podcast</string> <string name="episodes_label">Odcinki</string> + <string name="queue_label">Kolejka</string> <string name="all_episodes_short_label">Wszystkie</string> <string name="new_episodes_label">Nowe</string> <string name="favorite_episodes_label">Ulubione</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">Dziennik</string> <string name="subscriptions_label">Subskrypcje</string> <string name="subscriptions_list_label">Lista subskrypcji</string> - <string name="cancel_download_label">Anuluj pobieranie</string> <string name="playback_history_label">Historia odtwarzania</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net login</string> @@ -26,6 +26,7 @@ <string name="playback_statistics_label">Odtwarzanie</string> <string name="download_statistics_label">Pobrane</string> <string name="notification_pref_fragment">Powiadomienia</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Całkowity czas odtwarzania podcastów:</string> <string name="statistics_details_dialog">%1$d z %2$d odcinków rozpoczęto.\n\nOdtworzono %3$s z %4$s.</string> @@ -81,7 +82,6 @@ <string name="description_label">Opis</string> <string name="episodes_suffix">\u0020odcinków</string> <string name="processing_label">Przetwarzanie</string> - <string name="save_username_password_label">Zapisz nazwę użytkownika i hasło</string> <string name="close_label">Zamknij</string> <string name="retry_label">Spróbuj ponownie</string> <string name="auto_download_label">Dołącz do automatycznego pobierania</string> @@ -93,7 +93,6 @@ <string name="feed_volume_reduction_off">Wyłączona</string> <string name="feed_volume_reduction_light">Średnia</string> <string name="feed_volume_reduction_heavy">Silna</string> - <string name="parallel_downloads_suffix">\u0020równoległych pobierań</string> <string name="feed_auto_download_global">Globalnie domyślnie</string> <string name="feed_auto_download_always">Zawsze</string> <string name="feed_auto_download_never">Nigdy</string> @@ -151,7 +150,6 @@ <string name="hide_not_queued_episodes_label">Nie w kolejce</string> <string name="hide_has_media_label">Ma media</string> <string name="filtered_label">Przefiltrowany</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Ostatnie odświerzanie nie powiodło się</string> <string name="open_podcast">Otwórz Podcast</string> <string name="please_wait_for_data">Proszę czekać aż dane zostaną załadowane</string> <!--actions on feeditems--> @@ -223,16 +221,12 @@ <string name="download_error_details_message">%1$s \n\nAdres pliku:\n%2$s </string> <string name="download_error_device_not_found">Nie znaleziono urządzenia docelowego</string> - <string name="download_error_insufficient_space">Niewystarczająca ilość pamięci</string> <string name="download_error_http_data_error">Błąd danych HTTP</string> <string name="download_error_error_unknown">Nieznany błąd</string> - <string name="download_error_parser_exception">Wyjątek parsera</string> <string name="download_error_unsupported_type">Nieobsługiwany typ kanału</string> <string name="download_error_connection_error">Błąd połączenia</string> - <string name="download_error_unknown_host">Nieznany host</string> <string name="download_error_unauthorized">Błąd autoryzacji</string> <string name="download_error_file_type_type">Błąd rodzaju pliku</string> - <string name="download_error_forbidden">Zabronione</string> <string name="download_canceled_msg">Pobieranie anulowane</string> <string name="download_canceled_autodownload_enabled_msg">Pobieranie zatrzymane\nWyłączone <i>Automatyczne pobieranie</i> dla tego elementu</string> <string name="download_report_title">Pobieranie ukończone</string> @@ -248,14 +242,7 @@ <item quantity="many">%d elementów zostało do pobrania</item> <item quantity="other">%d elementów zostało do pobrania</item> </plurals> - <string name="downloads_processing">Przetwarzanie pobranych</string> <string name="download_notification_title">Pobieranie danych podcastu</string> - <plurals name="download_report_content"> - <item quantity="one">%d pobranie udane, %d błędne</item> - <item quantity="few">%d pobrań udanych, %d błędnych</item> - <item quantity="many">%d pobrań udanych, %d błędnych</item> - <item quantity="other">%d pobrań udanych, %d błędnych</item> - </plurals> <string name="download_log_title_unknown">Nieznany tytuł</string> <string name="download_type_feed">Kanał</string> <string name="download_type_media">Plik multimedialny</string> @@ -344,7 +331,6 @@ <string name="storage_pref">Pamięć</string> <string name="storage_sum">Automatyczne kasowanie odcinków, Import, Eksport</string> <string name="project_pref">Projekt</string> - <string name="queue_label">Kolejka</string> <string name="synchronization_pref">Synchronizacja</string> <string name="synchronization_sum">Synchronizuj z innymi urządzeniami za pomocą gpodder.net</string> <string name="automation">Automatyzacja</string> @@ -360,14 +346,9 @@ <string name="preference_search_clear_history">Wyczyść historię</string> <string name="media_player">Odtwarzacz</string> <string name="pref_episode_cleanup_title">Usuwanie odcinków</string> - <string name="pref_episode_cleanup_summary">Odcinki niebędące w kolejce i niebędące na liście ulubiobych powinny nadawać się do usunięcia, jeśli Automatyczne Pobieranie potrzebuje miejsca na nowe odcinki.</string> <string name="pref_pauseOnDisconnect_sum">Wstrzymaj odtwarzanie po rozłączeniu słuchawek lub Bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Wznów odtwarzanie kiedy słuchawki zostaną podłączone ponownie</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Wznów odtwarzanie po przywróceniu połączenia Bluetooth</string> - <string name="pref_hardwareForwardButtonSkips_title">Przycisk \'Do przodu\' pomija odcinek</string> - <string name="pref_hardwareForwardButtonSkips_sum">Naciśnięcie przycisku \'Do przodu\' na urządzeniu bluetooth skacze do następnego odcinka, zamiast przewijać</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Przycisk wstecz restartuje</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Podczas odtwarzania przycisk wstecz restartuje zamiast przewijać</string> <string name="pref_followQueue_sum">Przeskocz do następnego elementu kolejki po zakończeniu odtwarzania</string> <string name="pref_auto_delete_sum">Usuń odcinek kiedy jego odtwarzanie zostanie zakończone</string> <string name="pref_auto_delete_title">Automatyczne usuwanie</string> @@ -387,7 +368,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Zablokuj</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Ustaw częstotliwość</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Ustaw czas dnia</string> - <string name="pref_autoUpdateIntervallOrTime_every">co %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">o %1$s</string> <string name="pref_followQueue_title">Odtwarzanie ciągłe</string> <string name="pref_pauseOnHeadsetDisconnect_title">Rozłączenie słuchawek lub Bluetooth</string> @@ -422,7 +402,6 @@ <string name="pref_episode_cache_title">Pamięć podręczna odcinków</string> <string name="pref_episode_cache_summary">Całkowita liczba odcinków zapisanych na urządzeniu. Automatyczne pobieranie zostanie przerwane, jeśli zostanie ona osiągnięta.</string> <string name="pref_episode_cover_title">Użyj okładek odcinków</string> - <string name="pref_episode_cover_summary">Użyj okładek konkretnych odcinków kiedy to możliwe. Odznaczenie spowoduje, że aplikacja zawsze będzie używała okładki kanału.</string> <string name="pref_theme_title_use_system">Użyj motywu systemowego</string> <string name="pref_theme_title_light">Jasny</string> <string name="pref_theme_title_dark">Ciemny</string> @@ -442,8 +421,6 @@ <string name="pref_gpodnet_full_sync_title">Wymuś pełną synchronizację</string> <string name="pref_gpodnet_full_sync_sum">Synchronizuj wszystkie subskrypcje oraz stan odcinków z pomocą gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Zalogowano jako <i>%1$s</i> na urządzeniu <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Błąd synchronizacji</string> - <string name="pref_gpodnet_notifications_sum">To ustawienie nie dotyczy błędów autoryzacji.</string> <string name="pref_playback_speed_sum">Dostosuj prędkości dostępne dla odtwarzania o zmiennej prędkości</string> <string name="pref_feed_playback_speed_sum">Prędkość używana podczas odtwarzania odcinków z tego kanału</string> <string name="pref_feed_skip">Automatyczne pomijanie</string> @@ -458,8 +435,6 @@ <string name="pref_fast_forward_sum">Dostosuj liczbę sekund do przeskoczenia przy kliknięciu szybkiego przewijania do przodu</string> <string name="pref_rewind">Przewijanie do tyłu</string> <string name="pref_rewind_sum">Dostosuj liczbę sekund do przeskoczenia przy kliknięciu przewijania do tyłu</string> - <string name="pref_gpodnet_sethostname_title">Ustaw nazwę hosta</string> - <string name="pref_gpodnet_sethostname_use_default_host">Użyj domyślnego hosta</string> <string name="pref_expandNotify_title">Wysoki priorytet powiadomienia</string> <string name="pref_expandNotify_sum">Rozwija powiadomienie, aby pokazać przyciski odtwarzania</string> <string name="pref_persistNotify_title">Stałe przyciski odtwarzacza</string> @@ -470,10 +445,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Możesz tylko wybrać maksimum z %1$d przedmiotów.</string> <string name="pref_lockscreen_background_title">Ustaw tło ekranu blokady</string> <string name="pref_lockscreen_background_sum">Ustaw tło ekranu blokowania na aktualny obraz odcinka. W efekcie będzie zawsze pokazywał obraz w innych aplikacjach.</string> - <string name="pref_showDownloadReport_title">Błąd pobierania</string> - <string name="pref_showDownloadReport_sum">Jeżeli pobieranie się nie powiedzie, pokaż raport ze szczegółami błędu.</string> - <string name="pref_showAutoDownloadReport_title">Automatyczne pobieranie zakończone</string> - <string name="pref_showAutoDownloadReport_sum">Pokazuj powiadomienie dla odcinków pobranych automatycznie</string> <string name="pref_expand_notify_unsupport_toast">Android starszy niż 4.1 nie wspiera rozszerzonych powiadomień.</string> <string name="pref_enqueue_location_title">Pozycja w kolejce</string> <string name="pref_enqueue_location_sum">Dodaj odcinki do: %1$s</string> @@ -494,14 +465,12 @@ <string name="pref_current_value">Aktualna wartość: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Ustaw proxy sieciowe</string> - <string name="pref_faq">Najczęściej zadawane pytania</string> <string name="pref_no_browser_found">Nie znaleziono przeglądarki.</string> <string name="pref_cast_title">Obsługa Chromecast</string> <string name="pref_cast_message_play_flavor">Uruchom obsługę dla zdalnego odtwarzania mediów na innych urządzeniach (takich jak Chromecast, Audio Speakers albo Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast wymagadodatkowych bibliotek, które są zablokowane w tej wersji AntennaPod</string> <string name="pref_enqueue_downloaded_title">Kolejkuj pobrane</string> <string name="pref_enqueue_downloaded_summary">Dodaj pobrane odcinki do kolejki</string> - <string name="media_player_builtin">Wbudowany odtwarzacz Androida</string> <string name="media_player_exoplayer_recommended">ExoPlayer (rekomendowany)</string> <string name="media_player_switch_to_exoplayer">Zmień na ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Zmieniono na ExoPlayer</string> @@ -623,23 +592,12 @@ <string name="gpodnet_suggestions_header">SUGESTIE</string> <string name="gpodnet_search_hint">Szukaj na gpodder.net</string> <string name="gpodnetauth_login_title">Login</string> - <string name="gpodnetauth_login_descr">Witamy w procesie logowania do gpodder.net. Najpierw podaj swoje dane logowania:</string> <string name="gpodnetauth_login_butLabel">Login</string> - <string name="gpodnetauth_login_register">Jeśli nie masz jeszcze konta, możesz utworzyć je tutaj: -https://gpodder.net/register/</string> <string name="username_label">Nazwa użytkownika</string> <string name="password_label">Hasło</string> - <string name="gpodnetauth_device_title">Wybór urządzenia</string> <string name="gpodnetauth_device_descr">Utwórz nowe urządzenie dla swojego konta na gpodder.net lub wybierz istniejące:</string> - <string name="gpodnetauth_device_deviceID">Identyfikator urządzenia:\u0020</string> - <string name="gpodnetauth_device_caption">Tytuł</string> - <string name="gpodnetauth_device_butCreateNewDevice">Utwórz nowe urządzenie</string> - <string name="gpodnetauth_device_chooseExistingDevice">Wybierz istniejące urządzenie:</string> - <string name="gpodnetauth_device_errorEmpty">Identyfikator urządzenia nie może być pusty</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Identyfikator urządzenia w użyciu</string> <string name="gpodnetauth_device_caption_errorEmpty">Pole nie może być puste</string> <string name="gpodnetauth_device_butChoose">Wybierz</string> - <string name="gpodnetauth_finish_title">Logowanie zakończone sukcesem!</string> <string name="gpodnetauth_finish_descr">Gratulacje! Twoje konto na gpodder.net jest połączone z urządzeniem. AntennaPod będzie automatycznie synchronizować subskrypcje na urządzeniu z kontem na gpodder.net. </string> <string name="gpodnetauth_finish_butsyncnow">Rozpocznij synchronizację</string> <string name="gpodnetauth_finish_butgomainscreen">Idź do strony głównej</string> @@ -833,11 +791,7 @@ https://gpodder.net/register/</string> <string name="notification_channel_downloading_description">Pokazywane podczas aktywnego pobierania.</string> <string name="notification_channel_playing">Teraz odtwarzane</string> <string name="notification_channel_playing_description">Pozwala na kontrolowanie odtwarzania. To jest główne powiadomienie, które zobaczysz podczas odtwarzania podcastu.</string> - <string name="notification_channel_error">Błędy</string> - <string name="notification_channel_error_description">Pokazywane, gdy coś pójdzie nie tak, np. błąd pobierania lub błąd aktualizacji.</string> - <string name="notification_channel_sync_error">Błędy synchronizacji</string> <string name="notification_channel_sync_error_description">Pokazywane, gdy synchronizacja z gpodder się nie powiedzie.</string> - <string name="notification_channel_auto_download">Automatyczne pobieranie</string> <string name="notification_channel_episode_auto_download">Pokazywane, gdy odcinki zostały pobrane automatycznie</string> <!--Widget settings--> <string name="widget_settings">Ustawienia widżetu</string> diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml index a4b499bf0..2cfe826e9 100644 --- a/core/src/main/res/values-pt-rBR/strings.xml +++ b/core/src/main/res/values-pt-rBR/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Estatísticas</string> <string name="add_feed_label">Adicionar Podcast</string> <string name="episodes_label">Episódios</string> + <string name="queue_label">Fila</string> <string name="all_episodes_short_label">Todos</string> <string name="new_episodes_label">Novo</string> <string name="favorite_episodes_label">Favoritos</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Log</string> <string name="subscriptions_label">Assinaturas</string> <string name="subscriptions_list_label">Lista de Assinaturas</string> - <string name="cancel_download_label">Cancelar\nDownload</string> + <string name="cancel_download_label">Cancelar Download</string> <string name="playback_history_label">Histórico de reprodução</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net login</string> @@ -26,6 +27,7 @@ <string name="playback_statistics_label">Reprodução</string> <string name="download_statistics_label">Downloads</string> <string name="notification_pref_fragment">Notificações</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Tempo total de reprodução de episódios:</string> <string name="statistics_details_dialog">%1$d de %2$d episódios iniciados.\n\nReproduzidos %3$s de %4$s.</string> @@ -53,6 +55,7 @@ <string name="drawer_feed_counter_none">Nenhum</string> <!--Bug report activity--> <string name="log_file_share_exception">Nenhum aplicativo compatível encontrado</string> + <string name="export_logs_menu_title">Exportar histórico detalhado</string> <!--Webview actions--> <string name="open_in_browser_label">Abrir no navegador</string> <string name="copy_url_label">Copiar URL</string> @@ -81,7 +84,6 @@ <string name="description_label">Descrição</string> <string name="episodes_suffix">\u0020episódios</string> <string name="processing_label">Processando</string> - <string name="save_username_password_label">Salvar nome do usuário e senha</string> <string name="close_label">Fechar</string> <string name="retry_label">Tentar novamente</string> <string name="auto_download_label">Incluir em downloads automáticos</string> @@ -93,7 +95,6 @@ <string name="feed_volume_reduction_off">Desligar</string> <string name="feed_volume_reduction_light">Leve</string> <string name="feed_volume_reduction_heavy">Pesado</string> - <string name="parallel_downloads_suffix">\u0020 downloads paralelos</string> <string name="feed_auto_download_global">Padrão global</string> <string name="feed_auto_download_always">Sempre</string> <string name="feed_auto_download_never">Nunca</string> @@ -145,7 +146,6 @@ <string name="hide_not_queued_episodes_label">Não enfileirado</string> <string name="hide_has_media_label">Possui mídia</string> <string name="filtered_label">Filtrado</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Última Atualização falhou</string> <string name="open_podcast">Abrir Podcast</string> <string name="please_wait_for_data">Por favor, aguarde até que os dados sejam carregados</string> <!--actions on feeditems--> @@ -206,16 +206,12 @@ <string name="download_error_details">Detalhes</string> <string name="download_error_details_message">%1$s \n\nURL do arquivo:\n%2$s</string> <string name="download_error_device_not_found">Dispositivo de armazenamento não encontrado</string> - <string name="download_error_insufficient_space">Espaço insuficiente</string> <string name="download_error_http_data_error">Erro de HTTP Data</string> <string name="download_error_error_unknown">Erro desconhecido</string> - <string name="download_error_parser_exception">Parser Exception</string> <string name="download_error_unsupported_type">Tipo de feed não suportado</string> <string name="download_error_connection_error">Erro de conexão</string> - <string name="download_error_unknown_host">Host desconhecido</string> <string name="download_error_unauthorized">Erro de autenticação</string> <string name="download_error_file_type_type">Erro de Tipo de Arquivo</string> - <string name="download_error_forbidden">Proibido</string> <string name="download_canceled_msg">Download cancelado</string> <string name="download_canceled_autodownload_enabled_msg">Download cancelado\nDesabilitado <i>Download Automático</i> para este item</string> <string name="download_report_title">Downloads finalizados com erro(s)</string> @@ -229,12 +225,7 @@ <item quantity="one">%d download restante</item> <item quantity="other">%d downloads restantes</item> </plurals> - <string name="downloads_processing">Processando downloads</string> <string name="download_notification_title">Baixando dados do podcast</string> - <plurals name="download_report_content"> - <item quantity="one">%ddownload com sucesso, %dfalhou</item> - <item quantity="other">%ddownloads com sucesso, %dfalharam</item> - </plurals> <string name="download_log_title_unknown">Título desconhecido</string> <string name="download_type_feed">Feed</string> <string name="download_type_media">Arquivo de mídia</string> @@ -323,7 +314,6 @@ <string name="storage_pref">Armazenamento</string> <string name="storage_sum">Exclusão automática de episódio, importação, exportação</string> <string name="project_pref">Projeto</string> - <string name="queue_label">Fila</string> <string name="synchronization_pref">Sincronização</string> <string name="synchronization_sum">Sincroniza com outros dispositivos usando gpodder.net</string> <string name="automation">Automação</string> @@ -339,14 +329,9 @@ <string name="preference_search_clear_history">Limpar histórico</string> <string name="media_player">Reprodutor de mídia</string> <string name="pref_episode_cleanup_title">Limpar Episódio</string> - <string name="pref_episode_cleanup_summary">Episódios que não estão na fila e não estão nos favoritos podem ser removidos se o Download Automático precisar de espaço para novos episódios</string> <string name="pref_pauseOnDisconnect_sum">Pausar a reprodução quando o fone de ouvido ou bluetooth forem desconectados</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Continuar a reprodução quando os fones de ouvido forem reconectados</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Continuar a reprodução quando o bluetooth reconectar</string> - <string name="pref_hardwareForwardButtonSkips_title">Botão avançar pula</string> - <string name="pref_hardwareForwardButtonSkips_sum">Ao pressionar um botão de avanço em um dispositivo conectado por bluetooth, pule para o próximo episódio em vez de avançar.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Botão Anterior reinicia</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Ao pressionar o botão Anterior do hardware, reinicie a reprodução do episódio atual em vez de retroceder</string> <string name="pref_followQueue_sum">Pular para próximo item da fila quando a reprodução terminar</string> <string name="pref_auto_delete_sum">Remover episódio quando a reprodução for concluída</string> <string name="pref_auto_delete_title">Apagar automaticamente</string> @@ -366,7 +351,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Desabilitar</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Configurar Intervalo</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Configurar Tempo do dia</string> - <string name="pref_autoUpdateIntervallOrTime_every">cada %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">às %1$s</string> <string name="pref_followQueue_title">Reprodução contínua</string> <string name="pref_pauseOnHeadsetDisconnect_title">Fones de ouvido ou Bluetooth desconectado</string> @@ -401,7 +385,6 @@ <string name="pref_episode_cache_title">Cache de episódios</string> <string name="pref_episode_cache_summary">Número total de episódios baixados em cache no dispositivo. O download automático será suspenso se esse número for atingido.</string> <string name="pref_episode_cover_title">Usar capa do episódio</string> - <string name="pref_episode_cover_summary">Use a capa específica do episódio sempre que disponível. Se desmarcado, o aplicativo sempre usará a imagem da capa do podcast.</string> <string name="pref_theme_title_use_system">Usar tema do sistema</string> <string name="pref_theme_title_light">Claro</string> <string name="pref_theme_title_dark">Escuro</string> @@ -421,8 +404,6 @@ <string name="pref_gpodnet_full_sync_title">Forçar sincronização completa</string> <string name="pref_gpodnet_full_sync_sum">Sincronizar os estados das inscrições e episódios com o gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Entrou como <i>%1$s</i> com o dispositivo <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Sincronização falhou</string> - <string name="pref_gpodnet_notifications_sum">Essa configuração não se aplica a erros de autenticação.</string> <string name="pref_playback_speed_sum">Personalize as velocidades disponíveis para reprodução de áudio.</string> <string name="pref_feed_playback_speed_sum">A velocidade a ser usada ao iniciar a reprodução de áudio para episódios neste podcast</string> <string name="pref_feed_skip">Salto automático</string> @@ -437,8 +418,6 @@ <string name="pref_fast_forward_sum">Personalize os segundos para avançar quando o botão avanço rápido for clicado</string> <string name="pref_rewind">Tempo de retroceder</string> <string name="pref_rewind_sum">Personalize os segundos para voltar quando o botão retroceder for clicado</string> - <string name="pref_gpodnet_sethostname_title">Configurar hostname</string> - <string name="pref_gpodnet_sethostname_use_default_host">Usar host padrão</string> <string name="pref_expandNotify_title">Prioridade de notificação alta</string> <string name="pref_expandNotify_sum">Isso geralmente expande a notificação para exibir botões de reprodução.</string> <string name="pref_persistNotify_title">Controles de Reprodução Persistentes</string> @@ -449,10 +428,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Você só pode selecionar no máximo %1$d itens.</string> <string name="pref_lockscreen_background_title">Configurar plano de fundo da tela de bloqueio</string> <string name="pref_lockscreen_background_sum">Configurar o plano de fundo da tela de bloqueio para a imagem do episódio atual. Como um efeito colateral, também ira mostrar imagens de aplicativos de terceiros.</string> - <string name="pref_showDownloadReport_title">Download falhou</string> - <string name="pref_showDownloadReport_sum">Se os downloads falharem, gerar um relatório que mostra os detalhes da falha.</string> - <string name="pref_showAutoDownloadReport_title">Download automático finalizado</string> - <string name="pref_showAutoDownloadReport_sum">Mostra uma notificação para episódios baixados automaticamente.</string> <string name="pref_expand_notify_unsupport_toast">Versões do Android inferiores a 4.1 não suportam notificações expansíveis</string> <string name="pref_enqueue_location_title">Local da fila</string> <string name="pref_enqueue_location_sum">Adicionar episódios para: %1$s</string> @@ -473,14 +448,12 @@ <string name="pref_current_value">Valor atual: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Configurar um proxy da rede</string> - <string name="pref_faq">Perguntas mais frequentes</string> <string name="pref_no_browser_found">Nenhum navegador web encontrado.</string> <string name="pref_cast_title">Suporte ao Chromecast</string> <string name="pref_cast_message_play_flavor">Habilitar o suporte para reprodução remota de mídia em dispositivos Cast (como Chromecast, Caixa de som ou Android TV)</string> <string name="pref_cast_message_free_flavor">O Chromecast necessita de bibliotecas proprietárias de terceiros que estão desativadas nesta versão do AntennaPod</string> <string name="pref_enqueue_downloaded_title">Enfileirar os baixados</string> <string name="pref_enqueue_downloaded_summary">Adicionar episódios baixados à fila</string> - <string name="media_player_builtin">Reprodutor próprio do Android</string> <string name="media_player_exoplayer_recommended">ExoPlayer (recomendado)</string> <string name="media_player_switch_to_exoplayer">Alterar para ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Alterado para ExoPlayer</string> @@ -596,22 +569,12 @@ <string name="gpodnet_suggestions_header">SUGESTÕES</string> <string name="gpodnet_search_hint">Buscar no gpodder.net</string> <string name="gpodnetauth_login_title">Login</string> - <string name="gpodnetauth_login_descr">Bem-vindo ao processo de login gpodder.net. Primeiramente, digite suas informações:</string> <string name="gpodnetauth_login_butLabel">Login</string> - <string name="gpodnetauth_login_register">Se você ainda não possui uma conta, você pode criar uma aqui:\nhttps://gpodder.net/register/</string> <string name="username_label">Nome do usuário</string> <string name="password_label">Senha</string> - <string name="gpodnetauth_device_title">Seleção de dispositivo</string> <string name="gpodnetauth_device_descr">Crie um novo dispositivo para usar em sua conta gpodder.net ou escolha um já existente:</string> - <string name="gpodnetauth_device_deviceID">ID do dispositivo:\u0020</string> - <string name="gpodnetauth_device_caption">Descrição do dispositivo</string> - <string name="gpodnetauth_device_butCreateNewDevice">Criar novo dispositivo</string> - <string name="gpodnetauth_device_chooseExistingDevice">Escolher dispositivo existente:</string> - <string name="gpodnetauth_device_errorEmpty">ID do dispostivo não pode estar em branco</string> - <string name="gpodnetauth_device_errorAlreadyUsed">ID do dispositivo já está em uso</string> <string name="gpodnetauth_device_caption_errorEmpty">A legenda não deve ser vazia</string> <string name="gpodnetauth_device_butChoose">Escolher</string> - <string name="gpodnetauth_finish_title">Login realizado com sucesso!</string> <string name="gpodnetauth_finish_descr">Parabéns! Sua conta gpodder.net agora está conectada ao seu dispositivo. O AntennaPod irá, daqui em diante, sincronizar automaticamente assinaturas do seu dispositivo com sua conta gpodder.net.</string> <string name="gpodnetauth_finish_butsyncnow">Iniciar sincronização agora</string> <string name="gpodnetauth_finish_butgomainscreen">Ir para tela principal</string> @@ -805,11 +768,7 @@ <string name="notification_channel_downloading_description">Exibido enquanto estiver baixando.</string> <string name="notification_channel_playing">Reproduzindo agora</string> <string name="notification_channel_playing_description">Permite controlar a reprodução. Essa é a principal notificação vista ao reproduzir um podcast.</string> - <string name="notification_channel_error">Erros</string> - <string name="notification_channel_error_description">Exibido se algo der errado, por exemplo, se o download ou a atualização do feed falhar.</string> - <string name="notification_channel_sync_error">Erros de sincronização</string> <string name="notification_channel_sync_error_description">Exibido quando a sincronização do gpodder falhou.</string> - <string name="notification_channel_auto_download">Downloads automáticos</string> <string name="notification_channel_episode_auto_download">Exibido quando os episódios foram baixados automaticamente.</string> <!--Widget settings--> <string name="widget_settings">Configurações de widgets</string> diff --git a/core/src/main/res/values-pt/strings.xml b/core/src/main/res/values-pt/strings.xml index 7e2350504..0c56ddc1f 100644 --- a/core/src/main/res/values-pt/strings.xml +++ b/core/src/main/res/values-pt/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Estatísticas</string> <string name="add_feed_label">Adicionar podcast</string> <string name="episodes_label">Episódios</string> + <string name="queue_label">Fila</string> <string name="all_episodes_short_label">Todos</string> <string name="new_episodes_label">Novos</string> <string name="favorite_episodes_label">Favoritos</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Registo</string> <string name="subscriptions_label">Subscrições</string> <string name="subscriptions_list_label">Lista de subscrições</string> - <string name="cancel_download_label">Cancelar\ndescarga</string> + <string name="cancel_download_label">Cancelar descarga</string> <string name="playback_history_label">Histórico de reprodução</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">Dados gpodder.net</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Reprodução</string> <string name="download_statistics_label">Descargas</string> <string name="notification_pref_fragment">Notificações</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" não encontrada</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Tempo total dos episódios reproduzidos:</string> <string name="statistics_details_dialog">%1$d de %2$d episódios iniciados.\n\nReproduzidos %3$s de %4$s.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Nenhum</string> <!--Bug report activity--> <string name="log_file_share_exception">Não existem aplicações compatíveis</string> + <string name="export_logs_menu_title">Exportar registos detalhados</string> + <string name="confirm_export_log_dialog_message">Este tipo de registos pode conter informação privada como, por exemplo, a sua lista de subscrições.</string> <!--Webview actions--> <string name="open_in_browser_label">Abrir no navegador</string> <string name="copy_url_label">Copiar URL</string> @@ -81,7 +86,6 @@ <string name="description_label">Descrição</string> <string name="episodes_suffix">\u0020episódios</string> <string name="processing_label">A processar...</string> - <string name="save_username_password_label">Guardar utilizador e palavra-passe</string> <string name="close_label">Fechar</string> <string name="retry_label">Tentar novamente</string> <string name="auto_download_label">Incluir nas descargas automáticas</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">Desligada</string> <string name="feed_volume_reduction_light">Ligeira</string> <string name="feed_volume_reduction_heavy">Intensa</string> - <string name="parallel_downloads_suffix">\u0020descargas simultâneas.</string> + <string name="parallel_downloads">%1$d descargas em simultâneo</string> <string name="feed_auto_download_global">Predefinições</string> <string name="feed_auto_download_always">Sempre</string> <string name="feed_auto_download_never">Nunca</string> <string name="send_label">Enviar...</string> <string name="episode_cleanup_never">Nunca</string> + <string name="episode_cleanup_except_favorite_removal">Se não for favorito</string> <string name="episode_cleanup_queue_removal">Se não estiver na fila</string> <string name="episode_cleanup_after_listening">Ao terminar</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -113,7 +118,22 @@ <item quantity="one">%d selecionado</item> <item quantity="other">%d selecionados</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d episódio</item> + <item quantity="other">%d episódios</item> + </plurals> <string name="loading_more">Carregar mais...</string> + <string name="episode_notification">Notificações para episódios</string> + <string name="episode_notification_summary">Mostrar notificação sempre que for disponibilizado um novo episódio.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s tem um novo episódio</item> + <item quantity="other">%2$s tem%1$d novos episódios</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Novo episódio</item> + <item quantity="other">Novos episódios</item> + </plurals> + <string name="new_episode_notification_group_text">Existem novos episódios nas suas subscrições.</string> <!--Actions on feeds--> <string name="mark_all_read_label">Marcar tudo como reproduzido</string> <string name="mark_all_read_msg">Marcar todos os episódios como reproduzidos</string> @@ -145,7 +165,6 @@ <string name="hide_not_queued_episodes_label">Não na fila</string> <string name="hide_has_media_label">Tem ficheiro</string> <string name="filtered_label">Filtrados</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Última atualização falhada</string> <string name="open_podcast">Abrir podcast</string> <string name="please_wait_for_data">Aguarde pelo carregamento dos dados...</string> <!--actions on feeditems--> @@ -160,6 +179,10 @@ <string name="delete_label">Eliminar</string> <string name="delete_failed">Episódio não eliminado. Tente reiniciar o dispositivo.</string> <string name="delete_episode_label">Eliminar episódio</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d episódio selecionado, %d descarga removida.</item> + <item quantity="other">%d episódios selecionados, %d descargas removidas.</item> + </plurals> <string name="remove_new_flag_label">Remover a marca \"novo\"</string> <string name="removed_new_flag_label">A marca \"novo\" foi removida</string> <string name="mark_read_label">Marcar como reproduzido</string> @@ -206,16 +229,12 @@ <string name="download_error_details">Detalhes</string> <string name="download_error_details_message">%1$s \n\Ficheiro URL:\n%2$s</string> <string name="download_error_device_not_found">Cartão SD não encontrado</string> - <string name="download_error_insufficient_space">Espaço insuficiente</string> <string name="download_error_http_data_error">Erro HTTP</string> <string name="download_error_error_unknown">Erro desconhecido</string> - <string name="download_error_parser_exception">Exceção do processador</string> <string name="download_error_unsupported_type">Fonte não suportada</string> <string name="download_error_connection_error">Erro de ligação</string> - <string name="download_error_unknown_host">Servidor desconhecido</string> <string name="download_error_unauthorized">Erro de autenticação</string> <string name="download_error_file_type_type">Erro do tipo de ficheiro</string> - <string name="download_error_forbidden">Proibido</string> <string name="download_canceled_msg">Descarga cancelada</string> <string name="download_canceled_autodownload_enabled_msg">Descarga cancelada\n<i>Descarga automática</i> desativada para este item</string> <string name="download_report_title">Descargas terminadas com erros</string> @@ -229,12 +248,7 @@ <item quantity="one">%d descarga em curso</item> <item quantity="other">%d descargas em curso</item> </plurals> - <string name="downloads_processing">Processamento de descargas</string> <string name="download_notification_title">A descarregar dados do podcast</string> - <plurals name="download_report_content"> - <item quantity="one">%d descarga com sucesso, %d com falha</item> - <item quantity="other">%d descargas com sucesso, %d com falha</item> - </plurals> <string name="download_log_title_unknown">Título desconhecido</string> <string name="download_type_feed">Fonte</string> <string name="download_type_media">Ficheiro multimédia</string> @@ -267,6 +281,7 @@ <string name="player_go_to_picture_in_picture">Modo \'picture-in-picture\'</string> <string name="unknown_media_key">Tecla multimédia desconhecida: %1$d</string> <string name="error_file_not_found">Ficheiro não encontrado</string> + <string name="no_media_label">O item não contém um ficheiro multimédia</string> <!--Queue operations--> <string name="lock_queue">Bloquear fila</string> <string name="unlock_queue">Desbloquear fila</string> @@ -323,7 +338,6 @@ <string name="storage_pref">Armazenamento</string> <string name="storage_sum">Eliminação automática, importação e exportação</string> <string name="project_pref">Projeto</string> - <string name="queue_label">Fila</string> <string name="synchronization_pref">Sincronização</string> <string name="synchronization_sum">Sincronizar com outros dispositivos via gpodder.net</string> <string name="automation">Automatização</string> @@ -334,19 +348,24 @@ <string name="external_elements">Elementos externos</string> <string name="interruptions">Interrupções</string> <string name="playback_control">Controlo de reprodução</string> + <string name="reassign_hardware_buttons">Reatribuir botões do dispositivo</string> <string name="preference_search_hint">Pesquisar...</string> <string name="preference_search_no_results">Não existem resultados</string> <string name="preference_search_clear_history">Limpar histórico</string> <string name="media_player">Reprodutor multimédia</string> <string name="pref_episode_cleanup_title">Limpeza de episódios</string> - <string name="pref_episode_cleanup_summary">Os episódios que não estejam na fila e não sejam favoritos podem ser elegíveis para serem removidos se a Descarga automática necessitar de espaço para novos episódios.</string> + <string name="pref_episode_cleanup_summary">Episódios que são elegíveis para remoção se a Descarga automática precisar de espaço para novos episódios</string> <string name="pref_pauseOnDisconnect_sum">Pausa na reprodução ao desligar os auscultadores ou o bluetooth.</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Continuar reprodução ao ligar os auscultadores.</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Continuar reprodução ao estabelecer a ligação bluetooth.</string> - <string name="pref_hardwareForwardButtonSkips_title">Botão \'Seguinte\' para avançar</string> - <string name="pref_hardwareForwardButtonSkips_sum">Ao premir o botão Seguinte no dispositivo bluetooth, ir para o episódio seguinte em vez de avançar a reprodução.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Botão \'Anterior\' para reiniciar</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Ao premir o botão Anterior no dispositivo, reiniciar o episódio atual em vez de recuar para o episódio anterior.</string> + <string name="pref_hardware_forward_button_title">Botão Seguinte</string> + <string name="pref_hardware_forward_button_summary">Personalizar comportamento do botão Seguinte</string> + <string name="pref_hardware_previous_button_title">Botão Anterior</string> + <string name="pref_hardware_previous_button_summary">Personalizar comportamento do botão Anterior</string> + <string name="button_action_fast_forward">Avanço rápido</string> + <string name="button_action_rewind">Recuo rápido</string> + <string name="button_action_skip_episode">Ignorar episódio</string> + <string name="button_action_restart_episode">Reiniciar episódio</string> <string name="pref_followQueue_sum">Ir para a episódio seguinte ao terminar a reprodução.</string> <string name="pref_auto_delete_sum">Eliminar episódio ao terminar a reprodução.</string> <string name="pref_auto_delete_title">Eliminação automática</string> @@ -366,8 +385,11 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Desativar</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Definir intervalo</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Definir hora do dia</string> - <string name="pref_autoUpdateIntervallOrTime_every">a cada %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">às %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">A cada hora</item> + <item quantity="other">A cada %d horas</item> + </plurals> <string name="pref_followQueue_title">Reprodução contínua</string> <string name="pref_pauseOnHeadsetDisconnect_title">Auscultadores ou Bluetooth desligados</string> <string name="pref_unpauseOnHeadsetReconnect_title">Auscultadores inseridos</string> @@ -401,7 +423,9 @@ <string name="pref_episode_cache_title">Cache de episódios</string> <string name="pref_episode_cache_summary">Número máximo de episódios descarregados para colocar em cache. A descarga automática será suspensa se este número for atingido.</string> <string name="pref_episode_cover_title">Utilizar capa do episódio</string> - <string name="pref_episode_cover_summary">Utilizar imagem do episódio se disponível. Se desativar esta opção, será utilizada a imagem do podcast.</string> + <string name="pref_episode_cover_summary">Utilizar imagem do episódio, se disponível. Se desativar esta opção, será utilizada a imagem do podcast.</string> + <string name="pref_show_remain_time_title">Mostrar tempo restante </string> + <string name="pref_show_remain_time_summary">Mostra o tempo restante do episódio se a opção estiver ativa. Se desativar esta opção, será mostrado o tempo total.</string> <string name="pref_theme_title_use_system">Utilizar tema do sistema</string> <string name="pref_theme_title_light">Claro</string> <string name="pref_theme_title_dark">Escuro</string> @@ -421,8 +445,6 @@ <string name="pref_gpodnet_full_sync_title">Impor sincronização total</string> <string name="pref_gpodnet_full_sync_sum">Sincronizar todas as subscrições e estados dos episódios com o gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Sessão iniciada como <i>%1$s</i> com o dispositivo <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Falha de sinconização</string> - <string name="pref_gpodnet_notifications_sum">Esta definição não é aplicável aos erros de autenticação.</string> <string name="pref_playback_speed_sum">Personalizar disponibilidade das velocidades variáveis de reprodução</string> <string name="pref_feed_playback_speed_sum">Velocidade utilizada para a reprodução áudio dos episódios deste podcast</string> <string name="pref_feed_skip">Ignorar automaticamente</string> @@ -437,8 +459,6 @@ <string name="pref_fast_forward_sum">Personalizar o número de segundos a avançar ao tocar no botão de avanço rápido.</string> <string name="pref_rewind">Tempo a recuar</string> <string name="pref_rewind_sum">Personalizar o número de segundos a recuar ao tocar no botão de recuo rápido.</string> - <string name="pref_gpodnet_sethostname_title">Definir nome de servidor</string> - <string name="pref_gpodnet_sethostname_use_default_host">Utilizar predefinições</string> <string name="pref_expandNotify_title">Prioridade da notificação</string> <string name="pref_expandNotify_sum">Normalmente, esta opção é utilizada para expandir a notificação e mostrar os botões de reprodução.</string> <string name="pref_persistNotify_title">Controlos de reprodução</string> @@ -449,10 +469,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Apenas pode selecionar um máximo de %1$d itens.</string> <string name="pref_lockscreen_background_title">Definir fundo do ecrã de bloqueio</string> <string name="pref_lockscreen_background_sum">Define a imagem do episódio como fundo do ecrã de bloqueio. Efeito colateral: também será mostrada em outras aplicações.</string> - <string name="pref_showDownloadReport_title">Falha ao descarregar</string> - <string name="pref_showDownloadReport_sum">Se a descarga falhar, gera um relatório que mostra os detalhes do erro.</string> - <string name="pref_showAutoDownloadReport_title">Descarga automática terminada</string> - <string name="pref_showAutoDownloadReport_sum">Mostrar uma notificação para episódios descarregados automaticamente.</string> <string name="pref_expand_notify_unsupport_toast">As versões Android anteriores à 4.1 não possuem suporte à expansão de notificações</string> <string name="pref_enqueue_location_title">Localização na fila</string> <string name="pref_enqueue_location_sum">Adicionar episódios: %1$s</string> @@ -462,6 +478,7 @@ <string name="pref_smart_mark_as_played_disabled">Desativada</string> <string name="pref_image_cache_size_title">Cache de imagens</string> <string name="pref_image_cache_size_sum">Tamanho para a cache de imagens.</string> + <string name="documentation_support">Documentação e ajuda</string> <string name="visit_user_forum">Fórum de utilizadores</string> <string name="bug_report_title">Reporte de erros</string> <string name="open_bug_tracker">Abrir rastreador de erros</string> @@ -473,14 +490,14 @@ <string name="pref_current_value">Valor atual: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Definir um proxy de rede.</string> - <string name="pref_faq">Questões frequentes</string> <string name="pref_no_browser_found">Navegador web não encontrado</string> <string name="pref_cast_title">Suporte Chromecast</string> <string name="pref_cast_message_play_flavor">Ativar suporte a reprodução multimédia em dispositivos Cast (tais como Chromecast, Android TV...)</string> <string name="pref_cast_message_free_flavor">Chromecast necessita de bibliotecas proprietárias de terceiros que estão desativadas nesta versão do AntennaPod.</string> <string name="pref_enqueue_downloaded_title">Colocar descargas na fila</string> <string name="pref_enqueue_downloaded_summary">Adicionar à fila os episódios descarregados.</string> - <string name="media_player_builtin">Reprodutor nativo Android</string> + <string name="media_player_builtin">Reprodutor nativo Android (descontinuado)</string> + <string name="media_player_sonic">Reprodutor multimédia Sonic (descontinuado)</string> <string name="media_player_exoplayer_recommended">ExoPlayer (recomendado)</string> <string name="media_player_switch_to_exoplayer">Trocar para ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Trocou para ExoPlayer.</string> @@ -569,6 +586,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Definir temporizador</string> <string name="disable_sleeptimer_label">Desativar temporizador</string> + <string name="extend_sleep_timer_label">+%d min.</string> <string name="sleep_timer_label">Temporizador</string> <string name="time_dialog_invalid_input">Tem que introduzir um número inteiro</string> <string name="shake_to_reset_label">Agitar para repor</string> @@ -596,22 +614,22 @@ <string name="gpodnet_suggestions_header">Sugestões</string> <string name="gpodnet_search_hint">Pesquisar em gpodder.net</string> <string name="gpodnetauth_login_title">Acesso</string> - <string name="gpodnetauth_login_descr">Bem-vindo ao processo de acesso ao gpodder.net. Introduza os dados de acesso:</string> <string name="gpodnetauth_login_butLabel">Acesso</string> - <string name="gpodnetauth_login_register">Se ainda não possui uma conta, pode criar uma em:\nhttps://gpodder.net/register/</string> + <string name="create_account">Criar conta</string> <string name="username_label">Utilizador</string> <string name="password_label">Palavra-passe</string> - <string name="gpodnetauth_device_title">Seleção de dispositivo</string> + <string name="gpodnet_description">Gpodder.net é um serviço \'open source\' para sincronização de podcasts, independente, e que não está relacionado com o projeto AntennaPod.</string> + <string name="gpodnetauth_server_official">Servidor oficial gpodder.net</string> + <string name="gpodnetauth_server_custom">Servidor personalizado</string> + <string name="gpodnetauth_host">Nome do servidor</string> + <string name="gpodnetauth_select_server">Selecione o servidor</string> <string name="gpodnetauth_device_descr">Criar um novo dispositivo ou escolher um existente para aceder à sua conta gpodder.net</string> - <string name="gpodnetauth_device_deviceID">ID do dispositivo:\u0020</string> - <string name="gpodnetauth_device_caption">Nome</string> - <string name="gpodnetauth_device_butCreateNewDevice">Criar novo dispositivo</string> - <string name="gpodnetauth_device_chooseExistingDevice">Escolher dispositivo:</string> - <string name="gpodnetauth_device_errorEmpty">ID do dispositivo não pode estar vazia</string> - <string name="gpodnetauth_device_errorAlreadyUsed">ID de dispositivo já utilizada</string> + <string name="gpodnetauth_device_name">Nome do dispositivo</string> + <string name="gpodnetauth_device_name_default">AntennaPod em %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">A descrição não pode estar vazia</string> + <string name="gpodnetauth_existing_devices">Dispositivos existentes</string> + <string name="gpodnetauth_create_device">Criar dispositivo</string> <string name="gpodnetauth_device_butChoose">Escolher</string> - <string name="gpodnetauth_finish_title">Sessão iniciada!</string> <string name="gpodnetauth_finish_descr">Parabéns! A sua conta gpodder.net está vinculada ao seu dispositivo. Agora, já pode sincronizar as subscrições no dispositivo com a sua conta gpodder.net.</string> <string name="gpodnetauth_finish_butsyncnow">Sincronizar agora</string> <string name="gpodnetauth_finish_butgomainscreen">Ir para o ecrã principal</string> @@ -665,6 +683,7 @@ <string name="switch_pages">Trocar de páginas</string> <string name="position">Posição: %1$s</string> <string name="apply_action">Aplicar ação</string> + <string name="play_chapter">Reproduzir capítulo</string> <!--Feed information screen--> <string name="authentication_label">Autenticação</string> <string name="authentication_descr">Altere o seu nome de utilizador e palavra-passe para este podcast e seus episódios</string> @@ -799,18 +818,22 @@ <string name="cast_failed_receiver_player_error">O reprodutor encontrou um erro crítico</string> <string name="cast_failed_media_error_skipping">Erro de reprodução. A ignorar...</string> <!--Notification channels--> + <string name="notification_group_errors">Erros</string> + <string name="notification_group_news">Notícias</string> <string name="notification_channel_user_action">Requer ação</string> <string name="notification_channel_user_action_description">Mostrar se for necessária uma ação como, por exemplo, digitar uma palavra-passe.</string> <string name="notification_channel_downloading">A descarregar</string> <string name="notification_channel_downloading_description">Mostrar durante a descarga.</string> <string name="notification_channel_playing">Reprodução atual</string> <string name="notification_channel_playing_description">Permite o controlo da reprodução. Esta será a notificação que verá ao reproduzir um podcast.</string> - <string name="notification_channel_error">Erros</string> - <string name="notification_channel_error_description">Mostrar se ocorrerem erros como, por exemplo, não for possível descarregar ou atualizar a fonte.</string> - <string name="notification_channel_sync_error">Erros de sincronização</string> + <string name="notification_channel_download_error">Erro ao descarregar</string> + <string name="notification_channel_download_error_description">Mostrada se ocorrerem erros ao descarregar/atualizar.</string> + <string name="notification_channel_sync_error">Erro de sincronização</string> <string name="notification_channel_sync_error_description">Mostrar se não for possível sincronizar com gpodder.</string> - <string name="notification_channel_auto_download">Descargas automáticas</string> + <string name="notification_channel_auto_download">Descarga automática terminada</string> <string name="notification_channel_episode_auto_download">Notificar quando os episódios forem descarregados automaticamente.</string> + <string name="notification_channel_new_episode">Novo episódio</string> + <string name="notification_channel_new_episode_description">Mostrada se for encontrado um novo episódio de uma fonte, se as notificações estiverem ativas.</string> <!--Widget settings--> <string name="widget_settings">Definições do widget</string> <string name="widget_create_button">Criar widget</string> diff --git a/core/src/main/res/values-ru/strings.xml b/core/src/main/res/values-ru/strings.xml index 90284b2cc..5a6ba094d 100644 --- a/core/src/main/res/values-ru/strings.xml +++ b/core/src/main/res/values-ru/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Статистика</string> <string name="add_feed_label">Добавить подкаст</string> <string name="episodes_label">Выпуски</string> + <string name="queue_label">Очередь</string> <string name="all_episodes_short_label">Все</string> <string name="new_episodes_label">Новые</string> <string name="favorite_episodes_label">Избранное</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Воспроизведение</string> <string name="download_statistics_label">Загрузки</string> <string name="notification_pref_fragment">Уведомления</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" не найдено</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Общее время прослушивания выпусков:</string> <string name="statistics_details_dialog">%1$d из %2$d выпусков начато.\n\nПрослушано %3$s из %4$s.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Ничего</string> <!--Bug report activity--> <string name="log_file_share_exception">Совместимых приложений не найдено</string> + <string name="export_logs_menu_title">Экспортировать подробные логи</string> + <string name="confirm_export_log_dialog_message">Подробные логи могут содержать конфиденциальные данные, такие как перечень ваших подписок</string> <!--Webview actions--> <string name="open_in_browser_label">Открыть в браузере</string> <string name="copy_url_label">Скопировать ссылку</string> @@ -81,7 +86,6 @@ <string name="description_label">Описание</string> <string name="episodes_suffix">\u0020выпуск(ов)</string> <string name="processing_label">Обработка</string> - <string name="save_username_password_label">Сохранить имя пользователя и пароль</string> <string name="close_label">Закрыть</string> <string name="retry_label">Повторить</string> <string name="auto_download_label">Добавить в автозагрузки</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">Выключено</string> <string name="feed_volume_reduction_light">Слабое</string> <string name="feed_volume_reduction_heavy">Сильное</string> - <string name="parallel_downloads_suffix">\u0020одновременных загрузок</string> + <string name="parallel_downloads">%1$d одновременных загрузок</string> <string name="feed_auto_download_global">По умолчанию для всех</string> <string name="feed_auto_download_always">Всегда</string> <string name="feed_auto_download_never">Никогда</string> <string name="send_label">Отправить…</string> <string name="episode_cleanup_never">Никогда</string> + <string name="episode_cleanup_except_favorite_removal">Когда не в избранном</string> <string name="episode_cleanup_queue_removal">Когда не в очереди</string> <string name="episode_cleanup_after_listening">После прослушивания</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -119,7 +124,28 @@ <item quantity="many">Выбрано: %d</item> <item quantity="other">Выбрано: %d</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d выпуск</item> + <item quantity="few">%d выпуска</item> + <item quantity="many">%d выпусков</item> + <item quantity="other">%d выпусков</item> + </plurals> <string name="loading_more">Загружается…</string> + <string name="episode_notification">Уведомление о новом выпуске</string> + <string name="episode_notification_summary">Показывать уведомление при появлении новых выпусков</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s: новый выпуск</item> + <item quantity="few">%2$s: %1$d новых выпуска</item> + <item quantity="many">%2$s: %1$d новых выпусков</item> + <item quantity="other">%2$s: %1$d новых выпусков</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Новый выпуск</item> + <item quantity="few">Новые выпуски</item> + <item quantity="many">Новые выпуски</item> + <item quantity="other">Новые выпуски</item> + </plurals> + <string name="new_episode_notification_group_text">В списке Ваших подписок появились новые выпуски</string> <!--Actions on feeds--> <string name="mark_all_read_label">Отметить как прослушанное</string> <string name="mark_all_read_msg">Отметить все выпуски как прослушанные</string> @@ -151,7 +177,6 @@ <string name="hide_not_queued_episodes_label">Не в очереди</string> <string name="hide_has_media_label">С файлами</string> <string name="filtered_label">Отфильтровано</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Последнее обновление не удалось</string> <string name="open_podcast">Открыть подкаст</string> <string name="please_wait_for_data">Подождите, пока загружаются данные</string> <!--actions on feeditems--> @@ -168,6 +193,12 @@ <string name="delete_label">Удалить</string> <string name="delete_failed">Невозможно удалить файл. Попробуйте перезагрузить устройство.</string> <string name="delete_episode_label">Удалить выпуск</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d выпуск выбран, %d загрузка удалена</item> + <item quantity="few">%d выпуска выбрано, %d загрузка(и) удалены</item> + <item quantity="many">%d выпусков выбрано, %d загрузок удалено</item> + <item quantity="other">%d выпусков выбрано, %d загрузок удалено</item> + </plurals> <string name="remove_new_flag_label">Убрать пометку «Новый»</string> <string name="removed_new_flag_label">Пометка «Новый» убрана</string> <string name="mark_read_label">Отметить как прослушанное</string> @@ -222,16 +253,12 @@ <string name="download_error_details">Подробнее</string> <string name="download_error_details_message">%1$s \n\nСсылка на файл:\n%2$s</string> <string name="download_error_device_not_found">Устройство хранения не найдено</string> - <string name="download_error_insufficient_space">Недостаточно места</string> <string name="download_error_http_data_error">Ошибка данных HTTP</string> <string name="download_error_error_unknown">Неизвестная ошибка</string> - <string name="download_error_parser_exception">Ошибка обработки</string> <string name="download_error_unsupported_type">Неподдерживаемый тип канала</string> <string name="download_error_connection_error">Ошибка соединения</string> - <string name="download_error_unknown_host">Неизвестный узел</string> <string name="download_error_unauthorized">Ошибка авторизации</string> <string name="download_error_file_type_type">Ошибка типа файла</string> - <string name="download_error_forbidden">Запрещено</string> <string name="download_canceled_msg">Загрузка отменена</string> <string name="download_canceled_autodownload_enabled_msg">Загрузка отменена\n <i>Автозагрузка</i> отключена для этого выпуска</string> <string name="download_report_title">Загрузки завершились с ошибкой</string> @@ -247,14 +274,7 @@ <item quantity="many">Осталось %d загрузок</item> <item quantity="other">Осталось %d загрузок</item> </plurals> - <string name="downloads_processing">Производится загрузка</string> <string name="download_notification_title">Получение данных подкаста</string> - <plurals name="download_report_content"> - <item quantity="one">%d загрузка завершена, %d не удалось</item> - <item quantity="few">%d загрузок завершено, %d не удалось</item> - <item quantity="many"> %d загрузок завершено, %d не удалось</item> - <item quantity="other">%d загрузок завершено, %d не удалось</item> - </plurals> <string name="download_log_title_unknown">Неизвестное название</string> <string name="download_type_feed">Канал</string> <string name="download_type_media">Медиафайл</string> @@ -287,6 +307,7 @@ <string name="player_go_to_picture_in_picture">Картинка в картинке</string> <string name="unknown_media_key">AntennaPod - неизвестный ключ носителя: %1$d</string> <string name="error_file_not_found">Файл не найден</string> + <string name="no_media_label">Элемент не содержит медиафайла</string> <!--Queue operations--> <string name="lock_queue">Заблокировать очередь</string> <string name="unlock_queue">Разблокировать очередь</string> @@ -343,7 +364,6 @@ <string name="storage_pref">Хранилище</string> <string name="storage_sum">Автоматическое удаление выпусков, импорт, экспорт</string> <string name="project_pref">Проект</string> - <string name="queue_label">Очередь</string> <string name="synchronization_pref">Синхронизация</string> <string name="synchronization_sum">Синхронизация с другими устройствами с помощью gpodder.net</string> <string name="automation">Автоматизация</string> @@ -354,19 +374,24 @@ <string name="external_elements">Внешние органы управления</string> <string name="interruptions">Прерывания</string> <string name="playback_control">Управление воспроизведением</string> + <string name="reassign_hardware_buttons">Переназначить физические кнопки</string> <string name="preference_search_hint">Найти…</string> <string name="preference_search_no_results">Нет результатов</string> <string name="preference_search_clear_history">Очистить историю</string> <string name="media_player">Проигрыватель</string> <string name="pref_episode_cleanup_title">Удаление выпусков</string> - <string name="pref_episode_cleanup_summary">Выпуски, которые не стоят в очереди и не отмечены как избранные могут быть удалены для освобождения места под Автозагрузку.</string> + <string name="pref_episode_cleanup_summary">Выпуски, которые должны быть доступны для удаления, если автозагрузке потребуется пространство для новых выпусков</string> <string name="pref_pauseOnDisconnect_sum">Поставить на паузу, когда наушники или Bluetooth отключены</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Продолжать воспроизведение после подключения наушников</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Возобновить, когда восстановится Bluetooth-соединение</string> - <string name="pref_hardwareForwardButtonSkips_title">Пропускать кнопкой перемотки вперед</string> - <string name="pref_hardwareForwardButtonSkips_sum">При нажатии кнопки перемотки вперед на Bluetooth-устройстве включить следующий выпуск</string> - <string name="pref_hardwarePreviousButtonRestarts_title">В начало кнопкой перемотки назад</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">При нажатии на физическую кнопку перемотки назад переходить к началу выпуска вместо перемотки назад</string> + <string name="pref_hardware_forward_button_title">Кнопка Вперед</string> + <string name="pref_hardware_forward_button_summary">Настройка поведения кнопки Вперед</string> + <string name="pref_hardware_previous_button_title">Кнопка Назад</string> + <string name="pref_hardware_previous_button_summary">Настройка поведения кнопки Назад</string> + <string name="button_action_fast_forward">Перемотка вперед</string> + <string name="button_action_rewind">Перемотка назад</string> + <string name="button_action_skip_episode">Пропустить выпуск</string> + <string name="button_action_restart_episode">Повторить выпуск</string> <string name="pref_followQueue_sum">После завершения воспроизведения перейти к следующему в очереди</string> <string name="pref_auto_delete_sum">Удалять выпуск после воспроизведения</string> <string name="pref_auto_delete_title">Автоматическое удаление</string> @@ -386,8 +411,13 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Отключить</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Задать интервал</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Выбрать время</string> - <string name="pref_autoUpdateIntervallOrTime_every">каждые %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">в %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Каждый час</item> + <item quantity="few">Каждые %d часов</item> + <item quantity="many">Каждые %d часов</item> + <item quantity="other">Каждые %d часов</item> + </plurals> <string name="pref_followQueue_title">Непрерывное воспроизведение</string> <string name="pref_pauseOnHeadsetDisconnect_title">Отключение наушников или Bluetooth</string> <string name="pref_unpauseOnHeadsetReconnect_title">При подключении наушников</string> @@ -421,7 +451,9 @@ <string name="pref_episode_cache_title">Кэш выпусков</string> <string name="pref_episode_cache_summary">Общее количество загруженных в кэш выпусков. По достижении этого количества автоматическая загрузка будет приостановлена.</string> <string name="pref_episode_cover_title">Использовать обложку выпуска</string> - <string name="pref_episode_cover_summary">Отображать вместо обложки подкаста обложку выпуска, если она доступна.</string> + <string name="pref_episode_cover_summary">Отображать обложку выпуска в списках, если она доступна. Если не выбрано, приложение будет всегда использовать обложку подкаста.</string> + <string name="pref_show_remain_time_title">Показывать оставшееся время</string> + <string name="pref_show_remain_time_summary">Отображать оставшееся время выпусков. Если не выбрано, будет отображаться общее время.</string> <string name="pref_theme_title_use_system">Использовать системное оформление</string> <string name="pref_theme_title_light">Светлая</string> <string name="pref_theme_title_dark">Темная</string> @@ -441,8 +473,6 @@ <string name="pref_gpodnet_full_sync_title">Выполнить полную синхронизацию</string> <string name="pref_gpodnet_full_sync_sum">Синхронизировать состояния всех подписок и выпусков при помощи gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Вход как <i>%1$s</i> с устройства <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Сбой синхронизации</string> - <string name="pref_gpodnet_notifications_sum">Не затрагивает ошибки авторизации.</string> <string name="pref_playback_speed_sum">Выбрать значения скорости, доступные при воспроизведении</string> <string name="pref_feed_playback_speed_sum">Скорость, с которой будут изначально воспроизводиться выпуски этого подкаста</string> <string name="pref_feed_skip">Автоматический пропуск</string> @@ -457,8 +487,6 @@ <string name="pref_fast_forward_sum">Настройте длину шага в секундах при нажатии кнопки перемотки вперед</string> <string name="pref_rewind">Интервал быстрой перемотки назад</string> <string name="pref_rewind_sum">Настройте длину шага в секундах при нажатии кнопки перемотки назад</string> - <string name="pref_gpodnet_sethostname_title">Задать имя узла</string> - <string name="pref_gpodnet_sethostname_use_default_host">Использовать узел по умолчанию</string> <string name="pref_expandNotify_title">Уведомление с высоким приоритетом</string> <string name="pref_expandNotify_sum">Как правило, разворачивает уведомление, показывая кнопки управления воспроизведением.</string> <string name="pref_persistNotify_title">Постоянные кнопки воспроизведения</string> @@ -469,10 +497,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Нельзя выбрать больше %1$d элементов.</string> <string name="pref_lockscreen_background_title">Менять фон экрана блокировки</string> <string name="pref_lockscreen_background_sum">Изменяет фон экрана блокировки на обложку выпуска. Кроме того показывает обложку в сторонних приложениях.</string> - <string name="pref_showDownloadReport_title">Сбой загрузки</string> - <string name="pref_showDownloadReport_sum">Если загрузка не удается, показывать отчет с подробностями об ошибке.</string> - <string name="pref_showAutoDownloadReport_title">Автоматическая загрузка завершена</string> - <string name="pref_showAutoDownloadReport_sum">Показывать уведомление при автоматической загрузке выпусков</string> <string name="pref_expand_notify_unsupport_toast">Версии Android ниже 4.1 не поддерживают расширенные уведомления.</string> <string name="pref_enqueue_location_title">Размещение в очереди</string> <string name="pref_enqueue_location_sum">Добавлять выпуски %1$s</string> @@ -482,6 +506,7 @@ <string name="pref_smart_mark_as_played_disabled">Отключено</string> <string name="pref_image_cache_size_title">Размер кеша изображений</string> <string name="pref_image_cache_size_sum">Размер дискового кеша изображений</string> + <string name="documentation_support">Документация и Поддержка</string> <string name="visit_user_forum">Форум пользователей</string> <string name="bug_report_title">Сообщить об ошибке</string> <string name="open_bug_tracker">Перейти в систему отслеживания ошибок</string> @@ -493,14 +518,14 @@ <string name="pref_current_value">Текущее значение: %1$s</string> <string name="pref_proxy_title">Прокси</string> <string name="pref_proxy_sum">Настройки прокси</string> - <string name="pref_faq">Часто задаваемые вопросы</string> <string name="pref_no_browser_found">Веб-браузер не обнаружен.</string> <string name="pref_cast_title">Поддержка Chromecast</string> <string name="pref_cast_message_play_flavor">Включить воспроизведение на устройствах с Google Cast (Chromecast, колонки, ТВ на Android TV и др.)</string> <string name="pref_cast_message_free_flavor">Для работы Chromecast требуются собственнические библиотеки третьей стороны, которые не включены в данную версию AntennaPod</string> <string name="pref_enqueue_downloaded_title">Добавлять загруженные в очередь</string> <string name="pref_enqueue_downloaded_summary">Добавлять загруженные выпуски в очередь</string> - <string name="media_player_builtin">Встроенный в Android</string> + <string name="media_player_builtin">Встроенный Android плеер (устарело)</string> + <string name="media_player_sonic">Sonic Media Player (устарело) </string> <string name="media_player_exoplayer_recommended">ExoPlayer (рекомендовано)</string> <string name="media_player_switch_to_exoplayer">Переключить на ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Переключено на ExoPlayer.</string> @@ -589,6 +614,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Установить таймер сна</string> <string name="disable_sleeptimer_label">Отключить таймер сна</string> + <string name="extend_sleep_timer_label">+%d мин.</string> <string name="sleep_timer_label">Таймер сна</string> <string name="time_dialog_invalid_input">Неправильный ввод, время должно быть в виде числа</string> <string name="shake_to_reset_label">Сбросить встряхиванием</string> @@ -622,22 +648,22 @@ <string name="gpodnet_suggestions_header">Рекомендации</string> <string name="gpodnet_search_hint">Искать на gpodder.net</string> <string name="gpodnetauth_login_title">Войти</string> - <string name="gpodnetauth_login_descr">Добро пожаловать в процесс авторизации на gpodder.net. Сначала введите вашу информацию для авторизации:</string> <string name="gpodnetauth_login_butLabel">Войти</string> - <string name="gpodnetauth_login_register">Если у вас еще нет аккаунта, вы можете создать его здесь:\nhttps://gpodder.net/register/</string> + <string name="create_account">Создать аккаунт</string> <string name="username_label">Имя пользователя</string> <string name="password_label">Пароль</string> - <string name="gpodnetauth_device_title">Выбор устройства</string> + <string name="gpodnet_description">Gpodder.net - сервис управления подкастами с отрытым исходным кодом независящий от проекта AntennaPod.</string> + <string name="gpodnetauth_server_official">Официальный сервер gpodder.net</string> + <string name="gpodnetauth_server_custom">Свой сервер</string> + <string name="gpodnetauth_host">Имя хоста</string> + <string name="gpodnetauth_select_server">Выберите сервер</string> <string name="gpodnetauth_device_descr">Создайте новое устройство, чтобы использовать ваш аккаунт на gpodder.net или выберите существующее:</string> - <string name="gpodnetauth_device_deviceID">Идентификатор устройства:\u0020</string> - <string name="gpodnetauth_device_caption">Название устройства</string> - <string name="gpodnetauth_device_butCreateNewDevice">Создать новое устройство</string> - <string name="gpodnetauth_device_chooseExistingDevice">Выберите существующее устройство:</string> - <string name="gpodnetauth_device_errorEmpty">Поле с Device ID не должно быть пустым</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Device ID уже используется</string> + <string name="gpodnetauth_device_name">Название устройства</string> + <string name="gpodnetauth_device_name_default">AntennaPod на %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Обязательно заполнить</string> + <string name="gpodnetauth_existing_devices">Существующие устройства</string> + <string name="gpodnetauth_create_device">Создать устройство</string> <string name="gpodnetauth_device_butChoose">Выберите</string> - <string name="gpodnetauth_finish_title">Авторизация успешна!</string> <string name="gpodnetauth_finish_descr">Поздравляем! Ваш аккаунт на gpodder.net теперь связан с вашим устройством. AntennaPod теперь сможет автоматически синхронизировать ваши подписки с аккаунтом gpodder.net</string> <string name="gpodnetauth_finish_butsyncnow">Начать синхронизацию</string> <string name="gpodnetauth_finish_butgomainscreen">Перейти на главный экран</string> @@ -691,6 +717,7 @@ <string name="switch_pages">Переключить страницу</string> <string name="position">Позиция: %1$s</string> <string name="apply_action">Применить действие</string> + <string name="play_chapter">Запустить главу</string> <!--Feed information screen--> <string name="authentication_label">Авторизация</string> <string name="authentication_descr">Изменить имя пользователя и пароль для этого подкаста и его выпусков.</string> @@ -699,7 +726,7 @@ <string name="episode_filters_description">Перечень условий по включению или исключению выпуска из списков автоматической загрузки</string> <string name="episode_filters_include">Включить</string> <string name="episode_filters_exclude">Исключить</string> - <string name="episode_filters_hint">По одному слову \n«По фразе»</string> + <string name="episode_filters_hint">По одному слову \n\"По фразе\"</string> <string name="keep_updated">Постоянно обновлять</string> <string name="keep_updated_summary">Обновлять подкаст при (авто)обновлении всех подкастов</string> <string name="auto_download_disabled_globally">Автоматическая загрузка отключена в основных настройках AntennaPod</string> @@ -825,18 +852,22 @@ <string name="cast_failed_receiver_player_error">Серьезная ошибка воспроизведения в устройстве Google cast</string> <string name="cast_failed_media_error_skipping">Ошибка воспроизведения. Пропускаю…</string> <!--Notification channels--> + <string name="notification_group_errors">Ошибки</string> + <string name="notification_group_news">Обновления</string> <string name="notification_channel_user_action">Требуется действие</string> <string name="notification_channel_user_action_description">Показывается, когда от Вас требуется действие, например, ввести пароль.</string> <string name="notification_channel_downloading">Загружается</string> <string name="notification_channel_downloading_description">Показывается во время загрузки.</string> <string name="notification_channel_playing">Сейчас воспроизводится</string> <string name="notification_channel_playing_description">Позволяет управлять воспроизведением. Основное уведомление, показывается при воспроизведении подкаста.</string> - <string name="notification_channel_error">Ошибки</string> - <string name="notification_channel_error_description">Отображается, если что-то пошло не так, например, если не удалось загрузить или обновить канал.</string> - <string name="notification_channel_sync_error">Ошибки синхронизации</string> + <string name="notification_channel_download_error">Сбой загрузки</string> + <string name="notification_channel_download_error_description">Отображается, когда загрузка или обновление канала завершилось с ошибкой</string> + <string name="notification_channel_sync_error">Сбой синхронизации</string> <string name="notification_channel_sync_error_description">Отображается, если сбоит синхронизация gpodder.</string> - <string name="notification_channel_auto_download">Автозагрузка</string> + <string name="notification_channel_auto_download">Автоматическая загрузка завершена</string> <string name="notification_channel_episode_auto_download">Показывается, когда новые выпуски были автоматически загружены</string> + <string name="notification_channel_new_episode">Новый выпуск</string> + <string name="notification_channel_new_episode_description">Отображается, когда появился новый выпуск подкаста, для которого включены уведомления</string> <!--Widget settings--> <string name="widget_settings">Настройки виджета</string> <string name="widget_create_button">Создать виджет</string> diff --git a/core/src/main/res/values-sk/strings.xml b/core/src/main/res/values-sk/strings.xml new file mode 100644 index 000000000..163ea7dac --- /dev/null +++ b/core/src/main/res/values-sk/strings.xml @@ -0,0 +1,503 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activity and fragment titles--> + <string name="feed_update_receiver_name">Aktualizovať odbery</string> + <string name="feeds_label">Podcasty</string> + <string name="statistics_label">Štatistiky</string> + <string name="add_feed_label">Pridať podcast</string> + <string name="episodes_label">Epizódy</string> + <string name="all_episodes_short_label">Všetko</string> + <string name="new_episodes_label">Nové</string> + <string name="favorite_episodes_label">Obľúbené</string> + <string name="new_label">Nové</string> + <string name="settings_label">Nastavenia</string> + <string name="downloads_label">Preberanie</string> + <string name="downloads_running_label">Bežiace</string> + <string name="downloads_completed_label">Dokončené</string> + <string name="downloads_log_label">Záznam</string> + <string name="subscriptions_label">Odbery</string> + <string name="subscriptions_list_label">Zoznam odberov</string> + <string name="cancel_download_label">Zrušiť preberanie</string> + <string name="playback_history_label">História prehrávania</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">Prihlásenie do gpodder.net</string> + <string name="episode_cache_full_title">Vyrovnávacia pamäť epizód je plná</string> + <string name="episode_cache_full_message">Bol dosiahnutý prednastavený limit veľkosti vyrovnávacej pamäte epizód. Veľkosť vyrovnávacej pamäte môžete zmeniť v Nastaveniach.</string> + <string name="playback_statistics_label">Prehrávanie</string> + <string name="download_statistics_label">Preberania</string> + <string name="notification_pref_fragment">Oznámenia</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" nebol nájdený</string> + <!--Statistics fragment--> + <string name="total_time_listened_to_podcasts">Celkový čas prehrávaných podcastov:</string> + <string name="statistics_details_dialog">%1$d z %2$d epizód sa začala prehrávať.\n\nPrehrávané %3$s z %4$s.</string> + <string name="statistics_mode">Režim štatistík</string> + <string name="statistics_mode_normal">Spočítať skutočný čas prehrávania. Opätovné prehrávanie sa započíta znovu, zatiaľ čo označenie ako prehrané sa nepočíta vôbec</string> + <string name="statistics_mode_count_all">Súčet všetkých epizód, ktoré boli označené ako prehrané.</string> + <string name="statistics_speed_not_counted">Poznámka: Rýchlosť prehrávania sa nikdy neberie do úvahy.</string> + <string name="statistics_reset_data">Vynulovať štatistiky</string> + <string name="statistics_reset_data_msg">Táto operácia vymaže časovú históriu prehrávania všetkých epizód. Ste si istý že chcete vykonať túto akciu?</string> + <string name="statistics_counting_since">Od %s,\nste prehrali</string> + <!--Download Statistics fragment--> + <string name="total_size_downloaded_podcasts">Celková veľkosť epizód v zariadení:</string> + <!--Main activity--> + <string name="drawer_open">Otvoriť menu</string> + <string name="drawer_close">Zatvoriť menu</string> + <string name="drawer_preferences">Nastavenie lišty</string> + <string name="drawer_feed_order_unplayed_episodes">Usporiadať podľa počítadla</string> + <string name="drawer_feed_order_alphabetical">Usporiadať abecedne</string> + <string name="drawer_feed_order_last_update">Usporiadať podľa dátumu zverejnenia</string> + <string name="drawer_feed_order_most_played">Usporiadať podľa počtu prehratých epizód</string> + <string name="drawer_feed_counter_new_unplayed">Počet nových a neprehratých epizód</string> + <string name="drawer_feed_counter_new">Počet nových epizód</string> + <string name="drawer_feed_counter_unplayed">Počet neprehratých epizód</string> + <string name="drawer_feed_counter_downloaded">Počet stiahnutých epizód</string> + <string name="drawer_feed_counter_none">Žiadne</string> + <!--Bug report activity--> + <string name="log_file_share_exception">Žiadne kompatibilné aplikácie</string> + <string name="export_logs_menu_title">Exportovať podrobné záznamy</string> + <string name="confirm_export_log_dialog_message">Podrobné záznamy môžu obsahovať citlivé informácie ako napr. váš zoznam odoberaných kanálov</string> + <!--Webview actions--> + <string name="open_in_browser_label">Otvoriť v prehliadači</string> + <string name="copy_url_label">Kopírovať odkaz</string> + <string name="share_url_label">Zdielať odkaz</string> + <string name="copied_url_msg">Odkaz skopírovaný do schránky</string> + <string name="go_to_position_label">Skoč na túto pozíciu</string> + <!--Playback history--> + <string name="clear_history_label">Vymazať históriu</string> + <!--Other--> + <string name="confirm_label">Potvrdiť</string> + <string name="cancel_label">Zrušiť</string> + <string name="yes">Áno</string> + <string name="no">Nie</string> + <string name="reset">Vynulovať</string> + <string name="author_label">Autor(i)</string> + <string name="language_label">Jazyk</string> + <string name="url_label">Odkaz</string> + <string name="cover_label">Obrázok</string> + <string name="error_label">Chyba</string> + <string name="error_msg_prefix">Vyskytla sa chyba:</string> + <string name="needs_storage_permission">Pre túto operáciu je potrebné povolenie prístupu k úložisku</string> + <string name="refresh_label">Obnoviť</string> + <string name="external_storage_error_msg">Nie je k dispozícii žiadne externé úložisko. Skontrolujte, či je externé úložisko pripojené tak, aby aplikácia mohla fungovať správne.</string> + <string name="chapters_label">Kapitoly</string> + <string name="chapter_duration">Trvanie: %1$s</string> + <string name="description_label">Popis</string> + <string name="episodes_suffix">\u0020epizódy</string> + <string name="processing_label">Spracováva sa</string> + <string name="close_label">Ukončiť</string> + <string name="retry_label">Skúsiť znova</string> + <string name="auto_download_label">Pridať do automatického sťahovania</string> + <string name="auto_download_apply_to_items_title">Použiť aj na predchádzajúce epizódy</string> + <string name="auto_download_apply_to_items_message">Nové nastavenie <i>Automatické preberanie</i> sa automaticky použije pre nové epizódy.\nChcete ho tiež použiť pre už vydané epizódy?</string> + <string name="auto_delete_label">Automaticky vymazať epizódu</string> + <string name="feed_volume_reduction">Redukcia hlasitosti</string> + <string name="feed_volume_reduction_summary">Redukcia hlasitosti pre všetky epizódy tohoto odberu: %1$s</string> + <string name="feed_volume_reduction_off">Vypnutá</string> + <string name="feed_volume_reduction_light">Slabá</string> + <string name="feed_volume_reduction_heavy">Silná</string> + <string name="parallel_downloads">%1$d súbežné preberania</string> + <string name="feed_auto_download_global">Globálne predvolené</string> + <string name="feed_auto_download_always">Vždy</string> + <string name="feed_auto_download_never">Nikdy</string> + <string name="send_label">Poslať...</string> + <string name="episode_cleanup_never">Nikdy</string> + <string name="episode_cleanup_queue_removal">Ak nie je v poradí</string> + <string name="episode_cleanup_after_listening">Po dokončení</string> + <plurals name="episode_cleanup_hours_after_listening"> + <item quantity="one">1 hodinu po dokončení</item> + <item quantity="few">%d hodiny po dokončení</item> + <item quantity="many">%d hodín po dokončení</item> + <item quantity="other">%dhodín po dokončení </item> + </plurals> + <plurals name="episode_cleanup_days_after_listening"> + <item quantity="one">1 deň po dokončení</item> + <item quantity="few">%d dni po dokončení</item> + <item quantity="many">%d dní po dokončení</item> + <item quantity="other">%d dní po dokončení</item> + </plurals> + <plurals name="num_selected_label"> + <item quantity="one">%d vybraná</item> + <item quantity="few">%d vybrané</item> + <item quantity="many">%d vybraných</item> + <item quantity="other">%d vybraných</item> + </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d epizóda</item> + <item quantity="few">%d epizódy</item> + <item quantity="many">%d epizód</item> + <item quantity="other">%d epizód</item> + </plurals> + <string name="loading_more">Načítavam viac...</string> + <string name="episode_notification">Oznámenia o epizódach</string> + <string name="episode_notification_summary">Pri vydaní novej epizódy zobraziť oznámenie.</string> + <plurals name="new_episode_notification_title"> + <item quantity="one">Nová epizóda</item> + <item quantity="few">Nové epizódy</item> + <item quantity="many">Nové epizódy</item> + <item quantity="other">Nové epizódy</item> + </plurals> + <!--Actions on feeds--> + <string name="mark_all_read_label">Označiť všetko ako prehraté</string> + <string name="mark_all_read_msg">Označiť všetky epizódy ako prehraté</string> + <string name="remove_all_new_flags_label">Odstrániť všetky značky „nové“</string> + <string name="removed_all_new_flags_msg">Všetky značky „nové“ boli odstránené</string> + <string name="remove_all_new_flags_confirmation_msg">Potvrďte, že chcete odstrániť značku „nové“ zo všetkých epizód.</string> + <string name="show_info_label">Zobraziť informácie</string> + <string name="show_feed_settings_label">Zobraziť nastavenia podcastu</string> + <string name="feed_info_label">Informácie o podcaste</string> + <string name="feed_settings_label">Nastavenia podcastu</string> + <string name="rename_feed_label">Premenovať podcast</string> + <string name="remove_feed_label">Odstrániť podcast</string> + <string name="share_label">Zdieľanie</string> + <string name="share_label_with_ellipses">Zdieľať...</string> + <string name="share_file_label">Zdielať súbor</string> + <string name="share_website_url_label">Adresa webovej stránky</string> + <string name="feed_delete_confirmation_local_msg">Potvrďte, prosím, že chcete odstrániť podcast \"%1$s\". Súbory v miestnom priečinku zdroja sa neodstránia.</string> + <string name="feed_remover_msg">Odstraňovanie podcastu</string> + <string name="load_complete_feed">Obnoviť celý podcast</string> + <string name="multi_select">Viacnásobný výber</string> + <string name="select_all_above">Vybrať všetky nad</string> + <string name="select_all_below">Vybrať všetky pod</string> + <string name="hide_unplayed_episodes_label">Neprehraté</string> + <string name="hide_queued_episodes_label">V poradí</string> + <string name="hide_not_queued_episodes_label">Mimo poradia</string> + <string name="hide_has_media_label">S médiami</string> + <string name="filtered_label">Filtrované</string> + <string name="open_podcast">Otvoriť podcast</string> + <string name="please_wait_for_data">Počkajte, kým sa dáta načítajú</string> + <!--actions on feeditems--> + <string name="download_label">Stiahnuť</string> + <plurals name="downloading_batch_label"> + <item quantity="one">Sťahuje sa %d epizóda.</item> + <item quantity="few">Sťahujú sa %d epizódy.</item> + <item quantity="many">Sťahuje sa %d epizód.</item> + <item quantity="other">Sťahuje sa %d epizód.</item> + </plurals> + <string name="play_label">Prehrať</string> + <string name="pause_label">Pozastaviť</string> + <string name="stream_label">Streamovať</string> + <string name="delete_label">Vymazať</string> + <string name="delete_failed">Nemožno vymazať súbor. Skúste reštartovať zariadenie.</string> + <string name="delete_episode_label">Odstrániť epizódu</string> + <string name="remove_new_flag_label">Odstrániť značku „nové“</string> + <string name="removed_new_flag_label">Značka „nové“ bola odstránená</string> + <string name="mark_read_label">Označiť ako prehraté</string> + <string name="marked_as_read_label">Označené ako prehraté</string> + <string name="mark_read_no_media_label">Označiť ako prečítané</string> + <string name="marked_as_read_no_media_label">Označené ako prečítané</string> + <string name="play_this_to_seek_position">Preskočenie na určitú pozíciu funguje len pri prehrávaní epizódy.</string> + <plurals name="marked_read_batch_label"> + <item quantity="one">%depizóda bola označená ako prehraná.</item> + <item quantity="few">%depizódy boli označené ako prehrané.</item> + <item quantity="many">%depizód bolo označených ako prehrané.</item> + <item quantity="other">%depizód bolo označených ako prehrané.</item> + </plurals> + <string name="mark_unread_label">Označiť ako neprehrané</string> + <string name="mark_unread_label_no_media">Označiť ako prehrané</string> + <plurals name="marked_unread_batch_label"> + <item quantity="one">%depizóda bola označená ako neprehraná.</item> + <item quantity="few">%depizódy bolo označené ako neprehrané.</item> + <item quantity="many">%depizód bolo označených ako neprehrané.</item> + <item quantity="other">%depizód bolo označených ako neprehrané.</item> + </plurals> + <string name="add_to_queue_label">Pridať do poradia</string> + <string name="added_to_queue_label">Pridané do poradia</string> + <plurals name="added_to_queue_batch_label"> + <item quantity="one">%depizóda bola pridaná do poradia</item> + <item quantity="few">%depizódy boli pridaná do poradia</item> + <item quantity="many">%depizód bolo pridaných do poradia</item> + <item quantity="other">%depizód bolo pridaných do poradia</item> + </plurals> + <string name="remove_from_queue_label">Odstrániť z poradia</string> + <plurals name="removed_from_queue_batch_label"> + <item quantity="one">%depizóda bola odstránená z poradia</item> + <item quantity="few">%depizódy boli odstránené z poradia</item> + <item quantity="many">%depizód bolo odstránených z poradia</item> + <item quantity="other">%depizód bolo odstránených z poradia</item> + </plurals> + <string name="add_to_favorite_label">Pridať medzi obľúbené</string> + <string name="added_to_favorites">Pridané do obľúbených</string> + <string name="remove_from_favorite_label">Odstrániť z obľúbených</string> + <string name="removed_from_favorites">Odstránené z obľúbených</string> + <string name="visit_website_label">Navštíviť webovú stránku</string> + <string name="skip_episode_label">Preskočiť epizódu</string> + <string name="activate_auto_download">Aktivovať automatické sťahovanie</string> + <string name="deactivate_auto_download">Deaktivovať automatické sťahovanie</string> + <string name="reset_position">Obnoviť pozíciu prehrávania</string> + <string name="removed_item">Položka bola odstránená</string> + <string name="no_items_selected">Nie sú vybrané žiadne položky</string> + <!--Download messages and labels--> + <string name="download_successful">dokončené</string> + <string name="download_pending">Čakajúce preberania</string> + <string name="download_running">Prebieha preberanie</string> + <string name="download_error_details">Podrobnosti</string> + <string name="download_error_details_message">%1$s \n\nURL adresa súboru:\n%2$s</string> + <string name="download_error_device_not_found">Zariadenie úložiska nebolo nájdené</string> + <string name="download_error_http_data_error">Chyba dát HTTP</string> + <string name="download_error_error_unknown">Neznáma chyba</string> + <string name="download_error_unsupported_type">Nepodporovaný typ zdroja</string> + <string name="download_error_connection_error">Chyba pripojenia</string> + <string name="download_error_unauthorized">Chyba overenia</string> + <string name="download_error_file_type_type">Chyba typu súboru</string> + <string name="download_canceled_msg">Preberanie zrušené</string> + <string name="download_report_title">Pri sťahovaní nastali chyby</string> + <string name="download_report_content_title">Hlásenie o preberaniach</string> + <string name="download_error_malformed_url">Chybný odkaz</string> + <string name="download_error_io_error">Chyba IO</string> + <string name="download_error_request_error">Chyba požiadavky</string> + <string name="download_error_db_access">Chyba prístupu k databáze</string> + <plurals name="downloads_left"> + <item quantity="one">Zostáva %d súbor na stiahnutie</item> + <item quantity="few">Zostávajú %d súbory na stiahnutie</item> + <item quantity="many">Zostáva %d súborov na stiahnutie</item> + <item quantity="other">Zostáva %d súborov na stiahnutie</item> + </plurals> + <string name="download_notification_title">Sťahujú sa údaje podcastu</string> + <string name="download_log_title_unknown">Neznámy titul</string> + <string name="download_type_feed">Zdroj</string> + <string name="download_type_media">Mediálny súbor</string> + <string name="download_request_error_dialog_message_prefix">Pri pokuse o prevzatie súboru sa vyskytla chyba:\u0020</string> + <string name="confirm_mobile_download_dialog_title">Potvrďte sťahovanie cez mobilné dáta</string> + <string name="confirm_mobile_download_dialog_message_not_in_queue">Sťahovanie cez mobilné dáta je zakázané v nastaveniach.\n\nMáte na výber dve možnosti. Buď pridáte epizódu do zoznamu na sťahovanie a stiahne sa neskôr, alebo dočasne povolíte sťahovanie cez mobilné dáta.\n\n<small>Vaša voľba bude platná počas nasledujúcich 10 minút pre všetky ďalšie sťahovania epizód.</small></string> + <string name="confirm_mobile_download_dialog_message">Sťahovanie cez mobilné dáta je zakázané v nastaveniach.\n\nChcete dočasne povoliť sťahovanie?\n\n<small>Vaša voľba bude platná počas nasledujúcich 10 minút.</small></string> + <string name="confirm_mobile_streaming_button_always">Vždy</string> + <string name="confirm_mobile_streaming_button_once">Teraz</string> + <string name="confirm_mobile_download_dialog_enable_temporarily">Dočasne povoliť</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Chyba!</string> + <string name="player_stopped_msg">Nič sa neprehráva</string> + <string name="player_preparing_msg">Prebieha príprava</string> + <string name="player_ready_msg">Pripravený</string> + <string name="player_seeking_msg">Hľadá sa</string> + <string name="playback_error_server_died">Server zomrel</string> + <string name="playback_error_unsupported">Nepodporovaný typ média</string> + <string name="playback_error_unknown">Neznáma chyba</string> + <string name="player_go_to_picture_in_picture">Režim obraz v obraze</string> + <string name="error_file_not_found">Súbor nenájdený</string> + <!--Queue operations--> + <string name="checkbox_do_not_show_again">Znovu nezobrazovať</string> + <string name="undo">Späť</string> + <string name="move_to_top_label">Presunúť sa na začiatok</string> + <string name="move_to_bottom_label">Presunúť sa na koniec</string> + <string name="sort">Zoradiť</string> + <string name="keep_sorted">Udržať zoradené</string> + <string name="date">Dátum</string> + <string name="duration">Trvanie</string> + <string name="episode_title">Názov epizódy</string> + <string name="feed_title">Názov podcastu</string> + <string name="random">Náhodne</string> + <string name="ascending">Vzostupne</string> + <string name="descending">Zostupne</string> + <!--Variable Speed--> + <string name="download_plugin_label">Stiahnuť doplnok</string> + <string name="no_playback_plugin_title">Doplnok nie je nainštalovaný</string> + <!--Empty list labels--> + <string name="no_comp_downloads_head_label">Žiadne stiahnuté epizódy</string> + <string name="no_history_head_label">Žiadna história</string> + <string name="no_all_episodes_head_label">Žiadne epizódy</string> + <string name="no_new_episodes_head_label">Žiadne nové epizódy</string> + <string name="no_fav_episodes_head_label">Žiadne obľúbené epizódy</string> + <string name="no_chapters_head_label">Žiadne kapitoly</string> + <string name="no_chapters_label">Táto epizóda nemá žiadne kapitoly.</string> + <string name="no_subscriptions_head_label">Žiadne odbery</string> + <!--Preferences--> + <string name="storage_pref">Úložisko</string> + <string name="project_pref">Projekt</string> + <string name="synchronization_pref">Synchronizácia</string> + <string name="synchronization_sum">Synchronizácia s inými zariadeniami pomocou gpodder.net</string> + <string name="automation">Automatizácia</string> + <string name="download_pref_details">Podrobnosti</string> + <string name="import_export_pref">Import/Export</string> + <string name="import_export_search_keywords">zálohovanie, obnovenie, záloha, obnova, backup, restore</string> + <string name="appearance">Vzhľad</string> + <string name="external_elements">Vonkajšie prvky</string> + <string name="interruptions">Prerušenia</string> + <string name="playback_control">Ovládanie prehrávania</string> + <string name="preference_search_hint">Hľadať…</string> + <string name="preference_search_no_results">Žiadne výsledky</string> + <string name="preference_search_clear_history">Vymazať históriu</string> + <string name="media_player">Prehrávač médií</string> + <string name="pref_auto_delete_sum">Odstrániť epizódu po dokončení prehrávania</string> + <string name="pref_auto_delete_title">Automatické mazanie</string> + <string name="pref_favorite_keeps_episodes_sum">Ponechať epizódy ktoré sú označené ako obľúbené</string> + <string name="pref_favorite_keeps_episodes_title">Ponechať obľúbené epizódy</string> + <string name="playback_pref">Prehrávanie</string> + <string name="network_pref">Sieť</string> + <string name="pref_mobileUpdate_auto_download">Automatické sťahovanie</string> + <string name="pref_mobileUpdate_episode_download">Stiahnutie epizódy</string> + <string name="user_interface_label">Používateľské rozhranie</string> + <string name="pref_set_theme_title">Vybrať motív</string> + <string name="pref_set_theme_sum">Zmeňte vzhľad AntennaPod.</string> + <string name="pref_automatic_download_title">Automatické sťahovanie</string> + <string name="pref_parallel_downloads_title">Paralelné sťahovanie</string> + <string name="pref_theme_title_use_system">Použiť systémový motív</string> + <string name="pref_theme_title_light">Svetlá</string> + <string name="pref_theme_title_dark">Tmavá</string> + <string name="pref_theme_title_trueblack">Čierna (pre AMOLED)</string> + <string name="pref_gpodnet_authenticate_title">Prihlásiť</string> + <string name="pref_gpodnet_logout_title">Odhlásiť</string> + <string name="pref_feed_skip">Automatické preskočenie</string> + <string name="pref_feed_skip_sum">Preskočiť úvodné a záverečné reči.</string> + <string name="pref_feed_skip_ending">Preskočit posledných</string> + <string name="pref_feed_skip_intro">Preskočiť prvých</string> + <string name="pref_feed_skip_ending_toast">Posledných %d sekúnd bolo preskočených</string> + <string name="pref_feed_skip_intro_toast">Prvých %d sekúnd bolo preskočených</string> + <string name="pref_image_cache_size_title">Veľkosť vyrovnávacej pamäte pre obrázky</string> + <string name="pref_image_cache_size_sum">Veľkosť vyrovnávacej pamäte disku pre ukladanie obrázkov.</string> + <string name="bug_report_title">Nahlásiť chybu</string> + <string name="pref_proxy_title">Proxy</string> + <string name="media_player_exoplayer_recommended">ExoPlayer (odporúčané)</string> + <string name="media_player_switch_to_exoplayer">Prepnúť na ExoPlayer</string> + <string name="media_player_switched_to_exoplayer">Prepnuté na ExoPlayer.</string> + <string name="pref_skip_silence_title">Preskočiť ticho v zvuku</string> + <!--About screen--> + <string name="about_pref">O aplikácii</string> + <string name="antennapod_version">Verzia AntennaPod</string> + <string name="contributors">Prispievatelia</string> + <string name="developers">Vývojári</string> + <string name="translators">Prekladatelia</string> + <string name="special_thanks">Špeciálne poďakovanie</string> + <string name="privacy_policy">Zásady ochrany osobných údajov</string> + <string name="licenses">Licencie</string> + <!--Search--> + <!--Synchronization--> + <!--import and export--> + <string name="database">Databáza</string> + <string name="opml">OPML</string> + <string name="html">HTML</string> + <string name="database_import_summary">Importovať databázu AntennaPod z iného zariadenia</string> + <string name="opml_import_label">Importovať súbor OPML</string> + <string name="opml_reader_error">Pri načítaní súboru OPML sa vyskytla chyba:</string> + <string name="select_all_label">Označiť všetko</string> + <string name="deselect_all_label">Zrušiť označenie</string> + <string name="opml_export_label">Exportovať ako OPML</string> + <string name="html_export_label">Exportovať ako HTML</string> + <string name="database_export_label">Export databázy</string> + <string name="database_import_label">Import databázy</string> + <string name="export_error_label">Chyba exportu</string> + <string name="export_success_title">Export bol úspešný</string> + <string name="export_success_sum">Exportovaný súbor bol uložený do:\n\n%1$s</string> + <string name="opml_import_ask_read_permission">Na načítanie súboru OPML je potrebný prístup k externému úložisku</string> + <string name="import_select_file">Vyberte súbor, ktorý chcete importovať</string> + <!--Sleep timer--> + <string name="time_seconds">sekundy</string> + <string name="time_minutes">minúty</string> + <string name="time_hours">hodiny</string> + <plurals name="time_seconds_quantified"> + <item quantity="one">1 sekunda</item> + <item quantity="few">%d sekundy</item> + <item quantity="many">%d sekúnd</item> + <item quantity="other">%d sekúnd</item> + </plurals> + <plurals name="time_minutes_quantified"> + <item quantity="one">1 minúta</item> + <item quantity="few">%d minúty</item> + <item quantity="many">%d minút</item> + <item quantity="other">%d minút</item> + </plurals> + <plurals name="time_hours_quantified"> + <item quantity="one">1 hodina</item> + <item quantity="few">%d hodiny</item> + <item quantity="many">%d hodín</item> + <item quantity="other">%d hodín</item> + </plurals> + <!--gpodder.net--> + <string name="password_label">Heslo</string> + <string name="gpodnetsync_auth_error_title">chyba overenia gpodder.net</string> + <string name="gpodnetsync_error_title">chyba synchronizácie s gpodder.net</string> + <string name="gpodnetsync_error_descr">Počas synchronizácie sa vyskytla chyba:\u0020</string> + <!--Directory chooser--> + <string name="choose_data_directory">Zvoľte priečinok s údajmi</string> + <string name="create_folder_msg">Vytvoriť nový priečinok s názvom \"%1$s\"?</string> + <string name="create_folder_success">Nový priečinok bol vytvorený</string> + <!--Online feed view--> + <!--Content descriptions for image buttons--> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <!--Feed information screen--> + <string name="auto_download_settings_label">Nastavenia automatického preberania</string> + <string name="episode_filters_label">Filter epizód</string> + <string name="episode_filters_include">Pridať</string> + <string name="episode_filters_exclude">Vylúčiť</string> + <string name="keep_updated">Priebežne aktualizovať</string> + <!--Progress information--> + <string name="progress_upgrading_database">Inovácia databázy</string> + <!--AntennaPodSP--> + <!--Add podcast fragment--> + <string name="advanced">Pokročilé</string> + <string name="browse_gpoddernet_label">Prehľadávať gpodder.net</string> + <string name="discover">Prehľadávať</string> + <string name="discover_more">viac »</string> + <!--Local feeds--> + <string name="filter">Filter</string> + <!--Episodes apply actions--> + <string name="all_label">Všetko</string> + <string name="hide_is_favorite_label">Je v obľúbených</string> + <string name="not_favorite">Nie je v obľúbených</string> + <string name="hide_downloaded_episodes_label">Stiahnuté</string> + <string name="hide_not_downloaded_episodes_label">Nestiahnuté</string> + <string name="queued_label">V poradí</string> + <string name="not_queued_label">Nie je v poradí</string> + <string name="hide_paused_episodes_label">Pozastavené</string> + <string name="hide_played_episodes_label">Prehraté</string> + <!--Sort--> + <string name="sort_title_a_z">Názov (A \u2192 Z)</string> + <string name="sort_title_z_a">Názov (A \u2192 Z)</string> + <string name="sort_date_new_old">Dátum (Nové \u2192 Staré)</string> + <string name="sort_date_old_new">Dátum (Staré \u2192 Nové)</string> + <string name="sort_duration_short_long">Trvanie (Krátke \u2192 Dlhé)</string> + <string name="sort_duration_long_short">Trvanie (Dlhé \u2192 Krátke)</string> + <string name="sort_a_z">A \u2192 Z</string> + <string name="sort_z_a">Z \u2192 A</string> + <string name="sort_new_old">Nové \u2192 Staré</string> + <string name="sort_old_new">Staré \u2192 Nové</string> + <string name="sort_short_long">Krátke \u2192 Dlhé</string> + <string name="sort_long_short">Dlhé \u2192 Krátke</string> + <!--Rating dialog--> + <string name="rating_title">Páči sa vám AntennaPod?</string> + <string name="rating_message">Boli by sme radi, keby ste si našli čas a ohodnotili AntennaPod.</string> + <string name="rating_never_label">Dajte mi pokoj</string> + <string name="rating_later_label">Pripomenúť neskôr</string> + <string name="rating_now_label">Jasné, ideme na to!</string> + <!--Share episode dialog--> + <!--Audio controls--> + <string name="audio_controls">Ovládanie zvuku</string> + <string name="playback_speed">Rýchlosť prehrávania</string> + <string name="volume">Zvuk</string> + <string name="left_short">L</string> + <string name="right_short">R</string> + <string name="audio_effects">Zvukové efekty</string> + <string name="sonic_only">Iba Sonic</string> + <string name="exoplayer_only">Iba ExoPlayer</string> + <!--proxy settings--> + <string name="proxy_type_label">Typ</string> + <string name="host_label">Hostiteľ</string> + <string name="port_label">Port</string> + <string name="optional_hint">(Voliteľné)</string> + <string name="proxy_test_label">Test</string> + <string name="proxy_checking">Prebieha kontrola…</string> + <string name="proxy_test_successful">Test bol úspešný</string> + <string name="proxy_test_failed">Test zlyhal</string> + <string name="proxy_host_empty_error">Hostiteľ nemôže byť prázdny</string> + <string name="proxy_host_invalid_error">Hostiteľ nie je platná adresa IP alebo doména</string> + <string name="proxy_port_invalid_error">Neplatný port</string> + <!--Subscriptions fragment--> + <string name="subscription_num_columns">Počet stĺpcov</string> + <!--Casting--> + <string name="cast_failed_to_play">Nepodarilo sa spustiť prehrávanie média</string> + <string name="cast_failed_to_stop">Nepodarilo sa zastaviť prehrávanie média</string> + <string name="cast_failed_to_pause">Nepodarilo sa pozastaviť prehrávanie média</string> + <string name="cast_failed_setting_volume">Hlasitosť sa nepodarilo nastaviť</string> + <string name="cast_failed_receiver_player_error">Prehrávač narazil na vážnu chybu</string> + <string name="cast_failed_media_error_skipping">Pri prehrávaní média sa vyskytla chyba. Preskočí sa…</string> + <!--Notification channels--> + <string name="notification_channel_user_action">Vyžaduje sa akcia</string> + <string name="notification_channel_downloading">Sťahovanie</string> + <!--Widget settings--> + <string name="widget_settings">Nastavenia miniaplikácie</string> + <string name="widget_create_button">Vytvoriť miniaplikáciu</string> + <string name="widget_opacity">Nepriehľadnosť</string> + <!--On-Demand configuration--> + <string name="on_demand_config_setting_changed">Nastavenie bolo úspešne aktualizované.</string> +</resources> diff --git a/core/src/main/res/values-sv/strings.xml b/core/src/main/res/values-sv/strings.xml index fdd6de667..51965977f 100644 --- a/core/src/main/res/values-sv/strings.xml +++ b/core/src/main/res/values-sv/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">Statistik</string> <string name="add_feed_label">Lägg till podcast</string> <string name="episodes_label">Episoder</string> + <string name="queue_label">Kö</string> <string name="all_episodes_short_label">Alla</string> <string name="new_episodes_label">Nytt</string> <string name="favorite_episodes_label">Favoriter</string> @@ -17,7 +18,7 @@ <string name="downloads_log_label">Logg</string> <string name="subscriptions_label">Prenumerationer</string> <string name="subscriptions_list_label">Prenumerationslista</string> - <string name="cancel_download_label">Avbryt\nNedladdning</string> + <string name="cancel_download_label">Avbryt Nedladdning</string> <string name="playback_history_label">Uppspelningshistorik</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">Inloggning till gpodder.net</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">Uppspelning</string> <string name="download_statistics_label">Nedladdningar</string> <string name="notification_pref_fragment">Notifieringar</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" hittades inte</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Total uppspelningstid:</string> <string name="statistics_details_dialog">%1$d av %2$d episoder startade.\n\nSpelat %3$s av %4$s.</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">Inga</string> <!--Bug report activity--> <string name="log_file_share_exception">Hittade inga kompatibla appar</string> + <string name="export_logs_menu_title">Exportera detaljerade loggar</string> + <string name="confirm_export_log_dialog_message">Detaljerade loggar kan innehålla känslig information, såsom din prenumerationslista</string> <!--Webview actions--> <string name="open_in_browser_label">Öppna i webbläsare</string> <string name="copy_url_label">Kopiera URL</string> @@ -81,7 +86,6 @@ <string name="description_label">Beskrivning</string> <string name="episodes_suffix">\u0020episoder</string> <string name="processing_label">Bearbetar</string> - <string name="save_username_password_label">Spara användarnamn och lösenord</string> <string name="close_label">Stäng</string> <string name="retry_label">Försök igen</string> <string name="auto_download_label">Inkludera i automatiska nedladdningar</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">Av</string> <string name="feed_volume_reduction_light">Lätt</string> <string name="feed_volume_reduction_heavy">Tungt</string> - <string name="parallel_downloads_suffix">\u0020parallella nedladdningar</string> + <string name="parallel_downloads">%1$d parallella nedladdningar</string> <string name="feed_auto_download_global">Globala standardinställningar</string> <string name="feed_auto_download_always">Alltid</string> <string name="feed_auto_download_never">Aldrig</string> <string name="send_label">Skicka…</string> <string name="episode_cleanup_never">Aldrig</string> + <string name="episode_cleanup_except_favorite_removal">När ej favorit</string> <string name="episode_cleanup_queue_removal">Om inte köad</string> <string name="episode_cleanup_after_listening">Efter färdigspelad</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -113,7 +118,21 @@ <item quantity="one">%d vald</item> <item quantity="other">%d vald</item> </plurals> + <plurals name="num_episodes"> + <item quantity="one">%d episod</item> + <item quantity="other">%d episoder</item> + </plurals> <string name="loading_more">Laddar mer...</string> + <string name="episode_notification">Episodaviseringar</string> + <string name="episode_notification_summary">Visa en avisering när en episod släpps.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s har en ny episod</item> + <item quantity="other">%2$s har %1$d nya episoder</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">Nya Episoder</item> + <item quantity="other">Nya Episoder</item> + </plurals> <!--Actions on feeds--> <string name="mark_all_read_label">Markera alla som spelade</string> <string name="mark_all_read_msg">Markera alla episoder som spelade</string> @@ -145,7 +164,6 @@ <string name="hide_not_queued_episodes_label">Inte köade</string> <string name="hide_has_media_label">Har media</string> <string name="filtered_label">Filtrerad</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Senaste uppdateringen misslyckades</string> <string name="open_podcast">Öppna podcast</string> <string name="please_wait_for_data">Vänta tills datan laddats</string> <!--actions on feeditems--> @@ -160,6 +178,10 @@ <string name="delete_label">Ta bort</string> <string name="delete_failed">Kunde inte ta bort filen. Testa att starta om enheten.</string> <string name="delete_episode_label">Radera episod</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="one">%d episod vald, %d nedladdning raderad.</item> + <item quantity="other">%d episoder valda, %d nedladdning(ar) raderade.</item> + </plurals> <string name="remove_new_flag_label">Ta bort \"ny\"-flagga</string> <string name="removed_new_flag_label">Tog bort \"ny\"-flagga</string> <string name="mark_read_label">Markera som spelad</string> @@ -206,16 +228,12 @@ <string name="download_error_details">Detaljer</string> <string name="download_error_details_message">%1$s \n\nFil-URL:\n%2$s</string> <string name="download_error_device_not_found">Hittade ingen lagringsenhet</string> - <string name="download_error_insufficient_space">Otillräckligt utrymme</string> <string name="download_error_http_data_error">HTTP data fel</string> <string name="download_error_error_unknown">Okänt fel</string> - <string name="download_error_parser_exception">Tolkningsfel</string> <string name="download_error_unsupported_type">Flödestypen stöds inte</string> <string name="download_error_connection_error">Anslutningsfel</string> - <string name="download_error_unknown_host">Okänd värd</string> <string name="download_error_unauthorized">Autentiseringsfel</string> <string name="download_error_file_type_type">Filtypsfel</string> - <string name="download_error_forbidden">Förbjuden</string> <string name="download_canceled_msg">Nedladdning avbruten</string> <string name="download_canceled_autodownload_enabled_msg">Nedladdning avbruten\nStängde av <i>Automatisk nedladdning</i> för detta föremål</string> <string name="download_report_title">Nedladdningar avslutades med fel</string> @@ -229,12 +247,7 @@ <item quantity="one">%d nedladdning kvar</item> <item quantity="other">%d nedladdningar kvar</item> </plurals> - <string name="downloads_processing">Bearbetar nedladdningar</string> <string name="download_notification_title">Laddar ner podcastdata</string> - <plurals name="download_report_content"> - <item quantity="one">%d nedladdning lyckades, %d misslyckades</item> - <item quantity="other">%dnedladdningar lyckades, %d misslyckades</item> - </plurals> <string name="download_log_title_unknown">Okänd titel</string> <string name="download_type_feed">Flöde</string> <string name="download_type_media">Mediafil</string> @@ -267,6 +280,7 @@ <string name="player_go_to_picture_in_picture">Bild-i-bild läge</string> <string name="unknown_media_key">AntannaPod - Okänd mediaknapp: %1$d</string> <string name="error_file_not_found">Filen hittades inte</string> + <string name="no_media_label">Artikeln innehåller ingen mediafil</string> <!--Queue operations--> <string name="lock_queue">Lås Kön</string> <string name="unlock_queue">Lås upp Kön</string> @@ -323,7 +337,6 @@ <string name="storage_pref">Lagring</string> <string name="storage_sum">Automatisk episodradering, Import, Export</string> <string name="project_pref">Projekt</string> - <string name="queue_label">Kö</string> <string name="synchronization_pref">Synkronisering</string> <string name="synchronization_sum">Synkronisera med andra enheter via gpodder.net</string> <string name="automation">Automatisering</string> @@ -334,19 +347,24 @@ <string name="external_elements">Externa element</string> <string name="interruptions">Avbrott</string> <string name="playback_control">Uppspelningskontroll</string> + <string name="reassign_hardware_buttons">Omfördela hårdvaruknappar</string> <string name="preference_search_hint">Sök...</string> <string name="preference_search_no_results">Inga resultat</string> <string name="preference_search_clear_history">Resnsa historiken</string> <string name="media_player">Mediaspelare</string> <string name="pref_episode_cleanup_title">Episodupprensning</string> - <string name="pref_episode_cleanup_summary">Episoder som inte är i kön och inte är favoriter kan tas bort om Automatisk Nedladdning behöver utrymme för nya episoder</string> + <string name="pref_episode_cleanup_summary">Episoder som får tas bort om Automatisk Nedladdning behöver mer utrymme för nya episoder</string> <string name="pref_pauseOnDisconnect_sum">Pausa uppspelningen när hörlurar eller bluetooth kopplas ifrån.</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Fortsätt uppspelningen när hörlurarna återansluts</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Fortsätt uppspelningen när bluetooth återansluts</string> - <string name="pref_hardwareForwardButtonSkips_title">Knappen spola fram hoppar</string> - <string name="pref_hardwareForwardButtonSkips_sum">Hoppa till nästa episod istället för att snabbspola när snabbspolningsknappen trycks in på en blåtands-enhet.</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Knappen föregående startar om</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Starta om den nuvarande episoden när du trycker på hårdvaruknappen för föregående istället för att spola tillbaka</string> + <string name="pref_hardware_forward_button_title">Knappen nästa</string> + <string name="pref_hardware_forward_button_summary">Ändra beteendet för knappen nästa</string> + <string name="pref_hardware_previous_button_title">Knappen föregående</string> + <string name="pref_hardware_previous_button_summary">Ändra beteendet för knappen föregående</string> + <string name="button_action_fast_forward">Snabbspola framåt</string> + <string name="button_action_rewind">Backa</string> + <string name="button_action_skip_episode">Hoppa över episod</string> + <string name="button_action_restart_episode">Starta om episod</string> <string name="pref_followQueue_sum">Hoppa till nästa i kön när uppspelningen är klar</string> <string name="pref_auto_delete_sum">Ta bort episoden när uppspelningen är klar</string> <string name="pref_auto_delete_title">Automatisk borttagning</string> @@ -366,8 +384,11 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Avaktivera</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Välj intervall</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Välj tid på dagen</string> - <string name="pref_autoUpdateIntervallOrTime_every">var %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">vid %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Varje timme</item> + <item quantity="other">Var %d timmar</item> + </plurals> <string name="pref_followQueue_title">Kontinuerlig uppspelning</string> <string name="pref_pauseOnHeadsetDisconnect_title">Hörlurar eller Bluetooth kopplas bort</string> <string name="pref_unpauseOnHeadsetReconnect_title">Hörlurar återanslutna</string> @@ -401,7 +422,9 @@ <string name="pref_episode_cache_title">Episodcache</string> <string name="pref_episode_cache_summary">Totalt antal nedladdade epidoder som ligger i enhetens cache. Automatisk nedladdning kommer att vänta om detta antal nås.</string> <string name="pref_episode_cover_title">Använd Episodomslag</string> - <string name="pref_episode_cover_summary">Använd episodens egna omslag om tillgängligt. Appen kommer alltid att använda podcastens omslagsbild om rutan lämnas tom.</string> + <string name="pref_episode_cover_summary">Använd episodspecifika omslag i listan när de är tillgängliga. Om urkryssat kommer appen alltid använda podcastens omslagsbild.</string> + <string name="pref_show_remain_time_title">Visa Kvarvarande Tid</string> + <string name="pref_show_remain_time_summary">Visa kvarvarande tid för episoder när ikryssad. Om urkryssad visas totala tiden för episoder.</string> <string name="pref_theme_title_use_system">Använd systemtemat</string> <string name="pref_theme_title_light">Ljust</string> <string name="pref_theme_title_dark">Mörkt</string> @@ -421,8 +444,6 @@ <string name="pref_gpodnet_full_sync_title">Tvinga full synkronisering</string> <string name="pref_gpodnet_full_sync_sum">Synkronisera alla prenumerationer och episodstatus med gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Inloggad som <i>%1$s</i> med enhet <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Synkronisering misslyckades</string> - <string name="pref_gpodnet_notifications_sum">Denna inställning påverkar inte autentiseringsfel.</string> <string name="pref_playback_speed_sum">Anpassa de tillgängliga hastigheterna för variabel uppspelningshastighet</string> <string name="pref_feed_playback_speed_sum">Uppspelningshastigheten att använda för episoder i denna podcast</string> <string name="pref_feed_skip">Automatisk överhoppning</string> @@ -437,8 +458,6 @@ <string name="pref_fast_forward_sum">Anpassa antalet sekunder att hoppa framåt när snabbspolningsknappen används</string> <string name="pref_rewind">Snabbspolningslängd bakåt</string> <string name="pref_rewind_sum">Anpassa antalet sekunder att hoppa bakåt när snabbspolningsknappen bakåt används</string> - <string name="pref_gpodnet_sethostname_title">Sätt värdnamn</string> - <string name="pref_gpodnet_sethostname_use_default_host">Använd standardvärden</string> <string name="pref_expandNotify_title">Hög notifieringsprioritet</string> <string name="pref_expandNotify_sum">Detta expanderar oftast notifieringen och visar uppspelningskontroller.</string> <string name="pref_persistNotify_title">Bestående uppspelningskontroller</string> @@ -449,10 +468,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Du kan bara välja maximalt %1$d st.</string> <string name="pref_lockscreen_background_title">Välj låsskärmens bakgrund</string> <string name="pref_lockscreen_background_sum">Sätt låsskärmens bakgrund till den spelade episodens bild. En bieffekt är att även tredjepartsappar kan visa bilden.</string> - <string name="pref_showDownloadReport_title">Nedladdning misslyckades</string> - <string name="pref_showDownloadReport_sum">Visa en rapport med detaljer om felet när nedladdningar misslyckas.</string> - <string name="pref_showAutoDownloadReport_title">Automatisk nedladdning klar</string> - <string name="pref_showAutoDownloadReport_sum">Visa en avisering när episoder laddats ner automatiskt</string> <string name="pref_expand_notify_unsupport_toast">Androidversioner före 4.1 har inte stöd för expanderade aviseringar.</string> <string name="pref_enqueue_location_title">Köplats</string> <string name="pref_enqueue_location_sum">Lägg till episoder i: %1$s</string> @@ -462,6 +477,7 @@ <string name="pref_smart_mark_as_played_disabled">Avaktiverad</string> <string name="pref_image_cache_size_title">Bildcachestorlek</string> <string name="pref_image_cache_size_sum">Storleken på bildcachen på disken.</string> + <string name="documentation_support">Dokumentation & Support</string> <string name="visit_user_forum">Användarforum</string> <string name="bug_report_title">Rapportera bugg</string> <string name="open_bug_tracker">Öppna buggtrackern</string> @@ -473,14 +489,14 @@ <string name="pref_current_value">Nuvarande värde: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Använd en nätverksproxy</string> - <string name="pref_faq">Frekvent Frågade Frågor</string> <string name="pref_no_browser_found">Ingen webbläsare hittades.</string> <string name="pref_cast_title">Chromecast-stöd</string> <string name="pref_cast_message_play_flavor">Aktivera stöd för fjärruppspelning av media på Cast-enheter (såsom Chromecast, Ljudanläggningar eller Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast kräver propretiära tredjepartsbibliotek som inte är inkluderade i denna version av AntennaPod</string> <string name="pref_enqueue_downloaded_title">Köa Nedladdade</string> <string name="pref_enqueue_downloaded_summary">Lägg nedladdade episoder i uppspelningskön</string> - <string name="media_player_builtin">Andriods inbyggda spelare</string> + <string name="media_player_builtin">Androids inbyggda spelare (föråldrad)</string> + <string name="media_player_sonic">Sonic Mediaspelare (föråldrad)</string> <string name="media_player_exoplayer_recommended">ExoPlayer (rekommenderas)</string> <string name="media_player_switch_to_exoplayer">Byt till ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Bytte till ExpPlayer.</string> @@ -569,6 +585,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">Ställ in sömntimer</string> <string name="disable_sleeptimer_label">Stäng av sömntimer</string> + <string name="extend_sleep_timer_label">+%d min</string> <string name="sleep_timer_label">Sömntimer</string> <string name="time_dialog_invalid_input">Ogiltigt tal, tiden måste vara ett heltal</string> <string name="shake_to_reset_label">Skaka för att återställa</string> @@ -596,22 +613,22 @@ <string name="gpodnet_suggestions_header">FÖRSLAG</string> <string name="gpodnet_search_hint">Sök på gpodder.net</string> <string name="gpodnetauth_login_title">Inloggning</string> - <string name="gpodnetauth_login_descr">Välkommen till inloggningsprocessen för gpodder.net. Först, skriv in din inloggningsinformation:</string> <string name="gpodnetauth_login_butLabel">Logga in</string> - <string name="gpodnetauth_login_register">Om du inte har ett konto än, så kan du skapa ett här:\nhttps://gpodder.net/register/</string> + <string name="create_account">Skapa konto</string> <string name="username_label">Användarnamn</string> <string name="password_label">Lösenord</string> - <string name="gpodnetauth_device_title">Enhetsval</string> + <string name="gpodnet_description">Gpodder.net är en synkroniseringstjänst för podcast som har öppen källkod och är oberoende från AntennaPod projektet.</string> + <string name="gpodnetauth_server_official">Officiella gpodder.net servern</string> + <string name="gpodnetauth_server_custom">Egen Server</string> + <string name="gpodnetauth_host">Värdnamn</string> + <string name="gpodnetauth_select_server">Välj server</string> <string name="gpodnetauth_device_descr">Skapa en ny enhet för ditt gpodder.net konto eller välj en befintlig:</string> - <string name="gpodnetauth_device_deviceID">Enhets ID:\u0020</string> - <string name="gpodnetauth_device_caption">Rubrik</string> - <string name="gpodnetauth_device_butCreateNewDevice">Skapa ny enhet</string> - <string name="gpodnetauth_device_chooseExistingDevice">Välj befintlig enhet:</string> - <string name="gpodnetauth_device_errorEmpty">Enhets ID måste fyllas i</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Enhets ID används redan</string> + <string name="gpodnetauth_device_name">Enhetsnamn</string> + <string name="gpodnetauth_device_name_default">AntennaPod på %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Rubrik måste fyllas i</string> + <string name="gpodnetauth_existing_devices">Befintliga enheter</string> + <string name="gpodnetauth_create_device">Skapa enhet</string> <string name="gpodnetauth_device_butChoose">Välj</string> - <string name="gpodnetauth_finish_title">Inloggning lyckades!</string> <string name="gpodnetauth_finish_descr">Grattis! Ditt gpodder.net konto är nu länkat med din enhet. AntennaPod kommer från och med nu automatiskt synkronisera dina prenumerationer på din enhet med ditt gpodder.net konto.</string> <string name="gpodnetauth_finish_butsyncnow">Starta synkronisering nu</string> <string name="gpodnetauth_finish_butgomainscreen">Gå till huvudskärmen</string> @@ -665,6 +682,7 @@ <string name="switch_pages">Byt sida</string> <string name="position">Position: %1$s</string> <string name="apply_action">Utför åtgärd</string> + <string name="play_chapter">Spela kapitel</string> <!--Feed information screen--> <string name="authentication_label">Autentisering</string> <string name="authentication_descr">Byt ditt användarnamn och lösenord för den här podcasten och dess episoder.</string> @@ -799,18 +817,22 @@ <string name="cast_failed_receiver_player_error">Mottagande uppspelaren har stött på ett allvarligt fel</string> <string name="cast_failed_media_error_skipping">Fel vid uppspelning av media. Hoppar över...</string> <!--Notification channels--> + <string name="notification_group_errors">Fel</string> + <string name="notification_group_news">Nyheter</string> <string name="notification_channel_user_action">Åtgärd krävs</string> <string name="notification_channel_user_action_description">Visas om din åtgärd är obligatorisk, till exempel om du behöver ange ett lösenord.</string> <string name="notification_channel_downloading">Laddar ner</string> <string name="notification_channel_downloading_description">Visas under tiden som nedladdning pågår.</string> <string name="notification_channel_playing">Uppspelning pågår</string> <string name="notification_channel_playing_description">Medger kontroll över uppspelning. Detta är huvudnotifieringen som du ser när en podcast spelas.</string> - <string name="notification_channel_error">Fel</string> - <string name="notification_channel_error_description">Visas om något gick fel, till exempel om nedladdning eller flödesuppdatering misslyckas.</string> - <string name="notification_channel_sync_error">Synkroniseringsfel</string> + <string name="notification_channel_download_error">Nedladdning misslyckads</string> + <string name="notification_channel_download_error_description">Visas när nedladdning eller flödesuppdatering misslyckas.</string> + <string name="notification_channel_sync_error">Synkronisering misslyckades</string> <string name="notification_channel_sync_error_description">Visas när synkronisering med gpodder misslyckas.</string> - <string name="notification_channel_auto_download">Automatiska nedladdningar</string> + <string name="notification_channel_auto_download">Automatisk nedladdning klar</string> <string name="notification_channel_episode_auto_download">Visas när episoder har laddats ner automatiskt.</string> + <string name="notification_channel_new_episode">Ny Episod</string> + <string name="notification_channel_new_episode_description">Visas när en ny episod av en podcast hittades, när aviseringar är aktiverade</string> <!--Widget settings--> <string name="widget_settings">Widgetinställningar</string> <string name="widget_create_button">Skapa widget</string> diff --git a/core/src/main/res/values-tr/strings.xml b/core/src/main/res/values-tr/strings.xml index 2a91b66b6..bcaf0a3fb 100644 --- a/core/src/main/res/values-tr/strings.xml +++ b/core/src/main/res/values-tr/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">İstatistikler</string> <string name="add_feed_label">Cep yayını ekle</string> <string name="episodes_label">Bölümler</string> + <string name="queue_label">Kuyruk</string> <string name="all_episodes_short_label">Tümü</string> <string name="new_episodes_label">Yeni</string> <string name="favorite_episodes_label">Favoriler</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">Günlük</string> <string name="subscriptions_label">Abonelikler</string> <string name="subscriptions_list_label">Abonelik Listesi</string> - <string name="cancel_download_label">İndirmeyi İptal Et</string> <string name="playback_history_label">Çalma geçmişi</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">gpodder.net giriş</string> @@ -26,6 +26,7 @@ <string name="playback_statistics_label">Playback</string> <string name="download_statistics_label">Downloads</string> <string name="notification_pref_fragment">Notifications</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">Total time of episodes played:</string> <string name="statistics_details_dialog">%1$d out of %2$d episodes started.\n\nPlayed %3$s out of %4$s.</string> @@ -81,7 +82,6 @@ <string name="description_label">Tanım</string> <string name="episodes_suffix">\u0020bölüm</string> <string name="processing_label">İşleniyor</string> - <string name="save_username_password_label">Kullanıcı adı ve şifreyi kaydet</string> <string name="close_label">Kapat</string> <string name="retry_label">Yeniden dene</string> <string name="auto_download_label">Otomatik indirmelere dahil et</string> @@ -93,7 +93,6 @@ <string name="feed_volume_reduction_off">Off</string> <string name="feed_volume_reduction_light">Light</string> <string name="feed_volume_reduction_heavy">Heavy</string> - <string name="parallel_downloads_suffix">\u0020paralel indirmeler</string> <string name="feed_auto_download_global">Varsayılan ayarlar</string> <string name="feed_auto_download_always">Her zaman</string> <string name="feed_auto_download_never">Hiçbir zaman</string> @@ -145,7 +144,6 @@ <string name="hide_not_queued_episodes_label">Kuyrukta değil</string> <string name="hide_has_media_label">Medya var</string> <string name="filtered_label">Filtrelendi</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Son yenileme başarısız oldu</string> <string name="open_podcast">Cep yayını aç</string> <string name="please_wait_for_data">Please wait until the data is loaded</string> <!--actions on feeditems--> @@ -206,16 +204,12 @@ <string name="download_error_details">Detaylar</string> <string name="download_error_details_message">%1$s \n\nFile URL:\n%2$s</string> <string name="download_error_device_not_found">Depolama aygıtı bulunamadı</string> - <string name="download_error_insufficient_space">Yetersiz alan</string> <string name="download_error_http_data_error">HTTP Veri Hatası</string> <string name="download_error_error_unknown">Bilinmeyen Hata</string> - <string name="download_error_parser_exception">Ayrıştırıcı İstisnası</string> <string name="download_error_unsupported_type">Desteklenmeyen Besleme türü</string> <string name="download_error_connection_error">Baplantı hatası</string> - <string name="download_error_unknown_host">Bilinmeyen sunucu</string> <string name="download_error_unauthorized">Yetkilendirme hatası</string> <string name="download_error_file_type_type">Dosya Tipi Hatası</string> - <string name="download_error_forbidden">Yasak</string> <string name="download_canceled_msg">İndirme iptal edildi</string> <string name="download_canceled_autodownload_enabled_msg">İndirme iptal edildi\nBu öğe için <i>Otomatik İndirme</i> devre dışı</string> <string name="download_report_title">İndirme hata(lar) ile tamamlandı</string> @@ -229,12 +223,7 @@ <item quantity="one">%d indirme kaldı</item> <item quantity="other">%d indirme kaldı</item> </plurals> - <string name="downloads_processing">İndirmeler işleniyor</string> <string name="download_notification_title">Cep yayını verileri indiriliyor</string> - <plurals name="download_report_content"> - <item quantity="one">%d download succeeded, %d failed</item> - <item quantity="other">%d downloads succeeded, %d failed</item> - </plurals> <string name="download_log_title_unknown">bilinmeyen başlık</string> <string name="download_type_feed">Besleme</string> <string name="download_type_media">Medya dosyası</string> @@ -323,7 +312,6 @@ <string name="storage_pref">Depolama</string> <string name="storage_sum">Episode auto delete, Import, Export</string> <string name="project_pref">Proje</string> - <string name="queue_label">Kuyruk</string> <string name="synchronization_pref">Synchronization</string> <string name="synchronization_sum">Synchronize with other devices using gpodder.net</string> <string name="automation">Otomasyon</string> @@ -339,14 +327,9 @@ <string name="preference_search_clear_history">Clear history</string> <string name="media_player">Medya oynatıcı</string> <string name="pref_episode_cleanup_title">Bölüm Temizliği</string> - <string name="pref_episode_cleanup_summary">Yeni bölümleri otomatik indirme için alan gerekirse, kuyrukta veya favorilerde olmayan bölümler otomatik olarak silinebilir</string> <string name="pref_pauseOnDisconnect_sum">Kulaklıklar çıkarıldığında veya bluetooth bağlantısı kesildiğinde çalmayı duraklat</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Kulaklıklar yeniden bağlandığında çalmaya devam et</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Bluetooth yeniden bağlandığında çalmaya devam et</string> - <string name="pref_hardwareForwardButtonSkips_title">İleri düğmesi atlar</string> - <string name="pref_hardwareForwardButtonSkips_sum">Bluetooth ile bağlı bir cihazda ileri düğmesine basmak hızlı ileri sarmak yerine sonraki bölüme atlar</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Geri düğmesi yeniden başlatır</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">Geri düğmesine basmak hızlı geri sarmak yerine mevcut bölümü yeniden oynatır</string> <string name="pref_followQueue_sum">Çalma tamamlandığında kuyruktaki diğer öğeye geç</string> <string name="pref_auto_delete_sum">Çalma bittiğinde bölümü sil</string> <string name="pref_auto_delete_title">Otomatik Silme</string> @@ -366,7 +349,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Devre dışı</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Aralık ayarla</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Belirli saat ayarla</string> - <string name="pref_autoUpdateIntervallOrTime_every">her%1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">süre%1$s</string> <string name="pref_followQueue_title">Devamlı çalma</string> <string name="pref_pauseOnHeadsetDisconnect_title">Headphones or Bluetooth disconnect</string> @@ -401,7 +383,6 @@ <string name="pref_episode_cache_title">Bölüm ön belleği</string> <string name="pref_episode_cache_summary">Total number of downloaded episodes cached on the device. Automatic download will be suspended if this number is reached.</string> <string name="pref_episode_cover_title">Use Episode Cover</string> - <string name="pref_episode_cover_summary">Use the episode specific cover whenever available. If unchecked, the app will always use the podcast cover image.</string> <string name="pref_theme_title_use_system">Use system theme</string> <string name="pref_theme_title_light">Aydınlık</string> <string name="pref_theme_title_dark">Karanlık</string> @@ -421,8 +402,6 @@ <string name="pref_gpodnet_full_sync_title">Force full synchronization</string> <string name="pref_gpodnet_full_sync_sum">Sync all subscriptions and episode states with gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Logged in as <i>%1$s</i> with device <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">Synchronization failed</string> - <string name="pref_gpodnet_notifications_sum">This setting does not apply to authentication errors.</string> <string name="pref_playback_speed_sum">Customize the speeds available for variable speed playback</string> <string name="pref_feed_playback_speed_sum">The speed to use when starting audio playback for episodes in this podcast</string> <string name="pref_feed_skip">Auto Skip</string> @@ -437,8 +416,6 @@ <string name="pref_fast_forward_sum">Customize the number of seconds to jump forward when the fast forward button is clicked</string> <string name="pref_rewind">Rewind Skip Time</string> <string name="pref_rewind_sum">Customize the number of seconds to jump backwards when the rewind button is clicked</string> - <string name="pref_gpodnet_sethostname_title">Sunucu ismini ayarla</string> - <string name="pref_gpodnet_sethostname_use_default_host">Varsayılan sunucuyu kullan</string> <string name="pref_expandNotify_title">High Notification priority</string> <string name="pref_expandNotify_sum">This usually expands the notification to show playback buttons.</string> <string name="pref_persistNotify_title">Kalıcı oynatma kontrolleri</string> @@ -449,10 +426,6 @@ <string name="pref_compact_notification_buttons_dialog_error">You can only select a maximum of %1$d items.</string> <string name="pref_lockscreen_background_title">Set Lockscreen Background</string> <string name="pref_lockscreen_background_sum">Set the lockscreen background to the current episode\'s image. As a side effect, this will also show the image in third party apps.</string> - <string name="pref_showDownloadReport_title">Download failed</string> - <string name="pref_showDownloadReport_sum">Eğer indirme başarısız olursa, hatanın ayrıntılarını gösteren bir rapor oluştur.</string> - <string name="pref_showAutoDownloadReport_title">Automatic download completed</string> - <string name="pref_showAutoDownloadReport_sum">Show a notification for automatically downloaded episodes.</string> <string name="pref_expand_notify_unsupport_toast">Android 4.1 öncesi sürümler genişletilmiş bildirimleri desteklememektedir.</string> <string name="pref_enqueue_location_title">Enqueue Location</string> <string name="pref_enqueue_location_sum">Add episodes to: %1$s</string> @@ -473,14 +446,13 @@ <string name="pref_current_value">Current value: %1$s</string> <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Set a network proxy</string> - <string name="pref_faq">Frequently Asked Questions</string> <string name="pref_no_browser_found">No web browser found.</string> <string name="pref_cast_title">Chromecast support</string> <string name="pref_cast_message_play_flavor">Enable support for remote media playback on Cast devices (such as Chromecast, Audio Speakers or Android TV)</string> <string name="pref_cast_message_free_flavor">Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPod</string> <string name="pref_enqueue_downloaded_title">Enqueue Downloaded</string> <string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</string> - <string name="media_player_builtin">Built-in Android player</string> + <string name="media_player_exoplayer_recommended">ExoPlayer (tavsiye edilen)</string> <string name="media_player_switch_to_exoplayer">Switch to ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Switched to ExoPlayer.</string> <string name="pref_skip_silence_title">Skip Silence in Audio</string> @@ -505,6 +477,7 @@ <string name="pref_filter_feed_sum">Filter your subscriptions in navigation drawer and subscriptions screen.</string> <string name="no_filter_label">None</string> <string name="subscriptions_are_filtered">Subscriptions are filtered.</string> + <string name="subscriptions_counter_greater_zero">Sayacı sıfırdan büyük olanlar</string> <string name="auto_downloaded">Auto downloaded</string> <string name="not_auto_downloaded">Not auto downloaded</string> <string name="kept_updated">Kept updated</string> @@ -594,22 +567,12 @@ <string name="gpodnet_suggestions_header">ÖNERİLER</string> <string name="gpodnet_search_hint">gpodder.net\'te ara</string> <string name="gpodnetauth_login_title">Giriş</string> - <string name="gpodnetauth_login_descr">gpodder.net giriş işlemine hoşgeldiniz. Önce giriş bilgilerinizi yazın:</string> <string name="gpodnetauth_login_butLabel">Giriş</string> - <string name="gpodnetauth_login_register">Eğer bir hesabınız yoksa, buradan bir tane oluşturabilirsiniz:\nhttps://gpodder.net/register/</string> <string name="username_label">Kullanıcı adı</string> <string name="password_label">Parola</string> - <string name="gpodnetauth_device_title">Cihaz Seçimi</string> <string name="gpodnetauth_device_descr">gpodder.net hesabınızla kullanmak için yeni bir cihaz oluşturun veya var olan bir tanesini seçin:</string> - <string name="gpodnetauth_device_deviceID">Cihaz Kimliği:\u0020</string> - <string name="gpodnetauth_device_caption">Başlık</string> - <string name="gpodnetauth_device_butCreateNewDevice">Yeni cihaz oluştur</string> - <string name="gpodnetauth_device_chooseExistingDevice">Bir cihaz seç:</string> - <string name="gpodnetauth_device_errorEmpty">Cihaz ID\'si boş olamaz</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Cihaz ID\'si zaten var</string> <string name="gpodnetauth_device_caption_errorEmpty">Caption must not be empty</string> <string name="gpodnetauth_device_butChoose">Seç</string> - <string name="gpodnetauth_finish_title">Giriş başarılı</string> <string name="gpodnetauth_finish_descr">Tebrikler! gpodder.net hesabınız cihazınızla ilişkilendirildi. AntennaPod bundan sonra gpodder.net hesabınızla üyeliklerinizi otomatik olarak senkronize edecek.</string> <string name="gpodnetauth_finish_butsyncnow">Senkronizasyonu başlat</string> <string name="gpodnetauth_finish_butgomainscreen">Ana ekrana git</string> @@ -699,9 +662,11 @@ <string name="search_powered_by">Results by %1$s</string> <!--Local feeds--> <string name="add_local_folder">Add local folder</string> + <string name="local_folder">Yerel klasör</string> <string name="reconnect_local_folder">Re-connect local folder</string> <string name="reconnect_local_folder_warning">In case of permission denials, you can use this to re-connect to the exact same folder. Do not select another folder.</string> <string name="local_feed_description">This virtual podcast was created by adding a folder to AntennaPod.</string> + <string name="unable_to_start_system_file_manager">Sistem dosya yöneticisi başlatılamıyor</string> <string name="filter">Filter</string> <!--Episodes apply actions--> <string name="all_label">Tümü</string> @@ -801,11 +766,7 @@ <string name="notification_channel_downloading_description">Shown while currently downloading.</string> <string name="notification_channel_playing">Currently playing</string> <string name="notification_channel_playing_description">Allows to control playback. This is the main notification you see while playing a podcast.</string> - <string name="notification_channel_error">Errors</string> - <string name="notification_channel_error_description">Shown if something went wrong, for example if download or feed update fails.</string> - <string name="notification_channel_sync_error">Synchronization Errors</string> <string name="notification_channel_sync_error_description">Shown when gpodder synchronization fails.</string> - <string name="notification_channel_auto_download">Auto Downloads</string> <string name="notification_channel_episode_auto_download">Shown when episodes have been automatically downloaded.</string> <!--Widget settings--> <string name="widget_settings">Widget settings</string> diff --git a/core/src/main/res/values-uk/strings.xml b/core/src/main/res/values-uk/strings.xml index f9c2abe20..3a5c0910a 100644 --- a/core/src/main/res/values-uk/strings.xml +++ b/core/src/main/res/values-uk/strings.xml @@ -5,6 +5,7 @@ <string name="statistics_label">Статистика</string> <string name="add_feed_label">Додати подкаст</string> <string name="episodes_label">Епізоди</string> + <string name="queue_label">Черга</string> <string name="all_episodes_short_label">Всі</string> <string name="new_episodes_label">Новий</string> <string name="favorite_episodes_label">Улюблені</string> @@ -16,12 +17,12 @@ <string name="downloads_log_label">Журнал</string> <string name="subscriptions_label">Підписки</string> <string name="subscriptions_list_label">Перелік підписок</string> - <string name="cancel_download_label">Скасувати\nзавантаження</string> <string name="playback_history_label">Історія</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">Автентифікуватися на gpodder.net</string> <string name="episode_cache_full_title">Кеш епізодів заповнений</string> <string name="episode_cache_full_message">Досягнута межа розміру кешу епізодів. Розмір кешу можна збільшити в налаштуваннях.</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="statistics_details_dialog">%1$d з %2$d епізодів почато.\n\nПрослухано %3$s з %4$s.</string> <string name="statistics_mode">Режим статистики</string> @@ -72,14 +73,12 @@ <string name="description_label">Опис</string> <string name="episodes_suffix">\u0020епізодів</string> <string name="processing_label">Обробка</string> - <string name="save_username_password_label">Зберегти ім\'я користувача та пароль</string> <string name="close_label">Закрити</string> <string name="retry_label">Повторити знову</string> <string name="auto_download_label">Включити до автозавантаження</string> <string name="auto_download_apply_to_items_title">Застосувати до попередніх епізодів</string> <string name="auto_download_apply_to_items_message">Нове налаштування <i>Автозавантаження</i> буде автоматично застосоване до нових епізодів.\nБажаєте також застосувати його до тих епізодів що були опубліковані раніше?</string> <string name="auto_delete_label">Автовидалення епізоду</string> - <string name="parallel_downloads_suffix">\u0020паралельних завантажень</string> <string name="feed_auto_download_global">За замовчуванням</string> <string name="feed_auto_download_always">Завжди</string> <string name="feed_auto_download_never">Ніколи</string> @@ -121,7 +120,6 @@ <string name="hide_not_queued_episodes_label">Не в черзі</string> <string name="hide_has_media_label">Зі звуком або відео</string> <string name="filtered_label">Фільтровані</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Останнє оновлення було невдалим</string> <string name="open_podcast">Відкрити подкаст</string> <!--actions on feeditems--> <string name="download_label">Завантажити</string> @@ -184,16 +182,12 @@ <string name="download_error_details">Докладно</string> <string name="download_error_details_message">%1$s \n\nПосилання на файл:\n%2$s</string> <string name="download_error_device_not_found">Пристрій зберігання даних не знайдено</string> - <string name="download_error_insufficient_space">Недостатній простір для зберігання</string> <string name="download_error_http_data_error">Помилка HTTP</string> <string name="download_error_error_unknown">Щось трапилось</string> - <string name="download_error_parser_exception">Помилка парсера</string> <string name="download_error_unsupported_type">Тип каналу не підтримується</string> <string name="download_error_connection_error">Помилка з\'єднання</string> - <string name="download_error_unknown_host">Невідомий хост</string> <string name="download_error_unauthorized">Помилка автентифікації</string> <string name="download_error_file_type_type">Помилка типу файлу</string> - <string name="download_error_forbidden">Заборонено</string> <string name="download_canceled_msg">Завантаження скасоване</string> <string name="download_canceled_autodownload_enabled_msg">Завантаження скасоване\n<i>Автозавантаження</i> для цього елементу вимкнуто</string> <string name="download_report_title">Завантаження завершені з помилками</string> @@ -208,7 +202,6 @@ <item quantity="many">%d завантажень залишилось</item> <item quantity="other">%d завантажень залишилось</item> </plurals> - <string name="downloads_processing">Обробка завантаженого</string> <string name="download_notification_title">Завантаження даних подкасту</string> <string name="download_log_title_unknown">Невідомий заголовок</string> <string name="download_type_feed">Канал</string> @@ -285,7 +278,6 @@ <!--Preferences--> <string name="storage_pref">Зберігання</string> <string name="project_pref">Проект</string> - <string name="queue_label">Черга</string> <string name="automation">Автоматизація</string> <string name="download_pref_details">Детально</string> <string name="import_export_pref">Імпорт/Експорт</string> @@ -298,14 +290,9 @@ <string name="preference_search_clear_history">Очистити історію</string> <string name="media_player">Медіа програвач</string> <string name="pref_episode_cleanup_title">Очищення епізодів</string> - <string name="pref_episode_cleanup_summary">Епізоди що не знаходяться в черзі та не помічені як улюблені можуть бути видалені якщо Автозавантажувач потребуватиме місце для нових епізодів.</string> <string name="pref_pauseOnDisconnect_sum">Зупиняти відтворення коли навушники або блютуз від’єднано</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Поновити відтворення коли навушники повторно під’єднано</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Поновити відтворення коли блютуз повторно під’єднано</string> - <string name="pref_hardwareForwardButtonSkips_title">Кнопка перемотки пропускає</string> - <string name="pref_hardwareForwardButtonSkips_sum">При натисканні кнопки вперед на блютус пристрої, переходьте до наступного епізоду замість швидкого перемотування</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Кнопка \"назад\" повертає до початку</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">При натисканні апаратної кнопки \"назад\", замість перемотки, розпочати програвання поточного епізода заново</string> <string name="pref_followQueue_sum">Перейти до наступного епізода в черзі коли поточний закінчено</string> <string name="pref_auto_delete_sum">Видалити епізод після повного відтворення</string> <string name="pref_auto_delete_title">Автовидалення</string> @@ -322,7 +309,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Вимкнути</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Інтервал</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Встановити годину</string> - <string name="pref_autoUpdateIntervallOrTime_every">кожні %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">о %1$s</string> <string name="pref_followQueue_title">Грати безперервно</string> <string name="pref_unpauseOnHeadsetReconnect_title">Повторне під’єднання навушників</string> @@ -346,7 +332,6 @@ <string name="pref_parallel_downloads_title">Паралельні завантаження</string> <string name="pref_episode_cache_title">Кеш епізодів</string> <string name="pref_episode_cover_title">Використовувати обкладинку епізоду</string> - <string name="pref_episode_cover_summary">Відображати обкладинку епізоду замість обкладинки подкаста, якщо вона відрізняється.</string> <string name="pref_theme_title_use_system">Використовувати системну тему</string> <string name="pref_theme_title_light">Світла</string> <string name="pref_theme_title_dark">Темна</string> @@ -364,13 +349,10 @@ <string name="pref_gpodnet_sync_changes_sum">Синхронізувати підписки та зміни стану епізодів з gpodder.net</string> <string name="pref_gpodnet_full_sync_sum">Синхронізувати всі підписки та стан епізодів з gpodder.net.</string> <string name="pref_gpodnet_login_status"><![CDATA[Ви увійшли як <i>%1$s</i> з пристрою <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_sum">Це налаштування не застосовується до помилок автентифікації.</string> <string name="pref_fast_forward">Час, що пропускається кнопкою перемотки вперед</string> <string name="pref_fast_forward_sum">Налаштувати кількість секунд, які пропускаються при натисканні кнопки перемотки вперед</string> <string name="pref_rewind">Час, що пропускається кнопкою відмотки назад</string> <string name="pref_rewind_sum">Налаштувати кількість секунд які відмотуються при натисканні кнопки відмотки назад</string> - <string name="pref_gpodnet_sethostname_title">Встановити ім\'я хоста</string> - <string name="pref_gpodnet_sethostname_use_default_host">Використати хост по замовчанню</string> <string name="pref_expandNotify_title">Високий пріоритет сповіщення</string> <string name="pref_expandNotify_sum">Зазвичай це розширює сповіщення, щоб показати кнопки відтворення.</string> <string name="pref_persistNotify_title">Завжди показувати елементи керування відтворенням</string> @@ -379,7 +361,6 @@ <string name="pref_compact_notification_buttons_dialog_error">Ви можете обрати не більше ніж %1$d кнопок.</string> <string name="pref_lockscreen_background_title">Встановити фон екрана блокування</string> <string name="pref_lockscreen_background_sum">Встановити картинку поточного епізоду як фон екрана блокування. Побічний ефект - це зображення також буде видимим в інших додатках.</string> - <string name="pref_showDownloadReport_sum">У разі помилки при завантаженні створити детальний звіт про помилку.</string> <string name="pref_expand_notify_unsupport_toast">Android до версії 4.1 не підтримує розширені повідомлення.</string> <string name="pref_enqueue_location_sum">Додати епізоди до: %1$s</string> <string name="enqueue_location_after_current">Після поточного епізоду</string> @@ -395,14 +376,12 @@ <string name="pref_current_value">Поточне значення: %1$s</string> <string name="pref_proxy_title">Проксі</string> <string name="pref_proxy_sum">Застосувати проксі сервер</string> - <string name="pref_faq">Часті питання</string> <string name="pref_no_browser_found">Веб браузер не знайдено.</string> <string name="pref_cast_title">Підтримка для Chromecast</string> <string name="pref_cast_message_play_flavor">Включити підтримку програвання на таких пристроях як Chromecast або Android TV</string> <string name="pref_cast_message_free_flavor">Для підтримки Chromecast потрібні бібліотеки які не включені в цю версію AntennaPod</string> <string name="pref_enqueue_downloaded_title">Додати завантаження до черги</string> <string name="pref_enqueue_downloaded_summary">Додавати завантажені епізоди до черги</string> - <string name="media_player_builtin">Стандартний плеєр Android</string> <string name="pref_skip_silence_title">Пропуск тиші</string> <string name="pref_videoBehavior_title"> При виході з відеорежиму</string> <string name="pref_videoBehavior_sum">Поведінка при виході з відео</string> @@ -481,22 +460,12 @@ <string name="gpodnet_suggestions_header">РЕКОМЕНДАЦІЇ</string> <string name="gpodnet_search_hint">Пошук на gpodder.net</string> <string name="gpodnetauth_login_title">Логін</string> - <string name="gpodnetauth_login_descr">Ласкаво просимо до gpodder.net. Зпочатку заповнить вашу інформацію для входу</string> <string name="gpodnetauth_login_butLabel">Логін</string> - <string name="gpodnetauth_login_register">Якщо у вас немає облікового запису, ви можете створити його тут:\nhttps://gpodder.net/register/</string> <string name="username_label">Ім\'я користувача</string> <string name="password_label">Пароль</string> - <string name="gpodnetauth_device_title">Обрати пристрій</string> <string name="gpodnetauth_device_descr">Під\'єднати новий пристрій к gpodder.net обліковому запису о обрати інсуючий</string> - <string name="gpodnetauth_device_deviceID">ID Пристрою:\u0020</string> - <string name="gpodnetauth_device_caption">Заголовок</string> - <string name="gpodnetauth_device_butCreateNewDevice">Створити новий пристрій</string> - <string name="gpodnetauth_device_chooseExistingDevice">Вибрати існуючий пристрій</string> - <string name="gpodnetauth_device_errorEmpty">ID пристрою не можете бути пустим</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Таке ID пристрою вже є</string> <string name="gpodnetauth_device_caption_errorEmpty">Підпис не повинен бути пустим</string> <string name="gpodnetauth_device_butChoose">Обрати</string> - <string name="gpodnetauth_finish_title">Успішно зайшли</string> <string name="gpodnetauth_finish_descr">Поздоровляємо! Ваш обліковий запис на gpodder.net зараз пов\'язаний за вашим пристроєм</string> <string name="gpodnetauth_finish_butsyncnow">Почати синхронізацію</string> <string name="gpodnetauth_finish_butgomainscreen">Перейти до основного екрана</string> @@ -648,7 +617,6 @@ <string name="notification_channel_downloading_description">Показується під час завантаження.</string> <string name="notification_channel_playing">Відтворюється зараз</string> <string name="notification_channel_playing_description">Дозволяє керувати відтворенням. Це основне сповіщення, яке ви бачите під час відтворення подкасту.</string> - <string name="notification_channel_error">Помилки</string> <!--Widget settings--> <string name="widget_settings">Налаштування віджету</string> <string name="widget_create_button">Створити віджет</string> diff --git a/core/src/main/res/values-zh-rCN/strings.xml b/core/src/main/res/values-zh-rCN/strings.xml index fb74f256c..5846db831 100644 --- a/core/src/main/res/values-zh-rCN/strings.xml +++ b/core/src/main/res/values-zh-rCN/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">统计</string> <string name="add_feed_label">添加播客</string> <string name="episodes_label">曲目</string> + <string name="queue_label">播放列表</string> <string name="all_episodes_short_label">全部</string> <string name="new_episodes_label">最新</string> <string name="favorite_episodes_label">收藏</string> @@ -26,6 +27,8 @@ <string name="playback_statistics_label">回放</string> <string name="download_statistics_label">下载</string> <string name="notification_pref_fragment">通知</string> + <!--Google Assistant--> + <string name="app_action_not_found">\"%1$s\" 未找到</string> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">节目总播放时间:</string> <string name="statistics_details_dialog">听过了总计 %2$d 期播客中的 %1$d 期。\n\n播放了总计 %4$s 中的 %3$s。</string> @@ -53,6 +56,8 @@ <string name="drawer_feed_counter_none">无</string> <!--Bug report activity--> <string name="log_file_share_exception">没有找到兼容的应用程序</string> + <string name="export_logs_menu_title">导出详细日志</string> + <string name="confirm_export_log_dialog_message">详细日志可能包含敏感信息,如你的订阅列表</string> <!--Webview actions--> <string name="open_in_browser_label">在浏览器打开</string> <string name="copy_url_label">复制 URL</string> @@ -81,7 +86,6 @@ <string name="description_label">描述</string> <string name="episodes_suffix">\u0020 曲</string> <string name="processing_label">处理中</string> - <string name="save_username_password_label">保存用户名密码</string> <string name="close_label">关闭</string> <string name="retry_label">重试</string> <string name="auto_download_label">包含到自动下载</string> @@ -93,12 +97,13 @@ <string name="feed_volume_reduction_off">关闭</string> <string name="feed_volume_reduction_light">轻微</string> <string name="feed_volume_reduction_heavy">显著</string> - <string name="parallel_downloads_suffix">\u0020 并行下载</string> + <string name="parallel_downloads">%1$d 个并行下载</string> <string name="feed_auto_download_global">全局默认</string> <string name="feed_auto_download_always"> 总是</string> <string name="feed_auto_download_never">从不</string> <string name="send_label">发送</string> <string name="episode_cleanup_never">从不</string> + <string name="episode_cleanup_except_favorite_removal">当未收藏时</string> <string name="episode_cleanup_queue_removal">当不在队列中</string> <string name="episode_cleanup_after_listening">结束后</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -110,7 +115,19 @@ <plurals name="num_selected_label"> <item quantity="other">已选中%d</item> </plurals> + <plurals name="num_episodes"> + <item quantity="other">%d 期节目</item> + </plurals> <string name="loading_more">正加载更多…</string> + <string name="episode_notification">节目通知</string> + <string name="episode_notification_summary">当一期新节目发布时,显示一条通知。</string> + <plurals name="new_episode_notification_message"> + <item quantity="other">%2$s有 %1$d 期新节目</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="other">新节目</item> + </plurals> + <string name="new_episode_notification_group_text">你的订阅有新节目</string> <!--Actions on feeds--> <string name="mark_all_read_label">全部标识已读</string> <string name="mark_all_read_msg">将所有曲目标记为已播放</string> @@ -142,7 +159,6 @@ <string name="hide_not_queued_episodes_label">不在播放列表中</string> <string name="hide_has_media_label">包含媒体文件</string> <string name="filtered_label">已过滤的</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} 上次刷新失败</string> <string name="open_podcast">打开播客</string> <string name="please_wait_for_data">请等待数据加载完成</string> <!--actions on feeditems--> @@ -156,6 +172,9 @@ <string name="delete_label">删除</string> <string name="delete_failed">无法删除文件。重启可能解决该问题。</string> <string name="delete_episode_label">删除节目</string> + <plurals name="deleted_multi_episode_batch_label"> + <item quantity="other">%d 期节目被选中,%d 个下载被删除</item> + </plurals> <string name="remove_new_flag_label">移除“新的”标签</string> <string name="removed_new_flag_label">已移除“新的”标签</string> <string name="mark_read_label">标记已播放</string> @@ -198,16 +217,12 @@ <string name="download_error_details">详细信息</string> <string name="download_error_details_message">%1$s \n\nFile URL:\n%2$s</string> <string name="download_error_device_not_found">没有找到存储设备</string> - <string name="download_error_insufficient_space">空间不足</string> <string name="download_error_http_data_error">HTTP 数据错误</string> <string name="download_error_error_unknown">未知错误</string> - <string name="download_error_parser_exception">解析异常</string> <string name="download_error_unsupported_type">未提供的订阅类型</string> <string name="download_error_connection_error">链接错误</string> - <string name="download_error_unknown_host">未知主机</string> <string name="download_error_unauthorized">认证错误</string> <string name="download_error_file_type_type">文件类型错误</string> - <string name="download_error_forbidden">禁用的</string> <string name="download_canceled_msg">已取消下载</string> <string name="download_canceled_autodownload_enabled_msg">已取消下载\n对该曲目禁用<i>自动下载</i></string> <string name="download_report_title">下载完成</string> @@ -220,11 +235,7 @@ <plurals name="downloads_left"> <item quantity="other">剩余%d个下载项</item> </plurals> - <string name="downloads_processing">正在处理下载</string> <string name="download_notification_title">下载播客数据</string> - <plurals name="download_report_content"> - <item quantity="other">%d个下载成功,%d下载失败</item> - </plurals> <string name="download_log_title_unknown">未知标题</string> <string name="download_type_feed">订阅</string> <string name="download_type_media">媒体文件</string> @@ -257,6 +268,7 @@ <string name="player_go_to_picture_in_picture">画中画模式</string> <string name="unknown_media_key">AntennaPod - 未知媒体密钥: %1$d</string> <string name="error_file_not_found">文件未找到</string> + <string name="no_media_label">条目不包含一个媒体文件</string> <!--Queue operations--> <string name="lock_queue">锁定播放列表</string> <string name="unlock_queue">解锁播放列表</string> @@ -313,7 +325,6 @@ <string name="storage_pref">存储</string> <string name="storage_sum">播客剧集自动删除、导入和导出</string> <string name="project_pref">项目</string> - <string name="queue_label">播放列表</string> <string name="synchronization_pref">同步</string> <string name="synchronization_sum">用gpodder.net与其他设备同步</string> <string name="automation">自动化</string> @@ -324,19 +335,24 @@ <string name="external_elements">外部元素</string> <string name="interruptions">中断</string> <string name="playback_control">回放控制</string> + <string name="reassign_hardware_buttons">重新分配硬件按钮</string> <string name="preference_search_hint">搜索</string> <string name="preference_search_no_results">无结果</string> <string name="preference_search_clear_history">清除历史记录</string> <string name="media_player">媒体播放器</string> <string name="pref_episode_cleanup_title">清理曲目</string> - <string name="pref_episode_cleanup_summary">如果自动下载需要为新剧集腾出空间时不在列表和收藏里的剧集可以被移除</string> + <string name="pref_episode_cleanup_summary">在自动下载需要新曲目空间时,应该被删除的旧曲目</string> <string name="pref_pauseOnDisconnect_sum">暂停播放曲目当耳机或蓝牙重新连接</string> <string name="pref_unpauseOnHeadsetReconnect_sum">当耳机重新连接时恢复播放</string> <string name="pref_unpauseOnBluetoothReconnect_sum">恢复播放曲目当蓝牙重新连接</string> - <string name="pref_hardwareForwardButtonSkips_title"> 快进按钮跳过曲目</string> - <string name="pref_hardwareForwardButtonSkips_sum">当按压一个蓝牙连接设备的快进按钮时跳到下一项曲目而不是快进</string> - <string name="pref_hardwarePreviousButtonRestarts_title">“上一个”按钮重启</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">当按压硬件上一个按钮时重新开始播放当前曲目而不是倒回</string> + <string name="pref_hardware_forward_button_title">快进按钮</string> + <string name="pref_hardware_forward_button_summary">自定义快进按钮行为</string> + <string name="pref_hardware_previous_button_title">“上一个” 按钮</string> + <string name="pref_hardware_previous_button_summary">自定义“上一个”按钮行为</string> + <string name="button_action_fast_forward">快进</string> + <string name="button_action_rewind">倒退</string> + <string name="button_action_skip_episode">跳过节目</string> + <string name="button_action_restart_episode">重新开始节目</string> <string name="pref_followQueue_sum">播放完成跳转到播放列表下一项</string> <string name="pref_auto_delete_sum">当播放完成后删除曲目</string> <string name="pref_auto_delete_title">自动删除</string> @@ -356,8 +372,10 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">禁用</string> <string name="pref_autoUpdateIntervallOrTime_Interval">设置间隔</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">设置时间</string> - <string name="pref_autoUpdateIntervallOrTime_every">每%1$s秒</string> <string name="pref_autoUpdateIntervallOrTime_at">第%1$s秒</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="other">每 %d 小时</item> + </plurals> <string name="pref_followQueue_title">连续播放</string> <string name="pref_pauseOnHeadsetDisconnect_title">耳机或蓝牙断开</string> <string name="pref_unpauseOnHeadsetReconnect_title">耳机重新连接</string> @@ -392,7 +410,9 @@ <string name="pref_episode_cache_summary">缓存在设备上的已下载节目总数 若达到此数目,自动下载将被暂停</string> <string name="pref_episode_cover_title">使用音频封面</string> - <string name="pref_episode_cover_summary">只要可以就使用某一集的封面。如果未选中,AntennaPod将始终使用播客的封面图像。</string> + <string name="pref_episode_cover_summary">勾选后,在列表中使用一期节目特定的封面图。如果不勾选,应用程序将始终使用播客封面图像。</string> + <string name="pref_show_remain_time_title">显示剩余时间</string> + <string name="pref_show_remain_time_summary">勾选显示节目的剩余时间。如果未选中,则显示所有节目的持续时间。</string> <string name="pref_theme_title_use_system">使用系统主题</string> <string name="pref_theme_title_light">浅色</string> <string name="pref_theme_title_dark">暗色</string> @@ -412,8 +432,6 @@ <string name="pref_gpodnet_full_sync_title">强制完整同步</string> <string name="pref_gpodnet_full_sync_sum">与gpodder.net同步所有订阅和节目状态</string> <string name="pref_gpodnet_login_status"><![CDATA[在设备 <i>%2$s</i> 上以 <i>%1$s</i> 身份登录]]></string> - <string name="pref_gpodnet_notifications_title">同步失败</string> - <string name="pref_gpodnet_notifications_sum">该设置无法适用于验证错误。</string> <string name="pref_playback_speed_sum">自定义可用于变速播放的速度</string> <string name="pref_feed_playback_speed_sum">开始播放此播客中剧集时使用的速度</string> <string name="pref_feed_skip">自动跳过</string> @@ -428,8 +446,6 @@ <string name="pref_fast_forward_sum">自定义每次快进节目的秒数</string> <string name="pref_rewind">倒回跳过时间</string> <string name="pref_rewind_sum">自定义每次倒回节目的秒数</string> - <string name="pref_gpodnet_sethostname_title">设置主机名</string> - <string name="pref_gpodnet_sethostname_use_default_host">使用默认主机</string> <string name="pref_expandNotify_title">高通知优先级</string> <string name="pref_expandNotify_sum">这通常会拓展通知以显示播放按钮。</string> <string name="pref_persistNotify_title">保持播放控制</string> @@ -440,10 +456,6 @@ <string name="pref_compact_notification_buttons_dialog_error">你最多只能同时选择%1$d项</string> <string name="pref_lockscreen_background_title">设置锁屏背景</string> <string name="pref_lockscreen_background_sum">将锁屏背景设置为当前播放节目的封面图(潜在的副作用是图片可能会在出现在第三方应用中)。</string> - <string name="pref_showDownloadReport_title">下载失败</string> - <string name="pref_showDownloadReport_sum">如果下载失败,生成一份显示详细失败信息的报告。</string> - <string name="pref_showAutoDownloadReport_title">自动下载已完成</string> - <string name="pref_showAutoDownloadReport_sum">显示有关自动下载的剧集的通知。</string> <string name="pref_expand_notify_unsupport_toast">Android 4.1 之前不支持扩展通知。</string> <string name="pref_enqueue_location_title">排队位置</string> <string name="pref_enqueue_location_sum">添加音频至:%1$s</string> @@ -453,6 +465,7 @@ <string name="pref_smart_mark_as_played_disabled">已禁用</string> <string name="pref_image_cache_size_title">图像缓存大小</string> <string name="pref_image_cache_size_sum">用于缓存图像的存储空间大小</string> + <string name="documentation_support">文档 & 支持</string> <string name="visit_user_forum">用户论坛</string> <string name="bug_report_title">报告Bug</string> <string name="open_bug_tracker">打开Bug跟踪器</string> @@ -464,14 +477,14 @@ <string name="pref_current_value">当前值:%1$s</string> <string name="pref_proxy_title">代理</string> <string name="pref_proxy_sum">选择一个网络代理</string> - <string name="pref_faq">常见问题</string> <string name="pref_no_browser_found">无网络浏览器</string> <string name="pref_cast_title">Chromecast 支持</string> <string name="pref_cast_message_play_flavor">启用投影设备(例如 Chromecast 、 Audio Speakers 和 Android TV )上对于远端媒体回放的支持</string> <string name="pref_cast_message_free_flavor">Chromecast 所需要的第三方库文件在这个版本的 AntennaPod 中被禁用</string> <string name="pref_enqueue_downloaded_title">已下载队列</string> <string name="pref_enqueue_downloaded_summary">向队列添加已下载的节目</string> - <string name="media_player_builtin">内置安卓播放器</string> + <string name="media_player_builtin">内置安卓播放器 (已废弃)</string> + <string name="media_player_sonic">Sonic 媒体播放器 (已废弃)</string> <string name="media_player_exoplayer_recommended">ExoPlayer (推荐)</string> <string name="media_player_switch_to_exoplayer">转到ExoPlayer</string> <string name="media_player_switched_to_exoplayer">已转至ExoPlayer</string> @@ -560,6 +573,7 @@ <!--Sleep timer--> <string name="set_sleeptimer_label">设置休眠计时器</string> <string name="disable_sleeptimer_label">禁用休眠计时器</string> + <string name="extend_sleep_timer_label">+ %d 分钟</string> <string name="sleep_timer_label">休眠计时器</string> <string name="time_dialog_invalid_input">无效的输入, 时间是一个整数</string> <string name="shake_to_reset_label">摇动以重置</string> @@ -584,22 +598,22 @@ <string name="gpodnet_suggestions_header">建议</string> <string name="gpodnet_search_hint">搜索 gpodder.net</string> <string name="gpodnetauth_login_title">登录</string> - <string name="gpodnetauth_login_descr">欢迎进入 gpodder.net 登录流程. 首先, 输入请你的登录信息:</string> <string name="gpodnetauth_login_butLabel">登录</string> - <string name="gpodnetauth_login_register">如果您目前没有账户,可以从这里创建:\nhttps://gpodder.net/register/</string> + <string name="create_account">创建账户</string> <string name="username_label">用户名</string> <string name="password_label">密码</string> - <string name="gpodnetauth_device_title">设备选择</string> + <string name="gpodnet_description">Gpodder.net 是一个独立于 Antennapd 项目的开源播客同步服务。</string> + <string name="gpodnetauth_server_official">官方 gpodder.net 服务器</string> + <string name="gpodnetauth_server_custom">自定义服务器</string> + <string name="gpodnetauth_host">主机名</string> + <string name="gpodnetauth_select_server">选择服务器</string> <string name="gpodnetauth_device_descr">为你的 gpodder.net 账户创建一个新设备或者选择一个已存在的:</string> - <string name="gpodnetauth_device_deviceID">设备编号: \u0020</string> - <string name="gpodnetauth_device_caption">标题</string> - <string name="gpodnetauth_device_butCreateNewDevice">创建新设备</string> - <string name="gpodnetauth_device_chooseExistingDevice">选择已存在设备</string> - <string name="gpodnetauth_device_errorEmpty">设备编号必须填写</string> - <string name="gpodnetauth_device_errorAlreadyUsed">设备编号已被使用</string> + <string name="gpodnetauth_device_name">设备名</string> + <string name="gpodnetauth_device_name_default">AntennaPod 于 %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">标题不能为空</string> + <string name="gpodnetauth_existing_devices">现有设备</string> + <string name="gpodnetauth_create_device">创建设备</string> <string name="gpodnetauth_device_butChoose">选择</string> - <string name="gpodnetauth_finish_title">登录成功!</string> <string name="gpodnetauth_finish_descr">恭喜! 你的 gpodder.net 帐户与设备已连结完成. 现在开始 AntennaPod 将自动同步你 gpodder.net 帐户内的订阅信息到设备上.</string> <string name="gpodnetauth_finish_butsyncnow">开始同步</string> <string name="gpodnetauth_finish_butgomainscreen">返回主屏</string> @@ -653,6 +667,7 @@ <string name="switch_pages">切换页面</string> <string name="position">位置:%1$s</string> <string name="apply_action">应用动作</string> + <string name="play_chapter">播放章节</string> <!--Feed information screen--> <string name="authentication_label">验证</string> <string name="authentication_descr">给本播客及曲目变更用户名及密码</string> @@ -787,18 +802,22 @@ <string name="cast_failed_receiver_player_error">接收播放器遇到一个严重错误</string> <string name="cast_failed_media_error_skipping">媒体播放出错.跳转中...</string> <!--Notification channels--> + <string name="notification_group_errors">错误</string> + <string name="notification_group_news">新闻</string> <string name="notification_channel_user_action">需要操作</string> <string name="notification_channel_user_action_description">显示是否需要您的操作,比如是否需要您输入一个密码</string> <string name="notification_channel_downloading">正在下载</string> <string name="notification_channel_downloading_description">下载时显示</string> <string name="notification_channel_playing">当前播放</string> <string name="notification_channel_playing_description">允许控制回放。这是播放播客时您所见的主通知。</string> - <string name="notification_channel_error">错误</string> - <string name="notification_channel_error_description">出错时显示,比如下载或订阅源更新失败。</string> - <string name="notification_channel_sync_error">同步错误</string> + <string name="notification_channel_download_error">下载失败</string> + <string name="notification_channel_download_error_description">当下载或源更新失败是显示</string> + <string name="notification_channel_sync_error">同步失败了</string> <string name="notification_channel_sync_error_description">gpodder 同步出错时显示</string> - <string name="notification_channel_auto_download">自动下载</string> + <string name="notification_channel_auto_download">自动下载已完成</string> <string name="notification_channel_episode_auto_download">当节目已自动下载时显示。</string> + <string name="notification_channel_new_episode">新节目</string> + <string name="notification_channel_new_episode_description">当发现一个播客的新节目时显示,前提是在播客中启用通知</string> <!--Widget settings--> <string name="widget_settings">小部件设置</string> <string name="widget_create_button">创建小部件</string> diff --git a/core/src/main/res/values-zh-rTW/strings.xml b/core/src/main/res/values-zh-rTW/strings.xml index 2e3dacecc..c207455d5 100644 --- a/core/src/main/res/values-zh-rTW/strings.xml +++ b/core/src/main/res/values-zh-rTW/strings.xml @@ -6,6 +6,7 @@ <string name="statistics_label">統計</string> <string name="add_feed_label">新增 Podcast</string> <string name="episodes_label">單集</string> + <string name="queue_label">待播清單</string> <string name="all_episodes_short_label">全部</string> <string name="new_episodes_label">最新</string> <string name="favorite_episodes_label">最愛</string> @@ -17,7 +18,6 @@ <string name="downloads_log_label">日誌</string> <string name="subscriptions_label">訂閱</string> <string name="subscriptions_list_label">訂閱列表</string> - <string name="cancel_download_label">取消下載</string> <string name="playback_history_label">播放歷史</string> <string name="gpodnet_main_label">gpodder.net</string> <string name="gpodnet_auth_label">登入 gpodder.net</string> @@ -26,6 +26,7 @@ <string name="playback_statistics_label">播放</string> <string name="download_statistics_label">下載</string> <string name="notification_pref_fragment">通知</string> + <!--Google Assistant--> <!--Statistics fragment--> <string name="total_time_listened_to_podcasts">總播放時長:</string> <string name="statistics_details_dialog">聽過 %1$d/%2$d集。\n\n播過%3$s/%4$s集。</string> @@ -81,7 +82,6 @@ <string name="description_label">描述</string> <string name="episodes_suffix">\u0020單集</string> <string name="processing_label">處理中</string> - <string name="save_username_password_label">保存帳號及密碼</string> <string name="close_label">關閉</string> <string name="retry_label">重試</string> <string name="auto_download_label">加入自動下載</string> @@ -93,7 +93,6 @@ <string name="feed_volume_reduction_off">關閉</string> <string name="feed_volume_reduction_light">輕</string> <string name="feed_volume_reduction_heavy">重</string> - <string name="parallel_downloads_suffix">\u0020項同步下載</string> <string name="feed_auto_download_global">預設值</string> <string name="feed_auto_download_always">總是</string> <string name="feed_auto_download_never">不予下載</string> @@ -142,7 +141,6 @@ <string name="hide_not_queued_episodes_label">未列入待播清單</string> <string name="hide_has_media_label">包含媒體</string> <string name="filtered_label">已過濾</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} 更新失敗</string> <string name="open_podcast">打開 Podcast</string> <string name="please_wait_for_data">資料載入中,請稍候</string> <!--actions on feeditems--> @@ -198,16 +196,12 @@ <string name="download_error_details">詳情</string> <string name="download_error_details_message">%1$s \n\n檔案網址:\n%2$s</string> <string name="download_error_device_not_found">沒找到儲存空間</string> - <string name="download_error_insufficient_space">儲存空間不足</string> <string name="download_error_http_data_error">HTTP 資料有誤</string> <string name="download_error_error_unknown">位置錯誤</string> - <string name="download_error_parser_exception">解析器異常</string> <string name="download_error_unsupported_type">不支援此來源類型</string> <string name="download_error_connection_error">連接錯誤</string> - <string name="download_error_unknown_host">不明主機</string> <string name="download_error_unauthorized">驗證失敗</string> <string name="download_error_file_type_type">文件格式錯誤</string> - <string name="download_error_forbidden">禁止存取</string> <string name="download_canceled_msg">下載已取消</string> <string name="download_canceled_autodownload_enabled_msg">下載已取消\n這一項的 <i>自動下載</i> 已停用</string> <string name="download_report_title">下載已完成,但可能有錯誤</string> @@ -220,11 +214,7 @@ <plurals name="downloads_left"> <item quantity="other">剩餘%d 個下載</item> </plurals> - <string name="downloads_processing">正在下載</string> <string name="download_notification_title">Podcast 資料下載中</string> - <plurals name="download_report_content"> - <item quantity="other">成功下載 %d 個單集,失敗 %d 個</item> - </plurals> <string name="download_log_title_unknown">標題不明</string> <string name="download_type_feed">資料來源</string> <string name="download_type_media">媒體檔案</string> @@ -313,7 +303,6 @@ <string name="storage_pref">儲存空間</string> <string name="storage_sum">自動刪除、匯入、匯出</string> <string name="project_pref">專案</string> - <string name="queue_label">待播清單</string> <string name="synchronization_pref">同步</string> <string name="synchronization_sum">利用 gpodder.net 與其他裝置同步</string> <string name="automation">自動化</string> @@ -329,14 +318,9 @@ <string name="preference_search_clear_history">清除歷史紀錄</string> <string name="media_player">媒體播放器</string> <string name="pref_episode_cleanup_title">刪除單集時機</string> - <string name="pref_episode_cleanup_summary">在暫存集數已滿、自動下載功能需要更多空間的情形下,不在待播清單也未設定為最愛的各單集將被刪除。</string> <string name="pref_pauseOnDisconnect_sum">耳機或藍牙斷開連接時暫停播放</string> <string name="pref_unpauseOnHeadsetReconnect_sum">當耳機再次連接時繼續播放</string> <string name="pref_unpauseOnBluetoothReconnect_sum">當藍牙再次連接時繼續播放</string> - <string name="pref_hardwareForwardButtonSkips_title">快轉鈕視為跳過單集</string> - <string name="pref_hardwareForwardButtonSkips_sum">當按下藍牙連接裝置上的快轉鈕時,不要快轉,而是播放下一集</string> - <string name="pref_hardwarePreviousButtonRestarts_title">倒轉鈕視為重新播放</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">當按下實體的倒轉鈕時,不要倒轉,而是重新播放本集</string> <string name="pref_followQueue_sum">當播放完畢時自動跳至待播清單中的下一集</string> <string name="pref_auto_delete_sum">播放完畢後刪除該集</string> <string name="pref_auto_delete_title">自動刪除</string> @@ -356,7 +340,6 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">停用</string> <string name="pref_autoUpdateIntervallOrTime_Interval">設定週期</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">設定每日定時</string> - <string name="pref_autoUpdateIntervallOrTime_every">每 %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">於 %1$s</string> <string name="pref_followQueue_title">連續播放</string> <string name="pref_pauseOnHeadsetDisconnect_title">耳機或藍牙裝置拔除時</string> @@ -391,7 +374,6 @@ <string name="pref_episode_cache_title">暫存集數</string> <string name="pref_episode_cache_summary">在本機中可以暫存的集數,若達上限則將停止自動下載。</string> <string name="pref_episode_cover_title">使用單集的封面圖</string> - <string name="pref_episode_cover_summary">在單集有專屬封面的情況下使用該封面圖。如果取消,則一律使用 Podcast 的封面圖</string> <string name="pref_theme_title_use_system">依據系統設定</string> <string name="pref_theme_title_light">淡色</string> <string name="pref_theme_title_dark">深色</string> @@ -411,8 +393,6 @@ <string name="pref_gpodnet_full_sync_title">強制全部同步</string> <string name="pref_gpodnet_full_sync_sum">與 gpodder.net 同步所有的訂閱及聆聽狀態。</string> <string name="pref_gpodnet_login_status"><![CDATA[以 <i>%1$s</i> 登入,設備為 <i>%2$s</i>]]></string> - <string name="pref_gpodnet_notifications_title">同步失敗</string> - <string name="pref_gpodnet_notifications_sum">此設定不影響登入驗證錯誤。</string> <string name="pref_playback_speed_sum">自訂可選用的播放速度</string> <string name="pref_feed_playback_speed_sum">播放此 Podcast 中各單集時的播放速度</string> <string name="pref_feed_skip">自動跳過</string> @@ -427,8 +407,6 @@ <string name="pref_fast_forward_sum">自訂快轉鈕要往前快轉多少時間</string> <string name="pref_rewind">倒轉時間</string> <string name="pref_rewind_sum">自訂倒轉鈕要往後倒轉多少時間</string> - <string name="pref_gpodnet_sethostname_title">設定主機</string> - <string name="pref_gpodnet_sethostname_use_default_host">使用預設主機</string> <string name="pref_expandNotify_title">優先通知</string> <string name="pref_expandNotify_sum">此功能通常會加大通知訊息以便顯示控制鈕</string> <string name="pref_persistNotify_title">保留播放控制鈕</string> @@ -439,10 +417,6 @@ <string name="pref_compact_notification_buttons_dialog_error">您最多只能選擇 %1$d 項。</string> <string name="pref_lockscreen_background_title">設定鎖定畫面背景</string> <string name="pref_lockscreen_background_sum">在鎖定畫面背景採用本單集的圖片,同時也會在第三方 App 裡顯示圖片</string> - <string name="pref_showDownloadReport_title">下載失敗</string> - <string name="pref_showDownloadReport_sum">如果下載失敗,產生錯誤相關細節的報告</string> - <string name="pref_showAutoDownloadReport_title">自動下載完畢</string> - <string name="pref_showAutoDownloadReport_sum">顯示自動下載通知</string> <string name="pref_expand_notify_unsupport_toast">Android 4.1 以前尚未支援延伸通知工具。</string> <string name="pref_enqueue_location_title">待播清單新增位置</string> <string name="pref_enqueue_location_sum">將這幾集加到:%1$s</string> @@ -463,14 +437,12 @@ <string name="pref_current_value">當前設定:%1$s</string> <string name="pref_proxy_title">代理伺服器</string> <string name="pref_proxy_sum">設定代理伺服器</string> - <string name="pref_faq">常見問題</string> <string name="pref_no_browser_found">找不到任何瀏覽器</string> <string name="pref_cast_title">支援 Chromecast</string> <string name="pref_cast_message_play_flavor">啟用 Cast 設備(如 Chromecast、Audio Speakers、Google TV 等)遙控播放</string> <string name="pref_cast_message_free_flavor">Chromecast 相關支援需要其他第三方的私權軟體,所以在此版 AntennaPod 中停用。</string> <string name="pref_enqueue_downloaded_title">下載後加入待播清單</string> <string name="pref_enqueue_downloaded_summary">下載單集以後自動加入待播清單</string> - <string name="media_player_builtin">Android 內建播放器</string> <string name="media_player_exoplayer_recommended">ExoPlayer(推薦使用)</string> <string name="media_player_switch_to_exoplayer">切換至 ExoPlayer</string> <string name="media_player_switched_to_exoplayer">已切換至 ExoPlayer</string> @@ -583,22 +555,12 @@ <string name="gpodnet_suggestions_header">建議</string> <string name="gpodnet_search_hint">搜尋 gpodder.net</string> <string name="gpodnetauth_login_title">登入</string> - <string name="gpodnetauth_login_descr">歡迎登入 gpodder.net,請輸入登入資訊:</string> <string name="gpodnetauth_login_butLabel">登入</string> - <string name="gpodnetauth_login_register">如果您還沒有帳號,可以先註冊一個:\nhttps://gpodder.net/register/</string> <string name="username_label">帳號</string> <string name="password_label">密碼</string> - <string name="gpodnetauth_device_title">選擇裝置</string> <string name="gpodnetauth_device_descr">為您的 gpodder.net 帳號建立新設備或選取既有設備:</string> - <string name="gpodnetauth_device_deviceID">設備代號:\u0020</string> - <string name="gpodnetauth_device_caption">標題</string> - <string name="gpodnetauth_device_butCreateNewDevice">新增裝置</string> - <string name="gpodnetauth_device_chooseExistingDevice">選擇現存裝置:</string> - <string name="gpodnetauth_device_errorEmpty">設備代號不能留白</string> - <string name="gpodnetauth_device_errorAlreadyUsed">已經使用此設備代號</string> <string name="gpodnetauth_device_caption_errorEmpty">標題不能留白</string> <string name="gpodnetauth_device_butChoose">選擇</string> - <string name="gpodnetauth_finish_title">登入成功!</string> <string name="gpodnetauth_finish_descr">恭喜,您的 gpodder.net 帳號已經成功連結到當前的設備!AntennaPod 今後將自動與 gpodder.net 同步此設備的訂閱清單。</string> <string name="gpodnetauth_finish_butsyncnow">現在開始同步</string> <string name="gpodnetauth_finish_butgomainscreen">進入主螢幕</string> @@ -792,11 +754,7 @@ <string name="notification_channel_downloading_description">下載時顯示</string> <string name="notification_channel_playing">現正播放</string> <string name="notification_channel_playing_description">允許播放控制。這是您在播放 Podcast 時會看到的主要通知。</string> - <string name="notification_channel_error">錯誤通知</string> - <string name="notification_channel_error_description">如果有任何錯誤(比方說下載或更新來源出錯)時顯示</string> - <string name="notification_channel_sync_error">同步時發生錯誤</string> <string name="notification_channel_sync_error_description">在 gpodder 同步發生錯誤時顯示</string> - <string name="notification_channel_auto_download">自動下載</string> <string name="notification_channel_episode_auto_download">自動下載後顯示</string> <!--Widget settings--> <string name="widget_settings">小工具設定</string> diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 1ab44d847..6b3a10f46 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -96,6 +96,7 @@ </string-array> <string-array name="episode_cleanup_entries"> + <item>@string/episode_cleanup_except_favorite_removal</item> <item>@string/episode_cleanup_queue_removal</item> <item>0</item> <item>1</item> @@ -105,6 +106,20 @@ <item>@string/episode_cleanup_never</item> </string-array> + <string-array name="button_action_options"> + <item>@string/button_action_fast_forward</item> + <item>@string/button_action_rewind</item> + <item>@string/button_action_skip_episode</item> + <item>@string/button_action_restart_episode</item> + </string-array> + + <string-array name="button_action_values"> + <item>@string/keycode_media_fast_forward</item> + <item>@string/keycode_media_rewind</item> + <item>@string/keycode_media_next</item> + <item>@string/keycode_media_previous</item> + </string-array> + <string-array name="enqueue_location_options"> <item>@string/enqueue_location_back</item> <item>@string/enqueue_location_front</item> @@ -119,6 +134,7 @@ </string-array> <string-array name="episode_cleanup_values"> + <item>-3</item> <item>-1</item> <item>0</item> <item>12</item> @@ -234,15 +250,15 @@ </string-array> <string-array name="media_player_options"> + <item>@string/media_player_exoplayer_recommended</item> <item>@string/media_player_builtin</item> <item>@string/media_player_sonic</item> - <item>@string/media_player_exoplayer_recommended</item> </string-array> <string-array name="media_player_values"> + <item>exoplayer</item> <item>builtin</item> <item>sonic</item> - <item>exoplayer</item> </string-array> <!-- sort for podcast screen, not for queue --> diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml index 0a2a8916d..91ecae93d 100644 --- a/core/src/main/res/values/attrs.xml +++ b/core/src/main/res/values/attrs.xml @@ -62,12 +62,6 @@ <attr name="filter_dialog_clear" format="color"/> <attr name="filter_dialog_button_background" format="reference"/> <attr name="ic_notifications" format="reference"/> - - <declare-styleable name="SquareImageView"> - <attr name="direction" format="enum"> - <enum name="width" value="0"/> - <enum name="height" value="1"/> - <enum name="minimum" value="2"/> - </attr> - </declare-styleable> + <attr name="seek_background" format="color" /> + <attr name="ic_share" format="reference"/> </resources> diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index feee88bb4..fc2409e11 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -22,6 +22,8 @@ <color name="highlight_dark">#43707070</color> <color name="highlight_trueblack">#43707070</color> <color name="non_square_icon_background">#22777777</color> + <color name="seek_background_light">#90000000</color> + <color name="seek_background_dark">#905B5B5B</color> <color name="accent_light">#0078C2</color> <color name="accent_dark">#3D8BFF</color> diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml index 3c173b72d..87046cc0f 100644 --- a/core/src/main/res/values/ids.xml +++ b/core/src/main/res/values/ids.xml @@ -23,14 +23,4 @@ <item name="notification_auto_download_report" type="id"/> <item name="notification_playing" type="id"/> <item name="notification_streaming_confirmation" type="id"/> - - <!-- PendingIntent objects that use the same action but different extras need to use a unique request code --> - <item name="pending_intent_download_service_notification" type="id"/> - <item name="pending_intent_download_service_auth" type="id"/> - <item name="pending_intent_download_service_report" type="id"/> - <item name="pending_intent_download_service_autodownload_report" type="id"/> - <item name="pending_intent_allow_stream_always" type="id"/> - <item name="pending_intent_allow_stream_this_time" type="id"/> - <item name="pending_intent_player_activity" type="id"/> - <item name="pending_intent_sync_error" type="id"/> </resources>
\ No newline at end of file diff --git a/core/src/main/res/values/keycodes.xml b/core/src/main/res/values/keycodes.xml new file mode 100644 index 000000000..e0d44ce04 --- /dev/null +++ b/core/src/main/res/values/keycodes.xml @@ -0,0 +1,9 @@ +<resources + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="MissingTranslation"> + + <string name="keycode_media_next">87</string> + <string name="keycode_media_previous">88</string> + <string name="keycode_media_rewind">89</string> + <string name="keycode_media_fast_forward">90</string> +</resources> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 5d6fe3078..2efc36809 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ <string name="statistics_label">Statistics</string> <string name="add_feed_label">Add Podcast</string> <string name="episodes_label">Episodes</string> + <string name="queue_label">Queue</string> <string name="all_episodes_short_label">All</string> <string name="new_episodes_label">New</string> <string name="favorite_episodes_label">Favorites</string> @@ -32,6 +33,9 @@ <string name="download_statistics_label">Downloads</string> <string name="notification_pref_fragment">Notifications</string> + <!-- Google Assistant --> + <string name="app_action_not_found">\"%1$s\" not found</string> + <!-- Statistics fragment --> <string name="total_time_listened_to_podcasts">Total time of episodes played:</string> <string name="statistics_details_dialog">%1$d out of %2$d episodes started.\n\nPlayed %3$s out of %4$s.</string> @@ -95,7 +99,6 @@ <string name="description_label">Description</string> <string name="episodes_suffix">\u0020episodes</string> <string name="processing_label">Processing</string> - <string name="save_username_password_label">Save username and password</string> <string name="close_label">Close</string> <string name="retry_label">Retry</string> <string name="auto_download_label">Include in auto downloads</string> @@ -113,6 +116,7 @@ <string name="feed_auto_download_never">Never</string> <string name="send_label">Send…</string> <string name="episode_cleanup_never">Never</string> + <string name="episode_cleanup_except_favorite_removal">When not favorited</string> <string name="episode_cleanup_queue_removal">When not in queue</string> <string name="episode_cleanup_after_listening">After finishing</string> <plurals name="episode_cleanup_hours_after_listening"> @@ -128,11 +132,21 @@ <item quantity="other">%d selected</item> </plurals> <plurals name="num_episodes"> - <item quantity="zero">no episodes</item> <item quantity="one">%d episode</item> <item quantity="other">%d episodes</item> </plurals> <string name="loading_more">Loading more…</string> + <string name="episode_notification">Episode Notifications</string> + <string name="episode_notification_summary">Show a notification when a new episode is released.</string> + <plurals name="new_episode_notification_message"> + <item quantity="one">%2$s has a new episode</item> + <item quantity="other">%2$s has %1$d new episodes</item> + </plurals> + <plurals name="new_episode_notification_title"> + <item quantity="one">New Episode</item> + <item quantity="other">New Episodes</item> + </plurals> + <string name="new_episode_notification_group_text">Your subscriptions have new episodes.</string> <!-- Actions on feeds --> <string name="mark_all_read_label">Mark all as played</string> @@ -165,7 +179,7 @@ <string name="hide_not_queued_episodes_label">Not queued</string> <string name="hide_has_media_label">Has media</string> <string name="filtered_label">Filtered</string> - <string name="refresh_failed_msg">{fa-exclamation-circle} Last Refresh failed</string> + <string name="refresh_failed_msg">{fa-exclamation-circle} Last Refresh failed. Tap to view details.</string> <string name="open_podcast">Open Podcast</string> <string name="please_wait_for_data">Please wait until the data is loaded</string> @@ -231,18 +245,24 @@ <string name="download_running">Download running</string> <string name="download_error_details">Details</string> <string name="download_error_details_message">%1$s \n\nFile URL:\n%2$s</string> + <string name="download_error_tap_for_details">Tap to view details.</string> <string name="download_error_device_not_found">Storage Device not found</string> - <string name="download_error_insufficient_space">Insufficient Space</string> + <string name="download_error_insufficient_space">There is not enough space left on your device.</string> <string name="download_error_http_data_error">HTTP Data Error</string> <string name="download_error_error_unknown">Unknown Error</string> - <string name="download_error_parser_exception">Parser Exception</string> + <string name="download_error_parser_exception">The podcast host\'s server sent a broken podcast feed.</string> <string name="download_error_unsupported_type">Unsupported Feed Type</string> + <string name="download_error_unsupported_type_html">The podcast host\'s server sent a website, not a podcast.</string> + <string name="download_error_not_found">The podcast host\'s server does not know where to find the file. It may have been deleted.</string> <string name="download_error_connection_error">Connection Error</string> - <string name="download_error_unknown_host">Unknown Host</string> + <string name="download_error_unknown_host">Cannot find the server. Check if the address is typed correctly and if you have a working network connection.</string> <string name="download_error_unauthorized">Authentication Error</string> <string name="download_error_file_type_type">File Type Error</string> - <string name="download_error_forbidden">Forbidden</string> + <string name="download_error_forbidden">The podcast host\'s server refuses to respond.</string> <string name="download_canceled_msg">Download canceled</string> + <string name="download_error_wrong_size">The server connection was lost before completing the download</string> + <string name="download_error_blocked">The download was blocked by another app on your device.</string> + <string name="download_error_certificate">Unable to establish a secure connection. This can mean that another app on your device blocked the download, or that something is wrong with the server certificates.</string> <string name="download_canceled_autodownload_enabled_msg">Download canceled\nDisabled <i>Auto Download</i> for this item</string> <string name="download_report_title">Downloads completed with error(s)</string> <string name="auto_download_report_title">Auto-downloads completed</string> @@ -255,12 +275,8 @@ <item quantity="one">%d download left</item> <item quantity="other">%d downloads left</item> </plurals> - <string name="downloads_processing">Processing downloads</string> + <string name="service_shutting_down">Service shutting down</string> <string name="download_notification_title">Downloading podcast data</string> - <plurals name="download_report_content"> - <item quantity="one">%d download succeeded, %d failed</item> - <item quantity="other">%d downloads succeeded, %d failed</item> - </plurals> <string name="download_log_title_unknown">Unknown Title</string> <string name="download_type_feed">Feed</string> <string name="download_type_media">Media file</string> @@ -356,7 +372,6 @@ <string name="storage_pref">Storage</string> <string name="storage_sum">Episode auto delete, Import, Export</string> <string name="project_pref">Project</string> - <string name="queue_label">Queue</string> <string name="synchronization_pref">Synchronization</string> <string name="synchronization_sum">Synchronize with other devices using gpodder.net</string> <string name="automation">Automation</string> @@ -367,19 +382,24 @@ <string name="external_elements">External elements</string> <string name="interruptions">Interruptions</string> <string name="playback_control">Playback control</string> + <string name="reassign_hardware_buttons">Reassign hardware buttons</string> <string name="preference_search_hint">Search…</string> <string name="preference_search_no_results">No results</string> <string name="preference_search_clear_history">Clear history</string> <string name="media_player">Media player</string> <string name="pref_episode_cleanup_title">Episode Cleanup</string> - <string name="pref_episode_cleanup_summary">Episodes that aren\'t in the queue and aren\'t favorites should be eligible for removal if Auto Download needs space for new episodes</string> + <string name="pref_episode_cleanup_summary">Episodes that should be eligible for removal if Auto Download needs space for new episodes</string> <string name="pref_pauseOnDisconnect_sum">Pause playback when headphones or bluetooth are disconnected</string> <string name="pref_unpauseOnHeadsetReconnect_sum">Resume playback when the headphones are reconnected</string> <string name="pref_unpauseOnBluetoothReconnect_sum">Resume playback when bluetooth reconnects</string> - <string name="pref_hardwareForwardButtonSkips_title">Forward Button Skips</string> - <string name="pref_hardwareForwardButtonSkips_sum">When pressing a forward button on a bluetooth-connected device skip to the next episode instead of fast-forwarding</string> - <string name="pref_hardwarePreviousButtonRestarts_title">Previous button restarts</string> - <string name="pref_hardwarePreviousButtonRestarts_sum">When pressing a hardware previous button restart playing the current episode instead of rewinding</string> + <string name="pref_hardware_forward_button_title">Forward Button</string> + <string name="pref_hardware_forward_button_summary">Customize the forward button behavior</string> + <string name="pref_hardware_previous_button_title">Previous Button</string> + <string name="pref_hardware_previous_button_summary">Customize the previous button behavior</string> + <string name="button_action_fast_forward">Fast Forward</string> + <string name="button_action_rewind">Rewind</string> + <string name="button_action_skip_episode">Skip Episode</string> + <string name="button_action_restart_episode">Restart Episode</string> <string name="pref_followQueue_sum">Jump to next queue item when playback completes</string> <string name="pref_auto_delete_sum">Delete episode when playback completes</string> <string name="pref_auto_delete_title">Auto Delete</string> @@ -399,8 +419,11 @@ <string name="pref_autoUpdateIntervallOrTime_Disable">Disable</string> <string name="pref_autoUpdateIntervallOrTime_Interval">Set Interval</string> <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Set Time of Day</string> - <string name="pref_autoUpdateIntervallOrTime_every">every %1$s</string> <string name="pref_autoUpdateIntervallOrTime_at">at %1$s</string> + <plurals name="pref_autoUpdateIntervallOrTime_every_hours"> + <item quantity="one">Every hour</item> + <item quantity="other">Every %d hours</item> + </plurals> <string name="pref_followQueue_title">Continuous Playback</string> <string name="pref_pauseOnHeadsetDisconnect_title">Headphones or Bluetooth disconnect</string> <string name="pref_unpauseOnHeadsetReconnect_title">Headphones Reconnect</string> @@ -434,7 +457,9 @@ <string name="pref_episode_cache_title">Episode Cache</string> <string name="pref_episode_cache_summary">Total number of downloaded episodes cached on the device. Automatic download will be suspended if this number is reached.</string> <string name="pref_episode_cover_title">Use Episode Cover</string> - <string name="pref_episode_cover_summary">Use the episode specific cover whenever available. If unchecked, the app will always use the podcast cover image.</string> + <string name="pref_episode_cover_summary">Use the episode specific cover in lists whenever available. If unchecked, the app will always use the podcast cover image.</string> + <string name="pref_show_remain_time_title">Show Remaining Time</string> + <string name="pref_show_remain_time_summary">Display remaining time of episodes when checked. If unchecked, display total duration of episodes.</string> <string name="pref_theme_title_use_system">Use system theme</string> <string name="pref_theme_title_light">Light</string> <string name="pref_theme_title_dark">Dark</string> @@ -468,8 +493,6 @@ <string name="pref_fast_forward_sum">Customize the number of seconds to jump forward when the fast forward button is clicked</string> <string name="pref_rewind">Rewind Skip Time</string> <string name="pref_rewind_sum">Customize the number of seconds to jump backwards when the rewind button is clicked</string> - <string name="pref_gpodnet_sethostname_title">Set hostname</string> - <string name="pref_gpodnet_sethostname_use_default_host">Use default host</string> <string name="pref_expandNotify_title">High Notification priority</string> <string name="pref_expandNotify_sum">This usually expands the notification to show playback buttons.</string> <string name="pref_persistNotify_title">Persistent Playback Controls</string> @@ -507,8 +530,8 @@ <string name="pref_cast_message_free_flavor">Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPod</string> <string name="pref_enqueue_downloaded_title">Enqueue Downloaded</string> <string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</string> - <string name="media_player_builtin">Built-in Android player</string> - <string name="media_player_sonic" translatable="false">Sonic Media Player</string> + <string name="media_player_builtin">Built-in Android player (deprecated) </string> + <string name="media_player_sonic">Sonic Media Player (deprecated) </string> <string name="media_player_exoplayer_recommended">ExoPlayer (recommended)</string> <string name="media_player_switch_to_exoplayer">Switch to ExoPlayer</string> <string name="media_player_switched_to_exoplayer">Switched to ExoPlayer.</string> @@ -631,23 +654,24 @@ <string name="gpodnet_suggestions_header">SUGGESTIONS</string> <string name="gpodnet_search_hint">Search gpodder.net</string> <string name="gpodnetauth_login_title">Login</string> - <string name="gpodnetauth_login_descr">Welcome to the gpodder.net login process. First, type in your login information:</string> <string name="gpodnetauth_login_butLabel">Login</string> - <string name="gpodnetauth_login_register">If you do not have an account yet, you can create one here:\nhttps://gpodder.net/register/</string> + <string name="gpodnetauth_encryption_warning">Password and data are not encrypted!</string> + <string name="create_account">Create account</string> <string name="username_label">Username</string> <string name="password_label">Password</string> - <string name="gpodnetauth_device_title">Device Selection</string> + <string name="gpodnet_description">Gpodder.net is an open-source podcast synchronization service that is independent of the AntennaPod project.</string> + <string name="gpodnetauth_server_official">Official gpodder.net server</string> + <string name="gpodnetauth_server_custom">Custom server</string> + <string name="gpodnetauth_host">Hostname</string> + <string name="gpodnetauth_select_server">Select server</string> <string name="gpodnetauth_device_descr">Create a new device to use for your gpodder.net account or choose an existing one:</string> - <string name="gpodnetauth_device_deviceID">Device ID:\u0020</string> - <string name="gpodnetauth_device_caption">Caption</string> - <string name="gpodnetauth_device_butCreateNewDevice">Create new device</string> - <string name="gpodnetauth_device_chooseExistingDevice">Choose existing device:</string> - <string name="gpodnetauth_device_errorEmpty">Device ID must not be empty</string> - <string name="gpodnetauth_device_errorAlreadyUsed">Device ID already in use</string> + <string name="gpodnetauth_device_name">Device name</string> + <string name="gpodnetauth_device_name_default">AntennaPod on %1$s</string> <string name="gpodnetauth_device_caption_errorEmpty">Caption must not be empty</string> + <string name="gpodnetauth_existing_devices">Existing devices</string> + <string name="gpodnetauth_create_device">Create device</string> <string name="gpodnetauth_device_butChoose">Choose</string> - <string name="gpodnetauth_finish_title">Login successful!</string> <string name="gpodnetauth_finish_descr">Congratulations! Your gpodder.net account is now linked with your device. AntennaPod will from now on automatically sync subscriptions on your device with your gpodder.net account.</string> <string name="gpodnetauth_finish_butsyncnow">Start sync now</string> <string name="gpodnetauth_finish_butgomainscreen">Go to main screen</string> @@ -874,6 +898,8 @@ <string name="notification_channel_sync_error_description">Shown when gpodder synchronization fails.</string> <string name="notification_channel_auto_download">Automatic download completed</string> <string name="notification_channel_episode_auto_download">Shown when episodes have been automatically downloaded.</string> + <string name="notification_channel_new_episode">New Episode</string> + <string name="notification_channel_new_episode_description">Shown when a new episode of a podcast was found, where notifications are enabled</string> <!-- Widget settings --> <string name="widget_settings">Widget settings</string> diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index 7f7ecfe1e..533fa8420 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -21,6 +21,7 @@ <item name="drawer_activated_color">@color/highlight_light</item> <item name="android:textAllCaps">false</item> <item name="android:textColorHint">@color/grey600</item> + <item name="seek_background">@color/seek_background_light</item> <item name="storage">@drawable/ic_storage_black</item> <item name="ic_network">@drawable/ic_network_black</item> @@ -78,6 +79,7 @@ <item name="filter_dialog_clear">@color/filter_dialog_clear_light</item> <item name="filter_dialog_button_background">@drawable/filter_dialog_background_light</item> <item name="ic_notifications">@drawable/ic_notifications_black</item> + <item name="ic_share">@drawable/ic_share_black</item> </style> <style name="Theme.AntennaPod.Dark" parent="Theme.Base.AntennaPod.Dark"> @@ -101,6 +103,7 @@ <item name="action_icon_color">@color/white</item> <item name="android:textAllCaps">false</item> <item name="android:textColorHint">@color/medium_gray</item> + <item name="seek_background">@color/seek_background_dark</item> <item name="storage">@drawable/ic_storage_white</item> <item name="ic_network">@drawable/ic_network_white</item> @@ -158,6 +161,7 @@ <item name="filter_dialog_clear">@color/filter_dialog_clear_dark</item> <item name="filter_dialog_button_background">@drawable/filter_dialog_background_dark</item> <item name="ic_notifications">@drawable/ic_notifications_white</item> + <item name="ic_share">@drawable/ic_share_white</item> </style> <style name="Theme.AntennaPod.TrueBlack" parent="Theme.Base.AntennaPod.TrueBlack"> diff --git a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java index 4b5e4d588..48de7c6e1 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java @@ -2,20 +2,16 @@ package de.danoeh.antennapod.core; import android.content.Context; import android.util.Log; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.gms.common.GooglePlayServicesNotAvailableException; -import com.google.android.gms.common.GooglePlayServicesRepairableException; -import com.google.android.gms.security.ProviderInstaller; import de.danoeh.antennapod.core.cast.CastManager; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; import de.danoeh.antennapod.core.preferences.UsageStatistics; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm; import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.gui.NotificationUtils; +import de.danoeh.antennapod.net.ssl.SslProviderInstaller; import java.io.File; @@ -37,10 +33,6 @@ public class ClientConfig { public static DownloadServiceCallbacks downloadServiceCallbacks; - public static PlaybackServiceCallbacks playbackServiceCallbacks; - - public static AutomaticDownloadAlgorithm automaticDownloadAlgorithm; - public static CastCallbacks castCallbacks; private static boolean initialized = false; @@ -53,7 +45,7 @@ public class ClientConfig { UserPreferences.init(context); UsageStatistics.init(context); PlaybackPreferences.init(context); - installSslProvider(context); + SslProviderInstaller.install(context); NetworkUtils.init(context); // Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary // Google Play Service usage. @@ -69,15 +61,4 @@ public class ClientConfig { NotificationUtils.createChannels(context); initialized = true; } - - private static void installSslProvider(Context context) { - try { - ProviderInstaller.installIfNeeded(context); - } catch (GooglePlayServicesRepairableException e) { - e.printStackTrace(); - GoogleApiAvailability.getInstance().showErrorNotification(context, e.getConnectionStatusCode()); - } catch (GooglePlayServicesNotAvailableException e) { - e.printStackTrace(); - } - } } diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java index 6fa874eca..3884041b6 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java +++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java @@ -10,6 +10,7 @@ import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.common.images.WebImage; +import de.danoeh.antennapod.core.util.playback.PlayableException; import de.danoeh.antennapod.core.util.playback.RemoteMedia; import java.util.Calendar; import java.util.List; @@ -18,7 +19,6 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.Playable; /** @@ -52,7 +52,7 @@ public class CastUtils { public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999; public static boolean isCastable(Playable media) { - if (media == null || media instanceof ExternalMedia) { + if (media == null) { return false; } if (media instanceof FeedMedia || media instanceof RemoteMedia) { @@ -93,7 +93,7 @@ public class CastUtils { MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC); try{ media.loadMetadata(); - } catch (Playable.PlayableException e) { + } catch (PlayableException e) { Log.e(TAG, "Unable to load FeedMedia metadata", e); } FeedItem feedItem = media.getItem(); @@ -130,18 +130,12 @@ public class CastUtils { if (!TextUtils.isEmpty(feedItem.getLink())) { metadata.putString(KEY_EPISODE_LINK, feedItem.getLink()); } - } - String notes = null; - try { - notes = media.loadShownotes().call(); - } catch (Exception e) { - Log.e(TAG, "Unable to load FeedMedia notes", e); - } - if (notes != null) { - if (notes.length() > EPISODE_NOTES_MAX_LENGTH) { - notes = notes.substring(0, EPISODE_NOTES_MAX_LENGTH); + try { + DBReader.loadDescriptionOfFeedItem(feedItem); + metadata.putString(KEY_EPISODE_NOTES, feedItem.getDescription()); + } catch (Exception e) { + Log.e(TAG, "Unable to load FeedMedia notes", e); } - metadata.putString(KEY_EPISODE_NOTES, notes); } // This field only identifies the id on the device that has the original version. // Idea is to perhaps, on a first approach, check if the version on the local DB with the @@ -202,7 +196,7 @@ public class CastUtils { } else { Log.d(TAG, "FeedMedia object obtained does NOT match the MediaInfo provided. id=" + mediaId); } - } catch (Playable.PlayableException e) { + } catch (PlayableException e) { Log.e(TAG, "Unable to load FeedMedia metadata to compare with MediaInfo", e); } } else { diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java index f629793e2..e61896965 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java +++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java @@ -28,6 +28,7 @@ import de.danoeh.antennapod.core.cast.CastConsumer; import de.danoeh.antennapod.core.cast.CastManager; import de.danoeh.antennapod.core.cast.CastUtils; import de.danoeh.antennapod.core.cast.DefaultCastConsumer; +import de.danoeh.antennapod.core.util.playback.PlayableException; import de.danoeh.antennapod.core.util.playback.RemoteMedia; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; @@ -360,7 +361,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { if (prepareImmediately) { prepare(); } - } catch (Playable.PlayableException e) { + } catch (PlayableException e) { Log.e(TAG, "Error while loading media metadata", e); setPlayerStatus(PlayerStatus.STOPPED, null); } diff --git a/app/src/androidTest/java/de/test/antennapod/feed/FeedFilterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedFilterTest.java index fc2943205..8b4a13473 100644 --- a/app/src/androidTest/java/de/test/antennapod/feed/FeedFilterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedFilterTest.java @@ -1,19 +1,15 @@ -package de.test.antennapod.feed; +package de.danoeh.antennapod.core.feed; -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.FeedFilter; -import de.danoeh.antennapod.core.feed.FeedItem; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -@SmallTest public class FeedFilterTest { @Test - public void testNullFilter() throws Exception { + public void testNullFilter() { FeedFilter filter = new FeedFilter(); FeedItem item = new FeedItem(); item.setTitle("Hello world"); @@ -26,7 +22,7 @@ public class FeedFilterTest { } @Test - public void testBasicIncludeFilter() throws Exception { + public void testBasicIncludeFilter() { String includeFilter = "Hello"; FeedFilter filter = new FeedFilter(includeFilter, ""); FeedItem item = new FeedItem(); @@ -44,7 +40,7 @@ public class FeedFilterTest { } @Test - public void testBasicExcludeFilter() throws Exception { + public void testBasicExcludeFilter() { String excludeFilter = "Hello"; FeedFilter filter = new FeedFilter("", excludeFilter); FeedItem item = new FeedItem(); @@ -62,7 +58,7 @@ public class FeedFilterTest { } @Test - public void testComplexIncludeFilter() throws Exception { + public void testComplexIncludeFilter() { String includeFilter = "Hello \n\"Two words\""; FeedFilter filter = new FeedFilter(includeFilter, ""); FeedItem item = new FeedItem(); @@ -84,7 +80,7 @@ public class FeedFilterTest { } @Test - public void testComplexExcludeFilter() throws Exception { + public void testComplexExcludeFilter() { String excludeFilter = "Hello \"Two words\""; FeedFilter filter = new FeedFilter("", excludeFilter); FeedItem item = new FeedItem(); @@ -106,7 +102,7 @@ public class FeedFilterTest { } @Test - public void testComboFilter() throws Exception { + public void testComboFilter() { String includeFilter = "Hello world"; String excludeFilter = "dislike"; FeedFilter filter = new FeedFilter(includeFilter, excludeFilter); diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/FeedItemTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedItemTest.java index 6bd753561..4d9c247f7 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/feed/FeedItemTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedItemTest.java @@ -12,6 +12,9 @@ import static org.junit.Assert.assertFalse; public class FeedItemTest { + private static final String TEXT_LONG = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + private static final String TEXT_SHORT = "Lorem ipsum"; + private FeedItem original; private FeedItem changedFeedItem; @@ -22,21 +25,21 @@ public class FeedItemTest { } @Test - public void testUpdateFromOther_feedItemImageDownloadUrlChanged() throws Exception { + public void testUpdateFromOther_feedItemImageDownloadUrlChanged() { setNewFeedItemImageDownloadUrl(); original.updateFromOther(changedFeedItem); assertFeedItemImageWasUpdated(); } @Test - public void testUpdateFromOther_feedItemImageRemoved() throws Exception { + public void testUpdateFromOther_feedItemImageRemoved() { feedItemImageRemoved(); original.updateFromOther(changedFeedItem); assertFeedItemImageWasNotUpdated(); } @Test - public void testUpdateFromOther_feedItemImageAdded() throws Exception { + public void testUpdateFromOther_feedItemImageAdded() { original.setImageUrl(null); setNewFeedItemImageDownloadUrl(); original.updateFromOther(changedFeedItem); @@ -102,4 +105,34 @@ public class FeedItemTest { assertEquals(anyFeedItemWithImage().getImageUrl(), original.getImageUrl()); } + /** + * If one of `description` or `content:encoded` is null, use the other one. + */ + @Test + public void testShownotesNullValues() { + testShownotes(null, TEXT_LONG); + testShownotes(TEXT_LONG, null); + } + + /** + * If `description` is reasonably longer than `content:encoded`, use `description`. + */ + @Test + public void testShownotesLength() { + testShownotes(TEXT_SHORT, TEXT_LONG); + testShownotes(TEXT_LONG, TEXT_SHORT); + } + + /** + * Checks if the shownotes equal TEXT_LONG, using the given `description` and `content:encoded`. + * + * @param description Description of the feed item + * @param contentEncoded `content:encoded` of the feed item + */ + private void testShownotes(String description, String contentEncoded) { + FeedItem item = new FeedItem(); + item.setDescriptionIfLonger(description); + item.setDescriptionIfLonger(contentEncoded); + assertEquals(TEXT_LONG, item.getDescription()); + } }
\ No newline at end of file diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java index fbe4c6ace..b38f8586d 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.feed; import android.app.Application; import android.content.Context; import android.media.MediaMetadataRetriever; +import android.net.Uri; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; @@ -33,8 +34,11 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.PodDBAdapter; +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -93,7 +97,7 @@ public class LocalFeedUpdaterTest { public void testUpdateFeed_AddNewFeed() { // check for empty database List<Feed> feedListBefore = DBReader.getFeedList(); - assertTrue(feedListBefore.isEmpty()); + assertThat(feedListBefore, is(empty())); callUpdateFeed(LOCAL_FEED_DIR2); @@ -139,7 +143,7 @@ public class LocalFeedUpdaterTest { callUpdateFeed(LOCAL_FEED_DIR2); Feed feedAfter = verifySingleFeedInDatabase(); - assertTrue(feedAfter.getImageUrl().contains("local-feed2/folder.png")); + assertThat(feedAfter.getImageUrl(), endsWith("local-feed2/folder.png")); } /** @@ -151,7 +155,7 @@ public class LocalFeedUpdaterTest { Feed feedAfter = verifySingleFeedInDatabase(); String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon); - assertTrue(feedAfter.getImageUrl().contains(resourceEntryName)); + assertThat(feedAfter.getImageUrl(), endsWith(resourceEntryName)); } /** @@ -180,6 +184,65 @@ public class LocalFeedUpdaterTest { assertEquals(24, calendar.get(Calendar.SECOND)); } + @Test + public void testGetImageUrl_EmptyFolder() { + DocumentFile documentFolder = mockDocumentFolder(); + String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder); + String defaultImageName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon); + assertThat(imageUrl, endsWith(defaultImageName)); + } + + @Test + public void testGetImageUrl_NoImageButAudioFiles() { + DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3")); + String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder); + String defaultImageName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon); + assertThat(imageUrl, endsWith(defaultImageName)); + } + + @Test + public void testGetImageUrl_PreferredImagesFilenames() { + for (String filename : LocalFeedUpdater.PREFERRED_FEED_IMAGE_FILENAMES) { + DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + mockDocumentFile(filename, "image/jpeg")); // image MIME type doesn't matter + String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder); + assertThat(imageUrl, endsWith(filename)); + } + } + + @Test + public void testGetImageUrl_OtherImageFilenameJpg() { + DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + mockDocumentFile("my-image.jpg", "image/jpeg")); + String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder); + assertThat(imageUrl, endsWith("my-image.jpg")); + } + + @Test + public void testGetImageUrl_OtherImageFilenameJpeg() { + DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + mockDocumentFile("my-image.jpeg", "image/jpeg")); + String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder); + assertThat(imageUrl, endsWith("my-image.jpeg")); + } + + @Test + public void testGetImageUrl_OtherImageFilenamePng() { + DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + mockDocumentFile("my-image.png", "image/png")); + String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder); + assertThat(imageUrl, endsWith("my-image.png")); + } + + @Test + public void testGetImageUrl_OtherImageFilenameUnsupportedMimeType() { + DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"), + mockDocumentFile("my-image.svg", "image/svg+xml")); + String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder); + String defaultImageName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon); + assertThat(imageUrl, endsWith(defaultImageName)); + } + /** * Fill ShadowMediaMetadataRetriever with dummy duration and title. * @@ -238,4 +301,26 @@ public class LocalFeedUpdaterTest { List<FeedItem> feedItems = DBReader.getFeedItemList(feed); assertEquals(expectedItemCount, feedItems.size()); } + + /** + * Create a DocumentFile mock object. + */ + @NonNull + private static DocumentFile mockDocumentFile(@NonNull String fileName, @NonNull String mimeType) { + DocumentFile file = mock(DocumentFile.class); + when(file.getName()).thenReturn(fileName); + when(file.getUri()).thenReturn(Uri.parse("file:///path/" + fileName)); + when(file.getType()).thenReturn(mimeType); + return file; + } + + /** + * Create a DocumentFile folder mock object with a list of files. + */ + @NonNull + private static DocumentFile mockDocumentFolder(DocumentFile... files) { + DocumentFile documentFolder = mock(DocumentFile.class); + when(documentFolder.listFiles()).thenReturn(files); + return documentFolder; + } } diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBCleanupTests.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java index 339d3cea9..13d24adc1 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBCleanupTests.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java @@ -1,5 +1,6 @@ -package de.test.antennapod.storage; +package de.danoeh.antennapod.core.storage; +import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import androidx.preference.PreferenceManager; @@ -11,27 +12,34 @@ import java.util.Date; import java.util.List; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.SmallTest; + +import de.danoeh.antennapod.core.ApplicationCallbacks; +import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.PodDBAdapter; + import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -import static de.test.antennapod.storage.DBTestUtils.saveFeedlist; +import static de.danoeh.antennapod.core.storage.DbTestUtils.saveFeedlist; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** - * Test class for DBTasks + * Test class for DBTasks. */ -@SmallTest -public class DBCleanupTests { +@RunWith(RobolectricTestRunner.class) +public class DbCleanupTests { + static final int EPISODE_CACHE_SIZE = 5; private int cleanupAlgorithm; @@ -39,7 +47,7 @@ public class DBCleanupTests { private File destFolder; - public DBCleanupTests() { + public DbCleanupTests() { setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_DEFAULT); } @@ -47,24 +55,11 @@ public class DBCleanupTests { this.cleanupAlgorithm = cleanupAlgorithm; } - @After - public void tearDown() throws Exception { - assertTrue(PodDBAdapter.deleteDatabase()); - - cleanupDestFolder(destFolder); - assertTrue(destFolder.delete()); - } - - private void cleanupDestFolder(File destFolder) { - for (File f : destFolder.listFiles()) { - assertTrue(f.delete()); - } - } - @Before - public void setUp() throws Exception { + public void setUp() { context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - destFolder = new File(context.getCacheDir(), "DDCleanupTests"); + destFolder = new File(context.getCacheDir(), "DbCleanupTests"); + //noinspection ResultOfMethodCallIgnored destFolder.mkdir(); cleanupDestFolder(destFolder); assertNotNull(destFolder); @@ -78,24 +73,46 @@ public class DBCleanupTests { adapter.open(); adapter.close(); - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).edit(); + SharedPreferences.Editor prefEdit = PreferenceManager + .getDefaultSharedPreferences(context.getApplicationContext()).edit(); prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, Integer.toString(EPISODE_CACHE_SIZE)); prefEdit.putString(UserPreferences.PREF_EPISODE_CLEANUP, Integer.toString(cleanupAlgorithm)); prefEdit.putBoolean(UserPreferences.PREF_ENABLE_AUTODL, true); prefEdit.commit(); UserPreferences.init(context); + PlaybackPreferences.init(context); + + Application app = (Application) context; + ClientConfig.applicationCallbacks = mock(ApplicationCallbacks.class); + when(ClientConfig.applicationCallbacks.getApplicationInstance()).thenReturn(app); + } + + @After + public void tearDown() { + cleanupDestFolder(destFolder); + assertTrue(destFolder.delete()); + + DBWriter.tearDownTests(); + PodDBAdapter.tearDownTests(); + } + + private void cleanupDestFolder(File destFolder) { + //noinspection ConstantConditions + for (File f : destFolder.listFiles()) { + assertTrue(f.delete()); + } } @Test public void testPerformAutoCleanupShouldDelete() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; + final int numItems = EPISODE_CACHE_SIZE * 2; Feed feed = new Feed("url", null, "title"); List<FeedItem> items = new ArrayList<>(); feed.setItems(items); List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.PLAYED, false, false); + populateItems(numItems, feed, items, files, FeedItem.PLAYED, false, false); DBTasks.performAutoCleanup(context); for (int i = 0; i < files.size(); i++) { @@ -107,6 +124,7 @@ public class DBCleanupTests { } } + @SuppressWarnings("SameParameterValue") void populateItems(final int numItems, Feed feed, List<FeedItem> items, List<File> files, int itemState, boolean addToQueue, boolean addToFavorites) throws IOException { @@ -121,7 +139,8 @@ public class DBCleanupTests { File f = new File(destFolder, "file " + i); assertTrue(f.createNewFile()); files.add(f); - item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true, playbackCompletionDate, 0, 0)); + item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", + f.getAbsolutePath(), "url", true, playbackCompletionDate, 0, 0)); items.add(item); } @@ -139,19 +158,20 @@ public class DBCleanupTests { assertTrue(feed.getId() != 0); for (FeedItem item : items) { assertTrue(item.getId() != 0); + //noinspection ConstantConditions assertTrue(item.getMedia().getId() != 0); } } @Test public void testPerformAutoCleanupHandleUnplayed() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; + final int numItems = EPISODE_CACHE_SIZE * 2; Feed feed = new Feed("url", null, "title"); List<FeedItem> items = new ArrayList<>(); feed.setItems(items); List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.UNPLAYED, false, false); + populateItems(numItems, feed, items, files, FeedItem.UNPLAYED, false, false); DBTasks.performAutoCleanup(context); for (File file : files) { @@ -161,13 +181,13 @@ public class DBCleanupTests { @Test public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; + final int numItems = EPISODE_CACHE_SIZE * 2; Feed feed = new Feed("url", null, "title"); List<FeedItem> items = new ArrayList<>(); feed.setItems(items); List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.PLAYED, true, false); + populateItems(numItems, feed, items, files, FeedItem.PLAYED, true, false); DBTasks.performAutoCleanup(context); for (File file : files) { @@ -176,9 +196,9 @@ public class DBCleanupTests { } /** - * Reproduces a bug where DBTasks.performAutoCleanup(android.content.Context) would use the ID of the FeedItem in the - * call to DBWriter.deleteFeedMediaOfItem instead of the ID of the FeedMedia. This would cause the wrong item to be deleted. - * @throws IOException + * Reproduces a bug where DBTasks.performAutoCleanup(android.content.Context) would use the ID + * of the FeedItem in the call to DBWriter.deleteFeedMediaOfItem instead of the ID of the FeedMedia. + * This would cause the wrong item to be deleted. */ @Test public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue_withFeedsWithNoMedia() throws IOException { @@ -188,6 +208,7 @@ public class DBCleanupTests { // add candidate for performAutoCleanup List<Feed> feeds = saveFeedlist(1, 1, true); FeedMedia m = feeds.get(0).getItems().get(0).getMedia(); + //noinspection ConstantConditions m.setDownloaded(true); m.setFile_url("file"); PodDBAdapter adapter = PodDBAdapter.getInstance(); @@ -200,13 +221,13 @@ public class DBCleanupTests { @Test public void testPerformAutoCleanupShouldNotDeleteBecauseFavorite() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; + final int numItems = EPISODE_CACHE_SIZE * 2; Feed feed = new Feed("url", null, "title"); List<FeedItem> items = new ArrayList<>(); feed.setItems(items); List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.PLAYED, false, true); + populateItems(numItems, feed, items, files, FeedItem.PLAYED, false, true); DBTasks.performAutoCleanup(context); for (File file : files) { diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBQueueCleanupAlgorithmTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbQueueCleanupAlgorithmTest.java index de810c701..6e53bd20e 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBQueueCleanupAlgorithmTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbQueueCleanupAlgorithmTest.java @@ -1,16 +1,17 @@ -package de.test.antennapod.storage; +package de.danoeh.antennapod.core.storage; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import androidx.test.filters.SmallTest; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBTasks; + import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -18,28 +19,26 @@ import static org.junit.Assert.assertTrue; /** * Tests that the APQueueCleanupAlgorithm is working correctly. */ -@SmallTest -public class DBQueueCleanupAlgorithmTest extends DBCleanupTests { - - private static final String TAG = "DBQueueCleanupAlgorithmTest"; +@RunWith(RobolectricTestRunner.class) +public class DbQueueCleanupAlgorithmTest extends DbCleanupTests { - public DBQueueCleanupAlgorithmTest() { + public DbQueueCleanupAlgorithmTest() { setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_QUEUE); } /** * For APQueueCleanupAlgorithm we expect even unplayed episodes to be deleted if needed - * if they aren't in the queue + * if they aren't in the queue. */ @Test public void testPerformAutoCleanupHandleUnplayed() throws IOException { - final int NUM_ITEMS = EPISODE_CACHE_SIZE * 2; + final int numItems = EPISODE_CACHE_SIZE * 2; Feed feed = new Feed("url", null, "title"); List<FeedItem> items = new ArrayList<>(); feed.setItems(items); List<File> files = new ArrayList<>(); - populateItems(NUM_ITEMS, feed, items, files, FeedItem.UNPLAYED, false, false); + populateItems(numItems, feed, items, files, FeedItem.UNPLAYED, false, false); DBTasks.performAutoCleanup(context); for (int i = 0; i < files.size(); i++) { diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java index 4dd8a7427..2a1e6f4c8 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBReaderTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java @@ -1,4 +1,6 @@ -package de.test.antennapod.storage; +package de.danoeh.antennapod.core.storage; + +import android.content.Context; import java.util.ArrayList; import java.util.Date; @@ -6,19 +8,18 @@ import java.util.List; import java.util.Random; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.SmallTest; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.NavDrawerData; -import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.LongList; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -import static de.test.antennapod.storage.DBTestUtils.saveFeedlist; +import static de.danoeh.antennapod.core.storage.DbTestUtils.saveFeedlist; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -26,26 +27,30 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** - * Test class for DBReader + * Test class for DBReader. */ -@SmallTest -public class DBReaderTest { - - @After - public void tearDown() throws Exception { - assertTrue(PodDBAdapter.deleteDatabase()); - } +@SuppressWarnings("ConstantConditions") +@RunWith(RobolectricTestRunner.class) +public class DbReaderTest { @Before - public void setUp() throws Exception { - // create new database - PodDBAdapter.init(InstrumentationRegistry.getInstrumentation().getTargetContext()); + public void setUp() { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + UserPreferences.init(context); + + PodDBAdapter.init(context); PodDBAdapter.deleteDatabase(); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.close(); } + @After + public void tearDown() { + PodDBAdapter.tearDownTests(); + DBWriter.tearDownTests(); + } + @Test public void testGetFeedList() { List<Feed> feeds = saveFeedlist(10, 0, false); @@ -137,6 +142,7 @@ public class DBReaderTest { } } + @SuppressWarnings("SameParameterValue") private List<FeedItem> saveQueue(int numItems) { if (numItems <= 0) { throw new IllegalArgumentException("numItems<=0"); @@ -163,7 +169,7 @@ public class DBReaderTest { } @Test - public void testGetQueueIDList() { + public void testGetQueueIdList() { final int numItems = 10; List<FeedItem> queue = saveQueue(numItems); LongList ids = DBReader.getQueueIDList(); @@ -188,6 +194,7 @@ public class DBReaderTest { } } + @SuppressWarnings("SameParameterValue") private List<FeedItem> saveDownloadedItems(int numItems) { if (numItems <= 0) { throw new IllegalArgumentException("numItems<=0"); @@ -220,16 +227,17 @@ public class DBReaderTest { public void testGetDownloadedItems() { final int numItems = 10; List<FeedItem> downloaded = saveDownloadedItems(numItems); - List<FeedItem> downloaded_saved = DBReader.getDownloadedItems(); - assertNotNull(downloaded_saved); - assertEquals(downloaded.size(), downloaded_saved.size()); - for (FeedItem item : downloaded_saved) { + List<FeedItem> downloadedSaved = DBReader.getDownloadedItems(); + assertNotNull(downloadedSaved); + assertEquals(downloaded.size(), downloadedSaved.size()); + for (FeedItem item : downloadedSaved) { assertNotNull(item.getMedia()); assertTrue(item.getMedia().isDownloaded()); assertNotNull(item.getMedia().getDownload_url()); } } + @SuppressWarnings("SameParameterValue") private List<FeedItem> saveNewItems(int numItems) { List<Feed> feeds = saveFeedlist(numItems, numItems, true); List<FeedItem> items = new ArrayList<>(); @@ -286,7 +294,7 @@ public class DBReaderTest { final int numReturnedItems = Math.min(playedItems, DBReader.PLAYBACK_HISTORY_SIZE); final int numFeeds = 1; - Feed feed = DBTestUtils.saveFeedlist(numFeeds, numItems, true).get(0); + Feed feed = DbTestUtils.saveFeedlist(numFeeds, numItems, true).get(0); long[] ids = new long[playedItems]; PodDBAdapter adapter = PodDBAdapter.getInstance(); @@ -311,31 +319,31 @@ public class DBReaderTest { @Test public void testGetNavDrawerDataQueueEmptyNoUnreadItems() { - final int NUM_FEEDS = 10; - final int NUM_ITEMS = 10; - DBTestUtils.saveFeedlist(NUM_FEEDS, NUM_ITEMS, true); + final int numFeeds = 10; + final int numItems = 10; + DbTestUtils.saveFeedlist(numFeeds, numItems, true); NavDrawerData navDrawerData = DBReader.getNavDrawerData(); - assertEquals(NUM_FEEDS, navDrawerData.items.size()); + assertEquals(numFeeds, navDrawerData.items.size()); assertEquals(0, navDrawerData.numNewItems); assertEquals(0, navDrawerData.queueSize); } @Test public void testGetNavDrawerDataQueueNotEmptyWithUnreadItems() { - final int NUM_FEEDS = 10; - final int NUM_ITEMS = 10; - final int NUM_QUEUE = 1; - final int NUM_NEW = 2; - List<Feed> feeds = DBTestUtils.saveFeedlist(NUM_FEEDS, NUM_ITEMS, true); + final int numFeeds = 10; + final int numItems = 10; + final int numQueue = 1; + final int numNew = 2; + List<Feed> feeds = DbTestUtils.saveFeedlist(numFeeds, numItems, true); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - for (int i = 0; i < NUM_NEW; i++) { + for (int i = 0; i < numNew; i++) { FeedItem item = feeds.get(0).getItems().get(i); item.setNew(); adapter.setSingleFeedItem(item); } List<FeedItem> queue = new ArrayList<>(); - for (int i = 0; i < NUM_QUEUE; i++) { + for (int i = 0; i < numQueue; i++) { FeedItem item = feeds.get(1).getItems().get(i); queue.add(item); } @@ -344,14 +352,14 @@ public class DBReaderTest { adapter.close(); NavDrawerData navDrawerData = DBReader.getNavDrawerData(); - assertEquals(NUM_FEEDS, navDrawerData.items.size()); - assertEquals(NUM_NEW, navDrawerData.numNewItems); - assertEquals(NUM_QUEUE, navDrawerData.queueSize); + assertEquals(numFeeds, navDrawerData.items.size()); + assertEquals(numNew, navDrawerData.numNewItems); + assertEquals(numQueue, navDrawerData.queueSize); } @Test - public void testGetFeedItemlistCheckChaptersFalse() throws Exception { - List<Feed> feeds = DBTestUtils.saveFeedlist(10, 10, false, false, 0); + public void testGetFeedItemlistCheckChaptersFalse() { + List<Feed> feeds = DbTestUtils.saveFeedlist(10, 10, false, false, 0); for (Feed feed : feeds) { for (FeedItem item : feed.getItems()) { assertFalse(item.hasChapters()); @@ -360,7 +368,7 @@ public class DBReaderTest { } @Test - public void testGetFeedItemlistCheckChaptersTrue() throws Exception { + public void testGetFeedItemlistCheckChaptersTrue() { List<Feed> feeds = saveFeedlist(10, 10, false, true, 10); for (Feed feed : feeds) { for (FeedItem item : feed.getItems()) { @@ -370,7 +378,7 @@ public class DBReaderTest { } @Test - public void testLoadChaptersOfFeedItemNoChapters() throws Exception { + public void testLoadChaptersOfFeedItemNoChapters() { List<Feed> feeds = saveFeedlist(1, 3, false, false, 0); saveFeedlist(1, 3, false, true, 3); for (Feed feed : feeds) { @@ -384,25 +392,25 @@ public class DBReaderTest { } @Test - public void testLoadChaptersOfFeedItemWithChapters() throws Exception { - final int NUM_CHAPTERS = 3; - DBTestUtils.saveFeedlist(1, 3, false, false, 0); - List<Feed> feeds = saveFeedlist(1, 3, false, true, NUM_CHAPTERS); + public void testLoadChaptersOfFeedItemWithChapters() { + final int numChapters = 3; + DbTestUtils.saveFeedlist(1, 3, false, false, 0); + List<Feed> feeds = saveFeedlist(1, 3, false, true, numChapters); for (Feed feed : feeds) { for (FeedItem item : feed.getItems()) { assertTrue(item.hasChapters()); item.setChapters(DBReader.loadChaptersOfFeedItem(item)); assertTrue(item.hasChapters()); assertNotNull(item.getChapters()); - assertEquals(NUM_CHAPTERS, item.getChapters().size()); + assertEquals(numChapters, item.getChapters().size()); } } } @Test - public void testGetItemWithChapters() throws Exception { - final int NUM_CHAPTERS = 3; - List<Feed> feeds = saveFeedlist(1, 1, false, true, NUM_CHAPTERS); + public void testGetItemWithChapters() { + final int numChapters = 3; + List<Feed> feeds = saveFeedlist(1, 1, false, true, numChapters); FeedItem item1 = feeds.get(0).getItems().get(0); FeedItem item2 = DBReader.getFeedItem(item1.getId()); item2.setChapters(DBReader.loadChaptersOfFeedItem(item2)); diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java index 840a7d01f..400ddda36 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java @@ -1,4 +1,4 @@ -package de.test.antennapod.storage; +package de.danoeh.antennapod.core.storage; import java.util.ArrayList; import java.util.Collections; @@ -10,7 +10,6 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.SimpleChapter; -import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import static org.junit.Assert.assertTrue; @@ -18,9 +17,8 @@ import static org.junit.Assert.assertTrue; /** * Utility methods for DB* tests. */ -class DBTestUtils { +abstract class DbTestUtils { - private DBTestUtils(){} /** * Use this method when tests don't involve chapters. */ diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java index 652389d00..3efb2705f 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java @@ -1,38 +1,37 @@ -package de.test.antennapod.storage; +package de.danoeh.antennapod.core.storage; +import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; -import androidx.preference.PreferenceManager; import android.util.Log; import androidx.core.util.Consumer; +import androidx.preference.PreferenceManager; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.MediumTest; import org.awaitility.Awaitility; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import de.danoeh.antennapod.core.ApplicationCallbacks; +import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.FeedItemUtil; import static org.junit.Assert.assertEquals; @@ -41,83 +40,92 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** - * Test class for DBWriter + * Test class for {@link DBWriter}. */ -@MediumTest -public class DBWriterTest { +@RunWith(RobolectricTestRunner.class) +public class DbWriterTest { private static final String TAG = "DBWriterTest"; private static final String TEST_FOLDER = "testDBWriter"; private static final long TIMEOUT = 5L; + + private Context context; - @After - public void tearDown() throws Exception { - assertTrue(PodDBAdapter.deleteDatabase()); + @Before + public void setUp() { + context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + UserPreferences.init(context); + PlaybackPreferences.init(context); - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - File testDir = context.getExternalFilesDir(TEST_FOLDER); - assertNotNull(testDir); - for (File f : testDir.listFiles()) { - f.delete(); - } - } + Application app = (Application) context; + ClientConfig.applicationCallbacks = mock(ApplicationCallbacks.class); + when(ClientConfig.applicationCallbacks.getApplicationInstance()).thenReturn(app); - @Before - public void setUp() throws Exception { // create new database - PodDBAdapter.init(InstrumentationRegistry.getInstrumentation().getTargetContext()); + PodDBAdapter.init(context); PodDBAdapter.deleteDatabase(); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.close(); - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).edit(); + SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences( + context.getApplicationContext()).edit(); prefEdit.putBoolean(UserPreferences.PREF_DELETE_REMOVES_FROM_QUEUE, true).commit(); + } - UserPreferences.init(context); + @After + public void tearDown() { + PodDBAdapter.tearDownTests(); + DBWriter.tearDownTests(); + + File testDir = context.getExternalFilesDir(TEST_FOLDER); + assertNotNull(testDir); + for (File f : testDir.listFiles()) { + //noinspection ResultOfMethodCallIgnored + f.delete(); + } } @Test - public void testSetFeedMediaPlaybackInformation() - throws IOException, ExecutionException, InterruptedException, TimeoutException { - final int POSITION = 50; - final long LAST_PLAYED_TIME = 1000; - final int PLAYED_DURATION = 60; - final int DURATION = 100; + public void testSetFeedMediaPlaybackInformation() throws Exception { + final int position = 50; + final long lastPlayedTime = 1000; + final int playedDuration = 60; + final int duration = 100; Feed feed = new Feed("url", null, "title"); List<FeedItem> items = new ArrayList<>(); feed.setItems(items); FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), FeedItem.PLAYED, feed); items.add(item); - FeedMedia media = new FeedMedia(0, item, DURATION, 1, 1, "mime_type", "dummy path", "download_url", true, null, 0, 0); + FeedMedia media = new FeedMedia(0, item, duration, 1, 1, "mime_type", + "dummy path", "download_url", true, null, 0, 0); item.setMedia(media); DBWriter.setFeedItem(item).get(TIMEOUT, TimeUnit.SECONDS); - media.setPosition(POSITION); - media.setLastPlayedTime(LAST_PLAYED_TIME); - media.setPlayedDuration(PLAYED_DURATION); + media.setPosition(position); + media.setLastPlayedTime(lastPlayedTime); + media.setPlayedDuration(playedDuration); DBWriter.setFeedMediaPlaybackInformation(item.getMedia()).get(TIMEOUT, TimeUnit.SECONDS); FeedItem itemFromDb = DBReader.getFeedItem(item.getId()); FeedMedia mediaFromDb = itemFromDb.getMedia(); - assertEquals(POSITION, mediaFromDb.getPosition()); - assertEquals(LAST_PLAYED_TIME, mediaFromDb.getLastPlayedTime()); - assertEquals(PLAYED_DURATION, mediaFromDb.getPlayedDuration()); - assertEquals(DURATION, mediaFromDb.getDuration()); + assertEquals(position, mediaFromDb.getPosition()); + assertEquals(lastPlayedTime, mediaFromDb.getLastPlayedTime()); + assertEquals(playedDuration, mediaFromDb.getPlayedDuration()); + assertEquals(duration, mediaFromDb.getDuration()); } @Test - public void testDeleteFeedMediaOfItemFileExists() - throws IOException, ExecutionException, InterruptedException, TimeoutException { - File dest = new File(InstrumentationRegistry - .getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER), "testFile"); + public void testDeleteFeedMediaOfItemFileExists() throws Exception { + File dest = new File(context.getExternalFilesDir(TEST_FOLDER), "testFile"); assertTrue(dest.createNewFile()); @@ -126,7 +134,8 @@ public class DBWriterTest { feed.setItems(items); FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), FeedItem.PLAYED, feed); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null, 0, 0); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", + dest.getAbsolutePath(), "download_url", true, null, 0, 0); item.setMedia(media); items.add(item); @@ -138,7 +147,7 @@ public class DBWriterTest { assertTrue(media.getId() != 0); assertTrue(item.getId() != 0); - DBWriter.deleteFeedMediaOfItem(InstrumentationRegistry.getInstrumentation().getTargetContext(), media.getId()) + DBWriter.deleteFeedMediaOfItem(context, media.getId()) .get(TIMEOUT, TimeUnit.SECONDS); media = DBReader.getFeedMedia(media.getId()); assertNotNull(media); @@ -148,25 +157,24 @@ public class DBWriterTest { } @Test - public void testDeleteFeedMediaOfItemRemoveFromQueue() - throws IOException, ExecutionException, InterruptedException, TimeoutException { + public void testDeleteFeedMediaOfItemRemoveFromQueue() throws Exception { assertTrue(UserPreferences.shouldDeleteRemoveFromQueue()); - File dest = new File(InstrumentationRegistry - .getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER), "testFile"); + File dest = new File(context.getExternalFilesDir(TEST_FOLDER), "testFile"); assertTrue(dest.createNewFile()); Feed feed = new Feed("url", null, "title"); List<FeedItem> items = new ArrayList<>(); - List<FeedItem> queue = new ArrayList<>(); feed.setItems(items); FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), FeedItem.UNPLAYED, feed); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null, 0, 0); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", + dest.getAbsolutePath(), "download_url", true, null, 0, 0); item.setMedia(media); items.add(item); + List<FeedItem> queue = new ArrayList<>(); queue.add(item); PodDBAdapter adapter = PodDBAdapter.getInstance(); @@ -179,7 +187,7 @@ public class DBWriterTest { queue = DBReader.getQueue(); assertTrue(queue.size() != 0); - DBWriter.deleteFeedMediaOfItem(InstrumentationRegistry.getInstrumentation().getTargetContext(), media.getId()); + DBWriter.deleteFeedMediaOfItem(context, media.getId()); Awaitility.await().until(() -> !dest.exists()); media = DBReader.getFeedMedia(media.getId()); assertNotNull(media); @@ -191,8 +199,8 @@ public class DBWriterTest { } @Test - public void testDeleteFeed() throws ExecutionException, InterruptedException, IOException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + public void testDeleteFeed() throws Exception { + File destFolder = context.getExternalFilesDir(TEST_FOLDER); assertNotNull(destFolder); Feed feed = new Feed("url", null, "title"); @@ -208,7 +216,8 @@ public class DBWriterTest { assertTrue(enc.createNewFile()); itemFiles.add(enc); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null, 0, 0); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", + enc.getAbsolutePath(), "download_url", true, null, 0, 0); item.setMedia(media); } @@ -223,8 +232,7 @@ public class DBWriterTest { assertTrue(item.getMedia().getId() != 0); } - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); // check if files still exist for (File f : itemFiles) { @@ -248,8 +256,8 @@ public class DBWriterTest { } @Test - public void testDeleteFeedNoItems() throws IOException, ExecutionException, InterruptedException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + public void testDeleteFeedNoItems() throws Exception { + File destFolder = context.getExternalFilesDir(TEST_FOLDER); assertNotNull(destFolder); Feed feed = new Feed("url", null, "title"); @@ -263,8 +271,7 @@ public class DBWriterTest { assertTrue(feed.getId() != 0); - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -275,8 +282,8 @@ public class DBWriterTest { } @Test - public void testDeleteFeedNoFeedMedia() throws IOException, ExecutionException, InterruptedException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + public void testDeleteFeedNoFeedMedia() throws Exception { + File destFolder = context.getExternalFilesDir(TEST_FOLDER); assertNotNull(destFolder); Feed feed = new Feed("url", null, "title"); @@ -301,9 +308,7 @@ public class DBWriterTest { assertTrue(item.getId() != 0); } - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); - + DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -319,8 +324,8 @@ public class DBWriterTest { } @Test - public void testDeleteFeedWithQueueItems() throws ExecutionException, InterruptedException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + public void testDeleteFeedWithQueueItems() throws Exception { + File destFolder = context.getExternalFilesDir(TEST_FOLDER); assertNotNull(destFolder); Feed feed = new Feed("url", null, "title"); @@ -333,7 +338,8 @@ public class DBWriterTest { FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed); feed.getItems().add(item); File enc = new File(destFolder, "file " + i); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0, 0); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", + enc.getAbsolutePath(), "download_url", false, null, 0, 0); item.setMedia(media); } @@ -348,7 +354,6 @@ public class DBWriterTest { assertTrue(item.getMedia().getId() != 0); } - List<FeedItem> queue = new ArrayList<>(feed.getItems()); adapter.open(); adapter.setQueue(queue); @@ -358,8 +363,7 @@ public class DBWriterTest { queueCursor.close(); adapter.close(); - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); adapter.open(); Cursor c = adapter.getFeedCursor(feed.getId()); @@ -380,8 +384,8 @@ public class DBWriterTest { } @Test - public void testDeleteFeedNoDownloadedFiles() throws ExecutionException, InterruptedException, TimeoutException { - File destFolder = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + public void testDeleteFeedNoDownloadedFiles() throws Exception { + File destFolder = context.getExternalFilesDir(TEST_FOLDER); assertNotNull(destFolder); Feed feed = new Feed("url", null, "title"); @@ -394,7 +398,8 @@ public class DBWriterTest { FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed); feed.getItems().add(item); File enc = new File(destFolder, "file " + i); - FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null, 0, 0); + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", + enc.getAbsolutePath(), "download_url", false, null, 0, 0); item.setMedia(media); } @@ -409,8 +414,7 @@ public class DBWriterTest { assertTrue(item.getMedia().getId() != 0); } - DBWriter.deleteFeed(InstrumentationRegistry - .getInstrumentation().getTargetContext(), feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); + DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS); adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -446,8 +450,7 @@ public class DBWriterTest { adapter.close(); List<FeedItem> itemsToDelete = feed.getItems().subList(0, 2); - DBWriter.deleteFeedItems(InstrumentationRegistry.getInstrumentation() - .getTargetContext(), itemsToDelete).get(TIMEOUT, TimeUnit.SECONDS); + DBWriter.deleteFeedItems(context, itemsToDelete).get(TIMEOUT, TimeUnit.SECONDS); adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -468,7 +471,8 @@ public class DBWriterTest { Feed feed = new Feed("url", null, "title"); feed.setItems(new ArrayList<>()); FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed); - FeedMedia media = new FeedMedia(0, item, 10, 0, 1, "mime", null, "url", false, playbackCompletionDate, 0, 0); + FeedMedia media = new FeedMedia(0, item, 10, 0, 1, "mime", null, + "url", false, playbackCompletionDate, 0, 0); feed.getItems().add(item); item.setMedia(media); PodDBAdapter adapter = PodDBAdapter.getInstance(); @@ -480,8 +484,7 @@ public class DBWriterTest { } @Test - public void testAddItemToPlaybackHistoryNotPlayedYet() - throws ExecutionException, InterruptedException, TimeoutException { + public void testAddItemToPlaybackHistoryNotPlayedYet() throws Exception { FeedMedia media = playbackHistorySetup(null); DBWriter.addItemToPlaybackHistory(media).get(TIMEOUT, TimeUnit.SECONDS); PodDBAdapter adapter = PodDBAdapter.getInstance(); @@ -494,11 +497,10 @@ public class DBWriterTest { } @Test - public void testAddItemToPlaybackHistoryAlreadyPlayed() - throws ExecutionException, InterruptedException, TimeoutException { - final long OLD_DATE = 0; + public void testAddItemToPlaybackHistoryAlreadyPlayed() throws Exception { + final long oldDate = 0; - FeedMedia media = playbackHistorySetup(new Date(OLD_DATE)); + FeedMedia media = playbackHistorySetup(new Date(oldDate)); DBWriter.addItemToPlaybackHistory(media).get(TIMEOUT, TimeUnit.SECONDS); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -507,11 +509,11 @@ public class DBWriterTest { assertNotNull(media); assertNotNull(media.getPlaybackCompletionDate()); - assertNotEquals(media.getPlaybackCompletionDate().getTime(), OLD_DATE); + assertNotEquals(media.getPlaybackCompletionDate().getTime(), oldDate); } - private Feed queueTestSetupMultipleItems(final int numItems) throws InterruptedException, ExecutionException, TimeoutException { - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + @SuppressWarnings("SameParameterValue") + private Feed queueTestSetupMultipleItems(final int numItems) throws Exception { UserPreferences.setEnqueueLocation(UserPreferences.EnqueueLocation.BACK); Feed feed = new Feed("url", null, "title"); feed.setItems(new ArrayList<>()); @@ -539,8 +541,7 @@ public class DBWriterTest { } @Test - public void testAddQueueItemSingleItem() throws InterruptedException, ExecutionException, TimeoutException { - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + public void testAddQueueItemSingleItem() throws Exception { Feed feed = new Feed("url", null, "title"); feed.setItems(new ArrayList<>()); FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed); @@ -564,8 +565,7 @@ public class DBWriterTest { } @Test - public void testAddQueueItemSingleItemAlreadyInQueue() throws InterruptedException, ExecutionException, TimeoutException { - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + public void testAddQueueItemSingleItemAlreadyInQueue() throws Exception { Feed feed = new Feed("url", null, "title"); feed.setItems(new ArrayList<>()); FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed); @@ -599,18 +599,20 @@ public class DBWriterTest { } @Test - public void testAddQueueItemMultipleItems() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; + public void testAddQueueItemMultipleItems() throws Exception { + final int numItems = 10; - Feed feed = queueTestSetupMultipleItems(NUM_ITEMS); + Feed feed; + feed = queueTestSetupMultipleItems(numItems); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor cursor = adapter.getQueueIDCursor(); assertTrue(cursor.moveToFirst()); - assertEquals(NUM_ITEMS, cursor.getCount()); - List<Long> expectedIds = FeedItemUtil.getIdList(feed.getItems()); + assertEquals(numItems, cursor.getCount()); + List<Long> expectedIds; + expectedIds = FeedItemUtil.getIdList(feed.getItems()); List<Long> actualIds = new ArrayList<>(); - for (int i = 0; i < NUM_ITEMS; i++) { + for (int i = 0; i < numItems; i++) { assertTrue(cursor.moveToPosition(i)); actualIds.add(cursor.getLong(0)); } @@ -621,10 +623,10 @@ public class DBWriterTest { } @Test - public void testClearQueue() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; + public void testClearQueue() throws Exception { + final int numItems = 10; - queueTestSetupMultipleItems(NUM_ITEMS); + queueTestSetupMultipleItems(numItems); DBWriter.clearQueue().get(TIMEOUT, TimeUnit.SECONDS); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -635,12 +637,11 @@ public class DBWriterTest { } @Test - public void testRemoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Feed feed = createTestFeed(NUM_ITEMS); + public void testRemoveQueueItem() throws Exception { + final int numItems = 10; + Feed feed = createTestFeed(numItems); - for (int removeIndex = 0; removeIndex < NUM_ITEMS; removeIndex++) { + for (int removeIndex = 0; removeIndex < numItems; removeIndex++) { final FeedItem item = feed.getItems().get(removeIndex); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -651,7 +652,7 @@ public class DBWriterTest { adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor queue = adapter.getQueueIDCursor(); - assertEquals(NUM_ITEMS - 1, queue.getCount()); + assertEquals(numItems - 1, queue.getCount()); for (int i = 0; i < queue.getCount(); i++) { assertTrue(queue.moveToPosition(i)); final long queueID = queue.getLong(0); @@ -668,16 +669,13 @@ public class DBWriterTest { } @Test - public void testRemoveQueueItemMultipleItems() throws InterruptedException, ExecutionException, TimeoutException { - // Setup test data - // - final int NUM_ITEMS = 5; - final int NUM_IN_QUEUE = NUM_ITEMS - 1; // the last one not in queue for boundary condition - final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Feed feed = createTestFeed(NUM_ITEMS); + public void testRemoveQueueItemMultipleItems() throws Exception { + final int numItems = 5; + final int numInQueue = numItems - 1; // the last one not in queue for boundary condition + Feed feed = createTestFeed(numItems); - List<FeedItem> itemsToAdd = feed.getItems().subList(0, NUM_IN_QUEUE); - withPodDB(adapter -> adapter.setQueue(itemsToAdd) ); + List<FeedItem> itemsToAdd = feed.getItems().subList(0, numInQueue); + withPodDB(adapter -> adapter.setQueue(itemsToAdd)); // Actual tests // @@ -706,12 +704,13 @@ public class DBWriterTest { } @Test - public void testMoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; + public void testMoveQueueItem() throws Exception { + final int numItems = 10; Feed feed = new Feed("url", null, "title"); feed.setItems(new ArrayList<>()); - for (int i = 0; i < NUM_ITEMS; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed); + for (int i = 0; i < numItems; i++) { + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, + new Date(), FeedItem.PLAYED, feed); feed.getItems().add(item); } @@ -723,8 +722,8 @@ public class DBWriterTest { for (FeedItem item : feed.getItems()) { assertTrue(item.getId() != 0); } - for (int from = 0; from < NUM_ITEMS; from++) { - for (int to = 0; to < NUM_ITEMS; to++) { + for (int from = 0; from < numItems; from++) { + for (int to = 0; to < numItems; to++) { if (from == to) { continue; } @@ -740,7 +739,7 @@ public class DBWriterTest { adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor queue = adapter.getQueueIDCursor(); - assertEquals(NUM_ITEMS, queue.getCount()); + assertEquals(numItems, queue.getCount()); assertTrue(queue.moveToPosition(from)); assertNotEquals(fromID, queue.getLong(0)); assertTrue(queue.moveToPosition(to)); @@ -753,12 +752,13 @@ public class DBWriterTest { } @Test - public void testMarkFeedRead() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; + public void testMarkFeedRead() throws Exception { + final int numItems = 10; Feed feed = new Feed("url", null, "title"); feed.setItems(new ArrayList<>()); - for (int i = 0; i < NUM_ITEMS; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.UNPLAYED, feed); + for (int i = 0; i < numItems; i++) { + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, + new Date(), FeedItem.UNPLAYED, feed); feed.getItems().add(item); } @@ -780,12 +780,13 @@ public class DBWriterTest { } @Test - public void testMarkAllItemsReadSameFeed() throws InterruptedException, ExecutionException, TimeoutException { - final int NUM_ITEMS = 10; + public void testMarkAllItemsReadSameFeed() throws Exception { + final int numItems = 10; Feed feed = new Feed("url", null, "title"); feed.setItems(new ArrayList<>()); - for (int i = 0; i < NUM_ITEMS; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.UNPLAYED, feed); + for (int i = 0; i < numItems; i++) { + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, + new Date(), FeedItem.UNPLAYED, feed); feed.getItems().add(item); } @@ -810,7 +811,8 @@ public class DBWriterTest { Feed feed = new Feed("url", null, "title"); feed.setItems(new ArrayList<>()); for (int i = 0; i < numItems; i++) { - FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed); + FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, + new Date(), FeedItem.PLAYED, feed); feed.getItems().add(item); } @@ -832,10 +834,7 @@ public class DBWriterTest { } } - private static void assertQueueByItemIds( - String message, - long... itemIdsExpected - ) { + private static void assertQueueByItemIds(String message, long... itemIdsExpected) { List<FeedItem> queue = DBReader.getQueue(); List<Long> itemIdsActualList = toItemIds(queue); List<Long> itemIdsExpectedList = new ArrayList<>(itemIdsExpected.length); @@ -848,10 +847,9 @@ public class DBWriterTest { private static List<Long> toItemIds(List<FeedItem> items) { List<Long> itemIds = new ArrayList<>(items.size()); - for(FeedItem item : items) { + for (FeedItem item : items) { itemIds.add(item.getId()); } return itemIds; } - } diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java new file mode 100644 index 000000000..8c02391ca --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java @@ -0,0 +1,89 @@ +package de.danoeh.antennapod.core.storage; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests that the APFavoriteCleanupAlgorithm is working correctly. + */ +@RunWith(RobolectricTestRunner.class) +public class ExceptFavoriteCleanupAlgorithmTest extends DbCleanupTests { + private final int numberOfItems = EPISODE_CACHE_SIZE * 2; + + public ExceptFavoriteCleanupAlgorithmTest() { + setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE); + } + + @Test + public void testPerformAutoCleanupHandleUnplayed() throws IOException { + Feed feed = new Feed("url", null, "title"); + List<FeedItem> items = new ArrayList<>(); + feed.setItems(items); + List<File> files = new ArrayList<>(); + populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, false, false); + + DBTasks.performAutoCleanup(context); + for (int i = 0; i < files.size(); i++) { + if (i < EPISODE_CACHE_SIZE) { + assertTrue("Only enough items should be deleted", files.get(i).exists()); + } else { + assertFalse("Expected episode to be deleted", files.get(i).exists()); + } + } + } + + @Test + public void testPerformAutoCleanupDeletesQueued() throws IOException { + Feed feed = new Feed("url", null, "title"); + List<FeedItem> items = new ArrayList<>(); + feed.setItems(items); + List<File> files = new ArrayList<>(); + populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, true, false); + + DBTasks.performAutoCleanup(context); + for (int i = 0; i < files.size(); i++) { + if (i < EPISODE_CACHE_SIZE) { + assertTrue("Only enough items should be deleted", files.get(i).exists()); + } else { + assertFalse("Queued episodes should be deleted", files.get(i).exists()); + } + } + } + + @Test + public void testPerformAutoCleanupSavesFavorited() throws IOException { + Feed feed = new Feed("url", null, "title"); + List<FeedItem> items = new ArrayList<>(); + feed.setItems(items); + List<File> files = new ArrayList<>(); + populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, false, true); + + DBTasks.performAutoCleanup(context); + for (int i = 0; i < files.size(); i++) { + assertTrue("Favorite episodes should should not be deleted", files.get(i).exists()); + } + } + + @Override + public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue() throws IOException { + // Yes it should + } + + @Override + public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue_withFeedsWithNoMedia() throws IOException { + // Yes it should + } +} diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java index 6c5a9daf1..00fa1b8f5 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.storage; +import de.danoeh.antennapod.core.util.playback.RemoteMedia; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -17,9 +18,7 @@ import de.danoeh.antennapod.core.feed.FeedComponent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedMother; -import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation; -import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.Playable; import static de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation.AFTER_CURRENTLY_PLAYING; @@ -105,7 +104,7 @@ public class ItemEnqueuePositionCalculatorTest { {"case option after currently playing, no currentlyPlaying is null", concat(TFI_ID, QUEUE_DEFAULT_IDS), AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, ID_CURRENTLY_PLAYING_NULL}, - {"case option after currently playing, currentlyPlaying is externalMedia", + {"case option after currently playing, currentlyPlaying is not a feedMedia", concat(TFI_ID, QUEUE_DEFAULT_IDS), AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, ID_CURRENTLY_PLAYING_NOT_FEEDMEDIA}, {"case empty queue, option after currently playing", @@ -270,7 +269,7 @@ public class ItemEnqueuePositionCalculatorTest { } static Playable externalMedia() { - return new ExternalMedia("http://example.com/episode.mp3", MediaType.AUDIO); + return new RemoteMedia(createFeedItem(0)); } static final long ID_CURRENTLY_PLAYING_NULL = -1L; diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java new file mode 100644 index 000000000..c779b6d55 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java @@ -0,0 +1,100 @@ +package de.danoeh.antennapod.core.storage.mapper; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +public class FeedCursorMapperTest { + private PodDBAdapter adapter; + + @Before + public void setUp() { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + + PodDBAdapter.init(context); + adapter = PodDBAdapter.getInstance(); + + writeFeedToDatabase(); + } + + @After + public void tearDown() { + PodDBAdapter.tearDownTests(); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testFromCursor() { + try (Cursor cursor = adapter.getAllFeedsCursor()) { + cursor.moveToNext(); + Feed feed = FeedCursorMapper.convert(cursor); + assertTrue(feed.getId() >= 0); + assertEquals("feed custom title", feed.getTitle()); + assertEquals("feed custom title", feed.getCustomTitle()); + assertEquals("feed link", feed.getLink()); + assertEquals("feed description", feed.getDescription()); + assertEquals("feed payment link", feed.getPaymentLink()); + assertEquals("feed author", feed.getAuthor()); + assertEquals("feed language", feed.getLanguage()); + assertEquals("feed image url", feed.getImageUrl()); + assertEquals("feed file url", feed.getFile_url()); + assertEquals("feed download url", feed.getDownload_url()); + assertTrue(feed.isDownloaded()); + assertEquals("feed last update", feed.getLastUpdate()); + assertEquals("feed type", feed.getType()); + assertEquals("feed identifier", feed.getFeedIdentifier()); + assertTrue(feed.isPaged()); + assertEquals("feed next page link", feed.getNextPageLink()); + assertTrue(feed.getItemFilter().showUnplayed); + assertEquals(1, feed.getSortOrder().code); + assertTrue(feed.hasLastUpdateFailed()); + } + } + + /** + * Insert test data to the database. + * Uses raw database insert instead of adapter.setCompleteFeed() to avoid testing the Feed class + * against itself. + */ + private void writeFeedToDatabase() { + ContentValues values = new ContentValues(); + values.put(PodDBAdapter.KEY_TITLE, "feed title"); + values.put(PodDBAdapter.KEY_CUSTOM_TITLE, "feed custom title"); + values.put(PodDBAdapter.KEY_LINK, "feed link"); + values.put(PodDBAdapter.KEY_DESCRIPTION, "feed description"); + values.put(PodDBAdapter.KEY_PAYMENT_LINK, "feed payment link"); + values.put(PodDBAdapter.KEY_AUTHOR, "feed author"); + values.put(PodDBAdapter.KEY_LANGUAGE, "feed language"); + values.put(PodDBAdapter.KEY_IMAGE_URL, "feed image url"); + + values.put(PodDBAdapter.KEY_FILE_URL, "feed file url"); + values.put(PodDBAdapter.KEY_DOWNLOAD_URL, "feed download url"); + values.put(PodDBAdapter.KEY_DOWNLOADED, true); + values.put(PodDBAdapter.KEY_LASTUPDATE, "feed last update"); + values.put(PodDBAdapter.KEY_TYPE, "feed type"); + values.put(PodDBAdapter.KEY_FEED_IDENTIFIER, "feed identifier"); + + values.put(PodDBAdapter.KEY_IS_PAGED, true); + values.put(PodDBAdapter.KEY_NEXT_PAGE_LINK, "feed next page link"); + values.put(PodDBAdapter.KEY_HIDE, "unplayed"); + values.put(PodDBAdapter.KEY_SORT_ORDER, "1"); + values.put(PodDBAdapter.KEY_LAST_UPDATE_FAILED, true); + + adapter.insertTestData(PodDBAdapter.TABLE_NAME_FEEDS, values); + } +} diff --git a/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/AtomParserTest.java b/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/AtomParserTest.java new file mode 100644 index 000000000..1195bed69 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/AtomParserTest.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.File; +import java.util.Date; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for Atom feeds in FeedHandler. + */ +@RunWith(RobolectricTestRunner.class) +public class AtomParserTest { + + @Test + public void testAtomBasic() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-atom-testAtomBasic.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals(Feed.TYPE_ATOM1, feed.getType()); + assertEquals("title", feed.getTitle()); + assertEquals("http://example.com/feed", feed.getFeedIdentifier()); + assertEquals("http://example.com", feed.getLink()); + assertEquals("This is the description", feed.getDescription()); + assertEquals("http://example.com/payment", feed.getPaymentLink()); + assertEquals("http://example.com/picture", feed.getImageUrl()); + assertEquals(10, feed.getItems().size()); + for (int i = 0; i < feed.getItems().size(); i++) { + FeedItem item = feed.getItems().get(i); + assertEquals("http://example.com/item-" + i, item.getItemIdentifier()); + assertEquals("item-" + i, item.getTitle()); + assertNull(item.getDescription()); + assertEquals("http://example.com/items/" + i, item.getLink()); + assertEquals(new Date(i * 60000), item.getPubDate()); + assertNull(item.getPaymentLink()); + assertEquals("http://example.com/picture", item.getImageLocation()); + // media + assertTrue(item.hasMedia()); + FeedMedia media = item.getMedia(); + //noinspection ConstantConditions + assertEquals("http://example.com/media-" + i, media.getDownload_url()); + assertEquals(1024 * 1024, media.getSize()); + assertEquals("audio/mp3", media.getMime_type()); + // chapters + assertNull(item.getChapters()); + } + } + + @Test + public void testLogoWithWhitespace() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-atom-testLogoWithWhitespace.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals("title", feed.getTitle()); + assertEquals("http://example.com/feed", feed.getFeedIdentifier()); + assertEquals("http://example.com", feed.getLink()); + assertEquals("This is the description", feed.getDescription()); + assertEquals("http://example.com/payment", feed.getPaymentLink()); + assertEquals("https://example.com/image.png", feed.getImageUrl()); + assertEquals(0, feed.getItems().size()); + } +} diff --git a/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/FeedParserTestHelper.java b/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/FeedParserTestHelper.java new file mode 100644 index 000000000..c02a7d209 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/FeedParserTestHelper.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import androidx.annotation.NonNull; + +import java.io.File; + +import de.danoeh.antennapod.core.feed.Feed; + +/** + * Tests for FeedHandler. + */ +public abstract class FeedParserTestHelper { + + /** + * Returns the File object for a file in the resources folder. + */ + @NonNull + static File getFeedFile(@NonNull String fileName) { + //noinspection ConstantConditions + return new File(FeedParserTestHelper.class.getClassLoader().getResource(fileName).getFile()); + } + + /** + * Runs the feed parser on the given file. + */ + @NonNull + static Feed runFeedParser(@NonNull File feedFile) throws Exception { + FeedHandler handler = new FeedHandler(); + Feed parsedFeed = new Feed("http://example.com/feed", null); + parsedFeed.setFile_url(feedFile.getAbsolutePath()); + parsedFeed.setDownloaded(true); + handler.parseFeed(parsedFeed); + return parsedFeed; + } +} diff --git a/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/RssParserTest.java b/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/RssParserTest.java new file mode 100644 index 000000000..95b04a511 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/RssParserTest.java @@ -0,0 +1,85 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.File; +import java.util.Date; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for RSS feeds in FeedHandler. + */ +@RunWith(RobolectricTestRunner.class) +public class RssParserTest { + + @Test + public void testRss2Basic() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testRss2Basic.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals(Feed.TYPE_RSS2, feed.getType()); + assertEquals("title", feed.getTitle()); + assertEquals("en", feed.getLanguage()); + assertEquals("http://example.com", feed.getLink()); + assertEquals("This is the description", feed.getDescription()); + assertEquals("http://example.com/payment", feed.getPaymentLink()); + assertEquals("http://example.com/picture", feed.getImageUrl()); + assertEquals(10, feed.getItems().size()); + for (int i = 0; i < feed.getItems().size(); i++) { + FeedItem item = feed.getItems().get(i); + assertEquals("http://example.com/item-" + i, item.getItemIdentifier()); + assertEquals("item-" + i, item.getTitle()); + assertNull(item.getDescription()); + assertEquals("http://example.com/items/" + i, item.getLink()); + assertEquals(new Date(i * 60000), item.getPubDate()); + assertNull(item.getPaymentLink()); + assertEquals("http://example.com/picture", item.getImageLocation()); + // media + assertTrue(item.hasMedia()); + FeedMedia media = item.getMedia(); + //noinspection ConstantConditions + assertEquals("http://example.com/media-" + i, media.getDownload_url()); + assertEquals(1024 * 1024, media.getSize()); + assertEquals("audio/mp3", media.getMime_type()); + // chapters + assertNull(item.getChapters()); + } + } + + @Test + public void testImageWithWhitespace() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testImageWithWhitespace.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals("title", feed.getTitle()); + assertEquals("http://example.com", feed.getLink()); + assertEquals("This is the description", feed.getDescription()); + assertEquals("http://example.com/payment", feed.getPaymentLink()); + assertEquals("https://example.com/image.png", feed.getImageUrl()); + assertEquals(0, feed.getItems().size()); + } + + @Test + public void testMediaContentMime() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testMediaContentMime.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals("title", feed.getTitle()); + assertEquals("http://example.com", feed.getLink()); + assertEquals("This is the description", feed.getDescription()); + assertEquals("http://example.com/payment", feed.getPaymentLink()); + assertNull(feed.getImageUrl()); + assertEquals(1, feed.getItems().size()); + FeedItem feedItem = feed.getItems().get(0); + //noinspection ConstantConditions + assertEquals(MediaType.VIDEO, feedItem.getMedia().getMediaType()); + assertEquals("https://www.example.com/file.mp4", feedItem.getMedia().getDownload_url()); + } +} diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/util/DateUtilsTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/DateUtilsTest.java index 5d98f133c..92888ae8b 100644 --- a/core/src/androidTest/java/de/danoeh/antennapod/core/util/DateUtilsTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/util/DateUtilsTest.java @@ -1,6 +1,5 @@ package de.danoeh.antennapod.core.util; -import androidx.test.filters.SmallTest; import org.junit.Test; import java.util.Calendar; @@ -12,16 +11,11 @@ import static org.junit.Assert.assertEquals; /** * Unit test for {@link DateUtils}. - * - * Note: It NEEDS to be run in android devices, i.e., it cannot be run in standard JDK, because - * the test invokes some android platform-specific behavior in the underlying - * {@link java.text.SimpleDateFormat} used by {@link DateUtils}. - * */ -@SmallTest public class DateUtilsTest { + @Test - public void testParseDateWithMicroseconds() throws Exception { + public void testParseDateWithMicroseconds() { GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis() + 963); @@ -30,7 +24,7 @@ public class DateUtilsTest { } @Test - public void testParseDateWithCentiseconds() throws Exception { + public void testParseDateWithCentiseconds() { GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis() + 960); @@ -39,17 +33,17 @@ public class DateUtilsTest { } @Test - public void testParseDateWithDeciseconds() throws Exception { + public void testParseDateWithDeciseconds() { GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis() + 900); Date actual = DateUtils.parse("2015-03-28T13:31:04.9"); - assertEquals(expected.getTime()/1000, actual.getTime()/1000); - assertEquals(900, actual.getTime()%1000); + assertEquals(expected.getTime() / 1000, actual.getTime() / 1000); + assertEquals(900, actual.getTime() % 1000); } @Test - public void testParseDateWithMicrosecondsAndTimezone() throws Exception { + public void testParseDateWithMicrosecondsAndTimezone() { GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis() + 963); @@ -58,7 +52,7 @@ public class DateUtilsTest { } @Test - public void testParseDateWithCentisecondsAndTimezone() throws Exception { + public void testParseDateWithCentisecondsAndTimezone() { GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis() + 960); @@ -67,17 +61,17 @@ public class DateUtilsTest { } @Test - public void testParseDateWithDecisecondsAndTimezone() throws Exception { + public void testParseDateWithDecisecondsAndTimezone() { GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis() + 900); Date actual = DateUtils.parse("2015-03-28T13:31:04.9 +0700"); - assertEquals(expected.getTime()/1000, actual.getTime()/1000); - assertEquals(900, actual.getTime()%1000); + assertEquals(expected.getTime() / 1000, actual.getTime() / 1000); + assertEquals(900, actual.getTime() % 1000); } @Test - public void testParseDateWithTimezoneName() throws Exception { + public void testParseDateWithTimezoneName() { GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis()); @@ -86,7 +80,7 @@ public class DateUtilsTest { } @Test - public void testParseDateWithTimezoneName2() throws Exception { + public void testParseDateWithTimezoneName2() { GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 0); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis()); @@ -95,7 +89,7 @@ public class DateUtilsTest { } @Test - public void testParseDateWithTimeZoneOffset() throws Exception { + public void testParseDateWithTimeZoneOffset() { GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 12, 16, 12); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis()); @@ -104,7 +98,7 @@ public class DateUtilsTest { } @Test - public void testAsctime() throws Exception { + public void testAsctime() { GregorianCalendar exp = new GregorianCalendar(2011, 4, 25, 12, 33, 0); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis()); @@ -113,7 +107,7 @@ public class DateUtilsTest { } @Test - public void testMultipleConsecutiveSpaces() throws Exception { + public void testMultipleConsecutiveSpaces() { GregorianCalendar exp = new GregorianCalendar(2010, 2, 23, 6, 6, 26); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis()); @@ -121,14 +115,8 @@ public class DateUtilsTest { assertEquals(expected, actual); } - /** - * Requires Android platform. - * - * Reason: Standard JDK cannot parse timezone <code>-08:00</code> (ISO 8601 format). It only accepts - * <code>-0800</code> (RFC 822 format) - */ @Test - public void testParseDateWithNoTimezonePadding() throws Exception { + public void testParseDateWithNoTimezonePadding() { GregorianCalendar exp = new GregorianCalendar(2017, 1, 22, 22, 28, 0); exp.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected = new Date(exp.getTimeInMillis() + 2); @@ -143,7 +131,7 @@ public class DateUtilsTest { * @see #testParseDateWithNoTimezonePadding() */ @Test - public void testParseDateWithForCest() throws Exception { + public void testParseDateWithForCest() { GregorianCalendar exp1 = new GregorianCalendar(2017, 0, 28, 22, 0, 0); exp1.setTimeZone(TimeZone.getTimeZone("UTC")); Date expected1 = new Date(exp1.getTimeInMillis()); diff --git a/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java index f376c75a5..af22a4b9d 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/FilenameGeneratorTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/util/FilenameGeneratorTest.java @@ -1,22 +1,21 @@ -package de.test.antennapod.util; +package de.danoeh.antennapod.core.util; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.SmallTest; import android.text.TextUtils; import java.io.File; -import java.io.IOException; -import de.danoeh.antennapod.core.util.FileNameGenerator; import org.apache.commons.lang3.StringUtils; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; -@SmallTest +@RunWith(RobolectricTestRunner.class) public class FilenameGeneratorTest { public FilenameGeneratorTest() { @@ -24,21 +23,21 @@ public class FilenameGeneratorTest { } @Test - public void testGenerateFileName() throws IOException { + public void testGenerateFileName() throws Exception { String result = FileNameGenerator.generateFileName("abc abc"); assertEquals(result, "abc abc"); createFiles(result); } @Test - public void testGenerateFileName1() throws IOException { + public void testGenerateFileName1() throws Exception { String result = FileNameGenerator.generateFileName("ab/c: <abc"); assertEquals(result, "abc abc"); createFiles(result); } @Test - public void testGenerateFileName2() throws IOException { + public void testGenerateFileName2() throws Exception { String result = FileNameGenerator.generateFileName("abc abc "); assertEquals(result, "abc abc"); createFiles(result); @@ -69,7 +68,7 @@ public class FilenameGeneratorTest { } @Test - public void testLongFilename() throws IOException { + public void testLongFilename() throws Exception { String longName = StringUtils.repeat("x", 20 + FileNameGenerator.MAX_FILENAME_LENGTH); String result = FileNameGenerator.generateFileName(longName); assertTrue(result.length() <= FileNameGenerator.MAX_FILENAME_LENGTH); @@ -87,16 +86,13 @@ public class FilenameGeneratorTest { /** * Tests if files can be created. - * - * @throws IOException */ - private void createFiles(String name) throws IOException { + private void createFiles(String name) throws Exception { File cache = InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalCacheDir(); File testFile = new File(cache, name); - testFile.mkdir(); + assertTrue(testFile.mkdir()); assertTrue(testFile.exists()); - testFile.delete(); + assertTrue(testFile.delete()); assertTrue(testFile.createNewFile()); } - } diff --git a/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/URLCheckerTest.java index 7f26ff612..a4b3dee06 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/URLCheckerTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/util/URLCheckerTest.java @@ -1,17 +1,17 @@ -package de.test.antennapod.util; +package de.danoeh.antennapod.core.util; -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.util.URLChecker; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** - * Test class for URLChecker + * Test class for {@link URLChecker} */ -@SmallTest +@RunWith(RobolectricTestRunner.class) public class URLCheckerTest { @Test @@ -78,7 +78,7 @@ public class URLCheckerTest { } @Test - public void testAntennaPodSubscribeProtocolNoScheme() throws Exception { + public void testAntennaPodSubscribeProtocolNoScheme() { final String in = "antennapod-subscribe://example.com"; final String out = URLChecker.prepareURL(in); assertEquals("http://example.com", out); @@ -92,14 +92,14 @@ public class URLCheckerTest { } @Test - public void testAntennaPodSubscribeProtocolWithScheme() throws Exception { + public void testAntennaPodSubscribeProtocolWithScheme() { final String in = "antennapod-subscribe://https://example.com"; final String out = URLChecker.prepareURL(in); assertEquals("https://example.com", out); } @Test - public void testProtocolRelativeUrlIsAbsolute() throws Exception { + public void testProtocolRelativeUrlIsAbsolute() { final String in = "https://example.com"; final String inBase = "http://examplebase.com"; final String out = URLChecker.prepareURL(in, inBase); @@ -107,7 +107,7 @@ public class URLCheckerTest { } @Test - public void testProtocolRelativeUrlIsRelativeHttps() throws Exception { + public void testProtocolRelativeUrlIsRelativeHttps() { final String in = "//example.com"; final String inBase = "https://examplebase.com"; final String out = URLChecker.prepareURL(in, inBase); @@ -115,7 +115,7 @@ public class URLCheckerTest { } @Test - public void testProtocolRelativeUrlIsHttpsWithAPSubscribeProtocol() throws Exception { + public void testProtocolRelativeUrlIsHttpsWithApSubscribeProtocol() { final String in = "//example.com"; final String inBase = "antennapod-subscribe://https://examplebase.com"; final String out = URLChecker.prepareURL(in, inBase); @@ -123,7 +123,7 @@ public class URLCheckerTest { } @Test - public void testProtocolRelativeUrlBaseUrlNull() throws Exception { + public void testProtocolRelativeUrlBaseUrlNull() { final String in = "example.com"; final String out = URLChecker.prepareURL(in, null); assertEquals("http://example.com", out); diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/ChapterReaderTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/ChapterReaderTest.java new file mode 100644 index 000000000..84fe9d94d --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/ChapterReaderTest.java @@ -0,0 +1,188 @@ +package de.danoeh.antennapod.core.util.id3reader; + +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.ID3Chapter; +import de.danoeh.antennapod.core.util.EmbeddedChapterImage; +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import org.apache.commons.io.input.CountingInputStream; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; + +import static de.danoeh.antennapod.core.util.id3reader.Id3ReaderTest.concat; +import static de.danoeh.antennapod.core.util.id3reader.Id3ReaderTest.generateFrameHeader; +import static de.danoeh.antennapod.core.util.id3reader.Id3ReaderTest.generateId3Header; +import static org.junit.Assert.assertEquals; + +public class ChapterReaderTest { + private static final byte CHAPTER_WITHOUT_SUBFRAME_START_TIME = 23; + private static final byte[] CHAPTER_WITHOUT_SUBFRAME = { + 'C', 'H', '1', 0, // String ID for mapping to CTOC + 0, 0, 0, CHAPTER_WITHOUT_SUBFRAME_START_TIME, // Start time + 0, 0, 0, 0, // End time + 0, 0, 0, 0, // Start offset + 0, 0, 0, 0 // End offset + }; + + @Test + public void testReadFullTagWithChapter() throws IOException, ID3ReaderException { + byte[] chapter = concat( + generateFrameHeader(ChapterReader.FRAME_ID_CHAPTER, CHAPTER_WITHOUT_SUBFRAME.length), + CHAPTER_WITHOUT_SUBFRAME); + byte[] data = concat( + generateId3Header(chapter.length), + chapter); + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + ChapterReader reader = new ChapterReader(inputStream); + reader.readInputStream(); + assertEquals(1, reader.getChapters().size()); + assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, reader.getChapters().get(0).getStart()); + } + + @Test + public void testReadFullTagWithMultipleChapters() throws IOException, ID3ReaderException { + byte[] chapter = concat( + generateFrameHeader(ChapterReader.FRAME_ID_CHAPTER, CHAPTER_WITHOUT_SUBFRAME.length), + CHAPTER_WITHOUT_SUBFRAME); + byte[] data = concat( + generateId3Header(2 * chapter.length), + chapter, + chapter); + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + ChapterReader reader = new ChapterReader(inputStream); + reader.readInputStream(); + assertEquals(2, reader.getChapters().size()); + assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, reader.getChapters().get(0).getStart()); + assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, reader.getChapters().get(1).getStart()); + } + + @Test + public void testReadChapterWithoutSubframes() throws IOException, ID3ReaderException { + FrameHeader header = new FrameHeader(ChapterReader.FRAME_ID_CHAPTER, + CHAPTER_WITHOUT_SUBFRAME.length, (short) 0); + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(CHAPTER_WITHOUT_SUBFRAME)); + Chapter chapter = new ChapterReader(inputStream).readChapter(header); + assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, chapter.getStart()); + } + + @Test + public void testReadChapterWithTitle() throws IOException, ID3ReaderException { + byte[] title = { + ID3Reader.ENCODING_ISO, + 'H', 'e', 'l', 'l', 'o', // Title + 0 // Null-terminated + }; + byte[] chapterData = concat( + CHAPTER_WITHOUT_SUBFRAME, + generateFrameHeader(ChapterReader.FRAME_ID_TITLE, title.length), + title); + FrameHeader header = new FrameHeader(ChapterReader.FRAME_ID_CHAPTER, chapterData.length, (short) 0); + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(chapterData)); + ChapterReader reader = new ChapterReader(inputStream); + Chapter chapter = reader.readChapter(header); + assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, chapter.getStart()); + assertEquals("Hello", chapter.getTitle()); + } + + @Test + public void testReadTitleWithGarbage() throws IOException, ID3ReaderException { + byte[] titleSubframeContent = { + ID3Reader.ENCODING_ISO, + 'A', // Title + 0, // Null-terminated + 42, 42, 42, 42 // Garbage, should be ignored + }; + FrameHeader header = new FrameHeader(ChapterReader.FRAME_ID_TITLE, titleSubframeContent.length, (short) 0); + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(titleSubframeContent)); + ChapterReader reader = new ChapterReader(inputStream); + Chapter chapter = new ID3Chapter("", 0); + reader.readChapterSubFrame(header, chapter); + assertEquals("A", chapter.getTitle()); + + // Should skip the garbage and point to the next frame + assertEquals(titleSubframeContent.length, reader.getPosition()); + } + + @Test + public void testRealFileUltraschall() throws IOException, ID3ReaderException { + CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader() + .getResource("media-parser/ultraschall5.mp3").openStream()); + ChapterReader reader = new ChapterReader(inputStream); + reader.readInputStream(); + List<Chapter> chapters = reader.getChapters(); + + assertEquals(3, chapters.size()); + + assertEquals(0, chapters.get(0).getStart()); + assertEquals(4004, chapters.get(1).getStart()); + assertEquals(7999, chapters.get(2).getStart()); + + assertEquals("Marke 1", chapters.get(0).getTitle()); + assertEquals("Marke 2", chapters.get(1).getTitle()); + assertEquals("Marke 3", chapters.get(2).getTitle()); + + assertEquals("https://example.com", chapters.get(0).getLink()); + assertEquals("https://example.com", chapters.get(1).getLink()); + assertEquals("https://example.com", chapters.get(2).getLink()); + + assertEquals(EmbeddedChapterImage.makeUrl(16073, 2750569), chapters.get(0).getImageUrl()); + assertEquals(EmbeddedChapterImage.makeUrl(2766765, 15740), chapters.get(1).getImageUrl()); + assertEquals(EmbeddedChapterImage.makeUrl(2782628, 2750569), chapters.get(2).getImageUrl()); + } + + @Test + public void testRealFileAuphonic() throws IOException, ID3ReaderException { + CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader() + .getResource("media-parser/auphonic.mp3").openStream()); + ChapterReader reader = new ChapterReader(inputStream); + reader.readInputStream(); + List<Chapter> chapters = reader.getChapters(); + + assertEquals(4, chapters.size()); + + assertEquals(0, chapters.get(0).getStart()); + assertEquals(3000, chapters.get(1).getStart()); + assertEquals(6000, chapters.get(2).getStart()); + assertEquals(9000, chapters.get(3).getStart()); + + assertEquals("Chapter 1 - ❤️😊", chapters.get(0).getTitle()); + assertEquals("Chapter 2 - ßöÄ", chapters.get(1).getTitle()); + assertEquals("Chapter 3 - 爱", chapters.get(2).getTitle()); + assertEquals("Chapter 4", chapters.get(3).getTitle()); + + assertEquals("https://example.com", chapters.get(0).getLink()); + assertEquals("https://example.com", chapters.get(1).getLink()); + assertEquals("https://example.com", chapters.get(2).getLink()); + assertEquals("https://example.com", chapters.get(3).getLink()); + + assertEquals(EmbeddedChapterImage.makeUrl(765, 308), chapters.get(0).getImageUrl()); + assertEquals(EmbeddedChapterImage.makeUrl(1271, 308), chapters.get(1).getImageUrl()); + assertEquals(EmbeddedChapterImage.makeUrl(1771, 308), chapters.get(2).getImageUrl()); + assertEquals(EmbeddedChapterImage.makeUrl(2259, 308), chapters.get(3).getImageUrl()); + } + + @Test + public void testRealFileHindenburgJournalistPro() throws IOException, ID3ReaderException { + CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader() + .getResource("media-parser/hindenburg-journalist-pro.mp3").openStream()); + ChapterReader reader = new ChapterReader(inputStream); + reader.readInputStream(); + List<Chapter> chapters = reader.getChapters(); + + assertEquals(2, chapters.size()); + + assertEquals(0, chapters.get(0).getStart()); + assertEquals(5006, chapters.get(1).getStart()); + + assertEquals("Chapter Marker 1", chapters.get(0).getTitle()); + assertEquals("Chapter Marker 2", chapters.get(1).getTitle()); + + assertEquals("https://example.com/chapter1url", chapters.get(0).getLink()); + assertEquals("https://example.com/chapter2url", chapters.get(1).getLink()); + + assertEquals(EmbeddedChapterImage.makeUrl(5330, 4015), chapters.get(0).getImageUrl()); + assertEquals(EmbeddedChapterImage.makeUrl(9498, 4364), chapters.get(1).getImageUrl()); + } +} diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/Id3ReaderTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/Id3ReaderTest.java new file mode 100644 index 000000000..584141b83 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/util/id3reader/Id3ReaderTest.java @@ -0,0 +1,151 @@ +package de.danoeh.antennapod.core.util.id3reader; + +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; +import org.apache.commons.io.input.CountingInputStream; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class Id3ReaderTest { + @Test + public void testReadString() throws IOException { + byte[] data = { + ID3Reader.ENCODING_ISO, + 'T', 'e', 's', 't', + 0 // Null-terminated + }; + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + String string = new ID3Reader(inputStream).readEncodingAndString(1000); + assertEquals("Test", string); + } + + @Test + public void testReadMultipleStrings() throws IOException { + byte[] data = { + ID3Reader.ENCODING_ISO, + 'F', 'o', 'o', + 0, // Null-terminated + ID3Reader.ENCODING_ISO, + 'B', 'a', 'r', + 0 // Null-terminated + }; + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + ID3Reader reader = new ID3Reader(inputStream); + assertEquals("Foo", reader.readEncodingAndString(1000)); + assertEquals("Bar", reader.readEncodingAndString(1000)); + } + + @Test + public void testReadingLimit() throws IOException { + byte[] data = { + ID3Reader.ENCODING_ISO, + 'A', 'B', 'C', 'D' + }; + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + ID3Reader reader = new ID3Reader(inputStream); + assertEquals("ABC", reader.readEncodingAndString(4)); // Includes encoding + assertEquals('D', reader.readByte()); + } + + @Test + public void testReadUtf16RespectsBom() throws IOException { + byte[] data = { + ID3Reader.ENCODING_UTF16_WITH_BOM, + (byte) 0xff, (byte) 0xfe, // BOM: Little-endian + 'A', 0, 'B', 0, 'C', 0, + 0, 0, // Null-terminated + ID3Reader.ENCODING_UTF16_WITH_BOM, + (byte) 0xfe, (byte) 0xff, // BOM: Big-endian + 0, 'D', 0, 'E', 0, 'F', + 0, 0, // Null-terminated + }; + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + ID3Reader reader = new ID3Reader(inputStream); + assertEquals("ABC", reader.readEncodingAndString(1000)); + assertEquals("DEF", reader.readEncodingAndString(1000)); + } + + @Test + public void testReadUtf16NullPrefix() throws IOException { + byte[] data = { + ID3Reader.ENCODING_UTF16_WITH_BOM, + (byte) 0xff, (byte) 0xfe, // BOM + 0x00, 0x01, // Latin Capital Letter A with macron (Ā) + 0, 0, // Null-terminated + }; + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + String string = new ID3Reader(inputStream).readEncodingAndString(1000); + assertEquals("Ā", string); + } + + @Test + public void testReadingLimitUtf16() throws IOException { + byte[] data = { + ID3Reader.ENCODING_UTF16_WITHOUT_BOM, + 'A', 0, 'B', 0, 'C', 0, 'D', 0 + }; + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + ID3Reader reader = new ID3Reader(inputStream); + reader.readEncodingAndString(6); // Includes encoding, produces broken string + assertTrue("Should respect limit even if it breaks a symbol", reader.getPosition() <= 6); + } + + @Test + public void testReadTagHeader() throws IOException, ID3ReaderException { + byte[] data = generateId3Header(23); + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + TagHeader header = new ID3Reader(inputStream).readTagHeader(); + assertEquals("ID3", header.getId()); + assertEquals(42, header.getVersion()); + assertEquals(23, header.getSize()); + } + + @Test + public void testReadFrameHeader() throws IOException { + byte[] data = generateFrameHeader("CHAP", 42); + CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data)); + FrameHeader header = new ID3Reader(inputStream).readFrameHeader(); + assertEquals("CHAP", header.getId()); + assertEquals(42, header.getSize()); + } + + public static byte[] generateFrameHeader(String id, int size) { + return concat( + id.getBytes(StandardCharsets.ISO_8859_1), // Frame ID + new byte[] { + (byte) (size >> 24), (byte) (size >> 16), + (byte) (size >> 8), (byte) (size), // Size + 0, 0 // Flags + }); + } + + static byte[] generateId3Header(int size) { + return new byte[] { + 'I', 'D', '3', // Identifier + 0, 42, // Version + 0, // Flags + (byte) (size >> 24), (byte) (size >> 16), + (byte) (size >> 8), (byte) (size), // Size + }; + } + + static byte[] concat(byte[]... arrays) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + for (byte[] array : arrays) { + outputStream.write(array); + } + } catch (IOException e) { + fail(e.getMessage()); + } + return outputStream.toByteArray(); + } +} diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/playback/ExternalMediaTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/playback/ExternalMediaTest.java deleted file mode 100644 index d5e63eeba..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/util/playback/ExternalMediaTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.SharedPreferences; -import androidx.preference.PreferenceManager; - -import androidx.test.platform.app.InstrumentationRegistry; -import de.danoeh.antennapod.core.feed.MediaType; -import org.junit.After; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for {@link ExternalMedia} entity. - */ -@RunWith(RobolectricTestRunner.class) -public class ExternalMediaTest { - - private static final int NOT_SET = -1; - private static final int POSITION = 50; - private static final int LAST_PLAYED_TIME = 1650; - - @After - public void tearDown() { - clearSharedPrefs(); - } - - @SuppressLint("CommitPrefEdits") - private void clearSharedPrefs() { - SharedPreferences prefs = getDefaultSharedPrefs(); - SharedPreferences.Editor editor = prefs.edit(); - editor.clear(); - editor.commit(); - } - - private SharedPreferences getDefaultSharedPrefs() { - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - return PreferenceManager.getDefaultSharedPreferences(context); - } - - @Test - public void testSaveCurrentPositionUpdatesPreferences() { - assertEquals(NOT_SET, getDefaultSharedPrefs().getInt(ExternalMedia.PREF_POSITION, NOT_SET)); - assertEquals(NOT_SET, getDefaultSharedPrefs().getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, NOT_SET)); - - ExternalMedia media = new ExternalMedia("source", MediaType.AUDIO); - media.saveCurrentPosition(getDefaultSharedPrefs(), POSITION, LAST_PLAYED_TIME); - - assertEquals(POSITION, getDefaultSharedPrefs().getInt(ExternalMedia.PREF_POSITION, NOT_SET)); - assertEquals(LAST_PLAYED_TIME, getDefaultSharedPrefs().getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, NOT_SET)); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/playback/TimelineTest.java index ed37b7daa..987a75981 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/playback/TimelineTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/util/playback/TimelineTest.java @@ -1,24 +1,21 @@ -package de.test.antennapod.util.playback; +package de.danoeh.antennapod.core.util.playback; import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.filters.SmallTest; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import de.danoeh.antennapod.core.storage.DBReader; -import java.util.Date; -import java.util.List; - -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.util.playback.Playable; -import de.danoeh.antennapod.core.util.playback.Timeline; +import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -26,95 +23,89 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** - * Test class for timeline. + * Test class for {@link Timeline}. */ -@SmallTest +@RunWith(RobolectricTestRunner.class) public class TimelineTest { private Context context; + MockedStatic<DBReader> dbReaderMock; @Before public void setUp() { context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + // mock DBReader, because Timeline.processShownotes() calls FeedItem.loadShownotes() + // which calls DBReader.loadDescriptionOfFeedItem(), but we don't need the database here + dbReaderMock = Mockito.mockStatic(DBReader.class); } - private Playable newTestPlayable(List<Chapter> chapters, String shownotes, int duration) { - FeedItem item = new FeedItem(0, "Item", "item-id", "http://example.com/item", new Date(), FeedItem.PLAYED, null); - item.setChapters(chapters); - item.setContentEncoded(shownotes); - FeedMedia media = new FeedMedia(item, "http://example.com/episode", 100, "audio/mp3"); - media.setDuration(duration); - item.setMedia(media); - return media; + @After + public void tearDown() { + dbReaderMock.close(); } @Test - public void testProcessShownotesAddTimecodeHHMMSSNoChapters() { + public void testProcessShownotesAddTimecodeHhmmssNoChapters() { final String timeStr = "10:11:12"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11 + 12 * 1000; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode " + timeStr + " here.</p>"; + Timeline t = new Timeline(context, shownotes, Integer.MAX_VALUE); String res = t.processShownotes(); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } @Test - public void testProcessShownotesAddTimecodeHHMMSSMoreThen24HoursNoChapters() { + public void testProcessShownotesAddTimecodeHhmmssMoreThen24HoursNoChapters() { final String timeStr = "25:00:00"; final long time = 25 * 60 * 60 * 1000; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode " + timeStr + " here.</p>"; + Timeline t = new Timeline(context, shownotes, Integer.MAX_VALUE); String res = t.processShownotes(); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } @Test - public void testProcessShownotesAddTimecodeHHMMNoChapters() { + public void testProcessShownotesAddTimecodeHhmmNoChapters() { final String timeStr = "10:11"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode " + timeStr + " here.</p>"; + Timeline t = new Timeline(context, shownotes, Integer.MAX_VALUE); String res = t.processShownotes(); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } @Test - public void testProcessShownotesAddTimecodeMMSSNoChapters() { + public void testProcessShownotesAddTimecodeMmssNoChapters() { final String timeStr = "10:11"; final long time = 10 * 60 * 1000 + 11 * 1000; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", 11 * 60 * 1000); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode " + timeStr + " here.</p>"; + Timeline t = new Timeline(context, shownotes, 11 * 60 * 1000); String res = t.processShownotes(); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } @Test - public void testProcessShownotesAddTimecodeHMMSSNoChapters() { + public void testProcessShownotesAddTimecodeHmmssNoChapters() { final String timeStr = "2:11:12"; final long time = 2 * 60 * 60 * 1000 + 11 * 60 * 1000 + 12 * 1000; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); + + String shownotes = "<p> Some test text with a timecode " + timeStr + " here.</p>"; + Timeline t = new Timeline(context, shownotes, Integer.MAX_VALUE); String res = t.processShownotes(); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } @Test - public void testProcessShownotesAddTimecodeMSSNoChapters() { + public void testProcessShownotesAddTimecodeMssNoChapters() { final String timeStr = "1:12"; final long time = 60 * 1000 + 12 * 1000; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStr + " here.</p>", 2 * 60 * 1000); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode " + timeStr + " here.</p>"; + Timeline t = new Timeline(context, shownotes, 2 * 60 * 1000); String res = t.processShownotes(); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } @@ -123,9 +114,9 @@ public class TimelineTest { public void testProcessShownotesAddNoTimecodeDuration() { final String timeStr = "2:11:12"; final int time = 2 * 60 * 60 * 1000 + 11 * 60 * 1000 + 12 * 1000; - String originalText = "<p> Some test text with a timecode " + timeStr + " here.</p>"; - Playable p = newTestPlayable(null, originalText, time); - Timeline t = new Timeline(context, p); + + String shownotes = "<p> Some test text with a timecode " + timeStr + " here.</p>"; + Timeline t = new Timeline(context, shownotes, time); String res = t.processShownotes(); Document d = Jsoup.parse(res); assertEquals("Should not parse time codes that equal duration", 0, d.body().getElementsByTag("a").size()); @@ -135,12 +126,12 @@ public class TimelineTest { public void testProcessShownotesAddTimecodeMultipleFormatsNoChapters() { final String[] timeStrings = new String[]{ "10:12", "1:10:12" }; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStrings[0] + " here. Hey look another one " + timeStrings[1] + " here!</p>", 2 * 60 * 60 * 1000); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode " + timeStrings[0] + + " here. Hey look another one " + timeStrings[1] + " here!</p>"; + Timeline t = new Timeline(context, shownotes, 2 * 60 * 60 * 1000); String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{ 10 * 60 * 1000 + 12 * 1000, - 60 * 60 * 1000 + 10 * 60 * 1000 + 12 * 1000 }, timeStrings); + checkLinkCorrect(res, new long[]{10 * 60 * 1000 + 12 * 1000, + 60 * 60 * 1000 + 10 * 60 * 1000 + 12 * 1000}, timeStrings); } @Test @@ -149,11 +140,11 @@ public class TimelineTest { // One of these timecodes fits as HH:MM and one does not so both should be parsed as MM:SS. final String[] timeStrings = new String[]{ "10:12", "2:12" }; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode " - + timeStrings[0] + " here. Hey look another one " + timeStrings[1] + " here!</p>", 3 * 60 * 60 * 1000); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode " + timeStrings[0] + + " here. Hey look another one " + timeStrings[1] + " here!</p>"; + Timeline t = new Timeline(context, shownotes, 3 * 60 * 60 * 1000); String res = t.processShownotes(); - checkLinkCorrect(res, new long[]{ 10 * 60 * 1000 + 12 * 1000, 2 * 60 * 1000 + 12 * 1000 }, timeStrings); + checkLinkCorrect(res, new long[]{10 * 60 * 1000 + 12 * 1000, 2 * 60 * 1000 + 12 * 1000}, timeStrings); } @Test @@ -161,9 +152,8 @@ public class TimelineTest { final String timeStr = "10:11"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode (" - + timeStr + ") here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode (" + timeStr + ") here.</p>"; + Timeline t = new Timeline(context, shownotes, Integer.MAX_VALUE); String res = t.processShownotes(); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } @@ -173,9 +163,8 @@ public class TimelineTest { final String timeStr = "10:11"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode [" - + timeStr + "] here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode [" + timeStr + "] here.</p>"; + Timeline t = new Timeline(context, shownotes, Integer.MAX_VALUE); String res = t.processShownotes(); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } @@ -185,9 +174,8 @@ public class TimelineTest { final String timeStr = "10:11"; final long time = 3600 * 1000 * 10 + 60 * 1000 * 11; - Playable p = newTestPlayable(null, "<p> Some test text with a timecode <" - + timeStr + "> here.</p>", Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); + String shownotes = "<p> Some test text with a timecode <" + timeStr + "> here.</p>"; + Timeline t = new Timeline(context, shownotes, Integer.MAX_VALUE); String res = t.processShownotes(); checkLinkCorrect(res, new long[]{time}, new String[]{timeStr}); } @@ -202,8 +190,7 @@ public class TimelineTest { } shownotes.append("here.</p>"); - Playable p = newTestPlayable(null, shownotes.toString(), Integer.MAX_VALUE); - Timeline t = new Timeline(context, p); + Timeline t = new Timeline(context, shownotes.toString(), Integer.MAX_VALUE); String res = t.processShownotes(); checkLinkCorrect(res, new long[0], new String[0]); } diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/syndication/FeedDiscovererTest.java index b213a5efa..3df5230cc 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/FeedDiscovererTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/util/syndication/FeedDiscovererTest.java @@ -1,26 +1,28 @@ -package de.test.antennapod.util.syndication; +package de.danoeh.antennapod.core.util.syndication; import androidx.test.platform.app.InstrumentationRegistry; + import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; import java.io.File; import java.io.FileOutputStream; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Map; -import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** - * Test class for FeedDiscoverer + * Test class for {@link FeedDiscoverer} */ +@RunWith(RobolectricTestRunner.class) public class FeedDiscovererTest { private FeedDiscoverer fd; @@ -28,10 +30,11 @@ public class FeedDiscovererTest { private File testDir; @Before - public void setUp() throws Exception { + public void setUp() { fd = new FeedDiscoverer(); testDir = new File(InstrumentationRegistry .getInstrumentation().getTargetContext().getFilesDir(), "FeedDiscovererTest"); + //noinspection ResultOfMethodCallIgnored testDir.mkdir(); assertTrue(testDir.exists()); } @@ -41,6 +44,7 @@ public class FeedDiscovererTest { FileUtils.deleteDirectory(testDir); } + @SuppressWarnings("SameParameterValue") private String createTestHtmlString(String rel, String type, String href, String title) { return String.format("<html><head><title>Test</title><link rel=\"%s\" type=\"%s\" href=\"%s\" title=\"%s\"></head><body></body></html>", rel, type, href, title); @@ -69,7 +73,7 @@ public class FeedDiscovererTest { } else { File testFile = new File(testDir, "feed"); FileOutputStream out = new FileOutputStream(testFile); - IOUtils.write(html, out, Charset.forName("UTF-8")); + IOUtils.write(html, out, StandardCharsets.UTF_8); out.close(); res = fd.findLinks(testFile, base); } diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReaderTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReaderTest.java new file mode 100644 index 000000000..cf9228292 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReaderTest.java @@ -0,0 +1,44 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import de.danoeh.antennapod.core.feed.Chapter; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class VorbisCommentChapterReaderTest { + + @Test + public void testRealFilesAuphonic() throws IOException, VorbisCommentReaderException { + testRealFileAuphonic("media-parser/auphonic.ogg"); + testRealFileAuphonic("media-parser/auphonic.opus"); + } + + public void testRealFileAuphonic(String filename) throws IOException, VorbisCommentReaderException { + InputStream inputStream = getClass().getClassLoader() + .getResource(filename).openStream(); + VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); + reader.readInputStream(inputStream); + List<Chapter> chapters = reader.getChapters(); + + assertEquals(4, chapters.size()); + + assertEquals(0, chapters.get(0).getStart()); + assertEquals(3000, chapters.get(1).getStart()); + assertEquals(6000, chapters.get(2).getStart()); + assertEquals(9000, chapters.get(3).getStart()); + + assertEquals("Chapter 1 - ❤️😊", chapters.get(0).getTitle()); + assertEquals("Chapter 2 - ßöÄ", chapters.get(1).getTitle()); + assertEquals("Chapter 3 - 爱", chapters.get(2).getTitle()); + assertEquals("Chapter 4", chapters.get(3).getTitle()); + + assertEquals("https://example.com", chapters.get(0).getLink()); + assertEquals("https://example.com", chapters.get(1).getLink()); + assertEquals("https://example.com", chapters.get(2).getLink()); + assertEquals("https://example.com", chapters.get(3).getLink()); + } +} diff --git a/core/src/test/resources/feed-atom-testAtomBasic.xml b/core/src/test/resources/feed-atom-testAtomBasic.xml new file mode 100644 index 000000000..cefc4f979 --- /dev/null +++ b/core/src/test/resources/feed-atom-testAtomBasic.xml @@ -0,0 +1 @@ +<?xml version='1.0' encoding='UTF-8' ?><feed xmlns="http://www.w3.org/2005/Atom"><id>http://example.com/feed</id><title>title</title><link rel="alternate" href="http://example.com" /><subtitle>This is the description</subtitle><logo>http://example.com/picture</logo><link rel="payment" href="http://example.com/payment" type="text/html" /><entry><id>http://example.com/item-0</id><title>item-0</title><link rel="alternate" href="http://example.com/items/0" /><published>1970-01-01T00:00:00Z</published><link rel="enclosure" href="http://example.com/media-0" type="audio/mp3" length="1048576" /></entry><entry><id>http://example.com/item-1</id><title>item-1</title><link rel="alternate" href="http://example.com/items/1" /><published>1970-01-01T00:01:00Z</published><link rel="enclosure" href="http://example.com/media-1" type="audio/mp3" length="1048576" /></entry><entry><id>http://example.com/item-2</id><title>item-2</title><link rel="alternate" href="http://example.com/items/2" /><published>1970-01-01T00:02:00Z</published><link rel="enclosure" href="http://example.com/media-2" type="audio/mp3" length="1048576" /></entry><entry><id>http://example.com/item-3</id><title>item-3</title><link rel="alternate" href="http://example.com/items/3" /><published>1970-01-01T00:03:00Z</published><link rel="enclosure" href="http://example.com/media-3" type="audio/mp3" length="1048576" /></entry><entry><id>http://example.com/item-4</id><title>item-4</title><link rel="alternate" href="http://example.com/items/4" /><published>1970-01-01T00:04:00Z</published><link rel="enclosure" href="http://example.com/media-4" type="audio/mp3" length="1048576" /></entry><entry><id>http://example.com/item-5</id><title>item-5</title><link rel="alternate" href="http://example.com/items/5" /><published>1970-01-01T00:05:00Z</published><link rel="enclosure" href="http://example.com/media-5" type="audio/mp3" length="1048576" /></entry><entry><id>http://example.com/item-6</id><title>item-6</title><link rel="alternate" href="http://example.com/items/6" /><published>1970-01-01T00:06:00Z</published><link rel="enclosure" href="http://example.com/media-6" type="audio/mp3" length="1048576" /></entry><entry><id>http://example.com/item-7</id><title>item-7</title><link rel="alternate" href="http://example.com/items/7" /><published>1970-01-01T00:07:00Z</published><link rel="enclosure" href="http://example.com/media-7" type="audio/mp3" length="1048576" /></entry><entry><id>http://example.com/item-8</id><title>item-8</title><link rel="alternate" href="http://example.com/items/8" /><published>1970-01-01T00:08:00Z</published><link rel="enclosure" href="http://example.com/media-8" type="audio/mp3" length="1048576" /></entry><entry><id>http://example.com/item-9</id><title>item-9</title><link rel="alternate" href="http://example.com/items/9" /><published>1970-01-01T00:09:00Z</published><link rel="enclosure" href="http://example.com/media-9" type="audio/mp3" length="1048576" /></entry></feed>
\ No newline at end of file diff --git a/core/src/test/resources/feed-atom-testLogoWithWhitespace.xml b/core/src/test/resources/feed-atom-testLogoWithWhitespace.xml new file mode 100644 index 000000000..f4886d56a --- /dev/null +++ b/core/src/test/resources/feed-atom-testLogoWithWhitespace.xml @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8' ?><feed xmlns="http://www.w3.org/2005/Atom"><id>http://example.com/feed</id><title>title</title><link rel="alternate" href="http://example.com" /><subtitle>This is the description</subtitle><link rel="payment" href="http://example.com/payment" type="text/html" /><logo> https://example.com/image.png +</logo></feed>
\ No newline at end of file diff --git a/core/src/test/resources/feed-rss-testImageWithWhitespace.xml b/core/src/test/resources/feed-rss-testImageWithWhitespace.xml new file mode 100644 index 000000000..2be9401d2 --- /dev/null +++ b/core/src/test/resources/feed-rss-testImageWithWhitespace.xml @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8' ?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>title</title><description>This is the description</description><link>http://example.com</link><language>en</language><atom:link rel="payment" href="http://example.com/payment" type="text/html" /><image><url> https://example.com/image.png +</url></image></channel></rss>
\ No newline at end of file diff --git a/core/src/test/resources/feed-rss-testMediaContentMime.xml b/core/src/test/resources/feed-rss-testMediaContentMime.xml new file mode 100644 index 000000000..a715abb37 --- /dev/null +++ b/core/src/test/resources/feed-rss-testMediaContentMime.xml @@ -0,0 +1 @@ +<?xml version='1.0' encoding='UTF-8' ?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>title</title><description>This is the description</description><link>http://example.com</link><language>en</language><atom:link rel="payment" href="http://example.com/payment" type="text/html" /><item xmlns:media="http://search.yahoo.com/mrss/"><media:content url="https://www.example.com/file.mp4" medium="video" /></item></channel></rss>
\ No newline at end of file diff --git a/core/src/test/resources/feed-rss-testRss2Basic.xml b/core/src/test/resources/feed-rss-testRss2Basic.xml new file mode 100644 index 000000000..dd771b61a --- /dev/null +++ b/core/src/test/resources/feed-rss-testRss2Basic.xml @@ -0,0 +1 @@ +<?xml version='1.0' encoding='UTF-8' ?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>title</title><description>This is the description</description><link>http://example.com</link><language>en</language><image><url>http://example.com/picture</url></image><atom:link rel="payment" href="http://example.com/payment" type="text/html" /><item><title>item-0</title><link>http://example.com/items/0</link><pubDate>01 Jan 70 01:00:00 +0100</pubDate><guid>http://example.com/item-0</guid><enclosure url="http://example.com/media-0" length="1048576" type="audio/mp3" /></item><item><title>item-1</title><link>http://example.com/items/1</link><pubDate>01 Jan 70 01:01:00 +0100</pubDate><guid>http://example.com/item-1</guid><enclosure url="http://example.com/media-1" length="1048576" type="audio/mp3" /></item><item><title>item-2</title><link>http://example.com/items/2</link><pubDate>01 Jan 70 01:02:00 +0100</pubDate><guid>http://example.com/item-2</guid><enclosure url="http://example.com/media-2" length="1048576" type="audio/mp3" /></item><item><title>item-3</title><link>http://example.com/items/3</link><pubDate>01 Jan 70 01:03:00 +0100</pubDate><guid>http://example.com/item-3</guid><enclosure url="http://example.com/media-3" length="1048576" type="audio/mp3" /></item><item><title>item-4</title><link>http://example.com/items/4</link><pubDate>01 Jan 70 01:04:00 +0100</pubDate><guid>http://example.com/item-4</guid><enclosure url="http://example.com/media-4" length="1048576" type="audio/mp3" /></item><item><title>item-5</title><link>http://example.com/items/5</link><pubDate>01 Jan 70 01:05:00 +0100</pubDate><guid>http://example.com/item-5</guid><enclosure url="http://example.com/media-5" length="1048576" type="audio/mp3" /></item><item><title>item-6</title><link>http://example.com/items/6</link><pubDate>01 Jan 70 01:06:00 +0100</pubDate><guid>http://example.com/item-6</guid><enclosure url="http://example.com/media-6" length="1048576" type="audio/mp3" /></item><item><title>item-7</title><link>http://example.com/items/7</link><pubDate>01 Jan 70 01:07:00 +0100</pubDate><guid>http://example.com/item-7</guid><enclosure url="http://example.com/media-7" length="1048576" type="audio/mp3" /></item><item><title>item-8</title><link>http://example.com/items/8</link><pubDate>01 Jan 70 01:08:00 +0100</pubDate><guid>http://example.com/item-8</guid><enclosure url="http://example.com/media-8" length="1048576" type="audio/mp3" /></item><item><title>item-9</title><link>http://example.com/items/9</link><pubDate>01 Jan 70 01:09:00 +0100</pubDate><guid>http://example.com/item-9</guid><enclosure url="http://example.com/media-9" length="1048576" type="audio/mp3" /></item></channel></rss>
\ No newline at end of file diff --git a/core/src/test/resources/media-parser/auphonic.m4a b/core/src/test/resources/media-parser/auphonic.m4a Binary files differnew file mode 100644 index 000000000..ca59a80f6 --- /dev/null +++ b/core/src/test/resources/media-parser/auphonic.m4a diff --git a/core/src/test/resources/media-parser/auphonic.mp3 b/core/src/test/resources/media-parser/auphonic.mp3 Binary files differnew file mode 100644 index 000000000..ca2a7ed4f --- /dev/null +++ b/core/src/test/resources/media-parser/auphonic.mp3 diff --git a/core/src/test/resources/media-parser/auphonic.ogg b/core/src/test/resources/media-parser/auphonic.ogg Binary files differnew file mode 100644 index 000000000..de326517a --- /dev/null +++ b/core/src/test/resources/media-parser/auphonic.ogg diff --git a/core/src/test/resources/media-parser/auphonic.opus b/core/src/test/resources/media-parser/auphonic.opus Binary files differnew file mode 100644 index 000000000..08538ecb7 --- /dev/null +++ b/core/src/test/resources/media-parser/auphonic.opus diff --git a/core/src/test/resources/media-parser/hindenburg-journalist-pro.m4a b/core/src/test/resources/media-parser/hindenburg-journalist-pro.m4a Binary files differnew file mode 100644 index 000000000..bd64dd9da --- /dev/null +++ b/core/src/test/resources/media-parser/hindenburg-journalist-pro.m4a diff --git a/core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3 b/core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3 Binary files differnew file mode 100644 index 000000000..d341b6045 --- /dev/null +++ b/core/src/test/resources/media-parser/hindenburg-journalist-pro.mp3 diff --git a/core/src/test/resources/media-parser/ultraschall5.mp3 b/core/src/test/resources/media-parser/ultraschall5.mp3 Binary files differnew file mode 100644 index 000000000..a73029a54 --- /dev/null +++ b/core/src/test/resources/media-parser/ultraschall5.mp3 diff --git a/infrastructure.md b/infrastructure.md deleted file mode 100644 index 87a8bacb7..000000000 --- a/infrastructure.md +++ /dev/null @@ -1,65 +0,0 @@ -# AntennaPod infrastructure - -This document describes what services and accounts are in use for AntennaPod. The goal is to make it clear who has which passwords and keys. - -## App distribution -- F-Droid - - Automatic updates from GitHub tags - - F-Droid's signing keys -- Google Play - - Developer account owned by @mfietz - - @ByteHamster has (nearly full) access - - Can not manage permissions - - Upload using Gradle Play Publisher - - API key: @ByteHamster - - AntennaPod signing keys - - @mfietz, @ByteHamster, @danieloeh -- Amazon App Store - - Outdated - - None of the current developers has access - -## Web -- Main website (https://antennapod.org) - - Hosted on GitHub Pages - - Source: https://github.com/AntennaPod/antennapod.github.io - - Maintainer: @Keunes -- Forum (https://forum.antennapod.org) - - Hosted by @ByteHamster (personal root server) - - Powered by [Discourse](https://github.com/discourse/discourse) - - Admin: @ByteHamster - - Moderators: @ByteHamster, @Keunes -- Domain/DNS (`antennapod.org`) - - Managed by @mfietz -- Google Groups - - https://groups.google.com/forum/#!forum/antennapod - - No longer used, replaced with forum (https://forum.antennapod.org) - - Owners: @mfietz, @danieloeh, @ByteHamster, @Keunes -- Wiki - - https://github.com/AntennaPod/AntennaPod/wiki - - Managed on GitHub - - Mostly unmaintained - -## Email -- `@antennapod.org` - - Managed by @ByteHamster (mailbox only for `info@`) - - Used for the required contact address on Google Play - - Auto responder tells users to write on forum or GitHub instead -- `@forum.antennapod.org` - - Managed by @ByteHamster (catch-all mailbox) - - Used by the forum, checked every 5 minutes - - Allows to post+reply via email - -## Social media -- Twitter - - https://twitter.com/antennapod - - Email address of @mfietz - - @ByteHamster and @mfietz have the password - -## Development -- Translations - - https://transifex.com/antennapod/antennapod - - Pulled manually before releasing - - Team managers @mfietz, @ByteHamster -- Source repo - - https://github.com/AntennaPod - - Organization owners: @ByteHamster, @danieloeh, @mfietz, @TomHennen diff --git a/net/README.md b/net/README.md new file mode 100644 index 000000000..4d578407c --- /dev/null +++ b/net/README.md @@ -0,0 +1,3 @@ +# :net + +This folder contains modules that directly interact with the network. diff --git a/net/ssl/README.md b/net/ssl/README.md new file mode 100644 index 000000000..bf01f3ab6 --- /dev/null +++ b/net/ssl/README.md @@ -0,0 +1,3 @@ +# :net:ssl + +This module provides SSL backports and security provider implementations. diff --git a/net/ssl/build.gradle b/net/ssl/build.gradle new file mode 100644 index 000000000..9426b7234 --- /dev/null +++ b/net/ssl/build.gradle @@ -0,0 +1,65 @@ +apply plugin: "com.android.library" + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + + multiDexEnabled false + + testApplicationId "de.danoeh.antennapod.core.tests" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile("proguard-android.txt") + } + debug { + // debug build has method count over 64k single-dex threshold. + // For building debug build to use on Android < 21 (pre-Android 5) devices, + // you need to manually change class + // de.danoeh.antennapod.PodcastApp to extend MultiDexApplication . + // See Issue #2813 + multiDexEnabled true + } + } + + flavorDimensions "market" + productFlavors { + free { + dimension "market" + } + play { + dimension "market" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + + lintOptions { + disable 'GradleDependency' + warningsAsErrors true + abortOnError true + } +} + +dependencies { + annotationProcessor "androidx.annotation:annotation:$annotationVersion" + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" + + playImplementation "com.google.android.gms:play-services-base:$playServicesVersion" + freeImplementation "org.conscrypt:conscrypt-android:$conscryptVersion" +} diff --git a/net/ssl/src/free/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java b/net/ssl/src/free/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java new file mode 100644 index 000000000..48b5690cc --- /dev/null +++ b/net/ssl/src/free/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java @@ -0,0 +1,13 @@ +package de.danoeh.antennapod.net.ssl; + +import android.content.Context; +import org.conscrypt.Conscrypt; + +import java.security.Security; + +public class SslProviderInstaller { + public static void install(Context context) { + // Insert bundled conscrypt as highest security provider (overrides OS version). + Security.insertProviderAt(Conscrypt.newProvider(), 1); + } +} diff --git a/net/ssl/src/main/AndroidManifest.xml b/net/ssl/src/main/AndroidManifest.xml new file mode 100644 index 000000000..2acf91510 --- /dev/null +++ b/net/ssl/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="de.danoeh.antennapod.net.ssl" /> diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportCaCerts.java index 78c105e38..ecfc99e15 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java +++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportCaCerts.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.core.ssl; +package de.danoeh.antennapod.net.ssl; public class BackportCaCerts { public static final String SECTIGO_USER_TRUST = "-----BEGIN CERTIFICATE-----\n" diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportTrustManager.java index 81d2a0709..3a188b47a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java +++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/BackportTrustManager.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.core.ssl; +package de.danoeh.antennapod.net.ssl; import android.util.Log; diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/CompositeX509TrustManager.java index 7af96a492..16b2f0931 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java +++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/CompositeX509TrustManager.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.core.ssl; +package de.danoeh.antennapod.net.ssl; import javax.net.ssl.X509TrustManager; import java.security.cert.CertificateException; diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/NoV1SslSocketFactory.java index 96a42f22d..0e31cda68 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java +++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/NoV1SslSocketFactory.java @@ -1,6 +1,4 @@ -package de.danoeh.antennapod.core.ssl; - -import de.danoeh.antennapod.core.util.Flavors; +package de.danoeh.antennapod.net.ssl; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; @@ -22,7 +20,7 @@ public class NoV1SslSocketFactory extends SSLSocketFactory { try { SSLContext sslContext; - if (Flavors.FLAVOR == Flavors.FREE) { + if (BuildConfig.FLAVOR.equals("free")) { // Free flavor (bundles modern conscrypt): support for TLSv1.3 is guaranteed. sslContext = SSLContext.getInstance("TLSv1.3"); } else { @@ -84,7 +82,7 @@ public class NoV1SslSocketFactory extends SSLSocketFactory { } private void configureSocket(SSLSocket s) { - if (Flavors.FLAVOR == Flavors.FREE) { + if (BuildConfig.FLAVOR.equals("free")) { // Free flavor (bundles modern conscrypt): TLSv1.3 and modern cipher suites are // guaranteed. Protocols older than TLSv1.2 are now deprecated and can be disabled. s.setEnabledProtocols(new String[] { "TLSv1.3", "TLSv1.2" }); diff --git a/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/SslClientSetup.java b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/SslClientSetup.java new file mode 100644 index 000000000..666010d2f --- /dev/null +++ b/net/ssl/src/main/java/de/danoeh/antennapod/net/ssl/SslClientSetup.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.net.ssl; + +import android.os.Build; +import okhttp3.CipherSuite; +import okhttp3.ConnectionSpec; +import okhttp3.OkHttpClient; + +import javax.net.ssl.X509TrustManager; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SslClientSetup { + public static void installCertificates(OkHttpClient.Builder builder) { + if (BuildConfig.FLAVOR.equals("free")) { + // The Free flavor bundles a modern conscrypt (security provider), so CustomSslSocketFactory + // is only used to make sure that modern protocols (TLSv1.3 and TLSv1.2) are enabled and + // that old, deprecated, protocols (like SSLv3, TLSv1.0 and TLSv1.1) are disabled. + X509TrustManager trustManager = BackportTrustManager.create(); + builder.sslSocketFactory(new NoV1SslSocketFactory(trustManager), trustManager); + } else if (Build.VERSION.SDK_INT < 21) { + X509TrustManager trustManager = BackportTrustManager.create(); + builder.sslSocketFactory(new NoV1SslSocketFactory(trustManager), trustManager); + + // workaround for Android 4.x for certain web sites. + // see: https://github.com/square/okhttp/issues/4053#issuecomment-402579554 + List<CipherSuite> cipherSuites = new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites()); + cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA); + cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); + + ConnectionSpec legacyTls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .cipherSuites(cipherSuites.toArray(new CipherSuite[0])) + .build(); + builder.connectionSpecs(Arrays.asList(legacyTls, ConnectionSpec.CLEARTEXT)); + } + } +} diff --git a/net/ssl/src/play/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java b/net/ssl/src/play/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java new file mode 100644 index 000000000..6c89df5ec --- /dev/null +++ b/net/ssl/src/play/java/de/danoeh/antennapod/net/ssl/SslProviderInstaller.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.net.ssl; + +import android.content.Context; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.common.GooglePlayServicesNotAvailableException; +import com.google.android.gms.common.GooglePlayServicesRepairableException; +import com.google.android.gms.security.ProviderInstaller; + +public class SslProviderInstaller { + public static void install(Context context) { + try { + ProviderInstaller.installIfNeeded(context); + } catch (GooglePlayServicesRepairableException e) { + e.printStackTrace(); + GoogleApiAvailability.getInstance().showErrorNotification(context, e.getConnectionStatusCode()); + } catch (GooglePlayServicesNotAvailableException e) { + e.printStackTrace(); + } + } +} diff --git a/settings.gradle b/settings.gradle index bdf2d88b9..a87de1afa 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,5 @@ -include ':app', ':core' +include ':app' +include ':core' +include ':net:ssl' +include ':ui:app-start-intent' +include ':ui:common' diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 000000000..b20f459f1 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,3 @@ +# :ui + +This folder contains modules that display or directly interact with the UI. diff --git a/ui/app-start-intent/README.md b/ui/app-start-intent/README.md new file mode 100644 index 000000000..b796a56cc --- /dev/null +++ b/ui/app-start-intent/README.md @@ -0,0 +1,3 @@ +# :ui:app-start-intent + +This module provides classes that can start the main activities of the app with specific arguments. It does not require a dependency on the actual implementation of the activities, so it can be used to decouple the services from the UI. diff --git a/ui/app-start-intent/build.gradle b/ui/app-start-intent/build.gradle new file mode 100644 index 000000000..144ce72a1 --- /dev/null +++ b/ui/app-start-intent/build.gradle @@ -0,0 +1,51 @@ +apply plugin: "com.android.library" + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + + multiDexEnabled false + + testApplicationId "de.danoeh.antennapod.core.tests" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile("proguard-android.txt") + } + debug { + // debug build has method count over 64k single-dex threshold. + // For building debug build to use on Android < 21 (pre-Android 5) devices, + // you need to manually change class + // de.danoeh.antennapod.PodcastApp to extend MultiDexApplication . + // See Issue #2813 + multiDexEnabled true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + + lintOptions { + disable 'GradleDependency' + warningsAsErrors true + abortOnError true + } +} + +dependencies { + annotationProcessor "androidx.annotation:annotation:$annotationVersion" + implementation "androidx.appcompat:appcompat:$appcompatVersion" +} diff --git a/ui/app-start-intent/src/main/AndroidManifest.xml b/ui/app-start-intent/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9296616f9 --- /dev/null +++ b/ui/app-start-intent/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="de.danoeh.antennapod.ui.appstartintent" /> diff --git a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java new file mode 100644 index 000000000..33f96f141 --- /dev/null +++ b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.ui.appstartintent; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +/** + * Launches the main activity of the app with specific arguments. + * Does not require a dependency on the actual implementation of the activity. + */ +public class MainActivityStarter { + public static final String INTENT = "de.danoeh.antennapod.intents.MAIN_ACTIVITY"; + public static final String EXTRA_OPEN_PLAYER = "open_player"; + + private final Intent intent; + private final Context context; + + public MainActivityStarter(Context context) { + this.context = context; + intent = new Intent(INTENT); + intent.setPackage(context.getPackageName()); + } + + public Intent getIntent() { + return intent; + } + + public PendingIntent getPendingIntent() { + return PendingIntent.getActivity(context, R.id.pending_intent_player_activity, + getIntent(), PendingIntent.FLAG_UPDATE_CURRENT); + } + + public void start() { + context.startActivity(getIntent()); + } + + public MainActivityStarter withOpenPlayer() { + intent.putExtra(EXTRA_OPEN_PLAYER, true); + return this; + } +} diff --git a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/VideoPlayerActivityStarter.java b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/VideoPlayerActivityStarter.java new file mode 100644 index 000000000..7536d34b6 --- /dev/null +++ b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/VideoPlayerActivityStarter.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.ui.appstartintent; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +/** + * Launches the video player activity of the app with specific arguments. + * Does not require a dependency on the actual implementation of the activity. + */ +public class VideoPlayerActivityStarter { + public static final String INTENT = "de.danoeh.antennapod.intents.VIDEO_PLAYER"; + private final Intent intent; + private final Context context; + + public VideoPlayerActivityStarter(Context context) { + this.context = context; + intent = new Intent(INTENT); + intent.setPackage(context.getPackageName()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + } + } + + public Intent getIntent() { + return intent; + } + + public PendingIntent getPendingIntent() { + return PendingIntent.getActivity(context, R.id.pending_intent_video_player, + getIntent(), PendingIntent.FLAG_UPDATE_CURRENT); + } + + public void start() { + context.startActivity(getIntent()); + } +} diff --git a/ui/app-start-intent/src/main/res/values/pending_intent.xml b/ui/app-start-intent/src/main/res/values/pending_intent.xml new file mode 100644 index 000000000..1e426e954 --- /dev/null +++ b/ui/app-start-intent/src/main/res/values/pending_intent.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <item name="pending_intent_download_service_notification" type="id"/> + <item name="pending_intent_download_service_auth" type="id"/> + <item name="pending_intent_download_service_report" type="id"/> + <item name="pending_intent_download_service_autodownload_report" type="id"/> + <item name="pending_intent_allow_stream_always" type="id"/> + <item name="pending_intent_allow_stream_this_time" type="id"/> + <item name="pending_intent_player_activity" type="id"/> + <item name="pending_intent_video_player" type="id"/> + <item name="pending_intent_sync_error" type="id"/> +</resources> diff --git a/ui/common/README.md b/ui/common/README.md new file mode 100644 index 000000000..d96f1cf55 --- /dev/null +++ b/ui/common/README.md @@ -0,0 +1,3 @@ +# :ui:common + +This module provides basic UI functionality that is needed in multiple modules. UI elements that are only used in a single module should not be defined here. diff --git a/ui/common/build.gradle b/ui/common/build.gradle new file mode 100644 index 000000000..144ce72a1 --- /dev/null +++ b/ui/common/build.gradle @@ -0,0 +1,51 @@ +apply plugin: "com.android.library" + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + + multiDexEnabled false + + testApplicationId "de.danoeh.antennapod.core.tests" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile("proguard-android.txt") + } + debug { + // debug build has method count over 64k single-dex threshold. + // For building debug build to use on Android < 21 (pre-Android 5) devices, + // you need to manually change class + // de.danoeh.antennapod.PodcastApp to extend MultiDexApplication . + // See Issue #2813 + multiDexEnabled true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + + lintOptions { + disable 'GradleDependency' + warningsAsErrors true + abortOnError true + } +} + +dependencies { + annotationProcessor "androidx.annotation:annotation:$annotationVersion" + implementation "androidx.appcompat:appcompat:$appcompatVersion" +} diff --git a/ui/common/src/main/AndroidManifest.xml b/ui/common/src/main/AndroidManifest.xml new file mode 100644 index 000000000..bae316f55 --- /dev/null +++ b/ui/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="de.danoeh.antennapod.ui.common" /> diff --git a/app/src/main/java/de/danoeh/antennapod/view/CircularProgressBar.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java index 2fd570ece..a693c28b0 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/CircularProgressBar.java +++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java @@ -1,14 +1,14 @@ -package de.danoeh.antennapod.view; +package de.danoeh.antennapod.ui.common; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import androidx.annotation.Nullable; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.util.ThemeUtils; public class CircularProgressBar extends View { public static final float MINIMUM_PERCENTAGE = 0.005f; @@ -23,20 +23,20 @@ public class CircularProgressBar extends View { public CircularProgressBar(Context context) { super(context); - setup(); + setup(null); } public CircularProgressBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); - setup(); + setup(attrs); } public CircularProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - setup(); + setup(attrs); } - private void setup() { + private void setup(@Nullable AttributeSet attrs) { paintBackground.setAntiAlias(true); paintBackground.setStyle(Paint.Style.STROKE); @@ -44,7 +44,9 @@ public class CircularProgressBar extends View { paintProgress.setStyle(Paint.Style.STROKE); paintProgress.setStrokeCap(Paint.Cap.ROUND); - int color = ThemeUtils.getColorFromAttr(getContext(), R.attr.action_icon_color); + TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircularProgressBar); + int color = typedArray.getColor(R.styleable.CircularProgressBar_foregroundColor, Color.GREEN); + typedArray.recycle(); paintProgress.setColor(color); paintBackground.setColor(color); } diff --git a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedIndicatorView.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PlaybackSpeedIndicatorView.java index d7f1eac1d..c93ca01f5 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedIndicatorView.java +++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PlaybackSpeedIndicatorView.java @@ -1,15 +1,15 @@ -package de.danoeh.antennapod.view; +package de.danoeh.antennapod.ui.common; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import androidx.annotation.Nullable; -import de.danoeh.antennapod.R; public class PlaybackSpeedIndicatorView extends View { private static final float DEG_2_RAD = (float) (Math.PI / 180); @@ -19,37 +19,37 @@ public class PlaybackSpeedIndicatorView extends View { private final Paint arcPaint = new Paint(); private final Paint indicatorPaint = new Paint(); private final Path trianglePath = new Path(); + private final RectF arcBounds = new RectF(); private float angle = VALUE_UNSET; private float targetAngle = VALUE_UNSET; private float degreePerFrame = 2; private float paddingArc = 20; private float paddingIndicator = 10; - private RectF arcBounds = new RectF(); public PlaybackSpeedIndicatorView(Context context) { super(context); - setup(); + setup(null); } public PlaybackSpeedIndicatorView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); - setup(); + setup(attrs); } public PlaybackSpeedIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - setup(); + setup(attrs); } - private void setup() { + private void setup(@Nullable AttributeSet attrs) { setSpeed(1.0f); // Set default angle to 1.0 targetAngle = VALUE_UNSET; // Do not move to first value that is set externally - int[] colorAttrs = new int[] {R.attr.action_icon_color }; - TypedArray a = getContext().obtainStyledAttributes(colorAttrs); - arcPaint.setColor(a.getColor(0, 0xffffffff)); - indicatorPaint.setColor(a.getColor(0, 0xffffffff)); - a.recycle(); + TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.PlaybackSpeedIndicatorView); + int color = typedArray.getColor(R.styleable.PlaybackSpeedIndicatorView_foregroundColor, Color.GREEN); + typedArray.recycle(); + arcPaint.setColor(color); + indicatorPaint.setColor(color); arcPaint.setAntiAlias(true); arcPaint.setStyle(Paint.Style.STROKE); diff --git a/app/src/main/java/de/danoeh/antennapod/view/RecursiveRadioGroup.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java index ee5e7c51d..94ef73ffc 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/RecursiveRadioGroup.java +++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.view; +package de.danoeh.antennapod.ui.common; import android.content.Context; import android.util.AttributeSet; diff --git a/app/src/main/java/de/danoeh/antennapod/view/SquareImageView.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/SquareImageView.java index c256ede9e..dce15af18 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/SquareImageView.java +++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/SquareImageView.java @@ -1,10 +1,9 @@ -package de.danoeh.antennapod.view; +package de.danoeh.antennapod.ui.common; import android.content.Context; import android.content.res.TypedArray; import androidx.appcompat.widget.AppCompatImageView; import android.util.AttributeSet; -import de.danoeh.antennapod.core.R; /** * From http://stackoverflow.com/a/19449488/6839 diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java index 44b31f0be..392d09e07 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java +++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.core.util; +package de.danoeh.antennapod.ui.common; import android.content.Context; import androidx.annotation.AttrRes; diff --git a/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/WrappingGridView.java index 37792b4d1..4c8bb994c 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java +++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/WrappingGridView.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.view; +package de.danoeh.antennapod.ui.common; import android.content.Context; import android.util.AttributeSet; diff --git a/ui/common/src/main/res/values/styleable.xml b/ui/common/src/main/res/values/styleable.xml new file mode 100644 index 000000000..3542cc1b5 --- /dev/null +++ b/ui/common/src/main/res/values/styleable.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <declare-styleable name="SquareImageView"> + <attr name="direction" format="enum"> + <enum name="width" value="0"/> + <enum name="height" value="1"/> + <enum name="minimum" value="2"/> + </attr> + </declare-styleable> + + <declare-styleable name="CircularProgressBar"> + <attr name="foregroundColor" format="color" /> + </declare-styleable> + + <declare-styleable name="PlaybackSpeedIndicatorView"> + <attr name="foregroundColor" /> <!-- format omitted to avoid double definition --> + </declare-styleable> +</resources> |