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
similarity index 100%
rename from .github/PULL_REQUEST_TEMPLATE/default.md
rename to .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...'
+
+
+
+#### 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 |
diff --git a/README.md b/README.md
index c5dd33d7d..8eb13073d 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -87,6 +93,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 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/
@@ -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 allEpisodesMatcher;
- final List 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 chapters = item.getChapters();
- List 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 episodes = DBReader.getRecentlyPublishedEpisodes(0, 10);
+ final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered());
Matcher 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 episodes = DBReader.getRecentlyPublishedEpisodes(0, 10);
+ final List 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 mActivityRule = new IntentsTestRule<>(MainActivity.class, false, false);
+ public IntentsTestRule 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;
@@ -371,6 +372,17 @@ public class PreferencesTest {
.until(() -> enableAutodownloadOnBattery == UserPreferences.isEnableAutodownloadOnBattery());
}
+ @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);
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 @@
+ android:configChanges="keyboardHidden|orientation|screenSize"
+ android:exported="true">
@@ -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">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:label="@string/widget_settings"
+ android:exported="true">
@@ -114,21 +144,21 @@
android:exported="false">
-
+
-
-
-
+
@@ -136,7 +166,8 @@
+ android:label="@string/opml_import_label"
+ android:exported="true">
@@ -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">
-
-
-
-
-
-
-
+
+
@@ -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">
@@ -292,33 +320,26 @@
-
-
-
-
-
-
-
-
-
+
-
+
-
+
@@ -333,6 +354,10 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
+
+
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
@@ -90,12 +90,6 @@
website="https://github.com/square/okio"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
- {
+ request.setUsername(username);
+ request.setPassword(password);
- 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);
+ 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 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() {
+ @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 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;
@@ -360,23 +376,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 authTask = new AsyncTask() {
-
- 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> devices = new AtomicReference<>();
- new AsyncTask>() {
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- chooseDevice.setEnabled(false);
- spinnerDevices.setEnabled(false);
- createNewDevice.setEnabled(false);
- }
-
- @Override
- protected void onPostExecute(List gpodnetDevices) {
- super.onPostExecute(gpodnetDevices);
- if (gpodnetDevices != null) {
- List 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 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() {
-
- 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 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 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 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 {
private Playable media;
@@ -42,7 +42,7 @@ public class ChaptersListAdapter extends RecyclerView.Adapter 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 {
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
}
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
}
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= 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>) subscriber -> {
try {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHostname());
+ GpodnetPreferences.getHosturl());
List gpodnetPodcasts = service.searchPodcasts(query, 0);
List 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 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 loadData() {
- return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE));
+ return DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE, feedItemFilter);
}
@NonNull
@Override
protected List 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 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 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));
@@ -81,6 +86,12 @@ public class DownloadsFragment extends PagedToolbarFragment {
return root;
}
+ @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);
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 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 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 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 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.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 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 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 loadPodcastData(GpodnetService service) throws GpodnetServiceException;
final void loadData() {
- AsyncTask> loaderTask = new AsyncTask>() {
- volatile Exception exception = null;
-
- @Override
- protected List 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;
- }
- }
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ podcasts -> {
+ progressBar.setVisibility(View.GONE);
+ butRetry.setVisibility(View.GONE);
- @Override
- protected void onPostExecute(List 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);
+ 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> loadTask;
-
- private void cancelLoadTask() {
- if (loadTask != null && !loadTask.isCancelled()) {
- loadTask.cancel(true);
+ if (disposable != null) {
+ disposable.dispose();
}
}
private void startLoadTask() {
- cancelLoadTask();
- loadTask = new AsyncTask>() {
- private Exception exception;
-
- @Override
- protected List 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 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 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.
-Importujte, zorganizujte a přehrávejte
+Importujte, organizujte a přehrávejte
• 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ů
-Přidejte se do komunity AntennaPodu!
+Přidejte se ke komunitě AntennaPod!
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
deleted file mode 100644
index 77a6e1c70..000000000
Binary files a/app/src/main/play/listings/en-US/graphics/promo-graphic/promo-graphic.png and /dev/null differ
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
Dołącz do społeczności AntennaPod
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.
+
+Importovať, spravovať a prehrať
+• 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
+
+Sledovať, zdielať a oceniť
+• 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
+
+Spravovať systém
+• 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
+
+Pridať sa do komunity AntennaPod!
+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" />
@@ -68,13 +68,38 @@
android:max="100"
android:progress="100" />
+
+
+
+
+
+
+
+
-
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"/>
+
+
+
+
+
+
-
-
+ android:layout_centerVertical="true"
+ app:foregroundColor="?attr/action_icon_color"/>
-
+ tools:srcCompat="@drawable/ic_playback_speed_white"
+ app:foregroundColor="?attr/action_icon_color"/>
-
-
-
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="16dp">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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"/>
+
+
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 @@
-
-
+
+
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"/>
-
-
+
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 @@
-
-
-
-
-
\ 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 @@
-
-
-
+ android:orientation="vertical">
+
+
+
+
+
+
+
+ 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" />
-
+
-
+
-
+
-
+
-
+
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
+ android:orientation="vertical">
-
+
-
+
-
-
-
+
+ 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"/>
+
+
+
+
+ android:visibility="gone" />
-
-
-
-
-
-
-
\ No newline at end of file
+
\ 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 @@
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
-
-
-
-
-
\ No newline at end of file
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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">
-
-
-
-
+ android:layout_height="40dp"
+ app:foregroundColor="?attr/action_icon_color"/>
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">
-
@@ -75,6 +76,33 @@
android:layout_gravity="bottom|center"
android:orientation="vertical">
+
+
+
+
+
+
-
+ android:id="@+id/share_parent"
+ custom:showAsAction="ifRoom"
+ android:title="@string/share_label_with_ellipses"
+ android:icon="?attr/ic_share"
+ android:visible="true">
+
+
-
-
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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"/>
+
+
-
-
\ No newline at end of file
+
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 @@
-
-
+
-
-
+ android:summary="@string/pref_gpodnet_authenticate_sum"/>
-
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 @@
-
-
+
+
+
+
+
+
-
-
-
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" />
+
+
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.
- *
- * 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 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 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 final String[] properties;
- 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;
+ 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 filter(List items) {
- if(mProperties.length == 0) return items;
-
- List 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.
@@ -384,40 +390,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 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 loadChapters(Context context) {
- List chaptersFromDatabase = null;
- if (item.hasChapters()) {
- chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(item);
- }
-
- List 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) {
@@ -477,6 +449,18 @@ public class FeedMedia extends FeedFile implements Playable {
return item.getPaymentLink();
}
+ @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 chapters) {
- if(item != null) {
+ if (item != null) {
item.setChapters(chapters);
}
}
- @Override
- public Callable loadShownotes() {
- return () -> {
- if (item == null) {
- item = DBReader.getFeedItem(itemID);
- }
- return item.loadShownotes().call();
- };
- }
-
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
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
}
+
+ @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 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 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 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);
@@ -122,6 +111,31 @@ public class LocalFeedUpdater {
DBTasks.updateFeed(context, feed, removeUnlistedItems);
}
+ /**
+ * 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.
*/
@@ -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
@@ -264,6 +268,23 @@ public class UserPreferences {
return prefs.getBoolean(PREF_USE_EPISODE_COVER, true);
}
+ /**
+ * @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.
*
@@ -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 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 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());
+ 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 (httpNotFound || forbidden || notEnoughSpace || wrongFileType || httpGone || httpBadReq ) {
+ 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 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 {
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);
@@ -199,6 +210,11 @@ public class ExoPlayerWrapper implements IPlayer {
mediaSource = f.createMediaSource(Uri.parse(s));
}
+ @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,17 +201,28 @@ 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.");
}
}
+ /**
+ * 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.
@@ -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 candidates;
- final List queue = DBReader.getQueue();
- final List 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 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 candidates;
+ final List queue = DBReader.getQueue();
+ final List 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 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 getFeedItemList(final Feed feed) {
+ return getFeedItemList(feed, FeedItemFilter.unfiltered());
+ }
+
+ public static List 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 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 getRecentlyPublishedEpisodes(int offset, int limit) {
- Log.d(TAG, "getRecentlyPublishedEpisodes() called with: " + "offset = [" + offset + "]" + " limit = [" + limit + "]" );
+ public static List 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 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 true 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 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 feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters);
Comparator 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 feeds = DBReader.getFeedList();
+ ListIterator 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);
@@ -130,38 +146,6 @@ public final class DBTasks {
// See Issue #2577 for the details of the rationale
}
- /**
- * @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 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
*
@@ -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(
@@ -205,19 +189,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
*
@@ -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());
- }
- f.setId(feed.getId());
+ private static void refreshFeeds(Context context, List feeds, boolean loadAllPages,
+ boolean force, boolean initiatedByUser) throws DownloadRequestException {
+ List localFeeds = new ArrayList<>();
+ List normalFeeds = new ArrayList<>();
- if (f.isLocalFeed()) {
- new Thread(() -> LocalFeedUpdater.updateFeed(f, context)).start();
- } else {
- DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force, initiatedByUser);
+ for (Feed feed : feeds) {
+ if (feed.isLocalFeed()) {
+ localFeeds.add(feed);
+ } else {
+ normalFeeds.add(feed);
+ }
+ }
+
+ 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 feeds = DBReader.getFeedList();
for (Feed f : feeds) {
@@ -534,7 +517,7 @@ public final class DBTasks {
List 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
@@ -311,6 +311,14 @@ class DBUpgrader {
+ " ADD COLUMN " + PodDBAdapter.KEY_FEED_SKIP_ENDING + " INTEGER DEFAULT 0;");
}
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 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 feeds, boolean loadAllPages,
+ boolean force, boolean initiatedByUser) throws DownloadRequestException {
+ List 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 candidates = getCandidates();
+ List 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 getCandidates() {
+ List candidates = new ArrayList<>();
+ List 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
* robolectric/robolectric#1890.
*/
+ @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 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 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 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 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);
@@ -225,6 +265,45 @@ public class GpodnetService implements ISyncService {
}
}
+ /**
+ * Returns synchronization status of devices.
+ *
+ * This method requires authentication.
+ *
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public List> 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> result = new ArrayList<>();
+
+ JSONArray synchronizedDevices = syncStatus.getJSONArray("synchronized");
+ for (int i = 0; i < synchronizedDevices.length(); i++) {
+ JSONArray groupDevices = synchronizedDevices.getJSONArray(i);
+ List 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.
*
@@ -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();
@@ -261,6 +340,39 @@ public class GpodnetService implements ISyncService {
}
}
+ /**
+ * Links devices for synchronization.
+ *
+ * This method requires authentication.
+ *
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public void linkDevices(@NonNull List 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.
*
@@ -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 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 loadChaptersFromStreamUrl(Playable media, Context context) {
- List 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 loadChaptersFromFileUrl(Playable media) {
- if (!media.localFileAvailable()) {
- Log.e(TAG, "Could not load chapters from file url: local file not available");
- return null;
- }
- List chapters = ChapterUtils.readID3ChaptersFromPlayableFileUrl(media);
- if (chapters == null) {
- chapters = ChapterUtils.readOggChaptersFromPlayableFileUrl(media);
- }
- return chapters;
- }
-
- /**
- * Uses the download URL of a media object of a feeditem to read its ID3
- * chapters.
- */
- private static List 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 chaptersFromDatabase = null;
+ if (playable instanceof FeedMedia) {
+ FeedMedia feedMedia = (FeedMedia) playable;
+ if (feedMedia.getItem() == null) {
+ feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId()));
}
- List chapters = readChaptersFrom(in);
+ if (feedMedia.getItem().hasChapters()) {
+ chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem());
+ }
+ }
+
+ List chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context);
+ List chapters = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
+ if (chapters == null) {
+ // Do not try loading again. There are no chapters.
+ playable.setChapters(Collections.emptyList());
+ } else {
+ playable.setChapters(chapters);
+ }
+ }
+
+ public static List loadChaptersFromMediaFile(Playable playable, Context context) {
+ try (CountingInputStream in = openStream(playable, context)) {
+ List 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 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;
- }
-
- CountingInputStream in = null;
- try {
- in = new CountingInputStream(new BufferedInputStream(new FileInputStream(source)));
- List chapters = readChaptersFrom(in);
- if (!chapters.isEmpty()) {
- return chapters;
- }
- Log.i(TAG, "Chapters loaded");
} catch (IOException | ID3ReaderException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- } finally {
- IOUtils.closeQuietly(in);
+ Log.e(TAG, "Unable to load ID3 chapters: " + e.getMessage());
+ }
+
+ try (CountingInputStream in = openStream(playable, context)) {
+ List chapters = readOggChaptersFromInputStream(in);
+ if (!chapters.isEmpty()) {
+ Log.i(TAG, "Chapters loaded");
+ return chapters;
+ }
+ } 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 readChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
- ChapterReader reader = new ChapterReader();
- reader.readInputStream(in);
+ private static List readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
+ ChapterReader reader = new ChapterReader(in);
+ reader.readInputStream();
List 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 readOggChaptersFromPlayableStreamUrl(Playable media, Context context) {
- if (media == null || !media.streamAvailable()) {
- return null;
+ @NonNull
+ private static List readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException {
+ VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
+ reader.readInputStream(input);
+ List chapters = reader.getChapters();
+ if (chapters == null) {
+ return Collections.emptyList();
}
- 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);
+ Collections.sort(chapters, new ChapterStartTimeComparator());
+ enumerateEmptyChapterTitles(chapters);
+ if (chaptersValid(chapters)) {
+ return chapters;
}
- return null;
- }
-
- private static List 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);
- }
- }
- return null;
- }
-
- private static List 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 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();
- }
- 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);
+ 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;
- private final int code;
- private final int resId;
+ DownloadError(int code, int resId) {
+ this.code = code;
+ this.resId = 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);
+ }
- /** 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);
- }
+ /** 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.
- *
- *
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 {
- /**
- * 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 Type of the non-existent value
- * @return an empty {@code Optional}
- */
- public static Optional empty() {
- @SuppressWarnings("unchecked")
- Optional t = (Optional) 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 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 Optional 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 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 Optional 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:
- *
- *
it is also an {@code Optional} and;
- *
both instances have no value present or;
- *
the present values are "equal to" each other via {@code equals()}.
- *
- *
- * @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 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));
}
}
@@ -110,6 +112,15 @@ public class NotificationUtils {
return notificationChannel;
}
+ @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,
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 chapters;
- private ID3Chapter currentChapter;
+ private final List 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 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);
-
- readerPosition += number;
+ byte readByte() throws IOException {
+ return (byte) inputStream.read();
}
- 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;
+ short readShort() throws IOException {
+ char firstByte = (char) inputStream.read();
+ char secondByte = (char) inputStream.read();
+ return (short) ((firstByte << 8) | secondByte);
+ }
+
+ 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;
+ }
+
+ void expectChar(char expected) throws ID3ReaderException, IOException {
+ char read = (char) inputStream.read();
+ if (read != expected) {
+ throw new ID3ReaderException("Expected " + expected + " and got " + read);
}
}
- private FrameHeader createFrameHeader(char[] source)
- throws ID3ReaderException {
- if (source.length != HEADER_LENGTH) {
- throw new ID3ReaderException("Length of header must be "
- + HEADER_LENGTH);
- }
- String id = new String(source, 0, FRAME_ID_LENGTH);
+ @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);
+ }
- int size = (((int) source[4]) << 24) | (((int) source[5]) << 16)
- | (((int) source[6]) << 8) | source[7];
+ @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;
+ return Charset.forName("ISO-8859-1").newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
}
- 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) {
+ protected String readIsoStringNullTerminated(int max) throws IOException {
+ return readEncodedString(ENCODING_ISO, max);
+ }
+
+ @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);
+ }
+ }
+
+ /**
+ * 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;
- } else if (c == 0) {
- if (cZero == 0) {
- // termination character found
- break;
- } else {
- cZero = 0;
- }
- } else {
- buffer[i] = (byte) c;
- cZero = -1;
}
+ bytes.write(c);
}
- if (strBuffer != null) {
- strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString());
+ return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
+ }
+
+ /**
+ * 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 i;
+ return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
}
-
- int onStartTagHeader(TagHeader header) {
- return ACTION_SKIP;
- }
-
- int onStartFrameHeader(FrameHeader header, CountingInputStream input) throws IOException, ID3ReaderException {
- return ACTION_SKIP;
- }
-
- void onEndTag() {
-
- }
-
- void onNoTagHeaderFound() {
-
- }
-
}
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;
+import androidx.annotation.NonNull;
+
public class FrameHeader extends Header {
+ private final short flags;
- private final char flags;
+ public FrameHeader(String id, int size, short flags) {
+ super(id, size);
+ this.flags = flags;
+ }
- public FrameHeader(String id, int size, char 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;
+import androidx.annotation.NonNull;
+
public class TagHeader extends Header {
+ private final short version;
+ private final byte flags;
- private final char version;
- private final byte flags;
+ public TagHeader(String id, int size, short version, byte flags) {
+ super(id, size);
+ this.version = version;
+ this.flags = flags;
+ }
- 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 char getVersion() {
- return version;
- }
-
-
+ @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 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 loadShownotes() {
- return () -> "";
- }
-
- @Override
- public List 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 chapters) {
- this.chapters = chapters;
- }
-
- public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
- 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 androidx.annotation.Nullable;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.MediaType;
+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
@@ -38,13 +32,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
*/
@@ -67,6 +54,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.
@@ -89,6 +81,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 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(){}
+ @Nullable
+ String getImageLocation();
- 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);
- }
-
- }
}
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();
+ if (!PlaybackService.isRunning) {
+ throw new IllegalStateException("Trying to bind but service is not running");
}
- 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 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();
- }
-
- 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;
/**
@@ -128,11 +126,6 @@ public class RemoteMedia implements Playable {
//Already loaded
}
- @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 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(" ");
- 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
new file mode 100644
index 000000000..28a8446e4
Binary files /dev/null and b/core/src/main/res/drawable-hdpi/ic_notification_new.png differ
diff --git a/core/src/main/res/drawable-mdpi/ic_notification_new.png b/core/src/main/res/drawable-mdpi/ic_notification_new.png
new file mode 100644
index 000000000..02530f5e4
Binary files /dev/null and b/core/src/main/res/drawable-mdpi/ic_notification_new.png differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_notification_new.png b/core/src/main/res/drawable-xhdpi/ic_notification_new.png
new file mode 100644
index 000000000..49c696798
Binary files /dev/null and b/core/src/main/res/drawable-xhdpi/ic_notification_new.png differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_notification_new.png b/core/src/main/res/drawable-xxhdpi/ic_notification_new.png
new file mode 100644
index 000000000..ec6ef4f1e
Binary files /dev/null and b/core/src/main/res/drawable-xxhdpi/ic_notification_new.png differ
diff --git a/core/src/main/res/drawable-xxxhdpi/ic_notification_new.png b/core/src/main/res/drawable-xxxhdpi/ic_notification_new.png
new file mode 100644
index 000000000..66f968872
Binary files /dev/null and b/core/src/main/res/drawable-xxxhdpi/ic_notification_new.png differ
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 @@
-
-
-
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 @@
+
+
+
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 @@
+
+
+
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 @@
@@ -78,9 +79,61 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
+ android:textSize="14sp"
android:visibility="gone" />
+
+
+
+
+
+
+
+
+
+
+
-
\ 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 @@
+
+
+
+ تحديث الاشتراكات
+ بودكاستات
+ إحصائيات
+ إضافة بودكاست
+ حلقات
+ لائحة الاستماع
+ الكل
+ جديد
+ المفضلات
+ جديد
+ إعدادات
+ تنزيلات
+ جارى التشغيل
+ منتهى
+ سجل
+ إشتراكات
+ لائحة الإشتراكات
+ سجل التشغيل
+ gpodder.net
+ تسجيل الدخول لموقع gpodder
+ ذاكرة تخزين الحلقات ممتلئة
+ لقد تم تجاوز الحد الأقصى لتخزين الحلقات. المرجو الرفع من قيمة التخزين في قائمة الإعدادات.
+ تشغيل
+ تنزيلات
+ إشعارات
+
+
+ الوقت الكلي للحلقات المشغلة:
+ %1$d حلقة من %2$d بدأ.\n\nتشغيلها %3$s من مجموع %4$s.
+ نمط الإحصاءات
+ أحسب وقت التشغيل الفعلي. التشغيل مرتين يحسب مرتين, بينما التحديد بـ مشغلة لا يحتسب
+ أحسب كل الحلقات المحددة بـ مشغلة
+ ملاحظة: سرعة التشغيل لن تأخذ بالاعتبار.
+ صفر الاحصاءات
+ هذا سيمسح سجل وقت التشغيل لكل الحلقات. هل أنت متأكد؟
+ منذ %s,\nقمت بتشغيلها
+
+ الحجم الكلي للحلقات على الجهاز:
+
+ قائمة الفتح
+ قائمة الاغلاف
+ تفضيلات الدرج
+ رتب بعدد الحلقات
+ رتب أبجديا
+ رتب بتاريخ النشر
+ رتب بعدد الحلقات المشغلة
+ عدد الحلقات الجديدة وغير المشغلة
+ عدد الحلقات الجديدة
+ عدد الحلقات غير المشغلة
+ عدد الحلقات المنزلة
+ بدون
+
+ لا يوجد برنامج متوافق
+
+ افتح فى المتصفح
+ انسخ الرابط
+ مشاركة الرابط
+ تم نسخ الرابط لذاكرة القصاصات
+ اذهب لهذا التوقيت
+
+ مسح السجل
+
+ تأكيد
+ الغاء
+ نعم
+ لا
+ إعادة التعيين
+ الناشر(ون)
+ لغة
+ عنوان الموقع
+ صورة
+ خطأ
+ حدث خطأ:
+ هذه العلملية تحتاج السماح للوصول لمساحة التخزين
+ تحديث
+ لا توجد ذاكرة خارجية متاحة. فضلا تأكد من إتاحة الذاكرة الخارجية للتطبيق حتى يعمل جيدا.
+ فصول
+ مدة: %1$s
+ وصف
+ \u0020حلقات
+ جارى المعالجة
+ اغلاق
+ اعادة المحاولة
+ تضمين فى التنزيل التلقائي
+ طبق هذا على الحلقات الماضية
+ إعدادات التنزيل التلقائي الجديدة ستطبق على الحلقات الجديدة.\nهل تريد أيضا تطبيقها على الحلقات الماضية؟
+ مسح الحلقات تلقائيا
+ تقليل شدة الصوت
+ قلل شدة الصوت لحلقات هذه القناة: %1$s
+ بدون
+ خفيف
+ شديد
+ اعدادات افتراضية شاملة
+ دائما
+ ابدا
+ ارسال ...
+ ابدا
+ إذا ليس في لائحة الاستماع
+ بعد الانتهاء
+
+ %d ساعة بعد الأنتهاء
+ 1 ساعة بعد الأنتهاء
+ %d ساعتان بعد الأنتهاء
+ %d ساعة بعد الأنتهاء
+ %d ساعة بعد الأنتهاء
+ %d ساعة بعد الأنتهاء
+
+
+ %d يوم بعد الأنتهاء
+ 1 يوم بعد الأنتهاء
+ %d يومان بعد الأنتهاء
+ %d يوم بعد الأنتهاء
+ %d يوم بعد الأنتهاء
+ %d يوم بعد الأنتهاء
+
+
+ %d مختار
+ %d مختار
+ %d مختار
+ %d مختار
+ %d مختار
+ %d مختار
+
+ تحميل المزيد...
+
+ تعليم الكل بـ تم تشغيله
+ علم كل الحلقات بـ تم تشغيلها
+ نرجو تأكيد تعليم كل الحلقات بـ تم تشغيلها.
+ نرجو تأكيد تعليم كل الحلقات في هذه القناة بـ تم تشغيلها.
+ ازل كل العلامات الجديدة
+ أزيلت كل العلامات الجديدة
+ نرجو تأكيد إزالة كل العلامات الجديدة من كل الحلقات.
+ اظهر معلومات
+ أظهر اعدادات البودكاست
+ معلومات البودكاست
+ إعدادات البودكاست
+ تغيير اسم البودكاست
+ ازل البودكاست
+ مشاركة
+ مشاركة...
+ مشاركة ملف
+ عنوان موقع
+ عنوان URL قناة البودكاست
+ نرجو تأكيد رغبتك مسح بودكاست \"%1$s\" وكل حلقاته (بما فيها المنزلة).
+ نرجو تأكيد رغبتك إزالة بودكاست \"%1$s\". الملفات المنزلة لن يتم مسحها.
+ أزيل البودكاست
+ حدث كامل البودكاست
+ اختيار متعدد
+ اختار الكل أعلاه
+ اختار الكل أدناه
+ لم يتم تشغيله
+ ضمن لائحة الاستماع
+ ليس ضمن لائحة الاستماع
+ فيها وسائط
+ مصفى
+ افتح البودكاست
+ نرجو الانتظار حتى يتم تحميل البيانات
+
+ تنزيل
+
+ تنزيل %d حلقة.
+ تنزيل %d حلقة.
+ تنزيل %d حلقتان.
+ تنزيل %d حلقات.
+ تنزيل %d حلقات.
+ تنزيل %d حلقات.
+
+ تشغيل
+ ايقاف مؤقت
+ مباشر
+ مسح
+ لم نتمكن من مسح الملف. إعادة تشغيل الجهاز قد تساعد.
+ مسح الحلقة
+ أزل العلامات الجديدة
+ أزيلت العلامات الجديدة
+ علمها كـ مشغلة
+ تم تعليمها كـ مشغلة
+ علمها كـ مقروءة
+ علمت كـ مقروءة
+ للإنتقال للتوقيتات, يجب أن تشغل الحلقة
+
+ %d حلقة علمت كـ مقروءة.
+ %d حلقة علمت كـ مقروءة.
+ %d حلقتان علمتا كـ مقروءة.
+ %d حلقات علمت كـ مقروءة.
+ %d حلقات علمت كـ مقروءة.
+ %d حلقات علمت كـ مقروءة.
+
+ تعليمه ك لم يتم تشغيله
+ علمها كـ غير مقروءة
+
+ %d حلقة علمت كـ غير مقروءة.
+ %d حلقة علمت كـ غير مقروءة.
+ %d حلقتان علمتا كـ غير مقروءة.
+ %d حلقات علمت كـ غير مقروءة.
+ %d حلقات علمت كـ غير مقروءة.
+ %d حلقات علمت كـ غير مقروءة.
+
+ اضف للائحة الاستماع
+ أضيفت للائحة الاستماع
+
+ %d حلقة أضيفت الى لائحة الاستماع.
+ %d حلقة أضيفت الى لائحة الاستماع.
+ %d حلقتان أضيفتا الى لائحة الاستماع.
+ %d حلقات أضيفت الى لائحة الاستماع.
+ %d حلقات أضيفت الى لائحة الاستماع.
+ %d حلقات أضيفت الى لائحة الاستماع.
+
+ أزل من لائحة الاستماع
+
+ %d حلقة أزيلت من لائحة الاستماع.
+ %d حلقة أزيلت من لائحة الاستماع.
+ %d حلقتان أزيلتا من لائحة الاستماع.
+ %d حلقات أزيلت من لائحة الاستماع.
+ %d حلقات أزيلت من لائحة الاستماع.
+ %d حلقات أزيلت من لائحة الاستماع.
+
+ اضافة للمفضلات
+ تم اضافته للمفضلات
+ المسح من المفضلات
+ تم مسحه من المفضلات
+ زيارة الموقع
+ تخطى الحلقة
+ تفعيل التنزيل التلقائي
+ ايقاف التنزيل التلقائي
+ صفر موضع التشغيل
+ تم حزف العنصر
+ لم يتم اختيار أي عنصر
+
+ نجحت العملية
+ التنزيل فى الانتظار
+ جارى التنزيل
+ تفاصيل
+ %1$s \n\nURL الملف:\n%2$s
+ جهاز التخزين غير موجود
+ خطاء فى بيانات HTTP
+ خطاء غير معروف
+ نمط قناة غير مدعوم
+ خطاء فى الاتصال
+ خطاء فى التحقق
+ خطأ في نوع الملف
+ ألغي التنزيل
+ ألغي التنزيل\nمعطل التنزيل التلقائي لهذه المادة
+ أنتهى التنزيل مع خطأ (او أكثر)
+ التنزيل التلقائي أنتهى
+ تقرير التنزيل
+ غطأ في تنسيق URL
+ خطأ إدخال/إخراج
+ خطأ طلب
+ خطأ وصول لقاعدة البيانات
+
+ %d تنزيل بقي
+ %d تنزيل بقي
+ %d تنزيلان بقيا
+ %d تنزيلات بقت
+ %d تنزيلات بقت
+ %d تنزيلات بقت
+
+ تنزيل بيانات البودكاست
+ عنوان غير معروف
+ قناة
+ ملف وسائط
+ حدث خطأ عند محاولة تنزيل ملف:\u0020
+ لم نعطى بودكاست يمكن عرضه.
+ التحقق مطلوب
+ المورد الذي طلبته يتطلب اسم مستخدم وكلمة مرور
+ أكد التنزيل على بيانات الجوال
+ التنزيل على بيانات الجوال معطل في الإعدادات.\n\nبإمكانك اضافة الحلقة للائحة الإستماع أو السماح بالتنزيل مؤقتا.\n\nسنتذكر هذا الخيار لمدة 10 دقائق.
+ التنزيل على بيانات الجوال معطل في الاعدادات.\n\nهل تريد السماح بالتنزيل مؤقتا؟\n\nخيارك سيتم تذكره لمدة 10 دقائق.
+ أكد البث على بيانات الجوال
+ البث على بيانات الجوال معطل في الاعدادات. اضغط للبث رغم ذلك.
+ دائما
+ مرة
+ Enqueue
+ السماح مؤقتا
+
+ خطأ!
+ لا وسائط تشغل
+ تجهيز
+ جاهز
+ جارى القصد
+ تعطل الخادم
+ نمط وسائط غير مدعوم
+ أنتهى وقت العملية
+ لا يمكن الوصول لملف الوسائط
+ خطاء غير معروف
+ لا وسائط تشغل
+ تخزين مؤقت
+ نمط صورة-في-صورة
+ AntennaPod - مفتاح وسائط غير معروف: %1$d
+ ملف غير موجود
+
+ قفل لائحة الإستماع
+ فتح لائحة الإستماع
+ لائحة الإستماع مقفلة
+ لائحة الإستماع مفتوحة
+ إذا اقفلت لائحة الإستماع, لا يمكنت تبديل أو ترتيب الحلقات فيها.
+ لا تظهرها مرة ثانية
+ صففي لائحة الإستماع
+ تراجع
+ ارفعه للأعلى
+ أنزله للأدني
+ رتب
+ أبقها مرتبة
+ تاريخ
+ طول
+ عنوان الحلقة
+ عنوان البودكاست
+ عشوائي
+ خلط ذكي
+ تصاعدي
+ تنازلي
+ نرجو تأكيد تصفية لائحة الإستماع من كل الحلقات فيها
+ الوقت المتبقى:\u0020
+
+ تنزيل الإضافة
+ الإضافة غير منصبة
+ للتشغيل متغير السرعات نقترح تفعيل مشغل وسائط سونيك المضمن.
+ تفعيل سونيك
+ إعدادات أولية
+ %1$.2fx محفوظة كإعداد أولي.
+
+ لا حلقات بلائحة الاستماع
+ أضف حلقة بإختيار تنزيل, أو أضغط مطولا على الحلقة وأختر \"أضف الى لائحة الاستماع\".
+ الحلقة ليس مكتوب بها أي تفاصيل
+ لا يوجد تنزيل جاري
+ يمكنك تنزيل حلقات من صفحة تفاصيل البودكاست.
+ لا توجد حلقات منزلة
+ يمكنك تنزيل حلقات من صفحة تفاصيل البودكاست.
+ لا سجل للتنزيلات
+ ستظهر سجلات التنزيل هنا عند توفرها.
+ لا يوجد سجل
+
+ ستظهر هنا الحلقة بعد الاستماع إليها.
+ لا توجد حلقات
+ ستظهر هنا الحلقة بعد إضافتها.
+ لا توجد حلقات جديدة
+ ستظهر هنا اللحلقات التى وصلت حديثا.
+ لا توجد حلقات مفضلة
+ يمكنك إضافة حلقات للمفضلات بالضغط عليها لمدة طويلة.
+ لا توجد فصول
+ هذة الحلقة لا تتضمن فصول
+ لا توجد إشتراكات
+ للاشتراك في بودكاست ، اضغط على أيقونة علامة الجمع أدناه.
+
+ تخزين
+ الحذف التلقائي والاستيراد والتصدير للحلقة
+ المشروع
+ مزامنة
+ قم بالمزامنة مع الأجهزة الأخرى باستخدام gpodder.net
+ التشغيل الآلي
+ تفاصيل
+ استيراد/تصدير
+ لنسخ الاحتياطي و استرجاع
+ المظهر
+ العوامل الخارجية
+
+
+ المقاطعات
+ تحكم التشغيل
+ بحث
+ لا توجد نتائج
+ مسح السجل
+ مشغل وسائط
+ مسح تلقائى
+ توقيف التشغيل عند نزع سماعات الأذن او البلوتوث
+ عاود التشغيل عند ايصال سماعات الأذن او البلوتوث
+ عاود التشغيل عند إيصال سماعات البلوتوث
+ أذهب الى الحلقة التالية في لائحة الاستماع عندما ينتهي استماع السابقة.
+ أمسح الحلقة عندما ينتهي تشغيلها
+ مسح تلقائي
+ علم على الحلقات على أنها إنتهت حتى لو بقي أقل من مقدار ثوانٍ معين من وقت التشغيل
+ علم بذكاء أنها انتهت
+ تعليم الحلقة كمفضلة يبقيها على الجهاز
+ الاحتفاظ بالحلقات التي تم تخطيها
+ تعليم الحلقة كمفضلة يبقيها على الجهاز
+ إبقاء الحلقات المفضلة
+ تشغيل
+ تحكم سماعة الأذن, وقت التقدم, لائحة الاستماع
+ شبكة
+ الفاصل الزمني للتحديث، التحكم بالتنزيل ،شبكة الجوال
+ الفاصل الزمنى ووقت التحديث
+ حدد فاصل زمنى أو وقت محدد لتحديث البودكاستات تلقائيا
+ تعطيل
+ تعيين الفاصل الزمني
+ ضبط الوقت من اليوم
+ تشغيل مستمر
+ قطع اتصال سماعات الرأس أو البلوتوث
+ إعادة توصيل سماعات الرأس
+ إعادة توصيل البلوتوث
+ أفضل البث
+ عرض زر البث بدلاً من زر التنزيل في القوائم.
+ التحديث على شبكة الجوال
+ حدد ما يجب السماح به أثناء الاتصال على شبكة الجوال
+ تحديث البودكاست
+ صور الغلاف
+ تنزيل تلقائي
+ تحميل الحلقة
+ بث
+ واجهة الاستخدام
+ المظهر, الإشتراكات, شاشة الغلق
+ اختيار النمط
+ حدد عناصر قائمة البرنامج
+ قم بتغيير العناصر التي تظهر في قائمة البرنامج.
+ حدد ترتيب الإشتراكات
+ غير ترتيب إشتراكاتك
+ حدد عداد الاشتراكات
+ غير المعلومات المعروضة بعداد الإشتراكات. إيضا يغير ترتيب الإشتراكات إذا كان ترتيبها يحدد بالـ\'العداد\'
+ تغيير مظهر AntennaPod.
+ تنزيل تلقائي
+ حدد معطيات التحميل التلقائي للحلقات.
+ تمكين اختيار شبكة الWi-Fi المستخدمة
+ أسمح بالتنزيل التلقائي فقط على شبكات الواي فاي المختارة.
+ التنزيل عند عدم الشحن الجهاز
+ السماح بالتنزيل التلقائي عندما لا يتم شحن البطارية
+ التنزيلات المتوازية
+ تخزين الحلقات
+ العدد الإجمالي للحلقات التي تم تنزيلها والمخزنة مؤقتًا على الجهاز. سيتم تعليق التنزيل التلقائي إذا تم الوصول إلى هذا الرقم.
+ استخدم صورة غلاف الحلقة
+ استخدم نمط نظام التشغيل
+ فاتح
+ داكن
+ اسود (ِلأجهزة AMOLED)
+ غير محدود
+ ساعات
+ ساعة
+ يدوى
+ تسجيل الدخول
+ سجل دخول بحساب gpodder.net لتتزامن إشتراكاتك
+ تسجيل خروج
+ نجح تسجيل الخروج
+ تغيير بيانات تسجيل الدخول
+ قم بتغيير بيانات تسجيل الدخول لحساب gpodder.net الخاص بك.
+ قم بعملية التزامن الآن
+ زامن الإشتراكات وتغير حالة الحلقات مع gpodder.net
+ ابدأ مزامنة كاملة
+ زامن كل الإشتراكات وحالة الحلقات مع gpodder.net
+ %1$s بجهاز %2$s]]>
+ حدد السرعات المتوفرة في التشغيل متعدد السرعات
+ سرعة التشغيل الصوتي عند بدء تشغيل حلقات من هذا البودكاست
+ تخطي تلقائي
+ تخطى المقدمة والمؤخرة
+ تخطى الآخر
+ تخطى الأول
+ تخطى آخر %d ثانية
+ تخطى أول %d ثانية
+ عدل بيانات الوسائط مع سرعة التشغيل
+ الوقت المنقضي والمتبقي يتناسب مع سرع التشغيل
+ وقت التخطي السريع
+ قم بتخصيص عدد الثواني للانتقال إلى الأمام عند النقر فوق زر التقديم السريع
+ وقت التخطى للخلف
+ قم بتخصيص عدد الثواني للانتقال للخلف عند النقر فوق زر الإرجاع
+ أولوية عالية للإشعار
+ هذه عادة توسع الإشعار ليظهر أزرار التشغيل.
+ تحكم التشغيل ظاهر دوما
+ أبقي تحكم التشغيل في الإشعارات وشاشة القفل عند التوقف.
+ تعيين أزرار الإشعار المضغوط
+ غير أزرار التشغيل عند إزالة الإشعار. زري تشغيل/إيقاف سيكونا دوما ظاهران.
+ تعيين خلفية شاشة القفل
+ اضبط خلفية شاشة القفل على صورة الحلقة الحالية. كأثر جانبي ، سيعرض هذا أيضًا الصورة في تطبيقات الطرف الثالث.
+ إصدارات Android قبل 4.1 لا تدعم الإشعارات الموسعة.
+ مكان الـ Enqueue
+ خلف
+ أمام
+ بعد الحلقة الحالية
+ معطل
+ حجم ذاكرة التخزين المؤقت للصور
+ حجم ذاكرة التخزين المؤقت على القرص للصور.
+ منتدى المستخدم
+ بلغ عن خطأ بالتطبيق
+ إفتح نظام تتبع الأخطاء
+ تصدير السجلات
+ إنسخ لذاكرة القصاصات
+ تم النسخ لذاكرة القصاصات
+ تجريبي
+ حدد مشغل الوسائط الذي تريد استخدامه لتشغيل الملفات
+ خادم بروكسى
+ حدد خادم وكيل
+ لم يتم العثور على متصفح ويب.
+ دعم Chromecast
+ فعل تحكم التشغيل بأجهزة الكاست (مثل كرومكاست, سماعات الصوت, وتلفزيون اندرويد)
+ يتطلب Chromecast مكتبات برمجية مملوكة لجهات خارجية معطلة في هذا الإصدار من AntennaPod
+ الـ Enqueue المنزلة
+ أضف الحلقات المنزلة الى لائحة الاستماع
+ ExoPlayer (موصى به)
+ قم بالتبديل إلى ExoPlayer
+ تم بالتبديل إلى ExoPlayer
+ تخطي الصمت في الصوت
+ عند الخروج من الفيديو
+ التصرف عند ترك تشغيل فيديو
+ اوقف التشغيل
+ استمر في تشغيل الصوت
+ المسح يزيل الحلقة من لائحة الاستماع
+ أزل الحلقة من لائحة الاستماع آليا عند مسحها.
+ مصفاة الإشتراكات
+ صفي إشتراكاتك في درج الملاحة وشاشة الإشتراكات.
+ الإشتراكات مصففاة
+
+
+
+ تتزامن الإشتراكات الآن...
+ نجحت المزامنة
+ فشلت المزامنة
+
+ نقل الاشتراكات ولائحة الاستماع الى جهاز آخر
+ عرض إشتراكاتك لصديق
+ نقل إشتراكاتك لبرنامج بوكاست آخر
+ إستيراد اشتراكاتك من برنامج آخر
+ نقل الاشتراكات والحلقات المسموعة ولائحة الإستماع لـ AntennaPod على جهاز آخر
+ لم يتم اختيار أي ملف
+ اختر الكل
+ ألغ اختيار الكل
+ تصدير بصيغة OPML
+ تصدير بصيغة HTML
+ تصدير قاعدة البيانات
+ استيراد قاعدة البيانات
+ استيراد قاعدة بيانات سيمسح كل اشتراكاتك الحالية وسجل الاستماع. الأفضل أن تصدر قاعدة البيانات الحالية لأرشيف. هل تريد تبديل البيانات؟
+ حدث خطأ أثناء التصدير
+ تم التصدير بنجاح
+ الوصول الى مساحة التخزين الخارجية مطلوب لقراءة ملف الـ OPML
+ تصدير الحلقات المفضلة
+ تصدير الحلقات المفضلة لملف
+
+ ثواني
+ دقائق
+ ساعات
+
+ %d ثانية
+ 1 ثانية
+ %d ثانيتان
+ %d ثواني
+ %d ثواني
+ %d ثواني
+
+
+ %d دقيقة
+ 1 دقيقة
+ %d دقيقتان
+ %d دقائق
+ %d دقائق
+ %d دقائق
+
+
+ %d ساعة
+ 1 ساعة
+ %d ساعتان
+ %d ساعة
+ %d ساعة
+ %d ساعة
+
+
+ الفئات
+ أقوى البودكاستات
+ إقتراحات
+ تسجيل الدخول
+ تسجيل الدخول
+ إسم المستخدم
+ كلمة المرور
+ إختر
+ تهانينا! حسابك في gpodder.net مربوط الآن مغ جهازك. سيزامن AntennaPod من الآن وصاعدا إشتراكاتك على جهازك مع حسابك على gpodder.net.
+ ابدأ المزامنة الآن
+ إذهب إلى الصفحة الرئيسية
+ خطأ في إسم المستخدم أو كلمة المرور
+ تم بنجاح
+
+ إختيار المستند:
+ أنشأ مستندا جديدا
+ إختيار مستند البيانات
+ الوصول الى مساحة التخزين الخارجية مطلوب لتغيير مجلد البيانات
+ توقف التشغيل بدل اخفات الصوت عندما برنامج يشغل صوت
+ واصل التشغيل عندما تنتهى مكالمة هاتفية
+
+
+ التخطى للخلف
+ التقدم السريع
+ صوت
+ فيديو
+ الحلقة موجودة في لائحة الاستماع
+ الحلقة علمت كمفضلة
+ تحميل الصفحة التالية
+
+ تسجيل الدخول
+ إعدادات التنزيل التلقائي
+ التنزيل التلقائي معطل في الإعدادات الرئيسية لـ AntennaPod
+
+
+
+
+
+ الكل
+ إختيار كل الحلقات
+ لم يتم تشغيله
+ تم التنزيل
+ لم يتم التنزيل
+ حلقات مختارة ضمن لائحة الاستماع
+ حلقات مختارة ليست ضمن لائحة الاستماع
+ في المفضلة
+ ليست مفضلة
+ تم التنزيل
+ لم يتم التنزيل
+ ضمن لائحة الاستماع
+ ليست ضمن لائحة الاستماع
+ ايقاف مؤقت
+
+
+
+ موضع التشغيل
+
+ سرعة التشغيل
+ مستوى الصوت
+ يسار
+ يمين
+ تأثيرات صوتية
+ تحويل: ثنائي القناة الى أحادي
+ سونيك فقط
+ اكسوبلير فقط
+
+ النوع
+ خادم
+ رقم المنفذ
+ (أختياري)
+ اختبار
+ تفحص...
+
+
+ فشل بدء تشغيل وسائط
+ فشل ايقاف تشغيل وسائط
+ فشل توقف تشغيل وسائط
+
+ تحميل
+ يشغل حاليا
+ تسمح بتحكم التشغيل. هذا الاشعار الرئيسي الذي تراه عند تشغيل البودكاست.
+ أظهر عندما تفشل مزامنة gpodder.
+
+ إعدادات أداة الشاشة
+
+
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 @@
StadegoùOuzhpennañ ur podskignadRannoù
+ LostAn hollNevezSinedoù
@@ -17,7 +18,6 @@
KerzhlevrKoumanantoùRoll ar c\'houmanantoù
- Nullañ ar pellgargadennoùRoll istor seniñgpodder.netTitouroù kennaskañ gpodder.net
@@ -25,6 +25,7 @@
Tizhet eo bet bevenn pellgargadurioù ar rannoù. Gallout a rit kreskaat ment ar c\'hrubuilh en arventennoù.LennPellgargadurioù
+
%1$d war %2$d rann kroget.\n\nLennet %3$s war %4$s.Doareoù stadegoù
@@ -75,7 +76,6 @@
Deskrivadur\u0020rannoùO keweriañ
- Enrollañ ho titouroù kennaskañSerriñKlask en-droPellgargañ emgefreek
@@ -87,7 +87,6 @@
hini ebetizelpounner
- \u0020pellgargadurioù kensturDre ziouerBepredMorse
@@ -143,7 +142,6 @@
N\'emañ ket el lostGant ur mediaSilet
- {fa-exclamation-circle} C\'hwitet war an azgrenaat diwezhañDigeriñ ar podskignadGortozit dibenn pellgargadur ar roadennoù
@@ -219,16 +217,12 @@
Munudoù%1$s \n\nURL ar restr:\n%2$sN\'eo ket bet kavet ar c\'hadaviñ
- N\'eus ket trawalc\'h a blasFazi roadennoù HTTPFazi dianav
- Kemennadenn FaziDoare lanv anskorFazi kennaskañ
- Ostiz dianavFazi dilesaFazi rizh ar restr
- DifennetPellgargadur nulletPellgargadur nullet\nDiweredekaet ar pellgargadur emgefreek evit an elfenn-mañPellgargañ echuet gant fazi(où)
@@ -245,7 +239,6 @@
%d a bellgargadurioù a chom%d pellgargadur a chom
- O keweriañ ar pellgargadennoùO pellgargañ roadennoù ar podskignadTitl dianavLanv
@@ -331,7 +324,6 @@
KadaviñDilemel emgefreek ar rannoù, Enporzhiañ, EzporzhiañRaktres
- LostGoubredañOber gant gpodder.net evit goubredañ gant binviji allEmgefreekañ
@@ -347,14 +339,9 @@
Skarzhañ ar roll istorLenner liesvediaNaetaat ar rannoù
- 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.Paouez gant al lenn pa vez diluget selaouelloù pe bluetoothKenderc\'hel al lenn pa vez adluget ar selaouelloùAdstagañ gant al lenn pa vez adkennasket ar bluetooth
- An afell \'war-raok\' a dremen ar rann
- 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
- An afell \'lamm a-dreñv\' a adloc\'h ar rann
- Adloc\'hañ adalek ar penn-kentañ pa vez pouezet war un afell \'lamm a-dreñv\' kentoc\'h eget mont war-gilTremen d\'ar rann goude ur wech echuet gant unanDilemel ar rann p\'eo echuet gant al lennDilemel ent emgefreek
@@ -374,7 +361,6 @@
DiweredekaatDibab un etremezDibab un eur
- bep %1$sda %1$sLenn kendalc\'husLugañ ar selaouelloù
@@ -407,7 +393,6 @@
Niver a rannoù enrolletNiver hollek a rannoù pellgarget lakaet e krubuilh ar benveg. Diweredekaet e vo ar pellgargañ emgefreek mard eo tizhet an niver-mañ.Skeudenn ar rannoù
- Ober gant golo ar rann pa vez dioutañ. Mard eo digevasket e vo graet gant golo ar podskignad.Neuz ar reizhiadSklaerTeñval
@@ -427,7 +412,6 @@
Rediañ ur goubredañ klokGoubredañ an holl goumanantoù ha stadoù ar rannoù gant gpodder.net.%1$s gant ar benveg %2$s]]>
- An arventenn-mañ na vez ket arloet d\'ar rebuzadurioù fazi.Tizh dre ziouer ar rannoùLamm emgefreekTremen penn-kentañ ha dibenn ar rannoù
@@ -441,8 +425,6 @@
Personelaat an niver a eilennoù da lammat war-raok pa vez pouezet war an afell war-raok.Padelezh al lamm a-dreñvDibabit ar c\'hementad a eilennoù da lammat an a-dreñv pa vez pouezet war \'lamm a-dreñv\'
- Dibab un anv ostiz
- Ober gant an ostiz dre ziouerTevet rebuzadurioù a live uhelSañset e brasa ar rebuzadur evit diskouez an afelloù lenn.Afelloù lenn peurzalc\'hus
@@ -451,8 +433,6 @@
Gallout a rit dibab %1$d elfenn d\'ar muiañ.Kemmañ skramm-prennañ an drekleurLakaat skeudenn ar rann e plas skeudenn drekleur ar skramm-prennañ. Diskouez a raio ivez ar skeudenn en arloadoù all.
- Ma c\'hwit ar pellgargadurioù, sevel un danevell a ziskouez munudoù ar c\'hwitadenn.
- Diskouez ur rebuzadur evit ar rannoù pellgarget ent emgefreek.Handelvoù Android a-raok 4.1 na skoront ket ar rebuzadurioù astennet.Lec\'hiadur ar rannoù el lostOuzhpennañ ar rannoù da: %1$s
@@ -473,14 +453,12 @@
Talvoud bremanel: %1$sProksiArventennañ ur rouedad proksi
- Foar ar GoulennoùN\'eus bet kavet merdeer ebet.Skor ChromecastGweredekaat al lenn a-bell war ar binviji Cast (evel ChromeCast, Audio Speaker pe Android TV)Chromecast a c\'houlenn levraouegoù diavaez a zo diweredekaet en handelv-mañ eus AntennaPodOuzhpennañ el lost ur wech pellgargetOuzhpennañ ar rannoù pellgarget el lost
- Lenner genidik AndroidOber gant ExoPlayerKemmet eo bet al lenner evit ExoPlayerTremen ar mareoù didrouz en aodio
@@ -592,22 +570,12 @@
ALIOÙKlask war gpodder.nerKennaskañ
- Donemat war an hentenn kennaskañ gpodder.net. Da gentañ penn, biziatait ho titouroù kennaskañ:Kennaskañ
- Ma n\'ho peus ket a gont c\'hoazh e c\'hallit krouiñ unan amañ:\nhttps://gpodder.net/register/Anv arveriadGer-tremen
- Dibab ar benvegKrouiñ ur benveg nevez evit ho kont gpodder.net pe dibabit ur benveg a zo anezhañ:
- Naoudi ar benveg:\u0020
- Alc\'hwez
- Krouiñ ur benveg nevez
- Dibab ur benveg a zo anezhañ:
- Ret eo deoc\'h leuniañ naoudi ar benveg
- Implijet eo an naoudi-se endeoAn anv n\'hall ket bezañ goulloDibab
- Kennasket gant berzh!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.Kregiñ gant ar goubredañ bremañ Mont d\'ar skramm degemer
@@ -775,8 +743,6 @@
Diskouezet eo pa vez o pellgargañ.O lennEvit reoliañ al lenn. Ar rebuzadur pennañ an hini eo pa lennit ur podskignat.
- Fazioù
- Pellgargañ emgefreekDiskouezet eo pa vez pellgarget rannoù ent emgefreekGwellvezioù ar widjet
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 @@
EstadístiquesAfegeix podcastEpisodis
+ CuaTotNouPreferits
@@ -17,7 +18,6 @@
RegistreSubscripcionsLlista de subscripcions
- Cancel·la\nBaixadaHistorial de reproducciógpodder.netInici de sessió a gpodder.net
@@ -25,6 +25,7 @@
S\'ha arribat al límit de la memòria cau d\'episodis. Pots incrementar-ne la capacitat a la configuració.ReproduccióBaixades
+
%1$d de %2$depisodis començats.\n\nReproduïts %3$s de %4$s.Mode d\'estadístiques
@@ -75,7 +76,6 @@
Descripció\u0020episodisS\'està processant
- Desa nom d\'usuari i contrasenyaTancaReintentaInclou a baixades automàtiques
@@ -87,7 +87,6 @@
OffLleugerFort
- \u0020baixades en paral·lelValor predeterminat globalSempreMai
@@ -134,7 +133,6 @@
No en cuaConté medisFiltrat
- {fa-exclamation-circle} Darrera actualització fallidaObrir podcastPer favor, espera fins a que les dades estiguen carregades
@@ -195,16 +193,12 @@
Detalls%1$s \n\nURL del fitxer:\n%2$sNo s\'ha trobat cap dispositiu d\'emmagatzemament
- No hi ha prou espaiError de dades HTTPError desconegut
- Error de l\'analitzadorTipus de canal no suportatError de connexió
- Amfitrió desconegutError d\'autenticacióError de tipus de fitxer
- ProhibitS\'ha cancel·lat la baixadaBaixada cancel·lada\nDesactivada les baixades automàtiques per aquest elementBaixades completades amb error(s)
@@ -218,7 +212,6 @@
%d baixada pendent%d baixades pendents
- S\'estan processant les baixadesS\'estan baixant les dades del podcastTítol desconegutCanal
@@ -304,7 +297,6 @@
EmmagatzematgeAuto-esborrat d\'episodis, Importar, ExportarProjecte
- CuaSincronitzacióSincronitza amb altres dispositius usant gpodder.netAutomatització
@@ -320,14 +312,9 @@
Esborra l\'historialReproductor multimèdiaNeteja l\'episodi
- 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 episodisPausa la reproducció en desconnectar els auriculars o el bluetoothContinua la reproducció en connectar novament els auricularsContinua la reproducció en connectar novament el bluetooth
- Endavant per saltar
- En prémer el botó d\'avançada en un dispositiu bluetooth bota al següent episodi en lloc d\'avançar.
- Endarrere per reiniciar
- En prémer un botó físic, reinicieu l\'episodi actual en lloc de rebobinar-loSalta al següent element de la cua en acabar la reproduccióSuprimeix l\'episodi quan s\'acabi de reproduirEsborrat automàtic
@@ -347,7 +334,6 @@
DesactivarEstablir intervalEstablir hora del dia
- cada %1$sals %1$sReproducció continuadaConnexió d\'auriculars
@@ -380,7 +366,6 @@
Memòria cau d\'episodisNombre total d\'episodis baixats al dispositiu. La baixada automàtica serà suspesa si s\'arriba a aquest nombre.Usa la coberta de l\'episodi
- 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.Usa el tema del sistemaClarFosc
@@ -400,7 +385,6 @@
Força sincronització completaSincronitza amb gpodder.net totes les subscripcions i els estats dels episodis.%1$s amb el dispositiu %2$s]]>
- Aquest paràmetre no s\'aplica als errors d\'autenticació. La velocitat a usar quan comence la reproducció per a episodis en aquest podcastAuto OmetreOmet introduccions i crèdits finals
@@ -414,8 +398,6 @@
Personalitzar el nombre de segons del salt endavant quan es prem el botó d\'Avanç ràpid.Temps de salt del RebobinatPersonalitza el nombre de segons del salt endarrere quan es prem el botó de Rebobinat
- Definex nom del servidor
- Utilitza el servidor per defecteAlta prioritat de notificacionsAçò normalment expandeix la notificació per a mostrar botons de reproduccióBotons de reproducció persistents
@@ -424,8 +406,6 @@
Només pots seleccionar un màxim de %1$d elements.Estableix el fons del bloqueig de pantallaEstableix 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.
- Si les descàrregues fallen, genera un informe que mostra els detalls de la fallada.
- Mostra una notificació per a episodis descarregats automàticamentLes versions d\'Android anteriors a la 4.1 no suporten les notificacions ampliades.Posició d\'entrada en colaAfegit episodis a: %1$s
@@ -446,14 +426,12 @@
Valor actual: %1$sServidor intermediariEstableix un servidor intermediari
- Preguntes FreqüentsNo s\'ha trobat cap navegador web.Suport per a ChromecastHabilita el suport per la reproducció remota en dispositius de difusió (com ara Chromecast, Audio Speakers o Android TV). Chromecast requereix de llibreries propietàries de terceres parts que estan inhabilitades en aquesta versió d\'AntennaPodAfegeix les baixades a la cuaAfegeix els episodis descarregats a la cua
- Reproductor Android estàndardCanvia a ExoPlayerCanviat a ExoPlayerOmet Silenci en Audio
@@ -556,22 +534,12 @@
SUGGERÈNCIESCerca a gpodder.netInici de sessió
- Benvingut al procés d\'inici de sessió a gpodder.net. Primerament, introduïu la informació d\'accés:Entra
- Si no teniu compte, podeu crear-ne un aquí:\nhttps://gpodder.net/register/Nom d\'usuariContrasenya
- Selecció de dispositiuPer a utilitzar gpodder.net, creeu un nou dispositiu o seleccioneu-ne un d\'existent:
- ID de dispositiu:\u0020
- Llegenda
- Crea nou dispositiu
- Seleccioneu un dispositiu existent:
- L\'ID de dispositiu no pot ser buit
- L\'ID de dispositiu ja existeixEl títol no pot estar buit Selecciona
- Heu iniciat la sessió!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.Sincronitza araVés a la pantalla principal
@@ -739,8 +707,6 @@
Mostrar durant baixades.Reproducció actualPermet controlar la reproducció. Aquesta és la notificació principal que veureu durant la reproducció d\'un podcast.
- Errors
- Baixades automàtiquesMostrat quan els episodis han sigut descarregats automàticamentSettings del widget
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 @@
- Aktualizovat sbírky
+ Aktualizovat odběryPodcastyStatistikyPřidat podcastEpizody
+ FrontaVšeNovýOblíbené
@@ -15,8 +16,8 @@
Právě běžíDokončenoLog
- Sbírky
- Seznam sbírek
+ Odběry
+ Seznam odběrůZrušit stahováníHistorie přehrávánígpodder.net
@@ -26,6 +27,8 @@
PřehráváníStaženéUpozornění
+
+ \"%1$s\" nenalezenCelkový čas přehraných epizod:%1$d z %2$d započatých epizod.\n\nPřehraných %3$s z %4$s.
@@ -53,6 +56,8 @@
ŽádnéNenalezena kompatibilní aplikace
+ Exportovat detailní záznamy
+ Detailní záznamy mohou obsahovat citlivé informace jako seznam odběrůOtevřít v prohlížečiKopírovat URL
@@ -82,7 +87,6 @@
Popis\u0020epizodZpracovávám
- Uložit uživatelské jméno a hesloZavřítZkusit znovuZahrnout do automaticky stahovaných
@@ -94,12 +98,13 @@
VypnutoNízkéVysoké
- \u0020paralelních stahování
+ %1$d souběžných stahováníGlobální nastaveníVždyNikdyOdeslatNikdy
+ Pokud není mezi oblíbenýmiPokud není ve frontěPo dokončení
@@ -121,6 +126,7 @@
%d vybránoNačítají se další…
+ Upozornění na epizodyOznačit vše jako poslechnutéVšechny epizody označeny jako poslechnuté
@@ -152,7 +158,6 @@
Mimo frontuObsahuje médiaFiltrované
- {fa-exclamation-circle} Poslední aktualizace selhalaOtevřít podcastPočkejte prosím na dokončení načítání
@@ -223,16 +228,12 @@
Detaily%1$s \n\nURL souboru:\n%2$sÚložné zařízení nenalezeno
- Nedostatek volného místaHTTP chybaNeznámá chyba
- Výjimka parseruNepodporovaný typ kanáluChyba spojení
- Neznámý hostChyba přihlášeníChyba typu souboru
- ZákázánoStahování zrušenoStahování zrušeno\nVypnuto automatické stahování této položkyStahování dokončeno s chybou
@@ -248,14 +249,7 @@
%d čekajících na stažení%d čekajících na stažení
- Probíhá stahováníStahuji podcast data
-
- %d úspěšné stažení, %d selhalo
- %d úspěšná stažení, %d selhala
- %d úspěšných stažení, %d selhalo
- %d úspěšných stažení, %d selhalo
- Neznámý názevKanálSoubor
@@ -338,13 +332,12 @@
Epizody si můžete přidat mezi oblíbené dlouhým dotykem.Žádné kapitolyTato epizoda nemá žádné kapitoly.
- Žádné sbírky
- Pro přidání podcastu do sbírky se dotkněte ikonky plus níže.
+ Žádné odběry
+ Pro přihlášení odběru podcastu stiskněte ikonu plus níže.ÚložištěAutomatické mazání epizod, Import, ExportProjekt
- FrontaSynchronizaceSynchronizace s dalšími zařízeními pomocí služby gpodder.netAutomatizace
@@ -360,14 +353,17 @@
Vymazat historiiPřehrávač médiíVyčistit epizody
- 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é epizodyPři odpojení sluchátek nebo bluetooth připojení pozastavit přehrávání.Pokračovat v přehrávání po připojení sluchátekPokračovat v přehrávání po připojení bluetooth
- Tlačítko rychle vpřed přeskakuje
- 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.
- Tlačítko zpět restartuje
- 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
+ Tlačítno vpřed
+ Přizpůsobit tlačítko vpřed
+ Tlačítko předchozí
+ Přizpůsobit tlačítko předchozí
+ Posunout vpřed
+ Posunout zpět
+ Přeskočit epizodu
+ Znovu spustit epizoduPo přehrání položky z fronty přejít automaticky na dalšíSmazat díl po jeho přehráníAutomatické mazání
@@ -387,7 +383,6 @@
VypnoutNastavit intervalNastavit čas v průběhu dne
- každých %1$sv %1$sKontinuální přehráváníSluchátka nebo Bluetooth odpojeno
@@ -407,10 +402,10 @@
Vybrat motivZměnit navigační panelUpravit zobrazení položek v navigačním panelu.
- Nastavit pořadí sbírek
- Upravit pořadí vašich sbírek
- Nastavit čítač sbírek
- Změnit informaci zobrazenou čítačem sbírek. Též ovlivňuje řazení, je-li nastaveno na „podle čítače“.
+ Nastavit pořadí odběrů
+ Upravit pořadí vašich odběrů
+ Nastavit čítač odběrů
+ Změnit informaci zobrazenou čítačem odběrů. Též ovlivňuje řazení, je-li nastaveno na „podle čítače“.Změnit vzhled AntennaPod.Automatické stahováníNastavení automatického stahování epizod.
@@ -418,11 +413,12 @@
Povolit automatické stahování pouze pomocí vybraných Wi-Fi sítí.Stahovat, pokud neprobíhá nabíjeníPovolit automatické stahování i pokud není baterie nabíjena
- Paralelní stahování
+ Souběžné stahováníHistorie epizodCelkový počet epizod stažených na zařízení. Automatické stahování se zastaví při dosažení této hodnoty.Použít obrázek epizody
- 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.
+ Zobrazit zbývající čas
+ Pokud je vybráno, zobrazí se zbývající čas epizody. Jinak se zobrazí celková délka epizody.Použít systémové témaSvětlýTmavý
@@ -442,8 +438,6 @@
Synchronizovat vše ihnedSynchronizovat všechny odběry a stav epizod s gpodder.net.%1$s z přístroje %2$s]]>
- Synchronizace selhala
- Toto nastavení se netýká chyb přihlášení.Upravit předvybrané možnosti pro přehrávání zvuku různými rychlostmiRychlost, která bude použita při zahájení přehrávání epizod tohoto podcastuAutomatické přeskočení
@@ -458,8 +452,6 @@
Upravit o kolik sekund se přeskočí dopředu při stisku tlačítka rychle vpřed (FF).Délka času posunu zpětUpravit o kolik sekund se přeskočí zpět při stisku tlačítka přetočit zpět (RW).
- Nastavit hostname
- Použít přednastaveného hostaVysoká priorita pro oznámeníToto obvykle přidá tlačítka ovládání přehrávání do zpráv upozorněníPevné ovládání přehrávání
@@ -470,10 +462,6 @@
Lze vybrat maximálně %1$d položek.Nastavit pozadí uzamčené obrazovkyNastavit 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.
- Stahování selhalo
- Pokud selže stahování, vygenerovat report zobrazující detaily o chybě.
- Automatické stahování dokončeno
- Zobrazovat oznámení o automaticky stažených epizodách.Verze Androidu nižší než 4.1 nepodporují rozšířená oznámení.Pozice přidávání do frontyPřidávat epizody na: %1$s
@@ -494,14 +482,14 @@
Aktuální hodnota: %1$sProxyNastavit síťovou proxy
- Často kladené otázkyWebový prohlížeč nenalezen.Chromecast podporaPovolit podporu vzdáleného přehrávání médií na Cast přístrojích (jako třeba Chromecast, Audio Speakers nebo Android TV)Chromecast vyžaduje proprietární knihovny třetích stran, které jsou vypnuty v této verzi AntennaPodZařadit staženéPřidat stažené epizody do fronty
- Vestavěný přehrávač Androidu
+ Vestavěný přehrávač Androidu (zastaralé)
+ Přehrávač médií Sonic (zastaralé)ExoPlayer (doporučen)Přepnout na ExoPlayerPřepnuto na ExoPlayer
@@ -523,10 +511,10 @@
Vybrat stránkuMazání také odstraňuje epizody z frontyAutomaticky odstraní epizodu z fronty poté, co je smazána.
- Filtr sbírek
- Filtrujte svoje sbírky v navigačním panelu a na obrazovce odebíraných kanálů.
+ Filtr odběrů
+ Filtrujte svoje odběry v navigačním panelu a na obrazovce odběrů.Žádné
- Odebírané sbírky jsou filtrovány.
+ Odběry jsou filtrovány.Počet vyšší než nulaAutomaticky stahovánoNebylo automaticky staženo
@@ -552,18 +540,18 @@
Nahrávají se změny epizod…Stahují se změny epizod…Nahrává se stav poslechnutí…
- Synchronizují se sbírky…
+ Synchronizuji odběry…Synchronizace proběhla úspěšněSynchronizace selhala
- Přesunout sbírky a frontu do jiného zařízení
+ Přesunout odběry a frontu do jiného zařízeníDatabázeOPMLHTML
- Ukažte své sbírky přátelům
- Přenést sbírky do jiné podcastové aplikace
- Importovat vaše sbírky z jiné podcastové aplikace
- Přenést sbírky, poslechnuté epizody a frontu do aplikace AntennaPod na jiném zařízení
+ Ukažte své odběry přátelům
+ Přenést odběry do jiné podcastové aplikace
+ Importovat vaše odběry z jiné podcastové aplikace
+ Přenést odběry, poslechnuté epizody a frontu do aplikace AntennaPod na jiném zařízeníImportovat AntennaPod databázi z jiného zařízeníOPML importImportovat podcasty (OPML)
@@ -575,7 +563,7 @@
HTML exportExport databázeImport databáze
- 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?
+ 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?Čekejte prosím…Chyba exportuExport úspěšný
@@ -590,6 +578,7 @@
Nastavit časovač vypnutíDeaktivovat časovač vypnutí
+ +%d minutČasovač vypnutíNeplatný vstup, musí být zadáno celé čísloRestartujte zatřesením
@@ -623,22 +612,22 @@
DOPORUČENÉProhledat gpodder.netPřihlásit
- Vítejte do průvodce přihlášením ke gpodder.net účtu. Zadejte vaše přihlašovací údaje:Přihlásit
- Pokud ještě nemáte účet, můžete ho vytvořit zde:\nhttps://gpodder.net/register/
+ Vytvořit účetUživatelské jménoHeslo
- Výběr zařízení
+ Gpodder.net je open-source služba na synchronizaci podcástů nezávislá na AntennaPod projektu.
+ Oficiální server gpodder.net
+ Vlastní server
+ Hostname
+ Vybrat serverVytvořte nové nebo vyberte již existující zařízení pro použití s vašim gpodder.net účtem.
- ID zařízení:\u0020
- Nadpis
- Vytvořit nové zařízení
- Vybrat existující zařízení:
- ID zařízení nesmí být prázdné
- ID zařízení je již obsazeno
+ Název zařízení
+ AntennaPod na %1$sTitulek nesmí být prázdný
+ Existující zařízení
+ Vytvořit zařízeníVybrat
- Úspěšně přihlášeno!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.Synchronizovat nyníPřejít na hlavní obrazovku
@@ -674,7 +663,7 @@
Pro aktivování změn nastavení bylo třeba restartovat aplikaci AntennaPod.Odebírat
- Přidává se do sbírky…
+ Přidává se do odběrů…Spustit ukázkuZastavit ukázku
@@ -692,6 +681,7 @@
Přehodit stránkuPozice: %1$sVykonat
+ Přehrát kapitoluOvěřeníZměnit uživatelské jméno a heslo pro tento podcast a jeho epizody.
@@ -826,18 +816,22 @@
Přijímač zaznamenal závažnou chybuChyba přehrávání médií. Přeskakuji...
+ Chyby
+ NovinkyJe vyžadována činnost z vaší stranyZobrazuje se, pokud je požadována činnost z vaší strany. Například je-li potřeba zadat heslo.StahujiZobrazuje se v průběhu stahování.Přehrává seUmožňuje ovládat přehrávání. Toto je to hlavní oznámení, které uvidité při přehrávání podcastu.
- Chyby
- Upozorňovat pokud něco selže, například stahování či aktualizace odebíraného kanálu.
- Chyby synchronizace
+ Stahování selhalo
+ Zobrazeno pokud selže stahování či aktualizace odebíraného kanálu.
+ Synchronizace selhalaZobrazovat chybu synchronizace s gpodder.
- Automatické stahování
+ Automatické stahování dokončenoZobrazuje se po automatickém stažení epizod.
+ Nová epizoda
+ Zobrazeno pokud bude nalezena nová epizoda podcastu a upozornění jsou zapnuta.Nastavení widgetuVytvořit widget
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 @@
StatistikTilføj podcastUdsendelser
+ KøAlleNyeForetrukne
@@ -17,7 +18,7 @@
LogAbonnementerListe over abonnementer
- Annuller\noverførsel
+ Annuller overførselAfspilningshistorikgpodder.netLogin til gpodder.net
@@ -26,6 +27,8 @@
AfspilningOverførslerPåmindelser
+
+ \"%1$s\" ikke fundetSamlet tid for afspillede udsendelser:%1$d af %2$d udsendelser startet.\n\nAfspillet %3$s af %4$s.
@@ -53,6 +56,7 @@
IngenIngen kompatible apper fundet.
+ Eksporter detaljeret logÅbn i browserKopier webadresse
@@ -81,7 +85,6 @@
Beskrivelse\u0020udsendelserBehandler
- Gem brugernavn og adgangskodeLukPrøv igenInkludér i automatiske overførsler
@@ -93,7 +96,7 @@
FraLidtMeget
- \u0020parallelle overførsler
+ %1$d parallelle overførslerGlobal standardAltidAldrig
@@ -113,7 +116,22 @@
%d valgt%d valgte
+
+ %d episode
+ %d episoder
+ Indlæser mere ...
+ Påmindelser om episoder
+ Vis en notifikation når der er nye episoder
+
+ %2$s har en ny episode
+ %2$s har %1$d nye udsendelser
+
+
+ Nye episoder
+ Nye episoder
+
+ Dine abonnementer har nye udsendelserMarker alle som afspilletMarker alle udsendelser som afspillet
@@ -131,7 +149,7 @@
DelDel...Del fil
- Netsted adresse
+ Adresse på netstedPodcast-adresseBekræft venligst at du ønsker at slette podcasten \"%1$s\" og ALLE dens udsendelser (inklusive overførte udsendelser)Bekræft venligst at du vil fjerne podcasten \"%1$s\". Filerne i den lokale kildemappe vil ikke blive slettet.
@@ -145,7 +163,6 @@
Ikke sat i køHar medierFiltrerede
- {fa-exclamation-circle} Sidste opdatering fejledeÅbn podcastVær venlig og vent til data\'et er indlæst
@@ -160,6 +177,10 @@
SletKan ikke slette fil. En genstart af enheden vil sandsynligvis hjælpe.Slet udsendelse
+
+ %d udsendelse valgt, %d hentet udesendelse slettet.
+ %dudsendelser valgt, %d hentede udsendelser slettet.
+ Fjern \"ny\"-markeringFjernet \"ny\"-markeringMarker som afspillet
@@ -192,7 +213,7 @@
Føjet til foretrukneFjern fra foretrukneFjernet fra foretrukne
- Besøg webside
+ Besøg netstedSpring udsendelse overSlå Automatisk overførsel tilSlå Automatisk overførsel fra
@@ -206,16 +227,12 @@
Detaljer%1$s \n\nFil-URL:\n%2$sKan ikke finde lagerenhed
- Ikke nok pladsHTTP-datafejlUkendt fejl
- Parser-undtagelseFeedets type er ikke understøttetForbindelsesfejl
- Ukendt værtGodkendelsesfejlFiltypefejl
- Adgang nægtetOverførsel annulleretOverførsel annulleret\nAutomatisk overførsel blev slået fra for dette elementOverførsler afsluttet med fejl
@@ -229,12 +246,7 @@
%d overførsel mangler%d overførsler mangler
- Bearbejder overførte dataHenter podcast-data
-
- %d overførsel lykkedes, %d fejlede
- %d overførsler lykkedes, %d fejlede
- Ukendt titelFeedMediefil
@@ -267,6 +279,7 @@
Billed-i-billed-tilstandAntennaPod - Ukendt medienøgle: %1$dFil ikke fundet
+ Element indeholder ikke en mediefil.Lås køLås kø op
@@ -321,9 +334,8 @@
For at abonnere på en podcast, klik plus ikonet nedenforLagring
- Automatisk sletning, Importer, Exporter af episoder
+ Automatisk sletning af udsendelser, import, eksportProjekt
- KøSynkroniseringSynkroniser med andre enheder ved hjælp af gpodder.netAutomatisering
@@ -339,14 +351,17 @@
Slet historikMedieafspillerOprydning i udsendelser
- 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 udsendelserSæt afspilning på pause, hvis hovedtelefoner eller bluetooth afkoblesGenoptag afspilning når hovedtelefonerne tilsluttes igenGenoptag afspilning når bluetooth forbinder igen
- Fremadknap springer over
- Når der trykkes på næste knappen på de tilsluttede høretelefoner, skift til næste episode istedet for at springe frem.
- Tilbageknap genstarter
- Når der trykkes på en fysisk tilbageknap, skal den aktuelle udsendelse afspilles forfra i stedet for at der spoles tilbage.
+ Fremadknap
+ Konfigurer fremadknap
+ Tilbageknap
+ Konfigurer tilbageknap
+ Spol frem
+ Spol tilbage
+ Spring episode over
+ Genstart udsendelseGå til næste element i køen når afspilningen er færdigSlet udsendelsen når afspilningen er færdigSlet automatisk
@@ -357,7 +372,7 @@
Behold udsendelser, som er markeret som foretrukneBehold foretrukne udsendelserAfspilning
- Hovedtelefon kontrol, Overspring intervaller, Kø
+ Hovedtelefonstyring, overspringsintervaller, køNetværkOpdateringsinterval, overførselsindstillinger, mobildataOpdateringsinterval eller -klokkeslæt
@@ -366,8 +381,11 @@
Slå fraIndstil intervalIndstil klokkeslæt
- hver %1$skl. %1$s
+
+ Hver time
+ Hver %d. time
+ Kontinuerlig afspilningHovedtelefoner eller bluetooth afbrudtTilslutning af hovedtelefoner igen
@@ -401,7 +419,7 @@
Mellemlager for udsendelserTotal nummer af episoder downloades på din enhed. Stop automatisk download, når dette nummer nåes.Brug udsendelsesbillede
- Brug det udsendelsesspecifikke billede når muligt. Hvis dette slås fra, vil appen altid bruge podcastens billede.
+ Hvis tilbageværende tidBrug systemtemaLysMørk
@@ -421,10 +439,8 @@
Tving fuld synkroniseringSynkroniser tilstande for alle abonnementer og udsendelser med gpodder.net.%1$s med enheden %2$s]]>
- Synkronisering mislykkedes
- Denne indstilling vedrører ikke godkendelsesfejl.Tilpas de tilgængelige hastigheder for afspilning med variabel hastighed
- Hastighed, der skal bruges, når lydafspilning startes til afsnit i denne podcast
+ Hastighed, der skal bruges, når lydafspilning startes for udsendelser i denne podcastOverspring automatiskOverspring introduktioner og slut kreditter.Overspring sidste
@@ -437,8 +453,6 @@
Indstil antallet af sekunder, der skal springes fremad, når der trykkes på fremadspolingsknappenTidshop for tilbagespolingIndstil antallet af sekunder, der skal springes tilbage, når der trykkes på tilbagespolingsknappen
- Indstil værtsnavn
- Brug standardværtHøj prioritet for notifikationDette udvider normalt notifikationen til at vise afspilningsknapper.Vedholdende afspilningsknapper
@@ -449,10 +463,6 @@
Du kan højst vælge %1$d knapper.Indstil baggrund på låseskærmenSæt baggrunden på låseskærmen til billedet for den aktuelle udsendelse. Som en sidevirkning vil det også vise billedet i tredjepartsapps.
- Overførsel mislykkedes
- Lav en rapport, som viser detaljer om fejlene, hvis overførsler fejler
- Automatisk overførsel fuldført
- Vis en notifikation for automatisk overførte udsendelserAndroid-versioner før 4.1 understøtter ikke udvidede notifikationer.Placering i køFøj udsendelser til: %1$s
@@ -462,10 +472,11 @@
Slået fraStørrelse på mellemlager (cache) for billederStørrelse på diskmellemlageret (disk cache) for billeder
- Bruger froum
+ Dokumentation & Support
+ BrugerforumRapportér fejl i appenÅbn programfejlsdatabase
- Eksportere log
+ Eksportér loggeKopier til udklipsholderKopieret til udklipsholderEksperimentelt
@@ -473,14 +484,14 @@
Nuværende værdi: %1$sProxyIndstil en netværksproxy
- Ofte Stillede SpørgsmålIngen webbrowser fundetChromecast-understøttelseAktiver understøttelse af fjernafspilning på Cast-enheder (såsom Chromecast, højttalere med indbygget Chromecast, eller Android TV)Chromecast kræver tredjeparts proprietære biblioteker, som er slået fra i denne version af AntennaPodSæt overførte udsendelser i køFøj downloadede udsendelser til køen
- Indbygget Android-afspiller
+ Indbygget Android-afspiller (deprekeret)
+ Sonic-medieafspiller (deprekeret)ExoPlayer (anbefalet)Skift til ExoPlayerSkiftet til ExoPlayer.
@@ -501,7 +512,7 @@
Gå til side ...Vælg sideSlet fjernet fra kø
- Fjern automatisk afsnit fra køen, når den slettes.
+ Fjern automatisk udsendelse fra køen, når den slettes.Abonnement filterFiltrer dine abonnementer i navigationspanelet og på abonnementsoversigten.Ingen
@@ -513,9 +524,9 @@
Ikke holdt opdateretOm
- AntennaPod version
- Bidragere
- Alle kan hjælpe med at lave AntennaPod bedre - med kode, oversættelse eller hjælpe brugere i vores forum
+ AntennaPod-version
+ Bidragydere
+ Alle kan hjælpe med at lave AntennaPod bedre - med kode, oversættelse eller ved at hjælpe brugere i vores forumUdviklereOversættereSærlig tak
@@ -541,34 +552,35 @@
HTMLVis abonnementerFlyt abonnementer til anden podcast program
- Importerer abonnementer fra anden podcast program
- Flyt abonnementer, aflyttede afsnit og kø til AntennaPod på en anden enhed
- Importere AntennaPod database fra anden enhed
+ Importér dine abonnementer fra et andet podcast-program
+ Flyt abonnementer, aflyttede udsendelser og kø til AntennaPod på en anden enhed
+ Importér AntennaPod-database fra en anden enhedOPML-import
- Importere podcast liste (OPML)
+ Importér podcastliste (OPML)Der opstod en fejl, da OPML-dokumentet blev forsøgt indlæst:Ingen fil valgt!Vælg alleFravælg alleOPML-eksportHTML-eksport
- Eksportere database
- Importere database
- 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?
+ Eksport af database
+ Importér database
+ 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?Vent...EksportfejlEksport lykkedesDen eksporterede fil blev skrevet til:\n\n%1$sAdgang til eksternt lager er påkrævet for at læse OPML-filen
- Vælg fil til import
+ Vælg fil der skal importeresImporteretTryk venligst OK for at genstarte AntennaPod
- Databasen var exporteret af en nyere version af AntennaPod. Din nuværende installation ved ikke endnu hvordan den skal håndtere denne fil.
- Foretrukne eksport
- Eksportere gemte foretrukne til fil
+ Databasen blev eksporteret fra en nyere version af AntennaPod. Din nuværende installation ved endnu ikke hvordan den skal håndtere denne fil.
+ Eksport af foretrukne
+ Eksportér gemte foretrukne til filIndstil søvntimerSlå søvntimer fra
+ +%d minutterSøvntimerUgyldig indtastning: tid skal være et heltalRyst for at nulstille
@@ -596,22 +608,20 @@
FORSLAGSøg på gpodder.netLog ind
- Velkommen til gpodder.nets loginproces. Skriv først dine loginoplysninger:Log ind
- Hvis du ikke har en konto endnu, kan du oprette en her:\nhttps://gpodder.net/register/
+ Opret kontoBrugernavnAdgangskode
- Valg af enhed
+ Officiel gpodder.net server
+ Værtsnavn
+ Vælg serverOpret en ny enhed til at bruge med din gpodder.net-konto eller vælg en eksisterende:
- Enheds-id:\u0020
- Enhedsnavn
- Opret ny enhed
- Vælg en eksisterende enhed:
- Enheds-id må ikke være tomt
- Enheds-id er allerede i brug
+ Enhedsnavn
+ AntennaPod på %1$sEnhedsnavn må ikke være tomt
+ Eksisterende enheder
+ Opret ny enhedVælg
- Login lykkedes!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.Start synkronisering nuGå til hovedskærmen
@@ -659,7 +669,7 @@
VideoUdsendelse overføresUdsendelse er i køen
- Afsnit er markeret som favorit
+ Udsendelse er markeret som foretrukkenTræk for at ændre dette elements placeringIndlæs næste sideSkift sider
@@ -677,8 +687,8 @@
Hold opdateretInkludere denne podcast når alle podcast genindlæsesAutomatisk download slået fra i de generelle AntennaPod indstilinger
- Lytter efter:
- Afsnit på enhed:
+ Lyttet i:
+ Udsendelser på enheden:Plads brugt:Vis for alle podcasts »
@@ -691,7 +701,7 @@
Søg på Podcastindex.orgSøg i fyydAvanceret
- Tilføj podcast via RSS adresse
+ Tilføj podcast via RSS-adresseGennemse gpodder.netOpdagGem
@@ -758,7 +768,7 @@
Medtag:AfspilningspositionAdresse på mediefil
- Afsnit netsted
+ Adresse på udsendelseMediefilLydknapper
@@ -799,18 +809,20 @@
Modtagerafspilleren er stødt på en alvorlig fejlFejl ved afspilning af medie. Springer over…
+ Fejl
+ NyhederHandling påkrævetVist hvis din handling er nødvendig, for eksempel hvis du skal skrive et kodeord.HenterVises samtidig med den hentes.Spiller nuGiver adgang til at kontrollere afspilning. Dette er den mest normale notifikation du vil se, mens du afspiller en podcast.
- Fejl
- Vises hvis noget gik galt, for eksempel hvis en overførsel eller feed-opdatering fejlede.
- Synkroniseringsfejl
+ Overførsel mislykkedes
+ Synkronisering mislykkedesVises når gpodder-synkronisering fejler.
- Automatisk hentninger
+ Automatisk overførsel fuldførtVist når episoder automatisk var blevet downloaded.
+ Ny episodeKontrol opsætningOpret kontrol
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 @@
StatistikenPodcast hinzufügenEpisoden
+ WarteschlangeAlleNeuFavoriten
@@ -26,6 +27,8 @@
WiedergabeDownloadsBenachrichtigungen
+
+ \"%1$s\" nicht gefundenGesamtzeit aller gespielten Episoden%1$d von %2$d Episoden gestartet.\n\n%3$s von %4$s Episoden gespielt.
@@ -53,6 +56,8 @@
KeineKeine kompatiblen Apps gefunden
+ Detaillierte Logs exportieren
+ Detaillierte Logs können persönliche Informationen enthalten, wie zum Beispiel die Liste deiner AbonnementsIm Browser öffnenURL kopieren
@@ -81,7 +86,6 @@
Beschreibung\u0020EpisodenVerarbeite
- Benutzername und Password merkenSchließenErneut versuchenAutomatisch herunterladen
@@ -93,7 +97,7 @@
AusSchwachStark
- \u0020gleichzeitige Downloads
+ %1$d parallele DownloadsStandardwertImmerNie
@@ -113,7 +117,22 @@
%d ausgewählt%d ausgewählt
+
+ %d Episode
+ %d Episoden
+ Lade mehr...
+ Episodenbenachrichtigung
+ Benachrichtigung beim Erscheinen einer neuen Episode anzeigen.
+
+ Neue Episode bei %2$s
+ %1$d neue Episoden bei %2$s
+
+
+ Neue Episode
+ Neue Episoden
+
+ Deine Abonnements haben neue Episoden.Alle als gespielt markierenAlle Episoden als gespielt markiert
@@ -145,7 +164,6 @@
Nicht in WarteschlangeHat MedienGefiltert
- {fa-exclamation-circle} Aktualisierung fehlgeschlagenPodcast öffnenBitte warte, bis die Daten geladen sind
@@ -160,6 +178,10 @@
LöschenDie Datei kann nicht gelöscht werden. Eventuell hilft es, das Gerät neu zu starten.Episode löschen
+
+ %d Episode ausgewählt, %d Download gelöscht.
+ %d Episode ausgewählt, %d Download(s) gelöscht.
+ \"Neu\"-Markierung entfernen\"Neu\"-Markierung entferntAls gespielt markieren
@@ -206,16 +228,12 @@
Details%1$s \n\nDatei-URL:\n%2$sSpeichermedium nicht gefunden
- Zu wenig SpeicherplatzHTTP DatenfehlerUnbekannter Fehler
- ParserfehlerNicht unterstützter Feed-TypVerbindungsfehler
- Unbekannter HostAuthentifizierungsfehlerDateityp-Fehler
- VerbotenDownload abgebrochenDownload abgebrochen\nAutomatischen Download für diese Episode deaktiviertDownloads endeten mit Fehler(n)
@@ -229,12 +247,7 @@
%d Download übrig%d Downloads übrig
- Verarbeite DownloadsLade Podcast-Daten
-
- %d Download erfolgreich, %d fehlgeschlagen
- %d Downloads erfolgreich, %d fehlgeschlagen
- Unbekannter TitelFeedMediendatei
@@ -323,7 +336,6 @@
SpeicherAutomatisches Löschen von Episoden, Importieren, ExportierenProjekt
- WarteschlangeSynchronisationSynchronisiere über gpodder.net mit anderen GerätenAutomatisierung
@@ -339,14 +351,14 @@
Verlauf leerenMedienabspielerAutomatisches Löschen
- 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
+ Episoden, die gelöscht werden können, wenn beim automatischen Herunterladen Platz für neue Episoden benötigt wirdWiedergabe pausieren, wenn Kopfhörer ausgesteckt oder Bluetooth getrennt wirdWiedergabe fortsetzen, wenn Kopfhörer wieder eingesteckt werdenWiedergabe fortsetzen, wenn Bluetooth wieder verbunden ist
- \"Nächster\"-Taste springt zur nächsten Episode
- Wenn ein Vorwärts Button auf einem Bluetooth Gerät gedrückt wird, überspringe die nächste Episode anstatt vorzuspulen.
- Vorheriger-Taste startet neu
- Die Wiedergabe der aktuellen Episode neu starten, wenn die \"Vorheriger\"-Taste gedrückt wird (statt zurückzuspulen)
+ Vorspulen
+ Zurückspulen
+ Episode überspringen
+ Episode erneut wiedergebenSpringe zur nächsten Episode in der Warteschlange, wenn die Wiedergabe endetEpisode löschen, wenn die Wiedergabe endetAutomatisches Löschen
@@ -366,8 +378,11 @@
DeaktivierenIntervall einstellenTageszeit festlegen
- jede %1$sum %1$s
+
+ Jede Stunde
+ Alle %d Stunden
+ Durchgehendes AbspielenKopfhörer oder Bluetooth getrenntKopfhörer wieder eingesteckt
@@ -401,7 +416,9 @@
EpisodenspeicherGesamtzahl an Episoden, die auf dem Gerät gespeichert werden. Automatisches Herunterladen wird pausiert, wenn diese Anzahl erreicht ist.Episoden-Bilder verwenden
- Benutze Episoden-Bilder, wenn verfügbar. Ist die Option deaktiviert, wird immer das Podcast-Bild angezeigt.
+ Falls verfügbar episodenspezifische Titelbilder in Listen verwenden. Falls nicht ausgewählt wird immer das Titelbild des Podcasts verwendet.
+ Verbleibende Zeit anzeigen
+ Zeigt, falls ausgewählt, die verbleibende Zeit für die Episode, andernfalls die gesamte verbleibende Zeit aller Episoden.System-Design verwendenHellDunkel
@@ -421,8 +438,6 @@
Komplette Synchronisation erzwingenKompletten Abonnement- und Episoden-Status mit gpodder.net synchronisieren.%1$s mit dem Gerät %2$s]]>
- Synchronisation fehlgeschlagen
- Diese Einstellung gilt nicht für Authentifizierungsfehler.Anpassen der verfügbaren Geschwindigkeiten für die Wiedergabe mit variabler GeschwindigkeitAbspielgeschwindigkeit für Episoden dieses PodcastsAutomatisches Überspringen
@@ -437,8 +452,6 @@
Passe an, wie viele Sekunden vorgespult wird, wenn die entsprechende Hardware-Taste gedrückt wirdRückspulzeitPasse an, wie viele Sekunden zurückgespult wird, wenn die entsprechende Hardware-Taste gedrückt wird
- Hostname ändern
- Standard-Host verwendenHohe BenachrichtigungsprioritätDies erweitert normalerweise die Benachrichtigung und zeigt so die Wiedergabe-Buttons an.Persistente Wiedergabesteuerung
@@ -449,10 +462,6 @@
Du kannst maximal %1$d Elemente auswählen.Lockscreen-Hintergrund einstellenVerwende das aktuelle Episodenbild als Lockscreen-Hintergrund. Es wird als Nebeneffekt auch in anderen Apps gezeigt.
- Download fehlgeschlagen
- Wenn Downloads fehlschlagen, erstelle einen Bericht, der die Details des Fehlschlages beschreibt.
- Automatischer Download abgeschlossen
- Zeige eine Benachrichtigung für automatisch heruntergeladene Episoden.Android-Versionen vor 4.1 unterstützen keine erweiterten Benachrichtigungen.Position beim EinreihenFüge Episoden %1$s hinzu
@@ -462,6 +471,7 @@
DeaktiviertGröße des Bilder-ZwischenspeichersGröße des Zwischenspeichers für Bilder
+ Dokumentation + SupportBenutzerforumFehler meldenBug-Tracker öffnen
@@ -473,14 +483,14 @@
Aktueller Wert: %1$sProxyRichte einen Netzwerk-Proxy ein
- Häufig gestellte FragenKein Browser gefunden.Chromecast-UnterstützungAktiviere die Unterstützung von Cast-Geräten (Chromecast, Lautsprecher oder Android TV) zum entfernten AbspielenChromecast benötigt proprietäre Bibliotheken von Drittanbietern, die in dieser Version von AntennaPod deaktiviert sindDownloads einreihenFüge heruntergeladene Episoden zur Warteschlange hinzu
- Androids eingebauter Abspieler
+ Integrierter Android Player (veraltet)
+ Sonic Media Player (veraltet) ExoPlayer (empfohlen)Zu ExoPlayer wechselnZu ExoPlayer gewechselt.
@@ -569,6 +579,7 @@
Timer einstellenSchlummerfunktion deaktivieren
+ +%d minSchlummerfunktionUngültige Eingabe, Zeit muss eine Ganzzahl seinDurch Schütteln zurücksetzen
@@ -596,22 +607,22 @@
VORSCHLÄGEgpodder.net durchsuchenAnmeldung
- Willkommen beim gpodder.net Anmeldeprozess. Gib zuerst deine Anmeldeinformationen ein:Anmelden
- Falls du noch kein gpodder.net Profil hast, kannst du hier eines erstellen: https://gpodder.net/register/
+ Konto anlegenBenutzernamePasswort
- Geräte-Auswahl
+ Gpodder.net ist ein Open-Source-Dienst zur Synchronisation von Podcasts und unabhängig vom AntennaPod-Projekt.
+ Offizieller Server gpodder.net
+ Anderer Server
+ Hostname
+ Server auswählenErstelle ein neues Gerät für dein gpodder.net Profil oder wähle ein bereits vorhandenes:
- Geräte-ID:\u0020
- Beschreibung
- Neues Gerät erstellen
- Vorhandenes Gerät auswählen
- Geräte-ID darf nicht leer sein
- Geräte-ID wird bereits verwendet
+ Gerätename
+ AntennaPod auf %1$sBeschreibung darf nicht leer sein
+ Bestehende Geräte
+ Gerät anlegenAuswählen
- Anmeldung erfolgreich!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.Jetzt synchronisierenZum Hauptbildschirm zurückkehren
@@ -665,6 +676,7 @@
Seiten wechselnPosition: %1$sAktion anwenden
+ Kapitel abspielenAuthentifizierungÄndere den Benutzernamen und das Passwort für diesen Podcast und dessen Episoden.
@@ -799,18 +811,20 @@
Es wurde ein schwerer Fehler beim Empfangsgerät festgestelltFehler bei Wiedergabe. Überspringe...
+ Fehler
+ NachrichtenHandlung notwendigWird gezeigt, wenn deine Handlung notwendig ist, zum Beispiel wenn du ein Passwort eingeben musst.HerunterladenWird gezeigt beim Herunterladen.Jetzt spieltErlaubt es, die Wiedergabe zu steuern. Dies ist die Hauptbenachrichtigung, die du siehst, während ein Podcast abgespielt wird.
- Fehler
- Wird bei einem Problem angezeigt, wenn zum Beispiel ein Download oder die Aktualisierung eines Feed fehlschlägt.
- Fehler bei der Synchronisation
+ Download fehlgeschlagen
+ Synchronisation fehlgeschlagenWird angezeigt, wenn die gpodder-Synchronisierung fehlschlägt.
- Automatische Downloads
+ Automatischer Download abgeschlossenWird angezeigt, wenn Episoden automatisch heruntergeladen worden sind.
+ Neue EpisodeWidget-EinstellungenWidget erstellen
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 @@
EstadísticasAñadir pódcastEpisodios
+ ColaTodosNuevosFavoritos
@@ -17,7 +18,7 @@
RegistroSuscripcionesLista de suscripciones
- Cancelar\ndescarga
+ Cancelar DescargaHistorial de reproduccionesgpodder.netIniciar sesión en gpodder.net
@@ -26,6 +27,7 @@
ReproducciónDescargasNotificaciones
+
Tiempo total de reproducción de episodios:%1$d episodios iniciados de %2$d.\n\nReproducidos %3$s de %4$s.
@@ -53,6 +55,8 @@
NingunoNo se encontraro apps compatibles
+ Exportar registros detallados
+ Los registros detallados pueden contener información sensible, como su lista de suscripcionesAbrir en el navegadorCopiar URL
@@ -81,7 +85,6 @@
Descripción\u0020episodiosProcesando
- Guardar usuario y contraseñaCerrarReintentarIncluir en descargas automáticas
@@ -89,16 +92,17 @@
La nueva opción descarga automática se aplicará automáticamente a episodios nuevos.\n¿También desea aplicarlo a episodios anteriores?Borrar episodio automáticamenteReducción de volumen
- Bajar el volumen para episodios de este feed: %1$s
+ Bajar el volumen para episodios de este canal: %1$sApagadoLigeroFuerte
- \u0020descargas paralelas
+ %1$d descargas paralelasGlobal por defectoSiempreNuncaEnviar…Nunca
+ Cuando no esté en favoritosCuando no esté en la colaDespués de acabar
@@ -113,7 +117,22 @@
1%d seleccionado%d seleccionado
+
+ %d episodio
+ %d episodios
+ Cargando mas...
+ Notificaciones de Episodios
+ Mostrar una notificación cuando se estrene un nuevo episodio.
+
+ %2$s tiene un nuevo episodio
+ %2$s tiene %1$d episodios nuevos
+
+
+ Nuevo Episodio
+ Nuevos Episodios
+
+ Sus suscripciones tienen nuevos episodios.Marcar todos como reproducidosMarcados todos los episodios como reproducidos
@@ -122,7 +141,7 @@
Eliminar todas las marcas \"nuevo\"Eliminadas todas las marcas \"nuevo\"Por favor, confirma que quieres eliminar las marcas \"nuevo\" de todos los episodios.
- Información del programa
+ Mostrar informaciónMostrar ajustes del pódcastInformación del pódcastAjustes del pódcast
@@ -132,7 +151,7 @@
Compartir…Compartir el archivoDirección web
- URL del feed del podcast
+ URL del canal del podcastConfirme que quiere borrar el pódcast \"%1$s\" y TODOS los episodios (incluidos los descargados).Confirme que quiere borrar el podcast \"%1$s\". Los archivos en la carpeta origen local no serán borrados.Eliminando el pódcast
@@ -145,7 +164,6 @@
No en colaTiene multimediaFiltrados
- {fa-exclamation-circle} Error en la última actualizaciónAbrir pódcastEsperando a que los datos carguen
@@ -160,6 +178,10 @@
BorrarNo se puede borrar el fichero. Reiniciar el dispositivo podría ayudar.Borrar Episodio
+
+ %d episodio seleccionado, %d descarga eliminada.
+ %d episodios seleccionados, %d descarga(s) eliminada(s).
+ Eliminar marca \"nuevo\"Eliminada marca \"nuevo\"Marcar como reproducido
@@ -206,20 +228,16 @@
Detalles%1$s \n\nURL de archivo:\n%2$sNo se ha encontrado un dispositivo de almacenamiento
- Espacio insuficienteError de datos HTTPError desconocido
- Excepción del analizadorTipo de canal no admitidoError de conexión
- Host desconocidoError de autenticaciónTipo de archivo erróneo
- ProhibidoDescarga canceladaDescarga cancelada\nSe desactivó la descarga automática en este elementoDescargas completadas con error(es)
- Auto-descargas completadas
+ Descargas automáticas completadasInforme de descargasURL con formato incorrectoError de E/S
@@ -229,12 +247,7 @@
Queda %d descargaQuedan %d descargas
- Procesando descargasDescargando datos del pódcast
-
- %d descarga exitosa, %d fallidas
- %d descargas exitosas, %d fallidas
- Título desconocidoCanalArchivo multimedia
@@ -242,11 +255,11 @@
No se proporcionó ningún pódcast que pudiera mostrarse.Autenticación requeridaEl recurso solicitado requiere un usuario y contraseña
- Confirmar descarga por red móvil
+ Confirmar descarga vía datos móvilesSe 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\nSe recordará su elección durante 10 minutos.Se desactivaron las descargas por red de datos móviles en la configuración.\n\n¿Quiere permitir las descargas temporalmente?\n\nSe recordará su elección durante 10 minutos.
- Confirmar streaming por red móvil
- El streaming sobre datos móviles está deshabilitado en los ajustes. Toca para hacer el streaming de todas formas.
+ Confirmar reproducción vía datos móviles
+ La reproducción vía datos móviles está deshabilitada en los ajustes. Toca para escuchar en directo todas formas.SiempreUna vezAñadir a la cola
@@ -267,6 +280,7 @@
Modo picture-in-pictureAntennaPod - Tecla multimedia desconocida: %1$dArchivo no encontrado
+ El elemento no contiene un archivo multimediaBloquear colaDesbloquear cola
@@ -317,13 +331,12 @@
Puede añadir episodios a los favoritos presionándolos durante un tiempo prolongado.Sin capítulosEste episodio no tiene capítulos.
- No hay subscripciones
+ No hay suscripcionesPara suscribirse a un podcast, pulsa el icono \"más\" de abajo.Almacenamiento
- Auto borrar espisodio, Importar, Exportar
+ Borrado automático, Importar, ExportarProyecto
- ColaSincronizaciónSincronizar con otros dispositivos usando gpodder.netAutomatización
@@ -334,19 +347,24 @@
Elementos externosInterrupcionesControl de reproducción
+ Reasignar botones físicosBuscar...Sin resultadosBorrar historialReproductor multimediaLimpieza de episodios
- Los episodios que no estén en la cola ni en favoritos pueden eliminarse si la descarga automática necesita espacio para nuevos episodios
+ Episodios que pueden ser eliminados si la Descarga Automática necesita espacio para nuevos episodiosPausar la reproducción al desconectar los auriculares o el bluetoothReanudar la reproducción cuando se reconecten los auricularesReanudar la reproducción cuando se reconecte el bluetooth
- Botón avance: Saltar
- Al pulsar el botón de avance en un dispositivo conectado por bluetooth, salta al siguiente episodio en lugar de avanzar
- Botón retroceso: Reiniciar
- Al pulsar el botón físico de retroceso se comenzará el episodio de nuevo en lugar de retroceder
+ Botón de Avance
+ Personalizar el comportamiento del botón de avance
+ Botón de Retroceso
+ Personalizar el comportamiento del botón de retroceso
+ Avance Rápido
+ Rebobinar
+ Omitir episodio
+ Reiniciar EpisodioSaltar al siguiente elemento de la cola al acabar la reproducciónBorrar el episodio cuando finalice la reproducciónEliminar automáticamente
@@ -366,21 +384,24 @@
DeshabilitarAjustar intervaloAjustar hora del día
- todos los %1$sa las %1$s
+
+ Cada hora
+ Cada %d horas
+ Reproducción continuaDesconexión de los auricuales o BluetoothReconectar con los auricularesReconectar con Bluetooth
- Preferir Streaming
- Muestra el botón de stream en lugar del botón de descargar en las listas.
- Actualizaciones por red móvil
+ Preferir escuchar en directo
+ Muestra el botón de escuchar en directo en lugar del botón de descargar en las listas.
+ Descargas vía datos móvilesSeleccionar lo que se debe permitir descargar con datos móviles
- Actualización de podcast
+ Actualizar podcastImágenes de portada
- Auto descargar
+ Descarga automáticaDescarga de episodio
- Streaming
+ Escuchar en directoInterfaz de usuarioApariencia, Suscripción, Pantalla de bloqueoElegir un tema
@@ -401,7 +422,9 @@
Almacenamiento de episodiosNúmero total de episodios cacheados en el dispositivo. La descarga automática se suspenderá si se alcanza este número.Usar portada del episodio
- Usar la portada del episodio cuando sea posible. Si se desactiva, la aplicación siempre usará la portada del podcast.
+ Usar la portada de cada episodio en las listas cuando sea posible. Si se desactiva, la aplicación siempre usará la portada del podcast.
+ Mostrar Tiempo Restante
+ Muestra el tiempo restante de los episodios si está activado. Si se desactiva, muestra la duración total de los episodios.Usar tema del sistemaClaroOscuro
@@ -421,8 +444,6 @@
Forzar la sincronización completaSincronizar todas las suscripciones y episodios con gpodder.net.%1$s con dispositivo %2$s]]>
- Error en la sincronización
- Este ajuste no afecta a los errores de autenticación.Personalice las velocidades disponibles en la reproducción a velocidad variableLa velocidad a la que comenzarán los episodios de este podcastSaltar automático
@@ -437,8 +458,6 @@
Personalice el número de segundos que avanzará cuando se pulsa el botón de avanceIntervalo de retrocesoPersonalice el número de segundos que retrocederá cuando se pulsa el botón de retrocedeso
- Establecer nombre del dispositivo
- Usar el nombre predeterminadoAlta prioridad de las notificacionesEsto suele expandir las notificaciones para mostrar los botones de reproducción.Controles de reproducción persistentes
@@ -449,10 +468,6 @@
Sólo puede seleccionar un máximo de %1$d elementos.Establecer fondo de pantalla de bloqueoEstablecer 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.
- Descarga fallida
- Si la descarga falla, generar un informe con los detalles del fallo
- Auto-descarga completada
- Mostrar una notificación de los episodios descargados automáticamente.Las versiones de Android anteriores a la 4.1 no soportan notificaciones expandidasAñadir a la cola en cierta ubicaciónAñadir episodios a: %1$s
@@ -473,14 +488,15 @@
Valor actual: %1$sProxyConfigurar proxy de red
- Preguntas de Uso Frecuente (FAQ)No se ha encontrado un navegador web.Soporte para ChromecastHabilitar soporte para reproducción remota en dispositivos Cast (como Chromecast, altavoces o Android TV)Chromecast requiere librerías propietarias de terceros que están deshabilitadas en esta versión de AntennaPodAñadir descargados a la colaAñadir episodios descargados a la cola
- Reproductor Android integrado
+ Reproductor integrado de Android (obsoleto)
+ Sonic Media Player (obsoleto)
+ ExoPlayer (recomendado)Cambiar a ExoPlayerCambiado a ExoPlayerSaltar silencio en audio
@@ -505,7 +521,8 @@
Filtra tus suscripciones en el cajón de navegación y pantallas de suscripción.NingunoLas suscripciones están filtradas.
- Auto-descargado
+ Contador mayor que cero
+ Descargado automáticamente No auto-descargadoMantenido actualizadoNo mantenido actualizado
@@ -567,6 +584,7 @@
Establecer un temporizadorDesactivar el temporizador
+ +%d minTemporizadorEntrada no válida, el tiempo debe ser un número enteroAgita para reiniciar
@@ -594,22 +612,21 @@
SUGERENCIASBuscar en gpodder.netIniciar sesión
- Bienvenido al inicio de sesión de gpodder.net. Primero, escriba sus datos de inicio de sesión:Iniciar sesión
- Si aún no tiene una cuenta, puede crearla en:\nhttps://gpodder.net/register/
+ Crear cuentaUsuarioContraseña
- Selección del dispositivo
+ Gpodder.net es un servicio de sincronización de podcasts de código abierto que es independiente del proyecto AntennaPod.
+ Servidor oficial de gpodder.net
+ Servidor personalizado
+ Nombre del host
+ Seleccione el servidorCree un nuevo dispositivo para usar en su cuenta de gpodder.net o elija uno existente:
- Id. de dispositivo:\u0020
- Descripción
- Crear dispositivo nuevo
- Elegir dispositivo existente:
- El id. de dispositivo no puede estar vacío
- El id. de dispositivo ya está en uso
+ Nombre del dispositivoEl texto no puede estar en blanco
+ Dispositivos existentes
+ Crear dispositivoElegir
- ¡Inicio de sesión correcto!¡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.Comenzar sincronización ahoraIr a la pantalla principal
@@ -663,6 +680,7 @@
Cambiar páginasPosición: %1$sAplicar acción
+ Reproducir capítuloAutenticaciónCambiar nombre y contraseña de este pódcast y sus episodios
@@ -699,9 +717,11 @@
Resultados de %1$sAñadir carpeta local
+ Carpeta localRe-conectar carpeta localEn caso de falta de permisos, puedes usar esto para re-conectar la misma carpeta. No selecciones otra carpeta.Este podcast virtual fue creado añadiendo una carpeta a AntennaPod.
+ No se puede iniciar el administrador de archivos del sistemaFiltroTodos
@@ -795,24 +815,28 @@
El reproductor ha encontrado un error graveError reproduciendo medio. Saltando…
+ Errores
+ NuevosAcción necesariaSe muestra si su acción es necesaria, por ejemplo, si necesita introducir una contraseña.DescargandoSe muestra mientras se está descargando.ReproduciendoPermite controlar la reproducción. Es la notificación principal que se ve mientras se reproduce un pódcast.
- Errores
- Muestra si algo salió mal, por ejemplo, si falla la descarga o la actualización del feed.
- Errores de sincronización
+ Descarga fallida
+ Muestra cuando falla la descarga o la actualización del canal.
+ Sincronización fallidaMostrar cuando falle la sincronización de gpodder.
- Descargas automáticas
+ Descarga automática completadaMostrar cuándo los episodios se han descargado automáticamente.
+ Nuevo Episodio
+ Se muestra al encontrar un nuevo episodio de un podcast, si las notificaciones están activadasConfiguraciones del WidgetCrear widgetOpacidadConfiguración actualizada satisfactoriamente.
- Parece que usas mucho el stream. ¿Quieres mostrar el botón de stream en la lista de episodios?
+ Parece que usas mucho la reproducción en directo. ¿Quieres que las listas de episodios muestren los botones de escuchar en directo?Parece que usas mucho las descargas. ¿Quieres mostrar el botón de descargar en la lista de episodios?
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 @@
StatistikaLisa taskuhäälingSaated
+ JärjekordKõikUuedLemmikud
@@ -17,7 +18,7 @@
LogiTellimusedTellimuste nimekiri
- Tühista\nLaadi alla
+ Katkesta allalaadimineEsitamise ajalugugpodder.netgpodder.net kasutajanimi
@@ -26,6 +27,8 @@
EsitamineAllalaadimisedTeavitused
+
+ \"%1$s\" ei leitudSaadete kogupikkus:%1$d %2$d-st saatest on alustatud.\n\nKuulatud on %3$s saadet %4$s-st.
@@ -53,6 +56,8 @@
PoleÜhtegi ühilduvat rakendust ei leitud
+ Ekspordi täpne logi
+ Täpne logi võib sisaldada tundlikku infot nagu sinu tellimuste nimekiriAva veebisirvijasKopeeri URL
@@ -81,7 +86,6 @@
Kirjeldus\u0020saadetTöötlemine
- Salvesta kasutajanimi ja paroolSulgeProovi uuestiLisa automaatsetesse allalaadimistesse
@@ -93,12 +97,13 @@
VäljasKergeTugev
- \u0020samaaegset allalaadimist
+ %1$d paralleelset allalaadimistÜldine vaikeväärtusAlatiMitte kunagiSaada...Mitte kunagi
+ Kui pole lemmikKui pole järjekorrasPärast lõpetamist
@@ -113,7 +118,17 @@
%d valitud%d valitud
+
+ %d saade
+ %d saadet
+ Laadimine…
+ Saadete teavitused
+ Teate kuvamine, kui avaldatakse uus saade.
+
+ Uus saade
+ Uued saated
+ Märgi kuulatuksMärgi kõik saated kuulatuks
@@ -145,7 +160,6 @@
Pole järjekorrasOn meediafaileFiltreeritud
- {fa-exclamation-circle} Viimane värskendamine ebaõnnestusAva taskuhäälingPalun oota andmete laadimist
@@ -206,16 +220,12 @@
Üksikasjad%1$s \n\nFaili URL:\n%2$sSalvestuskohta ei leitud
- Pole piisavalt ruumiHTTP andmete vigaTundmatu tõrge
- Parsimise järjekordToetamata uudisvoo tüüpÜhenduse viga
- Tundmatu hostAutentimise vigaFailitüübi viga
- KeelatudAllalaadimine on tühistatudAllalaadimine tühistati\nKeelati selle saate automaatne allalaadimineAllalaadimised lõpetati veaga (vigadega)
@@ -229,12 +239,7 @@
%d allalaadimine jäänud%d allalaadimist jäänud
- Allalaadimiste töötlemineTaskuhäälingu andmete allalaadimine
-
- %d allalaadimine õnnestus, %d ebaõnnestus
- %d allalaadimist õnnestus, %d ebaõnnestus
- Tundmatu pealkiriUudisvoogMeediafail
@@ -323,7 +328,6 @@
SalvestusruumSaate automaatne kustutamine, importimine, eksportimineProjekt
- JärjekordSünkroonimineSünkrooni teiste seadmetega gpodder.net abilAutomaatika
@@ -339,14 +343,9 @@
Puhasta ajaluguMeediaesitajaSaadete kustutamine
- Saated, mis ei ole järjekorras ega lemmikud, on eemaldamise kandidaadid, kui automaatselt allalaaditavate saadete jaoks on vaja rohkem ruumiEsitus pausitakse, kui kõrvaklapid või bluetooth eemaldatakseEsitus jätkub, kui kõrvaklapid uuesti ühendatakseEsitus jätkub kui bluetooth uuesti ühendub
- Edasi nupp jätab vahele
- Edasi nupu vajutamine bluetoothi seadmel edasi kerimise asemel hüppab järgmisele saatele
- Tagasi nupp alustab uuesti
- Riistvaralise tagasi nupu vajutamine alustab tagasi kerimise asemel praegust saadet algusestKui saade lõpeb, siis esita kohe järgmine järjekorras olev saade.Kustuta saated, kui need on kuulatudAutomaatne kustutamine
@@ -366,7 +365,6 @@
Lülita väljaMäära intervallMäära aeg päevades
- iga %1$skell %1$sPidev esitaminePeakomplekti või Bluetoothi lahti ühendamisel
@@ -401,7 +399,6 @@
Saadete vahemäluSeadme puhvrisse allalaaditud saadete koguarv. Automaatne allalaadimine peatub, kui selle numbrini jõutakse.Kasuta saate kaanepilti
- Kasuta saate kaanepilti alati, kui see on olemas. Kui see pole märgitud, kasutab äpp alati taskuhäälingu kaanepilti.Kasuta süsteemi kujundustHeleTume
@@ -421,8 +418,6 @@
Nõua täielikku sünkroonimistSünkroniseeri kõiki tellimusi ja saate olekuid gpodder.net-iga.%1$s seadmega %2$s]]>
- Sünkroniseerimine ebaõnnestus
- See seadistus ei rakendu autentimise vigadele.Kohanda kiiruseid, mis on esitamisel saadavalMillise kiirusega esitatakse selle tellimuse saadete heliAutomaatne vahelejätmine
@@ -437,8 +432,6 @@
Määra, mitu sekundit edasi hüpatakse, kui vajutatakse edasi kerimise nuppuTagasi kerimise hüpeMäära, mitu sekundit tagasi hüpatakse, kui vajutatakse tagasi kerimise nuppu
- Määra hostinimi
- Kasuta vaikimisi hostiKõrge teate prioriteetSee tavaliselt kuvab teadet laiemana ning näha on esitusnupud.Püsivad taasesitamise nupud
@@ -449,10 +442,6 @@
Maksimaalselt saab valida %1$d kirjet.Määra lukustusekraani taustapiltMäära lukuekraani taustaks selle saate pilt. Kõrvalmõjuna kuvab see pilti ka teistes rakendustes.
- Allalaadimine ebaõnnestus
- Kui allalaadimised nurjuvad, genereeri raport, mis kuvab vea üksikasju.
- Automaatne allalaadimine on lõpetatud
- Teate kuvamine automaatselt allalaaditud saadete kohta.Vanemad Androidi versioonid kui 4.1 ei toeta laiendatud teavitusi.Järjekorra asukohtSaated lisatakse: %1$s
@@ -473,14 +462,12 @@
Praegune väärtus: %1$sVaheserverMäära võrgu vaheserver
- Korduma kippuvad küsimusedVeebilehitsejat ei leitud.Chromecasti tugiLuba meedia esitamine kaugseadmetest (Chromecast, kõlarid või Android TV)Chromecast vajab kolmanda osapoole omanduslikke teeke, mis on selles AntennaPodi versioonis välja lülitatudJärjekord allalaaditudAllalaaditud saadete lisamine järjekorda
- Sisseehitatud Androidi esitajaExoPlayer (soovitatud)Vaheta ExoPlayerileVahetati ExoPlayerile.
@@ -596,22 +583,12 @@
SOOVITUSEDOtsi gpodder.net-istLogi sisse
- Tere tulemast gpodder.net-i sisse logima. Kõigepealt sisesta sisselogimise andmed:Logi sisse
- Kui sul pole veel kontot, siis sa saad selle endale registreerida siin:\nhttps://gpodder.net/register/KasutajanimiParool
- Seadme valimineLoo oma gpodder.net konto jaoks uus seade või vali olemasolev:
- Seadme ID:\u0020
- Pealkiri
- Loo uus seade
- Vali olemasolev seade:
- Seadme ID ei tohi olla tühi
- Seadme ID on juba kasutusesPealkiri ei tohi olla tühiVali
- Sisse logitud!Palju õnne! Sinu gpodder.net konto on nüüd lingitud sinu seadmega. AntennaPod süngib nüüdsest tellimused sinu seadmes gpodder.net-i kontoga.Alusta kohe sünkroonimistMine peaekraanile
@@ -805,11 +782,7 @@
Näidatakse allalaadimise ajal.Praegu esitatakseVõimaldab esitust juhtida. See on saate kuulamise ajal peamine teade.
- Vead
- Näita, kui midagi läks valest. Näiteks, kui allalaadimine või uudivoo uuendamine ebaõnnestus.
- Sünkroniseerimise tõrkedNäidatakse, kui gpodder sünkroniseerimine ebaõnnestub.
- Automaatsed allalaadimisedNäita, kui saateid laaditi automaatselt alla.Vidina seaded
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 @@
EstatistikakGehitu podcastaSaioak
+ IlaraDenakBerriaGogokoak
@@ -26,6 +27,8 @@
ErreprodukzioaDeskargakJakinarazpenak
+
+ \"%1$s\" ezin da aurkituIkusitako saio denen denbora:%1$d kanpo %2$d hasitako saioetatik. %3$s \n\nErreproduzituak %4$setatik.
@@ -53,6 +56,8 @@
Bat ere ezEz dago app bateragarririk
+ Esportatu log zehatzak
+ Erregistro zehatzek informazio sentikorra izan dezakete, hala nola zure harpidetza-zerrendak.Nabigatzailean irekiURLa kopiatu
@@ -81,7 +86,6 @@
Deskribapena\u0020saioProzesatzen
- Gorde erabiltzailea eta pasahitzaItxiSaiatu berriroDeskarga automatikoetan sartu
@@ -93,12 +97,13 @@
ItzalitaGozoaIndartsua
- \u0020deskarga paraleloak
+ %1$d deskarga paraleloakGlobala aurrez zehaztuaBetiInoiz ezBidali...Inoiz ez
+ Gogoko ez deneanIlaran ez dagoeneanBukatu ondoren
@@ -113,7 +118,22 @@
%d aukeratua%d aukeratua
+
+ %d saio
+ %d saioak
+ Kargatzen...
+ Saio jakinarazpenak
+ Erakutsi jakinarazpena saio berri bat kaleratzen denean.
+
+ %2$s (e)k saio berri bat du
+ %2$s (e)k %1$d saio berri ditu
+
+
+ Saio berria
+ Saio berriak
+
+ Zure harpidetzak saio berriak ditu.Markatu denak ikusita bezalaMarkatu saio denak ikusita bezala
@@ -145,7 +165,6 @@
Ez dago ilaranMedia duIragaziak
- {fa-exclamation-circle} Errorea azken eguneraketanIreki podcastaMesedez, itxaron datuak kargatu arte
@@ -160,6 +179,10 @@
EzabatuEzin da fitxategia ezabatu. Gailua berrabiarazteak lagun dezake.Saioa ezabatu
+
+ %dsaio aukeratuta, jaitsiera %d ezabatuta.
+ %d saio aukeratuta, %d jaitsiera ezabatuta.
+ Kendu \"berria\" ikurra\"Berria\" ikurra kendu daMarkatu ikusita bezala
@@ -206,16 +229,12 @@
Xehetasunak%1$s \n\nartxibategiaren URL:\n%2$sEz da biltegiratze gailurik aurkitu
- Ez dago nahiko tokirikHTTP datuen erroreaErrore ezezaguna
- Analizatzailearen salbuespenaKanal mota ez onartuaKonexio errorea
- Ostalari ezezagunaEgiaztatze erroreaArtxibategi motaren errorea
- DebekaturikDeskarga ezeztatuaDeskarga ezeztatua\aktibatu da Auto deskarga elementu honetanDeskarga(k) osatu d(ir)a errorea(k) d(it)uela
@@ -229,12 +248,7 @@
%d deskarga zain%d deskarga zain
- Deskargak prozesatzenPodcastaren datuak deskargatzen
-
- Deskarga arrakastatsu %d , %d (e)k huts egin du(te)
- %d deskarga arrakastasuak, %d (e)k huts egin du(te)
- Izenburu ezezagunaKanalaMedia artxibategia
@@ -267,6 +281,7 @@
Picture-in-picture moduaAntennaPod - Media tekla ezezaguna: %1$dEz da artxibategirik aurkitu
+ Elementuak ez du multimedia fitxategirikBlokeatu ilaraDesblokeatu ilara
@@ -323,7 +338,6 @@
BiltegiaEzabatze automatikoa, jaso, bidaliProiektua
- IlaraSinkronizazioaSinkronizatzen gpodder.net erabiltzen duten beste gailu batzuekinAutomatizazioa
@@ -334,19 +348,24 @@
Kanpo elementuakEtenaldiakErreprodukzioaren kontrola
+ Birjarri hardware botoiakBilatu...Emaitzarik ezHistoria ezabatuMedia erreproduzigailuaSaioen garbitzea
- Ilaran edo gogokoetan ez dauden gertakariak ezabatu egin daitezke deskarga automatikoak gertakari berrietarako lekua behar badu.
+ Deskarga automatikoak saio berrietarako lekua behar badu kentzeko eskubidea izan beharko luketen saioakErreprodukzioa gelditu entzungailu edo bluetootha kentzeanErreprodukzioa jarraitu entzungailu edo bluettota berriz konektatzeanErreprodukzioa jarraitu bluetootha berriz konektatzean
- Aurrera botoia: jauzi
- Bluetooth bidez konektatutako gailuan aurrera botoia sakatzean, hurrengo saiora egingo du jauzi aurrera egin beharrean
- Atzera botoia: Berrabiarazi
- Atzera botoia sakatzean saioa berriz hasiko da atzera egin beharrean
+ Aurrera botoia
+ Pertsonalizatu aurrera botoiaren portaera
+ Atzera botoia
+ Pertsonalizatu atzera botoiaren portaera
+ Aurrera egin
+ Atzera egin
+ Baztertu saioa
+ Berrekarri saioaErreprodukzioa amaitzean ilarako hurrengo elementura jauziSaioa ezabatu erreprodukzioa amaitzeanAutomatikoki ezabatu
@@ -362,12 +381,15 @@
Eguneratze tartea, deskarga kontrolak, mugikorraren datuakEguneratzeko tartea edo orduaZehaztu tarte bat edo eguneko ordu jakin bat podcastak automatikoki freskatzeko
- Zuk ahal duzu tartea gustuko \"2orduro\" eguneko ordua \"7:00 AM\" adibidez edo desgaitu eguneraketa automatikoak\n\nOharra: Eguneraketa orduak ez dira zehatzak. Atzerapen txiki bat eman daiteke.
+ Zuk ahal duzu tartea gustuko \"2orduro\" ezarri, eta eguneko ordua \"7:00 AM\" adibidez edo desgaitu eguneraketa automatikoak\n\nOharra: Eguneraketa orduak ez dira zehatzak. Atzerapen txiki bat eman daiteke.DesgaituZehaztu tarteaZehaztu eguneko ordua
- %1$sdenak%1$setan
+
+ Orduro
+ %d orduro
+ Etengabeko erreprodukzioaEntzungailu edo Bluetootharen deskonexioaBirkonektatu entzungailuez
@@ -401,7 +423,9 @@
Saioak gordetzeaGailuan katxeatutako saioen zenbatekoa. Deskarga automatikoa bertan behera utziko da zenbaki honetara heltzean.Saioaren azala erabili
- Erabili atalaren azala ahal denean. Desaktibatzen bada, aplikazioak podcastaren azala erabiliko du beti.
+ Erabili atalaren azal espezifikoa zerrendetan eskuragarri dagoenean. Desaktibatzen bada, aplikazioak podcastaren azala erabiliko du beti.
+ Erakutsi geratzen den denbora
+ Erakutsi saioen gelditzen denbora markatuta daudenean. Markatu gabe badaude, erakutsi saioen iraupen osoa.Sistemaren gaia erabiliArgiaIluna
@@ -421,8 +445,6 @@
Sinkronizazio osoa behartuSinkronizatu harpidetza denak eta saioak gpodder.net-ekin%1$s gailu honekin %2$s]]>
- Akatsa sinkronizazioan
- Ezarpen honek ez du eraginik saio hasierako erroretan eraginik.Pertsonalizatu abiadura aldakorreko erreprodukzioan dauden abiadurakPodcast hauen berezko irakurtze abiaduraSalto automatikoa
@@ -439,8 +461,6 @@
Atzera egiteko tarteaPertsonalizatu zenbat segundu egingo duen atzera atzera botoia sakatzean
- Ezarri gailuaren izena
- Erabili lehenetsitako izenaJakinarazpenen lehentasunaHonek jakinarazpenak zabaltzen ditu erreprodukzio botoiak erakustekoErreprodukzio kontrol iraunkorrak
@@ -451,10 +471,6 @@
Ezingo duzu %1$d elementu baino gehiago aukeratu.Ezarri blokeo pantailaren atzealdeaEzarri saioaren irudia blokeo pantailarako atzealdea moduan. Horren eraginez, hirugarrenen aplikazio irudiak ere azalduko dira.
- Deskargak huts egin du
- Deskargak huts egiten badu, sortu txostena akatsaren xehetasunekin.
- Deskarga automatikoak osatuta
- Erakutsi automatikoki deskargatutako saioen jakinarazpen bat.Android 4.1 aurreko bertsioek ez dituzte zabaldutako jakinarazpenak jasatenIlaran gehitu kokalekuanSaioak hemen gehitu: %1$s
@@ -464,6 +480,7 @@
DesgaituaIrudiak biltegiratzeko tamainaDiskoko irudien biltegiratze tamaina
+ Dokumentazioa & LaguntzaErabiltzaileen foroaErrorearen berri emanErroreen bilatzailea irekirik
@@ -475,14 +492,14 @@
Egungo balioa: %1$sProxyKonfiguratu sareko proxya
- Ohiko galderakEz da web nabigatzailea aurkituChromecasterako euskarriaGaitu Cast gailuetan urrutira erreproduzitzeko euskarria (chromecast, altabozak edo Android TB modukoak)Chromecastek AntennaPod bertsio honetan desgaiturik dauden hirugarrenen liburutegiak behar dituGehitu deskargatutakoak ilararaGehitu deskargatutako saioak ilarara
- Integratutako Android erreproduzitzailea
+ Integratutako Android erreproduzigailua (zaharkituta)
+ Sonic Media Player (zaharkituta) ExoPlayer (gomendatua)Aldatu ExoPlayer-eraExoPlayer-era aldatu da.
@@ -571,6 +588,7 @@
Ezarri tenporizadore batDesgaitu tenporizadorea
+ +%d minutuTenporizadoreaBaliogabeko sarrera, denbora zenbaki osoa izan behar duAstindu berrezarteko
@@ -598,22 +616,22 @@
IRADOKIZUNAKBilatu gpodder.net-enHasi saioa
- Ongi etorri gpodder.net saio hasierara. Hasteko zure saio hasierako datuak sartu:Hasi saioa
- Oraindik konturik ez baduzu hemen sortu dezakezu:\nhttps://gpodder.net/register/
+ Kontua sortuErabioltzaileaPasahitza
- Gailua aukeratzea
+ Gpodder.net AntennaPod proiektutik independentea den sinkronizazio zerbitzu bat da.
+ gpodder.net zerbitzari ofiziala
+ Zerbitzari pertsonalizatua
+ Ostalari izena
+ Aukeratu zerbitzariaSortu gailu bat zure gpodder.net kontua erabiltzeko edo dagoenetako bat aukeratu:
- Gailuaren ID:\u0020
- Deskribapena
- Sortu gailu berria
- Dagoen gailuetako bat aukeratu:
- Gailuaren ID ezin du hutsik egon
- Gailuaren ID dagoeneko erabiltzen da
+ Gailuaren izena
+ AntennaPod aktibatuta %1$sTestuak ezin du hutsik egon
+ Dauden gailuak
+ Sortu gailuaAukeratu
- Saioa ondo hasi duzu!Zorionak! Zure gpodder.net kontua zure gailuarekin lotuta dago orain. Hemendik aurrera, AntennaPodek automatikoki sinkronizatuko ditu zure gailuaren harpidetzak bzure gpodder.net kontuarekin.Hasi sinkronizazioa orainJoan pantaila nagusira
@@ -648,7 +666,7 @@
Berrekin dei baten ondorenBeharrezkoa da AntennaPod berrabiaraztea aldaketak gauzatzeko.
- Eman izena
+ HarpidetuHarpidetzen...Aurretiko ikuspegiaGelditu aurretiko ikuspegia
@@ -667,6 +685,7 @@
Aldatu orriakKokalekua: %1$sAplikatu ekintza
+ Kapitulua abiaraziEgiaztatzeaAldatu podcast honen eta bere saioen erabiltzaile izena eta pasahitza.
@@ -801,18 +820,22 @@
Erreproduzigailuak akats larria aurkitu duErrorea medioa erreproduzitzean. Jauzi egiten …
+ Erroreak
+ BerriakBeharrezko ekintzaZure ekintza beharrezkoa den erakusten da, adibidez, pasahitz bat sartu behar duzun edo ez.DeskargatzenDeskargatu bitartean erakusten da.ErreproduzitzenErreprodukzioa kontrolatzeko aukera ematen du. Podcast bat erreproduzitzen den bitartean ikusten den jakinarazpen nagusia da.
- Erroreak
- Zerbait gaizki irten bada erakusten du, adibidez, jeitsi edo feed-eguneraketak huts egiten badute.
- Akatsak sinkronizazioan
+ Deskargak huts egin du
+ Deskargak edo jarioen eguneratzeak huts egiten duenean erakusten da.
+ Sinkronizazioak huts egin duErakutsi gpoder -en sinkronizazioak huts egitean.
- Deskarga automatikoak
+ Deskarga automatikoak osatutaPasarteak automatikoki deskargatu direnean erakusten da.
+ Saio berria
+ Saio berriak eskuragarri daudenean eta jakinarazpenak aktibatu direnean erakusten daWidget ezarpenakwidget-a sortu
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 @@
آمارافزودن پادکستقسمتها
+ صفهمهجدیدمحبوبها
@@ -17,7 +18,6 @@
گزارشاشتراکهافهرست اشتراکها
- لغو\nبارگیریتاریخچه پخشgpodder.netgpodder.net ورود
@@ -26,6 +26,7 @@
پخشبارگیریهااعلان ها
+
مجموع زمان پادکستهای پخششده:%1$d از %2$d قسمتها شروع شده است.\n\n%3$s از %4$s پخش.
@@ -81,7 +82,6 @@
شرح\u0020قسمتدر حال پردازش
- ذخیرهٔ نام کاربری و رمز عبوربستنتلاش مجددشامل بارگیری خودکار شود
@@ -93,7 +93,6 @@
خاموشسبکسنگین
- \u0020بارگیری همزمانپیشفرض جهانیهمیشههیچگاه
@@ -145,7 +144,6 @@
خارج از صفدارای رسانهفیلتر شده
- {fa-exclamation-circle} آخرین تازهسازی ناموفق بودباز کردن پادکستلطفا تا زمان دریافت اطلاعات منتظر بمانید
@@ -206,16 +204,12 @@
جزئیات%1$s\n\nنشانی پرونده:\n%2$sحافظهٔ خارجی یافت نشد
- فضای ناکافیخطای HTTP Data خطای ناشناخته
- خطای تجزیه عدم پشتیبانی از این نوع خوراکخطای اتصال
- میزبان ناشناسخطای احراز هویتخطای نوع پرونده
- ممنوعبارگیری لغو شدبارگیری لغو شد\nغیر فعال کردن بارگیری خودکار برای این مورد بارگیریها با خطا(ها) کامل شد
@@ -229,7 +223,6 @@
%d بارگیری باقی مانده است%d بارگیری باقی مانده است
- پردازش بارگیریهابارگیری داده پادکستعنوان ناشناختهخوراک
@@ -319,7 +312,6 @@
حافظهحذف خودکار قسمت، درونریزی، برونریزیپروژه
- صفهمگامسازیبا کمک gpodder.net با دستگاههای دیگر همگام کنیداتوماسیون
@@ -335,14 +327,9 @@
پاک کردن تاریخچهپخشکننده رسانهپاکسازی قسمت
- قسمتهایی که در صف موجود نیستند و در مورد علاقه هم نیستند باید واجد شرایط برای حذف باشد اگر بارگیری خودکار فضای بیشتری برای قسمتها جدید میخواهدمتوقفسازی پخش هنگامی که هدفنها یا بلوتوث قطع شودادامه پخش وقتی که هدفنها دوباره متصل شودادامه پخش هنگامی که بلوتوث دوباره متصل شود
- دکمه پرشهای به جلو
- هنگام فشار دادن دکمه جلو بر روی دستگاه متصل به بلوتوث ، به جای فوروارد ، به قسمت بعدی بروید
- دکمه «قبلی» دوباره راه اندازی شد
- هنگام فشار دادن دکمه فیزیکی «قبلی» ، قسمت فعلی را دوباره شروع کنید بجای عقب رفتنپس از پایان پخش ، به مورد صف بعدی برویدبا پایان پخش ، قسمت حذف شودحذف خودکار
@@ -362,7 +349,6 @@
از کار انداختنتنظیم بازهتنظیم زمان روز
- هر %1$sدر %1$sپخش مداومقطع اتصال هدفون یا بلوتوث
@@ -397,7 +383,6 @@
قسمت های ذخیره شدهتعداد کل قسمتهای قابل بارگیری در ذخیره گاه دستگاه. در صورت رسیدن به این تعداد بارگیری خودکار به حالت تعلیق در می آید.استفاده از عکس قسمت
- هر زمان که جلد مخصوص قسمت در دسترس بود از آن استفاده کن. در صورت لغو انتخاب ، برنامه همیشه از تصویر جلد پادکست استفاده می کند.از طرح زمینه سیستم استفاده کنیدروشنتاریک
@@ -417,8 +402,6 @@
همگام سازی کامل را اجبار کنهمه اشتراک ها و حالت های قسمت را با gpodder.net همگام سازی کنید.%1$s با وسیله %2$s]]>
- همگام سازی انجام نشد
- این تنظیمات در مورد خطاهای احراز هویت اعمال نمی شود.سرعتهای موجود برای بازپخش با سرعت متغیر را سفارشی کنیدسرعت شروع بازپخش صدا برای قسمت های این پادکستپرش خودکار
@@ -433,8 +416,6 @@
با کلیک بر روی دکمه سریع جلو ، تعداد ثانیه ها را برای پرش به جلو تنظیم کنیدزمان پرش برای عقب رفتن سریعبا کلیک روی دکمه برگشت تعداد ثانیه ها را برای پرش به عقب تنظیم کنید
- تنظیم نام میزبان
- استفاده از میزبان پیش فرضاعلان با الویت بالااین معمولاً اعلان را برای نمایش دکمه های پخش گسترش می دهد.کنترلهای پخش مداوم
@@ -445,10 +426,6 @@
شما فقط می توانید حداکثر %1$d مورد را انتخاب کنید.تنظیم پسزمینه صفحهٔ قفلپس زمینه صفحه قفل را روی تصویر جلد قسمت فعلی تنظیم کن. به عنوان اثر جانبی ، این تصویر در برنامه های شخص ثالث نیز نشان داده خواهد شد.
- دانلود ناموفق
- در صورت شکست در بارگیری، گزارشی تولید شود که جزئیات شکست را نشان دهد.
- بارگیری خودکار انجام شد
- اعلانی را برای قسمتهای بارگیری خودکار نشان دهید.اندرویدهای قدیمیتر از نسخه ۴٫۱ از اعلانهای بسطیافته پشتیبانی نمیکنند.مکان را از صف در آورقسمتها را به اضافه کنید به : %1$s
@@ -469,14 +446,12 @@
مقدار فعلی: %1$sپروکسیتنظیم پروکسی شبکه
- سوالات متداولمرورگر وب پیدا نشد.پشتیبانی از کرومکستپشتیبانی از پخش از راه دور رسانه را در دستگاه های Cast فعال کنید (مانند Chromecast ، بلندگوهای صوتی یا Android TV)کرومکست نیازمند کتابخانههای غیرآزاد است که در این نسخه از AntennaPod غیر فعال هستنددر صف نهادن بارگیریشدههاقسمتهای بارگیری شده را به صف اضافه کنید
- پخشکننده پیشفرض اندرویدExoPlayer (توصیه می شود)به ExoPlayer برویدبه ExoPlayer تغییر وضعیت داده شد.
@@ -592,22 +567,12 @@
پیشنهادهاجستوجوی gpodder.netورود
- به فرایند ورود gpodder.net خوش آمدید. نحست اطّلاعات ورودتان را بنویسید:ورود
- اگر هنوز حسابی ندارید، میتوانید اینجا یکی بسازید:\https://gpodder.net/register/nنام کاربریگذرواژه
- گزینش افزارهبرای استفادهٔ حساب gpodder,netتان حسابی جدید ساخته یا حسابی موجود را برگزینید:
- شناسهٔ افزاره:\u0020
- عنوان
- ایجاد افزارهٔ جدید
- گزینش موجود
- شناسهٔ افزاره نباید خلی باشد
- شناسهٔ افزاره از پیش در حال استفاده استعنوان نباید خالی باشدگزینش
- ورود موفق!تبریک می گویم! حساب gpodder.net شما اکنون با دستگاه شما پیوند داده شده است. آنتن پاد از این پس اشتراک های دستگاه شما را با حساب gpodder.net خود به طور خودکار همگام سازی می کند.شروع همگامسازیرفتن به صفحهٔ اصلی
@@ -801,11 +766,7 @@
هنگام بارگیری نشان داده می شود.در حال اجراامکان کنترل پخش را فراهم می کند. این اعلان اصلی است که هنگام پخش پادکست مشاهده می کنید.
- خطاها
- اگر مشکلی پیش آمد ، به عنوان مثال اگر در بارگیری یا به روزرسانی خبره مشکلی پیش آمد ، نشان داده می شود.
- خطاهای همگام سازیهنگامی که همگام سازی gpodder انجام نشد نشان داده می شود.
- بارگیریهای خودکاروقتی قسمتها به طور خودکار بارگیری می شوند ، نشان داده می شوند.تنظیمات ویجت
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 @@
TilastotLisää PodcastJaksot
+ JonoKaikkiUusiSuosikit
@@ -17,7 +18,6 @@
LokiTilauksetTilauslista
- Peruuta\nLatausSoittohistoriagpodder.netgpodder.net Kirjautuminen
@@ -25,6 +25,7 @@
Jaksojen välimuistin rajoitus on ylitetty. Voit lisätä välimuistin kokoa Asetuksissa.ToistoLataukset
+
%1$d jakso %2$d:sta aloitettu.\n\nSoitettu %3$s jaksoa %4$s:sta.Tilastointitila
@@ -75,7 +76,6 @@
Kuvaus\u0020jaksoaProsessoi
- Tallenna käyttäjätunnus ja salasanaSuljeYritä uudelleenLataa automaattisesti
@@ -87,7 +87,6 @@
Pois käytöstäKevytVoimakas
- \u0020yhtäaikaiset latauksetGlobaali oletusAinaEi koskaan
@@ -134,7 +133,6 @@
Ei jonossaSisältää mediaaSuodatettu
- {fa-exclamation-circle} Viimeisin päivitys epäonnistuiAvaa podcastOdota kunnes tiedot ovat ladattu
@@ -195,16 +193,12 @@
Tiedot%1$s \n\nTiedoston URL:\n%2$sTallennuslaitetta ei löytynyt
- Ei tarpeeksi tilaaHTTP Data virheTuntematon virhe
- JäsenninpoikkeusEi tuettu syötetyyppiYhteysvirhe
- Tuntematon isäntäTodentamisvirheTiedostotyyppivirhe
- Ei sallittuLataus peruutettuLataus peruutettu\nPoistettu Automaattinen lataus tälle tiedolle Lataukset valmistuivat virhe(id)en kanssa
@@ -218,7 +212,6 @@
%d lataus jäljellä%d latausta jäljellä
- Käsitellään latauksiaLadataan podcastin tietojaTuntematon otsikkoSyöte
@@ -304,7 +297,6 @@
TallennusJakson automaattinen poisto, tuonti, vientiProjekti
- JonoSynkronointiSynkronoi muiden laitteiden kanssa käyttäen gpodder.net-palveluaAutomaatio
@@ -320,14 +312,9 @@
Tyhjennä historiaMediasoitinJakson siivous
- Jaksot, jotka eivät ole jonossa ja eivät ole suosikkeja tulisi olla valmiita poistoon jos Automaattiinen lataus tarvitsee tilaa uusille jaksoillePysäytä soitto kun kuulokkeet tai bluetooth katkaistaanJatka soittoa kun kuulokkeet yhdistetään uudestaanJatka soittoa kun bluetooth yhdistyy uudestaan
- Seuraava nappi ohittaa
- Kun painetaan seuraava-nappia bluetooth-laitteessa, hyppää seuraavaan jaksoon eteenpäin siirtymisen sijasta.
- Edellinen-nappi aloittaa alusta
- Kun painetaan edellinen-nappia, aloita nykyinen jakso alusta taaksepäin siirtymisen sijastaHyppää seuraavaan jonossa kun soitto valmistuuPoista jakso toiston loputtuaAutomaattinen poisto
@@ -347,7 +334,6 @@
Poista käytöstäAseta aikaväliAseta ajankohta
- joka %1$saika %1$sJatkuva toistoKuulokkeiden uudelleenyhdistyminen
@@ -380,7 +366,6 @@
Jaksojen välimuistiLadattuja jaksoja yhteensä välimuistissa tällä laitteella. Automaattinen lataaminen pysäytetään, jos tämä raja ylittyy.Käytä jakson kansikuvaa
- Käytä jaksokohtaista kansikuvaa, kun se on saatavilla. Jos tämä ei ole valittuna, sovellus käyttää aina podcastin kansikuvaa.VaaleaTummaMusta (AMOLED valmis)
@@ -397,15 +382,12 @@
Synkronoi tilaukset ja jaksojen tilojen muutokset gpodder.net.Synkronoi kaikki tilaukset ja jaksojen tilastot gpodder.net%1$s laitteella %2$s]]>
- Tämä asetus ei vaikuta autentikointivirheisiin.Mukauta mediatietoja soiton nopeuteenNäytetty aika ja kesto mukautuvat soiton nopeuteenSeuraava skippaa aikaaKustomoi sekunnit jonka verran skipataaan kun painat Seuraava nappia.Edellinen skippaa aikaakustomoi sekunnit jonka verran hypätään takaisin kun painat Edellinen nappia.
- Aseta hostname
- Käytä oletusporttiaKorkea ilmoitusprioriteettiTämä laajentaa ilmoituksen näyttämään soittonapitPysyvät soittokontrollit
@@ -414,7 +396,6 @@
Voit valita vain maksimissaan %1$d asioita.Aseta lukitusruudun taustakuvaAseta lukitusruudun taustakuva nykyisen jakson kuvaan. Tämä kuva näkyy kolmannen osapuolen sovelluksissa.
- Jos lataus epäonnistuu, generoi raportti joka näyttää virheen tiedot.Android versiot ennen 4.1 eivät tue laajenettuja ilmoituksia.Poissa käytöstäKuvan välimuistin koko
@@ -430,7 +411,6 @@
Chromecast tarvitsee kolmannen osapuolen suljettuja kirjastoja, jotka on poistettu tässä AntennaPodissa.Lisää ladatut jonoonLisää ladatut jaksot jonoon
- Sisäänrakennettu Android-soittoSkippaa tyhjät kohdat audiossaVideo sulkiessaKun suljetaan videon soitto
@@ -497,22 +477,12 @@
EHDOTUKSETEtsi gpodder.netSisäänkirjautuminen
- Tervetuloa gpodder.net sisäänkirjautumiseen. Kirjoita kirjautumistiedot:Sisäänkirjautuminen
- Jos sinulla ei ole vielä tiliä, voit luoda sen täällä:\nhttps://gpodder.net/register/KäyttäjätunnusSalasana
- LaitevalintaLuo uusi laite gpodder.net tiliin tai valitse olemassaoleva laite:
- Laite ID:\u0020
- Kuvaus
- Luo uusi laite
- Valitse olemassaoleva laite:
- Laite ID ei saa olla tyhjä
- Laite ID on jo käytössäKuvaus ei saa olla tyhjäValitse
- Sisäänkirjautuminen onnistuiOnneksi olkoon! Sinun gpodder.net tili on liitetty laitteeseesi. AntennaPod alkaa automaattisesti synkronoimaan gpodder.net tiliisi.Aloita synkrointi nytMene pääsivulle
@@ -663,7 +633,6 @@
Näytetään kun ladataan.Soittaa nytSallii soiton kontrollit. Tämä on pääilmoitus, jonka näet kun podcast soitetaan.
- Virheet
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 @@
StatistiquesAjouter un podcastEpisodes
+ Liste de lectureTousNouveauxFavoris
@@ -26,6 +27,8 @@
LectureTéléchargementsNotifications
+
+ \"%1$s\" non trouvéDurée totale d\'écoute :%1$d épisodes démarrés sur %2$d\nsoit %3$s de lues sur %4$s.
@@ -53,6 +56,8 @@
AucunAucune application compatible trouvée
+ Export détaillé des logs
+ Les logs détaillés peuvent contenir des données sensibles, par exemple la liste des abonnementsOuvrir dans le navigateurCopier le lien
@@ -81,7 +86,6 @@
Description\u0020épisodesTraitement en cours
- Sauvegarder votre identifiant et votre mot de passeFermerRéessayerTélécharger automatiquement
@@ -93,13 +97,14 @@
aucunefaibleimportante
- \u0020téléchargements parallèles
+ %1$d téléchargements simultanésOption par défautToujoursJamaisEnvoyer...Jamais
- Quand l’épisode n\'est pas dans la liste de lecture
+ Quand pas un favori
+ Quand pas dans la liste de lectureAprès avoir terminé1 heure après avoir été écouté
@@ -113,7 +118,22 @@
%d sélectionné%d sélectionnés
+
+ %d épisode
+ %d épisodes
+ Chargement...
+ Notification des épisodes
+ Affiche une notification quand de nouveaux épisodes sont disponibles.
+
+ %2$s a un nouvel épisode
+ %2$s a %1$d nouveaux épisodes
+
+
+ Nouvel épisode
+ Nouveaux épisodes
+
+ Vos abonnements disposent de nouveaux épisodes.Marquer tous les épisodes comme lusTous les épisodes ont été marqués comme lus
@@ -145,7 +165,6 @@
Pas dans la liste de lectureAvec médiaFiltré
- {fa-exclamation-circle} La dernière mise à jour a échouéOuvrir le podcastMerci d\'attendre la fin du téléchargement des données
@@ -160,6 +179,10 @@
SupprimerSuppression du fichier impossible. Redémarrer pourrait aider.Suppression de l\'épisode
+
+ %d épisode sélectionné, %d téléchargement supprimé.
+ %d épisodes sélectionnés, %d téléchargement(s) supprimé(s).
+ Ne plus considérer nouveauLe statut \"nouveau\" a été suppriméMarquer comme lu
@@ -206,16 +229,12 @@
Détails%1$s \n\nLien du fichier :\n%2$sVolume de stockage non trouvé
- Espace insuffisantErreur de données HTTPErreur inconnue
- Message d\'erreurType de flux non géréErreur de connexion
- Hôte inconnuErreur d\'authentificationErreur format de fichier
- InterditTéléchargement annuléTéléchargement annulé\n Téléchargement Automatique désactivé pour cet élémentTéléchargements terminés avec des erreurs
@@ -229,12 +248,7 @@
%d téléchargement restant%d téléchargements restants
- Traitement des téléchargementsTéléchargement des données du podcast
-
- %1$d téléchargement réussi, %2$d échoué
- %1$d téléchargements réussis, %2$d échoués
- Titre inconnuFluxFichier média
@@ -267,6 +281,7 @@
Mode Picture-in-PictureAntennaPod - Touche média inconnue : %1$dFichier non trouvé
+ Aucun fichier média disponibleVerrouiller la liste de lectureDéverrouiller la liste de lecture
@@ -323,7 +338,6 @@
StockageSuppression automatique, importation, exportationProjet
- Liste de lectureSynchronisationUtiliser gpodder.net pour synchroniser avec d\'autres appareilsAutomatisation
@@ -334,19 +348,24 @@
Eléments externesInterruptionsContrôle de lecture
+ Réaffectation des boutonsChercher...Aucun résultatEffacer l\'historiqueLecteur multimédiaNettoyage des épisodes
- 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
+ Episodes pouvant être supprimés si le téléchargement automatique a besoin de plus de place.Interrompre la lecture quand les écouteurs ou le bluetooth sont déconnectésReprendre la lecture quand des écouteurs sont branchésReprendre la lecture quand le Bluetooth se reconnecte
- Le bouton \"saut avant\" saute l\'épisode
- Passer à l\'épisode suivant au lieu de faire un saut avant quand \"saut avant\" est pressé sur un périphérique bluetooth
- Le bouton \"saut arrière\" redémarre l\'épisode
- Repartir de zéro au lieu de faire un saut arrière quand un bouton physique \"saut arrière\" est pressé
+ Bouton Episode Suivant
+ Définir le comportement du bouton \"épisode suivant\"
+ Bouton Episode Précédent
+ Définir le comportement du bouton \"épisode précédent\"
+ Saut Avant
+ Saut Arrière
+ Sauter l\'épisode
+ Redémarrer l\'épisodeAprès la fin d\'un épisode, passer au suivantSupprimer l\'épisode quand la lecture est finieSuppression automatique
@@ -366,8 +385,11 @@
DésactiverIntervalleHeure
- toutes les %1$sà %1$s
+
+ Toutes les heures
+ Toutes les %d heures
+ Lecture continueDéconnexion des écouteurs ou du BluetoothConnexion des écouteurs
@@ -401,7 +423,9 @@
Nombre d\'épisodes stockésNombre maximum d\'épisodes stockés sur l\'appareil. Le téléchargement automatique sera suspendu si ce nombre est atteint.Image des épisodes
- Lorsqu\'elles existent, utiliser les images propres aux épisodes au lieu de celle du podcast.
+ 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é.
+ Afficher la durée restante
+ Pour les épisodes, montrer la durée restant à lire à la place de la durée totale.Thème du systèmeClairSombre
@@ -421,8 +445,6 @@
Forcer une synchronisation totaleSynchroniser tous les abonnements et tous les états des épisodes avec gpodder.net.%1$s avec l\'appareil %2$s]]>
- La synchronisation a échoué
- Ce paramètre ne s\'applique pas aux erreurs d\'authentification.Définir les vitesses disponibles lors de la lectureVitesse de lecture par défaut des épisodesSaut automatique
@@ -437,8 +459,6 @@
Nombre de secondes à sauter quand le bouton \"saut avant\" est presséDurée du saut arrièreNombre de secondes à sauter quand le bouton \"saut arrière\" est pressé
- Choisir un nom de domaine
- Utiliser le nom de domaine par défautPriorité haute de notificationPermet, généralement, d\'étendre la notification pour montrer les boutons de lectureBoutons de lecture permanents
@@ -449,10 +469,6 @@
Vous ne pouvez pas choisir plus de %1$d éléments.Changer l’arrière plan de l\'écran de verrouillagePlacer 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.
- Le téléchargement a échoué
- Si les téléchargements échouent, générer un rapport détaillé des échecs.
- Le téléchargement automatique est terminé
- Afficher une notification pour les épisodes téléchargés automatiquement.Les versions d\'Android antérieures à 4.1 ne sont pas compatibles avec les notifications élargiesEmplacement des épisodes téléchargésAjouter les épisodes : %1$s
@@ -462,6 +478,7 @@
DésactivéTaille du cache de l\'imageTaille de l’espace de stockage temporaire des images.
+ Documentation & SupportForum des utilisateursSignaler un bugOuvrir le suivi des bugs
@@ -473,15 +490,15 @@
Valeur actuelle : %1$sProxyParamétrer un réseau proxy
- Foire aux questionsAucun navigateur trouvé.Support ChromecastActiver la lecture à distance sur les appareils Cast (comme Chromecast, Audio Speaker ou Android TV)Chromecast nécessite des bibliothèques tierces qui sont désactivées dans cette version d\'AntennaPodAjouter à la liste après téléchargementMettre les épisodes dans la la liste de lecture après téléchargement
- Lecteur natif d\'Android
- ExoPlayer (recommendé)
+ Lecteur natif d\'Android (plus maintenu)
+ Sonic Media Player (plus maintenu)
+ ExoPlayer (recommandé)Utiliser ExoPlayer pour la lectureLecteur changé pour ExoPlayerSupprimer les silences audios
@@ -569,6 +586,7 @@
Activer le minuteurDésactiver le minuteur
+ +%d minMinuteur d\'arrêtEntrée invalide, la durée doit être un nombre entierSecouer pour redémarrer
@@ -596,22 +614,22 @@
SUGGESTIONSChercher sur gpodder.netSe connecter
- Bienvenue dans le processus de connexion à gpodder.net. Premièrement, veuillez entrer vos informations de connexion :Connexion
- SI vous n\'avez pas encore de compte, vous pouvez en créer un ici:\https://gpodder.net/register/
+ Créer un compteIdentifiantMot de passe
- Choix de l\'appareil
+ Gpodder.net est un service de synchronisation open-source indépendant du projet AntennaPod.
+ Serveur officiel de gpodder.net
+ Serveur personnalisé
+ Nom d\'hôte
+ Sélectionner le serveurCréer un nouvel appareil pour votre compte gpodder.net ou choisir un appareil existant :
- ID de l\'appareil :\u0020
- Légende
- Créer un nouvel appareil
- Choisir un appareil existant :
- L\'ID de l\'appareil ne peut pas être vide
- L\'ID de cet appareil est déjà en cours d\'utilisation
+ Nom de l\'appareil
+ AntennaPod sur %1$sLe nom ne peut pas être vide
+ Appareils déjà existants
+ Créer un appareilChoisir
- Connexion réussie !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.Commencer la synchronisationAller à l\'écran d\'accueil
@@ -665,6 +683,7 @@
Changer les pagesPosition : %1$sAppliquer l\'action
+ Lire le chapitreAuthentificationIdentifiant et mot de passe pour ce podcast.
@@ -679,7 +698,7 @@
Le téléchargement automatique n\'est pas activé dans les préférencesTemps d\'écoute :Episodes sur l\'appareil :
- Place utilisée :
+ Espace utilisé :Voir les statistiques pour tous les podcasts »Mise à jour de la base de données
@@ -719,8 +738,8 @@
TéléchargésEpisodes téléchargés sélectionnésNon téléchargés
- Épisodes non téléchargés sélectionnés
- Épisodes présents dans la liste de lecture sélectionnés
+ Episodes non téléchargés sélectionnés
+ Episodes présents dans la liste de lecture sélectionnésEpisodes absents de la liste de lecture sélectionnésSélectionner les épisodes avec des médiasEst un favori
@@ -799,18 +818,22 @@
Le lecteur de réception a rencontré une grave erreurErreur de lecture du média. Passage au suivant...
+ Erreurs
+ InformationsAction requiseS\'affiche si une action est requise. Par exemple, un mot de passe à saisir.Téléchargement en coursS\'affiche lorsqu\'un téléchargement est en cours.Lecture en coursPermet de contrôler la lecture. C\'est la notification principale pendant la lecture d\'un podcast.
- Erreurs
- S\'affiche quand quelque chose c\'est mal passé. Par exemple, un téléchargement ou une mise à jour de flux qui échoue.
- Erreurs de synchronisation
+ Erreur de téléchargement
+ S\'affiche quand un téléchargement ou la mise un jour d\'un abonnement échoue.
+ Echec de la synchronisationS\'affiche quand la synchronisation avec gpodder échoue.
- Téléchargement automatique
+ Fin du téléchargement automatiqueS\'affiche lorsque des épisodes ont été téléchargés automatiquement.
+ Nouveaux épisodes
+ S\'affiche quand de nouveaux épisodes sont disponible et que les notifications ont été activéesPréférences des widgetsCréer un widget
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 @@
EstatísticasEngadir PodcastEpisodios
+ ColaTodoNovoFavoritos
@@ -17,7 +18,7 @@
RexistroSubscriciónsLista de subscricións
- Cancelar\nDescarga
+ Cancelar descargaHistorial de reproducióngpodder.netgpodder.net Conexión
@@ -26,6 +27,8 @@
ReproduciónDescargasNotificacións
+
+ Non se atopa \"%1$s\"Duración total dos episodios reproducidos:%1$d de %2$d episodios iniciados.\n\nReproducidos %3$s de %4$s.
@@ -53,6 +56,8 @@
NingúnNon se atopan apps compatibles
+ Exportar rexistro detallado
+ O rexistro detallado podería conter información sensible, como a túa lista de subscriciónsAbrir no navegadorCopiar URL
@@ -81,7 +86,6 @@
Descrición\u0020episodiosProcesando
- Gardar nome de usuario e contrasinalPecharReintentarIncluír en descargas automáticas
@@ -93,12 +97,13 @@
ApagadoClaroForte
- \u0020descargas paralelas
- Valor xeral por omisión
+ %1$d descargas en paralelo
+ Valor xeral por defectoSempreNuncaEnviar...Nunca
+ Cando non favoritoCando non esté na colaTras rematar
@@ -113,7 +118,22 @@
%d seleccionado%d seleccionados
+
+ %d episodio
+ %d episodios
+ Cargando máis...
+ Notificación de episodios
+ Mostra unha notificación cando se publica un novo episodio.
+
+ %2$s ten un novo episodio
+ %2$s ten %1$d novos episodios
+
+
+ Novo episodio
+ Novos episodios
+
+ As túas subscricións teñen novos episodios.Marcar todo como reproducidoMarcáronse todos como reproducidos
@@ -145,7 +165,6 @@
Fora da colaTen multimediaFiltrado
- {fa-exclamation-circle} Erro na última actualizaciónAbrir podcastAgarda ata que se carguen os datos
@@ -160,6 +179,10 @@
BorrarNon se puido eliminar o ficheiro. Reiniciar o dispositivo podería axudar.Eliminar episodio
+
+ %d episodio seleccionado, %d descarga elimnada.
+ %d episodios seleccionados, %d descarga(s) eliminadas.
+ Quitar marca \"novo\"Eliminouse marca \"novo\"Marcar como reproducido
@@ -206,16 +229,12 @@
Detalles%1$s\n\nURL do ficheiro:\n %2$s Non se atopou dispositivo de almacenamento
- Non hai suficiente espacioFallo de datos HTTPFallo descoñecido
- Excepción no procesadorTipo de fonte non admitidaFallo na conexión
- Servidor descoñecidoFallo na autenticaciónFallo no tipo de ficheiro
- Non admitidoDescarga canceladaDescarga cancelada\nDesactivouse a Descarga Automática para este elementoDescargas completadas con erro(s)
@@ -229,12 +248,7 @@
%d descarga restante%d descargas restantes
- Procesando as descargasDescargando datos do podcast
-
- %d descarga correcta, %d fallou
- %d descargas correctas, %d fallaron
- Título descoñecidoFonteFicheiro de medios
@@ -267,6 +281,7 @@
Modo imaxe-en-imaxeAntennaPod - chave de medios descoñecida: %1$dNon se atopa o ficheiro
+ O elemento non conten un ficheiro multimediaBloquear a colaDesbloquear a cola
@@ -323,7 +338,6 @@
AlmacenamentoBorrado automático do episodio, Importar, ExportarProxecto
- ColaSincronizaciónSincronizar con outros dispositivos usando gpodder.netAutomatizado
@@ -334,19 +348,24 @@
Elementos externosInterrupciónsControl de reprodución
+ Reasignar botóns físicosBusca....Sen resultadosLimpar historialReprodutor de mediosLimpeza de episodios
- 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.
+ Episodios que se poden eliminar se a Descarga Automática precisa espazo para novos episodiosDeter a reprodución cando se desconectan os auriculares ou bluetoothRetomar a reprodución cando se conectan os auricularesRetomar a reprodución cando se reconecta o bluetooth
- O botón Adiante salta
- A premer no botón de adiante nun dispositivo conectado por bluetooth ir ao episodio seguinte no lugar de avance rápido
- O botón Anterior reinicia
- Cando se presiona Anterior no dispositivo reinicia o episodio no lugar de ir cara atrás
+ Botón Seguinte
+ Personalizar o comportamento do botón Seguinte
+ Botón Anterior
+ Personalizar o comportamento do botón Anterior
+ Avance rápido
+ Rebobinar
+ Saltar episodio
+ Reiniciar episodioSaltar ao seguinte elemento na cola cando remata o episodioEliminar o episodio cando remata a súa reproduciónBorrado automático
@@ -366,8 +385,11 @@
DesactivarEstablecer intervaloEstablecer hora do día
- cada %1$sas %1$s
+
+ Cada hora
+ Cada %d horas
+ Reprodución continuaAuriculares ou Bluetooth desconectadosReconexión de auriculares
@@ -401,7 +423,9 @@
Caché de episodiosO número total de episodios descargados na caché do dispositivo. A descarga automática suspenderase se se alcanza este número.Utilizar Portada do episodio
- Utilizar a portada específica do episodio cando sexa posible. Se non o marcas, a app mostrará sempre a portada do podcast.
+ 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.
+ Mostrar tempo restante
+ Se o seleccionas mostrará o tempo restante dos episodios. Se non, mostrará a duración total dos episodios.Utilizar o decorado do sistemaClaroOscuro
@@ -421,8 +445,6 @@
Forzar sincronización completaSincronizar todas as subscricións e estados de episodios con gpodder.net%1$s co dispositivo %2$s]]>
- Fallou a sincronización
- Esta preferencia non se aplica a fallos na autenticación.Personaliza as velocidades dispoñibles para reprodución de velocidade variableA velocidade para reproducir o contido dos episodios deste podcastSalto automático
@@ -437,8 +459,6 @@
Personaliza o número de segundos a avanzar cando o se preme o botón de avance rápidoRetroceso Salta tempoPersonaliza o número de segundos que se retrocede na reprodución cando se preme o botón retroceso
- Establecer servidor
- Utilizar servidor por omisiónAlta prioridade nas notificaciónsIsto expande as notificacións para mostrar os botóns de reproduciónControles persistentes de reprodución
@@ -449,10 +469,6 @@
Só podes selecionar un máximo de %1$d elementos.Establecer fondo da pantalla de bloqueoEstablecer o fondo de pantalla de bloqueo coa imaxe do episodio actual. Como consecuencia, esto tamén mostrará a imaxe en aplicacións de terceiros.
- Fallou a descarga
- Si falla a descarga, xerar un informe que informe dos detalles do fallo.
- Descarga automática completada
- Mostra unha notificación para os episodios descargados automáticamente.As versións de Android anteriores a 4.1 non teñen soporte para notificacións expandidas.Situación na colaEngadir episodios a: %1$s
@@ -462,6 +478,7 @@
DesactivadoTamaño da caché de imaxesTamaño da caché en disco para as imaxes.
+ Documentación & AxudaForo de usuariasInformar de falloAbrir seguimento de fallos
@@ -473,14 +490,14 @@
Valor actual: %1$sProxyEstablecer un proxy para a rede
- Preguntas Máis FrecuentesNon se atopou un navegador webSoporte ChromecastHabilitar o soporte de reprodución remota nun dispositivo Cast (como o Chromecast, Altofalantes ou Android TV)Chromecast precisa software propietario de terceiras partes que están desactivadas en esta versión de AntennaPodForon descargados os elementos da colaEngadir os episodios descargados a cola
- Reprodutor android nativo
+ Reprodutor incluído de Android (relegado)
+ Reprodutor Sonic Media (relegado)ExoPlayer (recomendado)Cambiar a ExoPlayerCambiaches a ExoPlayer.
@@ -569,6 +586,7 @@
Establecer apagado automáticoDesactivar o apagado automático
+ + %d minApagado automáticoEntrada incorrecta, o tempo ten que ser un número enteiroAxita para restablecer
@@ -596,22 +614,22 @@
SUXESTIÓNSBuscar en gpodder.netConexión
- Benvida ao proceso de conexión a gpodder.net. Primeiro, escriba os seus datos de conexión:Conexión
- Se aínda non tes unha conta, podes crear unha aquí:\nhttps://gpodder.net/register/
+ Crear contaNome de usuariaContrasinal
- Selección de dispositivo
+ Gpodder.net é un servizo de sincronización de podcast de código aberto que é independente do proxecto AntennaPod.
+ Servidor oficial gpodder.net
+ Servidor personalizado
+ Servidor
+ Elixe servidorCrear un novo dispositivo para usar coa túa conta gpodder.net ou escoller un existente:
- ID de dispositivo:\u0020
- Título
- Crear un novo dispositivo
- Escolle un novo dispositivo:
- O ID do dispositivo non pode quedar baleiro
- ID de dispositivo xa en uso
+ Nome do dispositivo
+ AntennaPod en %1$sO título non pode estar baleiro
+ Dispositivos existentes
+ Crear dispositivoEscoller
- Conexión correcta!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.netIniciar a sincronizaciónIr a pantalla principal
@@ -639,7 +657,7 @@
non se pode escribir en \"%1$s\"O cartafol non está baleiroO cartafol escollido non está baleiro. As descargas de medios e outros ficheiros situaranse directamente neste cartafol. Utilizalo igualmente?
- Escolle o cartafol por omisión
+ Escolle o cartafol por defectoPausar a reprodución en lugar de baixar o volume cando outra aplicación quere reproducir un son.Pausa para interrupciónsRetomar a reprodución despois de rematar a chamada telefónica
@@ -665,6 +683,7 @@
Cambiar de páxinaPosición: %1$sAplicar acción
+ Reproducir capítuloAutenticaciónCambiar o seu nome de usuaria e contrasinal para este podcast e os seus episodios.
@@ -799,18 +818,22 @@
O reprodutor receptor atopou un fallo graveFallo na reprodución de medios. Saltando...
+ Erros
+ NovasAcción requeridaMostrado si a súa acción é requerida, por exemplo si precisa introducir o contrasinal.DescargandoMostrado durante a descarga actual.Soando agoraPermite controlar a reprodución. Esta é a notificación principal que verá mentras reproduce un podcast.
- Fallos
- Mostrar se algo fallou, por exemplo se a descarga ou a actualización da fonte fallaron.
- Erros durante a sincronización
+ Fallou a descarga
+ Mostrado cando a descarga ou actualización da fonte fallou.
+ Fallou a sincronizaciónMostrado cando falla a sincronización con gpodder.
- Descargas Automáticas
+ Completouse a descarga automáticaMostrado cando os episodios se descargaron automáticamente.
+ Novo episodio
+ Móstrase cando se atopa un novo episodio dun podcast, se están activas as notificaciónsAxustes do WidgetCrear widget
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 @@
StatisztikaPodcast hozzáadásaEpizódok
+ Lejátszási sorMindÚjakKedvencek
@@ -17,7 +18,7 @@
NaplóFeliratkozásokFeliratkozások listája
- Letöltés\nmegszakítása
+ Letöltés megszakításaLejátszási naplógpodder.netgpodder.net bejelentkezés
@@ -26,6 +27,8 @@
LejátszásLetöltésekÉrtesítések
+
+ A(z) „%1$s” nem találhatóA lejátszott epizódok összideje:%1$d/%2$d epizód elindítva.\n\n%3$s/%4$s lejátszva.
@@ -79,7 +82,6 @@
Leírás\u0020epizódFeldolgozás
- Felhasználónév és jelszó mentéseBezárásÚjraHozzáadás az automatikus letöltésekhez
@@ -91,7 +93,6 @@
KiEnyheErős
- \u0020párhuzamos letöltésGlobális alapértelmezettMindigSoha
@@ -130,6 +131,7 @@
Megosztás…Fájl megosztásaWebcím
+ Podcast csatorna URLErő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).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.Podcast eltávolítása
@@ -142,7 +144,6 @@
Nem sorbaállítottMédiát tartalmazSzűrt
- {fa-exclamation-circle} A legutóbbi frissítés sikertelenPodcast megnyitásaVárjon az adatok betöltésére
@@ -203,16 +204,12 @@
Részletek%1$s \n\nFájl URL:\n%2$sTárolóeszköz nem található
- Túl kevés tárhelyHTTP adathibaIsmeretlen hiba
- Értelmező által dobott kivételNem támogatott csatornatípusKapcsolódási hiba
- Ismeretlen kiszolgálHitelesítési hibaFájltípus-hiba
- MegtiltvaLetöltés megszakítvaLetöltés megszakítva\nAutomatikus letöltése letiltva az elemnélA letöltések hibákkal fejeződtek be
@@ -226,12 +223,7 @@
%d letöltés van hátra%d letöltés van hátra
- Letöltések feldolgozásaPodcast adatok letöltése
-
- %d letöltés sikeres, %d sikertelen
- %d letöltés sikeres, %d sikertelen
- Ismeretlen címCsatornaMédiafájl
@@ -264,6 +256,7 @@
Kép a képben módAntennaPod – Ismeretlen médiabillentyű: %1$dA fájl nem találhat
+ Az elem nem tartalmaz médiafájltLejátszási sor zárolásaLejátszási sor feloldása
@@ -292,6 +285,8 @@
A bővítmény nincs telepítveA 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.Sonic engedélyezése
+ Előbeállítások
+ Már mentve lett előbeállításként: %1$.2fx.Nincsenek epizódok a sorbanAdjon 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.
@@ -318,7 +313,6 @@
TárolóEpizódok automatikus törlése, importálása, exportálásaProjekt
- Lejátszási sorSzinkronizálásSzinkronizáció más eszközökkel a gpodder.net segítségévelAutomatizálás
@@ -329,19 +323,24 @@
Külső elemekMegszakításokLejátszásvezérlés
+ Hardvergomb-társítások átrendezéseKeresés…Nincsenek találatokNapló törléseMédialejátszóEpizódok tisztítása
- 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.
+ Azon epizódok, melyek törölhetők, ha az Automatikus letöltésnek helyre van szüksége az új epizódok miatt.Lejátszás szüneteltetése fejhallgató vagy bluetooth leválasztásakorLejátszás folytatása a fejhallgatók újracsatlakoztatásakorLejátszás folytatása a bluetooth újracsatlakozásakor
- Az előre gomb átugorja
- Az bluetooth kapcsolaton csatlakozó eszköz előre gombjának megnyomásakor előretekerés helyett a következő számra ugrik
- Az előző gomb újraindítja
- A hardveres előző gomb megnyomásakor visszatekerés helyett újraindítja a jelenlegi epizódot
+ Tovább gomb
+ A tovább gomb viselkedésének testreszabása
+ Előző gomb
+ Az előző gomb viselkedésének testreszabása
+ Gyors előretekerés
+ Visszatekerés
+ Epizód kihagyása
+ Epizód újraindításaA lejátszás befejeztével ugrás a sor következő eleméreAz epizód törlése, ha a lejátszás véget értAutomatikus törlés
@@ -361,9 +360,13 @@
LetiltásIntervallum megadásaIdőpont megadása
- minden %1$sekkor: %1$s
+
+ Óránként
+ %d óránként
+ Folyamatos lejátszás
+ Fejhallgató vagy Bluetooth leválasztásaFejhallgató újracsatlakoztatásaBluetooth újracsatlakoztatásAdatátvitel előnyben részesítése
@@ -396,6 +399,8 @@
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.Epizód borítókép használataEpizó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.
+ Hátralévő idő megjelenítése
+ 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.Rendszertéma használataVilágosSötét
@@ -415,8 +420,6 @@
Teljes szinkronizáció kényszerítéseAz összes feliratkozásának és epizódállapotainak szinkronizálása a gpodder.nettel.%1$s, a(z) %2$s eszközzel]]>
- Szinkronizálás sikertelen
- Ez a beállítás a hitelesítési hibákra nem érvényes.A változó sebességű lejátszáshoz elérhető sebességek testreszabásaA podcast epizódjainak indításakor használandó lejátszási sebességAutomatikus kihagyás
@@ -431,20 +434,16 @@
Szabja testre, hogy hány másodperccel ugorjon előre az előretekerés gomb megnyomásakorVisszatekerés mértékeSzabja testre, hogy hány másodperccel ugorjon vissza a visszatekerés gomb megnyomásakor
- Gépnév megadása
- Alapértelmezett gépnév használataMagas értesítési prioritásEz általában kibővíti az értesítést, hogy megjelenítse a lejátszási gombokat.Állandó lejátszásvezérlőkAz értesítés és a zárképernyőn megjelenő vezérlők megtartása a lejátszás szüneteltetésekor.
+ Kompakt értesítési gombok beállítása
+ 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.Válasszon legfeljebb %1$d elemetLegfeljebb %1$d elemet választhat.Képernyőzár háttérkép beállításaA 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.
- Letöltés sikertelen
- Ha a letöltések sikertelenek, előállít egy jelentést, amely részletezi a hibát
- Elkészült az automatikus letöltés
- Értesítés megjelenítése az automatikusan letöltött epizódokhoz.A 4.1 előtti Android verziók nem támogatják a bővített értesítéseket.Hely sorbaállításaEpizódok hozzáadása ehhez: %1$s
@@ -454,6 +453,7 @@
LetiltvaKép gyorsítótár méreteKép gyorsítótár mérete a lemezen.
+ Dokumentációs és támogatásaFelhasználói fórumHibajelentésHibakövető megnyitása
@@ -465,14 +465,12 @@
Jelenlegi érték: %1$sProxyHálózati proxy beállítása
- Gyakran ismételt kérdésekNem található webböngésző.Chromecast támogatásA távoli médialejátszás engedélyezése a Cast eszközökön (mint a Chromecast, hangfalak vagy Android TV)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.Letöltött elemek sorbaállításaLetöltött epizódok sorhoz adása
- Beépített androidos lejátszóExoPlayer (javasolt)Váltás az ExoPlayerreÁtváltva az ExoPlayerre.
@@ -580,22 +578,12 @@
JAVASLATOKKeresés a gpodder.netenBejelentkezés
- Üdvözli a gpodder.net bejelentkezési folyamat. Először írja be a bejelentkezési információit:Bejelentkezés
- Ha még nincs fiókja, itt létrehozhat egyet:\nhttps://gpodder.net/register/FelhasználónévJelszó
- Eszköz kiválasztásaHozzon létre egy új eszközt a gpodder.net fiókjához, vagy válasszon egy meglévőt:
- Eszközazonosító:\u0020
- Felirat
- Új eszköz létrehozása
- Létező eszköz kiválasztása:
- Az eszközazonosító nem lehet üres
- Az eszközazonosító már használatban vanA felirat nem lehet üresKiválasztás
- Bejelentkezés sikeres!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.Szinkronizálás indítása mostUgrás a főképernyőre
@@ -769,9 +757,6 @@
Letöltés közben jelenik megMost játszottLehetővé teszi a lejátszás vezérlését. Ez a fő értesítés, amit a podcast lejátszásakor lát.
- Hibák
- Szinkronizálási hibák
- Automatikus letöltésekAkkor jelenik meg, ha az epizódok automatikusan letöltésre kerültek.Widget beállítások
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 @@
StatisticheAggiungi podcastEpisodi
+ CodaTuttiNovitàPreferiti
@@ -17,7 +18,7 @@
RegistroIscrizioniElenco iscrizioni
- Annulla\nil download
+ Annulla downloadCronologia riproduzionigpodder.netAccesso a gpodder.net
@@ -26,6 +27,8 @@
RiproduzioneDownloadNotifiche
+
+ \"%1$s\" non trovatoTempo totale episodi riprodotti:%1$d di %2$d episodi iniziati.\n\nRiprodotti %3$s di %4$s.
@@ -53,6 +56,8 @@
NessunoNessuna applicazione compatibile trovata
+ Esporta log dettagliati
+ I log dettagliati possono contenere informazioni sensibili come la lista delle iscrizioniApri nel browserCopia URL
@@ -81,7 +86,6 @@
Descrizione\u0020episodiElaborazione in corso
- Salva nome utente e passwordChiudiRiprovaIncludi nei download automatici
@@ -93,12 +97,13 @@
SpentoLeggeroDeciso
- \u0020download paralleli
+ %1$d download paralleliPredefinita globaleSempreMaiInvia...Mai
+ Quando non PreferitoQuando non è in codaDopo il completamento
@@ -113,7 +118,22 @@
%d selezionato%d selezionati
+
+ %d episodio
+ %d episodi
+ Caricamento successivi...
+ Notifiche episodi
+ Mostra una notifica quando viene pubblicato un episodio.
+
+ %2$s ha un nuovo episodio
+ %2$s ha %1$d nuovi episodi
+
+
+ Nuovo episodio
+ Nuovi episodi
+
+ Le tue sottoscrizioni hanno nuovi episodi.Segna tutti come riprodottiSegnati tutti gli episodi come riprodotti
@@ -145,7 +165,6 @@
Non in codaCon mediaFiltrati
- {fa-exclamation-circle} Ultimo aggiornamento fallitoApri podcastAttendi il caricamento dei dati
@@ -160,6 +179,10 @@
EliminaImpossibile eliminare il file. Prova a riavviare il dispositivo.Elimina episodio
+
+ %d episodio selezionato, %d download eliminato.
+ %d episodi selezionati, %d download eliminato(i).
+ Rimuovi flag \"nuovo\"Flag \"nuovo\" rimossoSegna come riprodotto
@@ -206,16 +229,12 @@
Dettagli%1$s \n\nURL file:\n%2$sSpazio di archiviazione non trovato
- Spazio insufficienteErrore dei dati HTTPErrore sconosciuto
- Eccezione del decodificatoreTipo di feed non supportatoErrore di connessione
- Host sconosciutoErrore di autenticazioneErrore del tipo di file
- ProibitoDownload annullatoDownload annullato\nDownload automatico disabilitato per questo elementoDownload completato con un errore (o errori)
@@ -229,12 +248,7 @@
%d download rimanente%d download rimanenti
- Elaborazione dei download in corsoScaricamento podcast in corso
-
- %d scaricamento completato, %d non riuscito.
- %d scaricamenti completati, %d non riusciti.
- Titolo sconosciutoFeedFile multimediali
@@ -267,6 +281,7 @@
Modalità picture-in-pictureAntennaPod - Chiave dell\'elemento multimediale sconosciuta: %1$dFile non trovato
+ L\'oggetto non contiene un file multimedialeBlocca la codaSblocca la coda
@@ -323,7 +338,6 @@
MemoriaEliminazione episodi, importazione, esportazioneProgetto
- CodaSincronizzazioneSincronizza con altri dispositivi tramite gpodder.netAutomazione
@@ -334,19 +348,24 @@
Elementi esterniInterruzioniControllo riproduzione
+ Riassegna pulsanti hardwareCerca...Nessun risultatoSvuota cronologiaRiproduttore multimedialePulizia episodi
- Gli episodi non in coda e che non sono tra i preferiti potrebbero essere rimossi se i Download automatici richiedono altro spazio.
+ Episodi cancellabili se il download automatico richiede altro spazio per nuovi episodiSospende la riproduzione quando le cuffie o il bluetooth vengono disconnessiRiprendi la riproduzione quando vengono riconnesse le cuffieRiprende la riproduzione quando il Bluetooth si riconnette
- Il tasto Avanti salta la traccia
- Premendo il tasto Avanti sul dispositivo Bluetooth connesso, passa all\'episodio successivo invece di andare avanti veloce
- Il tasto Indietro riavvia la traccia
- Premendo il tasto fisico Indietro, viene riavviata la traccia invece riavvolgere alcuni secondi
+ Tasto Avanti
+ Personalizza l\'azione del tasto Avanti
+ Tasto Indietro
+ Personalizza l\'azione del tasto Indietro
+ Avanti veloce
+ Riavvolgi
+ Salta episodio
+ Riavvia episodioPassa al successivo episodio della coda quando viene completata la riproduzioneElimina l\'episodio quando viene completata la riproduzioneElimina automaticamente
@@ -366,8 +385,11 @@
DisabilitaImposta IntervalloImposta orario
- ogni %1$salle %1$s
+
+ Ogni ora
+ Ogni %d ore
+ Riproduzione continuaDisconnessione cuffie o BluetoothRiconnessione cuffie
@@ -401,7 +423,9 @@
Cache degli episodiNumero di episodi scaricati memorizzabili sul dispositivo. I download automatici vengono interrotti se si raggiunge questo valore.Usa immagine episodio
- Visualizza l\'immagine dell\'episodio se disponibile. Se disattivata, verrà usata sempre l\'immagine del podcast.
+ Usa l\'immagine dell\'episodio, se disponibile. Se non selezionato, l\'app userà sempre l\'immagine di copertina del podcast.
+ Mostra tempo residuo
+ Mostra il tempo rimanente degli episodi. Se non selezionato, mostra la durata totale degli episodi.Usa tema di sistemaChiaroScuro
@@ -421,8 +445,6 @@
Forza sincronizzazione completaSincronizza le iscrizioni e lo stato di tutti gli episodi con gpodder.net.%1$s con il dispositivo %2$s]]>
- Sincronizzazione fallita
- Non si applica agli errori di autenticazione.Personalizzare le velocità disponibili per le varie velocità di riproduzione.Velocità da usare per la riproduzione degli episodi di questo podcastSalta automaticamente
@@ -437,8 +459,6 @@
Personalizza il numero di secondi da saltare in avanti quando si preme il tasto Avanti veloceTempo di salto indietroPersonalizza il numero di secondi da saltare indietro quando si preme il tasto Riavvolgi
- Imposta l\'hostname
- Usa l\'host di defaultPriorità notifiche superioriDi solito espande la notifica per mostrare i tasti di riproduzione.Controlli di riproduzione persistenti
@@ -449,10 +469,6 @@
Puoi selezionare al massimo %1$d voci.Cambia sfondo della schermata di bloccoSostituisce l\'immagine della schermata di blocco con quella dell\'episodio in riproduzione. Mostrerà l\'immagine anche in app di terze parti.
- Download fallito
- Se il download fallisce, genera un report che mostra i dettagli dell\'errore.
- Download automatico completato
- Mostra una notifica per gli episodi scaricati automaticamente.Le versioni Android precedenti alla 4.1 non supportano le notifiche estese.Posizione in codaPosizione nuovi episodi: %1$s
@@ -462,6 +478,7 @@
DisabilitatoDimensione cache delle immaginiSpazio su disco usato per la cache delle immagini.
+ Documentazione & supportoForum utentiSegnala un problemaApri il bug tracker
@@ -473,14 +490,14 @@
Impostazione attuale: %1$sProxyImposta proxy di rete
- Domande frequenti - FAQNessun browser web trovato.Supporto a ChromecastAbilita il supporto per la riproduzione multimediale remota su dispositivi Cast (Chromecast, casse esterne o Android TV)Chromecast richiede librerie proprietarie di terze parti che sono disabilitate in questa versione di AntennaPodAggiungi i download alla codaAggiunge gli episodi alla coda quando vengono scaricati
- Player Android integrato
+ Player Android integrato (obsoleto)
+ Sonic Media Player (obsoleto)ExoPlayer (consigliato)Passa ad ExoPlayerPassaggio ad ExoPlayer eseguito.
@@ -569,6 +586,7 @@
Imposta timerDisabilita il timer di spegnimento
+ +%d minTimer di spegnimentoInput non valido, il campo deve essere un numero intero.Scuoti per resettare
@@ -596,22 +614,22 @@
SUGGERIMENTICerca su gpodder.netLogin
- Benvenuto alla procedura di login di gpodder.net. Come prima cosa, inserisci le credenziali:Login
- Se non hai ancora un account, puoi crearne uno qui:\nhttps://gpodder.net/register/
+ Crea accountUsernamePassword
- Scelta del dispositivo
+ Gpodder.net è un servizio open source di sincronizzazione dei podcast indipendente dal progetto AntennaPod.
+ Server gpodder.net ufficiale
+ Server alternativo
+ Hostname
+ Seleziona serverCrea un nuovo dispositivo per utilizzare il tuo account gpodder.net o scegline uno esistente:
- ID del dispositivo:\u0020
- Caption
- Crea un nuovo dispositivo
- Scegli un dispositivo esistente:
- L\'ID del dispositivo non può essere vuoto
- ID del dispositivo già in uso
+ Nome dispositivo
+ AntennaPod su %1$sLa didascalia non può essere vuota
+ Dispositivi esistenti
+ Crea dispositivoScegli
- Login effettuato!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.Avvia la sincronizzazioneVai alla schermata principale
@@ -665,6 +683,7 @@
Cambia schermataPosizione: %1$sApplica la scelta
+ Riproduci capitoloAutenticazioneCambia il nome utente e la password per questo podcast e i suoi episodi.
@@ -799,18 +818,22 @@
Il dispositivo ricevente ha restituito un errore graveErrore nella riproduzione. Salto...
+ Errori
+ NovitàAzione richestaVisualizzato se è richiesto un intervento, ad esempio se è necessario inserire la password.ScaricamentoVisualizzato mentre il download è in corsoIn riproduzionePermette di controllare la riproduzione. E\' la principale notifica visualizzata quando un podcast è in riproduzione.
- Errori
- Viene mostrato se qualcosa fallisce, ad esempio il download o l\'aggiornamento del feed.
- Errori di sincronizzazione
+ Download fallito
+ Mostrato quando fallisce un download o aggiornamento del feed.
+ Sincronizzazione fallitaMostrati quando la sincronizzazione con gpodder fallisce.
- Download automatici
+ Download automatico completatoViene mostrato quando un episodio è stato scaricato automaticamente.
+ Nuovo episodio
+ Mostrato quando viene trovato un nuovo episodio di un podcast, se le notifiche sono attive.Impostazioni widgetCrea widget
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 @@
סטטיסטיקההוספת פודקאסטפרקים
+ תורהכולחדשמועדפים
@@ -17,7 +18,7 @@
יומןפודקאסטיםרשימת פודקאסטים
- ביטול\nהורדה
+ ביטול הורדההיסטוריית ניגוןgpodder.netכניסה אל gpodder.net
@@ -26,6 +27,8 @@
נגינההורדותהתראות
+
+ „%1$s” לא נמצאזמן הנגינה הכולל של הפרקים:%1$d מתוך %2$d פרקים החלו.\n\nנוגנו %3$s מתוך %4$s.
@@ -53,6 +56,8 @@
ללאלא נמצאו יישומונים תואמים
+ ייצוא יומנים מפורטים
+ יומנים מפורטים עשויים להכיל מידע רגיש כגון רשימת המינויים שלךפתיחה בדפדפןהעתקת כתובת
@@ -81,7 +86,6 @@
תיאור\u0020פרקיםמתבצע עיבוד
- שמירת שם משתמש וססמהסגירהלנסות שובלכלול בהורדות אוטומטיות
@@ -93,12 +97,13 @@
כבויטיפהמאוד
- \u0020הורדות במקביל
+ %1$d הורדות במקבילבררת מחדל גלובליתתמידאף פעםשליחה…אף פעם
+ כאשר לא במועדפיםכאשר לא בתוראחרי סיום
@@ -119,7 +124,28 @@
נבחרו %dנבחרו %d
+
+ פרק אחד
+ %d פרקים
+ %d פרקים
+ %d פרקים
+ נטענים עוד…
+ התראות פרקים
+ להציג התראה כאשר יוצא פרק חדש.
+
+ אצל %2$s יש פרק אחד חדש
+ אצל %2$s יש %1$d פרקים חדשים
+ אצל %2$s יש %1$d פרקים חדשים
+ אצל %2$s יש %1$d פרקים חדשים
+
+
+ פרק חדש
+ פרקים חדשים
+ פרקים חדשים
+ פרקים חדשים
+
+ למינוי שלך יש פרקים חדשים.לסמן הכול כנוגנולסמן את כל הפרקים כנוגנו
@@ -151,7 +177,6 @@
לא בתוריש מדיהמסונן
- {fa-exclamation-circle} הרענון האחרון נכשלפתיחת פודקאסטנא להמתין לסיום טעינת הנתונים
@@ -168,6 +193,12 @@
מחיקהלא ניתן למחוק קובץ. הפעלת המכשיר מחדש עשויה לסייע.מחיקת פרק
+
+ פרק אחד נבחר, הורדה אחת נמחקה.
+ %d פרקים נבחרו, %d הורדה/ות נמחקה/ו.
+ %d פרקים נבחרו, %d הורדה/ות נמחקה/ו.
+ %d פרקים נבחרו, %d הורדה/ות נמחקה/ו.
+ הסרת הסימון „חדש”הוסר הסימון „חדש”סימון כנצפה
@@ -222,16 +253,12 @@
פרטים%1$s \n\nכתובת הקובץ:\n%2$sהתקן האחסון לא נמצא
- אין די שטח אחסוןשגיאת נתוני HTTPשגיאה לא ידועה
- שגיאת מפענחסוג ההזנה אינו נתמךשגיאת חיבור
- שרת לא ידועשגיאת אימותשגיאת סוג קובץ
- אסורהורדה בוטלהההורדה בוטלה\nההורדה האוטומטית הושבתה עבור פריט זההורדות הושלמו עם שגיאה אחת או יותר
@@ -247,14 +274,7 @@
נותרו %d הורדותנותרו %d הורדות
- ההורדות בהליכי עיבודנתוני הפודקאסט מתקבלים
-
- הורדה %d הצליחה, %d נכשלו
- %d הורדות הצליחו, %d נכשלו
- %d הורדות הצליחו, %d נכשלו
- %d הורדות הצליחו, %d נכשלו
- כותרת לא ידועההזנהקובץ מדיה
@@ -287,6 +307,7 @@
מצב תמונה בתוך תמונהאנטנה־פּוֹד - מפתח מדיה לא ידוע: %1$dהקובץ לא נמצא
+ הפריט אינו מכיל קובץ מדיהנעילת תורשחרור תור
@@ -343,7 +364,6 @@
אחסוןמחיקה אוטומטית של פרקים, ייבוא, ייצואמיזם
- תורסנכרוןסנכרון עם מכשירים אחרים דרך gpodder.netאוטומציה
@@ -354,19 +374,24 @@
רכיבים חיצונייםהפרעותבקרת נגינה
+ הקצאת כפתורי חומרה מחדשחיפוש…אין תוצאותפינוי ההיסטוריהנגן מדיהניקוי פרקים
- פרקים שאינם בתור ואינם במועדפים אמורים לענות לתנאים של הסרה במקרה שההורדה האוטומטית זקוקה למקום לפרקים חדשים
+ פרקים שמועמדים להסרה אם ההורדה האוטומטית צריכה מקום לפרקים חדשיםהשהיית הנגינה כאשר האוזניות או ה־Bluetooth מנותקיםלהמשיך את הניגון כשהאוזניות מחוברות מחדשלהמשיך את הנגינה עם חיבור מחדש של ה־Bluetooth
- כפתור קדימה מדלג
- בעת לחיצה על כפתור הבא במכשיר bluetooth מחובר יש לדלג לפרק הבא במקום להריץ קדימה
- כפתור אחורה מתחיל מחדש
- לחיצה על כפתור החומרה אחורה מדלג מתחיל מחדש את נגינת הפרק הנוכחי במקום לחזור אחורה בפרק
+ כפתור קדימה
+ להתאים את התנהגות הכפתור קדימה
+ כפתור אחורה
+ להתאים את התנהגות הכפתור אחורה
+ האצה קדימה
+ חזרה לאחור
+ דילוג על פרק
+ התחלת הפרק מחדשלעבור לפריט הבא בתור כאשר הניגון מסתייםמחיקת פרק כשהניגון מסתייםמחיקה אוטומטית
@@ -386,8 +411,13 @@
השבתההגדרת הפרש זמןהגדרת הזמן ביום
- כל %1$sב־%1$s
+
+ כל שעה
+ כל שעתיים
+ כל %d שעות
+ כל %d שעות
+ ניגון מתמשךניתוק אוזניות או Bluetoothחיבור אוזניות מחדש
@@ -421,7 +451,9 @@
מטמון פרקיםהמספר הכולל של פרקים שהורדו ונשמרים במכשיר. הורדה אוטומטית תושבת אם הכמות הזאת הושגה.להשתמש בעטיפת הפרק
- להשתמש בעטיפת הפרק כאשר ניתן. אם האפשרות לא סומנה היישומון ישתמש בתמונת העטיפה של הפודקאסט.
+ להשתמש בעטיפת הפרק ברשימות כאשר ניתן. אם האפשרות לא סומנה היישומון ישתמש בתמונת העטיפה של הפודקאסט.
+ הצגת הזמן שנותר
+ הצגת הזמן שנותר לפרקים כאשר האפשרות מסומנת. אם אינה מסומנת, להציג את סך אורכם של הפרקים.להשתמש בערכת העיצוב של המערכתבהירכהה
@@ -441,8 +473,6 @@
לכפות סנכרון מלאסנכרון כל המינויים ומצבי הפרקים עם gpodder.net.%1$s עם ההתקן %2$s]]>
- הסנכרון נכשל
- הגדרה זו אינה חלה על שגיאות אימות.התאמת המהירויות הזמינות למהירות נגינה משתנההמהירות שתחול על פרקים בפודקאסט זה כשאלו מתחילים להתנגןלדלג אוטומטית
@@ -457,8 +487,6 @@
התאמת מספר השניות של הקפיצה קדימה בעת לחיצה על כפתור ההאצהזמן בקפיצה אחורההתאמת מספר השניות של הקפיצה אחורה בעת לחיצה על כפתור החזרה
- הגדרת שם מארח
- שימוש בשם מארח כבררת מחדלעדיפות גבוהה להתראההגדרה זו מרחיבה את ההתראה כדי שתציג גם כפתורי נגינה.פקדי נגינה קבועים
@@ -469,10 +497,6 @@
ניתן לבחור עד %1$d פריטים.הגדרת רקע מסך הנעילההגדרת רקע מסך הנעילה לתמונה של הפרק שמתנגן כעת. כתופעת לוואי, התמונה תופיע גם ביישומי צד שלישי.
- ההורדה נכשלה
- אם ההורדה נכשלת, יש ליצר דוח שמציג את פרטי הכשל.
- הורדה אוטומטית הושלמה
- להציג הודעה לפרקים שהתקבלו אוטומטית.גרסאות Android שקדמו ל־4.1 אינן תומכות בהתרעות מתרחבות.הוספת המיקום לתורהוספת פרקים אל: %1$s
@@ -482,6 +506,7 @@
מושבתגודל מטמון התמונותהנפח בכונן שעשוי לשמש למטמון של תמונות.
+ תיעוד ותמיכהפורום המשתמשיםדיווח על תקלהפתיחת מערכת מעקב התקלות
@@ -493,14 +518,14 @@
ערך נוכחי: %1$sמתווךהגדרת מתווך רשת
- שאלות נפוצותלא נמצא דפדפן.תמיכה ב־Chromecastהפעלת תמיכה בנגינת מדיה על התקני שידור מרוחקים (כגון Chromecast, רמקולים דיגיטליים או Android TV)לתמיכה ב־Chromecast נדרשות ספריות קנייניות מאת צד־שלישי שמושבתות בגרסה זו של אנטנה־פּוֹדהוספת הורדות לתורהוספת פרקים שהתקבלו לתור
- הנגן המובנה ב־Android
+ הנגן המובנה ב־Android (לא זמין עוד)
+ נגן המדיה Sonic (לא זמין עוד)ExoPlayer (מומלץ)מעבר ל־ExoPlayerהועבר ל־ExoPlayer.
@@ -589,6 +614,7 @@
הגדרת מתזמן שינההשבתת מתזמן שינה
+ +%d דק׳מתזמן שינהקלט שגוי, השעה חייב להיות מספר שלם וחיובייש לנער כדי לאפס
@@ -622,22 +648,22 @@
המלצותחיפוש ב־gpodder.netכניסה
- ברוך בואך לתהליך הכניסה ל־gpodder.net. ראשית, עליך להקליד את פרטי הכניסה שלך:כניסה
- אם עדיין אין לך חשבון, ניתן ליצור אחד כאן:\nhttps://gpodder.net/register/
+ יצירת חשבוןשם משתמשססמה
- בחירת מכשיר
+ Gpodder.net הוא שירות סנכרון פודקאסטים בקוד פתוח שאין לו קשר למיזם אנטנה־פּוֹד.
+ שרת gpodder.net רשמי
+ שרת בהתאמה אישית
+ שם מארח
+ בחירת שרתניתן ליצור התקן חדש לשימוש עם החשבון שלך ב־gpodder.net או לבחור בהתקן חדש:
- מזהה מכשיר:\u0020
- כותרת
- יצירת מכשיר חדש
- בחירת מכשיר קיים:
- מזהה המכשיר לא יכול להישאר ריק
- מזהה המכשיר כבר בשימוש
+ שם המכשיר
+ אנטנה־פּוֹד על %1$sהכותרת לא יכולה להישאר ריקה
+ מכשירים קיימים
+ יצירת מכשירבחירה
- נכנסת בהצלחה!מזל טוב! חשבון ה־gpodder.net שלך מקושר כעת עם המכשיר שלך. מעתה כל המינויים שלך יסונכרנו אוטומטית על ידי אנטנה־פּוֹד מהמכשיר שלך לחשבון ה־gpodder.net שלך.התחלת סנכרון כעתמעבר למסך הראשי
@@ -691,6 +717,7 @@
החלפת עמודיםמיקום: %1$sהחלת פעולה
+ לנגן פרקאימותשינוי שם המשתמש והססמה שלך לפודקאסט הזה ולפרקים שלו.
@@ -825,18 +852,22 @@
הנגן המקבל נתקל בשגיאה חמורהשגיאה בנגינת המדיה. מתבצע דילוג…
+ שגיאות
+ חדשותנדרשת פעולהמופיע אם נדרשת פעולה מצדך, למשך אם עליך להקליד ססמה.הורדהמופיע בזמן שמתרחשת הורדה.מתנגן כעתמאשר לשלוט בנגינה. זאת ההתראה הראשית שמופיעה בעת נגינת פודקאסט.
- שגיאות
- מופיע אם משהו משתבש, למשל אם הורדה או עדכון הזנה נכשלים.
- שגיאות סנכרון
+ ההורדה נכשלה
+ מופיע כאשר הורדה או עדכון הזנה נכשלים.
+ הסנכרון נכשלמופיע כאשר הסנכרון מול gpodder נכשל.
- הורדות אוטומטיות
+ ההורדה האוטומטית הושלמהמופיע כאשר פרקים התקבלו אוטומטית.
+ פרק חדש
+ מופיע כאשר נמצא פרק חדש בפודקאסט, כאשר התראות פעילותהגדרות וידג׳טיצירת וידג׳
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 @@
統計情報フィードを追加エピソード
+ キューすべて新規お気に入り
@@ -17,7 +18,6 @@
ログ購読購読リスト
- ダウンロードをキャンセル再生履歴gpodder.netgpodder.net ログイン
@@ -25,6 +25,7 @@
エピソードキャッシュが制限に達しました。設定でキャッシュサイズを増やすことができます。再生ダウンロード
+
%2$d から %1$d のエピソードが開始しました。\n\n%4$s から%3$s を再生しました。統計情報モード
@@ -75,7 +76,6 @@
説明\u0020エピソード処理中
- ユーザ名とパスワードを保存する閉じる再試行自動ダウンロードに含む
@@ -86,7 +86,6 @@
オフライトヘビー
- \u0020パラレル ダウンロード全般のデフォルト常にしない
@@ -127,7 +126,6 @@
キューに未追加メディアありフィルターしました
- {fa-exclamation-circle} 前回更新に失敗しましたポッドキャストを開くデータが読み込まれるまでしばらくお待ちください
@@ -182,16 +180,12 @@
詳細%1$s \n\nファイル URL:\n%2$sストレージ デバイスが見つかりません
- スペースが不足していますHTTPデータエラー不明なエラー
- 解析エラーサポートしないフィードタイプ接続エラー
- ホスト不明認証エラーファイルタイプ エラー
- 禁止ダウンロードをキャンセルしましたダウンロードをキャンセルしました\nこのアイテムの 自動ダウンロード を無効にしましたダウンロードがエラーで完了しました
@@ -204,7 +198,6 @@
%d ダウンロード残
- ダウンロード処理中ポッドキャストデータをダウンロード中タイトル不明フィード
@@ -290,7 +283,6 @@
ストレージエピソードの自動削除、インポート、エクスポートプロジェクト
- キュー自動詳細インポート/エクスポート
@@ -304,14 +296,9 @@
履歴をクリアメディアプレーヤーエピソード クリーンアップ
- キューに含まれておらず、お気に入りではないエピソードは、自動ダウンロードで新しいエピソードのためにスペースが必要な場合、除去の対象になりますヘッドフォンまたはBluetoothの接続が切断された時、再生を一時停止しますヘッドフォンが再接続された時に再生を再開しますBluetoothが再接続された時に再生を再開します
- 早送りボタンでスキップ
- Bluetoothで接続されたデバイスの早送りボタンを押したときに、早送りの代わりに次のエピソードにスキップします
- 戻るボタンで再開
- ハードウェアの戻るボタンを押したときに、巻き戻しの代わりに現在のエピソードの再生を再開します再生が完了した時に次のキューのアイテムに移動します再生が完了した時にエピソードを削除します自動削除
@@ -331,7 +318,6 @@
無効間隔をセット時間をセット
- %1$s ごと%1$s に連続再生ヘッドフォン再接続
@@ -364,7 +350,6 @@
エピソードキャッシュデバイスにキャッシュされたダウンロード済エピソードの合計数。この数に達すると自動ダウンロードが抑制されます。エピソードカバーを使用する
- エピソード固有のカバーが利用できる場合は常に使用します。 チェックされていない場合、アプリは常にポッドキャストのカバー画像を使用します。システムのテーマを使用するライトダーク
@@ -382,7 +367,6 @@
購読とエピソードの状態の変更を gpodder.net で同期します。すべての購読とエピソードの状態を gpodder.net で同期します。%1$s としてデバイス %2$s でログインしました]]>
- この設定は、認証エラーには適用されません。このポッドキャストでエピソードのオーディオ再生を開始するときに使用する速度メディア情報を再生速度に調整表示される位置と時間が再生速度に調整されます
@@ -390,8 +374,6 @@
早送りボタンがクリックされたときにジャンプする秒数をカスタマイズします巻き戻しスキップ時間巻き戻しボタンがクリックされたときに後方にジャンプする秒数をカスタマイズします
- ホスト名をセット
- デフォルトホストを使用優先度の高い通知これは通常再生ボタンを表示するように通知を展開します。永続再生コントロール
@@ -400,8 +382,6 @@
最大 %1$d のアイテムのみを選択できます。ロック画面の背景を設定ロック画面の背景を、現在のエピソードの画像に設定します。副作用として、これはサードパーティのアプリケーションでも画像を表示します。
- ダウンロードが失敗した場合、失敗の詳細を表示するレポートを生成します。
- 自動ダウンロードされたエピソードの通知を表示します。Androidバージョン4.1以前では、拡張通知をサポートしていません。キューに入れる場所エピソードを追加: %1$s
@@ -422,14 +402,12 @@
現在の値: %1$sプロキシネットワーク プロキシの設定
- よくある質問と答えWebブラウザーが見つかりません。Chromecast サポート(Chromecast、オーディオスピーカー、Android TV など) キャストデバイス上でリモートメディア再生のサポートを有効にしますChromecast は AntennaPod のこのバージョンで無効になっているサードパーティ独自のライブラリーが必要ですダウンロードのキューに入れるダウンロードしたエピソードをキューに追加します
- ビルトイン Android プレーヤー音声の無音をスキップビデオ終了時ビデオ再生から遷移時の動作
@@ -514,22 +492,12 @@
おススメgpodder.netを検索ログイン
- gpodder.netログインへようこそ。まずログイン情報を入力してください。ログイン
- まだアカウントをお持ちでなければ、ここで作成することができます。\nhttps://gpodder.net/register/ユーザー名パスワード
- 端末選択gpodder.net アカウントで使用する新しい端末を作成するか、既存のものを選択してください。
- 端末ID:\u0020
- キャプション
- 新しい端末を作成
- 既存の端末を選択:
- 端末IDは空にできません
- 端末IDは既に使用していますキャプションは空にできません選択
- ログインされました!おめでとうございます! あなたのgpodder.netアカウントが今お使いの端末とリンクされました。 AntennaPodは今から自動的にgpodder.netアカウントを使用して端末の購読を同期します。今すぐ同期を開始メイン画面に移動
@@ -684,8 +652,6 @@
現在のダウンロードが表示されます。現在再生中再生をコントロールできます。これはポッドキャスト再生中のメイン通知です。
- エラー
- 自動ダウンロードエピソードが自動ダウンロードされた時に表示します。ウィジェット設定
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 @@
통계팟캐스트 추가에피소드
+ 대기열전체신규즐겨찾기
@@ -17,7 +18,7 @@
기록구독구독 목록
- 다운로드\n취소
+ 다운로드 취소재생 기록gpodder.netgpodder.net 로그인
@@ -25,14 +26,21 @@
에피소드 캐시 한계값에 도달했습니다. 설정에서 캐시 크기를 늘릴 수 있습니다.재생다운로드
+ 알림
+
+ \"%1$s\" 없음
+ 재생한 에피소드 총 시간:에피소드 %1$d개 (전체 %2$d개) 시작.\n\n%3$s개 재생 (전체 %4$s개).통계 모드실제 재생한 시간을 계산합니다. 두 번 재생하면 두 번 계산되고, 재생한 것으로 표시하면 계산에 안 들어갑니다.
+ 재생했다고 표시한 모든 에피소드의 합주의: 재생 속도는 고려하지 않습니다통계 데이터 초기화모든 에피소드의 재생 시간 기록을 지웁니다. 정말로 계속 하시겠습니까?
+ %s 이후,\n재생한 항목:
+ 장치의 에피소드 총 크기:메뉴 열기메뉴 닫기
@@ -47,6 +55,9 @@
다운로드한 에피소드 수없음
+ 호환되는 앱이 없습니다
+ 자세한 기록 내보내기
+ 자세한 기록에는 구독 목록과 같은 민감한 정보가 들어 있을 수도 있습니다브라우저에서 열기URL 복사
@@ -75,7 +86,6 @@
설명\u0020에피소드처리 중
- 사용자 이름 및 암호 저장닫기다시 시도자동 다운로드에 포함
@@ -87,12 +97,13 @@
끄기가볍게무겁게
- \u0020동시 다운로드
+ %1$d개 병렬 다운로드전체 기본값항상안 함보내기…안 함
+ 즐겨찾기 아닐 때대기열에 없을 때끝나고 나서
@@ -104,7 +115,19 @@
%d개 선택
+
+ %d개 에피소드
+ 더 읽어들이기…
+ 에피소드 알림
+ 새 에피소드가 나오면 알림을 표시합니다.
+
+ %2$s에 새 에피소드 %1$d개
+
+
+ 새 에피소드
+
+ 구독에 새 에피소드가 있습니다.모두 재생했다고 표시모든 에피소드를 재생했다고 표시했습니다
@@ -119,8 +142,13 @@
팟캐스트 설정팟캐스트 이름 바꾸기팟캐스트 제거
+ 공유
+ 공유…파일 공유
+ 웹사이트 주소
+ 팟캐스트 피드 URL확인하면 \"%1$s\" 피드를 삭제하고 이 피드에서 다운로드한 모든 에피소드를 삭제합니다.
+ 정말로 \"%1$s\" 팟캐스트를 지울지 확인하십시오. 로컬 소스 폴더에 있는 파일은 삭제하지 않을 것입니다.팟캐스트 삭제하는 중전체 팟캐스트 새로 고침다중 선택
@@ -131,7 +159,6 @@
대기열 추가 안 함미디어 있음필터링함
- {fa-exclamation-circle} 최근 새로 고침 실패팟캐스트 열기데이터를 읽어 들일 때까지 기다리십시오
@@ -145,6 +172,9 @@
삭제파일을 삭제할 수 없습니다. 장치를 재부팅하면 동작할 수도 있습니다.에피소드 삭제
+
+ %d개 에피소드 선택. %d개 다운로드 삭제.
+ \"신규\" 플래그 제거\"신규\" 플래그 제거함재생했다고 표시
@@ -187,16 +217,12 @@
자세히%1$s \n\n파일 URL:\n%2$s저장 장치가 없습니다
- 저장 공간이 부족합니다HTTP 데이터 오류알 수 없는 오류
- 파서 프로그램 예외지원하지 않는 피드 종류연결 오류
- 알 수 없는 호스트인증 오류파일 종류 오류
- 금지됨다운로드 취소함다운로드 취소함\n이 항목에 자동 다운로드를 해제합니다다운로드 마침 (오류 있음)
@@ -209,7 +235,6 @@
다운로드 %d개 남음
- 다운로드 처리 중팟캐스트 데이터 다운로드 중알 수 없는 제목피드
@@ -223,6 +248,8 @@
휴대전화망 데이터 연결을 통한 다운로드는 설정에서 막혀 있습니다.\n\n임시로 다운로드를 열 수 있습니다\n\n여기서 선택한 사항은 10분 동안 유지됩니다.휴대전화망 스트리밍 확인휴대전화 데이터 연결을 통한 스트리밍은 사용하지 않게 설정되어 있습니다. 그래도 스트리밍을 하려면 누르십시오.
+ 항상
+ 한번만대기열에 추가임시로 허용
@@ -241,6 +268,7 @@
화면 속 화면 모드안테나팟 - 알 수 없는 미디어 키: %1$d파일이 없습니다
+ 항목에 미디어 파일이 들어있지 않습니다대기열 잠그기대기열 잠금 해제
@@ -269,6 +297,8 @@
플러그인을 설치하지 않았습니다여러가지 속도로 재생이 동작하려면 내부의 소닉 미디어 플레이어 사용을 추천합니다.소닉 사용
+ 프리셋
+ %1$.2f배속이 이미 프리셋으로 저장되었습니다.대기열의 에피소드 없음에피소드를 다운로드하면 추가합니다. 또는 에피소드를 길게 눌러 \"대기열에 추가\"를 선택합니다.
@@ -295,7 +325,6 @@
저장소에피소드 자동 삭제, 가져오기, 내보내기프로젝트
- 대기열동기화gpodder.net 사용하는 다른 장비와 동기화자동
@@ -306,19 +335,24 @@
외부 항목끼어들기재생 조작
+ 하드웨어 버튼 재할당검색…결과 없음기록 지우기미디어 플레이어에피소드 정리
- 대기열에 없고 즐겨찾기에 없는 에피소드는 자동 다운로드에서 새 에피소드에 공간이 필요할 경우 제거될 수 있습니다.
+ 자동 다운로드에서 새 에피소드에 공간이 더 필요할 때 삭제될 수 있는 에피소드헤드폰이나 블루투스가 연결 해제되었을 경우 일시정지합니다.헤드폰 다시 연결할 때 재생을 계속합니다.블루투스가 다시 연결되면 재생을 계속합니다.
- 앞으로 가기 버튼 건너뛰기
- 앞으로 가기 하드웨어 버튼을 눌렀을 경우 빨리감기 대신 다음 에피소드로 넘깁니다.
- 이전 버튼을 누르면 재시작
- 하드웨어 이전 버튼을 누르면 뒤로감기 대신 현재 에피소드를 재시작합니다
+ 다음 버튼
+ 다음 버튼의 동작을 직접 설정합니다
+ 이전 버튼
+ 이전 버튼의 동작을 직접 설정합니다
+ 빨리 감기
+ 뒤로 감기
+ 에피소드 건너뛰기
+ 에피소드 다시 시작재생을 마쳤을 때 다음 대기열로 이동재생이 끝나면 에피소드 삭제자동 삭제
@@ -338,9 +372,12 @@
사용 안 함주기 지정하루 중 시각 지정
- 매 %1$s%1$s에서
+
+ 매 %d시간
+ 연속 재생
+ 헤드폰 또는 블루투스 연결 끊김헤드폰 다시 연결블루투스 다시 연결스트리밍 우선
@@ -353,6 +390,7 @@
에피소드 다운로드스트리밍사용자 인터페이스
+ 모양, 구독, 잠금 화면테마 선택네비게이션 드로어 항목 설정네비게이션 드로어에 어떤 항목을 표시할지 바꿉니다.
@@ -372,6 +410,8 @@
장치에 임시 저장한 다운로드한 에피소드의 전체 개수. 이 숫자에 도달하면 자동 다운로드가 지연됩니다.에피소드 커버 사용에피소드마다 설정된 커버가 있으면 그 커버를 사용합니다. 사용하지 않으면, 앱에서는 항상 팟캐스트 커버 이미지를 사용합니다.
+ 남은 시간 표시
+ 체크하면 에피소드 남은 시간을 표시합니다. 체크하지 않으면 에피소드의 전체 시간을 표시합니다.시스템 테마 사용밝게어둡게
@@ -391,7 +431,7 @@
전체 동기화 강제gpodder.net의 모든 구독과 에피소드 상태를 동기화합니다.%1$s 사용자로 로그인, %2$s 장치]]>
- 이 설정은 인증 오류에는 적용되지 않습니다.
+ 재생 속도를 다양하게 할 경우 속도를 직접 설정이 팟캐스트의 에피소드를 재생할 때 사용할 오디오 재생 속도자동 건너뛰기소개 및 마지막 크레디트 건너뛰기
@@ -405,18 +445,16 @@
앞으로 감기 버튼을 눌렀을 때 앞으로 넘어갈 초를 지정합니다.뒤로 건너뛰기 시간뒤로 감기 버튼을 눌렀을 때 뒤로 넘어갈 초를 지정합니다.
- 호스트 이름 설정
- 기본 호스트 사용알림 우선순위 높게알림에서 재생 버튼이 표시되도록 확장합니다.재생 조작 항상 표시재생이 일시 중지했을 때에도 알림과 잠금 화면의 조작 기능을 유지합니다.
+ 간략한 알림 버튼 사용
+ 알림이 중첩되었을 경우 재생 버튼을 바꿉니다. 재생/일시정지 단추는 항상 포함됩니다.최대 %1$d개 항목 선택최대 %1$d개 항목만 선택할 수 있습니다.잠금 화면 배경 설정현재 에피소드의 이미지를 잠금 화면의 배경으로 설정합니다. 대신 이는 제3자 앱의 이미지도 표시하게 됩니다.
- 다운로드가 실패하면, 실패 이유를 자세히 표시하는 보고서를 만듭니다.
- 자동 다운로드 에피소드 에 대해 알림을 표시합니다.안드로이드 4.1 미만의 버전에서는 확장 알림을 지원하지 않습니다.대기열 추가 위치에피소드 추가 위치: %1$s
@@ -426,6 +464,7 @@
사용 안 함이미지 캐시 크기이미지에 사용할 디스크 캐시 크기
+ 문서 및 지원사용자 포럼문제점 보고버그 추적 사이트 열기
@@ -437,14 +476,15 @@
현재 값: %1$s프록시네트워크 프록시 설정
- 자주 묻는 질문웹브라우저가 없습니다.크롬캐스트 지원캐스트 장치의 원격 미디어 재생 기능 사용 (예: 크롬캐스트, 안드로이드 TV의 오디오 스피커)크롬캐스트는 서드파티 라이브러리가 필요하지만, 이 버전의 안테나팟에서는 사용하지 않게 되어 있습니다.다운로드한 항목 대기열에 추가다운로드한 에피소드를 대기열에 추가
- 내장 안드로이드 플레이어
+ 내장 안드로이드 플레이어 (권장하지 않음)
+ Sonic Media Player (권장하지 않음)
+ ExoPlayer (추천)ExoPlayer로 전환ExoPlayer로 전환함.오디오에서 묵음 구간 건너뛰기
@@ -465,6 +505,15 @@
페이지 선택제거된 항목 대기열에서 삭제삭제된 항목을 대기열에서 자동으로 삭제합니다.
+ 구독 필터
+ 네비게이션 드로어 및 구독 화면의 구독 내용을 필터링합니다.
+ 없음
+ 구독 필터 적용함.
+ 0보다 큰 개수
+ 자동 다운로드
+ 자동 다운로드 아님
+ 업데이트 상태로 유지
+ 업데이트 유지 아님정보안테나팟 버전
@@ -499,6 +548,7 @@
구독 목록, 재생한 에피소드 목록 및 대기열을 다른 장치의 안테나팟에 옮기기안테나팟 데이터베이스를 다른 장치에서 가져오기OPML 가져오기
+ 팟캐스트 목록 가져오기 (OPML)OPML 문서를 읽는데 오류가 발생했습니다:파일을 선택하지 않았습니다!모두 선택
@@ -514,12 +564,15 @@
내보낸 파일을 다음에 저장했습니다:\n\n%1$sOPML 파일을 읽으려면 외부 저장소 접근이 필요합니다가져올 파일을 선택하십시오
+ 가져오기 성공
+ 안테나팟을 다시 시작하려면 확인을 누르십시오이 데이터베이스는 더 새로운 버전의 안테나팟에서 내보낸 데이터베이스입니다. 현재 설치된 버전의 안테나팟에서는 이 파일을 제대로 처리하지 못할 수도 있습니다.즐겨찾기 내보내기저장한 즐겨찾기를 파일로 내보내기취침 타이머 설정취침 타이머 사용 않음
+ +%d분취침 타이머입력이 잘못되었습니다. 시간으로 숫자를 입력해야 합니다.흔들어서 타이머 초기화
@@ -544,22 +597,22 @@
추천gpodder.net 검색로그인
- gpodder.net 로그인입니다. 먼저 로그인 정보를 입력하십시오:로그인
- 아직 계정이 없으면 다음에서 만들 수 있습니다:\nhttps://gpodder.net/register/
+ 계정 만들기사용자 이름암호
- 장치 선택
+ gpodder.net은 안테나팟 프로젝트와는 독립적인 오픈소스 팟캐스트 동기화 서비스입니다.
+ 공식 gpodder.net 서버
+ 서버 사용자 지정
+ 호스트 이름
+ 서버 선택gpodder.net 계정에서 사용할 장치를 새로 만들거나 기존 장치를 선택하십시오:
- 장치 아이디:\u0020
- 설명
- 새 장치 만들기
- 기존 장치 선택:
- 장치 ID는 비어 있으면 안 됩니다
- 장치 ID를 이미 사용 중입니다
+ 장치 이름
+ 안테나팟, %1$s자막이 비어 있으면 안 됩니다
+ 기존 장치
+ 장치 만들기선택
- 로그인이 성공했습니다!축하합니다! gpodder.net 계정이 장치와 연결되었습니다. 이제 안테나팟에서 gpodder.net 계정의 구독 정보와 자동으로 동기화합니다.지금 동기화 시작메인 화면으로 이동
@@ -613,6 +666,7 @@
페이지 전환위치: %1$s동작 적용
+ 챕터 재생인증이 팟캐스트와 에피소드에 대한 사용자 이름과 비밀번호를 바꿉니다.
@@ -625,6 +679,10 @@
최신 업데이트 유지모든 팟캐스트를 (자동) 새로 고칠 때 이 팟캐스트 포함자동 다운로드는 안테나팟 메인 설정에서 꺼져 있습니다
+ 청취한 시간:
+ 장치에 들어 있는 에피소드:
+ 사용한 용량:
+ 모든 팟캐스트에 대한 뷰 »데이터베이스 업그레이드 중
@@ -632,16 +690,29 @@
팟캐스트 검색…iTunes 검색
+ podcastindex.org 검색fyyd 검색고급
+ RSS 주소로 팟캐스트 추가gpodder.net 둘러보기발견
+ 숨기기
+ 제안 사항을 감추도록 선택했습니다.더 보기 »
+ iTunes 추천
+ %1$s 검색 결과
+ 로컬 폴더 추가
+ 로컬 폴더
+ 로컬 폴더 다시 연결
+ 권한이 거부된 경우, 이 기능을 사용해 정확히 같은 폴더에 다시 연결할 수 있습니다. 다른 폴더를 선택하지 마십시오.
+ 이 가상 팟캐스트는 안테나팟에 폴더를 추가해 만들었습니다.
+ 시스템 파일 관리자를 시작할 수 없습니다필터모두모든 에피소드 선택함
+ 없음모든 에피소드 선택 해제함재생함재생한 에피소드 선택함
@@ -655,13 +726,17 @@
대기열에 없는 에피소드 선택함미디어가 있는 에피소드 선택함즐겨찾기 포함
+ 즐겨찾기 아님다운로드함다운로드 안 함대기열에 있음대기열에 없음미디어 있음
+ 미디어 없음일시 중지함
+ 일시 중지 아님재생함
+ 재생함 아님제목 (A \u2192 Z)제목 (Z \u2192 A)
@@ -682,6 +757,11 @@
나중에 알림해봅시다!
+ 포함:
+ 재생 위치
+ 미디어 파일 주소
+ 에피소드 웹페이지
+ 미디어 파일오디오 조작재생 속도
@@ -721,15 +801,22 @@
리시버 플레이어에서 심각한 오류가 발생했습니다미디어 재생에 오류. 건너뜁니다...
+ 오류
+ 뉴스사용자 조작 필요사용자 조작이 필요할 때 표시됩니다. 예를 들어 암호를 입력해야 할 때 표시됩니다.다운로드 중현재 다운로드 중일 때 표시됩니다.현재 재생 중재생을 조작할 수 있습니다 .팟캐스트를 재생할 때 볼 수 있는 메인 알림입니다.
- 오류
- 자동 다운로드
+ 다운로드 실패
+ 다운로드 또는 피드 업데이트가 실패할 때 표시.
+ 동기화 실패
+ gpodder 동기화가 실패할 때 표시.
+ 자동 다운로드 마침에피소드를 자동으로 다운로드했을 때 표시됩니다.
+ 새 에피소드
+ 알림 기능을 켰을 경우, 팟캐스트의 새 에피소드를 찾으면 표시됩니다.위젯 설정위젯 만들기
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 @@
StatistikaPridėti tinklalaidęEpizodai
+ EilėVisiNaujiMėgstami
@@ -17,7 +18,6 @@
ŽurnalasPrenumeratosPrenumeratų sąrašas
- Atšaukti\natsiuntimąAtkūrimo istorijagpodder.netgpodder.net prisijungimas
@@ -25,6 +25,7 @@
Pasiektas epizodų podėlio dydžio limitas. Nustatymuose galite padidinti podėlio dydį.AtkūrimasAtsiuntimai
+
Paleisti %1$d iš %2$d epizodų.\n\nPerklausyta %3$s iš %4$s.Statistikos režimas
@@ -75,7 +76,6 @@
Aprašymas\u0020epizodaiApdorojama
- Išsaugoti vartotojo vardą ir slaptažodįUžvertiBandyti vėlĮtraukti į automatinius atsiuntimus
@@ -87,7 +87,6 @@
IšjungtaŠvelniaiGausiai
- \u0020lygiagretūs atsiuntimaiGlobali numatytojiVisadaNiekada
@@ -140,7 +139,6 @@
Nesantys eilėjeTurintys medijos failųFiltruota
- {fa-exclamation-circle} Paskutinis atnaujinimas nepavykoAtverti tinklalaidęPrašome luktelėti, kol duomenys bus įkelti
@@ -211,16 +209,12 @@
Išsami informacija%1$s \n\nFailo URL:\n%2$sNerastas laikmenos įrenginys
- Trūksta laisvos vietosHTTP duomenų klaidaNežinoma klaida
- Išimtinė situacija analizatoriujeNepalaikomas sklaidos kanalo tipasSusijungimo klaida
- Nežinomas serverisTapatumo nustatymo klaidaFailo tipo klaida
- UždraustaAtsiuntimas atšauktasAtsiuntimas atšauktas\nAutomatinis atsiuntimas šiam elementui išjungtasAtsiuntimai užbaigti su klaida (-omis)
@@ -236,7 +230,6 @@
Liko %d atsiuntimųLiko %d atsiuntimų
- Apdorojami atsiuntimaiAtsiunčiami tinklalaidės duomenysNežinomas pavadinimasSklaidos kanalas
@@ -322,7 +315,6 @@
LaikmenaAutomatinis epizodų trynimas, importas, eksportasProjektas
- EilėSinchronizavimasSinchronizuoti su kitais įrenginiais naudojantis „gpodder.net“Automatizacija
@@ -338,14 +330,9 @@
Išvalyti istorijąMedijos grotuvasEpizodų valymas
- Epizodai, nesantys eilėje ar tarp mėgstamųjų, gali būti ištrinti automatinio atsiuntimo metu pritrūkus laisvos vietos naujiems epizodams Pristabdyti atkūrimą, kai atjungiamos ausinės ar „Bluetooth“Pratęsti atkūrimą, kai ausinės pakartotinai prijungiamosPratęsti atkūrimą, kai pakartotinai prisijungiama prie „Bluetooth“
- Mygtukas „pirmyn“ peršoka epizodą
- Paspaudus „Bluetooth“ įrenginio mygtuką „pirmyn“ peršokti į kitą epizodą vietoje greito persukimo į priekį.
- Mygtukas „ankstesnis“ paleidžia iš naujo
- Paspaudus aparatinį mygtuką „ankstesnis“ paleisti dabartinį epizodą nuo pradžių vietoj epizodo peršokimoAtkūrimui pasibaigus peršokti į kitą eilės elementąIštrinti epizodą pasibaigus atkūrimuiAutomatinis ištrynimas
@@ -365,7 +352,6 @@
IšjungtiNustatyti intervaląNustatyti dienos metą
- kas %1$slygiai %1$sNenutrūkstamas atkūrimasPakartotinai prijungus ausines
@@ -398,7 +384,6 @@
Epizodų podėlisBendras podėlyje atsiųstų epizodų skaičius šiame įrenginyje. Pasiekus šį skaičių automatinis atsiuntimas bus pristabdytas.Naudoti epizodo viršelį
- Naudoti epizodo viršelį, kai prieinama. Nepažymėjus, programėlė visada naudos tinklalaidės viršelio paveikslėlį.Naudoti sistemos temąŠviesiTamsi
@@ -418,7 +403,6 @@
Priverstinis pilnas sinchronizavimasSinchronizuoti visas prenumeratas bei epizodų būsenas su „gpodder.net“.%1$s naudojant įrenginį %2$s]]>
- Šis nustatymas negalioja tapatumo nustatymo klaidoms.Atkūrimo sparta, naudojama pradedant šios tinklalaidės epizodų atkūrimąAutomatinis praleidimasPraleisti įvadą ir pabaigos žodį.
@@ -432,8 +416,6 @@
Derinkite, per kiek sekundžių šoktelėti į priekį kai paspaudžiamas persukimo į priekį mygtukasAtsukimo atgal trukmėDerinkite, per kiek sekundžių šoktelėti atgal kai paspaudžiamas atsukimo atgal mygtukas
- Nustatyti serverį
- Naudoti numatytąjį serverįAukštas pranešimų prioritetasTai dažniausiai išskleidžia pranešimą, kad būtų rodomi atkūrimo valdymo mygtukai.Pastovūs atkūrimo valdikliai
@@ -442,8 +424,6 @@
Galite pasirinkti daugiausiai %1$d elementus.Nustatyti ekrano užrakto fonąAtkuriamo epizodo paveikslėlį naudoti kaip ekrano užrakto foną. Paveikslėlis taip pat bus matomas trečiųjų šalių programėlėse.
- Atsiuntimui nepavykus, sukurti ataskaitą su išsamiu klaidų aprašymu.
- Rodyti pranešimą automatiškai atsiuntus epizodus.Ankstesnės nei 4.1 „Android“ versijos nepalaiko išplėstų programos pranešimų.Pridėjimo į eilę vietaEpizodus pridėti: %1$s
@@ -464,14 +444,12 @@
Dabartinė reikšmė: %1$sĮgaliotasis serverisNustatyti įgaliotąjį tinklo serverį
- Dažniausiai užduodami klausimaiNerasta jokia interneto naršyklė.„Chromecast“ palaikymasĮjungti nuotolinio medijos atkūrimo „Cast“ įrenginiuose (pvz. „Chromecast“, garso kolonėlės ar „Android TV“) palaikymą„Chromecast“ palaikymui reikalingos nuosavybinės trečiųjų šalių bibliotekos, kurios yra negalimos šioje „AntennaPod“ versijoje.Atsiuntimus pridėti į eilęParsiuntus epizodus, pridėti juos į eilę
- Įtaisytoji „Android“ leistuvėPerjungti į „ExoPlayer“Perjungta į „ExoPlayer“.Praleisti tylą
@@ -580,22 +558,12 @@
PASIŪLYMAIIeškoti „gpodder.net“ svetainėjePrisijungti
- Sveiki! Tai prisijungimo prie „gpodder.net“ vedlys. Visų pirma, įveskite savo prisijungimo duomenis:Prisijungti
- Jei dar neturite paskyros, galite susikurti ją čia:\nhttps://gpodder.net/register/Vartotojo vardasSlaptažodis
- Įrenginio pasirinkimasSukurkite naują įrenginį savo „gpodder.net“ paskyrai arba pasirinkite esamą:
- Įrenginio ID:\u0020
- Pavadinimas
- Sukurti naują įrenginį
- Pasirinkti esamą įrenginį:
- Įrenginio ID laukelis negali būti tuščias
- Toks įrenginio ID jau naudojamasPavadinimas negali būti tuščiasPasirinkti
- Sėkmingai prisijungta!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.Pradėti sinchronizavimą dabarEiti į pagrindinį ekraną
@@ -763,8 +731,6 @@
Rodomas atsiuntimo metu.Šiuo metu atkuriamaLeidžia valdyti atkūrimą. Tai pagrindinis pranešimas matomas tinklalaidės atkūrimo metu.
- Klaidos
- Automatiniai atsiuntimaiRodomas automatiškai atsiuntus epizodus.Valdiklio nustatymai
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 @@
StatistikkLegg til podkastEpisoder
+ KøAlleNyeFavoritter
@@ -17,7 +18,6 @@
LoggAbonnementerAbonnementliste
- Avbryt\nLast nedAvspillingshistorikkgpodder.netgpodder.net-innlogging
@@ -26,6 +26,7 @@
AvspillingNedlastingerVarslinger
+
Total tid, avspilte episoderStartet %1$d av %2$d episoder.\n\nAvspilt %3$s av %4$s.
@@ -81,7 +82,6 @@
Beskrivelse\u0020episoderBehandler
- Lagre brukernavn og passordLukkPrøv igjenInkluder i automatiske nedlastninger
@@ -93,7 +93,6 @@
AvLettTung
- \u0020samtidige nedlastingerGlobal standardAlltidAldri
@@ -140,7 +139,6 @@
Ikke i køHar medierFiltrert
- {fa-exclamation-circle} Siste oppdatering mislyktesÅpne podkastVent til dataene er lastet inn
@@ -201,16 +199,12 @@
Detaljer%1$s \n\nFil-URL:\n%2$sLagringsenhet ikke funnet
- Ikke nok plassHTTP-datafeilUkjent feil
- Parser-unntakStrøm-typen er ikke støttetTilkoblingsfeil
- Ukjent vertAutentiseringsfeilFiltype-feil
- Ikke tillattNedlasting avbruttNedlasting avbrutt\nAutomatisk nedlasting for dette elementet er deaktivertNedlasting fullført med feilmeldinger
@@ -224,12 +218,7 @@
%d nedlasting gjenstår%d nedlastinger gjenstår
- Behandler nedlastningerLaster ned data til podkast
-
- %1$d nedlastninger lyktes, %2$d mislyktes
- %d nedlastinger lyktes, %d mislyktes
- Ukjent tittelStrømMediafil
@@ -318,7 +307,6 @@
LagringAuto-slett episoder, importer, eksporterProsjekt
- KøSynkroniseringSynkroniser med andre enheter over gpodder.netAutomasjon
@@ -334,14 +322,9 @@
Slett loggMediespillerEpisodeopprydding
- Episoder som ikke er i køen og ikke er favoritter skal kunne fjernes hvis auto-nedlasting trenger plass til nye episoderSett playback på pause når hodetelefoner eller bluetooth er frakobletGjenoppta avspilling når hodetelefoner gjeninnkoplesFortsett avspilling når bluetooth er tilkoblet igjen
- \"Fremover\" hopper over
- Hopp til neste episode i stedet for å spole når du trykker på \"Fremover\"-knappen
- Forriv
- Start episoden på nytt i stedet for å spole når du trykker på \"Tilbake\"-knappenHopp til neste element i køen når avspillingen er ferdigSlett episode når avspillingen er ferdigAutomatisk sletting
@@ -361,7 +344,6 @@
Skru avSett intervallAngi klokkeslett
- hver %1$sved %1$sKontinuerlig avspillingKoblet fra hodetelefoner eller Bluetooth
@@ -396,7 +378,6 @@
Mellomlager for episoderTotalt antall nedlastede episoder bufret på enheten. Automatisk nedlasting vil bli stoppet hvis dette nummeret er nådd.Bruk episode-cover
- Bruk et episode-spesifikt cover hvis det er tilgjengelig. Hvis dette ikke er valgt vil appen alltid bruke podkastens cover-bilde.Bruk systemets temaLystMørkt
@@ -416,8 +397,6 @@
Tving fram full synkroniseringSynkroniser alle abonnement og episoder med gpodder.net.%1$s med enhet %2$s]]>
- Synkronisering feilet
- Denne instillingen gjelder ikke autentiseringfeil.Hastigheten som brukes når episoder av denne podkasten spilles avAutomatisk hoppHopp over introer og \"rulletekster\"
@@ -431,8 +410,6 @@
Velg hvor mange sekunder som skal hoppes når du trykker på \"Spol fremover\"-knappenHopp tilbakeVelg hvor mange sekunder som skal hoppes tilbake når \"Spol tilbake\"-knappen trykkes
- Sett vertsnavn
- Bruk standard vertHøy varsling-prioritetDette utvider som regel varslingen for å vise kontroller.Vedvarende avspillingskontroller
@@ -442,9 +419,6 @@
Du kan kun velge opp til %1$d ting.Angi som bakgrunn på låseskjermenAngir låseskjermbakgrunnsbildet til å være den nåværende episodens bilde. Som en sideeffekt vil dette også vise bildet i tredjepartsapper.
- Generer en rapport som viser detaljer dersom nedlastinger feiler.
- Automatisk nedlasting fullført
- Vis en varsling for automatisk nedlastede episoderAndroid-versjoner tidligere enn 4.1 støtter ikke utvidede varsler.Plassering i køenLegg episoder til på: %1$s
@@ -465,14 +439,12 @@
Valgt: %1$sProxyVelg en nettverk-proxy
- FAQ (Ofte stilte spørsmål)Ingen nettleser funnet.Chromecast støtteAktiver støtte for fjern-avspilling på Cast-enheter (som Chromecast, høyttalere eller Android TV)Chromecast krever proprietær tredjeparts programvare som er deaktivert i denne utgaven av AntennaPodLegg til nedlastede i køenLegg til nedlastede episoder i køen
- Innebygd Android-spillerBytt til ExoPlayerByttet til ExoPlayerHopp over stillhet
@@ -565,22 +537,12 @@
FORSLAGSøk på gpodder.netLogg inn
- Velkommen til gpodder.net innlogginsprosess. Først begynner vi med å skrive inn innlogginsinformasjon.Logg inn
- Dersom du ikke har en konto enda kan du opprette en her:\nhttps://gpodder.net/register/BrukernavnPassord
- EnhetsvalgLag en ny enhet til å bruke for din gpodder.net konto eller velg en som allerede eksisterer.
- EnhetsID:\u0020
- Tekst
- Lag en ny enhet
- Velg eksisterende enhet:
- Device ID kan ikke være tom
- EnhetsID er allerede i brukTittel kan ikke være tomVelg
- Innlogging lyktes.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.Start synkronisering nå.Gå til hovedskjermen
@@ -751,10 +713,7 @@
Vises mens nedlasting foregår.Spilles nåKan styrre avspilling. Dette er hoved-varslingen du vil se mens en podkast spilles.
- Feil
- Feil ved synkroniseringVises når synkronisering mot gpodder feiler
- Automatiske nedlastingerVises når episoder har blitt lastet ned automatiskWidget-innstillinger
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 @@
StatistiekenPodcast toevoegenAfleveringen
+ WachtrijAlleNieuwFavorieten
@@ -17,7 +18,7 @@
LogboekAbonnementenAbonnementenlijst
- Download\nafbreken
+ Download afbrekenAfspeelgeschiedenisgpodder.netInloggen op gpodder.net
@@ -26,6 +27,8 @@
AfspelenDownloadsMeldingen
+
+ \'%1$s\' is niet aangetroffenTotaalduur van afgespeelde afleveringen:%1$d van %2$d afleveringen gestart.\n\n%3$s van %4$s afgespeeld.
@@ -53,6 +56,8 @@
GeenGeen compatibele apps aangetroffen
+ Uitgebreide logboeken exporteren
+ Uitgebreide logboeken kunnen gevoelige informatie bevatten, zoals de abonnementenlijstOpenen in browserURL kopiëren
@@ -81,7 +86,6 @@
Omschrijving\u0020afleveringenBezig met verwerken...
- Gebruikersnaam en wachtwoord opslaanSluitenOpnieuwMeenemen bij automatisch downloaden
@@ -93,12 +97,13 @@
UitGematigdAanzienlijk
- \u0020gelijktijdige downloads
+ %1$d gelijktijdige downloadsStandaardinstellingAltijdNooitVersturen...Nooit
+ Indien niet in favorietenIndien niet in wachtrijAls aflevering volledig is afgespeeld
@@ -113,7 +118,22 @@
%d geselecteerd%d geselecteerd
+
+ %d aflevering
+ %d afleveringen
+ Bezig met laden...
+ Afleveringsmeldingen
+ Toon een melding als er een nieuwe aflevering is uitgebracht.
+
+ Er zijn één nieuwe aflevering van %2$s
+ Er zijn %1$d nieuwe afleveringen van %2$s
+
+
+ Nieuwe aflevering
+ Nieuwe afleveringen
+
+ Er zijn nieuwe afleveringen beschikbaar.Alles als afgespeeld markerenAlle afleveringen zijn gemarkeerd als afgespeeld
@@ -145,7 +165,6 @@
Niet in de wachtrijBevat mediaGefilterd
- {fa-exclamation-circle} Vorige verversing misluktPodcast openenWacht tot de gegevens geladen zijn
@@ -160,6 +179,10 @@
VerwijderenKan bestand niet verwijderen; start je apparaat opnieuw op.Aflevering verwijderen
+
+ %d geselecteerde aflevering - %d download verwijderd
+ %d geselecteerde afleveringen - %d downloads verwijderd
+ \'Nieuw\'-label verwijderen\'Nieuw\'-label is verwijderdAls afgespeeld markeren
@@ -206,16 +229,12 @@
Details%1$s \n\nURL van bestand:\n%2$sOpslagmedium niet aangetroffen
- Onvoldoende ruimteHTTP-gegevensfoutOnbekende fout
- VerwerkingsuitzonderingNiet-ondersteunde feedsoortVerbindingsfout
- Onbekende hostAuthenticatiefoutBestandssoortfout
- Niet mogelijkDownload afgebrokenDownload afgebroken\nAutomatisch downloaden uitgeschakeld voor deze afleveringDownloads afgerond, maar met fout(en)
@@ -229,12 +248,7 @@
Nog %d downloadNog %d downloads
- Bezig met verwerken van downloadsBezig met downloaden van podcastgegevens
-
- %d download voltooid; %d mislukt
- %d downloads voltooid; %d mislukt
- Onbekende titelFeedMediabestand
@@ -267,6 +281,7 @@
Beeld-in-beeldmodusAntennaPod - onbekende mediatoets: %1$dBestand niet aangetroffen
+ Het item bevat geen mediabestandWachtrij vergrendelenWachtrij ontgrendelen
@@ -323,7 +338,6 @@
OpslagAutomatisch verwijderen, im- en exporterenProject
- WachtrijSynchronisatieSynchroniseer met andere apparaten met behulp van gpodder.netAutomatische acties
@@ -334,19 +348,24 @@
Externe elementenOnderbrekingenAfspeelbediening
+ Hardwareknoppen opnieuw toewijzenZoeken...Geen resultatenGeschiedenis wissenMediaspelerAutomatisch opschonen
- Afleveringen die niet in de wachtrij staan én geen favoriet zijn, mogen verwijderd worden als \'Automatisch downloaden\' ruimte nodig heeft voor nieuwe afleveringen
+ Afleveringen verwijderd mogen worden als \'Automatisch downloaden\' ruimte nodig heeft voor nieuwe afleveringenAfspelen pauzeren als de koptelefoon wordt losgekoppeld of de bluetoothverbinding verbrokenAfspelen hervatten als de koptelefoon weer wordt aangeslotenAfspelen hervatten als de bluetoothverbinding hersteld is
- \'Vooruit\' gebruiken voor overslaan
- Als je op de vooruitknop van een via bluetooth verbonden apparaat drukt, wordt de volgende aflevering geladen i.p.v. doorgespoeld
- \'Vorige\' gebruiken voor opnieuw afspelen
- Aflevering afspelen vanaf het begin i.p.v. terugspoelen als er op een fysieke \'vorige\'-knop wordt gedrukt
+ Vooruitknop
+ Pas het gedrag van de vooruitknop aan
+ Terugknop
+ Pas het gedrag van de terugknop aan
+ Vooruitspoelen
+ Terugspoelen
+ Aflevering overslaan
+ Afleveringen herstartenVolgende item in de wachtrij afspelen als de aflevering voltooid isAfleveringen verwijderen als ze zijn afgespeeldAutomatisch verwijderen
@@ -366,8 +385,11 @@
UitschakelenTussenpoos instellenTijdstip instellen
- elke %1$som %1$s
+
+ Elk uur
+ Elke %d uur
+ Doorlopend afspelenKoptelefoon- of bluetoothverbinding verbrokenOpnieuw aansluiten van hoofdtelefoon
@@ -402,6 +424,8 @@
Het totaal aantal gedownloade afleveringen dat moet worden opgeslagen op het apparaat. Automatische downloads worden onderbroken als dit aantal wordt bereikt.Omslag van aflevering gebruikenGebruik de bij de aflevering behorende omslag (indien beschikbaar). Als je dit niet inschakelt, dan wordt altijd de omslag van de podcast gebruikt.
+ Resterende tijd tonen
+ Schakel in om de resterende tijd van afleveringen te tonen. Schakel uit om de totale duur van afleveringen te tonen.Systeemthema gebruikenLichtDonker
@@ -421,8 +445,6 @@
Volledige synchronisatie afdwingenSynchroniseer alle abonnementen en afleveringsstatussen met gpodder.net.%1$s met apparaat %2$s]]>
- Synchronisatie mislukt
- Deze instelling is niet van toepassing op inlogfouten.Pas de beschikbare snelheden aan voor de variabele afspeelsnelheidDe te gebruiken snelheid bij het afspelen van afleveringen in deze podcastAutomatisch overslaan
@@ -437,8 +459,6 @@
Pas het aantal seconden aan waarmee wordt vooruitgespoeld per druk op de knopSnelheid van terugspoelenPas het aantal seconden aan waarmee wordt teruggespoeld per druk op de knop
- Hostnaam instellen
- Standaardhost gebruikenMelding met hoge prioriteitDit klapt meestal de melding uit zodat de bedieningsknoppen kunnen worden getoond.Bedieningsknoppen behouden
@@ -449,10 +469,6 @@
Je kunt maximaal %1$d knoppen kiezen.Achtergrondafbeelding vergrendelschermToon de afbeelding van de huidige aflevering op het vergrendelscherm. Hierdoor is de afbeelding ook beschikbaar voor andere apps.
- Downloaden mislukt
- Stel een verslag op met foutdetails als downloads mislukken.
- Automatisch downloaden voltooid
- Toon een melding bij automatisch gedownloade afleveringen.Android-versies lager dan 4.1 ondersteunen geen knoppen op meldingen.WachtrijlocatieAfleveringen toevoegen aan: %1$s
@@ -462,6 +478,7 @@
UitgeschakeldGrootte van afbeeldingscachePas de grootte aan van het cachegeheugen voor afbeeldingen.
+ Documentatie en ondersteuningGebruikersforumBug meldenBugtracker openen
@@ -473,14 +490,14 @@
Huidige instelling: %1$sProxyNetwerkproxy instellen
- Veelgestelde vragenGeen browser aangetroffen.Chromecast-ondersteuningOndersteuning activeren voor draadloos afspelen via Cast-apparaten (zoals Chromecast, luidsprekers of Android TV)Voor Chromecast is software van derden vereist die niet beschikbaar is in deze versie van AntennaPodGedownloade afleveringen in wachtrijVoeg gedownloade afleveringen toe aan de wachtrij
- Ingebouwde Android-speler
+ Ingebouwde Android-speler (verouderd)
+ Sonic-mediaspeler (verouderd)ExoPlayer (aanbevolen)ExoPlayer gebruikenOvergeschakeld naar ExoPlayer.
@@ -569,6 +586,7 @@
Slaaptimer instellenSlaaptimer uitschakelen
+ +%d min.SlaaptimerOngeldige invoer; de tijd moet een geheel getal zijnSchudden om opnieuw in te stellen
@@ -596,22 +614,22 @@
SUGGESTIESgpodder.net doorzoekenInloggen
- Welkom bij het inlogproces van gpodder.net. Typ eerst je inloggegevens:Inloggen
- Als je nog geen account hebt, dan kun je je hier registreren:\nhttps://gpodder.net/register/
+ Account aanmakenGebruikersnaamWachtwoord
- Apparaatkeuze
+ Gpodder.net is een open source podcast-synchronisatiedienst die niet betrokken is bij het AntennaPod-project.
+ Officiële gpodder.net-server
+ Aangepaste server
+ Hostnaam
+ Server kiezenVoeg een nieuw apparaat toe aan je gpodder.net-account of kies een bestaand:
- Apparaat-ID:\u0020
- Omschrijving
- Apparaat toevoegen
- Bestaand apparaat kiezen:
- Apparaat-ID mag niet blanco zijn
- Apparaat-ID wordt al gebruikt
+ Apparaatnaam
+ AntennaPod op %1$sApparaatomschrijving mag niet blanco zijn
+ Bestaande apparaten
+ Apparaat toevoegenKiezen
- Ingelogd!Gefeliciteerd! Je gpodder.net-account is nu gekoppeld aan je apparaat. AntennaPod zal voortaan abonnementen automatisch synchroniseren met je gpodder.net-account.Nu synchroniserenTerug naar hoofdscherm
@@ -665,6 +683,7 @@
Van pagina wisselenPositie: %1$sToepassen
+ Hoofdstuk afspelenAuthenticatieGebruikersnaam en wachtwoord wijzigen voor deze podcast en bijbehorende afleveringen.
@@ -799,18 +818,22 @@
Ernstige fout opgetreden op het afspelende Cast-apparaatKan media niet afspelen. Bezig met overslaan...
+ Foutmeldingen
+ NieuwsActie vereistTonen als een actie vereist is, bijvoorbeeld als je een wachtwoord moet invoeren.Bezig met downloaden...Tonen als er iets wordt gedownload.Nu aan het afspelenHiermee kun je het afspelen bedienen. Dit is de voornaamste melding tijdens het afspelen van een podcast.
- Foutmeldingen
- Wordt getoond als er iets misgaat, zoals downloaden of het bijwerken van de feed.
- Syschronisatiefouten
+ Downloaden mislukt
+ Wordt getoond als er iets misgaat, zoals het downloaden of het bijwerken van de feed.
+ Synchronisatie misluktWorden getoond als gpodder-synchronisatie mislukt.
- Automatisch downloaden
+ Automatisch downloaden voltooidWordt getoond als afleveringen automatisch zijn gedownload.
+ Nieuwe aflevering
+ Wordt getoond als er een nieuwe aflevering beschikbaar is en meldingen zijn ingeschakeld.WidgetinstellingenWidget maken
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 @@
StatystykiDodaj podcastOdcinki
+ KolejkaWszystkieNoweUlubione
@@ -17,7 +18,6 @@
DziennikSubskrypcjeLista subskrypcji
- Anuluj pobieranieHistoria odtwarzaniagpodder.netgpodder.net login
@@ -26,6 +26,7 @@
OdtwarzaniePobranePowiadomienia
+
Całkowity czas odtwarzania podcastów:%1$d z %2$d odcinków rozpoczęto.\n\nOdtworzono %3$s z %4$s.
@@ -81,7 +82,6 @@
Opis\u0020odcinkówPrzetwarzanie
- Zapisz nazwę użytkownika i hasłoZamknijSpróbuj ponownieDołącz do automatycznego pobierania
@@ -93,7 +93,6 @@
WyłączonaŚredniaSilna
- \u0020równoległych pobierańGlobalnie domyślnieZawszeNigdy
@@ -151,7 +150,6 @@
Nie w kolejceMa mediaPrzefiltrowany
- {fa-exclamation-circle} Ostatnie odświerzanie nie powiodło sięOtwórz PodcastProszę czekać aż dane zostaną załadowane
@@ -223,16 +221,12 @@
%1$s \n\nAdres pliku:\n%2$s
Nie znaleziono urządzenia docelowego
- Niewystarczająca ilość pamięciBłąd danych HTTPNieznany błąd
- Wyjątek parseraNieobsługiwany typ kanałuBłąd połączenia
- Nieznany hostBłąd autoryzacjiBłąd rodzaju pliku
- ZabronionePobieranie anulowanePobieranie zatrzymane\nWyłączone Automatyczne pobieranie dla tego elementuPobieranie ukończone
@@ -248,14 +242,7 @@
%d elementów zostało do pobrania%d elementów zostało do pobrania
- Przetwarzanie pobranychPobieranie danych podcastu
-
- %d pobranie udane, %d błędne
- %d pobrań udanych, %d błędnych
- %d pobrań udanych, %d błędnych
- %d pobrań udanych, %d błędnych
- Nieznany tytułKanałPlik multimedialny
@@ -344,7 +331,6 @@
PamięćAutomatyczne kasowanie odcinków, Import, EksportProjekt
- KolejkaSynchronizacjaSynchronizuj z innymi urządzeniami za pomocą gpodder.netAutomatyzacja
@@ -360,14 +346,9 @@
Wyczyść historięOdtwarzaczUsuwanie odcinków
- 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.Wstrzymaj odtwarzanie po rozłączeniu słuchawek lub BluetoothWznów odtwarzanie kiedy słuchawki zostaną podłączone ponownieWznów odtwarzanie po przywróceniu połączenia Bluetooth
- Przycisk \'Do przodu\' pomija odcinek
- Naciśnięcie przycisku \'Do przodu\' na urządzeniu bluetooth skacze do następnego odcinka, zamiast przewijać
- Przycisk wstecz restartuje
- Podczas odtwarzania przycisk wstecz restartuje zamiast przewijaćPrzeskocz do następnego elementu kolejki po zakończeniu odtwarzaniaUsuń odcinek kiedy jego odtwarzanie zostanie zakończoneAutomatyczne usuwanie
@@ -387,7 +368,6 @@
ZablokujUstaw częstotliwośćUstaw czas dnia
- co %1$so %1$sOdtwarzanie ciągłeRozłączenie słuchawek lub Bluetooth
@@ -422,7 +402,6 @@
Pamięć podręczna odcinkówCałkowita liczba odcinków zapisanych na urządzeniu. Automatyczne pobieranie zostanie przerwane, jeśli zostanie ona osiągnięta.Użyj okładek odcinków
- 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.Użyj motywu systemowegoJasnyCiemny
@@ -442,8 +421,6 @@
Wymuś pełną synchronizacjęSynchronizuj wszystkie subskrypcje oraz stan odcinków z pomocą gpodder.net.%1$s na urządzeniu %2$s]]>
- Błąd synchronizacji
- To ustawienie nie dotyczy błędów autoryzacji.Dostosuj prędkości dostępne dla odtwarzania o zmiennej prędkościPrędkość używana podczas odtwarzania odcinków z tego kanałuAutomatyczne pomijanie
@@ -458,8 +435,6 @@
Dostosuj liczbę sekund do przeskoczenia przy kliknięciu szybkiego przewijania do przoduPrzewijanie do tyłuDostosuj liczbę sekund do przeskoczenia przy kliknięciu przewijania do tyłu
- Ustaw nazwę hosta
- Użyj domyślnego hostaWysoki priorytet powiadomieniaRozwija powiadomienie, aby pokazać przyciski odtwarzaniaStałe przyciski odtwarzacza
@@ -470,10 +445,6 @@
Możesz tylko wybrać maksimum z %1$d przedmiotów.Ustaw tło ekranu blokadyUstaw tło ekranu blokowania na aktualny obraz odcinka. W efekcie będzie zawsze pokazywał obraz w innych aplikacjach.
- Błąd pobierania
- Jeżeli pobieranie się nie powiedzie, pokaż raport ze szczegółami błędu.
- Automatyczne pobieranie zakończone
- Pokazuj powiadomienie dla odcinków pobranych automatycznieAndroid starszy niż 4.1 nie wspiera rozszerzonych powiadomień.Pozycja w kolejceDodaj odcinki do: %1$s
@@ -494,14 +465,12 @@
Aktualna wartość: %1$sProxyUstaw proxy sieciowe
- Najczęściej zadawane pytaniaNie znaleziono przeglądarki.Obsługa ChromecastUruchom obsługę dla zdalnego odtwarzania mediów na innych urządzeniach (takich jak Chromecast, Audio Speakers albo Android TV)Chromecast wymagadodatkowych bibliotek, które są zablokowane w tej wersji AntennaPodKolejkuj pobraneDodaj pobrane odcinki do kolejki
- Wbudowany odtwarzacz AndroidaExoPlayer (rekomendowany)Zmień na ExoPlayerZmieniono na ExoPlayer
@@ -623,23 +592,12 @@
SUGESTIESzukaj na gpodder.netLogin
- Witamy w procesie logowania do gpodder.net. Najpierw podaj swoje dane logowania:Login
- Jeśli nie masz jeszcze konta, możesz utworzyć je tutaj:
-https://gpodder.net/register/Nazwa użytkownikaHasło
- Wybór urządzeniaUtwórz nowe urządzenie dla swojego konta na gpodder.net lub wybierz istniejące:
- Identyfikator urządzenia:\u0020
- Tytuł
- Utwórz nowe urządzenie
- Wybierz istniejące urządzenie:
- Identyfikator urządzenia nie może być pusty
- Identyfikator urządzenia w użyciuPole nie może być pusteWybierz
- Logowanie zakończone sukcesem!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. Rozpocznij synchronizacjęIdź do strony głównej
@@ -833,11 +791,7 @@ https://gpodder.net/register/Pokazywane podczas aktywnego pobierania.Teraz odtwarzanePozwala na kontrolowanie odtwarzania. To jest główne powiadomienie, które zobaczysz podczas odtwarzania podcastu.
- Błędy
- Pokazywane, gdy coś pójdzie nie tak, np. błąd pobierania lub błąd aktualizacji.
- Błędy synchronizacjiPokazywane, gdy synchronizacja z gpodder się nie powiedzie.
- Automatyczne pobieraniePokazywane, gdy odcinki zostały pobrane automatycznieUstawienia widżetu
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 @@
EstatísticasAdicionar PodcastEpisódios
+ FilaTodosNovoFavoritos
@@ -17,7 +18,7 @@
LogAssinaturasLista de Assinaturas
- Cancelar\nDownload
+ Cancelar DownloadHistórico de reproduçãogpodder.netgpodder.net login
@@ -26,6 +27,7 @@
ReproduçãoDownloadsNotificações
+
Tempo total de reprodução de episódios:%1$d de %2$d episódios iniciados.\n\nReproduzidos %3$s de %4$s.
@@ -53,6 +55,7 @@
NenhumNenhum aplicativo compatível encontrado
+ Exportar histórico detalhadoAbrir no navegadorCopiar URL
@@ -81,7 +84,6 @@
Descrição\u0020episódiosProcessando
- Salvar nome do usuário e senhaFecharTentar novamenteIncluir em downloads automáticos
@@ -93,7 +95,6 @@
DesligarLevePesado
- \u0020 downloads paralelosPadrão globalSempreNunca
@@ -145,7 +146,6 @@
Não enfileiradoPossui mídiaFiltrado
- {fa-exclamation-circle} Última Atualização falhouAbrir PodcastPor favor, aguarde até que os dados sejam carregados
@@ -206,16 +206,12 @@
Detalhes%1$s \n\nURL do arquivo:\n%2$sDispositivo de armazenamento não encontrado
- Espaço insuficienteErro de HTTP DataErro desconhecido
- Parser ExceptionTipo de feed não suportadoErro de conexão
- Host desconhecidoErro de autenticaçãoErro de Tipo de Arquivo
- ProibidoDownload canceladoDownload cancelado\nDesabilitado Download Automático para este itemDownloads finalizados com erro(s)
@@ -229,12 +225,7 @@
%d download restante%d downloads restantes
- Processando downloadsBaixando dados do podcast
-
- %ddownload com sucesso, %dfalhou
- %ddownloads com sucesso, %dfalharam
- Título desconhecidoFeedArquivo de mídia
@@ -323,7 +314,6 @@
ArmazenamentoExclusão automática de episódio, importação, exportaçãoProjeto
- FilaSincronizaçãoSincroniza com outros dispositivos usando gpodder.netAutomação
@@ -339,14 +329,9 @@
Limpar históricoReprodutor de mídiaLimpar Episódio
- 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ódiosPausar a reprodução quando o fone de ouvido ou bluetooth forem desconectadosContinuar a reprodução quando os fones de ouvido forem reconectadosContinuar a reprodução quando o bluetooth reconectar
- Botão avançar pula
- 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.
- Botão Anterior reinicia
- Ao pressionar o botão Anterior do hardware, reinicie a reprodução do episódio atual em vez de retrocederPular para próximo item da fila quando a reprodução terminarRemover episódio quando a reprodução for concluídaApagar automaticamente
@@ -366,7 +351,6 @@
DesabilitarConfigurar IntervaloConfigurar Tempo do dia
- cada %1$sàs %1$sReprodução contínuaFones de ouvido ou Bluetooth desconectado
@@ -401,7 +385,6 @@
Cache de episódiosNúmero total de episódios baixados em cache no dispositivo. O download automático será suspenso se esse número for atingido.Usar capa do episódio
- Use a capa específica do episódio sempre que disponível. Se desmarcado, o aplicativo sempre usará a imagem da capa do podcast.Usar tema do sistemaClaroEscuro
@@ -421,8 +404,6 @@
Forçar sincronização completaSincronizar os estados das inscrições e episódios com o gpodder.net.%1$s com o dispositivo %2$s]]>
- Sincronização falhou
- Essa configuração não se aplica a erros de autenticação.Personalize as velocidades disponíveis para reprodução de áudio.A velocidade a ser usada ao iniciar a reprodução de áudio para episódios neste podcastSalto automático
@@ -437,8 +418,6 @@
Personalize os segundos para avançar quando o botão avanço rápido for clicadoTempo de retrocederPersonalize os segundos para voltar quando o botão retroceder for clicado
- Configurar hostname
- Usar host padrãoPrioridade de notificação altaIsso geralmente expande a notificação para exibir botões de reprodução.Controles de Reprodução Persistentes
@@ -449,10 +428,6 @@
Você só pode selecionar no máximo %1$d itens.Configurar plano de fundo da tela de bloqueioConfigurar 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.
- Download falhou
- Se os downloads falharem, gerar um relatório que mostra os detalhes da falha.
- Download automático finalizado
- Mostra uma notificação para episódios baixados automaticamente.Versões do Android inferiores a 4.1 não suportam notificações expansíveisLocal da filaAdicionar episódios para: %1$s
@@ -473,14 +448,12 @@
Valor atual: %1$sProxyConfigurar um proxy da rede
- Perguntas mais frequentesNenhum navegador web encontrado.Suporte ao ChromecastHabilitar o suporte para reprodução remota de mídia em dispositivos Cast (como Chromecast, Caixa de som ou Android TV)O Chromecast necessita de bibliotecas proprietárias de terceiros que estão desativadas nesta versão do AntennaPodEnfileirar os baixadosAdicionar episódios baixados à fila
- Reprodutor próprio do AndroidExoPlayer (recomendado)Alterar para ExoPlayerAlterado para ExoPlayer
@@ -596,22 +569,12 @@
SUGESTÕESBuscar no gpodder.netLogin
- Bem-vindo ao processo de login gpodder.net. Primeiramente, digite suas informações:Login
- Se você ainda não possui uma conta, você pode criar uma aqui:\nhttps://gpodder.net/register/Nome do usuárioSenha
- Seleção de dispositivoCrie um novo dispositivo para usar em sua conta gpodder.net ou escolha um já existente:
- ID do dispositivo:\u0020
- Descrição do dispositivo
- Criar novo dispositivo
- Escolher dispositivo existente:
- ID do dispostivo não pode estar em branco
- ID do dispositivo já está em usoA legenda não deve ser vaziaEscolher
- Login realizado com sucesso!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.Iniciar sincronização agoraIr para tela principal
@@ -805,11 +768,7 @@
Exibido enquanto estiver baixando.Reproduzindo agoraPermite controlar a reprodução. Essa é a principal notificação vista ao reproduzir um podcast.
- Erros
- Exibido se algo der errado, por exemplo, se o download ou a atualização do feed falhar.
- Erros de sincronizaçãoExibido quando a sincronização do gpodder falhou.
- Downloads automáticosExibido quando os episódios foram baixados automaticamente.Configurações de widgets
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 @@
EstatísticasAdicionar podcastEpisódios
+ FilaTodosNovosFavoritos
@@ -17,7 +18,7 @@
RegistoSubscriçõesLista de subscrições
- Cancelar\ndescarga
+ Cancelar descargaHistórico de reproduçãogpodder.netDados gpodder.net
@@ -26,6 +27,8 @@
ReproduçãoDescargasNotificações
+
+ \"%1$s\" não encontradaTempo total dos episódios reproduzidos:%1$d de %2$d episódios iniciados.\n\nReproduzidos %3$s de %4$s.
@@ -53,6 +56,8 @@
NenhumNão existem aplicações compatíveis
+ Exportar registos detalhados
+ Este tipo de registos pode conter informação privada como, por exemplo, a sua lista de subscrições.Abrir no navegadorCopiar URL
@@ -81,7 +86,6 @@
Descrição\u0020episódiosA processar...
- Guardar utilizador e palavra-passeFecharTentar novamenteIncluir nas descargas automáticas
@@ -93,12 +97,13 @@
DesligadaLigeiraIntensa
- \u0020descargas simultâneas.
+ %1$d descargas em simultâneoPredefiniçõesSempreNuncaEnviar...Nunca
+ Se não for favoritoSe não estiver na filaAo terminar
@@ -113,7 +118,22 @@
%d selecionado%d selecionados
+
+ %d episódio
+ %d episódios
+ Carregar mais...
+ Notificações para episódios
+ Mostrar notificação sempre que for disponibilizado um novo episódio.
+
+ %2$s tem um novo episódio
+ %2$s tem%1$d novos episódios
+
+
+ Novo episódio
+ Novos episódios
+
+ Existem novos episódios nas suas subscrições.Marcar tudo como reproduzidoMarcar todos os episódios como reproduzidos
@@ -145,7 +165,6 @@
Não na filaTem ficheiroFiltrados
- {fa-exclamation-circle} Última atualização falhadaAbrir podcastAguarde pelo carregamento dos dados...
@@ -160,6 +179,10 @@
EliminarEpisódio não eliminado. Tente reiniciar o dispositivo.Eliminar episódio
+
+ %d episódio selecionado, %d descarga removida.
+ %d episódios selecionados, %d descargas removidas.
+ Remover a marca \"novo\"A marca \"novo\" foi removidaMarcar como reproduzido
@@ -206,16 +229,12 @@
Detalhes%1$s \n\Ficheiro URL:\n%2$sCartão SD não encontrado
- Espaço insuficienteErro HTTPErro desconhecido
- Exceção do processadorFonte não suportadaErro de ligação
- Servidor desconhecidoErro de autenticaçãoErro do tipo de ficheiro
- ProibidoDescarga canceladaDescarga cancelada\nDescarga automática desativada para este itemDescargas terminadas com erros
@@ -229,12 +248,7 @@
%d descarga em curso%d descargas em curso
- Processamento de descargasA descarregar dados do podcast
-
- %d descarga com sucesso, %d com falha
- %d descargas com sucesso, %d com falha
- Título desconhecidoFonteFicheiro multimédia
@@ -267,6 +281,7 @@
Modo \'picture-in-picture\'Tecla multimédia desconhecida: %1$dFicheiro não encontrado
+ O item não contém um ficheiro multimédiaBloquear filaDesbloquear fila
@@ -323,7 +338,6 @@
ArmazenamentoEliminação automática, importação e exportaçãoProjeto
- FilaSincronizaçãoSincronizar com outros dispositivos via gpodder.netAutomatização
@@ -334,19 +348,24 @@
Elementos externosInterrupçõesControlo de reprodução
+ Reatribuir botões do dispositivoPesquisar...Não existem resultadosLimpar históricoReprodutor multimédiaLimpeza de episódios
- 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.
+ Episódios que são elegíveis para remoção se a Descarga automática precisar de espaço para novos episódiosPausa na reprodução ao desligar os auscultadores ou o bluetooth.Continuar reprodução ao ligar os auscultadores.Continuar reprodução ao estabelecer a ligação bluetooth.
- Botão \'Seguinte\' para avançar
- Ao premir o botão Seguinte no dispositivo bluetooth, ir para o episódio seguinte em vez de avançar a reprodução.
- Botão \'Anterior\' para reiniciar
- Ao premir o botão Anterior no dispositivo, reiniciar o episódio atual em vez de recuar para o episódio anterior.
+ Botão Seguinte
+ Personalizar comportamento do botão Seguinte
+ Botão Anterior
+ Personalizar comportamento do botão Anterior
+ Avanço rápido
+ Recuo rápido
+ Ignorar episódio
+ Reiniciar episódioIr para a episódio seguinte ao terminar a reprodução.Eliminar episódio ao terminar a reprodução.Eliminação automática
@@ -366,8 +385,11 @@
DesativarDefinir intervaloDefinir hora do dia
- a cada %1$sàs %1$s
+
+ A cada hora
+ A cada %d horas
+ Reprodução contínuaAuscultadores ou Bluetooth desligadosAuscultadores inseridos
@@ -401,7 +423,9 @@
Cache de episódiosNúmero máximo de episódios descarregados para colocar em cache. A descarga automática será suspensa se este número for atingido.Utilizar capa do episódio
- Utilizar imagem do episódio se disponível. Se desativar esta opção, será utilizada a imagem do podcast.
+ Utilizar imagem do episódio, se disponível. Se desativar esta opção, será utilizada a imagem do podcast.
+ Mostrar tempo restante
+ Mostra o tempo restante do episódio se a opção estiver ativa. Se desativar esta opção, será mostrado o tempo total.Utilizar tema do sistemaClaroEscuro
@@ -421,8 +445,6 @@
Impor sincronização totalSincronizar todas as subscrições e estados dos episódios com o gpodder.net.%1$s com o dispositivo %2$s]]>
- Falha de sinconização
- Esta definição não é aplicável aos erros de autenticação.Personalizar disponibilidade das velocidades variáveis de reproduçãoVelocidade utilizada para a reprodução áudio dos episódios deste podcastIgnorar automaticamente
@@ -437,8 +459,6 @@
Personalizar o número de segundos a avançar ao tocar no botão de avanço rápido.Tempo a recuarPersonalizar o número de segundos a recuar ao tocar no botão de recuo rápido.
- Definir nome de servidor
- Utilizar predefiniçõesPrioridade da notificaçãoNormalmente, esta opção é utilizada para expandir a notificação e mostrar os botões de reprodução.Controlos de reprodução
@@ -449,10 +469,6 @@
Apenas pode selecionar um máximo de %1$d itens.Definir fundo do ecrã de bloqueioDefine a imagem do episódio como fundo do ecrã de bloqueio. Efeito colateral: também será mostrada em outras aplicações.
- Falha ao descarregar
- Se a descarga falhar, gera um relatório que mostra os detalhes do erro.
- Descarga automática terminada
- Mostrar uma notificação para episódios descarregados automaticamente.As versões Android anteriores à 4.1 não possuem suporte à expansão de notificaçõesLocalização na filaAdicionar episódios: %1$s
@@ -462,6 +478,7 @@
DesativadaCache de imagensTamanho para a cache de imagens.
+ Documentação e ajudaFórum de utilizadoresReporte de errosAbrir rastreador de erros
@@ -473,14 +490,14 @@
Valor atual: %1$sProxyDefinir um proxy de rede.
- Questões frequentesNavegador web não encontradoSuporte ChromecastAtivar suporte a reprodução multimédia em dispositivos Cast (tais como Chromecast, Android TV...)Chromecast necessita de bibliotecas proprietárias de terceiros que estão desativadas nesta versão do AntennaPod.Colocar descargas na filaAdicionar à fila os episódios descarregados.
- Reprodutor nativo Android
+ Reprodutor nativo Android (descontinuado)
+ Reprodutor multimédia Sonic (descontinuado)ExoPlayer (recomendado)Trocar para ExoPlayerTrocou para ExoPlayer.
@@ -569,6 +586,7 @@
Definir temporizadorDesativar temporizador
+ +%d min.TemporizadorTem que introduzir um número inteiroAgitar para repor
@@ -596,22 +614,22 @@
SugestõesPesquisar em gpodder.netAcesso
- Bem-vindo ao processo de acesso ao gpodder.net. Introduza os dados de acesso:Acesso
- Se ainda não possui uma conta, pode criar uma em:\nhttps://gpodder.net/register/
+ Criar contaUtilizadorPalavra-passe
- Seleção de dispositivo
+ Gpodder.net é um serviço \'open source\' para sincronização de podcasts, independente, e que não está relacionado com o projeto AntennaPod.
+ Servidor oficial gpodder.net
+ Servidor personalizado
+ Nome do servidor
+ Selecione o servidorCriar um novo dispositivo ou escolher um existente para aceder à sua conta gpodder.net
- ID do dispositivo:\u0020
- Nome
- Criar novo dispositivo
- Escolher dispositivo:
- ID do dispositivo não pode estar vazia
- ID de dispositivo já utilizada
+ Nome do dispositivo
+ AntennaPod em %1$sA descrição não pode estar vazia
+ Dispositivos existentes
+ Criar dispositivoEscolher
- Sessão iniciada!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.Sincronizar agoraIr para o ecrã principal
@@ -665,6 +683,7 @@
Trocar de páginasPosição: %1$sAplicar ação
+ Reproduzir capítuloAutenticaçãoAltere o seu nome de utilizador e palavra-passe para este podcast e seus episódios
@@ -799,18 +818,22 @@
O reprodutor encontrou um erro críticoErro de reprodução. A ignorar...
+ Erros
+ NotíciasRequer açãoMostrar se for necessária uma ação como, por exemplo, digitar uma palavra-passe.A descarregarMostrar durante a descarga.Reprodução atualPermite o controlo da reprodução. Esta será a notificação que verá ao reproduzir um podcast.
- Erros
- Mostrar se ocorrerem erros como, por exemplo, não for possível descarregar ou atualizar a fonte.
- Erros de sincronização
+ Erro ao descarregar
+ Mostrada se ocorrerem erros ao descarregar/atualizar.
+ Erro de sincronizaçãoMostrar se não for possível sincronizar com gpodder.
- Descargas automáticas
+ Descarga automática terminadaNotificar quando os episódios forem descarregados automaticamente.
+ Novo episódio
+ Mostrada se for encontrado um novo episódio de uma fonte, se as notificações estiverem ativas.Definições do widgetCriar widget
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 @@
СтатистикаДобавить подкастВыпуски
+ ОчередьВсеНовыеИзбранное
@@ -26,6 +27,8 @@
ВоспроизведениеЗагрузкиУведомления
+
+ \"%1$s\" не найденоОбщее время прослушивания выпусков:%1$d из %2$d выпусков начато.\n\nПрослушано %3$s из %4$s.
@@ -53,6 +56,8 @@
НичегоСовместимых приложений не найдено
+ Экспортировать подробные логи
+ Подробные логи могут содержать конфиденциальные данные, такие как перечень ваших подписокОткрыть в браузереСкопировать ссылку
@@ -81,7 +86,6 @@
Описание\u0020выпуск(ов)Обработка
- Сохранить имя пользователя и парольЗакрытьПовторитьДобавить в автозагрузки
@@ -93,12 +97,13 @@
ВыключеноСлабоеСильное
- \u0020одновременных загрузок
+ %1$d одновременных загрузокПо умолчанию для всехВсегдаНикогдаОтправить…Никогда
+ Когда не в избранномКогда не в очередиПосле прослушивания
@@ -119,7 +124,28 @@
Выбрано: %dВыбрано: %d
+
+ %d выпуск
+ %d выпуска
+ %d выпусков
+ %d выпусков
+ Загружается…
+ Уведомление о новом выпуске
+ Показывать уведомление при появлении новых выпусков
+
+ %2$s: новый выпуск
+ %2$s: %1$d новых выпуска
+ %2$s: %1$d новых выпусков
+ %2$s: %1$d новых выпусков
+
+
+ Новый выпуск
+ Новые выпуски
+ Новые выпуски
+ Новые выпуски
+
+ В списке Ваших подписок появились новые выпускиОтметить как прослушанноеОтметить все выпуски как прослушанные
@@ -151,7 +177,6 @@
Не в очередиС файламиОтфильтровано
- {fa-exclamation-circle} Последнее обновление не удалосьОткрыть подкастПодождите, пока загружаются данные
@@ -168,6 +193,12 @@
УдалитьНевозможно удалить файл. Попробуйте перезагрузить устройство.Удалить выпуск
+
+ %d выпуск выбран, %d загрузка удалена
+ %d выпуска выбрано, %d загрузка(и) удалены
+ %d выпусков выбрано, %d загрузок удалено
+ %d выпусков выбрано, %d загрузок удалено
+ Убрать пометку «Новый»Пометка «Новый» убранаОтметить как прослушанное
@@ -222,16 +253,12 @@
Подробнее%1$s \n\nСсылка на файл:\n%2$sУстройство хранения не найдено
- Недостаточно местаОшибка данных HTTPНеизвестная ошибка
- Ошибка обработкиНеподдерживаемый тип каналаОшибка соединения
- Неизвестный узелОшибка авторизацииОшибка типа файла
- ЗапрещеноЗагрузка отмененаЗагрузка отменена\n Автозагрузка отключена для этого выпускаЗагрузки завершились с ошибкой
@@ -247,14 +274,7 @@
Осталось %d загрузокОсталось %d загрузок
- Производится загрузкаПолучение данных подкаста
-
- %d загрузка завершена, %d не удалось
- %d загрузок завершено, %d не удалось
- %d загрузок завершено, %d не удалось
- %d загрузок завершено, %d не удалось
- Неизвестное названиеКаналМедиафайл
@@ -287,6 +307,7 @@
Картинка в картинкеAntennaPod - неизвестный ключ носителя: %1$dФайл не найден
+ Элемент не содержит медиафайлаЗаблокировать очередьРазблокировать очередь
@@ -343,7 +364,6 @@
ХранилищеАвтоматическое удаление выпусков, импорт, экспортПроект
- ОчередьСинхронизацияСинхронизация с другими устройствами с помощью gpodder.netАвтоматизация
@@ -354,19 +374,24 @@
Внешние органы управленияПрерыванияУправление воспроизведением
+ Переназначить физические кнопкиНайти…Нет результатовОчистить историюПроигрывательУдаление выпусков
- Выпуски, которые не стоят в очереди и не отмечены как избранные могут быть удалены для освобождения места под Автозагрузку.
+ Выпуски, которые должны быть доступны для удаления, если автозагрузке потребуется пространство для новых выпусковПоставить на паузу, когда наушники или Bluetooth отключеныПродолжать воспроизведение после подключения наушниковВозобновить, когда восстановится Bluetooth-соединение
- Пропускать кнопкой перемотки вперед
- При нажатии кнопки перемотки вперед на Bluetooth-устройстве включить следующий выпуск
- В начало кнопкой перемотки назад
- При нажатии на физическую кнопку перемотки назад переходить к началу выпуска вместо перемотки назад
+ Кнопка Вперед
+ Настройка поведения кнопки Вперед
+ Кнопка Назад
+ Настройка поведения кнопки Назад
+ Перемотка вперед
+ Перемотка назад
+ Пропустить выпуск
+ Повторить выпускПосле завершения воспроизведения перейти к следующему в очередиУдалять выпуск после воспроизведенияАвтоматическое удаление
@@ -386,8 +411,13 @@
ОтключитьЗадать интервалВыбрать время
- каждые %1$sв %1$s
+
+ Каждый час
+ Каждые %d часов
+ Каждые %d часов
+ Каждые %d часов
+ Непрерывное воспроизведениеОтключение наушников или BluetoothПри подключении наушников
@@ -421,7 +451,9 @@
Кэш выпусковОбщее количество загруженных в кэш выпусков. По достижении этого количества автоматическая загрузка будет приостановлена.Использовать обложку выпуска
- Отображать вместо обложки подкаста обложку выпуска, если она доступна.
+ Отображать обложку выпуска в списках, если она доступна. Если не выбрано, приложение будет всегда использовать обложку подкаста.
+ Показывать оставшееся время
+ Отображать оставшееся время выпусков. Если не выбрано, будет отображаться общее время.Использовать системное оформлениеСветлаяТемная
@@ -441,8 +473,6 @@
Выполнить полную синхронизациюСинхронизировать состояния всех подписок и выпусков при помощи gpodder.net.%1$s с устройства %2$s]]>
- Сбой синхронизации
- Не затрагивает ошибки авторизации.Выбрать значения скорости, доступные при воспроизведенииСкорость, с которой будут изначально воспроизводиться выпуски этого подкастаАвтоматический пропуск
@@ -457,8 +487,6 @@
Настройте длину шага в секундах при нажатии кнопки перемотки впередИнтервал быстрой перемотки назадНастройте длину шага в секундах при нажатии кнопки перемотки назад
- Задать имя узла
- Использовать узел по умолчаниюУведомление с высоким приоритетомКак правило, разворачивает уведомление, показывая кнопки управления воспроизведением.Постоянные кнопки воспроизведения
@@ -469,10 +497,6 @@
Нельзя выбрать больше %1$d элементов.Менять фон экрана блокировкиИзменяет фон экрана блокировки на обложку выпуска. Кроме того показывает обложку в сторонних приложениях.
- Сбой загрузки
- Если загрузка не удается, показывать отчет с подробностями об ошибке.
- Автоматическая загрузка завершена
- Показывать уведомление при автоматической загрузке выпусковВерсии Android ниже 4.1 не поддерживают расширенные уведомления.Размещение в очередиДобавлять выпуски %1$s
@@ -482,6 +506,7 @@
ОтключеноРазмер кеша изображенийРазмер дискового кеша изображений
+ Документация и ПоддержкаФорум пользователейСообщить об ошибкеПерейти в систему отслеживания ошибок
@@ -493,14 +518,14 @@
Текущее значение: %1$sПроксиНастройки прокси
- Часто задаваемые вопросыВеб-браузер не обнаружен.Поддержка ChromecastВключить воспроизведение на устройствах с Google Cast (Chromecast, колонки, ТВ на Android TV и др.)Для работы Chromecast требуются собственнические библиотеки третьей стороны, которые не включены в данную версию AntennaPodДобавлять загруженные в очередьДобавлять загруженные выпуски в очередь
- Встроенный в Android
+ Встроенный Android плеер (устарело)
+ Sonic Media Player (устарело) ExoPlayer (рекомендовано)Переключить на ExoPlayerПереключено на ExoPlayer.
@@ -589,6 +614,7 @@
Установить таймер снаОтключить таймер сна
+ +%d мин.Таймер снаНеправильный ввод, время должно быть в виде числаСбросить встряхиванием
@@ -622,22 +648,22 @@
РекомендацииИскать на gpodder.netВойти
- Добро пожаловать в процесс авторизации на gpodder.net. Сначала введите вашу информацию для авторизации:Войти
- Если у вас еще нет аккаунта, вы можете создать его здесь:\nhttps://gpodder.net/register/
+ Создать аккаунтИмя пользователяПароль
- Выбор устройства
+ Gpodder.net - сервис управления подкастами с отрытым исходным кодом независящий от проекта AntennaPod.
+ Официальный сервер gpodder.net
+ Свой сервер
+ Имя хоста
+ Выберите серверСоздайте новое устройство, чтобы использовать ваш аккаунт на gpodder.net или выберите существующее:
- Идентификатор устройства:\u0020
- Название устройства
- Создать новое устройство
- Выберите существующее устройство:
- Поле с Device ID не должно быть пустым
- Device ID уже используется
+ Название устройства
+ AntennaPod на %1$sОбязательно заполнить
+ Существующие устройства
+ Создать устройствоВыберите
- Авторизация успешна!Поздравляем! Ваш аккаунт на gpodder.net теперь связан с вашим устройством. AntennaPod теперь сможет автоматически синхронизировать ваши подписки с аккаунтом gpodder.netНачать синхронизациюПерейти на главный экран
@@ -691,6 +717,7 @@
Переключить страницуПозиция: %1$sПрименить действие
+ Запустить главуАвторизацияИзменить имя пользователя и пароль для этого подкаста и его выпусков.
@@ -699,7 +726,7 @@
Перечень условий по включению или исключению выпуска из списков автоматической загрузкиВключитьИсключить
- По одному слову \n«По фразе»
+ По одному слову \n\"По фразе\"Постоянно обновлятьОбновлять подкаст при (авто)обновлении всех подкастовАвтоматическая загрузка отключена в основных настройках AntennaPod
@@ -825,18 +852,22 @@
Серьезная ошибка воспроизведения в устройстве Google castОшибка воспроизведения. Пропускаю…
+ Ошибки
+ ОбновленияТребуется действиеПоказывается, когда от Вас требуется действие, например, ввести пароль.ЗагружаетсяПоказывается во время загрузки.Сейчас воспроизводитсяПозволяет управлять воспроизведением. Основное уведомление, показывается при воспроизведении подкаста.
- Ошибки
- Отображается, если что-то пошло не так, например, если не удалось загрузить или обновить канал.
- Ошибки синхронизации
+ Сбой загрузки
+ Отображается, когда загрузка или обновление канала завершилось с ошибкой
+ Сбой синхронизацииОтображается, если сбоит синхронизация gpodder.
- Автозагрузка
+ Автоматическая загрузка завершенаПоказывается, когда новые выпуски были автоматически загружены
+ Новый выпуск
+ Отображается, когда появился новый выпуск подкаста, для которого включены уведомленияНастройки виджетаСоздать виджет
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 @@
+
+
+
+ Aktualizovať odbery
+ Podcasty
+ Štatistiky
+ Pridať podcast
+ Epizódy
+ Všetko
+ Nové
+ Obľúbené
+ Nové
+ Nastavenia
+ Preberanie
+ Bežiace
+ Dokončené
+ Záznam
+ Odbery
+ Zoznam odberov
+ Zrušiť preberanie
+ História prehrávania
+ gpodder.net
+ Prihlásenie do gpodder.net
+ Vyrovnávacia pamäť epizód je plná
+ Bol dosiahnutý prednastavený limit veľkosti vyrovnávacej pamäte epizód. Veľkosť vyrovnávacej pamäte môžete zmeniť v Nastaveniach.
+ Prehrávanie
+ Preberania
+ Oznámenia
+
+ \"%1$s\" nebol nájdený
+
+ Celkový čas prehrávaných podcastov:
+ %1$d z %2$d epizód sa začala prehrávať.\n\nPrehrávané %3$s z %4$s.
+ Režim štatistík
+ 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
+ Súčet všetkých epizód, ktoré boli označené ako prehrané.
+ Poznámka: Rýchlosť prehrávania sa nikdy neberie do úvahy.
+ Vynulovať štatistiky
+ 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?
+ Od %s,\nste prehrali
+
+ Celková veľkosť epizód v zariadení:
+
+ Otvoriť menu
+ Zatvoriť menu
+ Nastavenie lišty
+ Usporiadať podľa počítadla
+ Usporiadať abecedne
+ Usporiadať podľa dátumu zverejnenia
+ Usporiadať podľa počtu prehratých epizód
+ Počet nových a neprehratých epizód
+ Počet nových epizód
+ Počet neprehratých epizód
+ Počet stiahnutých epizód
+ Žiadne
+
+ Žiadne kompatibilné aplikácie
+ Exportovať podrobné záznamy
+ Podrobné záznamy môžu obsahovať citlivé informácie ako napr. váš zoznam odoberaných kanálov
+
+ Otvoriť v prehliadači
+ Kopírovať odkaz
+ Zdielať odkaz
+ Odkaz skopírovaný do schránky
+ Skoč na túto pozíciu
+
+ Vymazať históriu
+
+ Potvrdiť
+ Zrušiť
+ Áno
+ Nie
+ Vynulovať
+ Autor(i)
+ Jazyk
+ Odkaz
+ Obrázok
+ Chyba
+ Vyskytla sa chyba:
+ Pre túto operáciu je potrebné povolenie prístupu k úložisku
+ Obnoviť
+ Nie je k dispozícii žiadne externé úložisko. Skontrolujte, či je externé úložisko pripojené tak, aby aplikácia mohla fungovať správne.
+ Kapitoly
+ Trvanie: %1$s
+ Popis
+ \u0020epizódy
+ Spracováva sa
+ Ukončiť
+ Skúsiť znova
+ Pridať do automatického sťahovania
+ Použiť aj na predchádzajúce epizódy
+ Nové nastavenie Automatické preberanie sa automaticky použije pre nové epizódy.\nChcete ho tiež použiť pre už vydané epizódy?
+ Automaticky vymazať epizódu
+ Redukcia hlasitosti
+ Redukcia hlasitosti pre všetky epizódy tohoto odberu: %1$s
+ Vypnutá
+ Slabá
+ Silná
+ %1$d súbežné preberania
+ Globálne predvolené
+ Vždy
+ Nikdy
+ Poslať...
+ Nikdy
+ Ak nie je v poradí
+ Po dokončení
+
+ 1 hodinu po dokončení
+ %d hodiny po dokončení
+ %d hodín po dokončení
+ %dhodín po dokončení
+
+
+ 1 deň po dokončení
+ %d dni po dokončení
+ %d dní po dokončení
+ %d dní po dokončení
+
+
+ %d vybraná
+ %d vybrané
+ %d vybraných
+ %d vybraných
+
+
+ %d epizóda
+ %d epizódy
+ %d epizód
+ %d epizód
+
+ Načítavam viac...
+ Oznámenia o epizódach
+ Pri vydaní novej epizódy zobraziť oznámenie.
+
+ Nová epizóda
+ Nové epizódy
+ Nové epizódy
+ Nové epizódy
+
+
+ Označiť všetko ako prehraté
+ Označiť všetky epizódy ako prehraté
+ Odstrániť všetky značky „nové“
+ Všetky značky „nové“ boli odstránené
+ Potvrďte, že chcete odstrániť značku „nové“ zo všetkých epizód.
+ Zobraziť informácie
+ Zobraziť nastavenia podcastu
+ Informácie o podcaste
+ Nastavenia podcastu
+ Premenovať podcast
+ Odstrániť podcast
+ Zdieľanie
+ Zdieľať...
+ Zdielať súbor
+ Adresa webovej stránky
+ Potvrďte, prosím, že chcete odstrániť podcast \"%1$s\". Súbory v miestnom priečinku zdroja sa neodstránia.
+ Odstraňovanie podcastu
+ Obnoviť celý podcast
+ Viacnásobný výber
+ Vybrať všetky nad
+ Vybrať všetky pod
+ Neprehraté
+ V poradí
+ Mimo poradia
+ S médiami
+ Filtrované
+ Otvoriť podcast
+ Počkajte, kým sa dáta načítajú
+
+ Stiahnuť
+
+ Sťahuje sa %d epizóda.
+ Sťahujú sa %d epizódy.
+ Sťahuje sa %d epizód.
+ Sťahuje sa %d epizód.
+
+ Prehrať
+ Pozastaviť
+ Streamovať
+ Vymazať
+ Nemožno vymazať súbor. Skúste reštartovať zariadenie.
+ Odstrániť epizódu
+ Odstrániť značku „nové“
+ Značka „nové“ bola odstránená
+ Označiť ako prehraté
+ Označené ako prehraté
+ Označiť ako prečítané
+ Označené ako prečítané
+ Preskočenie na určitú pozíciu funguje len pri prehrávaní epizódy.
+
+ %depizóda bola označená ako prehraná.
+ %depizódy boli označené ako prehrané.
+ %depizód bolo označených ako prehrané.
+ %depizód bolo označených ako prehrané.
+
+ Označiť ako neprehrané
+ Označiť ako prehrané
+
+ %depizóda bola označená ako neprehraná.
+ %depizódy bolo označené ako neprehrané.
+ %depizód bolo označených ako neprehrané.
+ %depizód bolo označených ako neprehrané.
+
+ Pridať do poradia
+ Pridané do poradia
+
+ %depizóda bola pridaná do poradia
+ %depizódy boli pridaná do poradia
+ %depizód bolo pridaných do poradia
+ %depizód bolo pridaných do poradia
+
+ Odstrániť z poradia
+
+ %depizóda bola odstránená z poradia
+ %depizódy boli odstránené z poradia
+ %depizód bolo odstránených z poradia
+ %depizód bolo odstránených z poradia
+
+ Pridať medzi obľúbené
+ Pridané do obľúbených
+ Odstrániť z obľúbených
+ Odstránené z obľúbených
+ Navštíviť webovú stránku
+ Preskočiť epizódu
+ Aktivovať automatické sťahovanie
+ Deaktivovať automatické sťahovanie
+ Obnoviť pozíciu prehrávania
+ Položka bola odstránená
+ Nie sú vybrané žiadne položky
+
+ dokončené
+ Čakajúce preberania
+ Prebieha preberanie
+ Podrobnosti
+ %1$s \n\nURL adresa súboru:\n%2$s
+ Zariadenie úložiska nebolo nájdené
+ Chyba dát HTTP
+ Neznáma chyba
+ Nepodporovaný typ zdroja
+ Chyba pripojenia
+ Chyba overenia
+ Chyba typu súboru
+ Preberanie zrušené
+ Pri sťahovaní nastali chyby
+ Hlásenie o preberaniach
+ Chybný odkaz
+ Chyba IO
+ Chyba požiadavky
+ Chyba prístupu k databáze
+
+ Zostáva %d súbor na stiahnutie
+ Zostávajú %d súbory na stiahnutie
+ Zostáva %d súborov na stiahnutie
+ Zostáva %d súborov na stiahnutie
+
+ Sťahujú sa údaje podcastu
+ Neznámy titul
+ Zdroj
+ Mediálny súbor
+ Pri pokuse o prevzatie súboru sa vyskytla chyba:\u0020
+ Potvrďte sťahovanie cez mobilné dáta
+ 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\nVaša voľba bude platná počas nasledujúcich 10 minút pre všetky ďalšie sťahovania epizód.
+ Sťahovanie cez mobilné dáta je zakázané v nastaveniach.\n\nChcete dočasne povoliť sťahovanie?\n\nVaša voľba bude platná počas nasledujúcich 10 minút.
+ Vždy
+ Teraz
+ Dočasne povoliť
+
+ Chyba!
+ Nič sa neprehráva
+ Prebieha príprava
+ Pripravený
+ Hľadá sa
+ Server zomrel
+ Nepodporovaný typ média
+ Neznáma chyba
+ Režim obraz v obraze
+ Súbor nenájdený
+
+ Znovu nezobrazovať
+ Späť
+ Presunúť sa na začiatok
+ Presunúť sa na koniec
+ Zoradiť
+ Udržať zoradené
+ Dátum
+ Trvanie
+ Názov epizódy
+ Názov podcastu
+ Náhodne
+ Vzostupne
+ Zostupne
+
+ Stiahnuť doplnok
+ Doplnok nie je nainštalovaný
+
+ Žiadne stiahnuté epizódy
+ Žiadna história
+ Žiadne epizódy
+ Žiadne nové epizódy
+ Žiadne obľúbené epizódy
+ Žiadne kapitoly
+ Táto epizóda nemá žiadne kapitoly.
+ Žiadne odbery
+
+ Úložisko
+ Projekt
+ Synchronizácia
+ Synchronizácia s inými zariadeniami pomocou gpodder.net
+ Automatizácia
+ Podrobnosti
+ Import/Export
+ zálohovanie, obnovenie, záloha, obnova, backup, restore
+ Vzhľad
+ Vonkajšie prvky
+ Prerušenia
+ Ovládanie prehrávania
+ Hľadať…
+ Žiadne výsledky
+ Vymazať históriu
+ Prehrávač médií
+ Odstrániť epizódu po dokončení prehrávania
+ Automatické mazanie
+ Ponechať epizódy ktoré sú označené ako obľúbené
+ Ponechať obľúbené epizódy
+ Prehrávanie
+ Sieť
+ Automatické sťahovanie
+ Stiahnutie epizódy
+ Používateľské rozhranie
+ Vybrať motív
+ Zmeňte vzhľad AntennaPod.
+ Automatické sťahovanie
+ Paralelné sťahovanie
+ Použiť systémový motív
+ Svetlá
+ Tmavá
+ Čierna (pre AMOLED)
+ Prihlásiť
+ Odhlásiť
+ Automatické preskočenie
+ Preskočiť úvodné a záverečné reči.
+ Preskočit posledných
+ Preskočiť prvých
+ Posledných %d sekúnd bolo preskočených
+ Prvých %d sekúnd bolo preskočených
+ Veľkosť vyrovnávacej pamäte pre obrázky
+ Veľkosť vyrovnávacej pamäte disku pre ukladanie obrázkov.
+ Nahlásiť chybu
+ Proxy
+ ExoPlayer (odporúčané)
+ Prepnúť na ExoPlayer
+ Prepnuté na ExoPlayer.
+ Preskočiť ticho v zvuku
+
+ O aplikácii
+ Verzia AntennaPod
+ Prispievatelia
+ Vývojári
+ Prekladatelia
+ Špeciálne poďakovanie
+ Zásady ochrany osobných údajov
+ Licencie
+
+
+
+ Databáza
+ OPML
+ HTML
+ Importovať databázu AntennaPod z iného zariadenia
+ Importovať súbor OPML
+ Pri načítaní súboru OPML sa vyskytla chyba:
+ Označiť všetko
+ Zrušiť označenie
+ Exportovať ako OPML
+ Exportovať ako HTML
+ Export databázy
+ Import databázy
+ Chyba exportu
+ Export bol úspešný
+ Exportovaný súbor bol uložený do:\n\n%1$s
+ Na načítanie súboru OPML je potrebný prístup k externému úložisku
+ Vyberte súbor, ktorý chcete importovať
+
+ sekundy
+ minúty
+ hodiny
+
+ 1 sekunda
+ %d sekundy
+ %d sekúnd
+ %d sekúnd
+
+
+ 1 minúta
+ %d minúty
+ %d minút
+ %d minút
+
+
+ 1 hodina
+ %d hodiny
+ %d hodín
+ %d hodín
+
+
+ Heslo
+ chyba overenia gpodder.net
+ chyba synchronizácie s gpodder.net
+ Počas synchronizácie sa vyskytla chyba:\u0020
+
+ Zvoľte priečinok s údajmi
+ Vytvoriť nový priečinok s názvom \"%1$s\"?
+ Nový priečinok bol vytvorený
+
+
+ Audio
+ Video
+
+ Nastavenia automatického preberania
+ Filter epizód
+ Pridať
+ Vylúčiť
+ Priebežne aktualizovať
+
+ Inovácia databázy
+
+
+ Pokročilé
+ Prehľadávať gpodder.net
+ Prehľadávať
+ viac »
+
+ Filter
+
+ Všetko
+ Je v obľúbených
+ Nie je v obľúbených
+ Stiahnuté
+ Nestiahnuté
+ V poradí
+ Nie je v poradí
+ Pozastavené
+ Prehraté
+
+ Názov (A \u2192 Z)
+ Názov (A \u2192 Z)
+ Dátum (Nové \u2192 Staré)
+ Dátum (Staré \u2192 Nové)
+ Trvanie (Krátke \u2192 Dlhé)
+ Trvanie (Dlhé \u2192 Krátke)
+ A \u2192 Z
+ Z \u2192 A
+ Nové \u2192 Staré
+ Staré \u2192 Nové
+ Krátke \u2192 Dlhé
+ Dlhé \u2192 Krátke
+
+ Páči sa vám AntennaPod?
+ Boli by sme radi, keby ste si našli čas a ohodnotili AntennaPod.
+ Dajte mi pokoj
+ Pripomenúť neskôr
+ Jasné, ideme na to!
+
+
+ Ovládanie zvuku
+ Rýchlosť prehrávania
+ Zvuk
+ L
+ R
+ Zvukové efekty
+ Iba Sonic
+ Iba ExoPlayer
+
+ Typ
+ Hostiteľ
+ Port
+ (Voliteľné)
+ Test
+ Prebieha kontrola…
+ Test bol úspešný
+ Test zlyhal
+ Hostiteľ nemôže byť prázdny
+ Hostiteľ nie je platná adresa IP alebo doména
+ Neplatný port
+
+ Počet stĺpcov
+
+ Nepodarilo sa spustiť prehrávanie média
+ Nepodarilo sa zastaviť prehrávanie média
+ Nepodarilo sa pozastaviť prehrávanie média
+ Hlasitosť sa nepodarilo nastaviť
+ Prehrávač narazil na vážnu chybu
+ Pri prehrávaní média sa vyskytla chyba. Preskočí sa…
+
+ Vyžaduje sa akcia
+ Sťahovanie
+
+ Nastavenia miniaplikácie
+ Vytvoriť miniaplikáciu
+ Nepriehľadnosť
+
+ Nastavenie bolo úspešne aktualizované.
+
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 @@
StatistikLägg till podcastEpisoder
+ KöAllaNyttFavoriter
@@ -17,7 +18,7 @@
LoggPrenumerationerPrenumerationslista
- Avbryt\nNedladdning
+ Avbryt NedladdningUppspelningshistorikgpodder.netInloggning till gpodder.net
@@ -26,6 +27,8 @@
UppspelningNedladdningarNotifieringar
+
+ \"%1$s\" hittades inteTotal uppspelningstid:%1$d av %2$d episoder startade.\n\nSpelat %3$s av %4$s.
@@ -53,6 +56,8 @@
IngaHittade inga kompatibla appar
+ Exportera detaljerade loggar
+ Detaljerade loggar kan innehålla känslig information, såsom din prenumerationslistaÖppna i webbläsareKopiera URL
@@ -81,7 +86,6 @@
Beskrivning\u0020episoderBearbetar
- Spara användarnamn och lösenordStängFörsök igenInkludera i automatiska nedladdningar
@@ -93,12 +97,13 @@
AvLättTungt
- \u0020parallella nedladdningar
+ %1$d parallella nedladdningarGlobala standardinställningarAlltidAldrigSkicka…Aldrig
+ När ej favoritOm inte köadEfter färdigspelad
@@ -113,7 +118,21 @@
%d vald%d vald
+
+ %d episod
+ %d episoder
+ Laddar mer...
+ Episodaviseringar
+ Visa en avisering när en episod släpps.
+
+ %2$s har en ny episod
+ %2$s har %1$d nya episoder
+
+
+ Nya Episoder
+ Nya Episoder
+ Markera alla som speladeMarkera alla episoder som spelade
@@ -145,7 +164,6 @@
Inte köadeHar mediaFiltrerad
- {fa-exclamation-circle} Senaste uppdateringen misslyckadesÖppna podcastVänta tills datan laddats
@@ -160,6 +178,10 @@
Ta bortKunde inte ta bort filen. Testa att starta om enheten.Radera episod
+
+ %d episod vald, %d nedladdning raderad.
+ %d episoder valda, %d nedladdning(ar) raderade.
+ Ta bort \"ny\"-flaggaTog bort \"ny\"-flaggaMarkera som spelad
@@ -206,16 +228,12 @@
Detaljer%1$s \n\nFil-URL:\n%2$sHittade ingen lagringsenhet
- Otillräckligt utrymmeHTTP data felOkänt fel
- TolkningsfelFlödestypen stöds inteAnslutningsfel
- Okänd värdAutentiseringsfelFiltypsfel
- FörbjudenNedladdning avbrutenNedladdning avbruten\nStängde av Automatisk nedladdning för detta föremålNedladdningar avslutades med fel
@@ -229,12 +247,7 @@
%d nedladdning kvar%d nedladdningar kvar
- Bearbetar nedladdningarLaddar ner podcastdata
-
- %d nedladdning lyckades, %d misslyckades
- %dnedladdningar lyckades, %d misslyckades
- Okänd titelFlödeMediafil
@@ -267,6 +280,7 @@
Bild-i-bild lägeAntannaPod - Okänd mediaknapp: %1$dFilen hittades inte
+ Artikeln innehåller ingen mediafilLås KönLås upp Kön
@@ -323,7 +337,6 @@
LagringAutomatisk episodradering, Import, ExportProjekt
- KöSynkroniseringSynkronisera med andra enheter via gpodder.netAutomatisering
@@ -334,19 +347,24 @@
Externa elementAvbrottUppspelningskontroll
+ Omfördela hårdvaruknapparSök...Inga resultatResnsa historikenMediaspelareEpisodupprensning
- Episoder som inte är i kön och inte är favoriter kan tas bort om Automatisk Nedladdning behöver utrymme för nya episoder
+ Episoder som får tas bort om Automatisk Nedladdning behöver mer utrymme för nya episoderPausa uppspelningen när hörlurar eller bluetooth kopplas ifrån.Fortsätt uppspelningen när hörlurarna återanslutsFortsätt uppspelningen när bluetooth återansluts
- Knappen spola fram hoppar
- Hoppa till nästa episod istället för att snabbspola när snabbspolningsknappen trycks in på en blåtands-enhet.
- Knappen föregående startar om
- Starta om den nuvarande episoden när du trycker på hårdvaruknappen för föregående istället för att spola tillbaka
+ Knappen nästa
+ Ändra beteendet för knappen nästa
+ Knappen föregående
+ Ändra beteendet för knappen föregående
+ Snabbspola framåt
+ Backa
+ Hoppa över episod
+ Starta om episodHoppa till nästa i kön när uppspelningen är klarTa bort episoden när uppspelningen är klarAutomatisk borttagning
@@ -366,8 +384,11 @@
AvaktiveraVälj intervallVälj tid på dagen
- var %1$svid %1$s
+
+ Varje timme
+ Var %d timmar
+ Kontinuerlig uppspelningHörlurar eller Bluetooth kopplas bortHörlurar återanslutna
@@ -401,7 +422,9 @@
EpisodcacheTotalt antal nedladdade epidoder som ligger i enhetens cache. Automatisk nedladdning kommer att vänta om detta antal nås.Använd Episodomslag
- Använd episodens egna omslag om tillgängligt. Appen kommer alltid att använda podcastens omslagsbild om rutan lämnas tom.
+ Använd episodspecifika omslag i listan när de är tillgängliga. Om urkryssat kommer appen alltid använda podcastens omslagsbild.
+ Visa Kvarvarande Tid
+ Visa kvarvarande tid för episoder när ikryssad. Om urkryssad visas totala tiden för episoder.Använd systemtematLjustMörkt
@@ -421,8 +444,6 @@
Tvinga full synkroniseringSynkronisera alla prenumerationer och episodstatus med gpodder.net.%1$s med enhet %2$s]]>
- Synkronisering misslyckades
- Denna inställning påverkar inte autentiseringsfel.Anpassa de tillgängliga hastigheterna för variabel uppspelningshastighetUppspelningshastigheten att använda för episoder i denna podcastAutomatisk överhoppning
@@ -437,8 +458,6 @@
Anpassa antalet sekunder att hoppa framåt när snabbspolningsknappen användsSnabbspolningslängd bakåtAnpassa antalet sekunder att hoppa bakåt när snabbspolningsknappen bakåt används
- Sätt värdnamn
- Använd standardvärdenHög notifieringsprioritetDetta expanderar oftast notifieringen och visar uppspelningskontroller.Bestående uppspelningskontroller
@@ -449,10 +468,6 @@
Du kan bara välja maximalt %1$d st.Välj låsskärmens bakgrundSätt låsskärmens bakgrund till den spelade episodens bild. En bieffekt är att även tredjepartsappar kan visa bilden.
- Nedladdning misslyckades
- Visa en rapport med detaljer om felet när nedladdningar misslyckas.
- Automatisk nedladdning klar
- Visa en avisering när episoder laddats ner automatisktAndroidversioner före 4.1 har inte stöd för expanderade aviseringar.KöplatsLägg till episoder i: %1$s
@@ -462,6 +477,7 @@
AvaktiveradBildcachestorlekStorleken på bildcachen på disken.
+ Dokumentation & SupportAnvändarforumRapportera buggÖppna buggtrackern
@@ -473,14 +489,14 @@
Nuvarande värde: %1$sProxyAnvänd en nätverksproxy
- Frekvent Frågade FrågorIngen webbläsare hittades.Chromecast-stödAktivera stöd för fjärruppspelning av media på Cast-enheter (såsom Chromecast, Ljudanläggningar eller Android TV)Chromecast kräver propretiära tredjepartsbibliotek som inte är inkluderade i denna version av AntennaPodKöa NedladdadeLägg nedladdade episoder i uppspelningskön
- Andriods inbyggda spelare
+ Androids inbyggda spelare (föråldrad)
+ Sonic Mediaspelare (föråldrad)ExoPlayer (rekommenderas)Byt till ExoPlayerBytte till ExpPlayer.
@@ -569,6 +585,7 @@
Ställ in sömntimerStäng av sömntimer
+ +%d minSömntimerOgiltigt tal, tiden måste vara ett heltalSkaka för att återställa
@@ -596,22 +613,22 @@
FÖRSLAGSök på gpodder.netInloggning
- Välkommen till inloggningsprocessen för gpodder.net. Först, skriv in din inloggningsinformation:Logga in
- Om du inte har ett konto än, så kan du skapa ett här:\nhttps://gpodder.net/register/
+ Skapa kontoAnvändarnamnLösenord
- Enhetsval
+ Gpodder.net är en synkroniseringstjänst för podcast som har öppen källkod och är oberoende från AntennaPod projektet.
+ Officiella gpodder.net servern
+ Egen Server
+ Värdnamn
+ Välj serverSkapa en ny enhet för ditt gpodder.net konto eller välj en befintlig:
- Enhets ID:\u0020
- Rubrik
- Skapa ny enhet
- Välj befintlig enhet:
- Enhets ID måste fyllas i
- Enhets ID används redan
+ Enhetsnamn
+ AntennaPod på %1$sRubrik måste fyllas i
+ Befintliga enheter
+ Skapa enhetVälj
- Inloggning lyckades!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.Starta synkronisering nuGå till huvudskärmen
@@ -665,6 +682,7 @@
Byt sidaPosition: %1$sUtför åtgärd
+ Spela kapitelAutentiseringByt ditt användarnamn och lösenord för den här podcasten och dess episoder.
@@ -799,18 +817,22 @@
Mottagande uppspelaren har stött på ett allvarligt felFel vid uppspelning av media. Hoppar över...
+ Fel
+ NyheterÅtgärd krävsVisas om din åtgärd är obligatorisk, till exempel om du behöver ange ett lösenord.Laddar nerVisas under tiden som nedladdning pågår.Uppspelning pågårMedger kontroll över uppspelning. Detta är huvudnotifieringen som du ser när en podcast spelas.
- Fel
- Visas om något gick fel, till exempel om nedladdning eller flödesuppdatering misslyckas.
- Synkroniseringsfel
+ Nedladdning misslyckads
+ Visas när nedladdning eller flödesuppdatering misslyckas.
+ Synkronisering misslyckadesVisas när synkronisering med gpodder misslyckas.
- Automatiska nedladdningar
+ Automatisk nedladdning klarVisas när episoder har laddats ner automatiskt.
+ Ny Episod
+ Visas när en ny episod av en podcast hittades, när aviseringar är aktiveradeWidgetinställningarSkapa widget
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 @@
İstatistiklerCep yayını ekleBölümler
+ KuyrukTümüYeniFavoriler
@@ -17,7 +18,6 @@
GünlükAboneliklerAbonelik Listesi
- İndirmeyi İptal EtÇalma geçmişigpodder.netgpodder.net giriş
@@ -26,6 +26,7 @@
PlaybackDownloadsNotifications
+
Total time of episodes played:%1$d out of %2$d episodes started.\n\nPlayed %3$s out of %4$s.
@@ -81,7 +82,6 @@
Tanım\u0020bölümİşleniyor
- Kullanıcı adı ve şifreyi kaydetKapatYeniden deneOtomatik indirmelere dahil et
@@ -93,7 +93,6 @@
OffLightHeavy
- \u0020paralel indirmelerVarsayılan ayarlarHer zamanHiçbir zaman
@@ -145,7 +144,6 @@
Kuyrukta değilMedya varFiltrelendi
- {fa-exclamation-circle} Son yenileme başarısız olduCep yayını açPlease wait until the data is loaded
@@ -206,16 +204,12 @@
Detaylar%1$s \n\nFile URL:\n%2$sDepolama aygıtı bulunamadı
- Yetersiz alanHTTP Veri HatasıBilinmeyen Hata
- Ayrıştırıcı İstisnasıDesteklenmeyen Besleme türüBaplantı hatası
- Bilinmeyen sunucuYetkilendirme hatasıDosya Tipi Hatası
- Yasakİndirme iptal edildiİndirme iptal edildi\nBu öğe için Otomatik İndirme devre dışıİndirme hata(lar) ile tamamlandı
@@ -229,12 +223,7 @@
%d indirme kaldı%d indirme kaldı
- İndirmeler işleniyorCep yayını verileri indiriliyor
-
- %d download succeeded, %d failed
- %d downloads succeeded, %d failed
- bilinmeyen başlıkBeslemeMedya dosyası
@@ -323,7 +312,6 @@
DepolamaEpisode auto delete, Import, ExportProje
- KuyrukSynchronizationSynchronize with other devices using gpodder.netOtomasyon
@@ -339,14 +327,9 @@
Clear historyMedya oynatıcıBölüm Temizliği
- Yeni bölümleri otomatik indirme için alan gerekirse, kuyrukta veya favorilerde olmayan bölümler otomatik olarak silinebilirKulaklıklar çıkarıldığında veya bluetooth bağlantısı kesildiğinde çalmayı duraklatKulaklıklar yeniden bağlandığında çalmaya devam etBluetooth yeniden bağlandığında çalmaya devam et
- İleri düğmesi atlar
- Bluetooth ile bağlı bir cihazda ileri düğmesine basmak hızlı ileri sarmak yerine sonraki bölüme atlar
- Geri düğmesi yeniden başlatır
- Geri düğmesine basmak hızlı geri sarmak yerine mevcut bölümü yeniden oynatırÇalma tamamlandığında kuyruktaki diğer öğeye geçÇalma bittiğinde bölümü silOtomatik Silme
@@ -366,7 +349,6 @@
Devre dışıAralık ayarlaBelirli saat ayarla
- her%1$ssüre%1$sDevamlı çalmaHeadphones or Bluetooth disconnect
@@ -401,7 +383,6 @@
Bölüm ön belleğiTotal number of downloaded episodes cached on the device. Automatic download will be suspended if this number is reached.Use Episode Cover
- Use the episode specific cover whenever available. If unchecked, the app will always use the podcast cover image.Use system themeAydınlıkKaranlık
@@ -421,8 +402,6 @@
Force full synchronizationSync all subscriptions and episode states with gpodder.net.%1$s with device %2$s]]>
- Synchronization failed
- This setting does not apply to authentication errors.Customize the speeds available for variable speed playbackThe speed to use when starting audio playback for episodes in this podcastAuto Skip
@@ -437,8 +416,6 @@
Customize the number of seconds to jump forward when the fast forward button is clickedRewind Skip TimeCustomize the number of seconds to jump backwards when the rewind button is clicked
- Sunucu ismini ayarla
- Varsayılan sunucuyu kullanHigh Notification priorityThis usually expands the notification to show playback buttons.Kalıcı oynatma kontrolleri
@@ -449,10 +426,6 @@
You can only select a maximum of %1$d items.Set Lockscreen BackgroundSet the lockscreen background to the current episode\'s image. As a side effect, this will also show the image in third party apps.
- Download failed
- Eğer indirme başarısız olursa, hatanın ayrıntılarını gösteren bir rapor oluştur.
- Automatic download completed
- Show a notification for automatically downloaded episodes.Android 4.1 öncesi sürümler genişletilmiş bildirimleri desteklememektedir.Enqueue LocationAdd episodes to: %1$s
@@ -473,14 +446,13 @@
Current value: %1$sProxySet a network proxy
- Frequently Asked QuestionsNo web browser found.Chromecast supportEnable support for remote media playback on Cast devices (such as Chromecast, Audio Speakers or Android TV)Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPodEnqueue DownloadedAdd downloaded episodes to the queue
- Built-in Android player
+ ExoPlayer (tavsiye edilen)Switch to ExoPlayerSwitched to ExoPlayer.Skip Silence in Audio
@@ -505,6 +477,7 @@
Filter your subscriptions in navigation drawer and subscriptions screen.NoneSubscriptions are filtered.
+ Sayacı sıfırdan büyük olanlarAuto downloadedNot auto downloadedKept updated
@@ -594,22 +567,12 @@
ÖNERİLERgpodder.net\'te araGiriş
- gpodder.net giriş işlemine hoşgeldiniz. Önce giriş bilgilerinizi yazın:Giriş
- Eğer bir hesabınız yoksa, buradan bir tane oluşturabilirsiniz:\nhttps://gpodder.net/register/Kullanıcı adıParola
- Cihaz Seçimigpodder.net hesabınızla kullanmak için yeni bir cihaz oluşturun veya var olan bir tanesini seçin:
- Cihaz Kimliği:\u0020
- Başlık
- Yeni cihaz oluştur
- Bir cihaz seç:
- Cihaz ID\'si boş olamaz
- Cihaz ID\'si zaten varCaption must not be emptySeç
- Giriş başarılıTebrikler! gpodder.net hesabınız cihazınızla ilişkilendirildi. AntennaPod bundan sonra gpodder.net hesabınızla üyeliklerinizi otomatik olarak senkronize edecek.Senkronizasyonu başlatAna ekrana git
@@ -699,9 +662,11 @@
Results by %1$sAdd local folder
+ Yerel klasörRe-connect local folderIn case of permission denials, you can use this to re-connect to the exact same folder. Do not select another folder.This virtual podcast was created by adding a folder to AntennaPod.
+ Sistem dosya yöneticisi başlatılamıyorFilterTümü
@@ -801,11 +766,7 @@
Shown while currently downloading.Currently playingAllows to control playback. This is the main notification you see while playing a podcast.
- Errors
- Shown if something went wrong, for example if download or feed update fails.
- Synchronization ErrorsShown when gpodder synchronization fails.
- Auto DownloadsShown when episodes have been automatically downloaded.Widget settings
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 @@
СтатистикаДодати подкастЕпізоди
+ ЧергаВсіНовийУлюблені
@@ -16,12 +17,12 @@
ЖурналПідпискиПерелік підписок
- Скасувати\nзавантаженняІсторіяgpodder.netАвтентифікуватися на gpodder.netКеш епізодів заповненийДосягнута межа розміру кешу епізодів. Розмір кешу можна збільшити в налаштуваннях.
+
%1$d з %2$d епізодів почато.\n\nПрослухано %3$s з %4$s.Режим статистики
@@ -72,14 +73,12 @@
Опис\u0020епізодівОбробка
- Зберегти ім\'я користувача та парольЗакритиПовторити зновуВключити до автозавантаженняЗастосувати до попередніх епізодівНове налаштування Автозавантаження буде автоматично застосоване до нових епізодів.\nБажаєте також застосувати його до тих епізодів що були опубліковані раніше?Автовидалення епізоду
- \u0020паралельних завантаженьЗа замовчуваннямЗавждиНіколи
@@ -121,7 +120,6 @@
Не в черзіЗі звуком або відеоФільтровані
- {fa-exclamation-circle} Останнє оновлення було невдалимВідкрити подкастЗавантажити
@@ -184,16 +182,12 @@
Докладно%1$s \n\nПосилання на файл:\n%2$sПристрій зберігання даних не знайдено
- Недостатній простір для зберіганняПомилка HTTPЩось трапилось
- Помилка парсераТип каналу не підтримуєтьсяПомилка з\'єднання
- Невідомий хостПомилка автентифікаціїПомилка типу файлу
- ЗабороненоЗавантаження скасованеЗавантаження скасоване\nАвтозавантаження для цього елементу вимкнутоЗавантаження завершені з помилками
@@ -208,7 +202,6 @@
%d завантажень залишилось%d завантажень залишилось
- Обробка завантаженогоЗавантаження даних подкастуНевідомий заголовокКанал
@@ -285,7 +278,6 @@
ЗберіганняПроект
- ЧергаАвтоматизаціяДетальноІмпорт/Експорт
@@ -298,14 +290,9 @@
Очистити історіюМедіа програвачОчищення епізодів
- Епізоди що не знаходяться в черзі та не помічені як улюблені можуть бути видалені якщо Автозавантажувач потребуватиме місце для нових епізодів.Зупиняти відтворення коли навушники або блютуз від’єднаноПоновити відтворення коли навушники повторно під’єднаноПоновити відтворення коли блютуз повторно під’єднано
- Кнопка перемотки пропускає
- При натисканні кнопки вперед на блютус пристрої, переходьте до наступного епізоду замість швидкого перемотування
- Кнопка \"назад\" повертає до початку
- При натисканні апаратної кнопки \"назад\", замість перемотки, розпочати програвання поточного епізода зановоПерейти до наступного епізода в черзі коли поточний закінченоВидалити епізод після повного відтворенняАвтовидалення
@@ -322,7 +309,6 @@
ВимкнутиІнтервалВстановити годину
- кожні %1$sо %1$sГрати безперервноПовторне під’єднання навушників
@@ -346,7 +332,6 @@
Паралельні завантаженняКеш епізодівВикористовувати обкладинку епізоду
- Відображати обкладинку епізоду замість обкладинки подкаста, якщо вона відрізняється.Використовувати системну темуСвітлаТемна
@@ -364,13 +349,10 @@
Синхронізувати підписки та зміни стану епізодів з gpodder.netСинхронізувати всі підписки та стан епізодів з gpodder.net.%1$s з пристрою %2$s]]>
- Це налаштування не застосовується до помилок автентифікації.Час, що пропускається кнопкою перемотки впередНалаштувати кількість секунд, які пропускаються при натисканні кнопки перемотки впередЧас, що пропускається кнопкою відмотки назадНалаштувати кількість секунд які відмотуються при натисканні кнопки відмотки назад
- Встановити ім\'я хоста
- Використати хост по замовчаннюВисокий пріоритет сповіщенняЗазвичай це розширює сповіщення, щоб показати кнопки відтворення.Завжди показувати елементи керування відтворенням
@@ -379,7 +361,6 @@
Ви можете обрати не більше ніж %1$d кнопок.Встановити фон екрана блокуванняВстановити картинку поточного епізоду як фон екрана блокування. Побічний ефект - це зображення також буде видимим в інших додатках.
- У разі помилки при завантаженні створити детальний звіт про помилку.Android до версії 4.1 не підтримує розширені повідомлення.Додати епізоди до: %1$sПісля поточного епізоду
@@ -395,14 +376,12 @@
Поточне значення: %1$sПроксіЗастосувати проксі сервер
- Часті питанняВеб браузер не знайдено.Підтримка для ChromecastВключити підтримку програвання на таких пристроях як Chromecast або Android TVДля підтримки Chromecast потрібні бібліотеки які не включені в цю версію AntennaPodДодати завантаження до чергиДодавати завантажені епізоди до черги
- Стандартний плеєр AndroidПропуск тиші При виході з відеорежимуПоведінка при виході з відео
@@ -481,22 +460,12 @@
РЕКОМЕНДАЦІЇПошук на gpodder.netЛогін
- Ласкаво просимо до gpodder.net. Зпочатку заповнить вашу інформацію для входуЛогін
- Якщо у вас немає облікового запису, ви можете створити його тут:\nhttps://gpodder.net/register/Ім\'я користувачаПароль
- Обрати пристрійПід\'єднати новий пристрій к gpodder.net обліковому запису о обрати інсуючий
- ID Пристрою:\u0020
- Заголовок
- Створити новий пристрій
- Вибрати існуючий пристрій
- ID пристрою не можете бути пустим
- Таке ID пристрою вже єПідпис не повинен бути пустимОбрати
- Успішно зайшлиПоздоровляємо! Ваш обліковий запис на gpodder.net зараз пов\'язаний за вашим пристроємПочати синхронізаціюПерейти до основного екрана
@@ -648,7 +617,6 @@
Показується під час завантаження.Відтворюється заразДозволяє керувати відтворенням. Це основне сповіщення, яке ви бачите під час відтворення подкасту.
- ПомилкиНалаштування віджетуСтворити віджет
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 @@
统计添加播客曲目
+ 播放列表全部最新收藏
@@ -26,6 +27,8 @@
回放下载通知
+
+ \"%1$s\" 未找到节目总播放时间:听过了总计 %2$d 期播客中的 %1$d 期。\n\n播放了总计 %4$s 中的 %3$s。
@@ -53,6 +56,8 @@
无没有找到兼容的应用程序
+ 导出详细日志
+ 详细日志可能包含敏感信息,如你的订阅列表在浏览器打开复制 URL
@@ -81,7 +86,6 @@
描述\u0020 曲处理中
- 保存用户名密码关闭重试包含到自动下载
@@ -93,12 +97,13 @@
关闭轻微显著
- \u0020 并行下载
+ %1$d 个并行下载全局默认 总是从不发送从不
+ 当未收藏时当不在队列中结束后
@@ -110,7 +115,19 @@
已选中%d
+
+ %d 期节目
+ 正加载更多…
+ 节目通知
+ 当一期新节目发布时,显示一条通知。
+
+ %2$s有 %1$d 期新节目
+
+
+ 新节目
+
+ 你的订阅有新节目全部标识已读将所有曲目标记为已播放
@@ -142,7 +159,6 @@
不在播放列表中包含媒体文件已过滤的
- {fa-exclamation-circle} 上次刷新失败打开播客请等待数据加载完成
@@ -156,6 +172,9 @@
删除无法删除文件。重启可能解决该问题。删除节目
+
+ %d 期节目被选中,%d 个下载被删除
+ 移除“新的”标签已移除“新的”标签标记已播放
@@ -198,16 +217,12 @@
详细信息%1$s \n\nFile URL:\n%2$s没有找到存储设备
- 空间不足HTTP 数据错误未知错误
- 解析异常未提供的订阅类型链接错误
- 未知主机认证错误文件类型错误
- 禁用的已取消下载已取消下载\n对该曲目禁用自动下载下载完成
@@ -220,11 +235,7 @@
剩余%d个下载项
- 正在处理下载下载播客数据
-
- %d个下载成功,%d下载失败
- 未知标题订阅媒体文件
@@ -257,6 +268,7 @@
画中画模式AntennaPod - 未知媒体密钥: %1$d文件未找到
+ 条目不包含一个媒体文件锁定播放列表解锁播放列表
@@ -313,7 +325,6 @@
存储播客剧集自动删除、导入和导出项目
- 播放列表同步用gpodder.net与其他设备同步自动化
@@ -324,19 +335,24 @@
外部元素中断回放控制
+ 重新分配硬件按钮搜索无结果清除历史记录媒体播放器清理曲目
- 如果自动下载需要为新剧集腾出空间时不在列表和收藏里的剧集可以被移除
+ 在自动下载需要新曲目空间时,应该被删除的旧曲目暂停播放曲目当耳机或蓝牙重新连接当耳机重新连接时恢复播放恢复播放曲目当蓝牙重新连接
- 快进按钮跳过曲目
- 当按压一个蓝牙连接设备的快进按钮时跳到下一项曲目而不是快进
- “上一个”按钮重启
- 当按压硬件上一个按钮时重新开始播放当前曲目而不是倒回
+ 快进按钮
+ 自定义快进按钮行为
+ “上一个” 按钮
+ 自定义“上一个”按钮行为
+ 快进
+ 倒退
+ 跳过节目
+ 重新开始节目播放完成跳转到播放列表下一项当播放完成后删除曲目自动删除
@@ -356,8 +372,10 @@
禁用设置间隔设置时间
- 每%1$s秒第%1$s秒
+
+ 每 %d 小时
+ 连续播放耳机或蓝牙断开耳机重新连接
@@ -392,7 +410,9 @@
缓存在设备上的已下载节目总数
若达到此数目,自动下载将被暂停使用音频封面
- 只要可以就使用某一集的封面。如果未选中,AntennaPod将始终使用播客的封面图像。
+ 勾选后,在列表中使用一期节目特定的封面图。如果不勾选,应用程序将始终使用播客封面图像。
+ 显示剩余时间
+ 勾选显示节目的剩余时间。如果未选中,则显示所有节目的持续时间。使用系统主题浅色暗色
@@ -412,8 +432,6 @@
强制完整同步与gpodder.net同步所有订阅和节目状态%2$s 上以 %1$s 身份登录]]>
- 同步失败
- 该设置无法适用于验证错误。自定义可用于变速播放的速度开始播放此播客中剧集时使用的速度自动跳过
@@ -428,8 +446,6 @@
自定义每次快进节目的秒数倒回跳过时间自定义每次倒回节目的秒数
- 设置主机名
- 使用默认主机高通知优先级这通常会拓展通知以显示播放按钮。保持播放控制
@@ -440,10 +456,6 @@
你最多只能同时选择%1$d项设置锁屏背景将锁屏背景设置为当前播放节目的封面图(潜在的副作用是图片可能会在出现在第三方应用中)。
- 下载失败
- 如果下载失败,生成一份显示详细失败信息的报告。
- 自动下载已完成
- 显示有关自动下载的剧集的通知。Android 4.1 之前不支持扩展通知。排队位置添加音频至:%1$s
@@ -453,6 +465,7 @@
已禁用图像缓存大小用于缓存图像的存储空间大小
+ 文档 & 支持用户论坛报告Bug打开Bug跟踪器
@@ -464,14 +477,14 @@
当前值:%1$s代理选择一个网络代理
- 常见问题无网络浏览器Chromecast 支持启用投影设备(例如 Chromecast 、 Audio Speakers 和 Android TV )上对于远端媒体回放的支持Chromecast 所需要的第三方库文件在这个版本的 AntennaPod 中被禁用已下载队列向队列添加已下载的节目
- 内置安卓播放器
+ 内置安卓播放器 (已废弃)
+ Sonic 媒体播放器 (已废弃)ExoPlayer (推荐)转到ExoPlayer已转至ExoPlayer
@@ -560,6 +573,7 @@
设置休眠计时器禁用休眠计时器
+ + %d 分钟休眠计时器无效的输入, 时间是一个整数摇动以重置
@@ -584,22 +598,22 @@
建议搜索 gpodder.net登录
- 欢迎进入 gpodder.net 登录流程. 首先, 输入请你的登录信息:登录
- 如果您目前没有账户,可以从这里创建:\nhttps://gpodder.net/register/
+ 创建账户用户名密码
- 设备选择
+ Gpodder.net 是一个独立于 Antennapd 项目的开源播客同步服务。
+ 官方 gpodder.net 服务器
+ 自定义服务器
+ 主机名
+ 选择服务器为你的 gpodder.net 账户创建一个新设备或者选择一个已存在的:
- 设备编号: \u0020
- 标题
- 创建新设备
- 选择已存在设备
- 设备编号必须填写
- 设备编号已被使用
+ 设备名
+ AntennaPod 于 %1$s标题不能为空
+ 现有设备
+ 创建设备选择
- 登录成功!恭喜! 你的 gpodder.net 帐户与设备已连结完成. 现在开始 AntennaPod 将自动同步你 gpodder.net 帐户内的订阅信息到设备上.开始同步返回主屏
@@ -653,6 +667,7 @@
切换页面位置:%1$s应用动作
+ 播放章节验证给本播客及曲目变更用户名及密码
@@ -787,18 +802,22 @@
接收播放器遇到一个严重错误媒体播放出错.跳转中...
+ 错误
+ 新闻需要操作显示是否需要您的操作,比如是否需要您输入一个密码正在下载下载时显示当前播放允许控制回放。这是播放播客时您所见的主通知。
- 错误
- 出错时显示,比如下载或订阅源更新失败。
- 同步错误
+ 下载失败
+ 当下载或源更新失败是显示
+ 同步失败了gpodder 同步出错时显示
- 自动下载
+ 自动下载已完成当节目已自动下载时显示。
+ 新节目
+ 当发现一个播客的新节目时显示,前提是在播客中启用通知小部件设置创建小部件
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 @@
統計新增 Podcast單集
+ 待播清單全部最新最愛
@@ -17,7 +18,6 @@
日誌訂閱訂閱列表
- 取消下載播放歷史gpodder.net登入 gpodder.net
@@ -26,6 +26,7 @@
播放下載通知
+
總播放時長:聽過 %1$d/%2$d集。\n\n播過%3$s/%4$s集。
@@ -81,7 +82,6 @@
描述\u0020單集處理中
- 保存帳號及密碼關閉重試加入自動下載
@@ -93,7 +93,6 @@
關閉輕重
- \u0020項同步下載預設值總是不予下載
@@ -142,7 +141,6 @@
未列入待播清單包含媒體已過濾
- {fa-exclamation-circle} 更新失敗打開 Podcast資料載入中,請稍候
@@ -198,16 +196,12 @@
詳情%1$s \n\n檔案網址:\n%2$s沒找到儲存空間
- 儲存空間不足HTTP 資料有誤位置錯誤
- 解析器異常不支援此來源類型連接錯誤
- 不明主機驗證失敗文件格式錯誤
- 禁止存取下載已取消下載已取消\n這一項的 自動下載 已停用下載已完成,但可能有錯誤
@@ -220,11 +214,7 @@
剩餘%d 個下載
- 正在下載Podcast 資料下載中
-
- 成功下載 %d 個單集,失敗 %d 個
- 標題不明資料來源媒體檔案
@@ -313,7 +303,6 @@
儲存空間自動刪除、匯入、匯出專案
- 待播清單同步利用 gpodder.net 與其他裝置同步自動化
@@ -329,14 +318,9 @@
清除歷史紀錄媒體播放器刪除單集時機
- 在暫存集數已滿、自動下載功能需要更多空間的情形下,不在待播清單也未設定為最愛的各單集將被刪除。耳機或藍牙斷開連接時暫停播放當耳機再次連接時繼續播放當藍牙再次連接時繼續播放
- 快轉鈕視為跳過單集
- 當按下藍牙連接裝置上的快轉鈕時,不要快轉,而是播放下一集
- 倒轉鈕視為重新播放
- 當按下實體的倒轉鈕時,不要倒轉,而是重新播放本集當播放完畢時自動跳至待播清單中的下一集播放完畢後刪除該集自動刪除
@@ -356,7 +340,6 @@
停用設定週期設定每日定時
- 每 %1$s於 %1$s連續播放耳機或藍牙裝置拔除時
@@ -391,7 +374,6 @@
暫存集數在本機中可以暫存的集數,若達上限則將停止自動下載。使用單集的封面圖
- 在單集有專屬封面的情況下使用該封面圖。如果取消,則一律使用 Podcast 的封面圖依據系統設定淡色深色
@@ -411,8 +393,6 @@
強制全部同步與 gpodder.net 同步所有的訂閱及聆聽狀態。%1$s 登入,設備為 %2$s]]>
- 同步失敗
- 此設定不影響登入驗證錯誤。自訂可選用的播放速度播放此 Podcast 中各單集時的播放速度自動跳過
@@ -427,8 +407,6 @@
自訂快轉鈕要往前快轉多少時間倒轉時間自訂倒轉鈕要往後倒轉多少時間
- 設定主機
- 使用預設主機優先通知此功能通常會加大通知訊息以便顯示控制鈕保留播放控制鈕
@@ -439,10 +417,6 @@
您最多只能選擇 %1$d 項。設定鎖定畫面背景在鎖定畫面背景採用本單集的圖片,同時也會在第三方 App 裡顯示圖片
- 下載失敗
- 如果下載失敗,產生錯誤相關細節的報告
- 自動下載完畢
- 顯示自動下載通知Android 4.1 以前尚未支援延伸通知工具。待播清單新增位置將這幾集加到:%1$s
@@ -463,14 +437,12 @@
當前設定:%1$s代理伺服器設定代理伺服器
- 常見問題找不到任何瀏覽器支援 Chromecast啟用 Cast 設備(如 Chromecast、Audio Speakers、Google TV 等)遙控播放Chromecast 相關支援需要其他第三方的私權軟體,所以在此版 AntennaPod 中停用。下載後加入待播清單下載單集以後自動加入待播清單
- Android 內建播放器ExoPlayer(推薦使用)切換至 ExoPlayer已切換至 ExoPlayer
@@ -583,22 +555,12 @@
建議搜尋 gpodder.net登入
- 歡迎登入 gpodder.net,請輸入登入資訊:登入
- 如果您還沒有帳號,可以先註冊一個:\nhttps://gpodder.net/register/帳號密碼
- 選擇裝置為您的 gpodder.net 帳號建立新設備或選取既有設備:
- 設備代號:\u0020
- 標題
- 新增裝置
- 選擇現存裝置:
- 設備代號不能留白
- 已經使用此設備代號標題不能留白選擇
- 登入成功!恭喜,您的 gpodder.net 帳號已經成功連結到當前的設備!AntennaPod 今後將自動與 gpodder.net 同步此設備的訂閱清單。現在開始同步進入主螢幕
@@ -792,11 +754,7 @@
下載時顯示現正播放允許播放控制。這是您在播放 Podcast 時會看到的主要通知。
- 錯誤通知
- 如果有任何錯誤(比方說下載或更新來源出錯)時顯示
- 同步時發生錯誤在 gpodder 同步發生錯誤時顯示
- 自動下載自動下載後顯示小工具設定
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/episode_cleanup_except_favorite_removal@string/episode_cleanup_queue_removal01
@@ -105,6 +106,20 @@
@string/episode_cleanup_never
+
+ @string/button_action_fast_forward
+ @string/button_action_rewind
+ @string/button_action_skip_episode
+ @string/button_action_restart_episode
+
+
+
+ @string/keycode_media_fast_forward
+ @string/keycode_media_rewind
+ @string/keycode_media_next
+ @string/keycode_media_previous
+
+
@string/enqueue_location_back@string/enqueue_location_front
@@ -119,6 +134,7 @@
+ -3-1012
@@ -234,15 +250,15 @@
+ @string/media_player_exoplayer_recommended@string/media_player_builtin@string/media_player_sonic
- @string/media_player_exoplayer_recommended
+ exoplayerbuiltinsonic
- exoplayer
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 @@
-
-
-
-
-
-
-
-
+
+
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 @@
#43707070#43707070#22777777
+ #90000000
+ #905B5B5B#0078C2#3D8BFF
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 @@
-
-
-
-
-
-
-
-
-
-
\ 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 @@
+
+
+ 87
+ 88
+ 89
+ 90
+
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 @@
StatisticsAdd PodcastEpisodes
+ QueueAllNewFavorites
@@ -32,6 +33,9 @@
DownloadsNotifications
+
+ \"%1$s\" not found
+
Total time of episodes played:%1$d out of %2$d episodes started.\n\nPlayed %3$s out of %4$s.
@@ -95,7 +99,6 @@
Description\u0020episodesProcessing
- Save username and passwordCloseRetryInclude in auto downloads
@@ -113,6 +116,7 @@
NeverSend…Never
+ When not favoritedWhen not in queueAfter finishing
@@ -128,11 +132,21 @@
%d selected
- no episodes%d episode%d episodesLoading more…
+ Episode Notifications
+ Show a notification when a new episode is released.
+
+ %2$s has a new episode
+ %2$s has %1$d new episodes
+
+
+ New Episode
+ New Episodes
+
+ Your subscriptions have new episodes.Mark all as played
@@ -165,7 +179,7 @@
Not queuedHas mediaFiltered
- {fa-exclamation-circle} Last Refresh failed
+ {fa-exclamation-circle} Last Refresh failed. Tap to view details.Open PodcastPlease wait until the data is loaded
@@ -231,18 +245,24 @@
Download runningDetails%1$s \n\nFile URL:\n%2$s
+ Tap to view details.Storage Device not found
- Insufficient Space
+ There is not enough space left on your device.HTTP Data ErrorUnknown Error
- Parser Exception
+ The podcast host\'s server sent a broken podcast feed.Unsupported Feed Type
+ The podcast host\'s server sent a website, not a podcast.
+ The podcast host\'s server does not know where to find the file. It may have been deleted.Connection Error
- Unknown Host
+ Cannot find the server. Check if the address is typed correctly and if you have a working network connection.Authentication ErrorFile Type Error
- Forbidden
+ The podcast host\'s server refuses to respond.Download canceled
+ The server connection was lost before completing the download
+ The download was blocked by another app on your device.
+ 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.Download canceled\nDisabled Auto Download for this itemDownloads completed with error(s)Auto-downloads completed
@@ -255,12 +275,8 @@
%d download left%d downloads left
- Processing downloads
+ Service shutting downDownloading podcast data
-
- %d download succeeded, %d failed
- %d downloads succeeded, %d failed
- Unknown TitleFeedMedia file
@@ -356,7 +372,6 @@
StorageEpisode auto delete, Import, ExportProject
- QueueSynchronizationSynchronize with other devices using gpodder.netAutomation
@@ -367,19 +382,24 @@
External elementsInterruptionsPlayback control
+ Reassign hardware buttonsSearch…No resultsClear historyMedia playerEpisode Cleanup
- Episodes that aren\'t in the queue and aren\'t favorites should be eligible for removal if Auto Download needs space for new episodes
+ Episodes that should be eligible for removal if Auto Download needs space for new episodesPause playback when headphones or bluetooth are disconnectedResume playback when the headphones are reconnectedResume playback when bluetooth reconnects
- Forward Button Skips
- When pressing a forward button on a bluetooth-connected device skip to the next episode instead of fast-forwarding
- Previous button restarts
- When pressing a hardware previous button restart playing the current episode instead of rewinding
+ Forward Button
+ Customize the forward button behavior
+ Previous Button
+ Customize the previous button behavior
+ Fast Forward
+ Rewind
+ Skip Episode
+ Restart EpisodeJump to next queue item when playback completesDelete episode when playback completesAuto Delete
@@ -399,8 +419,11 @@
DisableSet IntervalSet Time of Day
- every %1$sat %1$s
+
+ Every hour
+ Every %d hours
+ Continuous PlaybackHeadphones or Bluetooth disconnectHeadphones Reconnect
@@ -434,7 +457,9 @@
Episode CacheTotal number of downloaded episodes cached on the device. Automatic download will be suspended if this number is reached.Use Episode Cover
- Use the episode specific cover whenever available. If unchecked, the app will always use the podcast cover image.
+ Use the episode specific cover in lists whenever available. If unchecked, the app will always use the podcast cover image.
+ Show Remaining Time
+ Display remaining time of episodes when checked. If unchecked, display total duration of episodes.Use system themeLightDark
@@ -468,8 +493,6 @@
Customize the number of seconds to jump forward when the fast forward button is clickedRewind Skip TimeCustomize the number of seconds to jump backwards when the rewind button is clicked
- Set hostname
- Use default hostHigh Notification priorityThis usually expands the notification to show playback buttons.Persistent Playback Controls
@@ -507,8 +530,8 @@
Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPodEnqueue DownloadedAdd downloaded episodes to the queue
- Built-in Android player
- Sonic Media Player
+ Built-in Android player (deprecated)
+ Sonic Media Player (deprecated) ExoPlayer (recommended)Switch to ExoPlayerSwitched to ExoPlayer.
@@ -631,23 +654,24 @@
SUGGESTIONSSearch gpodder.netLogin
- Welcome to the gpodder.net login process. First, type in your login information:Login
- If you do not have an account yet, you can create one here:\nhttps://gpodder.net/register/
+ Password and data are not encrypted!
+ Create accountUsernamePassword
- Device Selection
+ Gpodder.net is an open-source podcast synchronization service that is independent of the AntennaPod project.
+ Official gpodder.net server
+ Custom server
+ Hostname
+ Select serverCreate a new device to use for your gpodder.net account or choose an existing one:
- Device ID:\u0020
- Caption
- Create new device
- Choose existing device:
- Device ID must not be empty
- Device ID already in use
+ Device name
+ AntennaPod on %1$sCaption must not be empty
+ Existing devices
+ Create deviceChoose
- Login successful!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.Start sync nowGo to main screen
@@ -874,6 +898,8 @@
Shown when gpodder synchronization fails.Automatic download completedShown when episodes have been automatically downloaded.
+ New Episode
+ Shown when a new episode of a podcast was found, where notifications are enabledWidget settings
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 @@
@color/highlight_lightfalse@color/grey600
+ @color/seek_background_light@drawable/ic_storage_black@drawable/ic_network_black
@@ -78,6 +79,7 @@
@color/filter_dialog_clear_light@drawable/filter_dialog_background_light@drawable/ic_notifications_black
+ @drawable/ic_share_black