1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-02-28 12:59:44 +00:00

Compare commits

..

188 Commits

Author SHA1 Message Date
Stypox
e22b046326 Fix inconsistency in getQuantity and add docs
`getQuantity()` was being called in a couple of places with `zeroCaseStringId=0`, but that wasn't documented anywhere, and if `count==0` then `getString(zeroCaseStringId /* == 0 */)` would be returned which doesn't make sense
2026-02-26 19:08:33 +01:00
Stypox
56fb31d0fd Merge pull request #13292 from dustdfg/kao_release_only 2026-02-26 11:01:56 +01:00
Yevhen Babiichuk (DustDFG)
042f9460b0 Don't show Keep Android Open popup on debug builds 2026-02-26 11:39:10 +02:00
Tobi
9f1e2c6fd0 Merge pull request #13282 from dustdfg/keep_android_open
Add warning banner about ongoing google certification for android apps
2026-02-25 17:08:49 -08:00
tobigr
66237abb3c KeepAndroidOpen: Choose website language from list of supported languages 2026-02-26 01:27:36 +01:00
Tobi
dd65db56a9 Merge pull request #13290 from dustdfg/correct_download_fragment
Correctly retrieve menu item inside download dialog
2026-02-25 15:42:05 -08:00
Yevhen Babiichuk (DustDFG)
195a76bb08 Correctly retrieve menu item inside download dialog 2026-02-26 01:09:07 +02:00
Yevhen Babiichuk (DustDFG)
06e4548c14 Add warning banner about ongoing google certification for android apps 2026-02-25 18:35:41 +02:00
Hosted Weblate
9a292e33f9 Translated using Weblate (Albanian)
Currently translated at 2.2% (2 of 89 strings)

Translated using Weblate (Georgian)

Currently translated at 55.0% (49 of 89 strings)

Translated using Weblate (Latvian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Georgian)

Currently translated at 96.5% (738 of 764 strings)

Co-authored-by: Flavjo Avdiu <flavjoavdiu10@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nikoloz <nukushatugushi@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ka/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sq/
Translation: NewPipe/Metadata
2026-02-22 11:32:29 +01:00
Tobi
d1ad1e843f Merge pull request #13276 from TeamNewPipe/depupdate
Update dependencies and Gradle to latest stable releases
2026-02-21 23:59:11 -08:00
Aayush Gupta
8902ce8678 Update dependencies and Gradle to latest stable releases
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-22 10:46:58 +08:00
Aayush Gupta
9f193a3b00 Merge pull request #13272 from theimpulson/coilport
Migrate from Picasso to Coil
2026-02-22 10:35:43 +08:00
Tobi
232d023c02 Merge pull request #13030 from iampopovich/fix/6815-double-tap-to-resume
[fix] Hide controls when resuming playback via double tap
2026-02-21 11:08:51 -08:00
Alex Popov
423f95a65d Refactor double tap logic to use isPlaying() method for better readability 2026-02-21 20:03:25 +01:00
Alex Popov
caebf8461a Hide controls when resuming playback via double tap
Remove redundant comment about hiding controls on double tap
2026-02-21 19:54:59 +01:00
Tobi
52a9b60180 Merge pull request #13148 from dustdfg/extractor_enum_set
Accomodate extractor changes (EnumSet for service media capabilities)
2026-02-21 09:10:55 -08:00
Tobi
4bad882668 Merge pull request #13037 from dustdfg/tablet_related_items
VideoDetailFragment: hide relatedItemsLayout in tablet mode after fullscreen
2026-02-21 09:03:11 -08:00
tobigr
63e19890bf Update extractor to the latest version 2026-02-21 17:00:05 +01:00
Yevhen Babiichuk (DustDFG)
08b7da4b2b Accomodate extractor changes (EnumSet for service media capabilities) 2026-02-21 16:55:31 +01:00
Aayush Gupta
402a0aaaa3 Delete unused preference title and summary
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-21 16:48:57 +08:00
Aayush Gupta
57364109f4 Migrate from Picasso to Coil
Based on changes from refactor

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-21 16:48:57 +08:00
Clippy
4481dd7fe6 Merge pull request #13135 from dustdfg/player_loop_refactor
Misc player loop/stream refactors
2026-02-21 06:25:58 +00:00
Aayush Gupta
273b09a3e8 App: Migrate from Java to Kotlin
Based on changes from refactor branch for code parity

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-21 00:06:12 +08:00
Tobi
35b70c5e9e Merge pull request #13194 from TeamNewPipe/r8fixes
Enable lint checks and resource shrinking
2026-02-20 00:47:16 -08:00
Aayush Gupta
653b33bdb9 FocusOverlayView: Avoid accessing restricted API
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
5eb5f7533d ic_smart_display: Fix invalid vector path on older devices
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
15829c882b lint: Supress more translation related errors
They should be fixed by translators or weblate in general.

 ../../src/main/res/values-lt/strings.xml:87: For locale "lt" (Lithuanian) the following quantity should also be defined: many (e.g. "1.1 obuolio")
 ../../src/main/res/values-is/strings.xml:318: The quantity 'one' matches more than one specific number in this locale (1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …), but the message did not include a formatting argument (such as %d). This is usually an internationalization error. See full issue explanation for more.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
10b943f37e DownloadDialog: Avoid using restricted API for menuitem
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
3d43e52afb activity_player_queue_control: Switch to app:tint instead of android:tint
../../src/main/res/layout/activity_player_queue_control.xml:208: Must use app:tint instead of android:tint

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
f64e40e958 DownloadRunnableFallback: Fix error with log tag being too long
Abbreviate Download -> DL

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
2182ff12b7 Use correct constant for hiding keyboard
../../src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt:509: Must be one or more of: InputMethodManager.HIDE_IMPLICIT_ONLY, InputMethodManager.HIDE_NOT_ALWAYS

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
b3f4cb8114 Use requireContext() instead of asserting non-null context
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
deb6b4230d FinishedMIssionStore: Throw exception if column is missing
../../src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java:105: Value must be ≥ 0 but getColumnIndex can be -1

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
2f3a993f8e ReCaptchaActivity: Supress lint error for missing super call
saveCookiesAndFinish method handles back navigation

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
582f852e7a Add missing permission checks for notifications
Notifications can be disabled manually even after permission has been granted once.
Always check if they are enabled before notifying the user.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
89cb87b2a9 Introduce lint configuration and enable checks
Supress missing translation errors as they are done by volunteers

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
8c016c95d2 Enable resources shrinking
AGP 9.0.0 has introduced additional resource shrinking tasks. Its better
to enable and fix this.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
5b009453f2 Address non-final resource IDs warnings
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Hosted Weblate
feb442492b Translated using Weblate (Dutch)
Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 57.3% (51 of 89 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Somali)

Currently translated at 72.1% (551 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 66.2% (59 of 89 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 66.2% (59 of 89 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 53.9% (48 of 89 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (Marathi)

Currently translated at 30.4% (233 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 56.1% (50 of 89 strings)

Co-authored-by: 439JBYL80IGQTF25UXNR0X1BG <439JBYL80IGQTF25UXNR0X1BG@users.noreply.hosted.weblate.org>
Co-authored-by: AMOL GHORPADE <amolghorpade4u@gmail.com>
Co-authored-by: Daria Szatan <bla_ke@tuta.io>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kairos <private.snazzy673@simplelogin.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Miq Maq <maqdishi@gmail.com>
Co-authored-by: Nicolás Pérez <nicoperez241@proton.me>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: delvani <del.cidrak@gmail.com>
Co-authored-by: igoroliveira-letras <igoroliveiradasilvaofc@gmail.com>
Co-authored-by: ojppe <ojppe@users.noreply.hosted.weblate.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2026-02-19 09:41:02 +01:00
Tobi
834f136102 Merge pull request #13247 from dustdfg/crashlog_fix
Partially revert:  ErrorActivity: Kotlin-fy buildMarkdown method
2026-02-19 00:37:56 -08:00
Tobi
01e77e2e26 Merge pull request #13256 from pierreeurope/fix/ttml-postprocessing-error-reporting
Fix subtitle post-processing error losing original exception
2026-02-19 00:21:17 -08:00
Tobi
4cfd36ce5b Merge pull request #13203 from dustdfg/video_detail_fragment_click_through
VideoDetailFragment: Forcefully catch click events uncaught by children
2026-02-18 23:45:35 -08:00
Tobi
51c9ed774c Merge pull request #13213 from aivelon/handle-comment-replies-screens
Handling Comment Replies fragments
2026-02-18 23:43:39 -08:00
vt
9f8055f018 remove existing comment replies screens before adding new one 2026-02-19 12:07:22 +05:30
Tobi
239f6c97a0 Merge pull request #13241 from aivelon/comment-replies-without-images
Fix comment replies header layout without avatar image
2026-02-18 03:20:19 -08:00
vt
7e7ad1e410 fix layout of comment replies header to work when avatar image is gone (disabled in content -> image settings) 2026-02-17 13:34:41 +05:30
pierreeurope
95367dd338 Fix subtitle post-processing error losing original exception
Previously, TtmlConverter.process() caught all exceptions during TTML
to SRT conversion and returned opaque error codes (1 for IOException,
8 for other exceptions). These error codes were then wrapped by
Postprocessing.run() into a generic RuntimeException with the message
'post-processing algorithm returned N', losing the original exception
and its stack trace.

This made it impossible for users and developers to diagnose the root
cause of subtitle download failures, as reported in #13206.

Now, IOExceptions are re-thrown directly, and other exceptions are
wrapped in an IOException with the original exception as the cause.
This allows DownloadMission.doPostprocessing() to properly catch and
report the actual error, including the full stack trace in the crash
report.

Fixes #13206

Signed-off-by: pierreeurope <pierre.europe@pm.me>
2026-02-16 12:46:30 +01:00
Yevhen Babiichuk (DustDFG)
70cdaf5550 Partially revert: ErrorActivity: Kotlin-fy buildMarkdown method
Partially reverts c3dbed54e5
Fix the bug where collapse container for several crashlogs is created
when only one crash log present
2026-02-13 13:43:54 +02:00
Aayush Gupta
3815f5f593 Translated using Weblate (Bengali (India))
Currently translated at 39.2% (300 of 764 strings)
2026-02-12 17:25:15 +01:00
Hosted Weblate
6c69a5409b Translated using Weblate (Slovak)
Currently translated at 77.5% (69 of 89 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 49.4% (44 of 89 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.2% (758 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (French)

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (German)

Currently translated at 100.0% (89 of 89 strings)

Added translation using Weblate (Gaelic)

Translated using Weblate (French)

Currently translated at 77.2% (68 of 88 strings)

Translated using Weblate (Turkish)

Currently translated at 34.0% (30 of 88 strings)

Co-authored-by: Erenay <erenaydev@proton.me>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Nico <n2778370@gmail.com>
Co-authored-by: S.B. MacQueen <macqueen1729@protonmail.com>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thadah D. Denyse <thadahdenyse@protonmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: delvani <del.cidrak@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2026-02-12 17:25:15 +01:00
Stypox
840664a8d7 Merge pull request #13236 from dustdfg/error_activity_newlines 2026-02-12 13:15:54 +01:00
Yevhen Babiichuk (DustDFG)
54090ca7b6 ErrorActivity use normal \n instead of \\n in resource strings 2026-02-11 16:54:35 +02:00
Aayush Gupta
8d45b6b8c9 Merge pull request #13225 from dustdfg/error_activity_kotlin
Convert ErrorActivity to kotlin
2026-02-11 21:50:37 +08:00
Aayush Gupta
c3dbed54e5 ErrorActivity: Kotlin-fy buildMarkdown method
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-11 21:40:02 +08:00
Aayush Gupta
8968aab578 ErrorActivity: Catch exceptions not throwables
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-11 21:40:02 +08:00
Aayush Gupta
d7a4435e94 ErrorActivity: Use better variable names and encapsulation
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-11 21:39:56 +08:00
Yevhen Babiichuk (DustDFG)
4a7eaed3a7 ErrorActivity convert to kotlin
Co-authored-by: Aayush Gupta <aayushgupta219@protonmail.com>
2026-02-11 18:31:12 +08:00
Yevhen Babiichuk (DustDFG)
869a3cea9b ErrorActivity small refactor 2026-02-11 18:31:12 +08:00
Yevhen Babiichuk (DustDFG)
3a0a3a42af Misc loop/stream based player refactors 2026-02-10 11:26:23 +02:00
Yevhen Babiichuk (DustDFG)
59e5018c2d player/helper/PlayerHelper#getTimeString replace ints with longs
Duration data in the player code incosnistently typed. Half code
uses ints and half uses longs. Recieve longs in this function to
allow both halfs of player code just use the function without
nasty long to int downcasting warnings/errors in code
2026-02-10 11:24:25 +02:00
Aayush Gupta
e6e0be772a Merge pull request #13026 from dustdfg/kotlin_merged2
Convert a bunch of files to kotlin
2026-02-10 16:40:51 +08:00
Aayush Gupta
224a5d0cb9 Minor improvements
- Use early return in case of nulls
- Use better variable names
- Remove non-required newlines, imports and add missing ones

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 16:33:43 +08:00
Yevhen Babiichuk (DustDFG)
c6fc94e7bd Convert newpipe/settings/export/PreferencesObjectInputStream to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
1eedfd7eee Convert /newpipe/util/StreamTypeUtil to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
2c7654a579 Covert newpipe/util/DependentPreferenceHelper to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
09a746dd6a Convert newpipe/util/ServiceHelper to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
d665a4f016 Convert newpipe/util/NewPipeTextViewHelper to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
6cf932b2a7 Convert newpipe/ExitActivity to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
48467669b6 Convert newpipe/util/PeertubeHelper to kotlin 2026-02-10 09:51:14 +02:00
Yevhen Babiichuk (DustDFG)
780e6a4848 Convert newpipe/util/PlayButtonHelper to kotlin 2026-02-10 09:51:13 +02:00
Aayush Gupta
13186c0b15 Merge pull request #13224 from dustdfg/kotlin_idiomatic
Kotlin misc idiomatic refactor
2026-02-10 15:43:15 +08:00
Yevhen Babiichuk (DustDFG)
edfdbe805f Uitilize kotlin elvis operator 2026-02-10 09:23:17 +02:00
Yevhen Babiichuk (DustDFG)
451409fc3b SharedPreferences.edit applies changes automatically 2026-02-10 09:23:17 +02:00
Yevhen Babiichuk (DustDFG)
289d22eed7 Utilize kotlins ifEmpty 2026-02-10 09:23:17 +02:00
Yevhen Babiichuk (DustDFG)
21f446a78e Refactor settings/preferencesearch/PreferenceSearchItem#allRelevantSearchFields
It doesn't need to return mutable list
2026-02-10 09:23:17 +02:00
Stypox
6214ae33f3 Merge pull request #13219 from dustdfg/kotlin_check_is_not_if
Correct inverted check (error fix on dev branch)
2026-02-09 14:51:06 +01:00
Yevhen Babiichuk (DustDFG)
37cef825a2 Correct inverted check
If performs action when value is true but check when false
Fix for d6be966db3
2026-02-09 14:31:56 +02:00
Aayush Gupta
dab8e056e9 Merge pull request #13137 from dustdfg/info_item_builder_dead
Remove dead code from info_list/InfoItemBuilder
2026-02-08 22:28:19 +08:00
Aayush Gupta
020dbdc82a Merge pull request #13131 from dustdfg/tabs_json_helper_refactor
TabsJsonHelper refactor
2026-02-08 22:06:25 +08:00
Aayush Gupta
5d7934249f Merge pull request #13028 from dustdfg/idiomatic_kotlin_exceptions
Replace Illegal{State,Argument} exceptions with more idiomatic kotlin code
2026-02-08 22:05:46 +08:00
Yevhen Babiichuk (DustDFG)
d6be966db3 Replace Illegal{State,Argument} exceptions with more idiomatic kotlin code 2026-02-08 21:59:10 +08:00
Yevhen Babiichuk (DustDFG)
5e1a1989be VideoDetailFragment: Forcefully catch click events uncaught by children
Because otherwise they will be caught by underlying view and
"click through" will happen. Workaround for #5600
2026-02-06 19:03:01 +02:00
Tobi
56a043669a Merge pull request #13161 from jpds/outdated-subscription-shuffle
FeedLoadManager: Shuffle the order outdated subscriptions are updated in
2026-02-06 02:13:56 -08:00
tobigr
85abc58158 Merge branch 'master' into dev 2026-02-05 23:01:30 +01:00
Tobi
8578bd9f95 Merge pull request #13200 from TeamNewPipe/release-0.28.3
Release 0.28.3
2026-02-05 13:27:30 -08:00
tobigr
13577f5421 NewPipe 0.28.3 (1008) 2026-02-05 18:06:15 +01:00
tobigr
dc8a62914a Add changelog for NewPipe 0.28.3 2026-02-05 18:06:15 +01:00
tobigr
74d5a8b173 Update extractor to version 0.25.2 2026-02-05 18:06:15 +01:00
Hosted Weblate
d2a8955964 Translated using Weblate (Basque)
Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.5% (745 of 764 strings)

Translated using Weblate (Kabyle)

Currently translated at 27.7% (212 of 764 strings)

Translated using Weblate (German)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.3% (759 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.5% (745 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Icelandic)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Malay)

Currently translated at 58.7% (449 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (764 of 764 strings)

Merge branch 'origin/dev' into Weblate.

Translated using Weblate (Somali)

Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Somali)

Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Danish)

Currently translated at 98.5% (753 of 764 strings)

Translated using Weblate (Danish)

Currently translated at 98.5% (753 of 764 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 80.4% (615 of 764 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 73.5% (562 of 764 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 73.5% (562 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Georgian)

Currently translated at 92.5% (707 of 764 strings)

Translated using Weblate (Kurdish)

Currently translated at 60.9% (466 of 764 strings)

Translated using Weblate (Catalan)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Catalan)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Greek)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (French)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (French)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Japanese)

Currently translated at 95.5% (730 of 764 strings)

Translated using Weblate (Belarusian)

Currently translated at 98.8% (755 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.3% (759 of 764 strings)

Translated using Weblate (Odia)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.1% (238 of 764 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.1% (238 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Tamazight (Central Atlas))

Currently translated at 18.5% (142 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 93.4% (714 of 764 strings)

Translated using Weblate (Slovak)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hindi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hindi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Asturian)

Currently translated at 60.6% (463 of 764 strings)

Translated using Weblate (Asturian)

Currently translated at 60.6% (463 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Sardinian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Sardinian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Chinese (Traditional Han script, Hong Kong))

Currently translated at 95.1% (727 of 764 strings)

Translated using Weblate (Albanian)

Currently translated at 76.3% (583 of 764 strings)

Translated using Weblate (Albanian)

Currently translated at 76.3% (583 of 764 strings)

Translated using Weblate (Nepali)

Currently translated at 56.2% (430 of 764 strings)

Translated using Weblate (Nepali)

Currently translated at 56.2% (430 of 764 strings)

Translated using Weblate (Finnish)

Currently translated at 94.2% (720 of 764 strings)

Translated using Weblate (Finnish)

Currently translated at 94.2% (720 of 764 strings)

Translated using Weblate (Uzbek (Latin script))

Currently translated at 59.2% (453 of 764 strings)

Translated using Weblate (Uzbek (Latin script))

Currently translated at 59.2% (453 of 764 strings)

Translated using Weblate (Punjabi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 62.5% (478 of 764 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 62.5% (478 of 764 strings)

Translated using Weblate (Icelandic)

Currently translated at 98.1% (750 of 764 strings)

Translated using Weblate (N’Ko)

Currently translated at 85.7% (655 of 764 strings)

Translated using Weblate (Croatian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Serbian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Serbian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bengali)

Currently translated at 74.2% (567 of 764 strings)

Translated using Weblate (German)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (German)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Romanian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Romanian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (ryu (generated) (ryu))

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Arabic)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Malayalam)

Currently translated at 73.0% (558 of 764 strings)

Translated using Weblate (Malayalam)

Currently translated at 73.0% (558 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Persian)

Currently translated at 95.6% (731 of 764 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.9% (687 of 764 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.9% (687 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 99.7% (762 of 764 strings)

Co-authored-by: 2-Seol <2Seol.0117@gmail.com>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andreas Westrell <andreas.westrell@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Anxhelo Lushka <github@lushka.al>
Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: Aurelian Ciocîltan <aurelianciociltan@gmail.com>
Co-authored-by: Bakary Kaba <mbkaba@live.fr>
Co-authored-by: Balázs Meskó <meskobalazs@mailbox.org>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Comrade KVRONV <naruto.tkntcube31@gmail.com>
Co-authored-by: D D <keptawesome@gmail.com>
Co-authored-by: David Rebolo Magariños <davidre345@hotmail.com>
Co-authored-by: Deleted User <jsdelvani@users.noreply.hosted.weblate.org>
Co-authored-by: Deleted User <noreply+10474@weblate.org>
Co-authored-by: Deleted User <noreply+19964@weblate.org>
Co-authored-by: Dormin <nkzo3d+1uozh1scczh8c@sharklasers.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Enol P. <enolp@softastur.org>
Co-authored-by: Erenay <erenaydev@proton.me>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Francesco Saltori <francescosaltori@gmail.com>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Guillem <guillemglez@gmail.com>
Co-authored-by: Hakim Oubouali <hakim.oubouali.skr@gmail.com>
Co-authored-by: Hoseok Seo <ddinghoya@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ishwor Ghimire <ghimire.esor09@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Kiss Attila <gaxeco4855@pro5g.com>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Mathias Hamza Vedsted-Mirza <mathiashamzamirza@outlook.com>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mukhamadjonov <abdukodir.9507@gmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
Co-authored-by: Nikoloz <nukushatugushi@gmail.com>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: SecularSteve <fairfull.playing@gmail.com>
Co-authored-by: ShareASmile <ShareASmile@users.noreply.hosted.weblate.org>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Sérgio Marques <smarquespt@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Thadah D. Denyse <thadahdenyse@protonmail.com>
Co-authored-by: Theophine Savio Theodore <theophinetheodore@gmail.com>
Co-authored-by: Trunars <trunars@abv.bg>
Co-authored-by: Vasilis K. <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Ville Rantanen <v.r@iki.fi>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: cehnemdark <cehennem1001@gmail.com>
Co-authored-by: gymka <gymka@archlinux.lt>
Co-authored-by: justcontributor <kty5663@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: winqooq <winqooq@gmail.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2026-02-05 18:06:15 +01:00
Hosted Weblate
955844b3e1 Translated using Weblate (Basque)
Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.5% (745 of 764 strings)

Translated using Weblate (Kabyle)

Currently translated at 27.7% (212 of 764 strings)

Translated using Weblate (German)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.3% (759 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.5% (745 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Icelandic)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Malay)

Currently translated at 58.7% (449 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (764 of 764 strings)

Merge branch 'origin/dev' into Weblate.

Translated using Weblate (Somali)

Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Somali)

Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Danish)

Currently translated at 98.5% (753 of 764 strings)

Translated using Weblate (Danish)

Currently translated at 98.5% (753 of 764 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 80.4% (615 of 764 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 73.5% (562 of 764 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 73.5% (562 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Georgian)

Currently translated at 92.5% (707 of 764 strings)

Translated using Weblate (Kurdish)

Currently translated at 60.9% (466 of 764 strings)

Translated using Weblate (Catalan)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Catalan)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Greek)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (French)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (French)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Japanese)

Currently translated at 95.5% (730 of 764 strings)

Translated using Weblate (Belarusian)

Currently translated at 98.8% (755 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.3% (759 of 764 strings)

Translated using Weblate (Odia)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.1% (238 of 764 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.1% (238 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Tamazight (Central Atlas))

Currently translated at 18.5% (142 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 93.4% (714 of 764 strings)

Translated using Weblate (Slovak)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hindi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hindi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Asturian)

Currently translated at 60.6% (463 of 764 strings)

Translated using Weblate (Asturian)

Currently translated at 60.6% (463 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Sardinian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Sardinian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Chinese (Traditional Han script, Hong Kong))

Currently translated at 95.1% (727 of 764 strings)

Translated using Weblate (Albanian)

Currently translated at 76.3% (583 of 764 strings)

Translated using Weblate (Albanian)

Currently translated at 76.3% (583 of 764 strings)

Translated using Weblate (Nepali)

Currently translated at 56.2% (430 of 764 strings)

Translated using Weblate (Nepali)

Currently translated at 56.2% (430 of 764 strings)

Translated using Weblate (Finnish)

Currently translated at 94.2% (720 of 764 strings)

Translated using Weblate (Finnish)

Currently translated at 94.2% (720 of 764 strings)

Translated using Weblate (Uzbek (Latin script))

Currently translated at 59.2% (453 of 764 strings)

Translated using Weblate (Uzbek (Latin script))

Currently translated at 59.2% (453 of 764 strings)

Translated using Weblate (Punjabi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 62.5% (478 of 764 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 62.5% (478 of 764 strings)

Translated using Weblate (Icelandic)

Currently translated at 98.1% (750 of 764 strings)

Translated using Weblate (N’Ko)

Currently translated at 85.7% (655 of 764 strings)

Translated using Weblate (Croatian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Serbian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Serbian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bengali)

Currently translated at 74.2% (567 of 764 strings)

Translated using Weblate (German)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (German)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Romanian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Romanian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (ryu (generated) (ryu))

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Arabic)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Malayalam)

Currently translated at 73.0% (558 of 764 strings)

Translated using Weblate (Malayalam)

Currently translated at 73.0% (558 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Persian)

Currently translated at 95.6% (731 of 764 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.9% (687 of 764 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.9% (687 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 99.7% (762 of 764 strings)

Co-authored-by: 2-Seol <2Seol.0117@gmail.com>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andreas Westrell <andreas.westrell@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Anxhelo Lushka <github@lushka.al>
Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: Aurelian Ciocîltan <aurelianciociltan@gmail.com>
Co-authored-by: Bakary Kaba <mbkaba@live.fr>
Co-authored-by: Balázs Meskó <meskobalazs@mailbox.org>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Comrade KVRONV <naruto.tkntcube31@gmail.com>
Co-authored-by: D D <keptawesome@gmail.com>
Co-authored-by: David Rebolo Magariños <davidre345@hotmail.com>
Co-authored-by: Deleted User <jsdelvani@users.noreply.hosted.weblate.org>
Co-authored-by: Deleted User <noreply+10474@weblate.org>
Co-authored-by: Deleted User <noreply+19964@weblate.org>
Co-authored-by: Dormin <nkzo3d+1uozh1scczh8c@sharklasers.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Enol P. <enolp@softastur.org>
Co-authored-by: Erenay <erenaydev@proton.me>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Francesco Saltori <francescosaltori@gmail.com>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Guillem <guillemglez@gmail.com>
Co-authored-by: Hakim Oubouali <hakim.oubouali.skr@gmail.com>
Co-authored-by: Hoseok Seo <ddinghoya@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ishwor Ghimire <ghimire.esor09@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Kiss Attila <gaxeco4855@pro5g.com>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Mathias Hamza Vedsted-Mirza <mathiashamzamirza@outlook.com>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mukhamadjonov <abdukodir.9507@gmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
Co-authored-by: Nikoloz <nukushatugushi@gmail.com>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: SecularSteve <fairfull.playing@gmail.com>
Co-authored-by: ShareASmile <ShareASmile@users.noreply.hosted.weblate.org>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Sérgio Marques <smarquespt@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Thadah D. Denyse <thadahdenyse@protonmail.com>
Co-authored-by: Theophine Savio Theodore <theophinetheodore@gmail.com>
Co-authored-by: Trunars <trunars@abv.bg>
Co-authored-by: Vasilis K. <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Ville Rantanen <v.r@iki.fi>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: cehnemdark <cehennem1001@gmail.com>
Co-authored-by: gymka <gymka@archlinux.lt>
Co-authored-by: justcontributor <kty5663@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: winqooq <winqooq@gmail.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2026-02-05 17:41:09 +01:00
Tobi
5112acf008 Merge pull request #13197 from TeamNewPipe/backport-13195-to-release-0.28.3
[Backport release-0.28.3] Fix 13139 resume playback
2026-02-05 08:27:48 -08:00
AbsurdlyLongUsername
fcb77fed93 Fix additional setRecovery from rebase errors
(cherry picked from commit 1554f77762)
2026-02-05 14:53:19 +00:00
AbsurdlyLongUsername
2027b6dbc7 Add conditional guard to prevent useVideoAndSubtitles overwriting recovery position that was set in Player.handleIntent for RESUME_PLAYBACK when resuming playback
(cherry picked from commit 118def08b4)
2026-02-05 14:53:19 +00:00
AbsurdlyLongUsername
90d5d5f4de Update useVideoAndSubtitles rename in comment
(cherry picked from commit 725cb70cbd)
2026-02-05 14:53:19 +00:00
AbsurdlyLongUsername
045e91df1c Small refactor getPlayQueueFromCache
(cherry picked from commit 5525d206dc)
2026-02-05 14:53:19 +00:00
Tobi
e74907561e Merge pull request #13195 from absurdlylongusername/fix-13139-resume-playback
Fix 13139 resume playback
2026-02-05 06:49:57 -08:00
AbsurdlyLongUsername
1554f77762 Fix additional setRecovery from rebase errors 2026-02-05 11:17:30 +00:00
AbsurdlyLongUsername
118def08b4 Add conditional guard to prevent useVideoAndSubtitles overwriting recovery position that was set in Player.handleIntent for RESUME_PLAYBACK when resuming playback 2026-02-05 05:05:41 +00:00
AbsurdlyLongUsername
725cb70cbd Update useVideoAndSubtitles rename in comment 2026-02-05 05:05:40 +00:00
AbsurdlyLongUsername
5525d206dc Small refactor getPlayQueueFromCache 2026-02-05 05:05:40 +00:00
Stypox
83f9646eec Merge pull request #13190 from TeamNewPipe/agp9fixes
Partially revert upgrade to AGP 9.0.0
2026-02-04 19:47:43 +01:00
Aayush Gupta
85d43fe45e proguard: Keep fields of generated proguard files
Inspired from https://github.com/protocolbuffers/protobuf/blob/main/java/lite.md#r8-rule-to-make-production-app-builds-work

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-04 16:22:48 +08:00
Aayush Gupta
8d6e68d6f4 Partially revert upgrade to AGP 9.0.0
Building is broken on ecrypted filesystems

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-04 15:56:44 +08:00
Yevhen Babiichuk (DustDFG)
07fe1e758a Refactor settings/tabs/TabsJsonHelper.java to use java streams 2026-02-02 23:59:01 +02:00
Aayush Gupta
15b5cef6c2 Merge pull request #13136 from TeamNewPipe/agp9
Upgrade Android Gradle Plugin to 9.0.0
2026-02-01 17:34:47 +08:00
Jonathan Davies
ae60f7d7eb FeedLoadManager: Shuffle the order outdated subscriptions are updated in 2026-01-31 20:47:48 +00:00
Tobi
739b6ae57b Merge pull request #13141 from salmanmkc/upgrade-github-actions-node24-general
Upgrade GitHub Actions to latest versions
2026-01-30 02:31:40 -08:00
Tobi
cc33b685a5 Merge pull request #13140 from salmanmkc/upgrade-github-actions-node24
Upgrade GitHub Actions for Node 24 compatibility
2026-01-30 02:31:29 -08:00
Salman Muin Kayser Chishti
d051e8ecc8 Upgrade GitHub Actions to latest versions
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
2026-01-30 09:16:42 +00:00
Salman Muin Kayser Chishti
51e62f09ba Upgrade GitHub Actions for Node 24 compatibility
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
2026-01-30 09:16:36 +00:00
Yevhen Babiichuk (DustDFG)
8a2c47bc12 Remove dead code from info_list/InfoItemBuilder
It no longer really builds any view and used only for stroing click
gesture callbacks. In the same way lik local/LocalItemBuilder does

Last usage of build functions: 2e9a860aaa
2026-01-29 15:01:40 +02:00
Aayush Gupta
a7aad63bbb Upgrade Kotlin and KSP
Fixes multiple build errors. Once parcelize is fixed, we should be good
to use built-in Kotlin completely

Ref: https://issuetracker.google.com/issues/478401081

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:59:25 +08:00
Aayush Gupta
fd192b4f3f Drop default properties
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:53 +08:00
Aayush Gupta
19e94bd30c Migrate from deprecated srcDir to directories method
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:53 +08:00
Aayush Gupta
7758a27694 Migrate from deprecated android block to ApplicationExtension
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:53 +08:00
Aayush Gupta
a3301dcfb1 Enable resValues as build feature
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:53 +08:00
Aayush Gupta
d045b27cea Migrate to built-in Kotlin
Ref: https://developer.android.com/build/migrate-to-built-in-kotlin

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:51 +08:00
Aayush Gupta
4f70235ee8 Enable proguard android optimizations
AGP 9.0+ requires enabling optimizations

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:11 +08:00
Aayush Gupta
54f9bcb03e Upgrade AGP to 9.0.0
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:11 +08:00
Stypox
addf1e23b3 Add changelogs for v0.28.2 (1007) 2026-01-28 03:28:23 +01:00
Stypox
a40d7ff70e Hotfix release v0.28.2 (1007) 2026-01-28 03:28:23 +01:00
Hosted Weblate
d53f7acfa4 Translated using Weblate (Somali)
Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Somali)

Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Danish)

Currently translated at 98.5% (753 of 764 strings)

Translated using Weblate (Danish)

Currently translated at 98.5% (753 of 764 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 80.4% (615 of 764 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 73.5% (562 of 764 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 73.5% (562 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Georgian)

Currently translated at 92.5% (707 of 764 strings)

Translated using Weblate (Kurdish)

Currently translated at 60.9% (466 of 764 strings)

Translated using Weblate (Catalan)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Catalan)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Greek)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (French)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (French)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Japanese)

Currently translated at 95.5% (730 of 764 strings)

Translated using Weblate (Belarusian)

Currently translated at 98.8% (755 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.3% (759 of 764 strings)

Translated using Weblate (Odia)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.1% (238 of 764 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.1% (238 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Tamazight (Central Atlas))

Currently translated at 18.5% (142 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 93.4% (714 of 764 strings)

Translated using Weblate (Slovak)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hindi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hindi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Asturian)

Currently translated at 60.6% (463 of 764 strings)

Translated using Weblate (Asturian)

Currently translated at 60.6% (463 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Sardinian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Sardinian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Chinese (Traditional Han script, Hong Kong))

Currently translated at 95.1% (727 of 764 strings)

Translated using Weblate (Albanian)

Currently translated at 76.3% (583 of 764 strings)

Translated using Weblate (Albanian)

Currently translated at 76.3% (583 of 764 strings)

Translated using Weblate (Nepali)

Currently translated at 56.2% (430 of 764 strings)

Translated using Weblate (Nepali)

Currently translated at 56.2% (430 of 764 strings)

Translated using Weblate (Finnish)

Currently translated at 94.2% (720 of 764 strings)

Translated using Weblate (Finnish)

Currently translated at 94.2% (720 of 764 strings)

Translated using Weblate (Uzbek (Latin script))

Currently translated at 59.2% (453 of 764 strings)

Translated using Weblate (Uzbek (Latin script))

Currently translated at 59.2% (453 of 764 strings)

Translated using Weblate (Punjabi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 62.5% (478 of 764 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 62.5% (478 of 764 strings)

Translated using Weblate (Icelandic)

Currently translated at 98.1% (750 of 764 strings)

Translated using Weblate (N’Ko)

Currently translated at 85.7% (655 of 764 strings)

Translated using Weblate (Croatian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Serbian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Serbian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bengali)

Currently translated at 74.2% (567 of 764 strings)

Translated using Weblate (German)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (German)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Romanian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Romanian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (ryu (generated) (ryu))

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Arabic)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Malayalam)

Currently translated at 73.0% (558 of 764 strings)

Translated using Weblate (Malayalam)

Currently translated at 73.0% (558 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Persian)

Currently translated at 95.6% (731 of 764 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.9% (687 of 764 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.9% (687 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 99.7% (762 of 764 strings)

Co-authored-by: 2-Seol <2Seol.0117@gmail.com>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andreas Westrell <andreas.westrell@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Anxhelo Lushka <github@lushka.al>
Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: Aurelian Ciocîltan <aurelianciociltan@gmail.com>
Co-authored-by: Bakary Kaba <mbkaba@live.fr>
Co-authored-by: Balázs Meskó <meskobalazs@mailbox.org>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: D D <keptawesome@gmail.com>
Co-authored-by: David Rebolo Magariños <davidre345@hotmail.com>
Co-authored-by: Deleted User <jsdelvani@users.noreply.hosted.weblate.org>
Co-authored-by: Deleted User <noreply+10474@weblate.org>
Co-authored-by: Deleted User <noreply+19964@weblate.org>
Co-authored-by: Dormin <nkzo3d+1uozh1scczh8c@sharklasers.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Enol P. <enolp@softastur.org>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Francesco Saltori <francescosaltori@gmail.com>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Guillem <guillemglez@gmail.com>
Co-authored-by: Hakim Oubouali <hakim.oubouali.skr@gmail.com>
Co-authored-by: Hoseok Seo <ddinghoya@gmail.com>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ishwor Ghimire <ghimire.esor09@gmail.com>
Co-authored-by: Kiss Attila <gaxeco4855@pro5g.com>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Mathias Hamza Vedsted-Mirza <mathiashamzamirza@outlook.com>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mukhamadjonov <abdukodir.9507@gmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
Co-authored-by: Nikoloz <nukushatugushi@gmail.com>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: SecularSteve <fairfull.playing@gmail.com>
Co-authored-by: ShareASmile <ShareASmile@users.noreply.hosted.weblate.org>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Sérgio Marques <smarquespt@gmail.com>
Co-authored-by: Theophine Savio Theodore <theophinetheodore@gmail.com>
Co-authored-by: Ville Rantanen <v.r@iki.fi>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: cehnemdark <cehennem1001@gmail.com>
Co-authored-by: gymka <gymka@archlinux.lt>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: winqooq <winqooq@gmail.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
2026-01-28 03:28:23 +01:00
Hosted Weblate
ee52b08546 Translated using Weblate (Basque)
Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 20.6% (18 of 87 strings)

Translated using Weblate (Latvian)

Currently translated at 96.9% (741 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.6% (761 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Thai)

Currently translated at 37.3% (285 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 33.3% (29 of 87 strings)

Translated using Weblate (Spanish)

Currently translated at 74.7% (65 of 87 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Lombard)

Currently translated at 4.9% (38 of 764 strings)

Translated using Weblate (Lombard)

Currently translated at 2.2% (2 of 87 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Arabic)

Currently translated at 97.7% (85 of 87 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 33.3% (29 of 87 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 95.8% (732 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 95.8% (732 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 95.6% (731 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (87 of 87 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 74.7% (65 of 87 strings)

Added translation using Weblate (Yiddish)

Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: CJ Montero <cristlad@proton.me>
Co-authored-by: Erenay <erenaydev@proton.me>
Co-authored-by: Hasan Kara <hasanbeytullahkara@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igi 5216 <dr.siantonov@gmail.com>
Co-authored-by: Mohammed al-Qubati <mhraqeeb@gmail.com>
Co-authored-by: Nicolás Pérez <ccnicolasperez@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Thadah D. Denyse <thadahdenyse@protonmail.com>
Co-authored-by: ThaiWithNoBraincell <altofskgd@gmail.com>
Co-authored-by: What's news <fishelschonfeld@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/eu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lmo/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translation: NewPipe/Metadata
2026-01-28 03:03:51 +01:00
Stypox
864725bf0f Merge pull request #12601 from AudricV/live-prefer-dash-and-fetch-bg-audio-only 2026-01-28 02:41:09 +01:00
Stypox
c2723096ab Avoid rebuilding BackgroundPlayerUi if already in place 2026-01-28 02:39:56 +01:00
Stypox
eb7351c858 Remove file committed by accident 2026-01-28 02:20:58 +01:00
Stypox
216867c597 Address review comments 2026-01-28 02:20:33 +01:00
AudricV
1d8ea0181f Disable fetching video and text tracks in background player
This reduces data usage for manifest sources with demuxed audio and
video, such as livestreams, for non-HLS sources only due to an
ExoPlayer bug.
2026-01-28 02:13:14 +01:00
AudricV
4648cac9c6 Allow changing video and text tracks state without stream info
This allows disabling these track types when stream info has been not
loaded while the ExoPlayer instance is.

It is now possible to do so with the background player, in order to
disable fetching video and text tracks for manifest sources,
especially used for livestreams.

Also set the recovery first before reloading play queue manager in the
useVideoAndSubtitles method of the Player class.
2026-01-28 02:13:14 +01:00
AudricV
0578e7fde0 Rename useVideoSource to useVideoAndSubtitles in Player
As both subtitles and video tracks are disabled in this method, the
goal of this rename is to highlight disabling/enabled subtitles.
2026-01-28 02:13:14 +01:00
AudricV
c670ad80ee Use DASH first instead of HLS and YouTube's DASH parser for lives 2026-01-28 02:13:14 +01:00
AudricV
077f34c922 Add a YouTube DASH manifest parser to make live DASH manifests usable
This is a hacky solution, a better one should be investigated and used.
2026-01-28 02:13:14 +01:00
Stypox
635b306de0 Merge pull request #13134 from TeamNewPipe/revert-12781-feat/similar-youtube-client-screen-rotation 2026-01-28 02:12:35 +01:00
Stypox
11af6a2902 Merge pull request #13129 from AudricV/npe_update 2026-01-28 02:08:26 +01:00
Stypox
7e3657831c Merge pull request #13133 from Stypox/missing-report-button 2026-01-28 02:05:16 +01:00
Stypox
c0613b5e54 Merge pull request #13132 from Stypox/regression-detailfragment-flickers 2026-01-28 02:04:15 +01:00
AudricV
bffee48bcb Update NewPipeExtractor 2026-01-28 02:01:45 +01:00
Stypox
49e95d95a1 Revert "Remember and restore orientation on fullscreen exit" 2026-01-28 01:44:39 +01:00
Stypox
dc160da034 Allow reporting ContentNotAvailableException 2026-01-28 00:14:21 +01:00
Stypox
5155b24ed6 Partial revert: fix VideoDetailFragment flickering
This partially reverts commit 92a07a3445, which was needed to fix ghost notifications. There I broke the "cycle" causing the useless notifications to popup in 2 different places (see points 3 and 4 of the commit description).
However, breaking the cycle in point 4 ("`PlayerHolder::tryBindIfNeeded()` is now used to passively try to bind, instead of `PlayerHolder::startService()`" was not correct, for the following reason.
I assumed that `ACTION_PLAYER_STARTED` was used for notifying that the player was instantiated anew, while it actually is used to notify that something is now ready for use: it could be the player, but it could also just be that the bottom sheet view was just added and thus the VideoDetailFragment needs to start the player.
Therefore, when handling `ACTION_PLAYER_STARTED` it is correct to start the player service and not just try to bind to it.
The other point in which I broke the cycle (point 3) should still prevent ghost notifications, although I could not test.
2026-01-27 23:44:25 +01:00
Stypox
4e0d542994 Merge pull request #12929 from TeamNewPipe/fix/playlist-remove-watched 2026-01-27 22:07:10 +01:00
Stypox
817fccb7a3 Swap && to reduce computation 2026-01-27 22:02:01 +01:00
tobigr
8d9af62736 Extract dialog creation into its own method 2026-01-27 22:02:01 +01:00
tobigr
8f32532acd "Removed watched videos" changed to "Remove watched streams"
Playlists can also contain audio-only items. Therefore, the term "stream" is used.
2026-01-27 22:01:59 +01:00
tobigr
0611d650e7 Use checkbox to remove partially watched videos 2026-01-27 22:00:15 +01:00
tobigr
d1f6337c6e Fix removing unwatched streams from playlist when using "remove watched"
The bug is caused by a wanted but forgotten inconsistency in the database.
A stream can be listed in the watch history (StreamHistoryEntity) while having no corresponding playback state (StreamStateEntity) containing the matching playback position. This is caused by the fact that NewPipe does not consider a watch time of less than five seconds to be worthy to be put into the StreamStateEntity because the video was most likely played by error. Those videos are, however, counted and stored in the watch history.
2026-01-27 22:00:15 +01:00
Stypox
d7dffb7a90 Add deprecation to LocalItemListAdapter.showFooter(true) 2026-01-27 21:35:21 +01:00
Stypox
d0f32b3842 Merge pull request #12996 from whistlingwoods/reapply-local-list-header-fix-by-j-haldane 2026-01-27 21:32:29 +01:00
AudricV
25b133946d [YouTube] Adapt YoutubeHttpDataSource to TVHTML5 client removal in NPE 2026-01-23 22:04:05 +01:00
Aayush Gupta
460cadf694 Merge pull request #13081 from TeamNewPipe/ktlint
Fix and enable multiple ktlint rules
2026-01-24 00:41:17 +08:00
Aayush Gupta
2fd2822053 ktlint: Drop non-required backing-property-naming supression
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:32:03 +08:00
Aayush Gupta
8ac8424cab ktlint: Drop unused chain-method-continuation violation supression
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:32:03 +08:00
Aayush Gupta
6b2a1cedef ktlint: Fix function-literal violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:32:03 +08:00
Aayush Gupta
8c5f13ab5c ktlint: Fix function-signature violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:32:03 +08:00
Aayush Gupta
eb97366e41 ktlint: Drop non-required violation supression
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:32:03 +08:00
Aayush Gupta
81ddd5a115 ktlint: Fix no-empty-first-line-in-method-block violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:32:02 +08:00
Aayush Gupta
7d5647b0ba ktlint: Fix standard_argument-list-wrapping violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:44 +08:00
Aayush Gupta
9b7874ff51 ktlint: Fix blank-line-between-when-conditions violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:44 +08:00
Aayush Gupta
ff2390b144 ktlint: Fix enum wrapping violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:44 +08:00
Aayush Gupta
81fd089a32 ktlint: Fix block comment violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:44 +08:00
Aayush Gupta
1466dd17b1 ktlint: Fix indentation violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:44 +08:00
Aayush Gupta
555cd3acb7 ktlint: Fix standard_kdoc violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:44 +08:00
Aayush Gupta
ea105e9026 ktlint: Drop non-required violation supressions again
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:44 +08:00
Aayush Gupta
e86846ba6a ktlint: Fix spacing-between-declarations-with-annotations violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:44 +08:00
Aayush Gupta
dcb2460c81 ktlint: Fix spacing-between-declarations-with-comments violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:44 +08:00
Aayush Gupta
6190db7d2f ktlint: Fix violations regarding statement wrapping
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:43 +08:00
Aayush Gupta
c60339fc58 ktlint: Drop non-required violation supression
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:14 +08:00
Aayush Gupta
80a543e7ab ktlint: Fix violation related to comment placement
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:14 +08:00
Aayush Gupta
c76d14dfd4 ktlint: Drop unused trailing commas
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:14 +08:00
Aayush Gupta
d6f3dee9f4 ktlint: Drop unused semi-colons
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:14 +08:00
Aayush Gupta
247cbf3d6f ktlint: Drop non-required violation supressions
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:14 +08:00
Aayush Gupta
0641c19388 ktlint: Fix multi-line if-else violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:14 +08:00
Aayush Gupta
2c808b0e86 ktlint: Fix ktlint_standard_annotation violations
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:14 +08:00
Aayush Gupta
f23d8eff57 ktlint: Order imports in lexicographic order
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:31:13 +08:00
Aayush Gupta
ed4b77b5aa ktlint: Set codestyle and ignore function naming for Composable
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-24 00:30:04 +08:00
Aayush Gupta
675dbd35d0 Merge pull request #13036 from dustdfg/kotlin_misc_refactor
Misc small kotlin based refactors
2026-01-23 14:51:15 +08:00
Aayush Gupta
eec3ac166a Merge pull request #12936 from TobiGr/uddate-extractor
Update NewPipe Extractor, checkstyle and sonarqube to latest versions
2026-01-22 17:53:14 +08:00
Aayush Gupta
bcb7469d30 Run checkstyle with JDK 21
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-22 17:41:08 +08:00
Aayush Gupta
40d7dcf3d3 Update Gradle to 9.3.0
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-22 17:41:08 +08:00
Aayush Gupta
16415d13ed Update more dependencies to latest stable release
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-22 17:41:08 +08:00
tobigr
3ab414252f Update NewPipe Extractor, checkstyle and sonarqube to latest versions
Add note that new webkit versions require a minSdk >= 23
2026-01-22 17:41:08 +08:00
Aayush Gupta
7d95ec71aa Merge pull request #13019 from dustdfg/orphaned_again
Remove file orphaned for long time
2026-01-19 14:01:48 +08:00
whistlingwoods
cea5dd474b Merge branch dev into reapply-local-list-header-fix-by-j-haldane 2026-01-15 19:24:32 +05:30
Yevhen Babiichuk (DustDFG)
6ddd4a7e63 VideoDetailFragment: hide relatedItemsLayout in tablet mode after fullscreen
Fixes: #7617
2026-01-11 18:04:26 +02:00
Yevhen Babiichuk (DustDFG)
4f0e62e599 Misc small kotlin based refactors
Java file here because it uses kotlin function which returns non null
2026-01-11 17:10:13 +02:00
Yevhen Babiichuk (DustDFG)
1eeba8daa7 Remove file orphaned for long time
It was not deleted by mistake, delete it now

Was orphaned at 1d69bd48be
2026-01-07 20:33:35 +02:00
whistlingwoods
738338d092 Remove unneeded LayoutInflater from LocalItemListAdapter
Co-Authored-By: j-haldane <200528955+j-haldane@users.noreply.github.com>
2026-01-04 00:43:47 +05:30
whistlingwoods
e758b5f890 Adapt header handling changes from other recyclerview adapters to fix...
... Crash in lists (ViewHolder views not attached) in StatisticsPlaylistFragment

Co-Authored-By: j-haldane <200528955+j-haldane@users.noreply.github.com>
2026-01-04 00:40:25 +05:30
477 changed files with 5474 additions and 5558 deletions

View File

@@ -6,39 +6,13 @@
root = true
[*.{kt,kts}]
ktlint_standard_annotation = disabled
ktlint_standard_argument-list-wrapping = disabled
ktlint_standard_backing-property-naming = disabled
ktlint_standard_blank-line-before-declaration = disabled
ktlint_standard_blank-line-between-when-conditions = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_code_style = android_studio
# https://pinterest.github.io/ktlint/latest/rules/standard/#function-naming
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled
ktlint_standard_comment-wrapping = disabled
ktlint_standard_enum-wrapping = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-literal = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_indent = disabled
ktlint_standard_kdoc = disabled
ktlint_standard_max-line-length = disabled
ktlint_standard_mixed-condition-operators = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_multiline-if-else = disabled
ktlint_standard_no-blank-line-in-list = disabled
ktlint_standard_no-consecutive-comments = disabled
ktlint_standard_no-empty-first-line-in-class-body = disabled
ktlint_standard_no-empty-first-line-in-method-block = disabled
ktlint_standard_no-line-break-after-else = disabled
ktlint_standard_no-semi = disabled
ktlint_standard_no-single-line-block-comment = disabled
ktlint_standard_package-name = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_property-naming = disabled
ktlint_standard_spacing-between-declarations-with-annotations = disabled
ktlint_standard_spacing-between-declarations-with-comments = disabled
ktlint_standard_statement-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled
ktlint_standard_try-catch-finally-spacing = disabled
ktlint_standard_when-entry-bracing = disabled

View File

@@ -22,7 +22,7 @@ jobs:
github.event.comment.author_association == 'MEMBER'
)
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Get backport metadata
# the target branch is the first argument after `/backport`
env:

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v4
- uses: gradle/actions/wrapper-validation@v5
- name: create and checkout branch
# push events already checked out the branch

View File

@@ -3,6 +3,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import com.android.build.api.dsl.ApplicationExtension
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
@@ -32,7 +34,7 @@ kotlin {
}
}
android {
configure<ApplicationExtension> {
compileSdk = 36
namespace = "org.schabi.newpipe"
@@ -42,9 +44,9 @@ android {
minSdk = 21
targetSdk = 35
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1006
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1008
versionName = "0.28.1"
versionName = "0.28.3"
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -77,19 +79,18 @@ android {
resValue("string", "app_name", "NewPipe $suffix")
}
isMinifyEnabled = true
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
lint {
checkReleaseBuilds = false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
lintConfig = file("lint.xml")
// Continue the debug build even when errors are found
abortOnError = false
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
// 5.0, avoid using them in switch case statements"), which affects only library projects
disable += "NonConstantResourceId"
}
compileOptions {
@@ -100,7 +101,7 @@ android {
sourceSets {
getByName("androidTest") {
assets.srcDir("$projectDir/schemas")
assets.directories += "$projectDir/schemas"
}
}
@@ -111,6 +112,7 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
resValues = true
}
packaging {
@@ -134,6 +136,13 @@ ksp {
// Custom dependency configuration for ktlint
val ktlint by configurations.creating
// https://checkstyle.org/#JRE_and_JDK
tasks.withType<Checkstyle>().configureEach {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
}
}
checkstyle {
configDirectory = rootProject.file("checkstyle")
isIgnoreFailures = false
@@ -263,7 +272,8 @@ dependencies {
implementation(libs.lisawray.groupie.viewbinding)
// Image loading
implementation(libs.squareup.picasso)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
// Markdown library for Android
implementation(libs.noties.markwon.core)

10
app/lint.xml Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<lint>
<issue id="MissingTranslation" severity="ignore" />
<issue id="MissingQuantity" severity="ignore" />
<issue id="ImpliedQuantity" severity="ignore" />
</lint>

View File

@@ -39,3 +39,8 @@
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
-keep class org.schabi.newpipe.settings.notifications.** { *; }
# Prevent R8 from stripping or renaming Protobuf internal fields
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
<fields>;
}

View File

@@ -176,28 +176,32 @@ class DatabaseMigrationTest {
databaseInV7.run {
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
"search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch1)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
"search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch2)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
"search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch1)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
"search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch2)
@@ -207,13 +211,17 @@ class DatabaseMigrationTest {
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
true, Migrations.MIGRATION_7_8
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_8,
true,
Migrations.MIGRATION_7_8
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
true, Migrations.MIGRATION_8_9
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV8 = getMigratedDatabase()
@@ -235,7 +243,8 @@ class DatabaseMigrationTest {
val remoteUid2: Long
databaseInV8.run {
localUid1 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
"playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "1")
put("is_thumbnail_permanent", false)
@@ -243,7 +252,8 @@ class DatabaseMigrationTest {
}
)
localUid2 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
"playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "2")
put("is_thumbnail_permanent", false)
@@ -251,25 +261,29 @@ class DatabaseMigrationTest {
}
)
delete(
"playlists", "uid = ?",
"playlists",
"uid = ?",
Array(1) { localUid1 }
)
remoteUid1 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
"remote_playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
}
)
remoteUid2 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
"remote_playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
}
)
delete(
"remote_playlists", "uid = ?",
"remote_playlists",
"uid = ?",
Array(1) { remoteUid2 }
)
close()

View File

@@ -4,6 +4,8 @@ import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import io.reactivex.rxjava3.core.Single
import java.io.IOException
import java.time.OffsetDateTime
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@@ -20,9 +22,6 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.stream.StreamType
import java.io.IOException
import java.time.OffsetDateTime
import kotlin.streams.toList
class FeedDAOTest {
private lateinit var db: AppDatabase
@@ -41,14 +40,21 @@ class FeedDAOTest {
private val stream7 = StreamEntity(7, serviceId, "https://youtube.com/watch?v=7", "stream 7", StreamType.VIDEO_STREAM, 1000, "channel-4", "https://youtube.com/channel/4", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
private val allStreams = listOf(
stream1, stream2, stream3, stream4, stream5, stream6, stream7
stream1,
stream2,
stream3,
stream4,
stream5,
stream6,
stream7
)
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java
context,
AppDatabase::class.java
).build()
feedDAO = db.feedDAO()
streamDAO = db.streamDAO()
@@ -65,7 +71,10 @@ class FeedDAOTest {
fun testUnlinkStreamsOlderThan_KeepOne() {
setupUnlinkDelete("2023-08-15T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
FeedGroupEntity.GROUP_ALL_ID,
includePlayed = true,
includePartiallyPlayed = true,
null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
@@ -76,7 +85,10 @@ class FeedDAOTest {
fun testUnlinkStreamsOlderThan_KeepMultiple() {
setupUnlinkDelete("2023-08-01T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
FeedGroupEntity.GROUP_ALL_ID,
includePlayed = true,
includePartiallyPlayed = true,
null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
@@ -112,7 +124,7 @@ class FeedDAOTest {
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")),
SubscriptionEntity.from(ChannelInfo(serviceId, "2", "https://youtube.com/channel/2", "https://youtube.com/channel/2", "channel-2")),
SubscriptionEntity.from(ChannelInfo(serviceId, "3", "https://youtube.com/channel/3", "https://youtube.com/channel/3", "channel-3")),
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4")),
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4"))
)
)
feedDAO.insertAll(
@@ -123,7 +135,7 @@ class FeedDAOTest {
FeedEntity(4, 2),
FeedEntity(5, 2),
FeedEntity(6, 3),
FeedEntity(7, 4),
FeedEntity(7, 4)
)
)
}

View File

@@ -1,6 +1,9 @@
package org.schabi.newpipe.local.history
import androidx.test.core.app.ApplicationProvider
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Assert.assertEquals
@@ -11,9 +14,6 @@ import org.schabi.newpipe.database.AppDatabase
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
import org.schabi.newpipe.testUtil.TestDatabase
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
class HistoryRecordManagerTest {
@@ -54,7 +54,7 @@ class HistoryRecordManagerTest {
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 0, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 1, search = "B"),
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B"),
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B")
)
// make sure all 4 were inserted
@@ -85,7 +85,7 @@ class HistoryRecordManagerTest {
val entries = listOf(
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "B"),
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C"),
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C")
)
// make sure all 3 were inserted
@@ -98,7 +98,6 @@ class HistoryRecordManagerTest {
}
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
// shuffle to make sure the order of items returned by queries depends only on
// SearchHistoryEntry.creationDate, not on the actual insertion time, so that we can
// verify that the `ORDER BY` clause does its job
@@ -121,7 +120,7 @@ class HistoryRecordManagerTest {
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
RELATED_SEARCHES_ENTRIES[4].search, // B
RELATED_SEARCHES_ENTRIES[5].search, // AA
RELATED_SEARCHES_ENTRIES[2].search, // BA
RELATED_SEARCHES_ENTRIES[2].search // BA
)
}
@@ -136,7 +135,7 @@ class HistoryRecordManagerTest {
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 3, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA"),
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA")
)
insertShuffledRelatedSearches(relatedSearches)
@@ -153,7 +152,7 @@ class HistoryRecordManagerTest {
assertThat(searches).containsExactly(
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
RELATED_SEARCHES_ENTRIES[5].search, // AA
RELATED_SEARCHES_ENTRIES[1].search, // BA
RELATED_SEARCHES_ENTRIES[1].search // BA
)
// also make sure that the string comparison is case insensitive
@@ -171,7 +170,7 @@ class HistoryRecordManagerTest {
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "B"),
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 2, search = "AA"),
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A")
)
}
}

View File

@@ -33,8 +33,12 @@ class LocalPlaylistManagerTest {
fun createPlaylist() {
val NEWPIPE_URL = "https://newpipe.net/"
val stream = StreamEntity(
serviceId = 1, url = NEWPIPE_URL, title = "title",
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
serviceId = 1,
url = NEWPIPE_URL,
title = "title",
streamType = StreamType.VIDEO_STREAM,
duration = 1,
uploader = "uploader",
uploaderUrl = NEWPIPE_URL
)
@@ -58,14 +62,22 @@ class LocalPlaylistManagerTest {
@Test()
fun createPlaylist_nonExistentStreamsAreUpserted() {
val stream = StreamEntity(
serviceId = 1, url = "https://newpipe.net/", title = "title",
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
serviceId = 1,
url = "https://newpipe.net/",
title = "title",
streamType = StreamType.VIDEO_STREAM,
duration = 1,
uploader = "uploader",
uploaderUrl = "https://newpipe.net/"
)
database.streamDAO().insert(stream)
val upserted = StreamEntity(
serviceId = 1, url = "https://newpipe.net/2", title = "title2",
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
serviceId = 1,
url = "https://newpipe.net/2",
title = "title2",
streamType = StreamType.VIDEO_STREAM,
duration = 1,
uploader = "uploader",
uploaderUrl = "https://newpipe.net/"
)

View File

@@ -17,21 +17,20 @@ class TrampolineSchedulerRule : TestRule {
private val scheduler = Schedulers.trampoline()
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
override fun evaluate() {
try {
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
RxJavaPlugins.setIoSchedulerHandler { scheduler }
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
RxJavaPlugins.setSingleSchedulerHandler { scheduler }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
override fun evaluate() {
try {
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
RxJavaPlugins.setIoSchedulerHandler { scheduler }
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
RxJavaPlugins.setSingleSchedulerHandler { scheduler }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
base.evaluate()
} finally {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
base.evaluate()
} finally {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
}
}

View File

@@ -156,41 +156,51 @@ class StreamItemAdapterTest {
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))),
1
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))),
2
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))),
3
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))),
4
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
5, MediaFormat.OGG
5,
MediaFormat.OGG
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
6, MediaFormat.FLAC
6,
MediaFormat.FLAC
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))),
7, MediaFormat.AIFF
7,
MediaFormat.AIFF
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))),
8, MediaFormat.M4A
8,
MediaFormat.M4A
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))),
9, MediaFormat.OPUS
9,
MediaFormat.OPUS
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))),
10, MediaFormat.OPUS
10,
MediaFormat.OPUS
)
}
@@ -213,16 +223,24 @@ class StreamItemAdapterTest {
helper.assertInvalidResponse(getResponse(mapOf()), 7)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
getResponse(mapOf(Pair("Content-Type", "audio/flac"))),
8,
MediaFormat.FLAC
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
getResponse(mapOf(Pair("Content-Type", "audio/wav"))),
9,
MediaFormat.WAV
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
getResponse(mapOf(Pair("Content-Type", "audio/opus"))),
10,
MediaFormat.OPUS
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))),
11,
MediaFormat.AIFF
)
}
@@ -230,39 +248,37 @@ class StreamItemAdapterTest {
* @return a list of video streams, in which their video only property mirrors the provided
* [videoOnly] vararg.
*/
private fun getVideoStreams(vararg videoOnly: Boolean) =
StreamItemAdapter.StreamInfoWrapper(
videoOnly.map {
VideoStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.MPEG_4)
.setResolution("720p")
.setIsVideoOnly(it)
.build()
},
context
)
private fun getVideoStreams(vararg videoOnly: Boolean) = StreamInfoWrapper(
videoOnly.map {
VideoStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.MPEG_4)
.setResolution("720p")
.setIsVideoOnly(it)
.build()
},
context
)
/**
* @return a list of audio streams, containing valid and null elements mirroring the provided
* [shouldBeValid] vararg.
*/
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
getSecondaryStreamsFromList(
shouldBeValid.map {
if (it) {
AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.OPUS)
.setAverageBitrate(192)
.build()
} else {
null
}
private fun getAudioStreams(vararg shouldBeValid: Boolean) = getSecondaryStreamsFromList(
shouldBeValid.map {
if (it) {
AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.OPUS)
.setAverageBitrate(192)
.build()
} else {
null
}
)
}
)
private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
val list = ArrayList<AudioStream>(size)
@@ -292,7 +308,7 @@ class StreamItemAdapterTest {
Assert.assertEquals(
"normal visibility (pos=[$position]) is not correct",
findViewById<View>(R.id.wo_sound_icon).visibility,
normalVisibility,
normalVisibility
)
}
spinner.adapter.getDropDownView(position, null, spinner).run {
@@ -307,18 +323,17 @@ class StreamItemAdapterTest {
/**
* Helper function that builds a secondary stream list.
*/
private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) =
SparseArrayCompat<SecondaryStreamHelper<T>?>(streams.size).apply {
streams.forEachIndexed { index, stream ->
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
SecondaryStreamHelper(
StreamItemAdapter.StreamInfoWrapper(streams, context),
it
)
}
put(index, secondaryStreamHelper)
private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) = SparseArrayCompat<SecondaryStreamHelper<T>?>(streams.size).apply {
streams.forEachIndexed { index, stream ->
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
SecondaryStreamHelper(
StreamItemAdapter.StreamInfoWrapper(streams, context),
it
)
}
put(index, secondaryStreamHelper)
}
}
private fun getResponse(headers: Map<String, String>): Response {
val listHeaders = HashMap<String, List<String>>()
@@ -345,7 +360,8 @@ class StreamItemAdapterTest {
index: Int
) {
assertFalse(
"invalid header returns valid value", retrieveMediaFormat(streams[index], response)
"invalid header returns valid value",
retrieveMediaFormat(streams[index], response)
)
assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
}
@@ -359,7 +375,8 @@ class StreamItemAdapterTest {
format: MediaFormat
) {
assertTrue(
"header was not recognized", retrieveMediaFormat(streams[index], response)
"header was not recognized",
retrieveMediaFormat(streams[index], response)
)
assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
}

View File

@@ -1,285 +0,0 @@
package org.schabi.newpipe;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.preference.PreferenceManager;
import com.jakewharton.processphoenix.ProcessPhoenix;
import org.acra.ACRA;
import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* App.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();
private boolean isFirstRun = false;
private boolean notificationsRequested = false;
private static App app;
@NonNull
public static App getApp() {
return app;
}
public boolean getNotificationsRequested() {
return notificationsRequested;
}
public void setNotificationsRequested() {
notificationsRequested = true;
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(base);
initACRA();
}
@Override
public void onCreate() {
super.onCreate();
app = this;
if (ProcessPhoenix.isPhoenixProcess(this)) {
Log.i(TAG, "This is a phoenix process! "
+ "Aborting initialization of App[onCreate]");
return;
}
// check if the last used preference version is set
// to determine whether this is the first app run
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
.getInt(getString(R.string.last_used_preferences_version), -1);
isFirstRun = lastUsedPrefVersion == -1;
// Initialize settings first because other initializations can use its values
NewPipeSettings.initSettings(this);
NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this));
Localization.initPrettyTime(Localization.resolvePrettyTime());
BridgeStateSaverInitializer.init(this);
StateSaver.init(this);
initNotificationChannels();
ServiceHelper.initServices(this);
// Initialize image loader
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
PicassoHelper.init(this);
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
prefs.getString(getString(R.string.image_quality_key),
getString(R.string.image_quality_default))));
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
configureRxJavaErrorHandler();
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl.INSTANCE);
}
@Override
public void onTerminate() {
super.onTerminate();
PicassoHelper.terminate();
}
protected Downloader getDownloader() {
final DownloaderImpl downloader = DownloaderImpl.init(null);
setCookiesToDownloader(downloader);
return downloader;
}
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
}
private void configureRxJavaErrorHandler() {
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
@Override
public void accept(@NonNull final Throwable throwable) {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
+ "throwable = [" + throwable.getClass().getName() + "]");
final Throwable actualThrowable;
if (throwable instanceof UndeliverableException) {
// As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception
actualThrowable = Objects.requireNonNull(throwable.getCause());
} else {
actualThrowable = throwable;
}
final List<Throwable> errors;
if (actualThrowable instanceof CompositeException) {
errors = ((CompositeException) actualThrowable).getExceptions();
} else {
errors = List.of(actualThrowable);
}
for (final Throwable error : errors) {
if (isThrowableIgnored(error)) {
return;
}
if (isThrowableCritical(error)) {
reportException(error);
return;
}
}
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
// When exception is not reported, log it
if (isDisposedRxExceptionsReported()) {
reportException(actualThrowable);
} else {
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
}
}
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
// Don't crash the application over a simple network problem
return ExceptionUtils.hasAssignableCause(throwable,
// network api cancellation
IOException.class, SocketException.class,
// blocking code disposed
InterruptedException.class, InterruptedIOException.class);
}
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
// Though these exceptions cannot be ignored
return ExceptionUtils.hasAssignableCause(throwable,
NullPointerException.class, IllegalArgumentException.class, // bug in app
OnErrorNotImplementedException.class, MissingBackpressureException.class,
IllegalStateException.class); // bug in operator
}
private void reportException(@NonNull final Throwable throwable) {
// Throw uncaught exception that will trigger the report system
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), throwable);
}
});
}
/**
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
*/
protected void initACRA() {
if (ACRA.isACRASenderServiceProcess()) {
return;
}
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
.withBuildConfigClass(BuildConfig.class);
ACRA.init(this, acraConfig);
}
private void initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description))
.build(),
new NotificationChannelCompat
.Builder(getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.app_update_notification_channel_name))
.setDescription(
getString(R.string.app_update_notification_channel_description))
.build(),
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH)
.setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description))
.build(),
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build(),
new NotificationChannelCompat
.Builder(getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(getString(R.string.streams_notification_channel_name))
.setDescription(
getString(R.string.streams_notification_channel_description))
.build()
);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
}
protected boolean isDisposedRxExceptionsReported() {
return false;
}
public boolean isFirstRun() {
return isFirstRun;
}
}

View File

@@ -0,0 +1,293 @@
package org.schabi.newpipe
import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.util.Log
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager
import coil3.ImageLoader
import coil3.SingletonImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.jakewharton.processphoenix.ProcessPhoenix
import io.reactivex.rxjava3.exceptions.CompositeException
import io.reactivex.rxjava3.exceptions.MissingBackpressureException
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
import io.reactivex.rxjava3.exceptions.UndeliverableException
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import java.io.IOException
import java.io.InterruptedIOException
import java.net.SocketException
import org.acra.ACRA.init
import org.acra.ACRA.isACRASenderServiceProcess
import org.acra.config.CoreConfigurationBuilder
import org.schabi.newpipe.error.ReCaptchaActivity
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
import org.schabi.newpipe.ktx.hasAssignableCause
import org.schabi.newpipe.settings.NewPipeSettings
import org.schabi.newpipe.util.BridgeStateSaverInitializer
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.StateSaver
import org.schabi.newpipe.util.image.ImageStrategy
import org.schabi.newpipe.util.image.PreferredImageQuality
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* App.kt is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
open class App :
Application(),
SingletonImageLoader.Factory {
var isFirstRun = false
private set
var notificationsRequested = false
private set
fun setNotificationsRequested() {
notificationsRequested = true
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
initACRA()
}
override fun onCreate() {
super.onCreate()
instance = this
if (ProcessPhoenix.isPhoenixProcess(this)) {
Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]")
return
}
// check if the last used preference version is set
// to determine whether this is the first app run
val lastUsedPrefVersion =
PreferenceManager
.getDefaultSharedPreferences(this)
.getInt(getString(R.string.last_used_preferences_version), -1)
isFirstRun = lastUsedPrefVersion == -1
// Initialize settings first because other initializations can use its values
NewPipeSettings.initSettings(this)
NewPipe.init(
getDownloader(),
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this)
)
Localization.initPrettyTime(Localization.resolvePrettyTime())
BridgeStateSaverInitializer.init(this)
StateSaver.init(this)
initNotificationChannels()
ServiceHelper.initServices(this)
// Initialize image loader
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
ImageStrategy.setPreferredImageQuality(
PreferredImageQuality.fromPreferenceKey(
this,
prefs.getString(
getString(R.string.image_quality_key),
getString(R.string.image_quality_default)
)
)
)
configureRxJavaErrorHandler()
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
}
override fun newImageLoader(context: Context): ImageLoader = ImageLoader
.Builder(this)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
.crossfade(true)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
}.build()
protected open fun getDownloader(): Downloader {
val downloader = DownloaderImpl.init(null)
setCookiesToDownloader(downloader)
return downloader
}
protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val key = getString(R.string.recaptcha_cookies_key)
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
downloader.updateYoutubeRestrictedModeCookies(this)
}
private fun configureRxJavaErrorHandler() {
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(
object : Consumer<Throwable> {
override fun accept(throwable: Throwable) {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
// As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception
val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
for (error in errors) {
if (isThrowableIgnored(error)) {
return
}
if (isThrowableCritical(error)) {
reportException(error)
return
}
}
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
// When exception is not reported, log it
if (isDisposedRxExceptionsReported()) {
reportException(actualThrowable)
} else {
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
}
}
fun isThrowableIgnored(throwable: Throwable): Boolean {
// Don't crash the application over a simple network problem
return throwable // network api cancellation
.hasAssignableCause(
IOException::class.java,
SocketException::class.java, // blocking code disposed
InterruptedException::class.java,
InterruptedIOException::class.java
)
}
fun isThrowableCritical(throwable: Throwable): Boolean {
// Though these exceptions cannot be ignored
return throwable
.hasAssignableCause(
// bug in app
NullPointerException::class.java,
IllegalArgumentException::class.java,
OnErrorNotImplementedException::class.java,
MissingBackpressureException::class.java,
// bug in operator
IllegalStateException::class.java
)
}
fun reportException(throwable: Throwable) {
// Throw uncaught exception that will trigger the report system
Thread
.currentThread()
.uncaughtExceptionHandler
.uncaughtException(Thread.currentThread(), throwable)
}
}
)
}
/**
* Called in [.attachBaseContext] after calling the `super` method.
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
*/
protected fun initACRA() {
if (isACRASenderServiceProcess()) {
return
}
val acraConfig =
CoreConfigurationBuilder()
.withBuildConfigClass(BuildConfig::class.java)
init(this, acraConfig)
}
private fun initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
val mainChannel =
NotificationChannelCompat
.Builder(
getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description))
.build()
val appUpdateChannel =
NotificationChannelCompat
.Builder(
getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.app_update_notification_channel_name))
.setDescription(getString(R.string.app_update_notification_channel_description))
.build()
val hashChannel =
NotificationChannelCompat
.Builder(
getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH
).setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description))
.build()
val errorReportChannel =
NotificationChannelCompat
.Builder(
getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build()
val newStreamChannel =
NotificationChannelCompat
.Builder(
getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT
).setName(getString(R.string.streams_notification_channel_name))
.setDescription(getString(R.string.streams_notification_channel_description))
.build()
val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
}
protected open fun isDisposedRxExceptionsReported(): Boolean = false
companion object {
const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
private val TAG = App::class.java.toString()
@JvmStatic
lateinit var instance: App
private set
}
}

View File

@@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader {
this.mCookies = new HashMap<>();
}
@NonNull
public OkHttpClient getClient() {
return client;
}
/**
* It's recommended to call exactly once in the entire lifetime of the application.
*
@@ -161,9 +166,7 @@ public final class DownloaderImpl extends Downloader {
String responseBodyToReturn = null;
try (ResponseBody body = response.body()) {
if (body != null) {
responseBodyToReturn = body.string();
}
responseBodyToReturn = body.string();
}
final String latestUrl = response.request().url().toString();

View File

@@ -1,50 +0,0 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import org.schabi.newpipe.util.NavigationHelper;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* ExitActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ExitActivity extends Activity {
public static void exitAndRemoveFromRecentApps(final Activity activity) {
final Intent intent = new Intent(activity, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.startActivity(intent);
}
@SuppressLint("NewApi")
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finishAndRemoveTask();
NavigationHelper.restartApp(this);
}
}

View File

@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import org.schabi.newpipe.util.NavigationHelper
class ExitActivity : Activity() {
@SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finishAndRemoveTask()
NavigationHelper.restartApp(this)
}
companion object {
@JvmStatic
fun exitAndRemoveFromRecentApps(activity: Activity) {
val intent = Intent(activity, ExitActivity::class.java)
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
or Intent.FLAG_ACTIVITY_CLEAR_TASK
or Intent.FLAG_ACTIVITY_NO_ANIMATION
)
activity.startActivity(intent)
}
}
}

View File

@@ -20,12 +20,14 @@
package org.schabi.newpipe;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -96,6 +98,8 @@ import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.FocusOverlayView;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -191,11 +195,17 @@ public class MainActivity extends AppCompatActivity {
NotificationWorker.initialize(this);
}
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
&& !App.getApp().isFirstRun()
&& !App.getInstance().isFirstRun()
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
}
// ReleaseVersionUtil.INSTANCE.isReleaseApk() will be true only for main official build
// We want every release build (nightly, nightly-refactor) to show the popup
if (!DEBUG) {
showKeepAndroidDialog();
}
MigrationManager.showUserInfoIfPresent(this);
}
@@ -203,7 +213,7 @@ public class MainActivity extends AppCompatActivity {
protected void onPostCreate(final Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
final App app = App.getApp();
final App app = App.getInstance();
if (sharedPreferences.getBoolean(app.getString(R.string.update_app_key), false)
&& sharedPreferences
@@ -309,25 +319,21 @@ public class MainActivity extends AppCompatActivity {
}
private boolean drawerItemSelected(final MenuItem item) {
switch (item.getGroupId()) {
case R.id.menu_services_group:
changeService(item);
break;
case R.id.menu_tabs_group:
tabSelected(item);
break;
case R.id.menu_kiosks_group:
try {
kioskSelected(item);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
}
break;
case R.id.menu_options_about_group:
optionsAboutSelected(item);
break;
default:
return false;
final int groupId = item.getGroupId();
if (groupId == R.id.menu_services_group) {
changeService(item);
} else if (groupId == R.id.menu_tabs_group) {
tabSelected(item);
} else if (groupId == R.id.menu_kiosks_group) {
try {
kioskSelected(item);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
}
} else if (groupId == R.id.menu_options_about_group) {
optionsAboutSelected(item);
} else {
return false;
}
mainBinding.getRoot().closeDrawers();
@@ -977,4 +983,58 @@ public class MainActivity extends AppCompatActivity {
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
}
private void showKeepAndroidDialog() {
final var prefs = PreferenceManager.getDefaultSharedPreferences(this);
final var now = Instant.now();
final var kaoLastCheck = Instant.ofEpochMilli(prefs.getLong(
getString(R.string.kao_last_checked_key),
0
));
final var supportedLannguages = List.of("fr", "de", "ca", "es", "id", "it", "pl",
"pt", "cs", "sk", "fa", "ar", "tr", "el", "th", "ru", "uk", "ko", "zh", "ja");
final var locale = Localization.getAppLocale();
final String kaoBaseUrl = "https://keepandroidopen.org/";
final String kaoURIString;
if (supportedLannguages.contains(locale.getLanguage())) {
if ("zh".equals(locale.getLanguage())) {
kaoURIString = kaoBaseUrl + ("TW".equals(locale.getCountry()) ? "zh-TW" : "zh-CN");
} else {
kaoURIString = kaoBaseUrl + locale.getLanguage();
}
} else {
kaoURIString = kaoBaseUrl;
}
final var kaoURI = Uri.parse(kaoURIString);
final var solutionURI = Uri.parse(
"https://github.com/woheller69/FreeDroidWarn?tab=readme-ov-file#solutions");
if (kaoLastCheck.plus(30, ChronoUnit.DAYS).isBefore(now)) {
final var dialog = new AlertDialog.Builder(this)
.setTitle("Keep Android Open")
.setCancelable(false)
.setMessage(this.getString(R.string.kao_dialog_warning))
.setPositiveButton(this.getString(android.R.string.ok), (d, w) -> {
prefs.edit()
.putLong(
getString(R.string.kao_last_checked_key),
now.toEpochMilli()
)
.apply();
})
.setNeutralButton(this.getString(R.string.kao_solution), null)
.setNegativeButton(this.getString(R.string.kao_dialog_more_info), null)
.show();
// If we use setNeutralButton and etc. dialog will close after pressing the buttons,
// but we want it to close only when positive button is pressed
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v ->
this.startActivity(new Intent(Intent.ACTION_VIEW, kaoURI))
);
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v ->
this.startActivity(new Intent(Intent.ACTION_VIEW, solutionURI))
);
}
}
}

View File

@@ -8,6 +8,7 @@ package org.schabi.newpipe
import android.content.Context
import androidx.room.Room.databaseBuilder
import kotlin.concurrent.Volatile
import org.schabi.newpipe.database.AppDatabase
import org.schabi.newpipe.database.Migrations.MIGRATION_1_2
import org.schabi.newpipe.database.Migrations.MIGRATION_2_3
@@ -17,7 +18,6 @@ import org.schabi.newpipe.database.Migrations.MIGRATION_5_6
import org.schabi.newpipe.database.Migrations.MIGRATION_6_7
import org.schabi.newpipe.database.Migrations.MIGRATION_7_8
import org.schabi.newpipe.database.Migrations.MIGRATION_8_9
import kotlin.concurrent.Volatile
object NewPipeDatabase {

View File

@@ -18,10 +18,10 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException
import java.io.IOException
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.util.ReleaseVersionUtil
import java.io.IOException
class NewVersionWorker(
context: Context,
@@ -46,7 +46,8 @@ class NewVersionWorker(
// Show toast stating that the app is up-to-date if the update check was manual.
ContextCompat.getMainExecutor(applicationContext).execute {
Toast.makeText(
applicationContext, R.string.app_update_unavailable_toast,
applicationContext,
R.string.app_update_unavailable_toast,
Toast.LENGTH_SHORT
).show()
}
@@ -58,7 +59,11 @@ class NewVersionWorker(
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntentCompat.getActivity(
applicationContext, 0, intent, 0, false
applicationContext,
0,
intent,
0,
false
)
val channelId = applicationContext.getString(R.string.app_update_notification_channel_id)
val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId)
@@ -71,12 +76,15 @@ class NewVersionWorker(
)
.setContentText(
applicationContext.getString(
R.string.app_update_available_notification_text, versionName
R.string.app_update_available_notification_text,
versionName
)
)
val notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager.notify(2000, notificationBuilder.build())
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(2000, notificationBuilder.build())
}
}
@Throws(IOException::class, ReCaptchaException::class)

View File

@@ -41,50 +41,50 @@ public final class QueueItemMenuUtil {
}
popupMenu.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.menu_item_remove:
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
case R.id.menu_item_details:
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
case R.id.menu_item_append_playlist:
PlaylistDialog.createCorrespondingDialog(
context,
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
)
);
final int itemId = menuItem.getItemId();
if (itemId == R.id.menu_item_remove) {
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
} else if (itemId == R.id.menu_item_details) {
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
} else if (itemId == R.id.menu_item_append_playlist) {
PlaylistDialog.createCorrespondingDialog(
context,
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
)
);
return true;
case R.id.menu_item_channel_details:
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails());
return true;
case R.id.menu_item_download:
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
info -> {
final DownloadDialog downloadDialog = new DownloadDialog(context,
info);
downloadDialog.show(fragmentManager, "downloadDialog");
});
return true;
return true;
} else if (itemId == R.id.menu_item_channel_details) {
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
} else if (itemId == R.id.menu_item_share) {
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails());
return true;
} else if (itemId == R.id.menu_item_download) {
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
info -> {
final DownloadDialog downloadDialog = new DownloadDialog(context,
info);
downloadDialog.show(fragmentManager, "downloadDialog");
});
return true;
}
return false;
});

View File

@@ -343,8 +343,7 @@ public class RouterActivity extends AppCompatActivity {
return;
}
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
currentService.getServiceInfo().getMediaCapabilities();
final var capabilities = currentService.getServiceInfo().getMediaCapabilities();
// Check if the service supports the choice
if ((isVideoPlayerSelected && capabilities.contains(VIDEO))
@@ -528,8 +527,7 @@ public class RouterActivity extends AppCompatActivity {
final List<AdapterChoiceItem> returnedItems = new ArrayList<>();
returnedItems.add(showInfo); // Always present
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
service.getServiceInfo().getMediaCapabilities();
final var capabilities = service.getServiceInfo().getMediaCapabilities();
if (linkType == LinkType.STREAM || linkType == LinkType.PLAYLIST) {
if (capabilities.contains(VIDEO)) {

View File

@@ -92,7 +92,7 @@ class AboutActivity : AppCompatActivity() {
return when (position) {
posAbout -> AboutFragment()
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
else -> error("Unknown position for ViewPager2")
}
}
@@ -105,7 +105,7 @@ class AboutActivity : AppCompatActivity() {
return when (position) {
posAbout -> R.string.tab_about
posLicense -> R.string.tab_licenses
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
else -> error("Unknown position for ViewPager2")
}
}
}
@@ -116,86 +116,152 @@ class AboutActivity : AppCompatActivity() {
*/
private val SOFTWARE_COMPONENTS = arrayListOf(
SoftwareComponent(
"ACRA", "2013", "Kevin Gaudin",
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
"ACRA",
"2013",
"Kevin Gaudin",
"https://github.com/ACRA/acra",
StandardLicenses.APACHE2
),
SoftwareComponent(
"AndroidX", "2005 - 2011", "The Android Open Source Project",
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
"AndroidX",
"2005 - 2011",
"The Android Open Source Project",
"https://developer.android.com/jetpack",
StandardLicenses.APACHE2
),
SoftwareComponent(
"ExoPlayer", "2014 - 2020", "Google, Inc.",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
"ExoPlayer",
"2014 - 2020",
"Google, Inc.",
"https://github.com/google/ExoPlayer",
StandardLicenses.APACHE2
),
SoftwareComponent(
"GigaGet", "2014 - 2015", "Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3
"GigaGet",
"2014 - 2015",
"Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet",
StandardLicenses.GPL3
),
SoftwareComponent(
"Groupie", "2016", "Lisa Wray",
"https://github.com/lisawray/groupie", StandardLicenses.MIT
"Groupie",
"2016",
"Lisa Wray",
"https://github.com/lisawray/groupie",
StandardLicenses.MIT
),
SoftwareComponent(
"Android-State", "2018", "Evernote",
"https://github.com/Evernote/android-state", StandardLicenses.EPL1
"Android-State",
"2018",
"Evernote",
"https://github.com/Evernote/android-state",
StandardLicenses.EPL1
),
SoftwareComponent(
"Bridge", "2021", "Livefront",
"https://github.com/livefront/bridge", StandardLicenses.APACHE2
"Bridge",
"2021",
"Livefront",
"https://github.com/livefront/bridge",
StandardLicenses.APACHE2
),
SoftwareComponent(
"Jsoup", "2009 - 2020", "Jonathan Hedley",
"https://github.com/jhy/jsoup", StandardLicenses.MIT
"Jsoup",
"2009 - 2020",
"Jonathan Hedley",
"https://github.com/jhy/jsoup",
StandardLicenses.MIT
),
SoftwareComponent(
"Markwon", "2019", "Dimitry Ivanov",
"https://github.com/noties/Markwon", StandardLicenses.APACHE2
"Markwon",
"2019",
"Dimitry Ivanov",
"https://github.com/noties/Markwon",
StandardLicenses.APACHE2
),
SoftwareComponent(
"Material Components for Android", "2016 - 2020", "Google, Inc.",
"Material Components for Android",
"2016 - 2020",
"Google, Inc.",
"https://github.com/material-components/material-components-android",
StandardLicenses.APACHE2
),
SoftwareComponent(
"NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3
"NewPipe Extractor",
"2017 - 2020",
"Christian Schabesberger",
"https://github.com/TeamNewPipe/NewPipeExtractor",
StandardLicenses.GPL3
),
SoftwareComponent(
"NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2
"NoNonsense-FilePicker",
"2016",
"Jonas Kalderstam",
"https://github.com/spacecowboy/NoNonsense-FilePicker",
StandardLicenses.MPL2
),
SoftwareComponent(
"OkHttp", "2019", "Square, Inc.",
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
"OkHttp",
"2019",
"Square, Inc.",
"https://square.github.io/okhttp/",
StandardLicenses.APACHE2
),
SoftwareComponent(
"Picasso", "2013", "Square, Inc.",
"https://square.github.io/picasso/", StandardLicenses.APACHE2
"Coil",
"2023",
"Coil Contributors",
"https://coil-kt.github.io/coil/",
StandardLicenses.APACHE2
),
SoftwareComponent(
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
"PrettyTime",
"2012 - 2020",
"Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime",
StandardLicenses.APACHE2
),
SoftwareComponent(
"ProcessPhoenix", "2015", "Jake Wharton",
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
"ProcessPhoenix",
"2015",
"Jake Wharton",
"https://github.com/JakeWharton/ProcessPhoenix",
StandardLicenses.APACHE2
),
SoftwareComponent(
"RxAndroid", "2015", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
"RxAndroid",
"2015",
"The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid",
StandardLicenses.APACHE2
),
SoftwareComponent(
"RxBinding", "2015", "Jake Wharton",
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2
"RxBinding",
"2015",
"Jake Wharton",
"https://github.com/JakeWharton/RxBinding",
StandardLicenses.APACHE2
),
SoftwareComponent(
"RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
"RxJava",
"2016 - 2020",
"RxJava Contributors",
"https://github.com/ReactiveX/RxJava",
StandardLicenses.APACHE2
),
SoftwareComponent(
"SearchPreference", "2018", "ByteHamster",
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
"SearchPreference",
"2018",
"ByteHamster",
"https://github.com/ByteHamster/SearchPreference",
StandardLicenses.MIT
),
SoftwareComponent(
"FreeDroidWarn",
"2026",
"woheller69",
"https://github.com/woheller69/FreeDroidWarn",
StandardLicenses.APACHE2
)
)
}
}

View File

@@ -1,8 +1,8 @@
package org.schabi.newpipe.about
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.io.Serializable
import kotlinx.parcelize.Parcelize
/**
* Class for storing information about a software license.

View File

@@ -97,7 +97,8 @@ class LicenseFragment : Fragment() {
.observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense ->
val webViewData = Base64.encodeToString(
formattedLicense.toByteArray(), Base64.NO_PADDING
formattedLicense.toByteArray(),
Base64.NO_PADDING
)
val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")

View File

@@ -1,9 +1,9 @@
package org.schabi.newpipe.about
import android.content.Context
import java.io.IOException
import org.schabi.newpipe.R
import org.schabi.newpipe.util.ThemeHelper
import java.io.IOException
/**
* @param context the context to use
@@ -28,13 +28,16 @@ fun getFormattedLicense(context: Context, license: License): String {
fun getLicenseStylesheet(context: Context): String {
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
val licenseBackgroundColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
context,
if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
)
val licenseTextColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
context,
if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
)
val youtubePrimaryColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
context,
if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
)
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"

View File

@@ -1,8 +1,8 @@
package org.schabi.newpipe.about
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.io.Serializable
import kotlinx.parcelize.Parcelize
@Parcelize
class SoftwareComponent

View File

@@ -1,11 +1,11 @@
package org.schabi.newpipe.database
import androidx.room.TypeConverter
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
class Converters {
/**

View File

@@ -14,6 +14,6 @@ interface LocalItem {
PLAYLIST_REMOTE_ITEM,
PLAYLIST_STREAM_ITEM,
STATISTIC_STREAM_ITEM,
STATISTIC_STREAM_ITEM
}
}

View File

@@ -8,7 +8,6 @@ package org.schabi.newpipe.database
import android.util.Log
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import org.schabi.newpipe.MainActivity
object Migrations {

View File

@@ -8,6 +8,7 @@ import androidx.room.Transaction
import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import java.time.OffsetDateTime
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
@@ -15,7 +16,6 @@ import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@Dao
abstract class FeedDAO {

View File

@@ -19,13 +19,17 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
entity = StreamEntity::class,
parentColumns = [StreamEntity.STREAM_ID],
childColumns = [STREAM_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
),
ForeignKey(
entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
)
]
)

View File

@@ -18,14 +18,18 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
entity = FeedGroupEntity::class,
parentColumns = [FeedGroupEntity.ID],
childColumns = [GROUP_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
),
ForeignKey(
entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
)
]
)

View File

@@ -4,10 +4,10 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.time.OffsetDateTime
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@Entity(
tableName = FEED_LAST_UPDATED_TABLE,
@@ -16,7 +16,9 @@ import java.time.OffsetDateTime
entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
)
]
)

View File

@@ -29,7 +29,7 @@ data class SearchHistoryEntry @JvmOverloads constructor(
@ColumnInfo(name = ID)
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val id: Long = 0
) {
@Ignore

View File

@@ -11,12 +11,12 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.ForeignKey.Companion.CASCADE
import androidx.room.Index
import java.time.OffsetDateTime
import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.JOIN_STREAM_ID
import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_ACCESS_DATE
import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
import java.time.OffsetDateTime
/**
* @param streamUid the stream id this history item will refer to

View File

@@ -2,10 +2,10 @@ package org.schabi.newpipe.database.history.model
import androidx.room.ColumnInfo
import androidx.room.Embedded
import java.time.OffsetDateTime
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime
data class StreamHistoryEntry(
@Embedded
@@ -30,16 +30,15 @@ data class StreamHistoryEntry(
accessDate.isEqual(other.accessDate)
}
fun toStreamInfoItem(): StreamInfoItem =
StreamInfoItem(
streamEntity.serviceId,
streamEntity.url,
streamEntity.title,
streamEntity.streamType,
).apply {
duration = streamEntity.duration
uploaderName = streamEntity.uploader
uploaderUrl = streamEntity.uploaderUrl
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
}
fun toStreamInfoItem(): StreamInfoItem = StreamInfoItem(
streamEntity.serviceId,
streamEntity.url,
streamEntity.title,
streamEntity.streamType
).apply {
duration = streamEntity.duration
uploaderName = streamEntity.uploader
uploaderUrl = streamEntity.uploaderUrl
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
}
}

View File

@@ -37,7 +37,7 @@ data class PlaylistEntity @JvmOverloads constructor(
name = item.orderingName,
isThumbnailPermanent = item.isThumbnailPermanent!!,
thumbnailStreamId = item.thumbnailStreamId!!,
displayIndex = item.displayIndex!!,
displayIndex = item.displayIndex!!
)
companion object {

View File

@@ -62,11 +62,7 @@ data class PlaylistRemoteEntity(
orderingName = playlistInfo.name,
url = playlistInfo.url,
thumbnailUrl = ImageStrategy.imageListToDbUrl(
if (playlistInfo.thumbnails.isEmpty()) {
playlistInfo.uploaderAvatars
} else {
playlistInfo.thumbnails
}
playlistInfo.thumbnails.ifEmpty { playlistInfo.uploaderAvatars }
),
uploader = playlistInfo.uploaderName,
streamCount = playlistInfo.streamCount

View File

@@ -9,13 +9,13 @@ package org.schabi.newpipe.database.stream
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Ignore
import java.time.OffsetDateTime
import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime
data class StreamStatisticsEntry(
@Embedded

View File

@@ -8,12 +8,12 @@ import androidx.room.Query
import androidx.room.Transaction
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import java.time.OffsetDateTime
import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.util.StreamTypeUtil
import java.time.OffsetDateTime
@Dao
abstract class StreamDAO : BasicDAO<StreamEntity> {
@@ -87,11 +87,10 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
private fun compareAndUpdateStream(newerStream: StreamEntity) {
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
?: throw IllegalStateException("Stream cannot be null just after insertion.")
?: error("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
// Use the existent upload date if the newer stream does not have a better precision
// (i.e. is an approximation). This is done to prevent unnecessary changes.
val hasBetterPrecision =

View File

@@ -5,6 +5,8 @@ import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import java.io.Serializable
import java.time.OffsetDateTime
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
@@ -14,8 +16,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.player.playqueue.PlayQueueItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.io.Serializable
import java.time.OffsetDateTime
@Entity(
tableName = STREAM_TABLE,
@@ -86,8 +86,12 @@ data class StreamEntity(
@Ignore
constructor(item: PlayQueueItem) : this(
serviceId = item.serviceId, url = item.url, title = item.title,
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
serviceId = item.serviceId,
url = item.url,
title = item.title,
streamType = item.streamType,
duration = item.duration,
uploader = item.uploader,
uploaderUrl = item.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails)
)

View File

@@ -100,7 +100,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
entity.uid = uidFromInsert
} else {
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
?: error("Subscription cannot be null just after insertion.")
entity.uid = subscriptionIdFromDb
update(entity)

View File

@@ -16,6 +16,7 @@ import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
@@ -31,7 +32,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.appcompat.widget.Toolbar;
import androidx.collection.SparseArrayCompat;
import androidx.documentfile.provider.DocumentFile;
@@ -113,7 +113,7 @@ public class DownloadDialog extends DialogFragment
private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null;
private ActionMenuItemView okButton = null;
private MenuItem okButton = null;
private Context context = null;
private boolean askForSavePath;
@@ -344,7 +344,7 @@ public class DownloadDialog extends DialogFragment
toolbar.setNavigationOnClickListener(v -> dismiss());
toolbar.setNavigationContentDescription(R.string.cancel);
okButton = toolbar.findViewById(R.id.okay);
okButton = toolbar.getMenu().findItem(R.id.okay);
okButton.setEnabled(false); // disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> {
@@ -558,17 +558,13 @@ public class DownloadDialog extends DialogFragment
}
boolean flag = true;
switch (checkedId) {
case R.id.audio_button:
setupAudioSpinner();
break;
case R.id.video_button:
setupVideoSpinner();
break;
case R.id.subtitle_button:
setupSubtitleSpinner();
flag = false;
break;
if (checkedId == R.id.audio_button) {
setupAudioSpinner();
} else if (checkedId == R.id.video_button) {
setupVideoSpinner();
} else if (checkedId == R.id.subtitle_button) {
setupSubtitleSpinner();
flag = false;
}
dialogBinding.threads.setEnabled(flag);
@@ -585,29 +581,26 @@ public class DownloadDialog extends DialogFragment
+ "position = [" + position + "], id = [" + id + "]");
}
switch (parent.getId()) {
case R.id.quality_spinner:
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.video_button:
selectedVideoIndex = position;
onVideoStreamSelected();
break;
case R.id.subtitle_button:
selectedSubtitleIndex = position;
break;
}
onItemSelectedSetFileName();
break;
case R.id.audio_track_spinner:
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
break;
case R.id.audio_stream_spinner:
selectedAudioIndex = position;
final int parentId = parent.getId();
if (parentId == R.id.quality_spinner) {
final int checkedRadioButtonId = dialogBinding.videoAudioGroup
.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.video_button) {
selectedVideoIndex = position;
onVideoStreamSelected();
} else if (checkedRadioButtonId == R.id.subtitle_button) {
selectedSubtitleIndex = position;
}
onItemSelectedSetFileName();
} else if (parentId == R.id.audio_track_spinner) {
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
} else if (parentId == R.id.audio_stream_spinner) {
selectedAudioIndex = position;
}
}
@@ -622,23 +615,20 @@ public class DownloadDialog extends DialogFragment
|| prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
// only update the file name field if it was not edited by the user
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
case R.id.video_button:
if (!prevFileName.equals(fileName)) {
// since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position
dialogBinding.fileName.setText(fileName);
}
break;
case R.id.subtitle_button:
final String setSubtitleLanguageCode = subtitleStreamsAdapter
.getItem(selectedSubtitleIndex).getLanguageTag();
// this will reset the cursor position, which is bad UX, but it can't be avoided
dialogBinding.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
break;
final int radioButtonId = dialogBinding.videoAudioGroup
.getCheckedRadioButtonId();
if (radioButtonId == R.id.audio_button || radioButtonId == R.id.video_button) {
if (!prevFileName.equals(fileName)) {
// since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position
dialogBinding.fileName.setText(fileName);
}
} else if (radioButtonId == R.id.subtitle_button) {
final String setSubtitleLanguageCode = subtitleStreamsAdapter
.getItem(selectedSubtitleIndex).getLanguageTag();
// this will reset the cursor position, which is bad UX, but it can't be avoided
dialogBinding.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
}
}
}
@@ -770,47 +760,44 @@ public class DownloadDialog extends DialogFragment
filenameTmp = getNameEditText().concat(".");
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
break;
case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
break;
case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) {
mimeTmp = format.mimeType;
}
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.audio_button) {
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
} else if (checkedRadioButtonId == R.id.video_button) {
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
} else if (checkedRadioButtonId == R.id.subtitle_button) {
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) {
mimeTmp = format.mimeType;
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) {
filenameTmp += format.getSuffix();
}
break;
default:
throw new RuntimeException("No stream selected");
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) {
filenameTmp += format.getSuffix();
}
} else {
throw new RuntimeException("No stream selected");
}
if (!askForSavePath && (mainStorage == null
@@ -1057,59 +1044,56 @@ public class DownloadDialog extends DialogFragment
long nearLength = 0;
// more download logic: select muxer, subtitle converter, etc.
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.audio_button) {
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
}
} else if (checkedRadioButtonId == R.id.video_button) {
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
}
break;
case R.id.video_button:
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
}
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
}
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
}
break;
case R.id.subtitle_button:
threads = 1; // use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
}
} else if (checkedRadioButtonId == R.id.subtitle_button) {
threads = 1; // use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[] {
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
}
break;
default:
return;
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
}
} else {
return;
}
if (secondaryStream == null) {

View File

@@ -1,324 +0,0 @@
package org.schabi.newpipe.error;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.IntentCompat;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.stream.Collectors;
/*
* Created by Christian Schabesberger on 24.10.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ErrorActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <
* NewPipe 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 for more details.
* <
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This activity is used to show error details and allow reporting them in various ways. Use {@link
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
*/
public class ErrorActivity extends AppCompatActivity {
// LOG TAGS
public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL =
"https://github.com/TeamNewPipe/NewPipe/issues";
private ErrorInfo errorInfo;
private String currentTimeStamp;
private ActivityErrorBinding activityErrorBinding;
////////////////////////////////////////////////////////////////////////
// Activity lifecycle
////////////////////////////////////////////////////////////////////////
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this);
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
setContentView(activityErrorBinding.getRoot());
final Intent intent = getIntent();
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.error_report_title);
actionBar.setDisplayShowTitleEnabled(true);
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
// important add guru meditation
addGuruMeditation();
// print current time, as zoned ISO8601 timestamp
final ZonedDateTime now = ZonedDateTime.now();
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL"));
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
ShareUtils.copyToClipboard(this, buildMarkdown()));
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB"));
// normal bugreport
buildInfo(errorInfo);
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this));
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
// print stack trace once again for debugging:
for (final String e : errorInfo.getStackTraces()) {
Log.e(TAG, e);
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.error_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.menu_item_share_error:
ShareUtils.shareText(getApplicationContext(),
getString(R.string.error_report_title), buildJson());
return true;
default:
return false;
}
}
private void openPrivacyPolicyDialog(final Context context, final String action) {
new AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
ShareUtils.openUrlInApp(context,
context.getString(R.string.privacy_policy_url)))
.setPositiveButton(R.string.accept, (dialog, which) -> {
if (action.equals("EMAIL")) { // send on email
final Intent i = new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse("mailto:")) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
+ getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson());
ShareUtils.openIntentInApp(context, i);
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
}
})
.setNegativeButton(R.string.decline, null)
.show();
}
private String formErrorText(final String[] el) {
final String separator = "-------------------------------------";
return Arrays.stream(el)
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
}
private void buildInfo(final ErrorInfo info) {
String text = "";
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
.replace("\\n", "\n"));
text += getUserActionString(info.getUserAction()) + "\n"
+ info.getRequest() + "\n"
+ getContentLanguageString() + "\n"
+ getContentCountryString() + "\n"
+ getAppLanguage() + "\n"
+ info.getServiceName() + "\n"
+ currentTimeStamp + "\n"
+ getPackageName() + "\n"
+ BuildConfig.VERSION_NAME + "\n"
+ getOsString();
activityErrorBinding.errorInfosView.setText(text);
}
private String buildJson() {
try {
return JsonWriter.string()
.object()
.value("user_action", getUserActionString(errorInfo.getUserAction()))
.value("request", errorInfo.getRequest())
.value("content_language", getContentLanguageString())
.value("content_country", getContentCountryString())
.value("app_language", getAppLanguage())
.value("service", errorInfo.getServiceName())
.value("package", getPackageName())
.value("version", BuildConfig.VERSION_NAME)
.value("os", getOsString())
.value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString())
.end()
.done();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build json");
e.printStackTrace();
}
return "";
}
private String buildMarkdown() {
try {
final StringBuilder htmlErrorReport = new StringBuilder();
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
if (!userComment.isEmpty()) {
htmlErrorReport.append(userComment).append("\n");
}
// basic error info
htmlErrorReport
.append("## Exception")
.append("\n* __User Action:__ ")
.append(getUserActionString(errorInfo.getUserAction()))
.append("\n* __Request:__ ").append(errorInfo.getRequest())
.append("\n* __Content Country:__ ").append(getContentCountryString())
.append("\n* __Content Language:__ ").append(getContentLanguageString())
.append("\n* __App Language:__ ").append(getAppLanguage())
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
.append("\n* __Package:__ ").append(getPackageName())
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
.append("\n* __OS:__ ").append(getOsString()).append("\n");
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorInfo.getStackTraces().length)
.append(")</b></summary><p>\n");
}
// add the logs
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
htmlErrorReport.append("<details><summary><b>Crash log ");
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1);
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
.append("</details>\n");
}
// make sure to close everything
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append("</p></details>\n");
}
htmlErrorReport.append("<hr>\n");
return htmlErrorReport.toString();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build markdown");
e.printStackTrace();
return "";
}
}
private String getUserActionString(final UserAction userAction) {
if (userAction == null) {
return "Your description is in another castle.";
} else {
return userAction.getMessage();
}
}
private String getContentCountryString() {
return Localization.getPreferredContentCountry(this).getCountryCode();
}
private String getContentLanguageString() {
return Localization.getPreferredLocalization(this).getLocalizationCode();
}
private String getAppLanguage() {
return Localization.getAppLocale().toString();
}
private String getOsString() {
final String osBase = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
? Build.VERSION.BASE_OS : "Android";
return System.getProperty("os.name")
+ " " + (osBase.isEmpty() ? "Android" : osBase)
+ " " + Build.VERSION.RELEASE
+ " - " + Build.VERSION.SDK_INT;
}
private void addGuruMeditation() {
//just an easter egg
String text = activityErrorBinding.errorSorryView.getText().toString();
text += "\n" + getString(R.string.guru_meditation);
activityErrorBinding.errorSorryView.setText(text);
}
}

View File

@@ -0,0 +1,281 @@
/*
* SPDX-FileCopyrightText: 2015-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.error
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.IntentCompat
import androidx.core.net.toUri
import com.grack.nanojson.JsonWriter
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityErrorBinding
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
/**
* This activity is used to show error details and allow reporting them in various ways.
* Use [ErrorUtil.openActivity] to correctly open this activity.
*/
class ErrorActivity : AppCompatActivity() {
private lateinit var errorInfo: ErrorInfo
private lateinit var currentTimeStamp: String
private lateinit var binding: ActivityErrorBinding
private val contentCountryString: String
get() = Localization.getPreferredContentCountry(this).countryCode
private val contentLanguageString: String
get() = Localization.getPreferredLocalization(this).localizationCode
private val appLanguage: String
get() = Localization.getAppLocale().toString()
private val osString: String
get() {
val name = System.getProperty("os.name")!!
val osBase = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Build.VERSION.BASE_OS.ifEmpty { "Android" }
} else {
"Android"
}
return "$name $osBase ${Build.VERSION.RELEASE} - ${Build.VERSION.SDK_INT}"
}
private val errorEmailSubject: String
get() = "$ERROR_EMAIL_SUBJECT ${getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}"
// /////////////////////////////////////////////////////////////////////
// Activity lifecycle
// /////////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeHelper.setDayNightMode(this)
ThemeHelper.setTheme(this)
binding = ActivityErrorBinding.inflate(layoutInflater)
setContentView(binding.getRoot())
setSupportActionBar(binding.toolbarLayout.toolbar)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setTitle(R.string.error_report_title)
setDisplayShowTitleEnabled(true)
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo::class.java)!!
// important add guru meditation
addGuruMeditation()
// print current time, as zoned ISO8601 timestamp
currentTimeStamp = ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
binding.errorReportEmailButton.setOnClickListener { _ ->
openPrivacyPolicyDialog(this, "EMAIL")
}
binding.errorReportCopyButton.setOnClickListener { _ ->
ShareUtils.copyToClipboard(this, buildMarkdown())
}
binding.errorReportGitHubButton.setOnClickListener { _ ->
openPrivacyPolicyDialog(this, "GITHUB")
}
// normal bugreport
buildInfo(errorInfo)
binding.errorMessageView.text = errorInfo.getMessage(this)
binding.errorView.text = formErrorText(errorInfo.stackTraces)
// print stack trace once again for debugging:
errorInfo.stackTraces.forEach { Log.e(TAG, it) }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.error_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
R.id.menu_item_share_error -> {
ShareUtils.shareText(
applicationContext,
getString(R.string.error_report_title),
buildJson()
)
true
}
else -> false
}
}
private fun openPrivacyPolicyDialog(context: Context, action: String) {
AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy) { _, _ ->
ShareUtils.openUrlInApp(context, context.getString(R.string.privacy_policy_url))
}
.setPositiveButton(R.string.accept) { _, _ ->
if (action == "EMAIL") { // send on email
val intent = Intent(Intent.ACTION_SENDTO)
.setData("mailto:".toUri()) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, arrayOf(ERROR_EMAIL_ADDRESS))
.putExtra(Intent.EXTRA_SUBJECT, errorEmailSubject)
.putExtra(Intent.EXTRA_TEXT, buildJson())
ShareUtils.openIntentInApp(context, intent)
} else if (action == "GITHUB") { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
}
}
.setNegativeButton(R.string.decline, null)
.show()
}
private fun formErrorText(stacktrace: Array<String>): String {
val separator = "-------------------------------------"
return stacktrace.joinToString(separator + "\n", separator + "\n", separator)
}
private fun buildInfo(info: ErrorInfo) {
binding.errorInfoLabelsView.text = getString(R.string.info_labels)
val text = info.userAction.message + "\n" +
info.request + "\n" +
contentLanguageString + "\n" +
contentCountryString + "\n" +
appLanguage + "\n" +
info.getServiceName() + "\n" +
currentTimeStamp + "\n" +
packageName + "\n" +
BuildConfig.VERSION_NAME + "\n" +
osString
binding.errorInfosView.text = text
}
private fun buildJson(): String {
try {
return JsonWriter.string()
.`object`()
.value("user_action", errorInfo.userAction.message)
.value("request", errorInfo.request)
.value("content_language", contentLanguageString)
.value("content_country", contentCountryString)
.value("app_language", appLanguage)
.value("service", errorInfo.getServiceName())
.value("package", packageName)
.value("version", BuildConfig.VERSION_NAME)
.value("os", osString)
.value("time", currentTimeStamp)
.array("exceptions", errorInfo.stackTraces.toList())
.value("user_comment", binding.errorCommentBox.getText().toString())
.end()
.done()
} catch (exception: Exception) {
Log.e(TAG, "Error while erroring: Could not build json", exception)
}
return ""
}
private fun buildMarkdown(): String {
try {
return buildString(1024) {
val userComment = binding.errorCommentBox.text.toString()
if (userComment.isNotEmpty()) {
appendLine(userComment)
}
// basic error info
appendLine("## Exception")
appendLine("* __User Action:__ ${errorInfo.userAction.message}")
appendLine("* __Request:__ ${errorInfo.request}")
appendLine("* __Content Country:__ $contentCountryString")
appendLine("* __Content Language:__ $contentLanguageString")
appendLine("* __App Language:__ $appLanguage")
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
appendLine("* __Timestamp:__ $currentTimeStamp")
appendLine("* __Package:__ $packageName")
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
appendLine("* __Version:__ ${BuildConfig.VERSION_NAME}")
appendLine("* __OS:__ $osString")
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.stackTraces.size > 1) {
append("<details><summary><b>Exceptions (")
append(errorInfo.stackTraces.size)
append(")</b></summary><p>\n")
}
// add the logs
errorInfo.stackTraces.forEachIndexed { index, stacktrace ->
append("<details><summary><b>Crash log ")
if (errorInfo.stackTraces.size > 1) {
append(index + 1)
}
append("</b>")
append("</summary><p>\n")
append("\n```\n${stacktrace}\n```\n")
append("</details>\n")
}
// make sure to close everything
if (errorInfo.stackTraces.size > 1) {
append("</p></details>\n")
}
append("<hr>\n")
}
} catch (exception: Exception) {
Log.e(TAG, "Error while erroring: Could not build markdown", exception)
return ""
}
}
private fun addGuruMeditation() {
// just an easter egg
var text = binding.errorSorryView.text.toString()
text += "\n" + getString(R.string.guru_meditation)
binding.errorSorryView.text = text
}
companion object {
// LOG TAGS
private val TAG = ErrorActivity::class.java.toString()
// BUNDLE TAGS
const val ERROR_INFO = "error_info"
private const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
private const val ERROR_EMAIL_SUBJECT = "Exception in "
private const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
}
}

View File

@@ -7,6 +7,7 @@ import androidx.core.content.ContextCompat
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.upstream.HttpDataSource
import com.google.android.exoplayer2.upstream.Loader
import java.net.UnknownHostException
import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Info
@@ -28,7 +29,6 @@ import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentExcepti
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.player.mediasource.FailedMediaSource
import org.schabi.newpipe.player.resolver.PlaybackResolver
import java.net.UnknownHostException
/**
* An error has occurred in the app. This class contains plain old parcelable data that can be used
@@ -59,7 +59,7 @@ class ErrorInfo private constructor(
* If present, this resource can alternatively be opened in browser (useful if NewPipe is
* badly broken).
*/
val openInBrowserUrl: String?,
val openInBrowserUrl: String?
) : Parcelable {
@JvmOverloads
@@ -68,7 +68,7 @@ class ErrorInfo private constructor(
userAction: UserAction,
request: String,
serviceId: Int? = null,
openInBrowserUrl: String? = null,
openInBrowserUrl: String? = null
) : this(
throwableToStringList(throwable),
userAction,
@@ -78,7 +78,7 @@ class ErrorInfo private constructor(
isReportable(throwable),
isRetryable(throwable),
(throwable as? ReCaptchaException)?.url,
openInBrowserUrl,
openInBrowserUrl
)
@JvmOverloads
@@ -87,7 +87,7 @@ class ErrorInfo private constructor(
userAction: UserAction,
request: String,
serviceId: Int? = null,
openInBrowserUrl: String? = null,
openInBrowserUrl: String? = null
) : this(
throwableListToStringList(throwables),
userAction,
@@ -97,7 +97,7 @@ class ErrorInfo private constructor(
throwables.any(::isReportable),
throwables.isEmpty() || throwables.any(::isRetryable),
throwables.firstNotNullOfOrNull { it as? ReCaptchaException }?.url,
openInBrowserUrl,
openInBrowserUrl
)
// constructor to manually build ErrorInfo when no throwable is available
@@ -118,7 +118,7 @@ class ErrorInfo private constructor(
throwable: Throwable,
userAction: UserAction,
request: String,
info: Info?,
info: Info?
) :
this(throwable, userAction, request, info?.serviceId, info?.url)
@@ -127,7 +127,7 @@ class ErrorInfo private constructor(
throwables: List<Throwable>,
userAction: UserAction,
request: String,
info: Info?,
info: Info?
) :
this(throwables, userAction, request, info?.serviceId, info?.url)
@@ -144,7 +144,7 @@ class ErrorInfo private constructor(
class ErrorMessage(
@StringRes
private val stringRes: Int,
private vararg val formatArgs: String,
private vararg val formatArgs: String
) : Parcelable {
fun getString(context: Context): String {
return if (formatArgs.isEmpty()) {
@@ -160,21 +160,19 @@ class ErrorInfo private constructor(
const val SERVICE_NONE = "<unknown_service>"
private fun getServiceName(serviceId: Int?) =
// not using getNameOfServiceById since we want to accept a nullable serviceId and we
private fun getServiceName(serviceId: Int?) = // not using getNameOfServiceById since we want to accept a nullable serviceId and we
// want to default to SERVICE_NONE
ServiceList.all()?.firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name
ServiceList.all().firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name
?: SERVICE_NONE
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
fun throwableListToStringList(throwableList: List<Throwable>) =
throwableList.map { it.stackTraceToString() }.toTypedArray()
fun throwableListToStringList(throwableList: List<Throwable>) = throwableList.map { it.stackTraceToString() }.toTypedArray()
fun getMessage(
throwable: Throwable?,
action: UserAction?,
serviceId: Int?,
serviceId: Int?
): ErrorMessage {
return when {
// player exceptions
@@ -193,18 +191,24 @@ class ErrorInfo private constructor(
ErrorMessage(R.string.player_http_invalid_status, cause.responseCode.toString())
}
}
cause is Loader.UnexpectedLoaderException && cause.cause is ExtractionException ->
getMessage(throwable, action, serviceId)
throwable.type == ExoPlaybackException.TYPE_SOURCE ->
ErrorMessage(R.string.player_stream_failure)
throwable.type == ExoPlaybackException.TYPE_UNEXPECTED ->
ErrorMessage(R.string.player_recoverable_failure)
else ->
ErrorMessage(R.string.player_unrecoverable_failure)
}
}
throwable is FailedMediaSource.FailedMediaSourceException ->
getMessage(throwable.cause, action, serviceId)
throwable is PlaybackResolver.ResolverException ->
ErrorMessage(R.string.player_stream_failure)
@@ -220,34 +224,46 @@ class ErrorInfo private constructor(
)
}
?: ErrorMessage(R.string.account_terminated)
throwable is AgeRestrictedContentException ->
ErrorMessage(R.string.restricted_video_no_stream)
throwable is GeographicRestrictionException ->
ErrorMessage(R.string.georestricted_content)
throwable is PaidContentException ->
ErrorMessage(R.string.paid_content)
throwable is PrivateContentException ->
ErrorMessage(R.string.private_content)
throwable is SoundCloudGoPlusContentException ->
ErrorMessage(R.string.soundcloud_go_plus_content)
throwable is UnsupportedContentInCountryException ->
ErrorMessage(R.string.unsupported_content_in_country)
throwable is YoutubeMusicPremiumContentException ->
ErrorMessage(R.string.youtube_music_premium_content)
throwable is SignInConfirmNotBotException ->
ErrorMessage(R.string.sign_in_confirm_not_bot_error, getServiceName(serviceId))
throwable is ContentNotAvailableException ->
ErrorMessage(R.string.content_not_available)
// other extractor exceptions
throwable is ContentNotSupportedException ->
ErrorMessage(R.string.content_not_supported)
// ReCaptchas will be handled in a special way anyway
throwable is ReCaptchaException ->
ErrorMessage(R.string.recaptcha_request_toast)
// test this at the end as many exceptions could be a subclass of IOException
throwable != null && throwable.isNetworkRelated ->
ErrorMessage(R.string.network_error)
// an extraction exception unrelated to the network
// is likely an issue with parsing the website
throwable is ExtractionException ->
@@ -256,16 +272,22 @@ class ErrorInfo private constructor(
// user actions (in case the exception is null or unrecognizable)
action == UserAction.UI_ERROR ->
ErrorMessage(R.string.app_ui_crash)
action == UserAction.REQUESTED_COMMENTS ->
ErrorMessage(R.string.error_unable_to_load_comments)
action == UserAction.SUBSCRIPTION_CHANGE ->
ErrorMessage(R.string.subscription_change_failed)
action == UserAction.SUBSCRIPTION_UPDATE ->
ErrorMessage(R.string.subscription_update_failed)
action == UserAction.LOAD_IMAGE ->
ErrorMessage(R.string.could_not_load_thumbnails)
action == UserAction.DOWNLOAD_OPEN_DIALOG ->
ErrorMessage(R.string.could_not_setup_download_menu)
else ->
ErrorMessage(R.string.error_snackbar_message)
}
@@ -276,15 +298,19 @@ class ErrorInfo private constructor(
// we don't have an exception, so this is a manually built error, which likely
// indicates that it's important and is thus reportable
null -> true
// the service explicitly said that content is not available (e.g. age restrictions,
// video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> false
// if the service explicitly said that content is not available (e.g. age
// restrictions, video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)
// we know the content is not supported, no need to let the user report it
is ContentNotSupportedException -> false
// happens often when there is no internet connection; we don't use
// `throwable.isNetworkRelated` since any `IOException` would make that function
// return true, but not all `IOException`s are network related
is UnknownHostException -> false
// by default, this is an unexpected exception, which the user could report
else -> true
}
@@ -292,14 +318,39 @@ class ErrorInfo private constructor(
fun isRetryable(throwable: Throwable?): Boolean {
return when (throwable) {
// we know the content is not available, retrying won't help
is ContentNotAvailableException -> false
// if we know the content is surely not available, retrying won't help
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)
// we know the content is not supported, retrying won't help
is ContentNotSupportedException -> false
// by default (including if throwable is null), enable retrying (though the retry
// button will be shown only if a way to perform the retry is implemented)
else -> true
}
}
/**
* Unfortunately sometimes [ContentNotAvailableException] may not indicate that the content
* is blocked/deleted/paid, but may just indicate that we could not extract it. This is an
* inconsistency in the exceptions thrown by the extractor, but until it is fixed, this
* function will distinguish between the two types.
* @return `true` if the content is not available because of a limitation imposed by the
* service or the owner, `false` if the extractor could not extract info about it
*/
fun isContentSurelyNotAvailable(e: ContentNotAvailableException): Boolean {
return when (e) {
is AccountTerminatedException,
is AgeRestrictedContentException,
is GeographicRestrictionException,
is PaidContentException,
is PrivateContentException,
is SoundCloudGoPlusContentException,
is UnsupportedContentInCountryException,
is YoutubeMusicPremiumContentException -> true
else -> false
}
}
}
}

View File

@@ -11,16 +11,16 @@ import androidx.fragment.app.Fragment
import com.jakewharton.rxbinding4.view.clicks
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.util.concurrent.TimeUnit
class ErrorPanelHelper(
private val fragment: Fragment,
rootView: View,
onRetry: Runnable?,
onRetry: Runnable?
) {
private val context: Context = rootView.context!!

View File

@@ -46,7 +46,7 @@ class ErrorUtil {
@JvmStatic
fun openActivity(context: Context, errorInfo: ErrorInfo) {
if (PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(MainActivity.KEY_IS_IN_BACKGROUND, true)
.getBoolean(MainActivity.KEY_IS_IN_BACKGROUND, true)
) {
createNotification(context, errorInfo)
} else {
@@ -134,8 +134,11 @@ class ErrorUtil {
)
)
NotificationManagerCompat.from(context)
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
val notificationManager = NotificationManagerCompat.from(context)
if (notificationManager.areNotificationsEnabled()) {
notificationManager
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
}
ContextCompat.getMainExecutor(context).execute {
// since the notification is silent, also show a toast, otherwise the user is confused

View File

@@ -126,6 +126,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
}
@Override
@SuppressLint("MissingSuperCall") // saveCookiesAndFinish method handles back navigation
public void onBackPressed() {
saveCookiesAndFinish();
}

View File

@@ -40,5 +40,5 @@ enum class UserAction(val message: String) {
OPEN_INFO_ITEM_DIALOG("open info item dialog"),
GETTING_MAIN_SCREEN_TAB("getting main screen tab"),
PLAY_ON_POPUP("play on popup"),
SUBSCRIPTIONS("loading subscriptions");
SUBSCRIPTIONS("loading subscriptions")
}

View File

@@ -118,7 +118,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.ArrayList;
import java.util.Iterator;
@@ -129,6 +129,7 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
@@ -160,8 +161,6 @@ public final class VideoDetailFragment
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
// tabs
private boolean showComments;
private boolean showRelatedItems;
@@ -206,8 +205,6 @@ public final class VideoDetailFragment
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State
protected boolean autoPlayEnabled = true;
@State
protected int originalOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@Nullable
private StreamInfo currentInfo = null;
@@ -648,6 +645,12 @@ public final class VideoDetailFragment
protected void initListeners() {
super.initListeners();
// Workaround for #5600
// Forcefully catch click events uncaught by children because otherwise
// they will be caught by underlying view and "click through" will happen
binding.getRoot().setOnClickListener(v -> { });
binding.getRoot().setOnLongClickListener(v -> true);
setOnClickListeners();
setOnLongClickListeners();
@@ -1424,8 +1427,10 @@ public final class VideoDetailFragment
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
// Rebound to the service if it was closed via notification or mini player
playerHolder.setListener(VideoDetailFragment.this);
playerHolder.tryBindIfNeeded(context);
if (!playerHolder.isBound()) {
playerHolder.startService(
false, VideoDetailFragment.this);
}
break;
}
}
@@ -1493,7 +1498,10 @@ public final class VideoDetailFragment
}
}
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
CoilUtils.dispose(binding.detailThumbnailImageView);
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
CoilUtils.dispose(binding.overlayThumbnail);
CoilUtils.dispose(binding.detailUploaderThumbnailView);
binding.detailThumbnailImageView.setImageBitmap(null);
binding.detailSubChannelThumbnailView.setImageBitmap(null);
}
@@ -1584,8 +1592,8 @@ public final class VideoDetailFragment
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
checkUpdateProgressInfo(info);
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
info.getThumbnails());
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables);
@@ -1635,8 +1643,8 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getUploaderAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
}
@@ -1667,11 +1675,11 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getSubChannelAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
info.getUploaderAvatars());
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
}
@@ -1899,7 +1907,11 @@ public final class VideoDetailFragment
}
if (binding.relatedItemsLayout != null) {
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
} else {
binding.relatedItemsLayout.setVisibility(View.GONE);
}
}
scrollToTop();
@@ -1908,29 +1920,23 @@ public final class VideoDetailFragment
@Override
public void onScreenRotationButtonClicked() {
final Optional<MainPlayerUi> playerUi = player != null
? player.UIs().get(MainPlayerUi.class)
: Optional.empty();
if (playerUi.isEmpty()) {
// On Android TV screen rotation is not supported
// In tablet user experience will be better if screen will not be rotated
// from landscape to portrait every time.
// Just turn on fullscreen mode in landscape orientation
// or portrait & unlocked global orientation
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
if (DeviceUtils.isTv(activity) || DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
return;
}
// On tablets and TVs, just toggle fullscreen UI without orientation change.
if (DeviceUtils.isTablet(activity) || DeviceUtils.isTv(activity)) {
playerUi.get().toggleFullscreen();
return;
}
final int newOrientation = isLandscape
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
if (playerUi.get().isFullscreen()) {
// EXITING FULLSCREEN
playerUi.get().toggleFullscreen();
activity.setRequestedOrientation(originalOrientation);
} else {
// ENTERING FULLSCREEN
originalOrientation = activity.getRequestedOrientation();
playerUi.get().toggleFullscreen();
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
}
activity.setRequestedOrientation(newOrientation);
}
/*
@@ -2435,8 +2441,7 @@ public final class VideoDetailFragment
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
}
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {

View File

@@ -53,13 +53,14 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@@ -73,7 +74,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
implements StateSaver.WriteRead {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@@ -160,34 +160,29 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
break;
default:
return false;
final int itemId = item.getItemId();
if (itemId == R.id.menu_item_notify) {
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
} else if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(requireContext());
} else if (itemId == R.id.menu_item_rss) {
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
} else if (itemId == R.id.menu_item_openInBrowser) {
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
} else if (itemId == R.id.menu_item_share) {
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
} else {
return false;
}
return true;
}
@@ -583,7 +578,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override
public void showLoading() {
super.showLoading();
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
CoilUtils.dispose(binding.channelAvatarView);
CoilUtils.dispose(binding.channelBannerImage);
CoilUtils.dispose(binding.subChannelAvatarView);
animate(binding.channelSubscribeButton, false, 100);
}
@@ -594,17 +591,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelBannerImage);
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
} else {
// do not waste space for the banner, if the user disabled images or there is not one
binding.channelBannerImage.setImageDrawable(null);
}
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.subChannelAvatarView);
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
result.getParentChannelAvatars());
binding.channelTitleView.setText(result.getName());
binding.channelSubscriberView.setVisibility(View.VISIBLE);

View File

@@ -25,8 +25,8 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextLinkifier;
import org.schabi.newpipe.util.text.LongPressLinkMovementMethod;
@@ -84,7 +84,7 @@ public final class CommentRepliesFragment
final CommentsInfoItem item = commentsInfoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars());
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
? View.VISIBLE : View.GONE);

View File

@@ -53,7 +53,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList;
@@ -62,6 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
@@ -71,8 +72,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
implements PlaylistControlViewHolder {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
private CompositeDisposable disposables;
private Subscription bookmarkReactor;
private AtomicBoolean isBookmarkButtonReady;
@@ -232,35 +231,30 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_openInBrowser:
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
break;
case R.id.menu_item_append_playlist:
if (currentInfo != null) {
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
}
break;
default:
return super.onOptionsItemSelected(item);
final int itemId = item.getItemId();
if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(requireContext());
} else if (itemId == R.id.menu_item_openInBrowser) {
ShareUtils.openUrlInBrowser(requireContext(), url);
} else if (itemId == R.id.menu_item_share) {
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
} else if (itemId == R.id.menu_item_bookmark) {
onBookmarkClicked();
} else if (itemId == R.id.menu_item_append_playlist) {
if (currentInfo != null) {
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
}
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@@ -276,7 +270,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
animate(headerBinding.getRoot(), false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList);
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
CoilUtils.dispose(headerBinding.uploaderAvatarView);
animate(headerBinding.uploaderLayout, false, 200);
}
@@ -327,8 +321,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
R.drawable.ic_radio)
);
} else {
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
.into(headerBinding.uploaderAvatarView);
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
result.getUploaderAvatars());
}
streamCount = result.getStreamCount();

View File

@@ -1009,7 +1009,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
}
suggestionListAdapter.submitList(suggestions,
() -> searchBinding.suggestionsList.scrollToPosition(0));
() -> {
if (searchBinding != null) {
searchBinding.suggestionsList.scrollToPosition(0);
}
});
if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading();

View File

@@ -1,131 +0,0 @@
package org.schabi.newpipe.info_list;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.OnClickGesture;
/*
* Created by Christian Schabesberger on 26.09.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemBuilder.java is part of NewPipe.
* </p>
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* </p>
* <p>
* NewPipe 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 for more details.
* </p>
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* </p>
*/
public class InfoItemBuilder {
private final Context context;
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
public InfoItemBuilder(final Context context) {
this.context = context;
}
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
return buildView(parent, infoItem, historyRecordManager, false);
}
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager,
final boolean useMiniVariant) {
final InfoItemHolder holder =
holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem, historyRecordManager);
return holder.itemView;
}
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
@NonNull final InfoItem.InfoType infoType,
final boolean useMiniVariant) {
switch (infoType) {
case STREAM:
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
: new StreamInfoItemHolder(this, parent);
case CHANNEL:
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
: new ChannelInfoItemHolder(this, parent);
case PLAYLIST:
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
: new PlaylistInfoItemHolder(this, parent);
case COMMENT:
return new CommentInfoItemHolder(this, parent);
default:
throw new RuntimeException("InfoType not expected = " + infoType.name());
}
}
public Context getContext() {
return context;
}
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
return onStreamSelectedListener;
}
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
this.onStreamSelectedListener = listener;
}
public OnClickGesture<ChannelInfoItem> getOnChannelSelectedListener() {
return onChannelSelectedListener;
}
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
this.onChannelSelectedListener = listener;
}
public OnClickGesture<PlaylistInfoItem> getOnPlaylistSelectedListener() {
return onPlaylistSelectedListener;
}
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
this.onPlaylistSelectedListener = listener;
}
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
return onCommentsSelectedListener;
}
public void setOnCommentsSelectedListener(
final OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
this.onCommentsSelectedListener = onCommentsSelectedListener;
}
}

View File

@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.info_list
import android.content.Context
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.OnClickGesture
class InfoItemBuilder(val context: Context) {
var onStreamSelectedListener: OnClickGesture<StreamInfoItem>? = null
var onChannelSelectedListener: OnClickGesture<ChannelInfoItem>? = null
var onPlaylistSelectedListener: OnClickGesture<PlaylistInfoItem>? = null
var onCommentsSelectedListener: OnClickGesture<CommentsInfoItem>? = null
}

View File

@@ -13,14 +13,17 @@ enum class ItemViewMode {
* Default mode.
*/
AUTO,
/**
* Full width list item with thumb on the left and two line title & uploader in right.
*/
LIST,
/**
* Grid mode places two cards per row.
*/
GRID,
/**
* A full width card in phone - portrait.
*/

View File

@@ -2,8 +2,8 @@ package org.schabi.newpipe.info_list
import android.util.Log
import com.xwray.groupie.GroupieAdapter
import org.schabi.newpipe.extractor.stream.StreamInfo
import kotlin.math.max
import org.schabi.newpipe.extractor.stream.StreamInfo
/**
* Custom RecyclerView.Adapter/GroupieAdapter for [StreamSegmentItem] for handling selection state.

View File

@@ -1,19 +1,18 @@
package org.schabi.newpipe.info_list
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ItemStreamSegmentBinding
import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
class StreamSegmentItem(
private val item: StreamSegment,
private val onClick: StreamSegmentAdapter.StreamSegmentListener
) : Item<GroupieViewHolder>() {
) : BindableItem<ItemStreamSegmentBinding>() {
companion object {
const val PAYLOAD_SELECT = 1
@@ -21,31 +20,35 @@ class StreamSegmentItem(
var isSelected = false
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
item.previewUrl?.let {
PicassoHelper.loadThumbnail(it)
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
}
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
override fun bind(viewBinding: ItemStreamSegmentBinding, position: Int) {
CoilHelper.loadThumbnail(viewBinding.previewImage, item.previewUrl)
viewBinding.textViewTitle.text = item.title
if (item.channelName == null) {
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.GONE
viewBinding.textViewChannel.visibility = View.GONE
// When the channel name is displayed there is less space
// and thus the segment title needs to be only one line height.
// But when there is no channel name displayed, the title can be two lines long.
// The default maxLines value is set to 1 to display all elements in the AS preview,
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
viewBinding.textViewTitle.maxLines = 2
} else {
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
viewBinding.textViewChannel.text = item.channelName
viewBinding.textViewChannel.visibility = View.VISIBLE
}
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
viewBinding.textViewStartSeconds.text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
viewHolder.root.isSelected = isSelected
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewBinding.root.setOnLongClickListener {
onClick.onItemLongClick(this, item.startTimeSeconds)
true
}
viewBinding.root.isSelected = isSelected
}
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
override fun bind(
viewHolder: GroupieViewHolder<ItemStreamSegmentBinding>,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.contains(PAYLOAD_SELECT)) {
viewHolder.root.isSelected = isSelected
return
@@ -54,4 +57,6 @@ class StreamSegmentItem(
}
override fun getLayout() = R.layout.item_stream_segment
override fun initializeViewBinding(view: View) = ItemStreamSegmentBinding.bind(view)
}

View File

@@ -346,7 +346,7 @@ public final class InfoItemDialog {
public static void reportErrorDuringInitialization(final Throwable throwable,
final InfoItem item) {
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
throwable,
UserAction.OPEN_INFO_ITEM_DIALOG,
"none",

View File

@@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
private final ImageView itemThumbnailView;
@@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemAdditionalDetailView.setText(getDetailLine(item));
}
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) {

View File

@@ -27,8 +27,8 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
public class CommentInfoItemHolder extends InfoItemHolder {
@@ -82,14 +82,12 @@ public class CommentInfoItemHolder extends InfoItemHolder {
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) {
if (!(infoItem instanceof CommentsInfoItem item)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars());
if (ImageStrategy.shouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,

View File

@@ -9,8 +9,8 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
@@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setText(item.getUploaderName());
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) {

View File

@@ -16,8 +16,8 @@ import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit;
@@ -87,7 +87,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {

View File

@@ -0,0 +1,13 @@
package org.schabi.newpipe.ktx
import android.graphics.Bitmap
import android.graphics.Rect
import androidx.core.graphics.BitmapCompat
@Suppress("NOTHING_TO_INLINE")
inline fun Bitmap.scale(
width: Int,
height: Int,
srcRect: Rect? = null,
scaleInLinearSpace: Boolean = true
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)

View File

@@ -41,14 +41,16 @@ fun View.animate(
execOnEnd: Runnable? = null
) {
if (DEBUG) {
val id = try {
resources.getResourceEntryName(id)
} catch (e: Exception) {
id.toString()
}
val id = runCatching { resources.getResourceEntryName(id) }.getOrDefault(id.toString())
val msg = String.format(
"%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", enterOrExit,
javaClass.simpleName, id, animationType, duration, delay, execOnEnd
"%8s → [%s:%s] [%s %s:%s] execOnEnd=%s",
enterOrExit,
javaClass.simpleName,
id,
animationType,
duration,
delay,
execOnEnd
)
Log.d(TAG, "animate(): $msg")
}
@@ -291,5 +293,9 @@ private class HideAndExecOnEndListener(private val view: View, execOnEnd: Runnab
}
enum class AnimationType {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
ALPHA,
SCALE_AND_ALPHA,
LIGHT_SCALE_AND_ALPHA,
SLIDE_AND_ALPHA,
LIGHT_SLIDE_AND_ALPHA
}

View File

@@ -28,6 +28,8 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ThemeHelper.getItemViewMode;
import java.util.function.Supplier;
/**
* This fragment is design to be used with persistent data such as
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained
@@ -100,7 +102,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
@Nullable
protected ViewBinding getListHeader() {
protected Supplier<View> getListHeaderSupplier() {
return null;
}
@@ -131,9 +133,9 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
itemsList = rootView.findViewById(R.id.items_list);
refreshItemViewMode();
headerRootBinding = getListHeader();
if (headerRootBinding != null) {
itemListAdapter.setHeader(headerRootBinding.getRoot());
final Supplier<View> listHeaderSupplier = getListHeaderSupplier();
if (listHeaderSupplier != null) {
itemListAdapter.setHeaderSupplier(listHeaderSupplier);
}
footerRootBinding = getListFooter();
itemListAdapter.setFooter(footerRootBinding.getRoot());
@@ -210,6 +212,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
showListFooter(false);
}
@Deprecated(since = "Calling this method with `true` may cause crashes, see "
+ "https://github.com/TeamNewPipe/NewPipe/pull/12996#pullrequestreview-3713317115")
@Override
public void showListFooter(final boolean show) {
if (itemsList == null) {

View File

@@ -37,6 +37,7 @@ import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/*
* Created by Christian Schabesberger on 01.08.16.
@@ -88,7 +89,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private final DateTimeFormatter dateTimeFormatter;
private boolean showFooter = false;
private View header = null;
private Supplier<View> headerSupplier = null;
private View footer = null;
private ItemViewMode itemViewMode = ItemViewMode.LIST;
private boolean useItemHandle = false;
@@ -97,6 +98,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
recordManager = new HistoryRecordManager(context);
localItemBuilder = new LocalItemBuilder(context);
localItems = new ArrayList<>();
dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Localization.getPreferredLocale(context));
}
@@ -124,7 +126,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
if (DEBUG) {
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + ", "
+ "localItems.size() = " + localItems.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "header = " + hasHeader() + ", footer = " + footer + ", "
+ "showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
@@ -144,7 +146,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
final int index = localItems.indexOf(data);
if (index != -1) {
localItems.remove(index);
notifyItemRemoved(index + (header != null ? 1 : 0));
notifyItemRemoved(index + (hasHeader() ? 1 : 0));
} else {
// this happens when
// 1) removeItem is called on infoItemDuplicate as in showStreamItemDialog of
@@ -189,9 +191,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
this.useItemHandle = useItemHandle;
}
public void setHeader(final View header) {
final boolean changed = header != this.header;
this.header = header;
public void setHeaderSupplier(@Nullable final Supplier<View> headerSupplier) {
final boolean changed = headerSupplier != this.headerSupplier;
this.headerSupplier = headerSupplier;
if (changed) {
notifyDataSetChanged();
}
@@ -201,6 +203,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
this.footer = view;
}
protected boolean hasHeader() {
return this.headerSupplier != null;
}
@Deprecated(since = "Calling this method with `true` may cause crashes, see "
+ "https://github.com/TeamNewPipe/NewPipe/pull/12996#pullrequestreview-3713317115")
public void showFooter(final boolean show) {
if (DEBUG) {
Log.d(TAG, "showFooter() called with: show = [" + show + "]");
@@ -211,6 +219,8 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
showFooter = show;
if (show) {
Log.w(TAG, "Calling LocalItemListAdapter.showFooter(true) may cause crashes, see https"
+ "://github.com/TeamNewPipe/NewPipe/pull/12996#pullrequestreview-3713317115");
notifyItemInserted(sizeConsideringHeader());
} else {
notifyItemRemoved(sizeConsideringHeader());
@@ -218,11 +228,11 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
}
private int adapterOffsetWithoutHeader(final int offset) {
return offset - (header != null ? 1 : 0);
return offset - (hasHeader() ? 1 : 0);
}
private int sizeConsideringHeader() {
return localItems.size() + (header != null ? 1 : 0);
return localItems.size() + (hasHeader() ? 1 : 0);
}
public ArrayList<LocalItem> getItemsList() {
@@ -232,7 +242,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
@Override
public int getItemCount() {
int count = localItems.size();
if (header != null) {
if (hasHeader()) {
count++;
}
if (footer != null && showFooter) {
@@ -242,7 +252,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
if (DEBUG) {
Log.d(TAG, "getItemCount() called, count = " + count + ", "
+ "localItems.size() = " + localItems.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "header = " + hasHeader() + ", footer = " + footer + ", "
+ "showFooter = " + showFooter);
}
return count;
@@ -255,9 +265,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
}
if (header != null && position == 0) {
if (hasHeader() && position == 0) {
return HEADER_TYPE;
} else if (header != null) {
} else if (hasHeader()) {
position--;
}
if (footer != null && position == localItems.size() && showFooter) {
@@ -318,7 +328,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
}
switch (type) {
case HEADER_TYPE:
return new HeaderFooterHolder(header);
return new HeaderFooterHolder(headerSupplier.get());
case FOOTER_TYPE:
return new HeaderFooterHolder(footer);
case LOCAL_PLAYLIST_HOLDER_TYPE:
@@ -366,14 +376,14 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
if (holder instanceof LocalItemHolder) {
// If header isn't null, offset the items by -1
if (header != null) {
if (hasHeader()) {
position--;
}
((LocalItemHolder) holder)
.updateFromItem(localItems.get(position), recordManager, dateTimeFormatter);
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
((HeaderFooterHolder) holder).view = header;
} else if (holder instanceof HeaderFooterHolder && position == 0 && hasHeader()) {
((HeaderFooterHolder) holder).view = headerSupplier.get();
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
&& footer != null && showFooter) {
((HeaderFooterHolder) holder).view = footer;
@@ -387,10 +397,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
for (final Object payload : payloads) {
if (payload instanceof StreamStateEntity) {
((LocalItemHolder) holder).updateState(localItems
.get(header == null ? position : position - 1), recordManager);
.get(hasHeader() ? position - 1 : position), recordManager);
} else if (payload instanceof Boolean) {
((LocalItemHolder) holder).updateState(localItems
.get(header == null ? position : position - 1), recordManager);
.get(hasHeader() ? position - 1 : position), recordManager);
}
}
} else {

View File

@@ -7,6 +7,9 @@ import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedEntity
@@ -18,9 +21,6 @@ import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
class FeedDatabaseManager(context: Context) {
private val database = NewPipeDatabase.getInstance(context)
@@ -85,14 +85,13 @@ class FeedDatabaseManager(context: Context) {
items: List<StreamInfoItem>,
oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE
) {
val itemsToInsert = ArrayList<StreamInfoItem>()
loop@ for (streamItem in items) {
val uploadDate = streamItem.uploadDate
val itemsToInsert = items.mapNotNull { stream ->
val uploadDate = stream.uploadDate
itemsToInsert += when {
uploadDate == null && streamItem.streamType == StreamType.LIVE_STREAM -> streamItem
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> streamItem
else -> continue@loop
when {
uploadDate == null && stream.streamType == StreamType.LIVE_STREAM -> stream
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> stream
else -> null
}
}

View File

@@ -53,6 +53,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.OffsetDateTime
import java.util.function.Consumer
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
@@ -81,8 +83,6 @@ import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.getItemViewMode
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.util.function.Consumer
class FeedFragment : BaseStateFragment<FeedState>() {
private var _feedBinding: FragmentFeedBinding? = null
@@ -91,7 +91,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private val disposables = CompositeDisposable()
private lateinit var viewModel: FeedViewModel
@State @JvmField var listState: Parcelable? = null
@State
@JvmField
var listState: Parcelable? = null
private var groupId = FeedGroupEntity.GROUP_ALL_ID
private var groupName = ""
@@ -149,7 +152,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
if (newState == RecyclerView.SCROLL_STATE_IDLE &&
!recyclerView.canScrollVertically(-1)
) {
if (tryGetNewItemsLoadedButton()?.isVisible == true) {
hideNewItemsLoaded(true)
}
@@ -253,7 +255,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
viewModel.getShowFutureItemsFromPreferences()
)
AlertDialog.Builder(context!!)
AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_hide_streams_title)
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
checkedDialogItems[which] = isChecked
@@ -387,8 +389,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
if (item is StreamItem && !isRefreshing) {
val stream = item.streamWithState.stream
NavigationHelper.openVideoDetailFragment(
requireContext(), fm,
stream.serviceId, stream.url, stream.title, null, false
requireContext(),
fm,
stream.serviceId,
stream.url,
stream.title,
null,
false
)
}
}
@@ -500,7 +507,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val isFastFeedModeEnabled = sharedPreferences.getBoolean(
getString(R.string.feed_use_dedicated_fetch_method_key), false
getString(R.string.feed_use_dedicated_fetch_method_key),
false
)
val builder = AlertDialog.Builder(requireContext())
@@ -535,7 +543,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private fun updateRelativeTimeViews() {
updateRefreshViewState()
groupAdapter.notifyItemRangeChanged(
0, groupAdapter.itemCount,
0,
groupAdapter.itemCount,
StreamItem.UPDATE_RELATIVE_TIME
)
}

View File

@@ -1,8 +1,8 @@
package org.schabi.newpipe.local.feed
import androidx.annotation.StringRes
import org.schabi.newpipe.local.feed.item.StreamItem
import java.time.OffsetDateTime
import org.schabi.newpipe.local.feed.item.StreamItem
sealed class FeedState {
data class ProgressState(

View File

@@ -14,6 +14,8 @@ import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.Function6
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.App
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
@@ -25,8 +27,6 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
class FeedViewModel(
private val application: Application,
@@ -64,8 +64,14 @@ class FeedViewModel(
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean,
t5: Long, t6: List<OffsetDateTime?> ->
Function6 {
t1: FeedEventManager.Event,
t2: Boolean,
t3: Boolean,
t4: Boolean,
t5: Long,
t6: List<OffsetDateTime?>
->
return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull())
}
)
@@ -73,12 +79,13 @@ class FeedViewModel(
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.map { (event, showPlayedItems, showPartiallyPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
val streamItems = if (event is SuccessResultEvent || event is IdleEvent) {
feedDatabaseManager
.getStreams(groupId, showPlayedItems, showPartiallyPlayedItems, showFutureItems)
.blockingGet(arrayListOf())
else
} else {
arrayListOf()
}
CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate)
}
@@ -122,8 +129,7 @@ class FeedViewModel(
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
this.showPlayedItems.onNext(showPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
this.apply()
putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
}
}
@@ -132,8 +138,7 @@ class FeedViewModel(
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
this.apply()
putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
}
}
@@ -142,30 +147,26 @@ class FeedViewModel(
fun setSaveShowFutureItems(showFutureItems: Boolean) {
this.showFutureItems.onNext(showFutureItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.apply()
putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
}
}
fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
companion object {
private fun getShowPlayedItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_watched_items_key), true)
private fun getShowPlayedItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_watched_items_key), true)
private fun getShowPartiallyPlayedItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_partially_watched_items_key), true)
private fun getShowPartiallyPlayedItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_partially_watched_items_key), true)
private fun getShowFutureItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_future_items_key), true)
private fun getShowFutureItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_future_items_key), true)
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
initializer {
FeedViewModel(
App.getApp(),
App.instance,
groupId,
// Read initial value from preferences
getShowPlayedItemsFromPreferences(context.applicationContext),

View File

@@ -6,6 +6,8 @@ import android.view.View
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.xwray.groupie.viewbinding.BindableItem
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.database.stream.StreamWithState
@@ -19,9 +21,7 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.util.image.PicassoHelper
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import org.schabi.newpipe.util.image.CoilHelper
data class StreamItem(
val streamWithState: StreamWithState,
@@ -101,7 +101,7 @@ data class StreamItem(
viewBinding.itemProgressView.visibility = View.GONE
}
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
CoilHelper.loadThumbnail(viewBinding.itemThumbnailView, stream.thumbnailUrl)
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =
@@ -132,6 +132,7 @@ data class StreamItem(
viewsAndDate.isEmpty() -> uploadDate!!
else -> Localization.concatenateStrings(viewsAndDate, uploadDate)
}
else -> viewsAndDate
}
}

View File

@@ -6,7 +6,6 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.provider.Settings
@@ -15,21 +14,19 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
/**
* Helper for everything related to show notifications about new streams to the user.
*/
class NotificationHelper(val context: Context) {
private val manager = NotificationManagerCompat.from(context)
private val iconLoadingTargets = ArrayList<Target>()
/**
* Show notifications for new streams from a single channel. The individual notifications are
@@ -41,7 +38,9 @@ class NotificationHelper(val context: Context) {
fun displayNewStreamsNotifications(data: FeedUpdateInfo) {
val newStreams = data.newStreams
val summary = context.resources.getQuantityString(
R.plurals.new_streams, newStreams.size, newStreams.size
R.plurals.new_streams,
newStreams.size,
newStreams.size
)
val summaryBuilder = NotificationCompat.Builder(
context,
@@ -68,69 +67,42 @@ class NotificationHelper(val context: Context) {
summaryBuilder.setStyle(style)
// open the channel page when clicking on the summary notification
val intent = NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
summaryBuilder.setContentIntent(
PendingIntentCompat.getActivity(
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
0,
false
)
PendingIntentCompat.getActivity(context, data.pseudoId, intent, 0, false)
)
// a Target is like a listener for image loading events
val target = object : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
// set channel icon only if there is actually one (for Android versions < 7.0)
summaryBuilder.setLargeIcon(bitmap)
val avatarIcon =
CoilHelper.loadBitmapBlocking(context, data.avatarUrl, R.drawable.ic_newpipe_triangle_white)
summaryBuilder.setLargeIcon(avatarIcon)
// Show individual stream notifications, set channel icon only if there is actually
// one
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
// Show individual stream notifications
showStreamNotifications(newStreams, data.serviceId, data.url, null)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
// Nothing to do
}
// Show individual stream notifications, set channel icon only if there is actually one
showStreamNotifications(newStreams, data.serviceId, avatarIcon)
// Show summary notification
if (manager.areNotificationsEnabled()) {
manager.notify(data.pseudoId, summaryBuilder.build())
}
// add the target to the list to hold a strong reference and prevent it from being garbage
// collected, since Picasso only holds weak references to targets
iconLoadingTargets.add(target)
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
}
private fun showStreamNotifications(
newStreams: List<StreamInfoItem>,
serviceId: Int,
channelUrl: String,
channelIcon: Bitmap?
) {
for (stream in newStreams) {
val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
manager.notify(stream.url.hashCode(), notification)
if (manager.areNotificationsEnabled()) {
newStreams.forEach { stream ->
val notification =
createStreamNotification(stream, serviceId, channelIcon)
manager.notify(stream.url.hashCode(), notification)
}
}
}
private fun createStreamNotification(
item: StreamInfoItem,
serviceId: Int,
channelUrl: String,
channelIcon: Bitmap?
): Notification {
return NotificationCompat.Builder(
@@ -141,7 +113,7 @@ class NotificationHelper(val context: Context) {
.setLargeIcon(channelIcon)
.setContentTitle(item.name)
.setContentText(item.uploaderName)
.setGroup(channelUrl)
.setGroup(item.uploaderUrl)
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
.setColorized(true)
.setAutoCancel(true)
@@ -181,8 +153,7 @@ class NotificationHelper(val context: Context) {
val manager = context.getSystemService<NotificationManager>()!!
val enabled = manager.areNotificationsEnabled()
val channel = manager.getNotificationChannel(channelId)
val importance = channel?.importance
enabled && channel != null && importance != NotificationManager.IMPORTANCE_NONE
enabled && channel?.importance != NotificationManager.IMPORTANCE_NONE
} else {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
@@ -212,7 +183,7 @@ class NotificationHelper(val context: Context) {
context.startActivity(intent)
} else {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:" + context.packageName)
intent.data = "package:${context.packageName}".toUri()
context.startActivity(intent)
}
}

View File

@@ -16,6 +16,7 @@ import androidx.work.WorkerParameters
import androidx.work.rxjava3.RxWorker
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.App
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
@@ -23,7 +24,6 @@ import org.schabi.newpipe.error.ErrorUtil
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.local.feed.service.FeedLoadManager
import org.schabi.newpipe.local.feed.service.FeedLoadService
import java.util.concurrent.TimeUnit
/*
* Worker which checks for new streams of subscribed channels
@@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit
*/
class NotificationWorker(
appContext: Context,
workerParams: WorkerParameters,
workerParams: WorkerParameters
) : RxWorker(appContext, workerParams) {
private val notificationHelper by lazy {
@@ -95,9 +95,8 @@ class NotificationWorker(
private val TAG = NotificationWorker::class.java.simpleName
private const val WORK_TAG = App.PACKAGE_NAME + "_streams_notifications"
private fun areNotificationsEnabled(context: Context) =
NotificationHelper.areNewStreamsNotificationsEnabled(context) &&
NotificationHelper.areNotificationsEnabledOnDevice(context)
private fun areNotificationsEnabled(context: Context) = NotificationHelper.areNewStreamsNotificationsEnabled(context) &&
NotificationHelper.areNotificationsEnabledOnDevice(context)
/**
* Schedules a task for the [NotificationWorker]

View File

@@ -2,8 +2,9 @@ package org.schabi.newpipe.local.feed.notifications
import android.content.Context
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.getStringSafe
/**
* Information for the Scheduler which checks for new streams.
@@ -20,11 +21,9 @@ data class ScheduleOptions(
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
return ScheduleOptions(
interval = TimeUnit.SECONDS.toMillis(
preferences.getString(
preferences.getStringSafe(
context.getString(R.string.streams_notifications_interval_key),
null
)?.toLongOrNull() ?: context.getString(
R.string.streams_notifications_interval_default
context.getString(R.string.streams_notifications_interval_default)
).toLong()
),
isRequireNonMeteredNetwork = preferences.getString(

View File

@@ -3,8 +3,8 @@ package org.schabi.newpipe.local.feed.service
import androidx.annotation.StringRes
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.processors.BehaviorProcessor
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
import java.util.concurrent.atomic.AtomicBoolean
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
object FeedEventManager {
private var processor: BehaviorProcessor<Event> = BehaviorProcessor.create()

View File

@@ -11,6 +11,10 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.NotificationMode
@@ -27,10 +31,6 @@ import org.schabi.newpipe.util.ChannelTabHelper
import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo
import org.schabi.newpipe.util.ExtractorHelper.getChannelTab
import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class FeedLoadManager(private val context: Context) {
@@ -60,7 +60,7 @@ class FeedLoadManager(private val context: Context) {
*/
fun startLoading(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
ignoreOutdatedThreshold: Boolean = false,
ignoreOutdatedThreshold: Boolean = false
): Single<List<Notification<FeedUpdateInfo>>> {
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val useFeedExtractor = defaultSharedPreferences.getBoolean(
@@ -85,9 +85,12 @@ class FeedLoadManager(private val context: Context) {
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(
outdatedThreshold
)
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
outdatedThreshold, NotificationMode.ENABLED
outdatedThreshold,
NotificationMode.ENABLED
)
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
}
@@ -108,7 +111,8 @@ class FeedLoadManager(private val context: Context) {
broadcastProgress()
}
.observeOn(Schedulers.io())
.flatMap { Flowable.fromIterable(it) }
// Randomize user subscription ordering to attempt to resist fingerprinting
.flatMap { Flowable.fromIterable(it.shuffled()) }
.takeWhile { !cancelSignal.get() }
.doOnNext { subscriptionEntity ->
// throttle YouTube extractions once every BATCH_SIZE to avoid being rate limited
@@ -186,7 +190,8 @@ class FeedLoadManager(private val context: Context) {
val channelInfo = getChannelInfo(
subscriptionEntity.serviceId,
subscriptionEntity.url, true
subscriptionEntity.url,
true
)
.onErrorReturn(storeOriginalErrorAndRethrow)
.blockingGet()
@@ -216,7 +221,8 @@ class FeedLoadManager(private val context: Context) {
) {
val infoItemsPage = getMoreChannelTabItems(
subscriptionEntity.serviceId,
linkHandler, channelTabInfo.nextPage
linkHandler,
channelTabInfo.nextPage
)
.blockingGet()
@@ -234,7 +240,7 @@ class FeedLoadManager(private val context: Context) {
subscriptionEntity,
originalInfo!!,
streams!!,
errors,
errors
)
)
} catch (e: Throwable) {
@@ -305,6 +311,7 @@ class FeedLoadManager(private val context: Context) {
feedDatabaseManager.markAsOutdated(info.uid)
}
}
notification.isOnError -> {
val error = notification.error
feedResultsHolder.addError(error!!)

View File

@@ -36,13 +36,13 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.functions.Function
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.App
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import java.util.concurrent.TimeUnit
class FeedLoadService : Service() {
companion object {
@@ -94,7 +94,8 @@ class FeedLoadService : Service() {
.doOnSubscribe {
startForeground(NOTIFICATION_ID, notificationBuilder.build())
}
.subscribe { _, error: Throwable? -> // explicitly mark error as nullable
.subscribe { _, error: Throwable? ->
// explicitly mark error as nullable
if (error != null) {
Log.e(TAG, "Error while storing result", error)
handleError(error)
@@ -184,7 +185,9 @@ class FeedLoadService : Service() {
}
}
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
}
}
// /////////////////////////////////////////////////////////////////////////

View File

@@ -3,5 +3,5 @@ package org.schabi.newpipe.local.feed.service
data class FeedLoadState(
val updateDescription: String,
val maxProgress: Int,
val currentProgress: Int,
val currentProgress: Int
)

View File

@@ -25,13 +25,13 @@ data class FeedUpdateInfo(
val description: String?,
val subscriberCount: Long?,
val streams: List<StreamInfoItem>,
val errors: List<Throwable>,
val errors: List<Throwable>
) {
constructor(
subscription: SubscriptionEntity,
info: Info,
streams: List<StreamInfoItem>,
errors: List<Throwable>,
errors: List<Throwable>
) : this(
uid = subscription.uid,
notificationMode = subscription.notificationMode,
@@ -46,7 +46,7 @@ data class FeedUpdateInfo(
description = (info as? ChannelInfo)?.description,
subscriberCount = (info as? ChannelInfo)?.subscriberCount,
streams = streams,
errors = errors,
errors = errors
)
/**

View File

@@ -13,7 +13,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import com.evernote.android.state.State;
import com.google.android.material.snackbar.Snackbar;
@@ -45,6 +44,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@@ -126,12 +126,12 @@ public class StatisticsPlaylistFragment
}
@Override
protected ViewBinding getListHeader() {
protected Supplier<View> getListHeaderSupplier() {
headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(),
itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
return headerBinding;
return headerBinding::getRoot;
}
@Override

View File

@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter;
@@ -30,17 +30,16 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistMetadataEntry)) {
if (!(localItem instanceof PlaylistMetadataEntry item)) {
return;
}
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemTitleView.setText(item.getOrderingName());
itemStreamCountView.setText(Localization.localizeStreamCountMini(
itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setVisibility(View.INVISIBLE);
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
if (item instanceof PlaylistDuplicatesEntry
&& ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) {

View File

@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter;
@@ -83,8 +83,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
item.getStreamEntity().getThumbnailUrl());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {

View File

@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter;
@@ -117,8 +117,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
item.getStreamEntity().getThumbnailUrl());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {

View File

@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter;
@@ -29,10 +29,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistRemoteEntity)) {
if (!(localItem instanceof PlaylistRemoteEntity item)) {
return;
}
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemTitleView.setText(item.getOrderingName());
itemStreamCountView.setText(Localization.localizeStreamCountMini(
@@ -45,7 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
}
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}

View File

@@ -44,7 +44,6 @@ private fun exportJustUrls(playlist: List<PlaylistStreamEntry>): String {
}
private fun exportAsYoutubeTempPlaylist(playlist: List<PlaylistStreamEntry>): String {
val videoIDs = playlist.asReversed().asSequence()
.mapNotNull { getYouTubeId(it.streamEntity.url) }
.take(50) // YouTube limitation: temp playlists can't have more than 50 items
@@ -64,6 +63,5 @@ private val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHand
* @return the video id
*/
private fun getYouTubeId(url: String): String? {
return try { linkHandler.getId(url) } catch (e: ParsingException) { null }
return runCatching { linkHandler.getId(url) }.getOrNull()
}

View File

@@ -1,5 +1,7 @@
package org.schabi.newpipe.local.playlist;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.local.playlist.ExportPlaylistKt.export;
@@ -22,6 +24,8 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -29,7 +33,6 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
import com.evernote.android.state.State;
import org.reactivestreams.Subscriber;
@@ -55,6 +58,7 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@@ -67,6 +71,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@@ -158,14 +163,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
@Override
protected ViewBinding getListHeader() {
protected Supplier<View> getListHeaderSupplier() {
headerBinding = LocalPlaylistHeaderBinding.inflate(activity.getLayoutInflater(), itemsList,
false);
false);
playlistControlBinding = headerBinding.playlistControl;
headerBinding.playlistTitleView.setSelected(true);
return headerBinding;
return headerBinding::getRoot;
}
@Override
@@ -365,17 +370,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
createRenameDialog();
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
if (!isRewritingPlaylist) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning)
.setTitle(R.string.remove_watched_popup_title)
.setPositiveButton(R.string.ok, (d, id) ->
removeWatchedStreams(false))
.setNeutralButton(
R.string.remove_watched_popup_yes_and_partially_watched_videos,
(d, id) -> removeWatchedStreams(true))
.setNegativeButton(R.string.cancel,
(d, id) -> d.cancel())
.show();
openRemoveWatchedConfirmationDialog();
}
} else if (item.getItemId() == R.id.menu_item_remove_duplicates) {
if (!isRewritingPlaylist) {
@@ -447,39 +442,28 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.getIsPlaylistThumbnailPermanent(playlistId);
boolean thumbnailVideoRemoved = false;
if (removePartiallyWatched) {
for (final var playlistItem : playlist) {
final int indexInHistory = Collections.binarySearch(historyStreamIds,
playlistItem.getStreamId());
final var streamStates = recordManager
.loadLocalStreamStateBatch(playlist).blockingGet();
if (indexInHistory < 0) {
itemsToKeep.add(playlistItem);
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved
&& playlistManager.getPlaylistThumbnailStreamId(playlistId)
== playlistItem.getStreamEntity().getUid()) {
thumbnailVideoRemoved = true;
}
}
} else {
final var streamStates = recordManager
.loadLocalStreamStateBatch(playlist).blockingGet();
for (int i = 0; i < playlist.size(); i++) {
final var playlistItem = playlist.get(i);
final var streamStateEntity = streamStates.get(i);
final int indexInHistory = Collections.binarySearch(historyStreamIds,
playlistItem.getStreamId());
final long duration = playlistItem.toStreamInfoItem().getDuration();
for (int i = 0; i < playlist.size(); i++) {
final var playlistItem = playlist.get(i);
final var streamStateEntity = streamStates.get(i);
final int indexInHistory = Collections.binarySearch(historyStreamIds,
playlistItem.getStreamId());
final long duration = playlistItem.toStreamInfoItem().getDuration();
if (indexInHistory < 0 || (streamStateEntity != null
&& !streamStateEntity.isFinished(duration))) {
itemsToKeep.add(playlistItem);
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved
&& playlistManager.getPlaylistThumbnailStreamId(playlistId)
== playlistItem.getStreamEntity().getUid()) {
thumbnailVideoRemoved = true;
}
if (indexInHistory < 0 // stream is not in history
// stream is in history but the streamStateEntity is null
// if the stream was played for less than 5 seconds, see
// StreamStateEntity#PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|| streamStateEntity == null
|| (!removePartiallyWatched
&& !streamStateEntity.isFinished(duration))) {
itemsToKeep.add(playlistItem);
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved
&& playlistManager.getPlaylistThumbnailStreamId(playlistId)
== playlistItem.getStreamEntity().getUid()) {
thumbnailVideoRemoved = true;
}
}
@@ -904,6 +888,35 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.show();
}
/**
* Opens a confirmation dialog to remove watched streams from the playlist.
* The user can also choose to remove partially watched streams.
*/
private void openRemoveWatchedConfirmationDialog() {
final android.widget.CheckBox removePartiallyWatchedCheckbox =
new android.widget.CheckBox(requireContext());
removePartiallyWatchedCheckbox.setText(
R.string.remove_watched_popup_partially_watched_streams);
// Wrap the checkbox in a container with dialog-like horizontal padding
// so it aligns with the dialog title and message on the start side.
final LinearLayout checkboxContainer = new LinearLayout(requireContext());
checkboxContainer.setOrientation(LinearLayout.VERTICAL);
final int padding = DeviceUtils.dpToPx(20, requireContext());
checkboxContainer.setPadding(padding, padding, padding, 0);
checkboxContainer.addView(removePartiallyWatchedCheckbox,
new LayoutParams(MATCH_PARENT, WRAP_CONTENT));
new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning)
.setTitle(R.string.remove_watched_popup_title)
.setView(checkboxContainer)
.setPositiveButton(R.string.yes, (d, id) ->
removeWatchedStreams(removePartiallyWatchedCheckbox.isChecked()))
.setNegativeButton(R.string.cancel, (d, id) -> d.cancel())
.show();
}
public void setTabsPagerAdapter(
@Nullable final MainFragment.SelectedTabsPagerAdapter tabsPagerAdapter) {
this.tabsPagerAdapter = tabsPagerAdapter;

View File

@@ -27,6 +27,9 @@ import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.GroupieViewHolder
import io.reactivex.rxjava3.disposables.CompositeDisposable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
import org.schabi.newpipe.databinding.DialogTitleBinding
@@ -62,9 +65,6 @@ import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private var _binding: FragmentSubscriptionBinding? = null
@@ -276,10 +276,13 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
when (item) {
is FeedGroupCardItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupCardGridItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupAddNewItem ->
FeedGroupDialog.newInstance().show(fm, null)
is FeedGroupAddNewGridItem ->
FeedGroupDialog.newInstance().show(fm, null)
}
@@ -294,6 +297,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
when (item) {
is FeedGroupCardItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
is FeedGroupCardGridItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
}
@@ -309,7 +313,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
title = getString(R.string.feed_groups_header_title),
onSortClicked = ::openReorderDialog,
onToggleListViewModeClicked = ::toggleListViewMode,
listViewMode = viewModel.getListViewMode(),
listViewMode = viewModel.getListViewMode()
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
@@ -342,9 +346,14 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
val actions = DialogInterface.OnClickListener { _, i ->
when (i) {
0 -> ShareUtils.shareText(
requireContext(), selectedItem.name, selectedItem.url, selectedItem.thumbnails
requireContext(),
selectedItem.name,
selectedItem.url,
selectedItem.thumbnails
)
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
2 -> deleteChannel(selectedItem)
}
}
@@ -374,7 +383,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> {
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
fm,
selectedItem.serviceId, selectedItem.url, selectedItem.name
selectedItem.serviceId,
selectedItem.url,
selectedItem.name
)
override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem)
@@ -404,6 +415,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
itemsListState = null
}
}
is SubscriptionState.ErrorState -> {
result.error?.let {
showError(ErrorInfo(result.error, UserAction.SOMETHING_ELSE, "Subscriptions"))

View File

@@ -37,13 +37,16 @@ class SubscriptionManager(context: Context) {
filterQuery.isNotEmpty() -> {
return if (showOnlyUngrouped) {
subscriptionTable.getSubscriptionsOnlyUngroupedFiltered(
currentGroupId, filterQuery
currentGroupId,
filterQuery
)
} else {
subscriptionTable.getSubscriptionsFiltered(filterQuery)
}
}
showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId)
else -> subscriptionTable.getAll()
}
}
@@ -67,19 +70,18 @@ class SubscriptionManager(context: Context) {
return listEntities
}
fun updateChannelInfo(info: ChannelInfo): Completable =
subscriptionTable.getSubscription(info.serviceId, info.url)
.flatMapCompletable {
Completable.fromRunnable {
it.apply {
name = info.name
avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars)
description = info.description
subscriberCount = info.subscriberCount
}
subscriptionTable.update(it)
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
.flatMapCompletable {
Completable.fromRunnable {
it.apply {
name = info.name
avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars)
description = info.description
subscriberCount = info.subscriberCount
}
subscriptionTable.update(it)
}
}
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
return subscriptionTable().getSubscription(serviceId, url)

View File

@@ -9,6 +9,7 @@ import com.xwray.groupie.Group
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.info_list.ItemViewMode
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.item.ChannelItem
@@ -16,7 +17,6 @@ import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import org.schabi.newpipe.util.ThemeHelper.getItemViewMode
import java.util.concurrent.TimeUnit
class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)

View File

@@ -23,6 +23,7 @@ import com.livefront.bridge.Bridge
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.Section
import java.io.Serializable
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
@@ -40,7 +41,6 @@ import org.schabi.newpipe.local.subscription.item.PickerIconItem
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.ThemeHelper
import java.io.Serializable
class FeedGroupDialog : DialogFragment(), BackPressable {
private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null
@@ -61,16 +61,41 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
data object DeleteScreen : ScreenState()
}
@State @JvmField var selectedIcon: FeedGroupIcon? = null
@State @JvmField var selectedSubscriptions: HashSet<Long> = HashSet()
@State @JvmField var wasSubscriptionSelectionChanged: Boolean = false
@State @JvmField var currentScreen: ScreenState = InitialScreen
@State
@JvmField
var selectedIcon: FeedGroupIcon? = null
@State @JvmField var subscriptionsListState: Parcelable? = null
@State @JvmField var iconsListState: Parcelable? = null
@State @JvmField var wasSearchSubscriptionsVisible = false
@State @JvmField var subscriptionsCurrentSearchQuery = ""
@State @JvmField var subscriptionsShowOnlyUngrouped = false
@State
@JvmField
var selectedSubscriptions: HashSet<Long> = HashSet()
@State
@JvmField
var wasSubscriptionSelectionChanged: Boolean = false
@State
@JvmField
var currentScreen: ScreenState = InitialScreen
@State
@JvmField
var subscriptionsListState: Parcelable? = null
@State
@JvmField
var iconsListState: Parcelable? = null
@State
@JvmField
var wasSearchSubscriptionsVisible = false
@State
@JvmField
var subscriptionsCurrentSearchQuery = ""
@State
@JvmField
var subscriptionsShowOnlyUngrouped = false
private val subscriptionMainSection = Section()
private val subscriptionEmptyFooter = Section()
@@ -153,8 +178,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
itemAnimator = null
adapter = subscriptionGroupAdapter
layoutManager = GridLayoutManager(
requireContext(), subscriptionGroupAdapter.spanCount,
RecyclerView.VERTICAL, false
requireContext(),
subscriptionGroupAdapter.spanCount,
RecyclerView.VERTICAL,
false
).apply {
spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup
}
@@ -300,7 +327,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
val feedGroupIcon = selectedIcon ?: icon
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
@@ -362,7 +389,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
val selectedCount = this.selectedSubscriptions.size
val selectedCountText = resources.getQuantityString(
R.plurals.feed_group_dialog_selection_count,
selectedCount, selectedCount
selectedCount,
selectedCount
)
feedGroupCreateBinding.selectedSubscriptionCountView.text = selectedCountText
feedGroupCreateBinding.subscriptionsHeaderInfo.text = selectedCountText
@@ -478,7 +506,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow(
searchLayoutBinding.toolbarSearchEditText.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
InputMethodManager.HIDE_NOT_ALWAYS
)
searchLayoutBinding.toolbarSearchEditText.clearFocus()
}
@@ -495,7 +523,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboard() {
inputMethodManager.hideSoftInputFromWindow(
feedGroupCreateBinding.groupNameInput.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
InputMethodManager.HIDE_NOT_ALWAYS
)
feedGroupCreateBinding.groupNameInput.clearFocus()
}

View File

@@ -55,7 +55,8 @@ class FeedGroupDialogViewModel(
private var subscriptionsDisposable = Flowable
.combineLatest(
subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId)
subscriptionsFlowable,
feedDatabaseManager.subscriptionIdsForGroup(groupId)
) { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() }
.subscribeOn(Schedulers.io())
.subscribe(mutableSubscriptionsLiveData::postValue)
@@ -125,7 +126,10 @@ class FeedGroupDialogViewModel(
) = viewModelFactory {
initializer {
FeedGroupDialogViewModel(
context.applicationContext, groupId, initialQuery, initialShowOnlyUngrouped
context.applicationContext,
groupId,
initialQuery,
initialShowOnlyUngrouped
)
}
}

View File

@@ -15,6 +15,7 @@ import com.evernote.android.state.State
import com.livefront.bridge.Bridge
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.TouchCallback
import java.util.Collections
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
@@ -22,7 +23,6 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
import org.schabi.newpipe.util.ThemeHelper
import java.util.Collections
class FeedGroupReorderDialog : DialogFragment() {
private var _binding: DialogFeedGroupReorderBinding? = null

View File

@@ -9,7 +9,7 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
class ChannelItem(
private val infoItem: ChannelInfoItem,
@@ -39,11 +39,14 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description
}
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
CoilHelper.loadAvatar(itemThumbnailView, infoItem.thumbnails)
gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) }
viewHolder.root.setOnLongClickListener { held(infoItem); true }
viewHolder.root.setOnLongClickListener {
held(infoItem)
true
}
}
}

View File

@@ -10,7 +10,7 @@ import org.schabi.newpipe.local.subscription.FeedGroupIcon
data class FeedGroupCardGridItem(
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
val name: String,
val icon: FeedGroupIcon,
val icon: FeedGroupIcon
) : BindableItem<FeedGroupCardGridItemBinding>() {
constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon)

View File

@@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity,
@@ -21,7 +21,7 @@ data class PickerSubscriptionItem(
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
CoilHelper.loadAvatar(viewBinding.thumbnailView, subscriptionEntity.avatarUrl)
viewBinding.titleView.text = subscriptionEntity.name
viewBinding.selectedHighlight.isVisible = isSelected
}

View File

@@ -144,7 +144,9 @@ public abstract class BaseImportExportService extends Service {
notificationBuilder.setContentText(text);
}
notificationManager.notify(getNotificationId(), notificationBuilder.build());
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
}
protected void stopService() {
@@ -174,7 +176,10 @@ public abstract class BaseImportExportService extends Service {
.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty))
.setContentText(textOrEmpty);
notificationManager.notify(getNotificationId(), notificationBuilder.build());
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
}
protected NotificationCompat.Builder createNotification() {

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