Compare commits

..

490 Commits

Author SHA1 Message Date
Profpatsch
eb277fe14b Player/handleIntent: call handleIntentPost unconditionally
We always need to handleIntentPost otherwise the VideoDetailFragment
is not setup correctly.
2025-09-06 15:31:14 +02:00
Profpatsch
d77771a60c Player/handleIntent: fix enqueue if player not running
In 063dcd41e5 I falsely claimed that the
fallthrough case is always degenerate, but it kinda somehow still
worked because if you long-click on e.g. the popup button, it would
call enqueue, but if nothing was running yet it would fallthrough to
the very last case and start the player with the video.

So let’s return to that and add a TODO for further refactoring in the
future.
2025-09-06 15:09:11 +02:00
Profpatsch
01f9a3de33 Fix Checkstyle & remove unused fields 2025-09-06 15:09:11 +02:00
Profpatsch
150649aea9 Player/handleIntent: Don’t delete queue when clicking on timestamp
Fixes https://github.com/TeamNewPipe/NewPipe/issues/11013

We finally are at the point where we can have good logic around
clicking on timestamps.

This is pretty straightforward:

1) if we are already playing the stream (usual case), we skip to the
   correct second directly
2) If we don’t have a queue yet, create a trivial one with the stream
3) If we have a queue, we insert the video as next item and start
  playing it.

The skipping logic in 1) is similar to the one further down in the old
optimization block, but will always correctly fire for timestamps now.
I copied it because it’s not quite the same code, and moving into a
separate method at this stage would complicate the code too much.
2025-09-06 15:09:11 +02:00
Profpatsch
3803d49489 Player/handleIntent: separate out the timestamp request into enum
Instead of implicitely reconstructing whether the intent was
intended (lol) to be a timestamp change, we create a new kind of
intent that *only* sets the data we need to switch to a new timestamp.

This means that the logic of what to do (opening a popup player) gets
moved from `InternalUrlsHandler.playOnPopup` to the
`Player.handleIntent` method, we only pass that we want to jump to a
new timestamp. Thus, the stream is now loaded *after* sending the
intent instead of before sending.

This is somewhat messy right now and still does not fix the issue of
queue deletion, but from now on the queue logic should get more
straightforward to implement.

In the end, everything should be a giant switch. Thus we don’t
fall-through anymore, but run the post-setup code manually by calling
`handeIntentPost` and then returning.
2025-09-06 15:06:53 +02:00
Profpatsch
25a4a9a253 Player/handleIntent: move prefs parameters into initPlayback
They are just read from the player preferences and don’t influence the
branching, no need to read them in the intent parsing logic.
2025-09-06 15:04:06 +02:00
Profpatsch
d534946550 Player: inline repeat mode cycling 2025-09-06 15:04:06 +02:00
Profpatsch
8fb3e90fe1 Player: remove unused REPEAT_MODE intent key 2025-09-06 15:04:06 +02:00
Profpatsch
5750ef6aa8 Player/handleIntent: start converting intent data to enum
The goal here is to convert all player intents to use a single enum
with extra data for each case. The queue ones are pretty easy, they
don’t carry any extra data. We fall through for everything else for
now.
2025-09-06 15:04:06 +02:00
Profpatsch
ab7d1377e5 Player/handleIntent: always early return on ENQUEUE an ENQUEUE_NEXT
We can do this, because:

1. if `playQueue` is not null, we return early
2. if `playQueue` is null and we need to enqueue:
  - the only “proper” case that could be triggered is
    the `RESUME_PLAYBACK` case, which is never `true` for the queuing
    intents, see the comment in `NavigationHelper.enqueueOnPlayer`
  - the generic `else` case is degenerate, because it would crash on
  `playQueue` being `null`.

This makes some sense, because there is no way to trigger the
enqueueing logic via the UI currently if there is no video playing
yet, in which case `playQueue` is not `null`.

So we need to transform this whole if desaster into a big switch.
2025-09-06 15:04:06 +02:00
Profpatsch
fd24c08529 Player/handleIntent: de morgan samePlayQueue
Okay, so this is the … only? branch in this if-chain that will
conditionally fire if `playQueue` *is* `null`, sometimes.

This is why the unconditional `initPlayback` in `else` is not passed a
`null` in many cases … because `RESUME_PLAYBACK` is `true` and
`playQueue` is `null`.

It’s gonna be hard to figure out which parts of that are intentional,
I say.
2025-09-06 15:04:06 +02:00
Profpatsch
e14ec3a4f9 NavigationHelper: inline trivial getPlayerIntent use 2025-09-06 15:04:06 +02:00
Profpatsch
b592403a66 NavigationHelper: push out resumePlayback one layer 2025-09-06 15:04:06 +02:00
Profpatsch
90e1ac56ef NavigationHelper: inline getPlayerEnqueueIntent
Funnily enough, I’m pretty sure that whole comment will be not
necessary, because we never check `resumePlayback` on handling the
intent anyway.
2025-09-06 15:04:06 +02:00
Profpatsch
32eb3afe16 Player/handleIntent: a few comments 2025-09-06 15:04:06 +02:00
Profpatsch
35c7f2f5d1 Player: Remove unused IS_MUTED intent key
The only use of the key was removed in commit
2a2c82e73b
but the handling logic stayed around. So let’s get rid of it.
2025-09-05 16:57:27 +02:00
Stypox
8afb00d2f0 Merge pull request #12603 from Stypox/better-error-panel 2025-09-05 13:31:39 +02:00
Stypox
f27ec53c08 Even more centralized error handling in ErrorInfo 2025-09-05 12:39:16 +02:00
Stypox
a3ddd616f9 Merge pull request #12578 from Stypox/better-error-messages 2025-09-04 13:18:40 +02:00
Stypox
79980e2078 Address PR reviews 2025-09-04 13:17:45 +02:00
Isira Seneviratne
b204fad9d5 Merge pull request #12471 from Isira-Seneviratne/Fix-notifications
Fix foreground service issues
2025-09-01 05:05:47 +05:30
Isira Seneviratne
08f51abefb Added comments 2025-08-31 22:25:12 +05:30
Stypox
204df4c45a Fix test 2025-08-30 14:58:08 +02:00
Stypox
989c0cfd28 Fix REPORT in snackbar not opening ErrorActivity if MainActivity not shown
Bug caused by https://github.com/TeamNewPipe/NewPipe/pull/11789
2025-08-30 14:39:23 +02:00
Stypox
a369deeef4 Allow ErrorInfo messages with formatArgs
- ErrorInfo.getMessage() now returns an ErrorMessage instance that can be formatted into a string using a context (this allows the construction of an ErrorInfo to remain independent of a Context)
- now the service ID is used in ErrorInfo.getMessage() to customize some messages based on the currently selected service
- player HTTP invalid statuses are now included in the message
- building a custom error message for AccountTerminatedException was moved from ErrorPanelHelper to ErrorInfo
2025-08-30 14:36:27 +02:00
Stypox
1bde2dcd9f Fix ordering of error messages conditions 2025-08-28 17:06:10 +02:00
Stypox
29a3ca83b5 Show better information about player errors 2025-08-28 17:06:09 +02:00
Stypox
38064be702 Add more specific error messages and deduplicate their handling 2025-08-28 17:05:52 +02:00
Tobi
d17eae9bad Merge pull request #12253 from Profpatsch/popup-overlay-alert-dialog
Overlay Permission: display explanatory dialog for Android > R
2025-08-27 02:50:45 -07:00
TobiGr
74562db965 Use androidx compat alert dialog 2025-08-27 11:45:31 +02:00
Profpatsch
386d5197d8 Permission: display explanatory dialog for Android > R
On Android > R, ACTION_MANAGE_OVERLAY_PERMISSION always brings the
user to the app selection screen.

https://developer.android.com/about/versions/11/privacy/permissions#manage_overlay

This is highly confusing behaviour from the system, so let’s add an
instruction before navigating to the settings menu.
2025-08-27 11:38:25 +02:00
Tobi
ccd76dea1f Merge pull request #12544 from Stypox/download-options
Add option to delete a download without also deleting file
2025-08-27 02:31:14 -07:00
Stypox
7644066c5a Add option to delete a download without also deleting file 2025-08-16 16:50:01 +02:00
Stypox
9bc8139b8c Merge pull request #12483 from TeamNewPipe/ignore-picasso-update 2025-08-11 17:48:30 +02:00
Tobi
ff3526b28d Merge pull request #12460 from Isira-Seneviratne/Short-count-refactor
Fix short count formatting for Android versions below 7.0
2025-08-01 10:56:41 -07:00
TobiGr
d6c0dc32d1 Correctly ignore new version check for picasso 2025-08-01 10:50:54 +02:00
Stypox
124ab56c5f Merge branch 'master' into dev 2025-07-31 23:52:01 +02:00
Stypox
95a0e0ca39 Merge pull request #12435 from TeamNewPipe/release-0.28.0 2025-07-31 23:51:10 +02:00
Stypox
4d97a7653d Merge pull request #12450 from TeamNewPipe/yt-trending-migration 2025-07-31 23:48:53 +02:00
Hosted Weblate
5aefa4aff2 Translated using Weblate (Tigrinya)
Currently translated at 12.7% (95 of 748 strings)

Co-authored-by: fool <thing-sauna-cussed@duck.com>
2025-07-31 23:43:24 +02:00
Stypox
b846746119 Update NewPipeExtractor to v0.24.8 2025-07-31 23:43:19 +02:00
Stypox
b7b836e941 Update the names of YT kiosks 2025-07-31 23:43:19 +02:00
Stypox
d96c0aebb1 Show tabs above kiosks in drawer 2025-07-31 23:43:19 +02:00
Stypox
8400a9ae8e Remove DEBUG statements and don't replace yt trending with live
You can use this command to test instead:

adb shell run-as org.schabi.newpipe.debug.pr12450 'sed -i '"'"'s#<int name="last_used_preferences_version" value="8" />#<int name="last_used_preferences_version" value="6" />#'"'"' shared_prefs/org.schabi.newpipe.debug.pr12450_preferences.xml' && adb shell run-as org.schabi.newpipe.debug.pr12450 'sed -i '"'"'s#\]}</string>#,{\&quot;tab_id\&quot;:5,\&quot;service_id\&quot;:0,\&quot;kiosk_id\&quot;:\&quot;Trending\&quot;},{\&quot;tab_id\&quot;:5,\&quot;service_id\&quot;:1,\&quot;kiosk_id\&quot;:\&quot;Top 50\&quot;}]}</string>#'"'"' shared_prefs/org.schabi.newpipe.debug.pr12450_preferences.xml'
2025-07-31 23:43:19 +02:00
Stypox
7cecd11f72 [YouTube] Add icons and strings for new trending pages 2025-07-31 23:43:19 +02:00
TobiGr
ed93603815 WIP: Add SettingsMigration to change YouTube trending kiosk tab 2025-07-31 23:43:19 +02:00
Stypox
56f79fac13 Merge branch 'release-0.28.0' into dev 2025-07-30 11:42:06 +02:00
Stypox
86efde5996 Merge pull request #12476 from TeamNewPipe/weblate 2025-07-29 20:23:08 +02:00
Stypox
ca9fc14c2a Fix name of nepali language (there was a leftover N) 2025-07-29 20:19:31 +02:00
tobigr
7130adb4ec Clean strings 2025-07-29 20:19:31 +02:00
tobigr
e08d2d8726 Add new locals to the in-app language chooser 2025-07-29 20:19:31 +02:00
Isira Seneviratne
ef29c318b0 Remove NewApi suppression 2025-07-29 06:18:27 +05:30
Hosted Weblate
6516fb96fd Translated using Weblate (Romanian)
Currently translated at 100.0% (748 of 748 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (748 of 748 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (748 of 748 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (748 of 748 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (748 of 748 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (748 of 748 strings)

Translated using Weblate (French)

Currently translated at 100.0% (748 of 748 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (745 of 749 strings)

Translated using Weblate (Estonian)

Currently translated at 18.6% (16 of 86 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.5% (746 of 749 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.7% (747 of 749 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.7% (747 of 749 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Slovak)

Currently translated at 99.7% (747 of 749 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.5% (746 of 749 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (German)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (German)

Currently translated at 100.0% (749 of 749 strings)

Translated using Weblate (Tamazight (Central Atlas))

Currently translated at 19.2% (144 of 749 strings)

Translated using Weblate (Macedonian)

Currently translated at 79.3% (594 of 749 strings)

Translated using Weblate (Slovenian)

Currently translated at 54.6% (409 of 749 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.7% (95 of 747 strings)

Translated using Weblate (Tigrinya)

Currently translated at 3.4% (3 of 86 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (743 of 747 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 90.6% (78 of 86 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (French)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Polish)

Currently translated at 58.1% (50 of 86 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (German)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (German)

Currently translated at 100.0% (747 of 747 strings)

Co-authored-by: 439JBYL80IGQTF25UXNR0X1BG <439JBYL80IGQTF25UXNR0X1BG@users.noreply.hosted.weblate.org>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Drugi Sapog <dindrugi@users.noreply.hosted.weblate.org>
Co-authored-by: Dual Natan <dvapatinatan@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Hakim Oubouali <hakim.oubouali.skr@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Matej U <mateju@src.gnome.org>
Co-authored-by: Michael Moroni <michaelmoroni@disroot.org>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: NTFSynergy <ntfsynergy@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: Rex_sa <rex.sa@pm.me>
Co-authored-by: Stypox <stypox@pm.me>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
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: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yassin Amir <y6b5@proton.me>
Co-authored-by: erti <erti@users.noreply.hosted.weblate.org>
Co-authored-by: ikanakova <ikanakova@users.noreply.hosted.weblate.org>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: whistlingwoods <72640314+whistlingwoods@users.noreply.github.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim5@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: 赖诚俊 <cosmic.universe.glitch@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
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/et/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ti/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2025-07-28 23:05:05 +02:00
Stypox
e9922fe162 Merge pull request #12470 from litetex/cleanup-PlayerHelper-localization 2025-07-28 15:30:07 +02:00
Stypox
eea2b7417e Fix player formatters resetting too early on language change
formatters() is called again by the player before the user has a chance to click on the language in the language chooser.

So the correct solution would probably be to attach to https://developer.android.com/reference/android/content/Intent#ACTION_LOCALE_CHANGED, but let's keep it simple. I added `PlayerHelper.resetFormat();` in `ContentSettingsFragment.onDestroy()` and it works. It will mean the player formatters will be reset every time the user exits content settings, but whatever.
2025-07-28 15:29:06 +02:00
litetex
893a1cb699 Encapsulate Formatters in PlayerHelper
and reset them when the language is changed/changing.
This way they will be re-initialized on the next call.

Also Remove a bunch of outdated/non-thread safe code (STRING_FORMATTER)
2025-07-28 15:11:27 +02:00
litetex
ebd5e1a318 Remove unused method 2025-07-28 15:11:27 +02:00
litetex
70841db92f Cleanup `Localization` formatting 2025-07-28 15:11:27 +02:00
litetex
859555e129 Use regions 2025-07-28 15:11:27 +02:00
Stypox
c1cef19b33 Merge pull request #12455 from TobiGr/nextPage-nullable 2025-07-28 14:52:08 +02:00
Stypox
9ba30887f9 Improve null checking further in SearchFragment.handleNextItems 2025-07-28 14:43:46 +02:00
Stypox
0ef38e3a4d Merge pull request #12472 from TeamNewPipe/user-agent-140 2025-07-28 14:03:42 +02:00
Isira Seneviratne
9f11db8e06 Improve scale display 2025-07-28 09:02:52 +05:30
Isira Seneviratne
fece0741e5 Suppress NewApi 2025-07-27 15:47:06 +05:30
TobiGr
a9ce2e9605 Update USER_AGENT to Firefox 140 ESR 2025-07-27 09:39:53 +02:00
Isira Seneviratne
b9b47fc520 Update manifest, startForeground call 2025-07-27 11:58:01 +05:30
Isira Seneviratne
59db955493 Fix new streams notification issue 2025-07-27 11:31:23 +05:30
Isira Seneviratne
22a709d53b Merge pull request #12388 from mikooomich/sdk35
Target SDK 35
2025-07-24 08:18:32 +05:30
Michael Zh
329d76c857 Bump emulator target 33 -> 35 2025-07-23 22:30:34 -04:00
Isira Seneviratne
9f526e8e8f Fix short count formatting for Android versions below 7.0 2025-07-24 07:56:44 +05:30
Michael Zh
50caba6606 Fix compile
Co-Authored-By: Isira Seneviratne <31027858+Isira-Seneviratne@users.noreply.github.com>
2025-07-23 18:49:28 -04:00
Michael Zh
26443f9f14 WIP: Fix compile 2025-07-23 18:45:30 -04:00
Michael Zh
366129eee2 Fix error toast crash 2025-07-23 18:45:30 -04:00
Michael Zh
4c8d44b6ba Bump compileSdk to 36 and targetSdk to 35
* Sdk 36 requires edge to edge, so use 35 so we can opt out for now
2025-07-23 18:45:30 -04:00
Michael Zh
14cd562ebd Update manifest for sdk34 FGS changes 2025-07-23 18:45:30 -04:00
Michael Zh
04ef608f7a Specify RECEIVER_EXPORTED/RECEIVER_NOT_EXPORTED for sdk34 2025-07-23 18:45:30 -04:00
Stypox
71fcc5ebce Release v0.28.0 (1005) 2025-07-23 14:22:07 +02:00
TobiGr
30e33d59e8 Use correct fix for nextPage being null while creating error report in SearchFragment.handleNextItems() 2025-07-22 16:12:02 +02:00
Kouki Badr
a4bd82be8a fix: handle nullable nextPage behavior when searching albums #12401 (#12408)
* fix: handle nullable nextPage behavior when searching albums #12401

* feat: add nullable annotation to newPage attribute in SearchFragment

* Updated more usages of InfoItemsPage#getNextPage. Nullability is already handled in these areas so no other changes needed

---------

Co-authored-by: Siddhesh Naik <siddheshnaik20@protonmail.com>
2025-07-22 08:58:56 +05:30
litetex
45589dbf26 Merge pull request #12444 from Isira-Seneviratne/Per-app-language
Enable per-app language preferences for Android < 13
2025-07-20 22:20:12 +02:00
litetex
99ae3fdd4e Removed no longer needed translation key 2025-07-20 22:05:05 +02:00
litetex
f48e73eb2a Cleaned up some code related to app language
* Use build constants when possible
* Inline variables
* Don't use var for normal-sized types (that way it's easier to review)
* Split code into methods
2025-07-20 21:52:07 +02:00
Isira Seneviratne
99003bab07 Clean up imports 2025-07-20 16:43:37 +05:30
Isira Seneviratne
9e14f93186 Properly handle when system language is selected 2025-07-20 16:27:07 +05:30
Isira Seneviratne
abd9aade87 Update AppCompat 2025-07-20 05:24:56 +05:30
Isira Seneviratne
b8f9c125cd Add link for future reference 2025-07-20 05:03:20 +05:30
Isira Seneviratne
893a227ab1 Enable per-app language preferences for Android < 13 2025-07-20 04:50:49 +05:30
Stypox
0db859e225 Merge pull request #12438 from TeamNewPipe/soundcloud/top_50 2025-07-19 20:53:44 +02:00
Stypox
e61f98bd47 Merge pull request #12434 from TeamNewPipe/fix-new-badge-links-on-readme 2025-07-19 20:44:03 +02:00
Stypox
991d9ea3df Fix state not saved 2025-07-19 20:39:55 +02:00
Stypox
f94892166d Improve comment 2025-07-19 20:34:09 +02:00
Stypox
9697112db6 Show error panel in EmptyFragment 2025-07-19 19:41:13 +02:00
litetex
f64dba0107 Fix new badge links on Readme being rendered incorrectly
For all non default Readmes
2025-07-19 22:45:32 +05:30
litetex
9bf01e1241 Fix new badge links on Readme being rendered incorrectly 2025-07-19 22:45:32 +05:30
Stypox
474efbebc1 Merge pull request #12437 from TeamNewPipe/localization-main-page-content 2025-07-19 19:04:34 +02:00
tobigr
fe58ec85ed Fix error detection when loading main page tabs
Do not crash if something unexpected happens.
2025-07-19 13:37:54 +02:00
tobigr
941f85781b Display dialog informing the user about the removal of the Top 50 kiosk 2025-07-19 13:37:54 +02:00
tobigr
7e0ee4eb7a Update Extractor and add migration to remove SoundCloud Top 50 kiosk 2025-07-18 18:59:28 +02:00
tobigr
4a41214df4 Do not capitalize "page" for main page content options 2025-07-18 10:28:59 +02:00
Stypox
938265d127 Update NewPipeExtractor 2025-07-17 23:57:03 +02:00
Stypox
ba4e7a3c7f Add changelog for v0.28.0 (1005) 2025-07-17 10:18:10 +02:00
Hosted Weblate
58b5ccb66f Translated using Weblate (Czech)
Currently translated at 100.0% (85 of 85 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (745 of 745 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (745 of 745 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (745 of 745 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (745 of 745 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Serbian)

Currently translated at 99.7% (745 of 747 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (French)

Currently translated at 100.0% (747 of 747 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (Tigrinya)

Currently translated at 12.6% (94 of 744 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 8.2% (7 of 85 strings)

Translated using Weblate (Serbian)

Currently translated at 16.4% (14 of 85 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Azerbaijani (Southern))

Currently translated at 1.1% (1 of 85 strings)

Added translation using Weblate (Azerbaijani (Southern))

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Serbian)

Currently translated at 16.4% (14 of 85 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (85 of 85 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (85 of 85 strings)

Translated using Weblate (Romanian)

Currently translated at 99.7% (742 of 744 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (85 of 85 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (French)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (German)

Currently translated at 100.0% (744 of 744 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 97.6% (83 of 85 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (85 of 85 strings)

Translated using Weblate (Kabyle)

Currently translated at 29.0% (215 of 741 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (741 of 741 strings)

Added translation using Weblate (Luri (Bakhtiari))

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Persian)

Currently translated at 94.3% (699 of 741 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Persian)

Currently translated at 94.1% (698 of 741 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Finnish)

Currently translated at 98.5% (730 of 741 strings)

Translated using Weblate (French)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (85 of 85 strings)

Translated using Weblate (French)

Currently translated at 100.0% (85 of 85 strings)

Translated using Weblate (German)

Currently translated at 100.0% (85 of 85 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (German)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 69.4% (59 of 85 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Latvian)

Currently translated at 21.4% (18 of 84 strings)

Translated using Weblate (Latvian)

Currently translated at 99.7% (739 of 741 strings)

Translated using Weblate (Latvian)

Currently translated at 20.2% (17 of 84 strings)

Translated using Weblate (Latvian)

Currently translated at 16.6% (14 of 84 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (French)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Greek)

Currently translated at 32.1% (27 of 84 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Urdu)

Currently translated at 69.2% (513 of 741 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Catalan)

Currently translated at 90.2% (669 of 741 strings)

Translated using Weblate (Estonian)

Currently translated at 17.8% (15 of 84 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 76.7% (569 of 741 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 76.2% (565 of 741 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Kabyle)

Currently translated at 28.8% (214 of 741 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (740 of 741 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 67.8% (57 of 84 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (737 of 741 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (French)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (German)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Breton)

Currently translated at 13.6% (101 of 741 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (741 of 741 strings)

Translated using Weblate (Breton)

Currently translated at 12.1% (90 of 741 strings)

Translated using Weblate (Breton)

Currently translated at 7.6% (57 of 741 strings)

Translated using Weblate (Breton)

Currently translated at 7.4% (55 of 741 strings)

Translated using Weblate (Breton)

Currently translated at 7.4% (55 of 741 strings)

Translated using Weblate (Breton)

Currently translated at 7.4% (55 of 741 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.8% (740 of 741 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (741 of 741 strings)

Added translation using Weblate (Breton)

Translated using Weblate (Romanian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Finnish)

Currently translated at 98.5% (729 of 740 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (French)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (740 of 740 strings)

Co-authored-by: 439JBYL80IGQTF25UXNR0X1BG <439JBYL80IGQTF25UXNR0X1BG@users.noreply.hosted.weblate.org>
Co-authored-by: AP <kubanto@users.noreply.hosted.weblate.org>
Co-authored-by: Abu Sarim Hindi <sarfaraz.ahmed78@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: Balázs Meskó <meskobalazs@mailbox.org>
Co-authored-by: Bastian <basti.anderl774@gmail.com>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Darth23G (DarthGamer23) <fref2329@gmail.com>
Co-authored-by: Deleted User <noreply+48943@weblate.org>
Co-authored-by: Dream X <nodem49316@daupload.com>
Co-authored-by: Drugi Sapog <dindrugi@users.noreply.hosted.weblate.org>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Fareedar Islami <fareedar.islami@gmail.com>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jay Tromp <jaytromp@pm.me>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Jordi Cambrells <cambrells@users.noreply.hosted.weblate.org>
Co-authored-by: Jordi Cambrells <hanta.hrabal@gmail.com>
Co-authored-by: Juzé <dedakir923@exoular.com>
Co-authored-by: KaGaster <mohamed.kooli@medtech.tn>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Mandeep <mandeeps708@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Michael Moroni <michaelmoroni@disroot.org>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Mohammed al-Qubati <mhraqeeb@gmail.com>
Co-authored-by: Mücteba <muctebanesiri@gmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nick Wick <NickWick@users.noreply.hosted.weblate.org>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Q. Boii <sf1hks@marketmail.info>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: THANOS SIOURDAKIS <siourdakisthanos@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: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yasser Althuwaini <ymth2000@outlook.com>
Co-authored-by: Yauhen <bugomol@users.noreply.hosted.weblate.org>
Co-authored-by: ab_09 <ab_09@users.noreply.hosted.weblate.org>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: dekiw39846 <dekiw39846@bariswc.com>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: gfbdrgn <erfvvgtyhbnjhyuu@wireconnected.com>
Co-authored-by: late <late@users.noreply.hosted.weblate.org>
Co-authored-by: moton03 <moton.cat@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: rimasx <riks_12@hot.ee>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim5@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
Co-authored-by: Саша Петровић <salepetronije@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: 李恩霆 <timothylee0802@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/azb/
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/el/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/en_GB/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/et/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ta/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translation: NewPipe/Metadata
2025-07-17 09:16:38 +02:00
Stypox
4e94b2602d Merge pull request #12258 from Profpatsch/show-service-name-in-search 2025-07-16 15:02:43 +02:00
Stypox
4ddc0648ef Merge pull request #12412 from Stypox/fix-ghost-notifications 2025-07-14 21:56:22 +02:00
Tobi
4c920a4406 Merge pull request #12367 from watermelon42/3783_Import_Soundcloud_likes
Support Soundcloud likes in channel and feed
2025-07-14 01:01:02 -07:00
watermelon42
1c0eabf75c Updated extractor version to latest commit 2025-07-13 16:21:42 +02:00
watermelon42
f119a368d8 Added support for importing Soundcloud likes as a new tab before About in a user's channel.
The likes are also retrieved in the feed if the user is subscribed to.
2025-07-11 09:50:33 +02:00
Stypox
f3c20d43be Merge pull request #12410 from Stypox/fix-android-auto-thumbnails 2025-07-08 11:42:20 +02:00
Tobi
c9559fa801 Merge pull request #12416 from Stypox/fix-fullscreen-clear-queue-prompt
Fix fullscreen eliciting "clear queue" prompt
2025-07-07 11:47:27 -07:00
Stypox
8ab79488e9 Merge pull request #12409 from Stypox/readme-badge-size-fix 2025-07-07 16:26:20 +02:00
Stypox
f0b26e208b Update notice about rewrite in the README 2025-07-07 16:25:38 +02:00
Stypox
79084568f2 Fix fullscreen eliciting "clear queue" prompt 2025-07-07 15:07:46 +02:00
Stypox
705b5e5580 Fix ghost notifications on Android 10
Fixes #12400, see there for explanation. Citing from there:

So apparently the problem is onGetRoot always returning a BrowserRoot instance. Making it return null solved the issue (but again, breaks Android Auto compatibility). It turns out (see https://stackoverflow.com/q/63818988/) that onGetRoot is also used for media resumption https://developer.android.com/media/implement/surfaces/mobile#mediabrowserservice_implementation, which causes a new notification to pop up (in this case a useless notification because our onGetRoot does not return something that can be used for resumption). So what needs to be done is to check if rootHints?.getBoolean(EXTRA_RECENT) == true and if that's the case not return anything (as EXTRA_RECENT is used by the system for resumption).

The PackageValidator file is taken from 329a21b63c/common/src/main/java/com/example/android/uamp/media/PackageValidator.kt .
2025-07-07 01:06:59 +02:00
Stypox
a4d457b2b2 Use Kotlin's .toUri() instead of Uri.parse() 2025-07-06 15:05:30 +02:00
Stypox
834c93f22a Fix thumbnails appearing on Android Auto even if disabled 2025-07-06 14:49:09 +02:00
Stypox
a0adeb0099 Fix "Get it on F-Droid" appearing giant in README 2025-07-06 13:51:59 +02:00
Isira Seneviratne
2dd11f70a3 Merge pull request #12356 from dev-victoria/fix-json-importing-oldandroid
check if the JSON MimeType is supported
2025-07-04 07:06:34 +05:30
Stypox
d048bca8b4 Temporarily disable sonarcloud CI step 2025-06-28 15:17:18 +02:00
Diana Victoria Furrer
0c9f5ddcaf change according to Isira-Seneviratne suggestion 2025-06-17 15:42:01 +02:00
Diana Victoria Furrer
aa75a1449f use MimeTypeMap from android webkit to check if the json MimeType is unsupported 2025-06-15 02:19:56 +02:00
Profpatsch
16e32dfc96 SearchFragment: show filter in brackets behind service name
This is still not perfect, but it will show the selected search filter
in addition to the service name, like: “Search YouTube (Playlists)”.

It will not distinguish between a YouTube Music and Youtube filter, so
it will display the same thing. Could be improved, but then the text
gets too long! :(
2025-06-05 14:30:04 +02:00
Stypox
8c4a789f78 Merge pull request #12302 from davidasunmo/update-readmes 2025-06-04 11:59:56 +02:00
Stypox
769e98acd0 Show search filter in search bar hint 2025-06-04 11:54:31 +02:00
Stypox
8e036b5e69 Merge pull request #12325 from dev-victoria/FeedGroupTab 2025-06-04 11:24:32 +02:00
Stypox
571b7bc74b Improve layout of select_feed_group_item 2025-06-04 11:18:04 +02:00
Audric V.
033cc08c26 Merge pull request #12322 from dev-victoria/tiny-code-fixes
Fix equality comparison in Tab class
2025-05-31 16:56:37 +02:00
Diana Victoria Furrer
205d18f4c4 Use GroupName for the Settings Text.
The Tabname displays the default Feed title.
2025-05-31 14:11:26 +02:00
Diana Victoria Furrer
712724211c added FeedGroup to Tab Settings UnitTest 2025-05-31 01:41:06 +02:00
Diana Victoria Furrer
fd09e6147f # Fixed Feed Group Titlebar
- use default fragment_feed_title for TabName
- only clear FeedFragment bar subtitle when it matches the groupName to clear.
2025-05-31 01:30:49 +02:00
Diana Victoria Furrer
279caac915 # Change
Layout select_feed_group_item (FeedGroup Picker in the Settings)
Remove rounded style from the icons
2025-05-30 21:00:37 +02:00
Diana Victoria Furrer
f8ed8e575e # Change
Added FEEDGROUP Tab Code to
 - ChooseTabsFragment
 - Tab

Added strings:
- feed_group_page_summary
2025-05-30 20:47:37 +02:00
Diana Victoria Furrer
436626fa83 # Change
Adjusted select_feed_group_fragment Layout
 - reference select_feed_group_item layout
 - use new Strings

Added strings:
- select_a_feed_group
- no_feed_group_created_yet
2025-05-30 17:54:49 +02:00
Diana Victoria Furrer
7c3989ff93 # Change
Adjusted the new Class SelectFeedGroupFragment for its Role
- Renamed Variables
- adjusted Imports
- adjusted Interface with FeedGroupEntity Values
2025-05-30 17:45:51 +02:00
Diana Victoria Furrer
e6c4690e7d # Copied Layouts
Copied select_channel_fragment to select_feed_group_fragment

Copied select_channel_item to select_feed_group_item

# Change
Replaced the Layout references in the new Class SelectFeedGroupFragment
2025-05-30 17:07:19 +02:00
Diana Victoria Furrer
86869f0a14 Copied SelectFeedGroupFragment from SelectChannelFragment 2025-05-30 16:55:07 +02:00
Diana Victoria Furrer
aa0b45c05f ChannelTab.equals fix comparison 2025-05-30 13:21:45 +02:00
David Asunmo
55bf74b4a7 Fix CI status badge 2025-05-24 02:15:45 +01:00
David Asunmo
de3d11568d Add nightly builds to all readmes
Add matrix to .ru
2025-05-22 03:29:12 +01:00
David Asunmo
16077dee80 Add matrix chat link to all READMEs 2025-05-22 03:29:12 +01:00
Stypox
c9155f7834 Merge pull request #12298 from davidasunmo/add-dev-refactor-nightly-badges
Add dev and refactor nightly build badges
2025-05-20 11:13:38 +02:00
David
7dd1abdf9c Add dev and refactor nightly build badges
bottom text
2025-05-20 02:22:47 +01:00
Profpatsch
f3858e70a3 Merge pull request #11789 from Thompson3142/fix_background_crash_focus
Fix background crash focus
2025-05-09 23:41:38 +02:00
Thompson3142
76202e6b4b Remove no longer needed dependency 2025-05-09 22:29:05 +02:00
Thompson3142
90e2f234e7 Initial commit for better handling of background crashes
Fix crashing behaviour with entry in SharedPreferences

A few minor improvements

Added docs for isInBackground

Some more minor changes

Overwrite methods in MainActivity instead of creating a new class
2025-05-09 22:29:00 +02:00
Profpatsch
42a52b7118 Merge pull request #12259 from Profpatsch/put-@-on-right-side-of-rtl-usernames
Comments: Put @ on the right side of right-to-left usernames
2025-05-08 21:46:00 +02:00
Stypox
d9dccfa8af Merge branch 'master' into dev 2025-05-08 15:04:06 +02:00
Profpatsch
e554c77f2e Comments: Put @ on the right side of right-to-left usernames
From the discussion in
https://github.com/TeamNewPipe/NewPipe/pull/12188 it reads more
natural for RTL readers.
2025-05-07 14:20:44 +02:00
Stypox
81b4e3f970 Hotfix release v0.27.7 (1004) 2025-05-07 12:52:43 +02:00
TobiGr
ef068e1eca Update NewPipe Extractor and add new proguard rules
New rules are required since Rhino and Rhino Engine 1.8.0
2025-05-07 12:50:37 +02:00
Stypox
8407b5aefd Add translated changelogs for v0.27.7
Copied from 985.txt
2025-05-07 12:49:31 +02:00
Stypox
b6aa07545a Add changelog for v0.26.7 (1004) 2025-05-07 12:48:59 +02:00
Stypox
1dcb1953ba Update NewPipeExtractor to v0.24.6
For some reason
com.github.TeamNewPipe.NewPipeExtractor:v0.24.6
didn't work, but
com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:v0.24.6
as suggested on https://jitpack.io/#TeamNewPipe/NewPipeExtractor/v0.24.6 worked...
2025-05-07 12:36:08 +02:00
Profpatsch
862a8e8f26 Merge pull request #12188 from VougJo23/commentsfix
fix: support RTL usernames in comment header
2025-05-07 12:20:23 +02:00
Profpatsch
88395fa852 Merge pull request #12202 from AndrianaBilali/fix/timestamp-clicks-in-replies
Fix timestamps not working in comment replies
2025-05-07 12:07:03 +02:00
VougJo23
8d679626f0 fix: support RTL usernames in comment header
The `@` gets added by the youtube API and thus is a fixed member of
the username, so we do some simple detection logic to handle that
case (otherwise the `@` will be at the right side of a RTL username,
which is different of how Youtube displays these usernames in the
browser).

Fixes https://github.com/TeamNewPipe/NewPipe/issues/12141
2025-05-07 12:05:09 +02:00
Profpatsch
d2dc20c551 SearchFragment: show service name in search hint
The only hint (haha) which service one is searching in is currently
the color of the background. This is super confusing, yesterday a
friend tried to search for a video on youtube and the app was set to
Bandcamp, and they were super confused why nothing turned up.

So let’s put the name of the service in the hint!

The `updateService()` thing is a little confused, but I didn’t want
to refactor to improve the logic. It’s not doing anything
computationally intensive anyway.

For PeerTube, the sidebar calls it FramaTube but the service name is
PeerTube, I’m not sure why that is the case. Looks like the string
depends on the name of the instance? Hm, can be improved later I
think.
2025-05-07 10:12:41 +02:00
Andriana
e7f3750f5e Fix timestamps not working in comment replies
Use LinkMovementMethodCompat for comment links

Co-authored-by: Isira Seneviratne <31027858+Isira-Seneviratne@users.noreply.github.com>

Update import

Use LongPressLinkMovementMethod
2025-05-06 17:12:17 +02:00
j-haldane
48e826e912 Fix header crash in History List view (#12214)
* Adapt header handling changes from other recyclerview adapters to fix issue #4475 in StatisticsPlaylistFragment

* Remove unneeded LayoutInflater

* Revert "Remove unneeded LayoutInflater"

This reverts commit ab73dc1e72.

* Revert "Adapt header handling changes from other recyclerview adapters to fix issue #4475 in StatisticsPlaylistFragment"

This reverts commit 2abe71cc98.

* Remove header animation causing view recycling issue
2025-05-06 17:07:45 +02:00
Profpatsch
088cb8353e Merge pull request #12256 from Profpatsch/improve-jitpack-workaraund-docs
build.gradle: Improve jitpack workaround doc & fix hash
2025-05-06 12:56:38 +02:00
Profpatsch
5ca544bc42 build.gradle: Improve jitpack workaround doc & fix hash 2025-05-06 10:48:20 +02:00
Stypox
aa1b7f8584 Merge pull request #12215 from naveensingh/fix-image-minimizer
Fix image minimizer pattern
2025-04-28 07:34:06 +02:00
Naveen Singh
ce16c6df5f Fix image minimizer pattern
Added non-capturing group that matches either:

 - `user-attachments/assets`
 - `owner/repo/assets/digits`
2025-04-27 19:35:31 -04:00
Stypox
276bf390b2 Merge pull request #12117 from malania02/dev
Show download date of downloaded videos
2025-04-11 20:17:27 +02:00
malania02
f39eda086f Fix for overlapping 2025-04-09 23:40:14 +02:00
Stypox
756327da39 Merge pull request #12093 from mileskrell/mileskrell/support-per-app-language-preferences
Support per-app language preferences
2025-04-08 23:13:07 +02:00
Stypox
5840d3a437 Merge pull request #12150 from FineFindus/fix/potoken-index
[YouTube] Access first element if array size is one
2025-04-08 23:06:04 +02:00
FineFindus
e1dedd45ed [YouTube] Access first element if array size is one
Fixes a regression, where if the challenge data array size was one, the second element
would be accessed, leading to a crash.
This was introduced when porting the challenge parsing from JS to
Kotlin.

Ref: 53b599b042
2025-04-02 22:14:01 +02:00
malania02
912f07a1dd Missing lines added 2025-03-30 14:50:05 +02:00
Miles Krell
205466c56a Move call to setApplicationLocales 2025-03-27 19:14:41 -04:00
Miles Krell
7f10312d0a Move migration to NewPipeSettings 2025-03-23 17:39:21 -04:00
malania02
63be3220e7 Show download date 2025-03-22 16:19:26 +01:00
malania02
536b78f2e6 textview for download date added 2025-03-22 16:13:45 +01:00
malania02
6d6b73ef73 textview for download date added 2025-03-22 16:09:58 +01:00
Stypox
196c27792b Merge pull request #12044 from TeamNewPipe/android-auto
Add support for Android Auto *(season 2)*
2025-03-21 11:21:58 +01:00
Stypox
b3789315ad Merge pull request #12104 from TeamNewPipe/update-npe
Update NewPipe Extractor and add new proguard rules
2025-03-21 10:52:37 +01:00
Miles Krell
c7bf498c04 Don't show toast because of changing content language or country 2025-03-16 20:27:05 -04:00
Miles Krell
35abb99dac Only show toast on Android <13 2025-03-16 20:15:38 -04:00
Miles Krell
70416e73f3 Move app language setting migration to SettingMigrations 2025-03-16 19:24:04 -04:00
TobiGr
a0b76c3385 Update NewPipe Extractor and add new proguard rules
New rules are required since Rhino and Rhino Engine 1.8.0
2025-03-16 22:08:10 +01:00
Tobi
c232193a46 Merge pull request #12083 from har-123/bugfix/11894_fix_duplicate_menu_options
Fix duplicate menu options in ChannelFragment
2025-03-16 10:34:52 +01:00
Siddhesh Naik
f289bea6b3 Fix sonar warning 2025-03-16 12:44:05 +05:30
Harshita
48b200868a BF-11894 : Fix the menu disappearing on performing backGesture 2025-03-16 12:44:05 +05:30
Harshita
54bf7f0ced BF-11894 : Fix the Duplicate menu options in ChannelFragment 2025-03-16 12:44:05 +05:30
Miles Krell
980a35a708 Move migration to separate method 2025-03-15 23:00:31 -04:00
Miles Krell
da106e2361 Don't try to migrate "system" app language 2025-03-15 22:54:17 -04:00
Miles Krell
3532ac96b4 Migrate from pre-Android 13 app language pref 2025-03-15 22:13:01 -04:00
Miles Krell
87693a2ad1 Redirect to per-app language settings on Android 13+ 2025-03-15 21:56:02 -04:00
Hosted Weblate
d321e57620 Translated using Weblate (Czech)
Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Catalan)

Currently translated at 88.2% (653 of 740 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 90.4% (76 of 84 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Croatian)

Currently translated at 99.7% (738 of 740 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (739 of 740 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (French)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Greek)

Currently translated at 25.0% (21 of 84 strings)

Translated using Weblate (Greek)

Currently translated at 23.8% (20 of 84 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (French)

Currently translated at 99.5% (737 of 740 strings)

Co-authored-by: 439JBYL80IGQTF25UXNR0X1BG <439JBYL80IGQTF25UXNR0X1BG@users.noreply.hosted.weblate.org>
Co-authored-by: Andrey F <firsan777@mail.ru>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Antonin Del Fabbro <message@antonin.one>
Co-authored-by: Christian Eichert <c@zp1.net>
Co-authored-by: Drugi Sapog <dindrugi@users.noreply.hosted.weblate.org>
Co-authored-by: Eduardo Calixto <eduardogubertcalixto@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jan Layola <gilajan@protonmail.com>
Co-authored-by: Kevin Wang <wmk153024@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Petr Kadlec <mormegil@centrum.cz>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sergio Marques <so.boston.android@gmail.com>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: XxVictoriaxX <evakonoob@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: whistlingwoods <72640314+whistlingwoods@users.noreply.github.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/el/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2025-03-15 17:43:36 +01:00
Tobi
fb4a65a14a Merge pull request #12043 from TeamNewPipe/hide-view-logs
Disable logs about view animations by default
2025-03-15 17:17:59 +01:00
Stypox
3047704e1c Merge pull request #12089 from mileskrell/mileskrell/fix-audio-track-labels
Disambiguate audio track labels
2025-03-15 12:45:20 +01:00
Stypox
3dcfdaf510 Merge pull request #12065 from tfga/YouTubeTemporaryPlaylist
Share as YouTube temporary playlist
2025-03-15 10:11:59 +01:00
Thiago F. G. Albuquerque
2ceb70236e sharePlaylist(): converting javadoc from Markdown back to "classic javadoc"
(request from @Stypox)
2025-03-14 21:56:42 -03:00
Thiago F. G. Albuquerque
be097f26c8 Deleting the "explanatory text" bellow the title
<string name="share_playlist_with_titles_message">Share playlist with details such as playlist name and video titles or as a simple list of video URLs</string>
    Share playlist with details such as playlist name and video titles or as a simple list of video URLs</string>

(Discussion: https://github.com/TeamNewPipe/NewPipe/pull/12065#discussion_r1994349485)
2025-03-13 19:10:26 -03:00
Thiago F. G. Albuquerque
098f60d593 Don't add the title when sharing as YouTube temp playlist 2025-03-13 18:16:09 -03:00
Thiago F. G. Albuquerque
eb0568044a R.string.share_playlist_as_youtube_temporary_playlist: pt-BR
+ Minor fixes to related translations
2025-03-12 19:09:31 -03:00
Thiago F. G. Albuquerque
f3b3d5c3e7 R.string.share_playlist_as_youtube_temporary_playlist 2025-03-12 19:08:09 -03:00
Miles Krell
b888dc72cf Support per-app language preferences 2025-03-11 23:29:23 -04:00
Thiago F. G. Albuquerque
599d86151a Making ktLint happy 2025-03-11 21:26:58 -03:00
tfga
587df093ea YouTube video IDs are 11 characters long
Co-authored-by: Stypox <stypox@pm.me>
2025-03-11 20:35:41 -03:00
tfga
8830e87242 YouTube video IDs are 11 characters long
Co-authored-by: Stypox <stypox@pm.me>
2025-03-11 20:35:18 -03:00
Thiago F. G. Albuquerque
f96b8f7b2a Comment: maximum length of 50 items
(PR review from @Stypox)
2025-03-11 20:19:54 -03:00
Thiago F. G. Albuquerque
c28478ae53 getYouTubeId(): Changing implementation to use YoutubeStreamLinkHandler
(PR review from @Stypox)
2025-03-11 20:12:25 -03:00
Miles Krell
10110397fd Use display name instead of only the language 2025-03-10 22:01:09 -04:00
tfga
d81244e77c YT temp playlist URL: http => https
Co-authored-by: Stypox <stypox@pm.me>
2025-03-10 19:11:20 -03:00
Stypox
ea20ca9e72 Merge pull request #12067 from Isira-Seneviratne/Fix-notification-grouping
Fix stream notification grouping
2025-02-28 11:51:11 +01:00
Isira Seneviratne
f0c89494dd Fix stream notification grouping 2025-02-27 09:15:40 +05:30
Thiago F. G. Albuquerque
0fd2d4fed6 [#11930] Removing Apache Commons Collections
It's no longer needed after the conversion to Kotlin.
2025-02-26 21:29:48 -03:00
Thiago F. G. Albuquerque
3c7b026d7d [#11930] Updating javadoc 2025-02-25 20:23:07 -03:00
Thiago F. G. Albuquerque
998d84de6c [#11930] Converting to Kotlin 2025-02-25 18:56:12 -03:00
Thiago F. G. Albuquerque
76a02d5858 [#11930] Extracting to a separate file 2025-02-24 20:16:40 -03:00
Thiago F. G. Albuquerque
24bb71a23f [#11930] Making it more efficient: Reverse iteration + limit(50) + reverse 2025-02-24 19:22:36 -03:00
Stypox
49b71942ad Fix style and add comment about null player 2025-02-24 14:21:05 +01:00
Thompson3142
c9ec257a5e Ugly fix for broken text colors in dark mode (#12035)
* Ugly fix for broken text colors in dark mode

* Add comment for clarification

* Added error prevention

* Update app/src/main/java/org/schabi/newpipe/MainActivity.java

---------

Co-authored-by: Stypox <stypox@pm.me>
2025-02-21 09:38:58 +00:00
Thiago F. G. Albuquerque
b1f995a78c [#11930] Playlist with more than 50 items 2025-02-20 16:26:03 -03:00
Thiago F. G. Albuquerque
acac50a1d1 [#11930] Non-Youtube URLs should be ignored 2025-02-19 16:29:34 -03:00
Thiago F. G. Albuquerque
c6b87cd316 [#11930] Making CheckStyle happy 2025-02-18 20:59:13 -03:00
Thiago F. G. Albuquerque
94d4c21cc7 [#11930] @Test export_justUrls() 2025-02-18 17:47:22 -03:00
Stypox
a7a7dc5363 Handle player and player service separately
This is, again, a consequence of the commit "Drop some assumptions on how PlayerService is started and reused".
This commit notified VideoDetailFragment of player starting and stopping independently of the player.
Read the comments in the code changes for more information.
2025-02-18 19:27:46 +01:00
Stypox
126f4b0e30 Fix crash when closing video detail fragment
This bug started appearing because the way to close the player is now unified in PlayerHolder.stopService(), which causes the player to reach back to the video detail fragment with a notification of the shutdown (i.e. onServiceStopped() is called). This is fixed by adding a nullability check on the binding.
2025-02-18 18:03:10 +01:00
Stypox
6558794d26 Try to bind to PlayerService when MainActivity starts
Fixes mini-player not appearing on app start if the player service is already playing something.

The PlayerService (and the player) may be started from an external intent that does not involve the MainActivity (e.g. RouterActivity or Android Auto's media browser interface).
This PR tries to bind to the PlayerService as soon as the MainActivity starts, but only does so in a passive way, i.e. if the service is not already running it is not started.
Once the connection between PlayerHolder and PlayerService is setup, the ACTION_PLAYER_STARTED broadcast is sent to MainActivity so that it can setup the bottom mini-player.
Another important thing this commit does is to check whether the player is open before actually adding the mini-player view, since the PlayerService could be bound even without a running player (e.g. Android Auto's media browser is being used). This is a consequence of commit "Drop some assumptions on how PlayerService is started and reused".
2025-02-18 17:49:38 +01:00
Stypox
1d12874937 Merge pull request #12046 from TobiGr/weblate
Update translations
2025-02-16 22:01:41 +01:00
Stypox
1d98518bfa Fix loading remote playlists in media browser 2025-02-16 21:44:50 +01:00
Stypox
e5458bcb14 Properly handle item errors during media browser loading
Non-item errors, i.e. critical parsing errors of the page, are still handled properly.
2025-02-16 21:44:50 +01:00
Stypox
dc62d211f5 Properly stop PlayerService
This commit is a consequence of the commit "Drop some assumptions on how PlayerService is started and reused". Since the assumptions on how the PlayerService is started and reused have changed, we also need to adapt the way it is stopped. This means allowing the service to remain alive even after the player is destroyed, in case the system is still accessing PlayerService e.g. through the media browser interface. The foreground service needs to be stopped and the notification removed in any case.
2025-02-16 21:44:49 +01:00
Stypox
ec6612dd71 Call exoPlayer.prepare() on PlaybackPreparer.onPrepare()
If a playbackPreparer is set, then instead of calling `player.prepare()`, the MediaSessionConnector will call `playbackPreparer.onPrepare(true)` instead, as seen below.
This commit makes it so that playbackPreparer.onPrepare(true) restores the original behavior of just calling player.prepare().

From MediaSessionConnector -> MediaSessionCompat.Callback implementation:
```java
    @Override
    public void onPlay() {
      if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
        if (player.getPlaybackState() == Player.STATE_IDLE) {
          if (playbackPreparer != null) {
            playbackPreparer.onPrepare(/* playWhenReady= */ true);
          } else {
            player.prepare();
          }
        } else if (player.getPlaybackState() == Player.STATE_ENDED) {
          seekTo(player, player.getCurrentMediaItemIndex(), C.TIME_UNSET);
        }
        Assertions.checkNotNull(player).play();
      }
    }
```
2025-02-16 21:44:49 +01:00
Stypox
064e1d39c7 Use the media browser implementation in PlayerService
Now the media browser queries are replied to by MediaBrowserImpl

Co-authored-by: Haggai Eran <haggai.eran@gmail.com>
2025-02-16 21:44:05 +01:00
Stypox
4c88a193bd Add MediaBrowserImpl
This class implements the media browser service interface as a standalone class for clearer separation of concerns (otherwise everything would need to go in PlayerService, since PlayerService overrides MediaBrowserServiceCompat)

Co-authored-by: Haggai Eran <haggai.eran@gmail.com>
Co-authored-by: Profpatsch <mail@profpatsch.de>
2025-02-16 21:43:46 +01:00
Stypox
3fcac10e7f Add MediaBrowserPlaybackPreparer
This class will receive the media URLs generated by [MediaBrowserImpl] and will start playback of the corresponding streams or playlists.

Co-authored-by: Haggai Eran <haggai.eran@gmail.com>
Co-authored-by: Profpatsch <mail@profpatsch.de>
2025-02-16 21:43:35 +01:00
Stypox
6cedd117fe Add StreamHistoryEntry.toStreamInfoItem()
Co-authored-by: Haggai Eran <haggai.eran@gmail.com>
2025-02-16 21:40:55 +01:00
Stypox
5eabcb52b5 Add getThumbnailUrl() to PlaylistLocalItem interface
Co-authored-by: Haggai Eran <haggai.eran@gmail.com>
2025-02-16 21:40:48 +01:00
Stypox
690b40d0c4 Allow creating PlayQueue from ListInfo and index 2025-02-16 21:40:47 +01:00
Stypox
9bb2c0b484 Add getPlaylist(id) to RemotePlaylistManager
Co-authored-by: Haggai Eran <haggai.eran@gmail.com>
2025-02-16 21:40:36 +01:00
Stypox
1e08cc8c8f Add MediaBrowserCommon with info item's and pages' IDs
Co-authored-by: Haggai Eran <haggai.eran@gmail.com>
2025-02-16 21:40:29 +01:00
Stypox
7d17468266 Instantiate media session and connector in PlayerService
This changes significantly how the MediaSessionCompat and MediaSessionConnector objects are used:
- now they are tied to the service and not to the player, and so they might be reused with multiple players (which should be allowed)
- now they can exist even if there is no player (which is fundamental to be able to answer media browser queries)
2025-02-16 21:40:13 +01:00
Stypox
5819546ea9 Have PlayerService implement MediaBrowserServiceCompat
Co-authored-by: Haggai Eran <haggai.eran@gmail.com>
2025-02-16 21:36:59 +01:00
Stypox
cfb6e114d6 Disable logs about view animations by default 2025-02-16 10:31:42 +01:00
Stypox
b764ad33c4 Drop some assumptions on how PlayerService is started and reused
Read the comments in the lines changed to understand more
2025-02-15 17:48:19 +01:00
Hosted Weblate
430b4eb916 Translated using Weblate (Persian)
Currently translated at 92.7% (686 of 740 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Georgian)

Currently translated at 83.3% (70 of 84 strings)

Translated using Weblate (Estonian)

Currently translated at 16.6% (14 of 84 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Mainfränkisch)

Currently translated at 1.0% (8 of 740 strings)

Translated using Weblate (Bavarian)

Currently translated at 3.9% (29 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (84 of 84 strings)

Added translation using Weblate (Mainfränkisch)

Translated using Weblate (Thai)

Currently translated at 36.6% (271 of 740 strings)

Translated using Weblate (Armenian)

Currently translated at 28.2% (209 of 740 strings)

Translated using Weblate (Georgian)

Currently translated at 85.7% (72 of 84 strings)

Translated using Weblate (Thai)

Currently translated at 34.3% (254 of 740 strings)

Translated using Weblate (Gujarati)

Currently translated at 11.3% (84 of 740 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Nepali)

Currently translated at 1.1% (1 of 84 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (French)

Currently translated at 100.0% (84 of 84 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Gujarati)

Currently translated at 11.0% (82 of 740 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (740 of 740 strings)

Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Davit Mayilyan <davit.mayilyan@protonmail.ch>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Garfield2150 <knd.garfield@gmail.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Goudarz Jafari <goudarz.jafari@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kchenik Poudel <Kakapoudel7@gmail.com>
Co-authored-by: Kuko <kuko7@protonmail.ch>
Co-authored-by: Paul Sibila <p.aul@mail.de>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Temuri Doghonadze <temuri.doghonadze@gmail.com>
Co-authored-by: freddyLovesUs <compteperso@outlook.com>
Co-authored-by: રાજ ભાતેલીઆ <rajbhatelia@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/et/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ka/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ne/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translation: NewPipe/Metadata
2025-02-15 13:08:00 +01:00
Thiago F. G. Albuquerque
2339f51ad4 [#11930] Share as YouTube temporary playlist
Initial commit.
2025-02-14 21:14:42 -03:00
Stypox
c6e1721884 Add translated changelogs for v0.27.6 (1003)
Copied from 1002.txt
2025-02-05 11:30:37 +01:00
Stypox
94684fe380 Merge branch 'weblate-dev' into dev 2025-02-05 11:29:14 +01:00
Hosted Weblate
398a2f55ce Merge branch 'origin/dev' into Weblate. 2025-02-05 11:28:09 +01:00
Stypox
1f7b3b5b06 Add changelog for v0.27.6 (1003) 2025-02-05 11:25:58 +01:00
Stypox
909ed616c4 Hotfix release v0.27.6 (1003) 2025-02-05 11:14:17 +01:00
Stypox
dd223af28d Merge pull request #11955 from Stypox/po-token
[YouTube] Add support for poTokens
2025-02-05 10:52:16 +01:00
Stypox
dbee8d8128 Update NewPipeExtractor to v0.24.5
Using commit 9f83b385a since JitPack is buggy...
2025-02-05 10:24:34 +01:00
Stypox
b62a09b5b3 Use WebSettingsCompat.setSafeBrowsingEnabled 2025-02-04 21:50:10 +01:00
Stypox
87317c6faf Reorder functions in PoTokenWebView 2025-02-04 21:38:01 +01:00
Stypox
53b599b042 Make JavaScript code compatible with older WebViews 2025-02-04 21:38:01 +01:00
Stypox
21df24abfd Detect when WebView is broken and return null poToken
Some old Android devices have a broken WebView implementation, that can't execute the poToken code. This is now detected and the getWebClientPoToken return null instead of throwing an error in such a case, to allow the extractor to try to extract the video data even without a poToken.
2025-02-04 11:22:50 +01:00
Hosted Weblate
ca4592a935 Translated using Weblate (Russian)
Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Estonian)

Currently translated at 13.2% (11 of 83 strings)

Translated using Weblate (Latin)

Currently translated at 8.6% (64 of 740 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (83 of 83 strings)

Translated using Weblate (Turkish)

Currently translated at 48.1% (40 of 83 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 96.3% (80 of 83 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (83 of 83 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Vietnamese)

Currently translated at 78.3% (65 of 83 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (83 of 83 strings)

Translated using Weblate (German)

Currently translated at 100.0% (83 of 83 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (83 of 83 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (83 of 83 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (739 of 740 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (740 of 740 strings)

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

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (N’Ko)

Currently translated at 89.4% (662 of 740 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 19.8% (147 of 740 strings)

Translated using Weblate (Georgian)

Currently translated at 89.1% (660 of 740 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.0% (733 of 740 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 65.4% (484 of 740 strings)

Translated using Weblate (Somali)

Currently translated at 75.1% (556 of 740 strings)

Translated using Weblate (Uzbek (Latin script))

Currently translated at 62.0% (459 of 740 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 7.4% (55 of 740 strings)

Translated using Weblate (Odia)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Santali)

Currently translated at 14.5% (108 of 740 strings)

Translated using Weblate (Bengali)

Currently translated at 76.7% (568 of 740 strings)

Translated using Weblate (Sardinian)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Bengali (India))

Currently translated at 40.1% (297 of 740 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 84.0% (622 of 740 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 97.7% (723 of 740 strings)

Translated using Weblate (Malayalam)

Currently translated at 76.4% (566 of 740 strings)

Translated using Weblate (Interlingua)

Currently translated at 32.2% (239 of 740 strings)

Translated using Weblate (Filipino)

Currently translated at 31.3% (232 of 740 strings)

Translated using Weblate (Thai)

Currently translated at 30.0% (222 of 740 strings)

Translated using Weblate (Nepali)

Currently translated at 59.0% (437 of 740 strings)

Translated using Weblate (Danish)

Currently translated at 99.8% (739 of 740 strings)

Translated using Weblate (Galician)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Malay)

Currently translated at 57.9% (429 of 740 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 94.0% (696 of 740 strings)

Translated using Weblate (Estonian)

Currently translated at 99.7% (738 of 740 strings)

Translated using Weblate (Punjabi)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Albanian)

Currently translated at 79.8% (591 of 740 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 75.1% (556 of 740 strings)

Translated using Weblate (Urdu)

Currently translated at 68.2% (505 of 740 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Catalan)

Currently translated at 87.0% (644 of 740 strings)

Translated using Weblate (Kurdish)

Currently translated at 63.7% (472 of 740 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.7% (738 of 740 strings)

Translated using Weblate (Telugu)

Currently translated at 58.1% (430 of 740 strings)

Translated using Weblate (Hindi)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Finnish)

Currently translated at 97.9% (725 of 740 strings)

Translated using Weblate (Croatian)

Currently translated at 98.9% (732 of 740 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Lithuanian)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Swedish)

Currently translated at 99.8% (739 of 740 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 54.3% (402 of 740 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (738 of 740 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (738 of 740 strings)

Translated using Weblate (Asturian)

Currently translated at 63.3% (469 of 740 strings)

Translated using Weblate (Persian)

Currently translated at 92.4% (684 of 740 strings)

Translated using Weblate (Polish)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Turkish)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (738 of 740 strings)

Translated using Weblate (Arabic)

Currently translated at 99.7% (738 of 740 strings)

Translated using Weblate (Czech)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Esperanto)

Currently translated at 71.4% (529 of 740 strings)

Translated using Weblate (Slovak)

Currently translated at 99.8% (739 of 740 strings)

Translated using Weblate (Romanian)

Currently translated at 99.8% (739 of 740 strings)

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

Currently translated at 99.3% (735 of 740 strings)

Translated using Weblate (Basque)

Currently translated at 99.8% (739 of 740 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (738 of 740 strings)

Translated using Weblate (Korean)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Serbian)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Dutch)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (738 of 740 strings)

Translated using Weblate (German)

Currently translated at 99.5% (737 of 740 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (French)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Macedonian)

Currently translated at 6.0% (5 of 82 strings)

Translated using Weblate (Macedonian)

Currently translated at 80.6% (597 of 740 strings)

Translated using Weblate (French)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (French)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (French)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Esperanto)

Currently translated at 71.7% (531 of 740 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Kabyle)

Currently translated at 28.7% (213 of 740 strings)

Translated using Weblate (Estonian)

Currently translated at 12.1% (10 of 82 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Gujarati)

Currently translated at 11.0% (82 of 740 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Estonian)

Currently translated at 10.9% (9 of 82 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (French)

Currently translated at 98.7% (81 of 82 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (French)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Undetermined)

Currently translated at 2.4% (2 of 82 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (82 of 82 strings)

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

Currently translated at 28.0% (23 of 82 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Gujarati)

Currently translated at 9.4% (70 of 740 strings)

Translated using Weblate (Estonian)

Currently translated at 9.7% (8 of 82 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Hungarian)

Currently translated at 74.3% (61 of 82 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (81 of 82 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hungarian)

Currently translated at 64.6% (53 of 82 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (80 of 82 strings)

Translated using Weblate (German)

Currently translated at 100.0% (82 of 82 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (740 of 740 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Andrey F <firsan777@mail.ru>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Anthony Romero <dagazcii@gmail.com>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Bảo Nam (Namm) <namb20994@gmail.com>
Co-authored-by: C. Rüdinger <Mail-an-CR@web.de>
Co-authored-by: Ding User <dengus@users.noreply.hosted.weblate.org>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: GeoCup <geokapaniaris@gmail.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: H Tamás <hovanszki@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jaidyn Ann <jadedctrl@posteo.net>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: MatthieuPh <matthieu.philippe@protonmail.com>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Miguel <mp0187595@tutamail.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nicolas SALMIERI <1salmieri.nicolas@gmail.com>
Co-authored-by: NormalRandomPeople <normal.scribe833@silomails.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Rijolo <rijolo4790@gholar.com>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Szia Tomi <sziatomi01@gmail.com>
Co-authored-by: TobiGr <TobiGr@users.noreply.github.com>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: VisionR1 <25982450+VisionR1@users.noreply.github.com>
Co-authored-by: Vtrytobe <vtrytobe@gmail.com>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gfbdrgng <hnaofegnp@hldrive.com>
Co-authored-by: hajayad577 <hajayad577@numerobo.com>
Co-authored-by: jpkaster 77 <jpkaster81@gmail.com>
Co-authored-by: polarwood <wreckfitzgerald@proton.me>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: yummysheepouo <jerry88182821@gmail.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Валентин Барсуков <valikbars04@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Co-authored-by: મેબીરાજ <rajbhatelia@gmail.com>
Co-authored-by: રાજ ભાતેલીઆ <rajbhatelia@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
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/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/et/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/mk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ta/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/und/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2025-02-04 10:59:39 +01:00
Stypox
3fc487310b Use Runnable instead of () -> Unit if converted to Runnable anyway 2025-02-04 10:23:45 +01:00
Stypox
056809cb0d Use "this" instead of "globalThis" as global scope
globalThis was introduced only on newer versions of JS
2025-02-04 10:22:10 +01:00
AudricV
a60bb3e7af [YouTube] Change BotGuard endpoint to youtube.com's one
This prevents non-abilities to fetch BotGuard challenge and send its
result with the jnn-pa.googleapis.com domain (domain block like done
on Pi-hole lists or DNS servers).

That's what the official website uses to send the challenge execution
result, however it uses InnerTube to fetch the challenge. Embeds
still use the jnn-pa.googleapis.com domain.

Also rename the makeJnnPaGoogleapisRequest method appropriately.
2025-02-03 13:05:39 +01:00
AudricV
ecd3f6c2ee [YouTube] Clarify BotGuard API key's origin and disable related Sonar warning 2025-02-01 15:40:16 +01:00
AudricV
70ff47b810 [YouTube] Get visitorData from the service to get valid responses 2025-02-01 15:39:07 +01:00
AudricV
b8e050f6c4 Adapt YoutubeHttpDataSource to extractor changes and improve requests
Always use POST requests and the same body that official HTML5 clients
use for a while.
2025-01-31 22:50:10 +01:00
AudricV
46d0bc1004 Update NewPipeExtractor 2025-01-31 22:28:08 +01:00
Stypox
e7fe84f2c7 Make sure downloadAndRunBotguard() is called after <script> loaded 2025-01-31 21:47:46 +01:00
Stypox
2b183a0576 Wrap logs in BuildConfig.DEBUG 2025-01-31 21:47:46 +01:00
Stypox
f856bd9306 Recreate poToken generator if current is broken
This will be tried only once, and afterwards an error will be thrown
2025-01-31 21:47:45 +01:00
Stypox
0066b322e1 Unify running on main thread 2025-01-31 21:47:45 +01:00
Stypox
3bdae81c0a Fix checkstyle 2025-01-31 21:47:45 +01:00
Stypox
6010c4ea7f Connect poToken generation to extractor 2025-01-31 21:47:45 +01:00
Stypox
690b3410e9 Interfaces for poTokens + WebView implementation 2025-01-31 21:47:44 +01:00
Profpatsch
ba86ce137b Merge pull request #11969 from neosis91/dev
DownloaderImpl: Auto-close resources and simplify header setting
2025-01-31 15:56:39 +01:00
Bertrand Jaunet
410c01547c DownloaderImpl: Auto-close resources and simplify header setting
The headers should be overwritten in the same way, based on how
`.header` is the same as `.removeHeader().addHeader()`.

We weren’t closing the request resources after using them, potentially
leaking file handles. This will add autoclosing for both the request
and the body objects.
2025-01-31 12:36:27 +01:00
Stypox
47263f5254 Merge pull request #11959 from Stypox/fix-loading-stream-twice
Fix loading StreamInfo twice on first VideoDetailFragment opening
2025-01-27 14:56:51 +01:00
Stypox
01bf855015 Fix naming in VideoDetailFragment: video->stream, videoUrl->url 2025-01-27 14:52:35 +01:00
Profpatsch
ebf3008729 Merge pull request #11870 from TeamNewPipe/sidebar_donations
Add link to donation page on app drawer
2025-01-27 13:59:29 +01:00
Christian Schabesberger
33ecfb757e Sidebar: Add donation link to app drawer
This creates a donation link that leads to our donation page on the
NewPipe website.
2025-01-27 13:43:34 +01:00
Stypox
ffe26d882b Fix loading StreamInfo twice on first VideoDetailFragment opening 2025-01-26 12:39:07 +01:00
Stypox
83f8141fe7 Merge pull request #11806 from Thompson3142/fix_subtitle_size
Fix caption sizes not being changed
2025-01-25 18:10:56 +01:00
Profpatsch
9253640fae Merge pull request #11887 from Nikunj-Aggarwal/bg-iso-timestamp
Convert error report timestamps to ISO format
2025-01-23 19:51:18 +01:00
Stypox
8b5aa5cd9b Merge branch 'master' into dev 2025-01-22 11:10:22 +01:00
Stypox
58393ad4ef Release v0.27.5 (1002) 2025-01-21 23:34:42 +01:00
Stypox
977f7e28b5 Add changelogs for hotfix release v0.27.5 (1002) 2025-01-21 23:34:12 +01:00
Stypox
99e77249de Update NewPipeExtractor to v0.24.4 2025-01-21 23:19:49 +01:00
Profpatsch
a955408053 Merge pull request #11928 from LeMeuble/bug-checksum-deleted-file
Fix the issue of getting the checksum of a removed file
2025-01-21 17:40:50 +01:00
Thompson3142
86203d6800 MainPlayer/PopupPlayer: Use system settings for subtitle size
This will use the exact subtitle sizes the user requested, both for
the main and the popup player. They will always be the same fraction
of the video, even if the popup player is resized.
2025-01-21 17:23:08 +01:00
Profpatsch
edd19641ac ErrorActivity: add Timestamp and Package/Service to markdown export
These were displayed in the UI, but not added into the markdown export
string.
2025-01-21 16:25:54 +01:00
Nikunj-Aggarwal
65749cbac0 ErrorActivity: Use a proper zoned ISO timestamp
Will have a timezone offset and be parsable as valid ISO8601
timestamp.

Also change the label in the UI to just say “Timestamp”
2025-01-21 16:24:07 +01:00
LeMeuble
658ddfc921 Fix issue of checksum for removed file 2025-01-16 10:46:25 +01:00
Stypox
f7d0fd545d Merge pull request #11879 from tom93/pr/fix-image-minimizer-multiple-images
Fix image-minimizer on lines containing multiple images
2025-01-04 09:34:07 +01:00
Tom Levy
27e6be792f Fix image-minimizer on lines containing multiple images 2025-01-04 08:15:44 +00:00
Stypox
3fc0147f47 Merge pull request #11784 from Rishi2003Das/typo_change
Correct a Typo in Contributing.md
2024-12-17 10:42:34 +01:00
Rishi Das
c6b05c6094 Update CONTRIBUTING.md 2024-12-08 02:00:41 +05:30
Rishi Das
240a2fe36b Update CONTRIBUTING.md 2024-12-08 02:00:04 +05:30
Rishi Das
de46e3abb3 Update CONTRIBUTING.md 2024-12-08 01:59:14 +05:30
Stypox
70748fa0bc Use JDK 21 in build-release-apk.yml
See https://github.com/TeamNewPipe/NewPipe/issues/11754
2024-12-02 13:49:30 +01:00
Stypox
3847b32c11 Release v0.27.4 (1001)
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
2024-11-30 15:11:23 +01:00
Stypox
9054575f6c Add changelog for v0.27.4 (1001) 2024-11-30 15:10:38 +01:00
Stypox
0dca92dd59 Merge branch 'master' into dev 2024-11-30 14:55:31 +01:00
Hosted Weblate
b19cd00dba Translated using Weblate (Malay)
Currently translated at 9.8% (8 of 81 strings)

Translated using Weblate (Malay)

Currently translated at 57.9% (429 of 740 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 96.2% (78 of 81 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 64.1% (52 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 50.6% (41 of 81 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 4.9% (4 of 81 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 32.0% (26 of 81 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Turkish)

Currently translated at 46.9% (38 of 81 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Japanese)

Currently translated at 12.3% (10 of 81 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (740 of 740 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Aliberk Sandıkçı <git@aliberksandikci.com.tr>
Co-authored-by: Dampuzakura <dampuzakura@users.noreply.hosted.weblate.org>
Co-authored-by: Eder Etxebarria Rojo <eder@betxepare.eus>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: H Tamás <hovanszki@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: LeoL <leonardo.lapa.04@protonmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Shafiq Jamzuri <shafiq.joe@yandex.com>
Co-authored-by: ShareASmile <ShareASmile@users.noreply.hosted.weblate.org>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar_LY/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ja/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ms/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translation: NewPipe/Metadata
2024-11-30 14:55:05 +01:00
Stypox
88d8d90bbd Merge pull request #11765 from Stypox/release-workflow
Add build-release-apk workflow
2024-11-30 14:53:16 +01:00
Stypox
c569f08a32 Add build-release-apk workflow 2024-11-30 13:39:18 +01:00
Stypox
246fc034c1 Add build-release-apk workflow 2024-11-30 13:29:38 +01:00
Stypox
52942ffd30 Merge pull request #11738 from cillyvms/a13-player-notifs
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Always allow changing player notification preferences on Android 13+
2024-11-27 19:12:19 +01:00
Stypox
e4b0245530 Merge pull request #11734 from Thompson3142/fix_timestamp_popup_time
Fix player resuming from start when clicking on a timestamp
2024-11-27 18:38:49 +01:00
Tobi
c6b8bcf0f4 Merge pull request #11745 from Stypox/truncate-before-export
Fix downloading/exporting when overwriting file would not truncate
2024-11-27 17:37:53 +01:00
Stypox
e31a8ad7a2 Mock openAndTruncateStream instead of getStream in test 2024-11-27 16:37:25 +01:00
Stypox
b21981a9c7 Add comments to explain why openAndTruncateStream() 2024-11-27 16:34:50 +01:00
Thompson3142
f9711a3402 Removed call to setRecovery() entirely 2024-11-24 22:12:25 +01:00
Stypox
df941670a8 Fix downloading/exporting when overwriting file would not truncate 2024-11-24 18:36:54 +01:00
Stypox
57e66b17c6 Merge branch 'master' into dev
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
2024-11-24 17:43:45 +01:00
Stypox
d298a12533 Merge pull request #11712 from TeamNewPipe/release-0.27.3
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Release v0.27.3 (1000)
2024-11-24 17:41:05 +01:00
Hosted Weblate
a79bc3db14 Translated using Weblate (Italian)
Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 28.3% (23 of 81 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (735 of 739 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 23.4% (19 of 81 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 23.4% (19 of 81 strings)

Translated using Weblate (Punjabi (Pakistan))

Currently translated at 16.9% (125 of 739 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 66.6% (54 of 81 strings)

Translated using Weblate (Albanian)

Currently translated at 1.2% (1 of 81 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Polish)

Currently translated at 60.4% (49 of 81 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 19.7% (16 of 81 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (German)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (German)

Currently translated at 100.0% (739 of 739 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: D <dici.handy@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: H Tamás <hovanszki@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: ShareASmile <ShareASmile@users.noreply.hosted.weblate.org>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Szymon Siemieniuk <szymonsiemieniuk01@gmail.com>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: Vladi69 <vladimirogalante@yahoo.it>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
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/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sq/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translation: NewPipe/Metadata
2024-11-24 17:32:32 +01:00
Stypox
661e6155c1 Update NewPipeExtractor to v0.24.3 2024-11-24 17:32:27 +01:00
Stypox
12558172d1 Merge pull request #11714 from AudricV/yt_more-audio-track-types-support
Add support for secondary audio track type
2024-11-24 17:01:03 +01:00
AudricV
dc3f55674f Add support for secondary audio track type 2024-11-24 16:43:22 +01:00
Stypox
acf2e88cb3 Merge pull request #11743 from TeamNewPipe/slower-feed
Throttle feed loading to avoid YouTube rate limits
2024-11-24 16:35:13 +01:00
Stypox
726c12e934 Only throttle YouTube feed loading 2024-11-24 16:22:19 +01:00
Stypox
33b96d238a Throttle loading subscriptions feed to avoid YouTube rate limits 2024-11-24 14:06:53 +01:00
cillyvms
213f49f5c4 Allow changing player notification preferences regardless of system settings on Android 13 and above. 2024-11-22 14:21:46 +01:00
Thompson3142
16c79c8219 Fixed player resuming from start when clicking on a timestamp 2024-11-21 22:42:42 +01:00
ShareASmile
14081505cd Update backup and restore explanation & improve hindi, punjabi and assamese READMEs (#11243)
* update backup and restore explanation in punjabi README

* Update backup and restore explanation in hindi README

* add_matrix_link to hindi and punjabi README

also translate Warning in hindi & punjabi language Readme's

* improve hindi and punjabi readme

add missing link #supported-services in hindi readme (that is #समर्थित-सेवाएँ}
improve translation of supported services in punjabi
Use Fdroid Hindi badge instead of english in hindi readme

* revert translate Warning in hindi & punjabi language Readme's

* update backup and restore explanation in assamese README

* fix assamese readme librapay donate button not showing and fix weird formating

* add matrix chat link to assamese readme & fix Newpipe logo not showing

* Update Matrix room URL to new link

oh! I missed this one earlier

* remove references to Bitcoin and Bountysource donation options in hindi readme

* more improvements in punjabi README

* fix CONTRIBUTING.md link in punjabi readme

* fix CONTRIBUTING.md link in assamese readme

* add missing paragraphs in hindi translation for hi readme

* revert localisation of app name NewPipe as it stands out

* address the review and place supported-services at correct place in hindi readme

do required changes for punjabi
do much needed improvements in assamese readme

* fix formatting issues in assamese readme

* fix link to releases in punjabi readme

* resolve conflicts
2024-11-20 10:42:29 +01:00
Tobi
ebd4880188 Merge pull request #10969 from yosrinajar/Read-Me-Translation
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Readme translation to arabic
2024-11-19 14:38:52 +01:00
Profpatsch
ffcba175ff Merge pull request #11330 from Isira-Seneviratne/Java-10-URL-NP
Apply URL encode/decode changes
2024-11-19 14:05:04 +01:00
Isira Seneviratne
c7848e5e86 Apply URL encode/decode changes 2024-11-19 13:17:10 +01:00
yosrinajar
6d686b93cb fixed all readme files 2024-11-19 12:17:25 +01:00
yosrinajar
2cc38f59d3 Readme translation to arabic 2024-11-19 12:16:30 +01:00
Stypox
8bf24e6b14 Merge branch 'dev' into release-0.27.3 2024-11-18 17:09:27 +01:00
Stypox
10e7a5cf9c Merge pull request #11268 from TeamNewPipe/user-agent
Update user agent to Firefox ESR 128
2024-11-18 17:06:31 +01:00
Stypox
9f2f219613 Merge branch 'dev' into release-0.27.3 2024-11-18 17:01:58 +01:00
Stypox
841471bf85 Merge pull request #10892 from KaGaster/el-koko
update README.fr.md
2024-11-18 16:59:50 +01:00
Stypox
06d25b0310 Merge pull request #11244 from Isira-Seneviratne/Android-elapsed-time
Use Android's elapsed time formatting
2024-11-18 16:56:41 +01:00
Mohamed Kooli
3c8d81a3c2 add README.fr.md 2024-11-18 16:55:23 +01:00
Stypox
cf870add49 Release v0.27.3 (1000) 2024-11-17 20:45:45 +01:00
Stypox
a962e6d633 Add changelog for v0.27.3 (1000) 2024-11-17 17:11:40 +01:00
Hosted Weblate
970ef9357b Merge branch 'origin/dev' into Weblate. 2024-11-17 16:58:51 +01:00
H Tamás
4ba961fe7a Translated using Weblate (Hungarian)
Currently translated at 18.7% (15 of 80 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
2024-11-17 16:58:43 +01:00
Stypox
e6c03bf4ac Merge pull request #11711 from Stypox/prepare-for-0.27.3
Actually fix playlist bookmark layout
2024-11-17 16:55:02 +01:00
Stypox
1f39523429 Update NewPipeExtractor 2024-11-16 14:17:37 +01:00
Stypox
b43031fb99 Ellipsize uploader text in playlist bookmark 2024-11-16 14:17:37 +01:00
Stypox
986cd52da0 Fix crash because of no height set on playlist bookmark
This is a consequence of https://github.com/TeamNewPipe/NewPipe/pull/11024

x
2024-11-16 14:17:32 +01:00
Hosted Weblate
bcd4579187 Translated using Weblate (Hebrew)
Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.3% (734 of 739 strings)

Translated using Weblate (Welsh)

Currently translated at 3.7% (3 of 80 strings)

Translated using Weblate (Bulgarian)

Currently translated at 5.0% (4 of 80 strings)

Added translation using Weblate (Welsh)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 99.7% (737 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 99.4% (735 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 98.1% (725 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 97.8% (723 of 739 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Galician)

Currently translated at 98.5% (728 of 739 strings)

Translated using Weblate (Burmese)

Currently translated at 2.9% (22 of 739 strings)

Translated using Weblate (Tagalog)

Currently translated at 8.1% (60 of 739 strings)

Translated using Weblate (French)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Tamil)

Currently translated at 23.7% (19 of 80 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Dutch)

Currently translated at 62.5% (50 of 80 strings)

Translated using Weblate (Persian)

Currently translated at 92.9% (687 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Catalan)

Currently translated at 87.1% (644 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 66.2% (53 of 80 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Albanian)

Currently translated at 79.8% (590 of 739 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (French)

Currently translated at 90.0% (72 of 80 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Telugu)

Currently translated at 58.5% (433 of 739 strings)

Translated using Weblate (Esperanto)

Currently translated at 70.2% (519 of 739 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 97.8% (723 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 17.5% (14 of 80 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 65.0% (52 of 80 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Tamil)

Currently translated at 25.0% (20 of 80 strings)

Translated using Weblate (Hungarian)

Currently translated at 18.7% (15 of 80 strings)

Translated using Weblate (Galician)

Currently translated at 98.3% (727 of 739 strings)

Translated using Weblate (Finnish)

Currently translated at 98.3% (727 of 739 strings)

Translated using Weblate (German)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 80.2% (593 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 18.7% (15 of 80 strings)

Translated using Weblate (Tamil)

Currently translated at 47.0% (348 of 739 strings)

Translated using Weblate (Tatar)

Currently translated at 6.4% (48 of 739 strings)

Added translation using Weblate (Tatar)

Translated using Weblate (Slovak)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Hungarian)

Currently translated at 16.2% (13 of 80 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Icelandic)

Currently translated at 97.5% (721 of 739 strings)

Translated using Weblate (Tamil)

Currently translated at 21.2% (17 of 80 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (German)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (French)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 97.1% (718 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (79 of 79 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 11.3% (9 of 79 strings)

Translated using Weblate (Finnish)

Currently translated at 11.3% (9 of 79 strings)

Translated using Weblate (German)

Currently translated at 100.0% (79 of 79 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Finnish)

Currently translated at 97.4% (720 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (79 of 79 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 96.6% (714 of 739 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (79 of 79 strings)

Translated using Weblate (Latvian)

Currently translated at 92.1% (681 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 91.0% (673 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Vietnamese)

Currently translated at 76.9% (60 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Tagalog)

Currently translated at 1.2% (1 of 78 strings)

Translated using Weblate (Latvian)

Currently translated at 87.1% (644 of 739 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Bulgarian)

Currently translated at 80.1% (592 of 739 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.3% (727 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Basque)

Currently translated at 42.3% (33 of 78 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Marathi)

Currently translated at 31.9% (236 of 739 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 66.0% (488 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (French)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Interlingua)

Currently translated at 32.4% (240 of 739 strings)

Translated using Weblate (Mongolian)

Currently translated at 5.5% (41 of 739 strings)

Added translation using Weblate (Mongolian)

Translated using Weblate (Interlingua)

Currently translated at 32.0% (237 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Tigrinya)

Currently translated at 9.4% (70 of 739 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Icelandic)

Currently translated at 3.8% (3 of 78 strings)

Translated using Weblate (Icelandic)

Currently translated at 96.0% (710 of 739 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Turkish)

Currently translated at 44.8% (35 of 78 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 94.3% (697 of 739 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 75.6% (59 of 78 strings)

Translated using Weblate (Albanian)

Currently translated at 78.7% (582 of 739 strings)

Translated using Weblate (Dutch)

Currently translated at 61.5% (48 of 78 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Armenian)

Currently translated at 27.8% (206 of 739 strings)

Translated using Weblate (German)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Burmese)

Currently translated at 2.5% (19 of 739 strings)

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

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Tigrinya)

Currently translated at 9.3% (69 of 739 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Tamil)

Currently translated at 46.5% (344 of 739 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (German)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Turkish)

Currently translated at 44.8% (35 of 78 strings)

Translated using Weblate (Belarusian)

Currently translated at 98.9% (731 of 739 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Romanian)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Co-authored-by: --//-- <htetoh2006@outlook.com>
Co-authored-by: 09pulse <junis.mednis@gmail.com>
Co-authored-by: Adrien N <adriennathaniel1999@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: Andrés Paredes <andresparedeszaa@gmail.com>
Co-authored-by: AntonAkovP <anton.akov@gmail.com>
Co-authored-by: Anxhelo Lushka <anxhelo1995@gmail.com>
Co-authored-by: Balázs Meskó <meskobalazs@mailbox.org>
Co-authored-by: BennyBeat <bennybeat@gmail.com>
Co-authored-by: Bálint Katona <katonabalint0901@gmail.com>
Co-authored-by: Coool (github.com/Coool) <coool@mail.lv>
Co-authored-by: D D <keptawesome@gmail.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Daniels Gaho <mouth_many452@slmails.com>
Co-authored-by: Davit Mayilyan <davit.mayilyan@protonmail.ch>
Co-authored-by: DevMikey123 <minecraftmikey20yt@gmail.com>
Co-authored-by: Faeh jaekhan <hooby.facsimile081@simplelogin.com>
Co-authored-by: Femini <Olpi@users.noreply.hosted.weblate.org>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Flavian <3zorro.1@gmail.com>
Co-authored-by: Flo P <florian@policnik.de>
Co-authored-by: Francesco James Fanti <francescojamesfanti@gmail.com>
Co-authored-by: Freddy Morán Jr <freddynic159@gmail.com>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Gold Ayan <thangaayyanar@gmail.com>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Gonzalo Vidal <idigbacon@gmail.com>
Co-authored-by: Gustavo A <gustavo.shortage796@slmails.com>
Co-authored-by: H Tamás <hovanszki@gmail.com>
Co-authored-by: Hoseok Seo <ddinghoya@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hydra3 <hydra3black@gmail.com>
Co-authored-by: Hứa Đức Quân <huaducquan14@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Inn Charge <inncharge@abv.bg>
Co-authored-by: Jan Novotny <aplikace62@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Jimi Sainio <kitsu193@gmail.com>
Co-authored-by: Jose Delvani <delvani.eletricista@gmail.com>
Co-authored-by: Jose Delvani <jsdelvani@users.noreply.hosted.weblate.org>
Co-authored-by: Kartik Jivane <jivanekartik21@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: LuanaBanana29 <luana.baron@protonmail.com>
Co-authored-by: Luna <social.pvxuu@slmail.me>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Onebyone <onebyone222@ccmail.uk>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: PepeV_nRT <pepev.nrt@gmail.com>
Co-authored-by: Phi Huynh <huynhkhaphi.ltp20@gmail.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Rhoslyn Prys <rprys@posteo.net>
Co-authored-by: Riku <riksu9000@gmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Sandeep Balaji <besandeep21@gmail.com>
Co-authored-by: SejeroDev <sejerodev@gmail.com>
Co-authored-by: ShareASmile <ShareASmile@users.noreply.hosted.weblate.org>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Teoman <teoteot1122@gmail.com>
Co-authored-by: Timur Seber <seber.tatsoft@gmail.com>
Co-authored-by: TobiGr <TobiGr@users.noreply.github.com>
Co-authored-by: Tzvika <mmm_45@walla.com>
Co-authored-by: Tấn Lực Trương <september122022ios16@gmail.com>
Co-authored-by: Vas R <mrkomododragon1234@gmail.com>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: W L <wl@mailhole.de>
Co-authored-by: WB <dln0@proton.me>
Co-authored-by: Wydow <wydow@protonmail.com>
Co-authored-by: X <dieeeazpnnqbpddh@cock.email>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Zorro <3zorro.1@gmail.com>
Co-authored-by: abfreeman <freemanab@protonmail.com>
Co-authored-by: algimantas <algimantas@margevicius.lt>
Co-authored-by: billy appetie <billy_appetie@users.noreply.hosted.weblate.org>
Co-authored-by: dulgun <dulguun.tuguldur11@gmail.com>
Co-authored-by: fsbat0 <fsbat0@users.noreply.hosted.weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gfbdrgng <hnaofegnp@hldrive.com>
Co-authored-by: j <jonas84@infocus.lt>
Co-authored-by: justcontributor <dumkty5663@gmail.com>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: kuriokurio <kuriokurio@proton.me>
Co-authored-by: mamarama9904 <mamarama9904@gmail.com>
Co-authored-by: nick vurgaft <slipperygate@gmail.com>
Co-authored-by: p3nguin-kun.png <p3nguinkun@proton.me>
Co-authored-by: rakijagamer-2003 <rakijaisthebest@abv.bg>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: triaza <triazatriborinane@gmail.com>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: weldu <fsbat0@users.noreply.hosted.weblate.org>
Co-authored-by: ε <aaypkzixad@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
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/bg/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cy/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
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/fi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/is/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ko/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ta/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translation: NewPipe/Metadata
2024-11-14 17:37:31 +01:00
Stypox
6fe417abc6 Merge pull request #11024 from AbdeltwabMF/fix/rtl_lang_adjustment_bookmark
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Adjust the playlist bookmark item layout for RTL languages
2024-11-14 16:26:25 +01:00
Stypox
a229ab68d5 Merge pull request #11696 from codyit/history-remove-dialog-override
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Remove history dialog override so clicking "Start playing in the background" would only enqueue the current item instead of the full history which is usually massive
2024-11-12 10:43:01 +01:00
Stypox
544b30290d Merge pull request #11694 from VishramKidPG123/fix-typo-in-readme
Fix a typo in README
2024-11-12 10:32:01 +01:00
Cody T.-H. Chiu
cb300724da Remove history dialog override so clicking "Start playing in the background" would only enqueue the current item instead of the full history which is usually massive 2024-11-12 18:24:23 +13:00
VishramKidPG123
0ac5a269ff Update README.md 2024-11-11 22:40:29 -05:00
Tobi
0009613608 Merge pull request #11140 from shrimprugbysnowowl/dev
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Adding Hash of Signing Key to README
2024-11-11 07:38:13 +01:00
Tobi
7c18d4dd01 Update README.md 2024-11-11 07:35:37 +01:00
Tobi
fe1c538f9c Update README.md 2024-11-11 07:34:45 +01:00
Stypox
f08e07873a Merge pull request #11566 from nicholasala/fix/#10993-strange-playlist-order
Fixed playlist order
2024-11-10 15:45:33 +01:00
TobiGr
1193b02ca1 Update user agent to Firefox ESR128 2024-11-03 11:52:31 +01:00
Tobi
c0b36b86b9 Merge pull request #11614 from rmtilde/fix-related-items-enque-popup-crash
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Fix related items list enqueue popup crash
2024-11-03 10:13:45 +01:00
rmtilde
66ec596f67 Update app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java
Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2024-11-03 18:26:38 +11:00
Tobi
90404a23ce Merge pull request #11621 from u7656655/fixing-ui-crash-11468
Fix UI crash when user navigates away before the download dialog appears
2024-11-02 23:30:35 +01:00
Tobi
64ad05d813 Merge pull request #11629 from Two-Ai/kotlin-getStringSafe
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Add null-safe SharedPreferences.getStringSafe
2024-10-27 20:58:25 +01:00
TwoAi
734b6e2b67 Add null-safe SharedPreferences.getStringSafe
Null-safe alternative to SharedPreferences.getString that guarantees the return value is non-null when defValue is non-null.
2024-10-27 20:38:28 +01:00
Tobi
94f992a2e2 Merge pull request #11656 from litetex/better-control-over-version
[Build] Make it possible control the version code and name
2024-10-27 20:05:53 +01:00
litetex
c8550695aa Make it possible control the version code and name 2024-10-27 17:51:22 +01:00
Tobi
cdac50bab3 Merge pull request #11596 from Thompson3142/fix_scrubbing_seekbar_preview_crash
Fix seekbar crashing on drag with faulty frameset
2024-10-27 16:19:44 +01:00
Thompson3142
23961548c0 Formatting changes (back to original) 2024-10-27 14:38:25 +01:00
Thompson3142
ba1e9c8e1b Update comment
Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2024-10-27 14:17:32 +01:00
Tobi
f4baf4628e Update app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java 2024-10-27 09:41:45 +01:00
Tobi
05a87da827 Merge pull request #11651 from u7656655/fix-addtoplaylist-crash
Fix crash after adding item to a playlist caused by null thumbnail URL
2024-10-27 09:15:49 +01:00
Jacob Hawkins
fef40014a0 Added not null check for thumbnail URL before performing comparison 2024-10-27 17:38:57 +11:00
rmtilde
1996c1176c Merge branch 'TeamNewPipe:dev' into fix-related-items-enque-popup-crash 2024-10-26 20:33:17 +11:00
Elva Kang
0190bcee25 Fix line length violation 2024-10-24 16:04:53 +11:00
Elva Kang
1ed4928f40 Add comment for fragment lifecycle checks before showing DownloadDialog 2024-10-24 11:47:23 +11:00
Elva Kang
63bc982cb2 Merge branch 'TeamNewPipe:dev' into fixing-ui-crash-11468 2024-10-24 11:11:37 +11:00
Stypox
3a286515f2 Merge pull request #11636 from litetex/fix-build-2024-10
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Fix compilation
2024-10-23 22:18:48 +02:00
litetex
2e96b65fda Replaced `Icepick with Bridge and Android-State`
* IcePick fails on Java 21 (default in Android Studio 2024.2)
* Bridge is the most modern alternative that is currently available. It is backed by ``Android-State`` and can be configured with various frameworks
* In the long term this should be replaced with something better
2024-10-23 21:28:07 +02:00
litetex
2482615460 Fix Android Gradle plugin warning 2024-10-22 21:40:16 +02:00
litetex
9384365061 Update Gradle to latest version 2024-10-22 21:39:44 +02:00
litetex
b1d4b66aa6 Replace symlink with original
Co-Authored-By: Thompson3142 <115718208+thompson3142@users.noreply.github.com>
2024-10-22 21:24:10 +02:00
litetex
ea0da5fdbd Delete symlink 2024-10-22 21:24:09 +02:00
litetex
d80b6a759c Use working Extractor version
The tag can't be resolved by Jitpack so use the commit-hash instead
2024-10-22 21:23:34 +02:00
litetex
8106ba68b5 CI: Use Java 21 2024-10-22 21:23:26 +02:00
litetex
ee15a72e4f Fix build failing locally due to outdated kotlin version 2024-10-22 21:03:08 +02:00
Elva Kang
2eb256799d Revert "Project now runs"
This reverts commit 53edd054aa.
2024-10-20 10:29:48 +11:00
Elva Kang
0cf4732d8a Fix UI crash when user navigates away before the download dialog appears 2024-10-19 19:43:34 +11:00
Jacob Hawkins
53edd054aa Project now runs 2024-10-17 15:14:15 +11:00
rmtilde
678f0a786a Merge pull request #1 from rmtilde/fix-related-items-enqueue-on-video-change
Fix Crash on Related Items Modal
2024-10-17 13:37:19 +11:00
rmtilde
b14f65804d Added comments to explain changes 2024-10-16 23:58:32 +11:00
u7310752
781a69d60d Chanegd related videos enqueue modal to attach to parent fragment instead 2024-10-16 20:52:43 +11:00
Thompson3142
eb9f300e60 Fix seekbar preview crashes (#11584)
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
No Response / noResponse (push) Has been cancelled
Fixed crashes from recycled bitmaps by creating real copies of bitmaps if necessary + some minor refactoring
2024-10-10 10:32:06 +02:00
Nicholas Sala
063568b620 Fixed playlist order between "Bookmarked Playlists" list and "add to playlist" dialog list. Now both lists are sorted using case insensitive order if the user has not yet adjusted manually the order. 2024-09-26 13:24:26 +00:00
Mihael_River
035c394cf6 Fixing the 404 page not found, when clicking on "contribution notes" in multiple README.md's translated into different languages (#11487)
Link to contribution notes wasn't working

* Update README.de.md, fix grammar in README.de.md
* Update README.asm.md
* Update README.fr.md
* Update README.hi.md
* Update README.it.md
* Update README.pa.md
* Update README.pt_BR.md
* Update README.ru.md
* Update README.sr.md

---------

Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2024-08-30 16:32:42 +02:00
Tobi
fad3120b00 Merge pull request #11428 from Two-Ai/remove-returnActivity-test
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
Remove outdated returnActivity test code
2024-08-15 01:53:28 +02:00
TwoAi
38c823a042 Remove outdated returnActivity test code
returnActivity was removed in 463dd8e
2024-08-10 23:09:54 -04:00
Stypox
51ee2f8d1e Merge branch 'master' into dev
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
2024-07-25 21:20:44 +02:00
Stypox
d442b45836 Remove code committed accidentally
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
2024-07-25 20:58:29 +02:00
Stypox
dbcb721dc2 Don't warn about rhino class in proguard
Likely related to 01a7b20655 but I am not completely sure.
I tested the app and it works well, so I think that org.mozilla.javascript.JavaToJSONConverters is not used really.

This is the full list of errors:
Missing class java.beans.BeanDescriptor (referenced from: java.lang.Object org.mozilla.javascript.JavaToJSONConverters.lambda$static$4(java.lang.Object))
Missing class java.beans.BeanInfo (referenced from: java.lang.Object org.mozilla.javascript.JavaToJSONConverters.lambda$static$4(java.lang.Object))
Missing class java.beans.IntrospectionException (referenced from: java.lang.Object org.mozilla.javascript.JavaToJSONConverters.lambda$static$4(java.lang.Object))
Missing class java.beans.Introspector (referenced from: java.lang.Object org.mozilla.javascript.JavaToJSONConverters.lambda$static$4(java.lang.Object))
Missing class java.beans.PropertyDescriptor (referenced from: java.lang.Object org.mozilla.javascript.JavaToJSONConverters.lambda$static$4(java.lang.Object))
2024-07-25 20:56:16 +02:00
Stypox
64a8f6575b Merge pull request #11351 from Stypox/update-npe
Hotfix release v0.27.2
2024-07-25 19:30:05 +02:00
Stypox
03a6b5c7b9 Add changelogs for hotfix release v0.27.2 (999) 2024-07-25 18:57:58 +02:00
Stypox
56b6241311 Hotfix release v0.27.2 (999) 2024-07-25 18:43:03 +02:00
Stypox
947ac2826a Update NewPipeExtractor to v0.24.2 2024-07-25 18:40:50 +02:00
opusforlife2
0e8303f13a Update Matrix room link, and prioritise it (#11350)
* Update Matrix room link, and prioritise it

* Update Matrix room link in CONTRIBUTING.md

* Prioritise Matrix in contribution doc too
2024-07-25 16:21:21 +02:00
Stypox
72e9f7f9cf Merge branch 'master' into dev
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
2024-07-15 10:17:27 +02:00
#27
ad6b676c81 Update README.pt_BR.md (#11275) 2024-07-13 19:32:24 +02:00
Stypox
0f64158469 Hotfix release v0.27.1 (998)
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
2024-07-11 23:41:53 +02:00
Stypox
acc5be92ac Add changelogs for hotfix release v0.27.1 (998) 2024-07-11 23:39:53 +02:00
Stypox
0e0cee1bce Update NewPipeExtractor to v0.24.1 2024-07-11 23:27:26 +02:00
Stypox
6f71c000ad Merge pull request #11261 from Stypox/fix-media-session-ui-npe
Fix crash in MediaSessionPlayerUi while destroying player
2024-07-11 23:17:43 +02:00
Stypox
9f766ebf78 Fix NPE in MediaSessionPlayerUi while destroying player 2024-07-11 09:41:33 +02:00
Isira Seneviratne
07c63f794e Update documentation 2024-07-07 14:25:02 +05:30
Isira Seneviratne
26dd86e967 Use Android's elapsed time formatting 2024-07-07 10:46:17 +05:30
Tobi
5e5e77f746 Merge pull request #11230 from TeamNewPipe/idea_icon
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
add NP icon for Android Studio's NewUI
2024-07-03 09:50:40 +02:00
Stypox
1f309854bc Run CI on pull requests to refactor branch, too
Some checks failed
CI / build-and-test-jvm (push) Has been cancelled
CI / test-android (21, x86, default) (push) Has been cancelled
CI / test-android (33, x86_64, google_apis) (push) Has been cancelled
CI / sonar (push) Has been cancelled
2024-07-02 17:37:09 +02:00
Christian Schabesberger
2ac0d1f13a add NP icon for Android Studio's NewUI 2024-07-02 09:31:34 +02:00
Stypox
4eeea7b787 Merge pull request #11209 from EricDriussi/kotlin-contributing
Some checks failed
No Response / noResponse (push) Has been cancelled
Remove kotlin code restriction from contribution guidelines
2024-06-25 08:26:13 +02:00
Eric Driussi
e64c01d2da Remove kotlin restriction 2024-06-24 09:47:29 +01:00
Tobi
0c7a91f852 Merge pull request #11067 from snaik20/fix_rss_button_visibility
Fix RSS button visibility
2024-06-17 11:49:33 +02:00
Tobi
a2d93b389c Merge pull request #11110 from Neznak/add-peertube-instance
[Peertube] Handle `subscribeto.me` instance links automatically
2024-06-17 11:48:25 +02:00
Tobi
c795214abb Merge pull request #11112 from aryn-ydv/extend-playlist-description
Make playlist description clickable to show more / less content
2024-06-17 11:32:10 +02:00
shrimprugbysnowowl
71822a47a5 Update README.md 2024-06-07 14:24:59 +00:00
shrimprugbysnowowl
e1bf67c676 Update README.md 2024-06-07 14:20:06 +00:00
Aryan Yadav
8583c48264 fixes #11093 2024-05-28 10:14:46 +05:30
Neznak
2a3d133bcf Add missing Peertube instance subscribeto.me to the links Newpipe handles 2024-05-27 14:08:18 +03:00
Isira Seneviratne
3e3d1fd265 Merge pull request #11075 from Isira-Seneviratne/Comment-touch-lambda
Convert comment touch listener to a lambda
2024-05-26 04:34:49 +05:30
Tobi
8645618f1a Merge pull request #11094 from moontoaster/update-prettytime-to-fix-ukrainian
Update PrettyTime to 5.0.8
2024-05-23 21:24:40 +02:00
moontoaster
e48ce5a103 Update PrettyTime to 5.0.8
This version contains a fix for Ukrainian locale which fixes #11092.
2024-05-23 20:57:05 +03:00
Abd El-Twab M. Fakhry
c02ceda22f Use layout constraints instead of static height 2024-05-18 16:47:41 +03:00
Isira Seneviratne
46139340fe Convert comment touch listener to a lambda 2024-05-15 06:51:57 +05:30
Siddhesh Naik
7204407690 Fix RSS button visibility
- The `onPrepareMenu` callback is invoked after setting the visibility
  of the menu items.
- Due to this, the menu item resets to it's default visibility.
- Now updating the menu item within the callback.
- Also migrated to the MenuHost framework to reduce dependency on
  deprecated APIs.
2024-05-13 02:28:21 +05:30
Stypox
e37336eef2 Merge pull request #10918 from Stypox/non-transitive-r
Migrate to non-transitive R classes
2024-05-08 10:35:08 +02:00
Abd El-Twab M. Fakhry
cf21b9feaf Revert "Fix compilation error when parsing unsupported file format"
This reverts commit 8267d325ed.
2024-05-01 17:21:24 +03:00
Abd El-Twab M. Fakhry
b74cab6642 Adjust the playlist bookmark item layout for RTL languages 2024-05-01 01:38:46 +03:00
Abd El-Twab M. Fakhry
8267d325ed Fix compilation error when parsing unsupported file format 2024-04-30 23:41:02 +03:00
Siddhesh Naik
879d7a24f0 Fix github worklow for Android tests (#11014)
- The github workflow fails when running android tests.
- The workflow is trying to launch an x86 emulator on aarch-64 (macos-latest) host.
- The macos-latest system seem to be used originally as it supports
  hardware acceleration.
- This is no longer recomended, and ubuntu-latest host can handle the
  same and be faster than macos-latest.

Doc: https://github.com/marketplace/actions/android-emulator-runner#running-hardware-accelerated-emulators-on-linux-runners
2024-04-29 02:45:18 +05:30
Stypox
9e4ac2eacb Merge pull request #11003 from ashutosh001/dev
Update README.md
2024-04-26 11:05:54 +02:00
ashutosh001
d9d6fff48f Update README.md 2024-04-26 06:23:46 +05:30
Stypox
9828586762 Fix indentation for ktlint 2024-04-23 20:16:04 +02:00
Hosted Weblate
8caaa6d297 Merge branch 'origin/dev' into Weblate. 2024-04-23 19:27:20 +02:00
Stypox
83ca6b9468 Update NewPipeExtractor to v0.24.0 2024-04-23 19:25:13 +02:00
Stypox
24e65ef018 Merge branch 'dev' 2024-04-23 19:23:20 +02:00
Stypox
a69bbab732 Merge pull request from GHSA-wxrm-jhpf-vp6v
Fix preferences import vulnerability
2024-04-23 19:22:17 +02:00
Stypox
a557ac3c7b Merge pull request #10929 from TeamNewPipe/release-0.27.0
Release v0.27.0 (997)
2024-04-23 19:21:12 +02:00
Stypox
d61b4b89ea Merge pull request #10992 from Stypox/fix-download-nnfp
Fix free storage space check for all APIs
2024-04-23 18:42:57 +02:00
Stypox
b8daf16b92 Update app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java
Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2024-04-23 18:39:56 +02:00
Stypox
caa3812e13 Ignore all errors when getting free storage space
It's not a critical check that needs to be perfomed, so in case something does not work on some device/version, let's just ignore the error.
2024-04-23 18:05:31 +02:00
Hosted Weblate
23a087c498 Translated using Weblate (Romanian)
Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Croatian)

Currently translated at 99.5% (735 of 738 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (738 of 738 strings)

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

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (French (Louisiana))

Currently translated at 0.2% (2 of 738 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (738 of 738 strings)

Added translation using Weblate (French (Louisiana))

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (French)

Currently translated at 99.7% (736 of 738 strings)

Added translation using Weblate (Arabic (Tunisian))

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

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

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Kannada)

Currently translated at 5.8% (43 of 738 strings)

Translated using Weblate (Kannada)

Currently translated at 5.1% (4 of 78 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (737 of 738 strings)

Translated using Weblate (German)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (German)

Currently translated at 100.0% (738 of 738 strings)

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

Currently translated at 99.0% (731 of 738 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 50.0% (39 of 78 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (734 of 738 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Slovak)

Currently translated at 21.7% (17 of 78 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 62.8% (49 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Polish)

Currently translated at 61.5% (48 of 78 strings)

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

Currently translated at 23.0% (18 of 78 strings)

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: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Flavian <3zorro.1@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jonatan Nyberg <jonatan@autistici.org>
Co-authored-by: Jose Delvani <delvani.eletricista@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Ray <ray@users.noreply.hosted.weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sergio Marques <so.boston.android@gmail.com>
Co-authored-by: ShareASmile <ShareASmile@users.noreply.hosted.weblate.org>
Co-authored-by: Tấn Lực Trương <september122022ios16@gmail.com>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: abhijithkjg <abhijithkj2001@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: yosrinajar <yosron3@gmail.com>
Co-authored-by: zeineb-b <zeinebbouhejba21@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
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/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/kn/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2024-04-23 18:00:52 +02:00
Stypox
c3c39a7b24 Fix free storage space check for all APIs
See https://stackoverflow.com/q/31171838
See https://pubs.opengroup.org/onlinepubs/9699919799/functions/fstatvfs.html
2024-04-23 12:16:06 +02:00
Stypox
00770fc634 Update NewPipeExtractor 2024-04-20 13:11:08 +02:00
Stypox
5bf77160f7 Merge pull request #10952 from bg172/release-0.27.0
Add an intuitive prefix for the duration of lists in the UI
2024-04-11 09:27:54 +02:00
Stypox
d9da84c412 Merge pull request #10957 from Stypox/fix-feed-npe
Fix NPE if avatarUrl is null when reloading feed
2024-04-11 09:26:11 +02:00
Audric V
b3a6318672 Merge pull request #10959 from Stypox/fix-comment-replies-state
Fix not saving comment replies state on config change
2024-04-10 16:24:31 +02:00
Stypox
67b41b970d Fix not saving comment replies state on config change 2024-04-10 10:52:47 +02:00
Stypox
3738e30949 Fix NPE when avatarUrl is empty 2024-04-09 20:18:21 +02:00
Stypox
0ba73b11c1 Update NewPipeExtractor 2024-04-08 00:03:37 +02:00
bg1722
13baaa31cd add an intuitive prefix for the duration of lists on UI, and avoid using the new prefix for single videos 2024-04-06 07:58:05 +02:00
TobiGr
f0db2aa43c Improve documentation 2024-04-04 15:49:12 +02:00
Stypox
f704721b59 Release v0.27.0 (997) 2024-04-01 14:23:48 +02:00
Stypox
7abf0f4886 Update NewPipeExtractor to YT comments fix PR
https://github.com/TeamNewPipe/NewPipeExtractor/pull/1163
2024-04-01 14:23:04 +02:00
Stypox
c915b6e68b Add changelog for v0.27.0 (997) 2024-04-01 14:16:51 +02:00
Hosted Weblate
0b28c688c6 Translated using Weblate (Estonian)
Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 46.7% (36 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (German)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Korean)

Currently translated at 31.1% (24 of 77 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Korean)

Currently translated at 98.9% (726 of 734 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (German)

Currently translated at 99.7% (732 of 734 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (German)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Hungarian)

Currently translated at 16.8% (13 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (730 of 730 strings)

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

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (730 of 730 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Apious <apious@kakao.com>
Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Michael Moroni <michaelmoroni@disroot.org>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Pi-Cla <pirateclip@protonmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Tim Trek <T.Trek@byom.de>
Co-authored-by: TobiGr <TobiGr@users.noreply.github.com>
Co-authored-by: Tấn Lực Trương <september122022ios16@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ko/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translation: NewPipe/Metadata
2024-04-01 13:38:40 +02:00
Stypox
2756ef6d2f Show notification when failing to import settings 2024-03-30 18:53:45 +01:00
Stypox
7da1d30010 Expose all import/export errors to the user 2024-03-30 18:47:20 +01:00
Stypox
8e192acb63 Add test zips and extensive tests for ImportExportManager
Now all possible combinations of files in the zip (present or not) are checked at the same time
2024-03-30 18:42:11 +01:00
Stypox
d8423499dc Use JSON for settings imports/exports 2024-03-30 16:58:12 +01:00
TobiGr
974167fcb8 Add comment that empty constructors are needed for IcePick
See 5e7ad6ffd1 and https://github.com/TeamNewPipe/NewPipe/pull/10781#discussion_r1545351144
2024-03-30 16:19:02 +01:00
Stypox
6afdbd6fd3 Add test: vulnerable settings should fail importing 2024-03-30 16:12:41 +01:00
Stypox
d8668ed226 Show snackbar error when settings import fails 2024-03-30 16:12:41 +01:00
Stypox
d75a6eaa41 Fix vulnerability with whitelist-aware ObjectInputStream
Only a few specific classes are now allowed.
2024-03-30 16:12:35 +01:00
Stypox
235fb92638 Make checkstyle accept javadocs with long links 2024-03-30 15:49:06 +01:00
Stypox
ea18b4ea1f Move import export manager to separate folder 2024-03-30 15:49:05 +01:00
Stypox
58f5ec0181 Merge pull request #9580 from pratyaksh1610/branch-8232
Moved player notification setting to notification section
2024-03-30 15:38:33 +01:00
pratyaksh1610
e42c9abdde moved player notification to notification section 2024-03-30 15:23:46 +01:00
Stypox
5e7ad6ffd1 Fix fragments without empty constructor 2024-03-30 15:15:31 +01:00
Stypox
4c8238874e Merge pull request #8221 from GGAutomaton/feature-7870
Sort bookmarked playlists
2024-03-30 15:02:37 +01:00
Stypox
38d4887901 Undo some unneeded changes to LocalPlaylistManager 2024-03-30 14:46:13 +01:00
Stypox
c9051d33c1 Fix warnings and allow moving only up and down even in grid 2024-03-30 14:39:40 +01:00
Stypox
3cc0205def Fix inconsistencies when removing playlist
Remove checkDisplayIndexModified because it was causing more problems than it solved. Now when adding new playlists they won't necessarily appear at the top, but will get sorted alphabetically along with the other playlists with index -1. This will be the case until any playlist is sorted, at which point all indices are assigned and newly added playlists will appear at the top again.
2024-03-30 14:14:31 +01:00
Stypox
90979e2a81 Fix PlaylistLocalItemTest 2024-03-29 20:58:07 +01:00
Stypox
e66e1b542c Also sort playlist duplicates by display index 2024-03-29 20:55:24 +01:00
Stypox
92e9c3e42e Fix DatabaseMigrationTest
Complete removal of unneeded index, and remove default value for `remote_playlists.display_index`.
2024-03-29 20:43:55 +01:00
Stypox
4591c09637 Apply review 2024-03-29 18:08:37 +01:00
Stypox
e1ce3fef1b Merge branch 'dev' into pr8221 2024-03-29 18:08:31 +01:00
Stypox
f4fb960c62 Migrate to non-transitive R classes 2024-03-29 00:17:13 +01:00
GGAutomaton
8ad7bf60d7 Delete saveImmediate warnings & add comments 2022-06-23 23:31:56 +08:00
GGAutomaton
898a936064 Update index modification logic & redo sorting in the merge algorithm 2022-06-23 23:19:59 +08:00
GGAutomaton
4e401bc059 Update playlists in parallel 2022-06-23 20:36:21 +08:00
GGAutomaton
9ecef6f011 Add abstract methods in PlaylistLocalItem & rename setIsModified 2022-06-23 19:20:16 +08:00
GGAutomaton
ba394a7ab4 Update test and Javadoc 2022-05-11 18:08:14 +08:00
GGAutomaton
d32490a4be Create sub-package and default interval for DebounceSaver & sort playlists in db 2022-05-11 16:47:34 +08:00
GGAutomaton
6526ff1612 Add tests 2022-04-17 20:20:20 +08:00
GGAutomaton
bb5390d63a Reuse DebounceSaver 2022-04-17 14:53:02 +08:00
GGAutomaton
bd1aae8d66 Fix sonar warning 2022-04-16 12:44:24 +08:00
GGAutomaton
c24aed054f Fix sonar warning and typo 2022-04-16 12:00:02 +08:00
GGAutomaton
0aa08a5e40 Use new item holder 2022-04-15 23:19:24 +08:00
GGAutomaton
3c48825699 Debounced saver & bugfix & clean code 2022-04-15 20:44:54 +08:00
GGAutomaton
bfb56b4144 UI design and behavior 2022-04-14 16:59:52 +08:00
GGAutomaton
ba8370bcfd Save changes to the database and bugfix 2022-04-14 12:13:42 +08:00
GGAutomaton
813f55152a Merge branch 'TeamNewPipe:dev' into feature-7870 2022-04-13 22:48:26 +08:00
GGAutomaton
270a541a7c Implement algorithm to merge playlists 2022-04-13 22:46:24 +08:00
GGAutomaton
c34549a47d Update database migrations and getter/setter 2022-04-13 21:35:38 +08:00
GGAutomaton
96d6b309ec Migrate database 2022-04-13 19:41:07 +08:00
1059 changed files with 20701 additions and 5646 deletions

View File

@@ -6,7 +6,7 @@ NewPipe contribution guidelines
## Crash reporting ## Crash reporting
Report crashes through the **automated crash report system** of NewPipe. Report crashes through the **automated crash report system** of NewPipe.
This way all the data needed for debugging is included in your bugreport for GitHub. This way all the data needed for debugging is included in your bug report for GitHub.
You'll see *exactly* what is sent, be able to add **your comments**, and then send it. You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
## Issue reporting/feature requests ## Issue reporting/feature requests
@@ -42,10 +42,6 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions. * Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out. * NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
### Kotlin in NewPipe
* NewPipe will remain mostly Java for time being
* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language.
### Creating a Pull Request (PR) ### Creating a Pull Request (PR)
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub. * Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
@@ -83,6 +79,6 @@ The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as ch
## Communication ## Communication
* The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)! * You can use a Matrix account to join the NewPipe channel at [#newpipe:matrix.newpipe-ev.de](https://matrix.to/#/#newpipe:matrix.newpipe-ev.de). Some convenient clients, available both for phone and desktop, are listed at that link.
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link. * Alternatively, the #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) can also be joined, as it is bridged to the Matrix room. [Click here for webchat](https://web.libera.chat/#newpipe)!
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC. * You can post your suggestions, changes, ideas etc. on either GitHub or Matrix (including via IRC).

View File

@@ -3,9 +3,9 @@ contact_links:
- name: ❓ Question - name: ❓ Question
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
about: Ask about anything NewPipe-related about: Ask about anything NewPipe-related
- name: 💬 Matrix
url: https://matrix.to/#/#newpipe:matrix.newpipe-ev.de
about: Chat with us via Matrix for quick Q/A
- name: 💬 IRC - name: 💬 IRC
url: https://web.libera.chat/#newpipe url: https://web.libera.chat/#newpipe
about: Chat with us via IRC for quick Q/A about: Chat with us via IRC for quick Q/A
- name: 💬 Matrix
url: https://matrix.to/#/#newpipe:libera.chat
about: Chat with us via Matrix for quick Q/A

38
.github/workflows/build-release-apk.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: "Build unsigned release APK on master"
on:
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: 'master'
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
cache: 'gradle'
- name: "Build release APK"
run: ./gradlew assembleRelease --stacktrace
- name: "Rename APK"
run: |
VERSION_NAME="$(jq -r ".elements[0].versionName" "app/build/outputs/apk/release/output-metadata.json")"
echo "Version name: $VERSION_NAME" >> "$GITHUB_STEP_SUMMARY"
echo '```json' >> "$GITHUB_STEP_SUMMARY"
cat "app/build/outputs/apk/release/output-metadata.json" >> "$GITHUB_STEP_SUMMARY"
echo >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
# assume there is only one APK in that folder
mv app/build/outputs/apk/release/*.apk "app/build/outputs/apk/release/NewPipe_v$VERSION_NAME.apk"
- name: "Upload APK"
uses: actions/upload-artifact@v4
with:
name: app
path: app/build/outputs/apk/release/*.apk

View File

@@ -6,6 +6,7 @@ on:
branches: branches:
- dev - dev
- master - master
- refactor
- release** - release**
paths-ignore: paths-ignore:
- 'README.md' - 'README.md'
@@ -46,10 +47,10 @@ jobs:
BRANCH: ${{ github.head_ref }} BRANCH: ${{ github.head_ref }}
run: git checkout -B "$BRANCH" run: git checkout -B "$BRANCH"
- name: set up JDK 17 - name: set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 21
distribution: "temurin" distribution: "temurin"
cache: 'gradle' cache: 'gradle'
@@ -63,8 +64,7 @@ jobs:
path: app/build/outputs/apk/debug/*.apk path: app/build/outputs/apk/debug/*.apk
test-android: test-android:
# macos has hardware acceleration. See android-emulator-runner action runs-on: ubuntu-latest
runs-on: macos-latest
timeout-minutes: 20 timeout-minutes: 20
strategy: strategy:
matrix: matrix:
@@ -72,8 +72,8 @@ jobs:
- api-level: 21 - api-level: 21
target: default target: default
arch: x86 arch: x86
- api-level: 33 - api-level: 35
target: google_apis # emulator API 33 only exists with Google APIs target: default
arch: x86_64 arch: x86_64
permissions: permissions:
@@ -82,10 +82,16 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: set up JDK 17 - name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 21
distribution: "temurin" distribution: "temurin"
cache: 'gradle' cache: 'gradle'
@@ -105,6 +111,7 @@ jobs:
path: app/build/reports/androidTests/connected/** path: app/build/reports/androidTests/connected/**
sonar: sonar:
if: ${{ false }} # the key has expired and needs to be regenerated by the sonar admins
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -115,10 +122,10 @@ jobs:
with: with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 17 - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 21
distribution: "temurin" distribution: "temurin"
cache: 'gradle' cache: 'gradle'

View File

@@ -32,12 +32,12 @@ module.exports = async ({github, context}) => {
} }
// Regex for finding images (simple variant) ![ALT_TEXT](https://*.githubusercontent.com/<number>/<variousHexStringsAnd->.<fileExtension>) // Regex for finding images (simple variant) ![ALT_TEXT](https://*.githubusercontent.com/<number>/<variousHexStringsAnd->.<fileExtension>)
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm; const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[([^\]]*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[(.*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm; const REGEX_ASSETS_IMAGE_LOOKUP = /\!\[([^\]]*)\]\((https:\/\/github\.com\/(?:user-attachments\/assets|[-\w\d]+\/[-\w\d]+\/assets\/\d+)\/[\-0-9a-f]{32,512})\)/gm;
// Check if we found something // Check if we found something
let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody) let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
|| REGEX_ASSETS_IMAGE_LOCKUP.test(initialBody); || REGEX_ASSETS_IMAGE_LOOKUP.test(initialBody);
if (!foundSimpleImages) { if (!foundSimpleImages) {
console.log('Found no simple images to process'); console.log('Found no simple images to process');
return; return;
@@ -52,7 +52,7 @@ module.exports = async ({github, context}) => {
// Try to find and replace the images with minimized ones // Try to find and replace the images with minimized ones
let newBody = await replaceAsync(initialBody, REGEX_USER_CONTENT_IMAGE_LOOKUP, minimizeAsync); let newBody = await replaceAsync(initialBody, REGEX_USER_CONTENT_IMAGE_LOOKUP, minimizeAsync);
newBody = await replaceAsync(newBody, REGEX_ASSETS_IMAGE_LOCKUP, minimizeAsync); newBody = await replaceAsync(newBody, REGEX_ASSETS_IMAGE_LOOKUP, minimizeAsync);
if (!wasMatchModified) { if (!wasMatchModified) {
console.log('Nothing was modified. Skipping update'); console.log('Nothing was modified. Skipping update');

21
.idea/icon.svg generated Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:#CD201F;}
.st1{fill:#FFFFFF;}
</style>
<g id="Alapkör">
<circle id="XMLID_23_" class="st0" cx="50" cy="50" r="50"/>
</g>
<g id="Elemek">
<path id="XMLID_19_" class="st1" d="M47,28.2c-9-5.3-15.3-9-15.3-9v61.7c0,0,30.4-18,52.3-30.9C72.1,43,57.7,34.5,47,28.2z"/>
</g>
<g id="Fedő">
<path id="XMLID_5_" class="st0" d="M48.4,40.1c-4.1-2.4-7-4.1-7-4.1V64c0,0,13.9-8.2,23.8-14C59.8,46.8,53.3,42.9,48.4,40.1z"/>
<rect id="XMLID_4_" x="41.4" y="55.6" class="st0" width="6.2" height="21"/>
</g>
<g id="Vonalak">
</g>
</svg>

After

Width:  |  Height:  |  Size: 850 B

View File

@@ -1,26 +1,32 @@
<h3 align="center">We are planning to <i>rewrite</i> large chunks of the codebase, to bring about <a href="https://github.com/TeamNewPipe/NewPipe/discussions/10118">a new, modern and stable NewPipe</a>!</h3> <h3 align="center">We are <i>rewriting</i> large chunks of the codebase, to bring about <a href="https://newpipe.net/blog/pinned/announcement/newpipe-0.27.6-rewrite-team-states/#the-refactor">a modern and stable NewPipe</a>! You can download nightly builds <a href="https://github.com/TeamNewPipe/NewPipe-refactor-nightly/releases">here</a>.</h3>
<h4 align="center">Please do <b>not</b> open pull requests for <i>new features</i> now, only bugfix PRs will be accepted.</h4> <h4 align="center">Please work on the <code>refactor</code> branch if you want to contribute <i>new features</i>. The current codebase is in maintenance mode and will only receive <i>bugfixes</i>.</h4>
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p> <p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2> <h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">A libre lightweight streaming front-end for Android.</h4> <h4 align="center">A libre lightweight streaming front-end for Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg" alt="Get it on F-Droid" height=80/></a></p> <p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg" alt="Get it on F-Droid" width=206/></a></p>
<p align="center"> <p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a> <a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub NewPipe releases"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://github.com/TeamNewPipe/NewPipe-nightly/releases" alt="GitHub NewPipe nightly releases"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe-nightly.svg?labelColor=purple&label=dev%20nightly"></a>
<a href="https://github.com/TeamNewPipe/NewPipe-refactor-nightly/releases" alt="GitHub NewPipe refactor nightly releases"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe-refactor-nightly.svg?labelColor=purple&label=refactor%20nightly"></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a> <a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a> <a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/actions/workflows/ci.yml/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a> <a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
</p>
<p align="center">
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a> <a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://matrix.to/#/#newpipe:matrix.newpipe-ev.de" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a> <a href="https://matrix.to/#/#newpipe:matrix.newpipe-ev.de" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
</p> </p>
<hr> <hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#supported-services">Supported Services</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation-and-updates">Installation and updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p> <p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#supported-services">Supported Services</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation-and-updates">Installation and updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p> <p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr> <hr>
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)* *Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md), [العربية](README.ar.md)*
> [!warning] > [!warning]
> <b>THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b> > <b>THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
@@ -96,7 +102,7 @@ Also, since they are free and open source software, neither the app nor the Extr
## Installation and updates ## Installation and updates
You can install NewPipe using one of the following methods: You can install NewPipe using one of the following methods:
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/ 1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it. 2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases), [compare the signing key](#apk-info) and install it.
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users. 3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users.
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods. 4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up. 5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up.
@@ -104,12 +110,20 @@ You can install NewPipe using one of the following methods:
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK. We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure: In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists 1. Back up your data via Settings > Backup and Restore > Export Database so you keep your history, subscriptions, and playlists
2. Uninstall NewPipe 2. Uninstall NewPipe
3. Download the APK from the new source and install it 3. Download the APK from the new source and install it
4. Import the data from step 1 via Settings > Content > Import Database 4. Import the data from step 1 via Settings > Backup and Restore > Import Database
<b>Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.</b> > [!Note]
> When you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.
### APK Info
This is the SHA fingerprint of NewPipe's signing key to verify downloaded APKs which are signed by us. The fingerprint is also available on [NewPipe's website](https://newpipe.net#download). This is relevant for method 2.
```
CB:84:06:9B:D6:81:16:BA:FA:E5:EE:4E:E5:B0:8A:56:7A:A6:D8:98:40:4E:7C:B1:2F:9E:75:6D:F5:CF:5C:AB
```
## Contribution ## Contribution
Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md). Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).

View File

@@ -12,16 +12,23 @@ plugins {
} }
android { android {
compileSdk 34 compileSdk 36
namespace 'org.schabi.newpipe' namespace 'org.schabi.newpipe'
defaultConfig { defaultConfig {
applicationId "org.schabi.newpipe" applicationId "org.schabi.newpipe"
resValue "string", "app_name", "NewPipe" resValue "string", "app_name", "NewPipe"
minSdk 21 minSdk 21
targetSdk 33 targetSdk 35
versionCode 996 if (System.properties.containsKey('versionCodeOverride')) {
versionName "0.26.1" versionCode System.getProperty('versionCodeOverride') as Integer
} else {
versionCode 1005
}
versionName "0.28.0"
if (System.properties.containsKey('versionNameSuffix')) {
versionNameSuffix System.getProperty('versionNameSuffix')
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -90,8 +97,13 @@ android {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
} }
androidResources {
generateLocaleConfig = true
}
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
} }
packagingOptions { packagingOptions {
@@ -112,7 +124,7 @@ ext {
androidxRoomVersion = '2.6.1' androidxRoomVersion = '2.6.1'
androidxWorkVersion = '2.8.1' androidxWorkVersion = '2.8.1'
icepickVersion = '3.2.0' stateSaverVersion = '1.4.1'
exoPlayerVersion = '2.18.7' exoPlayerVersion = '2.18.7'
googleAutoServiceVersion = '1.1.1' googleAutoServiceVersion = '1.1.1'
groupieVersion = '2.10.1' groupieVersion = '2.10.1'
@@ -197,8 +209,12 @@ dependencies {
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub // Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
// name and the commit hash with the commit hash of the (pushed) commit you want to test // name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/ // This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' implementation 'com.github.TeamNewPipe:nanojson:e9d656ddb49a412a5a0a5d5ef20ca7ef09549996'
implementation 'com.github.Stypox:NewPipeExtractor:aaf3231fc75d7b4177549fec4aa7e672bfe84015' // WORKAROUND: if you get errors with the NewPipeExtractor dependency, replace `v0.24.3` with
// the corresponding commit hash, since JitPack sometimes deletes artifacts.
// If theres already a git hash, just add more of it to the end (or remove a letter)
// to cause jitpack to regenerate the artifact.
implementation 'com.github.TeamNewPipe:NewPipeExtractor:0023b22095a2d62a60cdfc87f4b5cd85c8b266c3'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/ /** Checkstyle **/
@@ -209,7 +225,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
/** AndroidX **/ /** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
@@ -231,11 +247,13 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.11.0'
implementation "androidx.webkit:webkit:1.9.0"
/** Third-party libraries **/ /** Third-party libraries **/
// Instance state boilerplate elimination // Instance state boilerplate elimination
implementation "frankiesardo:icepick:${icepickVersion}" implementation 'com.github.livefront:bridge:v2.0.2'
kapt "frankiesardo:icepick-processor:${icepickVersion}" implementation "com.evernote:android-state:$stateSaverVersion"
kapt "com.evernote:android-state-processor:$stateSaverVersion"
// HTML parser // HTML parser
implementation "org.jsoup:jsoup:1.17.2" implementation "org.jsoup:jsoup:1.17.2"
@@ -262,7 +280,7 @@ dependencies {
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}" implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
// Image loading // Image loading
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828! //noinspection NewerVersionAvailable,GradleDependency --> 2.8 is the last version, not 2.71828!
implementation "com.squareup.picasso:picasso:2.8" implementation "com.squareup.picasso:picasso:2.8"
// Markdown library for Android // Markdown library for Android
@@ -282,7 +300,7 @@ dependencies {
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// Date and time formatting // Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.7.Final" implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
/** Debugging **/ /** Debugging **/
// Memory leak detection // Memory leak detection

View File

@@ -5,22 +5,21 @@
## Rules for NewPipeExtractor ## Rules for NewPipeExtractor
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; } -keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
## Rules for Rhino and Rhino Engine
-keep class org.mozilla.javascript.* { *; }
-keep class org.mozilla.javascript.** { *; } -keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.javascript.engine.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter -keep class org.mozilla.classfile.ClassFileWriter
-dontwarn org.mozilla.javascript.JavaToJSONConverters
-dontwarn org.mozilla.javascript.tools.** -dontwarn org.mozilla.javascript.tools.**
-keep class javax.script.** { *; }
-dontwarn javax.script.**
-keep class jdk.dynalink.** { *; }
-dontwarn jdk.dynalink.**
## Rules for ExoPlayer ## Rules for ExoPlayer
-keep class com.google.android.exoplayer2.** { *; } -keep class com.google.android.exoplayer2.** { *; }
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
-dontwarn icepick.**
-keep class icepick.** { *; }
-keep class **$$Icepick { *; }
-keepclasseswithmembernames class * {
@icepick.* <fields>;
}
-keepnames class * { @icepick.State *;}
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp ## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
-dontwarn okhttp3.** -dontwarn okhttp3.**
-dontwarn okio.** -dontwarn okio.**

View File

@@ -0,0 +1,730 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "7591e8039faa74d8c0517dc867af9d3e",
"entities": [
{
"tableName": "subscriptions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "avatarUrl",
"columnName": "avatar_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subscriberCount",
"columnName": "subscriber_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "notificationMode",
"columnName": "notification_mode",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_subscriptions_service_id_url",
"unique": true,
"columnNames": [
"service_id",
"url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
}
],
"foreignKeys": []
},
{
"tableName": "search_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "creationDate",
"columnName": "creation_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "search",
"columnName": "search",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_search_history_search",
"unique": false,
"columnNames": [
"search"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
}
],
"foreignKeys": []
},
{
"tableName": "streams",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "streamType",
"columnName": "stream_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploader_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnail_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "viewCount",
"columnName": "view_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "textualUploadDate",
"columnName": "textual_upload_date",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "upload_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isUploadDateApproximation",
"columnName": "is_upload_date_approximation",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_streams_service_id_url",
"unique": true,
"columnNames": [
"service_id",
"url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
}
],
"foreignKeys": []
},
{
"tableName": "stream_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "streamUid",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accessDate",
"columnName": "access_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "repeatCount",
"columnName": "repeat_count",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"stream_id",
"access_date"
]
},
"indices": [
{
"name": "index_stream_history_stream_id",
"unique": false,
"columnNames": [
"stream_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
}
],
"foreignKeys": [
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "stream_state",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "streamUid",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "progressMillis",
"columnName": "progress_time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"stream_id"
]
},
"indices": [],
"foreignKeys": [
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "playlists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isThumbnailPermanent",
"columnName": "is_thumbnail_permanent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnailStreamId",
"columnName": "thumbnail_stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayIndex",
"columnName": "display_index",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "playlist_stream_join",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "playlistUid",
"columnName": "playlist_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "streamUid",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "index",
"columnName": "join_index",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"playlist_id",
"join_index"
]
},
"indices": [
{
"name": "index_playlist_stream_join_playlist_id_join_index",
"unique": true,
"columnNames": [
"playlist_id",
"join_index"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
},
{
"name": "index_playlist_stream_join_stream_id",
"unique": false,
"columnNames": [
"stream_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
}
],
"foreignKeys": [
{
"table": "playlists",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"playlist_id"
],
"referencedColumns": [
"uid"
]
},
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "remote_playlists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnail_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayIndex",
"columnName": "display_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "streamCount",
"columnName": "stream_count",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_remote_playlists_service_id_url",
"unique": true,
"columnNames": [
"service_id",
"url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
}
],
"foreignKeys": []
},
{
"tableName": "feed",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "streamId",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subscriptionId",
"columnName": "subscription_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"stream_id",
"subscription_id"
]
},
"indices": [
{
"name": "index_feed_subscription_id",
"unique": false,
"columnNames": [
"subscription_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
}
],
"foreignKeys": [
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
},
{
"table": "subscriptions",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"subscription_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "feed_group",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sortOrder",
"columnName": "sort_order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_feed_group_sort_order",
"unique": false,
"columnNames": [
"sort_order"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
}
],
"foreignKeys": []
},
{
"tableName": "feed_group_subscription_join",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "feedGroupId",
"columnName": "group_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subscriptionId",
"columnName": "subscription_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"group_id",
"subscription_id"
]
},
"indices": [
{
"name": "index_feed_group_subscription_join_subscription_id",
"unique": false,
"columnNames": [
"subscription_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
}
],
"foreignKeys": [
{
"table": "feed_group",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"group_id"
],
"referencedColumns": [
"uid"
]
},
{
"table": "subscriptions",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"subscription_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "feed_last_updated",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "subscriptionId",
"columnName": "subscription_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"subscription_id"
]
},
"indices": [],
"foreignKeys": [
{
"table": "subscriptions",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"subscription_id"
],
"referencedColumns": [
"uid"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7591e8039faa74d8c0517dc867af9d3e')"
]
}
}

View File

@@ -13,6 +13,8 @@ import org.junit.Assert.assertNull
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
import org.schabi.newpipe.extractor.ServiceList import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
@@ -22,13 +24,17 @@ class DatabaseMigrationTest {
private const val DEFAULT_SERVICE_ID = 0 private const val DEFAULT_SERVICE_ID = 0
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4" private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
private const val DEFAULT_TITLE = "Test Title" private const val DEFAULT_TITLE = "Test Title"
private const val DEFAULT_NAME = "Test Name"
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
private const val DEFAULT_DURATION = 480L private const val DEFAULT_DURATION = 480L
private const val DEFAULT_UPLOADER_NAME = "Uploader Test" private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg" private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
private const val DEFAULT_SECOND_SERVICE_ID = 0 private const val DEFAULT_SECOND_SERVICE_ID = 1
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc" private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
private const val DEFAULT_THIRD_SERVICE_ID = 2
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
} }
@get:Rule @get:Rule
@@ -115,6 +121,13 @@ class DatabaseMigrationTest {
Migrations.MIGRATION_7_8 Migrations.MIGRATION_7_8
) )
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV3 = getMigratedDatabase() val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
@@ -198,6 +211,11 @@ class DatabaseMigrationTest {
true, Migrations.MIGRATION_7_8 true, Migrations.MIGRATION_7_8
) )
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
true, Migrations.MIGRATION_8_9
)
val migratedDatabaseV8 = getMigratedDatabase() val migratedDatabaseV8 = getMigratedDatabase()
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst() val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
@@ -207,6 +225,94 @@ class DatabaseMigrationTest {
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId) assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
} }
@Test
fun migrateDatabaseFrom8to9() {
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
val localUid1: Long
val localUid2: Long
val remoteUid1: Long
val remoteUid2: Long
databaseInV8.run {
localUid1 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "1")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
localUid2 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "2")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
delete(
"playlists", "uid = ?",
Array(1) { localUid1 }
)
remoteUid1 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
}
)
remoteUid2 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
}
)
delete(
"remote_playlists", "uid = ?",
Array(1) { remoteUid2 }
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV9 = getMigratedDatabase()
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(1, localListFromDB.size)
assertEquals(localUid2, localListFromDB[0].uid)
assertEquals(-1, localListFromDB[0].displayIndex)
assertEquals(1, remoteListFromDB.size)
assertEquals(remoteUid1, remoteListFromDB[0].uid)
assertEquals(-1, remoteListFromDB[0].displayIndex)
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
)
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
PlaylistRemoteEntity(
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
)
)
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(2, localListFromDB.size)
assertEquals(localUid3, localListFromDB[1].uid)
assertEquals(-1, localListFromDB[1].displayIndex)
assertEquals(2, remoteListFromDB.size)
assertEquals(remoteUid3, remoteListFromDB[1].uid)
assertEquals(-1, remoteListFromDB[1].displayIndex)
}
private fun getMigratedDatabase(): AppDatabase { private fun getMigratedDatabase(): AppDatabase {
val database: AppDatabase = Room.databaseBuilder( val database: AppDatabase = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),

View File

@@ -12,6 +12,7 @@ import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -23,8 +24,23 @@ import static org.junit.Assert.assertTrue;
@LargeTest @LargeTest
public class ErrorInfoTest { public class ErrorInfoTest {
/**
* @param errorInfo the error info to access
* @return the private field errorInfo.message.stringRes using reflection
*/
private int getMessageFromErrorInfo(final ErrorInfo errorInfo)
throws NoSuchFieldException, IllegalAccessException {
final var message = ErrorInfo.class.getDeclaredField("message");
message.setAccessible(true);
final var messageValue = (ErrorInfo.Companion.ErrorMessage) message.get(errorInfo);
final var stringRes = ErrorInfo.Companion.ErrorMessage.class.getDeclaredField("stringRes");
stringRes.setAccessible(true);
return (int) Objects.requireNonNull(stringRes.get(messageValue));
}
@Test @Test
public void errorInfoTestParcelable() { public void errorInfoTestParcelable() throws NoSuchFieldException, IllegalAccessException {
final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"), final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId()); UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
// Obtain a Parcel object and write the parcelable object to it: // Obtain a Parcel object and write the parcelable object to it:
@@ -39,7 +55,7 @@ public class ErrorInfoTest {
assertEquals(ServiceList.YouTube.getServiceInfo().getName(), assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
infoFromParcel.getServiceName()); infoFromParcel.getServiceName());
assertEquals("request", infoFromParcel.getRequest()); assertEquals("request", infoFromParcel.getRequest());
assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId()); assertEquals(R.string.parsing_error, getMessageFromErrorInfo(infoFromParcel));
parcel.recycle(); parcel.recycle();
} }

View File

@@ -9,6 +9,8 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- We need to be able to open links in the browser on API 30+ --> <!-- We need to be able to open links in the browser on API 30+ -->
@@ -57,6 +59,15 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
<service <service
android:name=".player.PlayerService" android:name=".player.PlayerService"
android:exported="true" android:exported="true"
@@ -64,6 +75,9 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service> </service>
<activity <activity
@@ -82,9 +96,22 @@
android:exported="false" android:exported="false"
android:label="@string/title_activity_about" /> android:label="@string/title_activity_about" />
<service android:name=".local.subscription.services.SubscriptionsImportService" /> <service
<service android:name=".local.subscription.services.SubscriptionsExportService" /> android:name=".local.subscription.services.SubscriptionsImportService"
<service android:name=".local.feed.service.FeedLoadService" /> android:foregroundServiceType="dataSync" />
<service
android:name=".local.subscription.services.SubscriptionsExportService"
android:foregroundServiceType="dataSync" />
<service
android:name=".local.feed.service.FeedLoadService"
android:foregroundServiceType="dataSync" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<activity <activity
android:name=".PanicResponderActivity" android:name=".PanicResponderActivity"
@@ -116,7 +143,8 @@
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<service android:name="us.shandian.giga.service.DownloadManagerService" /> <service android:name="us.shandian.giga.service.DownloadManagerService"
android:foregroundServiceType="dataSync" />
<activity <activity
android:name=".util.FilePickerActivityHelper" android:name=".util.FilePickerActivityHelper"
@@ -367,6 +395,7 @@
<data android:host="tilvids.com" /> <data android:host="tilvids.com" />
<data android:host="video.lqdn.fr" /> <data android:host="video.lqdn.fr" />
<data android:host="video.ploud.fr" /> <data android:host="video.ploud.fr" />
<data android:host="subscribeto.me" />
<data android:pathPrefix="/videos/" /> <!-- it contains playlists --> <data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
<data android:pathPrefix="/w/" /> <!-- short video URLs --> <data android:pathPrefix="/w/" /> <!-- short video URLs -->
@@ -408,6 +437,7 @@
</activity> </activity>
<service <service
android:name=".RouterActivity$FetcherService" android:name=".RouterActivity$FetcherService"
android:foregroundServiceType="dataSync"
android:exported="false" /> android:exported="false" />
<!-- opting out of sending metrics to Google in Android System WebView --> <!-- opting out of sending metrics to Google in Android System WebView -->
@@ -423,5 +453,10 @@
<meta-data <meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive" android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" /> android:value="true" />
<!-- Android Auto -->
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@mipmap/ic_launcher" />
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en"><head><title></title><script>
/**
* Factory method to create and load a BotGuardClient instance.
* @param options - Configuration options for the BotGuardClient.
* @returns A promise that resolves to a loaded BotGuardClient instance.
*/
function loadBotGuard(challengeData) {
this.vm = this[challengeData.globalName];
this.program = challengeData.program;
this.vmFunctions = {};
this.syncSnapshotFunction = null;
if (!this.vm)
throw new Error('[BotGuardClient]: VM not found in the global object');
if (!this.vm.a)
throw new Error('[BotGuardClient]: Could not load program');
const vmFunctionsCallback = function (
asyncSnapshotFunction,
shutdownFunction,
passEventFunction,
checkCameraFunction
) {
this.vmFunctions = {
asyncSnapshotFunction: asyncSnapshotFunction,
shutdownFunction: shutdownFunction,
passEventFunction: passEventFunction,
checkCameraFunction: checkCameraFunction
};
};
this.syncSnapshotFunction = this.vm.a(this.program, vmFunctionsCallback, true, this.userInteractionElement, function () {/** no-op */ }, [ [], [] ])[0]
// an asynchronous function runs in the background and it will eventually call
// `vmFunctionsCallback`, however we need to manually tell JavaScript to pass
// control to the things running in the background by interrupting this async
// function in any way, e.g. with a delay of 1ms. The loop is most probably not
// needed but is there just because.
return new Promise(function (resolve, reject) {
i = 0
refreshIntervalId = setInterval(function () {
if (!!this.vmFunctions.asyncSnapshotFunction) {
resolve(this)
clearInterval(refreshIntervalId);
}
if (i >= 10000) {
reject("asyncSnapshotFunction is null even after 10 seconds")
clearInterval(refreshIntervalId);
}
i += 1;
}, 1);
})
}
/**
* Takes a snapshot asynchronously.
* @returns The snapshot result.
* @example
* ```ts
* const result = await botguard.snapshot({
* contentBinding: {
* c: "a=6&a2=10&b=SZWDwKVIuixOp7Y4euGTgwckbJA&c=1729143849&d=1&t=7200&c1a=1&c6a=1&c6b=1&hh=HrMb5mRWTyxGJphDr0nW2Oxonh0_wl2BDqWuLHyeKLo",
* e: "ENGAGEMENT_TYPE_VIDEO_LIKE",
* encryptedVideoId: "P-vC09ZJcnM"
* }
* });
*
* console.log(result);
* ```
*/
function snapshot(args) {
return new Promise(function (resolve, reject) {
if (!this.vmFunctions.asyncSnapshotFunction)
return reject(new Error('[BotGuardClient]: Async snapshot function not found'));
this.vmFunctions.asyncSnapshotFunction(function (response) { resolve(response) }, [
args.contentBinding,
args.signedTimestamp,
args.webPoSignalOutput,
args.skipPrivacyBuffer
]);
});
}
function runBotGuard(challengeData) {
const interpreterJavascript = challengeData.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
const webPoSignalOutput = [];
return loadBotGuard({
globalName: challengeData.globalName,
globalObj: this,
program: challengeData.program
}).then(function (botguard) {
return botguard.snapshot({ webPoSignalOutput: webPoSignalOutput })
}).then(function (botguardResponse) {
return { webPoSignalOutput: webPoSignalOutput, botguardResponse: botguardResponse }
})
}
function obtainPoToken(webPoSignalOutput, integrityToken, identifier) {
const getMinter = webPoSignalOutput[0];
if (!getMinter)
throw new Error('PMD:Undefined');
const mintCallback = getMinter(integrityToken);
if (!(mintCallback instanceof Function))
throw new Error('APF:Failed');
const result = mintCallback(identifier);
if (!result)
throw new Error('YNJ:Undefined');
if (!(result instanceof Uint8Array))
throw new Error('ODM:Invalid');
return result;
}
</script></head><body></body></html>

View File

@@ -17,14 +17,17 @@ import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.error.ReCaptchaActivity; import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader; 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.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; 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.image.PreferredImageQuality;
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
@@ -99,8 +102,9 @@ public class App extends Application {
NewPipe.init(getDownloader(), NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this), Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this)); Localization.getPreferredContentCountry(this));
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext())); Localization.initPrettyTime(Localization.resolvePrettyTime());
BridgeStateSaverInitializer.init(this);
StateSaver.init(this); StateSaver.init(this);
initNotificationChannels(); initNotificationChannels();
@@ -116,6 +120,8 @@ public class App extends Application {
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false)); && prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
configureRxJavaErrorHandler(); configureRxJavaErrorHandler();
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl.INSTANCE);
} }
@Override @Override

View File

@@ -10,8 +10,9 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import icepick.Icepick; import com.evernote.android.state.State;
import icepick.State; import com.livefront.bridge.Bridge;
public abstract class BaseFragment extends Fragment { public abstract class BaseFragment extends Fragment {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
@@ -48,7 +49,7 @@ public abstract class BaseFragment extends Fragment {
+ "savedInstanceState = [" + savedInstanceState + "]"); + "savedInstanceState = [" + savedInstanceState + "]");
} }
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState); Bridge.restoreInstanceState(this, savedInstanceState);
if (savedInstanceState != null) { if (savedInstanceState != null) {
onRestoreInstanceState(savedInstanceState); onRestoreInstanceState(savedInstanceState);
} }
@@ -70,7 +71,7 @@ public abstract class BaseFragment extends Fragment {
@Override @Override
public void onSaveInstanceState(@NonNull final Bundle outState) { public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Bridge.saveInstanceState(this, outState);
} }
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {

View File

@@ -29,7 +29,7 @@ import okhttp3.ResponseBody;
public final class DownloaderImpl extends Downloader { public final class DownloaderImpl extends Downloader {
public static final String USER_AGENT = public static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"; "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY = public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
"youtube_restricted_mode_key"; "youtube_restricted_mode_key";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000"; public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
@@ -137,7 +137,8 @@ public final class DownloaderImpl extends Downloader {
} }
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder() final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url) .method(httpMethod, requestBody)
.url(url)
.addHeader("User-Agent", USER_AGENT); .addHeader("User-Agent", USER_AGENT);
final String cookies = getCookies(url); final String cookies = getCookies(url);
@@ -145,38 +146,33 @@ public final class DownloaderImpl extends Downloader {
requestBuilder.addHeader("Cookie", cookies); requestBuilder.addHeader("Cookie", cookies);
} }
for (final Map.Entry<String, List<String>> pair : headers.entrySet()) { headers.forEach((headerName, headerValueList) -> {
final String headerName = pair.getKey(); requestBuilder.removeHeader(headerName);
final List<String> headerValueList = pair.getValue(); headerValueList.forEach(headerValue ->
requestBuilder.addHeader(headerName, headerValue));
});
if (headerValueList.size() > 1) { try (
requestBuilder.removeHeader(headerName); okhttp3.Response response = client.newCall(requestBuilder.build()).execute()
for (final String headerValue : headerValueList) { ) {
requestBuilder.addHeader(headerName, headerValue); if (response.code() == 429) {
} throw new ReCaptchaException("reCaptcha Challenge requested", url);
} else if (headerValueList.size() == 1) {
requestBuilder.header(headerName, headerValueList.get(0));
} }
String responseBodyToReturn = null;
try (ResponseBody body = response.body()) {
if (body != null) {
responseBodyToReturn = body.string();
}
}
final String latestUrl = response.request().url().toString();
return new Response(
response.code(),
response.message(),
response.headers().toMultimap(),
responseBodyToReturn,
latestUrl);
} }
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
if (response.code() == 429) {
response.close();
throw new ReCaptchaException("reCaptcha Challenge requested", url);
}
final ResponseBody body = response.body();
String responseBodyToReturn = null;
if (body != null) {
responseBodyToReturn = body.string();
}
final String latestUrl = response.request().url().toString();
return new Response(response.code(), response.message(), response.headers().toMultimap(),
responseBodyToReturn, latestUrl);
} }
} }

View File

@@ -20,8 +20,6 @@
package org.schabi.newpipe; package org.schabi.newpipe;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -38,6 +36,7 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.FrameLayout; import android.widget.FrameLayout;
@@ -49,6 +48,7 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat; import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout; import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@@ -80,6 +80,7 @@ import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.settings.UpdateSettingsFragment; import org.schabi.newpipe.settings.UpdateSettingsFragment;
import org.schabi.newpipe.settings.migration.MigrationManager;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
@@ -92,6 +93,7 @@ import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.FocusOverlayView; import org.schabi.newpipe.views.FocusOverlayView;
import java.util.ArrayList; import java.util.ArrayList;
@@ -120,10 +122,14 @@ public class MainActivity extends AppCompatActivity {
private static final int ITEM_ID_DOWNLOADS = -4; private static final int ITEM_ID_DOWNLOADS = -4;
private static final int ITEM_ID_HISTORY = -5; private static final int ITEM_ID_HISTORY = -5;
private static final int ITEM_ID_SETTINGS = 0; private static final int ITEM_ID_SETTINGS = 0;
private static final int ITEM_ID_ABOUT = 1; private static final int ITEM_ID_DONATION = 1;
private static final int ITEM_ID_ABOUT = 2;
private static final int ORDER = 0; private static final int ORDER = 0;
public static final String KEY_IS_IN_BACKGROUND = "is_in_background";
private SharedPreferences sharedPreferences;
private SharedPreferences.Editor sharedPrefEditor;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Activity's LifeCycle // Activity's LifeCycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@@ -135,11 +141,26 @@ public class MainActivity extends AppCompatActivity {
+ "savedInstanceState = [" + savedInstanceState + "]"); + "savedInstanceState = [" + savedInstanceState + "]");
} }
Localization.migrateAppLanguageSettingIfNecessary(getApplicationContext());
ThemeHelper.setDayNightMode(this); ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
assureCorrectAppLanguage(this); // Fixes text color turning black in dark/black mode:
// https://github.com/TeamNewPipe/NewPipe/issues/12016
// For further reference see: https://issuetracker.google.com/issues/37124582
if (DeviceUtils.supportsWebView()) {
try {
new WebView(this);
} catch (final Throwable e) {
if (DEBUG) {
Log.e(TAG, "Failed to create WebView", e);
}
}
}
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
sharedPrefEditor = sharedPreferences.edit();
mainBinding = ActivityMainBinding.inflate(getLayoutInflater()); mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
drawerLayoutBinding = mainBinding.drawerLayout; drawerLayoutBinding = mainBinding.drawerLayout;
@@ -174,6 +195,8 @@ public class MainActivity extends AppCompatActivity {
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) { && ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
UpdateSettingsFragment.askForConsentToUpdateChecks(this); UpdateSettingsFragment.askForConsentToUpdateChecks(this);
} }
MigrationManager.showUserInfoIfPresent(this);
} }
@Override @Override
@@ -181,16 +204,29 @@ public class MainActivity extends AppCompatActivity {
super.onPostCreate(savedInstanceState); super.onPostCreate(savedInstanceState);
final App app = App.getApp(); final App app = App.getApp();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
if (prefs.getBoolean(app.getString(R.string.update_app_key), false) if (sharedPreferences.getBoolean(app.getString(R.string.update_app_key), false)
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) { && sharedPreferences
.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
// Start the worker which is checking all conditions // Start the worker which is checking all conditions
// and eventually searching for a new version. // and eventually searching for a new version.
NewVersionWorker.enqueueNewVersionCheckingWork(app, false); NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
} }
} }
@Override
protected void onStart() {
super.onStart();
sharedPrefEditor.putBoolean(KEY_IS_IN_BACKGROUND, false).apply();
Log.d(TAG, "App moved to foreground");
}
@Override
protected void onStop() {
super.onStop();
sharedPrefEditor.putBoolean(KEY_IS_IN_BACKGROUND, true).apply();
Log.d(TAG, "App moved to background");
}
private void setupDrawer() throws ExtractionException { private void setupDrawer() throws ExtractionException {
addDrawerMenuForCurrentService(); addDrawerMenuForCurrentService();
@@ -228,19 +264,6 @@ public class MainActivity extends AppCompatActivity {
*/ */
private void addDrawerMenuForCurrentService() throws ExtractionException { private void addDrawerMenuForCurrentService() throws ExtractionException {
//Tabs //Tabs
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
final StreamingService service = NewPipe.getService(currentServiceId);
int kioskMenuItemId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks));
kioskMenuItemId++;
}
drawerLayoutBinding.navigation.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
R.string.tab_subscriptions) R.string.tab_subscriptions)
@@ -258,10 +281,28 @@ public class MainActivity extends AppCompatActivity {
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
.setIcon(R.drawable.ic_history); .setIcon(R.drawable.ic_history);
//Kiosks
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
final StreamingService service = NewPipe.getService(currentServiceId);
int kioskMenuItemId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_kiosks_group, kioskMenuItemId, 0, KioskTranslator
.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks));
kioskMenuItemId++;
}
//Settings and About //Settings and About
drawerLayoutBinding.navigation.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
.setIcon(R.drawable.ic_settings); .setIcon(R.drawable.ic_settings);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_DONATION, ORDER,
R.string.donation_title)
.setIcon(R.drawable.volunteer_activism_ic);
drawerLayoutBinding.navigation.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
.setIcon(R.drawable.ic_info_outline); .setIcon(R.drawable.ic_info_outline);
@@ -273,10 +314,13 @@ public class MainActivity extends AppCompatActivity {
changeService(item); changeService(item);
break; break;
case R.id.menu_tabs_group: case R.id.menu_tabs_group:
tabSelected(item);
break;
case R.id.menu_kiosks_group:
try { try {
tabSelected(item); kioskSelected(item);
} catch (final Exception e) { } catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting main page tab", e); ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
} }
break; break;
case R.id.menu_options_about_group: case R.id.menu_options_about_group:
@@ -300,7 +344,7 @@ public class MainActivity extends AppCompatActivity {
.setChecked(true); .setChecked(true);
} }
private void tabSelected(final MenuItem item) throws ExtractionException { private void tabSelected(final MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case ITEM_ID_SUBSCRIPTIONS: case ITEM_ID_SUBSCRIPTIONS:
NavigationHelper.openSubscriptionFragment(getSupportFragmentManager()); NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
@@ -317,18 +361,19 @@ public class MainActivity extends AppCompatActivity {
case ITEM_ID_HISTORY: case ITEM_ID_HISTORY:
NavigationHelper.openStatisticFragment(getSupportFragmentManager()); NavigationHelper.openStatisticFragment(getSupportFragmentManager());
break; break;
default: }
final StreamingService currentService = ServiceHelper.getSelectedService(this); }
int kioskMenuItemId = 0;
for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) { private void kioskSelected(final MenuItem item) throws ExtractionException {
if (kioskMenuItemId == item.getItemId()) { final StreamingService currentService = ServiceHelper.getSelectedService(this);
NavigationHelper.openKioskFragment(getSupportFragmentManager(), int kioskMenuItemId = 0;
currentService.getServiceId(), kioskId); for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
break; if (kioskMenuItemId == item.getItemId()) {
} NavigationHelper.openKioskFragment(getSupportFragmentManager(),
kioskMenuItemId++; currentService.getServiceId(), kioskId);
}
break; break;
}
kioskMenuItemId++;
} }
} }
@@ -337,6 +382,9 @@ public class MainActivity extends AppCompatActivity {
case ITEM_ID_SETTINGS: case ITEM_ID_SETTINGS:
NavigationHelper.openSettings(this); NavigationHelper.openSettings(this);
break; break;
case ITEM_ID_DONATION:
ShareUtils.openUrlInBrowser(this, getString(R.string.donation_url));
break;
case ITEM_ID_ABOUT: case ITEM_ID_ABOUT:
NavigationHelper.openAbout(this); NavigationHelper.openAbout(this);
break; break;
@@ -366,6 +414,7 @@ public class MainActivity extends AppCompatActivity {
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group); drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group);
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group); drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_kiosks_group);
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group); drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
// Show up or down arrow // Show up or down arrow
@@ -459,9 +508,8 @@ public class MainActivity extends AppCompatActivity {
@Override @Override
protected void onResume() { protected void onResume() {
assureCorrectAppLanguage(this);
// Change the date format to match the selected language on resume // Change the date format to match the selected language on resume
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext())); Localization.initPrettyTime(Localization.resolvePrettyTime());
super.onResume(); super.onResume();
// Close drawer on return, and don't show animation, // Close drawer on return, and don't show animation,
@@ -483,13 +531,11 @@ public class MainActivity extends AppCompatActivity {
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e); ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
} }
final SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) { if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Theme has changed, recreating activity..."); Log.d(TAG, "Theme has changed, recreating activity...");
} }
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); sharedPrefEditor.putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
ActivityCompat.recreate(this); ActivityCompat.recreate(this);
} }
@@ -497,7 +543,7 @@ public class MainActivity extends AppCompatActivity {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "main page has changed, recreating main fragment..."); Log.d(TAG, "main page has changed, recreating main fragment...");
} }
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply(); sharedPrefEditor.putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
NavigationHelper.openMainActivity(this); NavigationHelper.openMainActivity(this);
} }
@@ -839,7 +885,8 @@ public class MainActivity extends AppCompatActivity {
@Override @Override
public void onReceive(final Context context, final Intent intent) { public void onReceive(final Context context, final Intent intent) {
if (Objects.equals(intent.getAction(), if (Objects.equals(intent.getAction(),
VideoDetailFragment.ACTION_PLAYER_STARTED)) { VideoDetailFragment.ACTION_PLAYER_STARTED)
&& PlayerHolder.getInstance().isPlayerOpen()) {
openMiniPlayerIfMissing(); openMiniPlayerIfMissing();
// At this point the player is added 100%, we can unregister. Other actions // At this point the player is added 100%, we can unregister. Other actions
// are useless since the fragment will not be removed after that. // are useless since the fragment will not be removed after that.
@@ -850,7 +897,12 @@ public class MainActivity extends AppCompatActivity {
}; };
final IntentFilter intentFilter = new IntentFilter(); final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED); intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
registerReceiver(broadcastReceiver, intentFilter); ContextCompat.registerReceiver(this, broadcastReceiver, intentFilter,
ContextCompat.RECEIVER_EXPORTED);
// If the PlayerHolder is not bound yet, but the service is running, try to bind to it.
// Once the connection is established, the ACTION_PLAYER_STARTED will be sent.
PlayerHolder.getInstance().tryBindIfNeeded(this);
} }
} }
@@ -924,4 +976,5 @@ public class MainActivity extends AppCompatActivity {
return sheetState == BottomSheetBehavior.STATE_HIDDEN return sheetState == BottomSheetBehavior.STATE_HIDDEN
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED; || sheetState == BottomSheetBehavior.STATE_COLLAPSED;
} }
} }

View File

@@ -8,6 +8,7 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6; import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7; import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8; import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
@@ -28,7 +29,7 @@ public final class NewPipeDatabase {
return Room return Room
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8) MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
.build(); .build();
} }

View File

@@ -41,6 +41,9 @@ import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding; import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
@@ -55,20 +58,10 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.StreamingService.LinkType; import org.schabi.newpipe.extractor.StreamingService.LinkType;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
@@ -81,7 +74,6 @@ import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@@ -98,8 +90,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import icepick.Icepick;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
@@ -131,7 +121,6 @@ public class RouterActivity extends AppCompatActivity {
ThemeHelper.setDayNightMode(this); ThemeHelper.setDayNightMode(this);
setTheme(ThemeHelper.isLightThemeSelected(this) setTheme(ThemeHelper.isLightThemeSelected(this)
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
Localization.assureCorrectAppLanguage(this);
// Pass-through touch events to background activities // Pass-through touch events to background activities
// so that our transparent window won't lock UI in the mean time // so that our transparent window won't lock UI in the mean time
@@ -152,7 +141,7 @@ public class RouterActivity extends AppCompatActivity {
getWindow().setAttributes(params); getWindow().setAttributes(params);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState); Bridge.restoreInstanceState(this, savedInstanceState);
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates // FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments // We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
@@ -197,7 +186,7 @@ public class RouterActivity extends AppCompatActivity {
@Override @Override
protected void onSaveInstanceState(@NonNull final Bundle outState) { protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Bridge.saveInstanceState(this, outState);
} }
@Override @Override
@@ -261,7 +250,8 @@ public class RouterActivity extends AppCompatActivity {
showUnsupportedUrlDialog(url); showUnsupportedUrlDialog(url);
} }
}, throwable -> handleError(this, new ErrorInfo(throwable, }, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url)))); UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url,
null, url))));
} }
/** /**
@@ -270,40 +260,19 @@ public class RouterActivity extends AppCompatActivity {
* @param errorInfo the error information * @param errorInfo the error information
*/ */
private static void handleError(final Context context, final ErrorInfo errorInfo) { private static void handleError(final Context context, final ErrorInfo errorInfo) {
if (errorInfo.getThrowable() != null) { if (errorInfo.getRecaptchaUrl() != null) {
errorInfo.getThrowable().printStackTrace();
}
if (errorInfo.getThrowable() instanceof ReCaptchaException) {
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity // Starting ReCaptcha Challenge Activity
final Intent intent = new Intent(context, ReCaptchaActivity.class); final Intent intent = new Intent(context, ReCaptchaActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.getRecaptchaUrl());
context.startActivity(intent); context.startActivity(intent);
} else if (errorInfo.getThrowable() != null } else if (errorInfo.isReportable()) {
&& ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) {
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) {
Toast.makeText(context, R.string.restricted_video_no_stream,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) {
Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PaidContentException) {
Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PrivateContentException) {
Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) {
Toast.makeText(context, R.string.soundcloud_go_plus_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) {
Toast.makeText(context, R.string.youtube_music_premium_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) {
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
} else {
ErrorUtil.createNotification(context, errorInfo); ErrorUtil.createNotification(context, errorInfo);
} else {
// this exception does not usually indicate a problem that should be reported,
// so just show a toast instead of the notification
Toast.makeText(context, errorInfo.getMessage(context), Toast.LENGTH_LONG).show();
} }
if (context instanceof RouterActivity) { if (context instanceof RouterActivity) {
@@ -666,7 +635,8 @@ public class RouterActivity extends AppCompatActivity {
startActivity(intent); startActivity(intent);
finish(); finish();
}, throwable -> handleError(this, new ErrorInfo(throwable, }, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl))) UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl,
null, currentUrl)))
); );
return; return;
} }
@@ -853,10 +823,10 @@ public class RouterActivity extends AppCompatActivity {
}) })
)), )),
throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo( throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo(
throwable, throwable, UserAction.REQUESTED_STREAM,
UserAction.REQUESTED_STREAM,
"Tried to add " + currentUrl + " to a playlist", "Tried to add " + currentUrl + " to a playlist",
((RouterActivity) ctx).currentService.getServiceId()) ((RouterActivity) ctx).currentService.getServiceId(),
currentUrl)
)) ))
) )
); );
@@ -996,7 +966,7 @@ public class RouterActivity extends AppCompatActivity {
} }
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction, }, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
choice.url + " opened with " + choice.playerChoice, choice.url + " opened with " + choice.playerChoice,
choice.serviceId))); choice.serviceId, choice.url)));
} }
} }

View File

@@ -16,14 +16,12 @@ import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityAboutBinding import org.schabi.newpipe.databinding.ActivityAboutBinding
import org.schabi.newpipe.databinding.FragmentAboutBinding import org.schabi.newpipe.databinding.FragmentAboutBinding
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils import org.schabi.newpipe.util.external_communication.ShareUtils
class AboutActivity : AppCompatActivity() { class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Localization.assureCorrectAppLanguage(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeHelper.setTheme(this) ThemeHelper.setTheme(this)
title = getString(R.string.title_activity_about) title = getString(R.string.title_activity_about)
@@ -138,8 +136,12 @@ class AboutActivity : AppCompatActivity() {
"https://github.com/lisawray/groupie", StandardLicenses.MIT "https://github.com/lisawray/groupie", StandardLicenses.MIT
), ),
SoftwareComponent( SoftwareComponent(
"Icepick", "2015", "Frankie Sardo", "Android-State", "2018", "Evernote",
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1 "https://github.com/Evernote/android-state", StandardLicenses.EPL1
),
SoftwareComponent(
"Bridge", "2021", "Livefront",
"https://github.com/livefront/bridge", StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"Jsoup", "2009 - 2020", "Jonathan Hedley", "Jsoup", "2009 - 2020", "Jonathan Hedley",

View File

@@ -19,7 +19,6 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FragmentLicensesBinding import org.schabi.newpipe.databinding.FragmentLicensesBinding
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
import org.schabi.newpipe.ktx.parcelableArrayList import org.schabi.newpipe.ktx.parcelableArrayList
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.external_communication.ShareUtils import org.schabi.newpipe.util.external_communication.ShareUtils
/** /**
@@ -100,7 +99,6 @@ class LicenseFragment : Fragment() {
val webView = WebView(context) val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64") webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
Localization.assureCorrectAppLanguage(context)
val builder = AlertDialog.Builder(requireContext()) val builder = AlertDialog.Builder(requireContext())
.setTitle(softwareComponent.name) .setTitle(softwareComponent.name)
.setView(webView) .setView(webView)

View File

@@ -1,6 +1,6 @@
package org.schabi.newpipe.database; package org.schabi.newpipe.database;
import static org.schabi.newpipe.database.Migrations.DB_VER_8; import static org.schabi.newpipe.database.Migrations.DB_VER_9;
import androidx.room.Database; import androidx.room.Database;
import androidx.room.RoomDatabase; import androidx.room.RoomDatabase;
@@ -38,7 +38,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
FeedLastUpdatedEntity.class FeedLastUpdatedEntity.class
}, },
version = DB_VER_8 version = DB_VER_9
) )
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "newpipe.db"; public static final String DATABASE_NAME = "newpipe.db";

View File

@@ -26,6 +26,7 @@ public final class Migrations {
public static final int DB_VER_6 = 6; public static final int DB_VER_6 = 6;
public static final int DB_VER_7 = 7; public static final int DB_VER_7 = 7;
public static final int DB_VER_8 = 8; public static final int DB_VER_8 = 8;
public static final int DB_VER_9 = 9;
private static final String TAG = Migrations.class.getName(); private static final String TAG = Migrations.class.getName();
public static final boolean DEBUG = MainActivity.DEBUG; public static final boolean DEBUG = MainActivity.DEBUG;
@@ -187,7 +188,7 @@ public final class Migrations {
@Override @Override
public void migrate(@NonNull final SupportSQLiteDatabase database) { public void migrate(@NonNull final SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
+ "INTEGER NOT NULL DEFAULT 0"); + "INTEGER NOT NULL DEFAULT 0");
} }
}; };
@@ -245,6 +246,62 @@ public final class Migrations {
} }
}; };
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
try {
database.beginTransaction();
// Update playlists.
// Create a temp table to initialize display_index.
database.execSQL("CREATE TABLE `playlists_tmp` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
+ "`display_index` INTEGER NOT NULL)");
database.execSQL("INSERT INTO `playlists_tmp` "
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
+ "`display_index`) "
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
+ "-1 "
+ "FROM `playlists`");
// Replace the old table, note that this also removes the index on the name which
// we don't need anymore.
database.execSQL("DROP TABLE `playlists`");
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
// Update remote_playlists.
// Create a temp table to initialize display_index.
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
+ "`display_index` INTEGER NOT NULL,"
+ "`stream_count` INTEGER)");
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
+ "`stream_count`)"
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
+ "-1, `stream_count` FROM `remote_playlists`");
// Replace the old table, note that this also removes the index on the name which
// we don't need anymore.
database.execSQL("DROP TABLE `remote_playlists`");
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
// Create index on the new table.
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
+ "ON `remote_playlists` (`service_id`, `url`)");
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
};
private Migrations() { private Migrations() {
} }
} }

View File

@@ -3,6 +3,8 @@ package org.schabi.newpipe.database.history.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Embedded import androidx.room.Embedded
import org.schabi.newpipe.database.stream.model.StreamEntity 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 import java.time.OffsetDateTime
data class StreamHistoryEntry( data class StreamHistoryEntry(
@@ -27,4 +29,17 @@ data class StreamHistoryEntry(
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId && return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
accessDate.isEqual(other.accessDate) 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)
}
} }

View File

@@ -13,12 +13,17 @@ public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED) @ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
public final long timesStreamIsContained; public final long timesStreamIsContained;
@SuppressWarnings("checkstyle:ParameterNumber")
public PlaylistDuplicatesEntry(final long uid, public PlaylistDuplicatesEntry(final long uid,
final String name, final String name,
final String thumbnailUrl, final String thumbnailUrl,
final boolean isThumbnailPermanent,
final long thumbnailStreamId,
final long displayIndex,
final long streamCount, final long streamCount,
final long timesStreamIsContained) { final long timesStreamIsContained) {
super(uid, name, thumbnailUrl, streamCount); super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
streamCount);
this.timesStreamIsContained = timesStreamIsContained; this.timesStreamIsContained = timesStreamIsContained;
} }
} }

View File

@@ -1,22 +1,18 @@
package org.schabi.newpipe.database.playlist; package org.schabi.newpipe.database.playlist;
import org.schabi.newpipe.database.LocalItem; import androidx.annotation.Nullable;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import java.util.Comparator; import org.schabi.newpipe.database.LocalItem;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public interface PlaylistLocalItem extends LocalItem { public interface PlaylistLocalItem extends LocalItem {
String getOrderingName(); String getOrderingName();
static List<PlaylistLocalItem> merge( long getDisplayIndex();
final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) { long getUid();
return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
.sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName, void setDisplayIndex(long displayIndex);
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
.collect(Collectors.toList()); @Nullable
} String getThumbnailUrl();
} }

View File

@@ -2,27 +2,42 @@ package org.schabi.newpipe.database.playlist;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
import androidx.annotation.Nullable;
public class PlaylistMetadataEntry implements PlaylistLocalItem { public class PlaylistMetadataEntry implements PlaylistLocalItem {
public static final String PLAYLIST_STREAM_COUNT = "streamCount"; public static final String PLAYLIST_STREAM_COUNT = "streamCount";
@ColumnInfo(name = PLAYLIST_ID) @ColumnInfo(name = PLAYLIST_ID)
public final long uid; private final long uid;
@ColumnInfo(name = PLAYLIST_NAME) @ColumnInfo(name = PLAYLIST_NAME)
public final String name; public final String name;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
private final boolean isThumbnailPermanent;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
private final long thumbnailStreamId;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
public final String thumbnailUrl; public final String thumbnailUrl;
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
private long displayIndex;
@ColumnInfo(name = PLAYLIST_STREAM_COUNT) @ColumnInfo(name = PLAYLIST_STREAM_COUNT)
public final long streamCount; public final long streamCount;
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl, public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
final long streamCount) { final boolean isThumbnailPermanent, final long thumbnailStreamId,
final long displayIndex, final long streamCount) {
this.uid = uid; this.uid = uid;
this.name = name; this.name = name;
this.thumbnailUrl = thumbnailUrl; this.thumbnailUrl = thumbnailUrl;
this.isThumbnailPermanent = isThumbnailPermanent;
this.thumbnailStreamId = thumbnailStreamId;
this.displayIndex = displayIndex;
this.streamCount = streamCount; this.streamCount = streamCount;
} }
@@ -35,4 +50,33 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
public String getOrderingName() { public String getOrderingName() {
return name; return name;
} }
public boolean isThumbnailPermanent() {
return isThumbnailPermanent;
}
public long getThumbnailStreamId() {
return thumbnailStreamId;
}
@Override
public long getDisplayIndex() {
return displayIndex;
}
@Override
public long getUid() {
return uid;
}
@Override
public void setDisplayIndex(final long displayIndex) {
this.displayIndex = displayIndex;
}
@Nullable
@Override
public String getThumbnailUrl() {
return thumbnailUrl;
}
} }

View File

@@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.dao;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction;
import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
@@ -36,4 +37,17 @@ public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE) @Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
Flowable<Long> getCount(); Flowable<Long> getCount();
@Transaction
default long upsertPlaylist(final PlaylistEntity playlist) {
final long playlistId = playlist.getUid();
if (playlistId == -1) {
// This situation is probably impossible.
return insert(playlist);
} else {
update(playlist);
return playlistId;
}
}
} }

View File

@@ -11,6 +11,7 @@ import java.util.List;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
@@ -31,10 +32,18 @@ public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId); Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
+ REMOTE_PLAYLIST_ID + " = :playlistId")
Flowable<PlaylistRemoteEntity> getPlaylist(long playlistId);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url); Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
+ " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
Flowable<List<PlaylistRemoteEntity>> getPlaylists();
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url " + " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")

View File

@@ -18,10 +18,12 @@ import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED; import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
@@ -91,7 +93,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId); Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
@Transaction @Transaction
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + "," @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
+ PLAYLIST_DISPLAY_INDEX + ", "
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = " + " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'" + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
@@ -105,7 +109,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " GROUP BY " + PLAYLIST_ID + " GROUP BY " + PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") + " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata(); Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@@ -126,8 +130,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId); Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
@Transaction @Transaction
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " @Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
+ PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
+ PLAYLIST_DISPLAY_INDEX + ", "
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = " + " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'" + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
@@ -149,6 +154,6 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
+ " AND :streamUrl = :streamUrl" + " AND :streamUrl = :streamUrl"
+ " GROUP BY " + JOIN_PLAYLIST_ID + " GROUP BY " + JOIN_PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") + " ORDER BY " + PLAYLIST_DISPLAY_INDEX + ", " + PLAYLIST_NAME)
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl); Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
} }

View File

@@ -2,16 +2,15 @@ package org.schabi.newpipe.database.playlist.model;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.Index; import androidx.room.Ignore;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
@Entity(tableName = PLAYLIST_TABLE, @Entity(tableName = PLAYLIST_TABLE)
indices = {@Index(value = {PLAYLIST_NAME})})
public class PlaylistEntity { public class PlaylistEntity {
public static final String DEFAULT_THUMBNAIL = "drawable://" public static final String DEFAULT_THUMBNAIL = "drawable://"
@@ -22,6 +21,7 @@ public class PlaylistEntity {
public static final String PLAYLIST_ID = "uid"; public static final String PLAYLIST_ID = "uid";
public static final String PLAYLIST_NAME = "name"; public static final String PLAYLIST_NAME = "name";
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent"; public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id"; public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
@@ -38,11 +38,24 @@ public class PlaylistEntity {
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID) @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
private long thumbnailStreamId; private long thumbnailStreamId;
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
private long displayIndex;
public PlaylistEntity(final String name, final boolean isThumbnailPermanent, public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
final long thumbnailStreamId) { final long thumbnailStreamId, final long displayIndex) {
this.name = name; this.name = name;
this.isThumbnailPermanent = isThumbnailPermanent; this.isThumbnailPermanent = isThumbnailPermanent;
this.thumbnailStreamId = thumbnailStreamId; this.thumbnailStreamId = thumbnailStreamId;
this.displayIndex = displayIndex;
}
@Ignore
public PlaylistEntity(final PlaylistMetadataEntry item) {
this.uid = item.getUid();
this.name = item.name;
this.isThumbnailPermanent = item.isThumbnailPermanent();
this.thumbnailStreamId = item.getThumbnailStreamId();
this.displayIndex = item.getDisplayIndex();
} }
public long getUid() { public long getUid() {
@@ -77,4 +90,11 @@ public class PlaylistEntity {
this.isThumbnailPermanent = isThumbnailSet; this.isThumbnailPermanent = isThumbnailSet;
} }
public long getDisplayIndex() {
return displayIndex;
}
public void setDisplayIndex(final long displayIndex) {
this.displayIndex = displayIndex;
}
} }

View File

@@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.model;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.Ignore; import androidx.room.Ignore;
@@ -21,7 +22,6 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
@Entity(tableName = REMOTE_PLAYLIST_TABLE, @Entity(tableName = REMOTE_PLAYLIST_TABLE,
indices = { indices = {
@Index(value = {REMOTE_PLAYLIST_NAME}),
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true) @Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
}) })
public class PlaylistRemoteEntity implements PlaylistLocalItem { public class PlaylistRemoteEntity implements PlaylistLocalItem {
@@ -32,6 +32,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
public static final String REMOTE_PLAYLIST_URL = "url"; public static final String REMOTE_PLAYLIST_URL = "url";
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index";
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count"; public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@@ -53,6 +54,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
private String uploader; private String uploader;
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
private long displayIndex = -1; // Make sure the new item is on the top
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
private Long streamCount; private Long streamCount;
@@ -67,6 +71,19 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
this.streamCount = streamCount; this.streamCount = streamCount;
} }
@Ignore
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
final String thumbnailUrl, final String uploader,
final long displayIndex, final Long streamCount) {
this.serviceId = serviceId;
this.name = name;
this.url = url;
this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader;
this.displayIndex = displayIndex;
this.streamCount = streamCount;
}
@Ignore @Ignore
public PlaylistRemoteEntity(final PlaylistInfo info) { public PlaylistRemoteEntity(final PlaylistInfo info) {
this(info.getServiceId(), info.getName(), info.getUrl(), this(info.getServiceId(), info.getName(), info.getUrl(),
@@ -93,6 +110,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
&& TextUtils.equals(getUploader(), info.getUploaderName()); && TextUtils.equals(getUploader(), info.getUploaderName());
} }
@Override
public long getUid() { public long getUid() {
return uid; return uid;
} }
@@ -117,6 +135,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
this.name = name; this.name = name;
} }
@Nullable
@Override
public String getThumbnailUrl() { public String getThumbnailUrl() {
return thumbnailUrl; return thumbnailUrl;
} }
@@ -141,6 +161,16 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
this.uploader = uploader; this.uploader = uploader;
} }
@Override
public long getDisplayIndex() {
return displayIndex;
}
@Override
public void setDisplayIndex(final long displayIndex) {
this.displayIndex = displayIndex;
}
public Long getStreamCount() { public Long getStreamCount() {
return streamCount; return streamCount;
} }

View File

@@ -1,6 +1,7 @@
package org.schabi.newpipe.database.subscription; package org.schabi.newpipe.database.subscription;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.Ignore; import androidx.room.Ignore;
@@ -95,11 +96,12 @@ public class SubscriptionEntity {
this.name = name; this.name = name;
} }
@Nullable
public String getAvatarUrl() { public String getAvatarUrl() {
return avatarUrl; return avatarUrl;
} }
public void setAvatarUrl(final String avatarUrl) { public void setAvatarUrl(@Nullable final String avatarUrl) {
this.avatarUrl = avatarUrl; this.avatarUrl = avatarUrl;
} }

View File

@@ -20,8 +20,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.fragment.MissionsFragment; import us.shandian.giga.ui.fragment.MissionsFragment;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadActivity extends AppCompatActivity { public class DownloadActivity extends AppCompatActivity {
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag"; private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
@@ -33,7 +31,6 @@ public class DownloadActivity extends AppCompatActivity {
i.setClass(this, DownloadManagerService.class); i.setClass(this, DownloadManagerService.class);
startService(i); startService(i);
assureCorrectAppLanguage(this);
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.download;
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP; import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery; import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity; import android.app.Activity;
import android.content.ComponentName; import android.content.ComponentName;
@@ -39,6 +38,8 @@ import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import com.nononsenseapps.filepicker.Utils; import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
@@ -59,6 +60,8 @@ import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.AudioTrackAdapter;
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
@@ -67,8 +70,6 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper; import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
import org.schabi.newpipe.util.AudioTrackAdapter;
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.io.File; import java.io.File;
@@ -79,8 +80,6 @@ import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import icepick.Icepick;
import icepick.State;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.postprocessing.Postprocessing;
@@ -214,7 +213,7 @@ public class DownloadDialog extends DialogFragment
context = getContext(); context = getContext();
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState); Bridge.restoreInstanceState(this, savedInstanceState);
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks); this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams); this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
@@ -372,7 +371,7 @@ public class DownloadDialog extends DialogFragment
@Override @Override
public void onSaveInstanceState(@NonNull final Bundle outState) { public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Bridge.saveInstanceState(this, outState);
} }
@@ -390,8 +389,7 @@ public class DownloadDialog extends DialogFragment
} }
}, throwable -> ErrorUtil.showSnackbar(context, }, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading video stream size", "Downloading video stream size", currentInfo))));
currentInfo.getServiceId()))));
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams()) disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
.subscribe(result -> { .subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
@@ -400,8 +398,7 @@ public class DownloadDialog extends DialogFragment
} }
}, throwable -> ErrorUtil.showSnackbar(context, }, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading audio stream size", "Downloading audio stream size", currentInfo))));
currentInfo.getServiceId()))));
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams) disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
.subscribe(result -> { .subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
@@ -410,8 +407,7 @@ public class DownloadDialog extends DialogFragment
} }
}, throwable -> ErrorUtil.showSnackbar(context, }, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading subtitle stream size", "Downloading subtitle stream size", currentInfo))));
currentInfo.getServiceId()))));
} }
private void setupAudioTrackSpinner() { private void setupAudioTrackSpinner() {
@@ -751,7 +747,6 @@ public class DownloadDialog extends DialogFragment
} }
private void showFailedDialog(@StringRes final int msg) { private void showFailedDialog(@StringRes final int msg) {
assureCorrectAppLanguage(requireContext());
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setTitle(R.string.general_error) .setTitle(R.string.general_error)
.setMessage(msg) .setMessage(msg)
@@ -859,20 +854,19 @@ public class DownloadDialog extends DialogFragment
return; return;
} }
// Check for free memory space (for api 24 and up) // Check for free storage space
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { final long freeSpace = mainStorage.getFreeStorageSpace();
final long freeSpace = mainStorage.getFreeMemory(); if (freeSpace <= size) {
if (freeSpace <= size) { Toast.makeText(context, getString(R.
Toast.makeText(context, getString(R. string.error_insufficient_storage), Toast.LENGTH_LONG).show();
string.error_insufficient_storage), Toast.LENGTH_LONG).show(); // move the user to storage setting tab
// move the user to storage setting tab final Intent storageSettingsIntent = new Intent(Settings.
final Intent storageSettingsIntent = new Intent(Settings. ACTION_INTERNAL_STORAGE_SETTINGS);
ACTION_INTERNAL_STORAGE_SETTINGS); if (storageSettingsIntent.resolveActivity(context.getPackageManager())
if (storageSettingsIntent.resolveActivity(context.getPackageManager()) != null) { != null) {
startActivity(storageSettingsIntent); startActivity(storageSettingsIntent);
}
return;
} }
return;
} }
// check for existing file with the same name // check for existing file with the same name

View File

@@ -1,11 +1,13 @@
package org.schabi.newpipe.error package org.schabi.newpipe.error;
import android.content.Context import android.content.Context;
import org.acra.ReportField
import org.acra.data.CrashReportData import androidx.annotation.NonNull;
import org.acra.sender.ReportSender
import org.schabi.newpipe.R import org.acra.ReportField;
import org.schabi.newpipe.error.ErrorUtil.Companion.openActivity import org.acra.data.CrashReportData;
import org.acra.sender.ReportSender;
import org.schabi.newpipe.R;
/* /*
* Created by Christian Schabesberger on 13.09.16. * Created by Christian Schabesberger on 13.09.16.
@@ -26,19 +28,16 @@ import org.schabi.newpipe.error.ErrorUtil.Companion.openActivity
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>. * along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/ */
class AcraReportSender : ReportSender {
override fun send(context: Context, errorContent: CrashReportData) {
openActivity( public class AcraReportSender implements ReportSender {
context,
ErrorInfo( @Override
errorContent.getString(ReportField.STACK_TRACE)?.let { arrayOf(it) } public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
?: emptyArray(), ErrorUtil.openActivity(context, new ErrorInfo(
new String[]{report.getString(ReportField.STACK_TRACE)},
UserAction.UI_ERROR, UserAction.UI_ERROR,
ErrorInfo.SERVICE_NONE,
"ACRA report", "ACRA report",
R.string.app_ui_crash null,
) R.string.app_ui_crash));
)
} }
} }

View File

@@ -0,0 +1,44 @@
package org.schabi.newpipe.error;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.auto.service.AutoService;
import org.acra.config.CoreConfiguration;
import org.acra.sender.ReportSender;
import org.acra.sender.ReportSenderFactory;
import org.schabi.newpipe.App;
/*
* Created by Christian Schabesberger on 13.09.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* AcraReportSenderFactory.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/>.
*/
/**
* Used by ACRA in {@link App}.initAcra() as the factory for report senders.
*/
@AutoService(ReportSenderFactory.class)
public class AcraReportSenderFactory implements ReportSenderFactory {
@NonNull
public ReportSender create(@NonNull final Context context,
@NonNull final CoreConfiguration config) {
return new AcraReportSender();
}
}

View File

@@ -0,0 +1,324 @@
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

@@ -1,353 +0,0 @@
package org.schabi.newpipe.error
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
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.MenuItem
import android.view.View
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.MainActivity
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.LocalDateTime
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 [ ][ErrorUtil.openActivity] to correctly open this activity.
*/
class ErrorActivity : AppCompatActivity() {
private lateinit var errorInfo: ErrorInfo
private lateinit var currentTimeStamp: String
private lateinit var activityErrorBinding: ActivityErrorBinding
// //////////////////////////////////////////////////////////////////////
// Activity lifecycle
// //////////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
Localization.assureCorrectAppLanguage(this)
super.onCreate(savedInstanceState)
ThemeHelper.setDayNightMode(this)
ThemeHelper.setTheme(this)
activityErrorBinding = ActivityErrorBinding.inflate(
layoutInflater
)
setContentView(activityErrorBinding.root)
val intent = intent
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar)
val actionBar = supportActionBar
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setTitle(R.string.error_report_title)
actionBar.setDisplayShowTitleEnabled(true)
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo::class.java)!!
// important add guru meditation
addGuruMeditation()
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now())
activityErrorBinding.errorReportEmailButton.setOnClickListener { _: View? ->
openPrivacyPolicyDialog(
this,
"EMAIL"
)
}
activityErrorBinding.errorReportCopyButton.setOnClickListener { _: View? ->
ShareUtils.copyToClipboard(
this,
buildMarkdown()
)
}
activityErrorBinding.errorReportGitHubButton.setOnClickListener { _: View? ->
openPrivacyPolicyDialog(
this,
"GITHUB"
)
}
// normal bugreport
buildInfo(errorInfo)
activityErrorBinding.errorMessageView.setText(errorInfo.messageStringId)
activityErrorBinding.errorView.text = formErrorText(errorInfo.stackTraces)
// print stack trace once again for debugging:
for (e in errorInfo.stackTraces) {
Log.e(TAG, e)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.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) { _: DialogInterface?, _: Int ->
ShareUtils.openUrlInApp(
context,
context.getString(R.string.privacy_policy_url)
)
}
.setPositiveButton(R.string.accept) { _: DialogInterface?, _: Int ->
if (action == "EMAIL") { // send on email
val i = Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse("mailto:")) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, arrayOf(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 == "GITHUB") { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
}
}
.setNegativeButton(R.string.decline, null)
.show()
}
private fun formErrorText(el: Array<String>): String {
val separator = "-------------------------------------"
return Arrays.stream(el)
.collect(
Collectors.joining(
"""
$separator
""".trimIndent(),
"""
$separator
""".trimIndent(),
separator
)
)
}
private fun buildInfo(info: ErrorInfo) {
var text = ""
activityErrorBinding.errorInfoLabelsView.text = getString(R.string.info_labels)
.replace("\\n", "\n")
text += """
${getUserActionString(info.userAction)}
${info.request}
$contentLanguageString
$contentCountryString
$appLanguage
${info.serviceName}
$currentTimeStamp
$packageName
${BuildConfig.VERSION_NAME}
$osString
""".trimIndent()
activityErrorBinding.errorInfosView.text = text
}
private fun buildJson(): String {
try {
return JsonWriter.string()
.`object`()
.value("user_action", getUserActionString(errorInfo.userAction))
.value("request", errorInfo.request)
.value("content_language", contentLanguageString)
.value("content_country", contentCountryString)
.value("app_language", appLanguage)
.value("service", errorInfo.serviceName)
.value("package", packageName)
.value("version", BuildConfig.VERSION_NAME)
.value("os", osString)
.value("time", currentTimeStamp)
.array("exceptions", listOf(*errorInfo.stackTraces))
.value(
"user_comment",
activityErrorBinding.errorCommentBox.text
.toString()
)
.end()
.done()
} catch (e: Throwable) {
Log.e(TAG, "Error while erroring: Could not build json")
e.printStackTrace()
}
return ""
}
private fun buildMarkdown(): String {
return try {
val htmlErrorReport = StringBuilder()
val userComment = activityErrorBinding.errorCommentBox.text.toString()
if (userComment.isNotEmpty()) {
htmlErrorReport.append(userComment).append("\n")
}
// basic error info
htmlErrorReport
.append("## Exception")
.append("\n* __User Action:__ ")
.append(getUserActionString(errorInfo.userAction))
.append("\n* __Request:__ ").append(errorInfo.request)
.append("\n* __Content Country:__ ").append(contentCountryString)
.append("\n* __Content Language:__ ").append(contentLanguageString)
.append("\n* __App Language:__ ").append(appLanguage)
.append("\n* __Service:__ ").append(errorInfo.serviceName)
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
.append("\n* __OS:__ ").append(osString).append("\n")
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.stackTraces.size > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorInfo.stackTraces.size)
.append(")</b></summary><p>\n")
}
// add the logs
for (i in errorInfo.stackTraces.indices) {
htmlErrorReport.append("<details><summary><b>Crash log ")
if (errorInfo.stackTraces.size > 1) {
htmlErrorReport.append(i + 1)
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorInfo.stackTraces[i]).append("\n```\n")
.append("</details>\n")
}
// make sure to close everything
if (errorInfo.stackTraces.size > 1) {
htmlErrorReport.append("</p></details>\n")
}
htmlErrorReport.append("<hr>\n")
htmlErrorReport.toString()
} catch (e: Throwable) {
Log.e(TAG, "Error while erroring: Could not build markdown")
e.printStackTrace()
""
}
}
private fun getUserActionString(userAction: UserAction?): String {
return userAction?.message ?: "Your description is in another castle."
}
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(applicationContext).toString()
private val osString: String
get() {
val osBase =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) Build.VERSION.BASE_OS else "Android"
return (
(System.getProperty("os.name") ?: "unknown operating system (os.name not set)") +
" " + (osBase.ifEmpty { "Android" }) +
" " + Build.VERSION.RELEASE +
" - " + Build.VERSION.SDK_INT
)
}
private fun addGuruMeditation() {
// just an easter egg
var text = activityErrorBinding.errorSorryView.text.toString()
text += """
${getString(R.string.guru_meditation)}
""".trimIndent()
activityErrorBinding.errorSorryView.text = text
}
companion object {
// LOG TAGS
val TAG = ErrorActivity::class.java.toString()
// BUNDLE TAGS
const val ERROR_INFO = "error_info"
const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
const val ERROR_EMAIL_SUBJECT = "Exception in "
const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
val CURRENT_TIMESTAMP_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
/**
* Get the checked activity.
*
* @param returnActivity the activity to return to
* @return the casted return activity or null
*/
@JvmStatic
fun getReturnActivity(returnActivity: Class<*>?): Class<out Activity?>? {
var checkedReturnActivity: Class<out Activity?>? = null
if (returnActivity != null) {
checkedReturnActivity = if (Activity::class.java.isAssignableFrom(returnActivity)) {
returnActivity.asSubclass(Activity::class.java)
} else {
MainActivity::class.java
}
}
return checkedReturnActivity
}
}
}

View File

@@ -1,115 +1,304 @@
package org.schabi.newpipe.error package org.schabi.newpipe.error
import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import com.google.android.exoplayer2.ExoPlaybackException import com.google.android.exoplayer2.ExoPlaybackException
import kotlinx.parcelize.IgnoredOnParcel import com.google.android.exoplayer2.upstream.HttpDataSource
import com.google.android.exoplayer2.upstream.Loader
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Info import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.ServiceList.YouTube
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
import org.schabi.newpipe.extractor.exceptions.ExtractionException import org.schabi.newpipe.extractor.exceptions.ExtractionException
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException
import org.schabi.newpipe.extractor.exceptions.PaidContentException
import org.schabi.newpipe.extractor.exceptions.PrivateContentException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.exceptions.SignInConfirmNotBotException
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
import org.schabi.newpipe.extractor.exceptions.UnsupportedContentInCountryException
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
import org.schabi.newpipe.ktx.isNetworkRelated import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper 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
* to report the error and to show it to the user along with correct action buttons.
*/
@Parcelize @Parcelize
class ErrorInfo( class ErrorInfo private constructor(
val stackTraces: Array<String>, val stackTraces: Array<String>,
val userAction: UserAction, val userAction: UserAction,
val serviceName: String,
val request: String, val request: String,
val messageStringId: Int val serviceId: Int?,
private val message: ErrorMessage,
/**
* If `true`, a report button will be shown for this error. Otherwise the error is not something
* that can really be reported (e.g. a network issue, or content not being available at all).
*/
val isReportable: Boolean,
/**
* If `true`, the process causing this error can be retried, otherwise not.
*/
val isRetryable: Boolean,
/**
* If present, indicates that the exception was a ReCaptchaException, and this is the URL
* provided by the service that can be used to solve the ReCaptcha challenge.
*/
val recaptchaUrl: String?,
/**
* If present, this resource can alternatively be opened in browser (useful if NewPipe is
* badly broken).
*/
val openInBrowserUrl: String?,
) : Parcelable { ) : Parcelable {
// no need to store throwable, all data for report is in other variables @JvmOverloads
// also, the throwable might not be serializable, see TeamNewPipe/NewPipe#7302 constructor(
@IgnoredOnParcel
var throwable: Throwable? = null
private constructor(
throwable: Throwable, throwable: Throwable,
userAction: UserAction, userAction: UserAction,
serviceName: String, request: String,
request: String serviceId: Int? = null,
openInBrowserUrl: String? = null,
) : this( ) : this(
throwableToStringList(throwable), throwableToStringList(throwable),
userAction, userAction,
serviceName,
request, request,
getMessageStringId(throwable, userAction) serviceId,
) { getMessage(throwable, userAction, serviceId),
this.throwable = throwable isReportable(throwable),
} isRetryable(throwable),
(throwable as? ReCaptchaException)?.url,
openInBrowserUrl,
)
private constructor( @JvmOverloads
throwable: List<Throwable>, constructor(
throwables: List<Throwable>,
userAction: UserAction, userAction: UserAction,
serviceName: String, request: String,
request: String serviceId: Int? = null,
openInBrowserUrl: String? = null,
) : this( ) : this(
throwableListToStringList(throwable), throwableListToStringList(throwables),
userAction, userAction,
serviceName,
request, request,
getMessageStringId(throwable.firstOrNull(), userAction) serviceId,
) { getMessage(throwables.firstOrNull(), userAction, serviceId),
this.throwable = throwable.firstOrNull() throwables.any(::isReportable),
throwables.isEmpty() || throwables.any(::isRetryable),
throwables.firstNotNullOfOrNull { it as? ReCaptchaException }?.url,
openInBrowserUrl,
)
// constructor to manually build ErrorInfo when no throwable is available
constructor(
stackTraces: Array<String>,
userAction: UserAction,
request: String,
serviceId: Int?,
@StringRes message: Int
) :
this(
stackTraces, userAction, request, serviceId, ErrorMessage(message),
true, false, null, null
)
// constructor with only one throwable to extract service id and openInBrowserUrl from an Info
constructor(
throwable: Throwable,
userAction: UserAction,
request: String,
info: Info?,
) :
this(throwable, userAction, request, info?.serviceId, info?.url)
// constructor with multiple throwables to extract service id and openInBrowserUrl from an Info
constructor(
throwables: List<Throwable>,
userAction: UserAction,
request: String,
info: Info?,
) :
this(throwables, userAction, request, info?.serviceId, info?.url)
fun getServiceName(): String {
return getServiceName(serviceId)
} }
// constructors with single throwable fun getMessage(context: Context): String {
constructor(throwable: Throwable, userAction: UserAction, request: String) : return message.getString(context)
this(throwable, userAction, SERVICE_NONE, request) }
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
this(throwable, userAction, getInfoServiceName(info), request)
// constructors with list of throwables
constructor(throwable: List<Throwable>, userAction: UserAction, request: String) :
this(throwable, userAction, SERVICE_NONE, request)
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int) :
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
this(throwable, userAction, getInfoServiceName(info), request)
companion object { companion object {
const val SERVICE_NONE = "none" @Parcelize
class ErrorMessage(
@StringRes
private val stringRes: Int,
private vararg val formatArgs: String,
) : Parcelable {
fun getString(context: Context): String {
return if (formatArgs.isEmpty()) {
// use ContextCompat.getString() just in case context is not AppCompatActivity
ContextCompat.getString(context, stringRes)
} else {
// ContextCompat.getString() with formatArgs does not exist, so we just
// replicate its source code but with formatArgs
ContextCompat.getContextForLanguage(context).getString(stringRes, *formatArgs)
}
}
}
const val SERVICE_NONE = "<unknown_service>"
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
?: SERVICE_NONE
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString()) fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
fun throwableListToStringList(throwableList: List<Throwable>) = fun throwableListToStringList(throwableList: List<Throwable>) =
throwableList.map { it.stackTraceToString() }.toTypedArray() throwableList.map { it.stackTraceToString() }.toTypedArray()
private fun getInfoServiceName(info: Info?) = fun getMessage(
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
@StringRes
private fun getMessageStringId(
throwable: Throwable?, throwable: Throwable?,
action: UserAction action: UserAction?,
): Int { serviceId: Int?,
): ErrorMessage {
return when { return when {
throwable is AccountTerminatedException -> R.string.account_terminated // player exceptions
throwable is ContentNotAvailableException -> R.string.content_not_available // some may be IOException, so do these checks before isNetworkRelated!
throwable != null && throwable.isNetworkRelated -> R.string.network_error
throwable is ContentNotSupportedException -> R.string.content_not_supported
throwable is ExtractionException -> R.string.parsing_error
throwable is ExoPlaybackException -> { throwable is ExoPlaybackException -> {
when (throwable.type) { val cause = throwable.cause
ExoPlaybackException.TYPE_SOURCE -> R.string.player_stream_failure when {
ExoPlaybackException.TYPE_UNEXPECTED -> R.string.player_recoverable_failure cause is HttpDataSource.InvalidResponseCodeException -> {
else -> R.string.player_unrecoverable_failure if (cause.responseCode == 403) {
if (serviceId == YouTube.serviceId) {
ErrorMessage(R.string.youtube_player_http_403)
} else {
ErrorMessage(R.string.player_http_403)
}
} else {
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)
} }
} }
action == UserAction.UI_ERROR -> R.string.app_ui_crash throwable is FailedMediaSource.FailedMediaSourceException ->
action == UserAction.REQUESTED_COMMENTS -> R.string.error_unable_to_load_comments getMessage(throwable.cause, action, serviceId)
action == UserAction.SUBSCRIPTION_CHANGE -> R.string.subscription_change_failed throwable is PlaybackResolver.ResolverException ->
action == UserAction.SUBSCRIPTION_UPDATE -> R.string.subscription_update_failed ErrorMessage(R.string.player_stream_failure)
action == UserAction.LOAD_IMAGE -> R.string.could_not_load_thumbnails
action == UserAction.DOWNLOAD_OPEN_DIALOG -> R.string.could_not_setup_download_menu // content not available exceptions
else -> R.string.general_error throwable is AccountTerminatedException ->
throwable.message
?.takeIf { reason -> !reason.isEmpty() }
?.let { reason ->
ErrorMessage(
R.string.account_terminated_service_provides_reason,
getServiceName(serviceId),
reason
)
}
?: 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 ->
ErrorMessage(R.string.parsing_error)
// 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)
}
}
fun isReportable(throwable: Throwable?): Boolean {
return when (throwable) {
// 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
// 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
}
}
fun isRetryable(throwable: Throwable?): Boolean {
return when (throwable) {
// we know the content is not available, retrying won't help
is ContentNotAvailableException -> false
// 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
} }
} }
} }

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.error
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
@@ -14,28 +13,14 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException
import org.schabi.newpipe.extractor.exceptions.PaidContentException
import org.schabi.newpipe.extractor.exceptions.PrivateContentException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.isInterruptedCaused
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.external_communication.ShareUtils import org.schabi.newpipe.util.external_communication.ShareUtils
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class ErrorPanelHelper( class ErrorPanelHelper(
private val fragment: Fragment, private val fragment: Fragment,
rootView: View, rootView: View,
onRetry: Runnable onRetry: Runnable?,
) { ) {
private val context: Context = rootView.context!! private val context: Context = rootView.context!!
@@ -56,12 +41,15 @@ class ErrorPanelHelper(
errorPanelRoot.findViewById(R.id.error_open_in_browser) errorPanelRoot.findViewById(R.id.error_open_in_browser)
private var errorDisposable: Disposable? = null private var errorDisposable: Disposable? = null
private var retryShouldBeShown: Boolean = (onRetry != null)
init { init {
errorDisposable = errorRetryButton.clicks() if (onRetry != null) {
.debounce(300, TimeUnit.MILLISECONDS) errorDisposable = errorRetryButton.clicks()
.observeOn(AndroidSchedulers.mainThread()) .debounce(300, TimeUnit.MILLISECONDS)
.subscribe { onRetry.run() } .observeOn(AndroidSchedulers.mainThread())
.subscribe { onRetry.run() }
}
} }
private fun ensureDefaultVisibility() { private fun ensureDefaultVisibility() {
@@ -75,64 +63,32 @@ class ErrorPanelHelper(
} }
fun showError(errorInfo: ErrorInfo) { fun showError(errorInfo: ErrorInfo) {
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [$errorInfo.throwable]")
}
return
}
ensureDefaultVisibility() ensureDefaultVisibility()
errorTextView.text = errorInfo.getMessage(context)
if (errorInfo.throwable is ReCaptchaException) { if (errorInfo.recaptchaUrl != null) {
errorTextView.setText(R.string.recaptcha_request_toast) showAndSetErrorButtonAction(R.string.recaptcha_solve) {
showAndSetErrorButtonAction(
R.string.recaptcha_solve
) {
// Starting ReCaptcha Challenge Activity // Starting ReCaptcha Challenge Activity
val intent = Intent(context, ReCaptchaActivity::class.java) val intent = Intent(context, ReCaptchaActivity::class.java)
intent.putExtra( intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.recaptchaUrl)
ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
(errorInfo.throwable as ReCaptchaException).url
)
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST) fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
errorActionButton.setOnClickListener(null) errorActionButton.setOnClickListener(null)
} }
} else if (errorInfo.isReportable) {
errorRetryButton.isVisible = true showAndSetErrorButtonAction(R.string.error_snackbar_action) {
showAndSetOpenInBrowserButtonAction(errorInfo)
} else if (errorInfo.throwable is AccountTerminatedException) {
errorTextView.setText(R.string.account_terminated)
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
errorServiceInfoTextView.text = context.resources.getString(
R.string.service_provides_reason,
ServiceHelper.getSelectedService(context)?.serviceInfo?.name ?: "<unknown>"
)
errorServiceInfoTextView.isVisible = true
errorServiceExplanationTextView.text =
(errorInfo.throwable as AccountTerminatedException).message
errorServiceExplanationTextView.isVisible = true
}
} else {
showAndSetErrorButtonAction(
R.string.error_snackbar_action
) {
ErrorUtil.openActivity(context, errorInfo) ErrorUtil.openActivity(context, errorInfo)
} }
}
errorTextView.setText(getExceptionDescription(errorInfo.throwable)) if (errorInfo.isRetryable) {
errorRetryButton.isVisible = retryShouldBeShown
}
if (errorInfo.throwable !is ContentNotAvailableException && if (errorInfo.openInBrowserUrl != null) {
errorInfo.throwable !is ContentNotSupportedException errorOpenInBrowserButton.isVisible = true
) { errorOpenInBrowserButton.setOnClickListener {
// show retry button only for content which is not unavailable or unsupported ShareUtils.openUrlInBrowser(context, errorInfo.openInBrowserUrl)
errorRetryButton.isVisible = true
} }
showAndSetOpenInBrowserButtonAction(errorInfo)
} }
setRootVisible() setRootVisible()
@@ -150,15 +106,6 @@ class ErrorPanelHelper(
errorActionButton.setOnClickListener(listener) errorActionButton.setOnClickListener(listener)
} }
private fun showAndSetOpenInBrowserButtonAction(
errorInfo: ErrorInfo
) {
errorOpenInBrowserButton.isVisible = true
errorOpenInBrowserButton.setOnClickListener {
ShareUtils.openUrlInBrowser(context, errorInfo.request)
}
}
fun showTextError(errorString: String) { fun showTextError(errorString: String) {
ensureDefaultVisibility() ensureDefaultVisibility()
@@ -189,27 +136,5 @@ class ErrorPanelHelper(
companion object { companion object {
val TAG: String = ErrorPanelHelper::class.simpleName!! val TAG: String = ErrorPanelHelper::class.simpleName!!
val DEBUG: Boolean = MainActivity.DEBUG val DEBUG: Boolean = MainActivity.DEBUG
@StringRes
fun getExceptionDescription(throwable: Throwable?): Int {
return when (throwable) {
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
is GeographicRestrictionException -> R.string.georestricted_content
is PaidContentException -> R.string.paid_content
is PrivateContentException -> R.string.private_content
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
is ContentNotAvailableException -> R.string.content_not_available
is ContentNotSupportedException -> R.string.content_not_supported
else -> {
// show retry button only for content which is not unavailable or unsupported
if (throwable != null && throwable.isNetworkRelated) {
R.string.network_error
} else {
R.string.error_snackbar_message
}
}
}
}
} }
} }

View File

@@ -10,8 +10,11 @@ import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R import org.schabi.newpipe.R
/** /**
@@ -35,12 +38,20 @@ class ErrorUtil {
* activity (since the workflow would be interrupted anyway in that case). So never use this * activity (since the workflow would be interrupted anyway in that case). So never use this
* for background services. * for background services.
* *
* If the crashed occurred while the app was in the background open a notification instead
*
* @param context the context to use to start the new activity * @param context the context to use to start the new activity
* @param errorInfo the error info to be reported * @param errorInfo the error info to be reported
*/ */
@JvmStatic @JvmStatic
fun openActivity(context: Context, errorInfo: ErrorInfo) { fun openActivity(context: Context, errorInfo: ErrorInfo) {
context.startActivity(getErrorActivityIntent(context, errorInfo)) if (PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(MainActivity.KEY_IS_IN_BACKGROUND, true)
) {
createNotification(context, errorInfo)
} else {
context.startActivity(getErrorActivityIntent(context, errorInfo))
}
} }
/** /**
@@ -54,7 +65,7 @@ class ErrorUtil {
*/ */
@JvmStatic @JvmStatic
fun showSnackbar(context: Context, errorInfo: ErrorInfo) { fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
val rootView = if (context is Activity) context.findViewById<View>(R.id.content) else null val rootView = (context as? Activity)?.findViewById<View>(android.R.id.content)
showSnackbar(context, rootView, errorInfo) showSnackbar(context, rootView, errorInfo)
} }
@@ -71,7 +82,7 @@ class ErrorUtil {
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) { fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
var rootView = fragment.view var rootView = fragment.view
if (rootView == null && fragment.activity != null) { if (rootView == null && fragment.activity != null) {
rootView = fragment.requireActivity().findViewById(R.id.content) rootView = fragment.requireActivity().findViewById(android.R.id.content)
} }
showSnackbar(fragment.requireContext(), rootView, errorInfo) showSnackbar(fragment.requireContext(), rootView, errorInfo)
} }
@@ -111,7 +122,7 @@ class ErrorUtil {
) )
.setSmallIcon(R.drawable.ic_bug_report) .setSmallIcon(R.drawable.ic_bug_report)
.setContentTitle(context.getString(R.string.error_report_notification_title)) .setContentTitle(context.getString(R.string.error_report_notification_title))
.setContentText(context.getString(errorInfo.messageStringId)) .setContentText(errorInfo.getMessage(context))
.setAutoCancel(true) .setAutoCancel(true)
.setContentIntent( .setContentIntent(
PendingIntentCompat.getActivity( PendingIntentCompat.getActivity(
@@ -126,9 +137,11 @@ class ErrorUtil {
NotificationManagerCompat.from(context) NotificationManagerCompat.from(context)
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build()) .notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
// since the notification is silent, also show a toast, otherwise the user is confused ContextCompat.getMainExecutor(context).execute {
Toast.makeText(context, R.string.error_report_notification_toast, Toast.LENGTH_SHORT) // since the notification is silent, also show a toast, otherwise the user is confused
.show() Toast.makeText(context, R.string.error_report_notification_toast, Toast.LENGTH_SHORT)
.show()
}
} }
private fun getErrorActivityIntent(context: Context, errorInfo: ErrorInfo): Intent { private fun getErrorActivityIntent(context: Context, errorInfo: ErrorInfo): Intent {
@@ -143,10 +156,10 @@ class ErrorUtil {
// fallback to showing a notification if no root view is available // fallback to showing a notification if no root view is available
createNotification(context, errorInfo) createNotification(context, errorInfo)
} else { } else {
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG) Snackbar.make(rootView, errorInfo.getMessage(context), Snackbar.LENGTH_LONG)
.setActionTextColor(Color.YELLOW) .setActionTextColor(Color.YELLOW)
.setAction(context.getString(R.string.error_snackbar_action).uppercase()) { .setAction(context.getString(R.string.error_snackbar_action).uppercase()) {
openActivity(context, errorInfo) context.startActivity(getErrorActivityIntent(context, errorInfo))
}.show() }.show()
} }
} }

View File

@@ -0,0 +1,233 @@
package org.schabi.newpipe.error;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.CookieManager;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.util.ThemeHelper;
/*
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* ReCaptchaActivity.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 ReCaptchaActivity extends AppCompatActivity {
public static final int RECAPTCHA_REQUEST = 10;
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
public static final String TAG = ReCaptchaActivity.class.toString();
public static final String YT_URL = "https://www.youtube.com";
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
public static String sanitizeRecaptchaUrl(@Nullable final String url) {
if (url == null || url.trim().isEmpty()) {
return YT_URL; // YouTube is the most likely service to have thrown a recaptcha
} else {
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "");
}
}
private ActivityRecaptchaBinding recaptchaBinding;
private String foundCookies = "";
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(final Bundle savedInstanceState) {
ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState);
recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater());
setContentView(recaptchaBinding.getRoot());
setSupportActionBar(recaptchaBinding.toolbar);
final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA));
// set return to Cancel by default
setResult(RESULT_CANCELED);
// enable Javascript
final WebSettings webSettings = recaptchaBinding.reCaptchaWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(final WebView view,
final WebResourceRequest request) {
if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString());
}
handleCookiesFromUrl(request.getUrl().toString());
return false;
}
@Override
public void onPageFinished(final WebView view, final String url) {
super.onPageFinished(view, url);
handleCookiesFromUrl(url);
}
});
// cleaning cache, history and cookies from webView
recaptchaBinding.reCaptchaWebView.clearCache(true);
recaptchaBinding.reCaptchaWebView.clearHistory();
CookieManager.getInstance().removeAllCookies(null);
recaptchaBinding.reCaptchaWebView.loadUrl(url);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setTitle(R.string.title_activity_recaptcha);
actionBar.setSubtitle(R.string.subtitle_activity_recaptcha);
}
return true;
}
@Override
public void onBackPressed() {
saveCookiesAndFinish();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.menu_item_done) {
saveCookiesAndFinish();
return true;
}
return false;
}
private void saveCookiesAndFinish() {
// try to get cookies of unclosed page
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.getUrl());
if (MainActivity.DEBUG) {
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
}
if (!foundCookies.isEmpty()) {
// save cookies to preferences
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
prefs.edit().putString(key, foundCookies).apply();
// give cookies to Downloader class
DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies);
setResult(RESULT_OK);
}
// Navigate to blank page (unloads youtube to prevent background playback)
recaptchaBinding.reCaptchaWebView.loadUrl("about:blank");
final Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(this, intent);
}
private void handleCookiesFromUrl(@Nullable final String url) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookiesFromUrl: url=" + (url == null ? "null" : url));
}
if (url == null) {
return;
}
final String cookies = CookieManager.getInstance().getCookie(url);
handleCookies(cookies);
// sometimes cookies are inside the url
final int abuseStart = url.indexOf("google_abuse=");
if (abuseStart != -1) {
final int abuseEnd = url.indexOf("+path");
try {
handleCookies(Utils.decodeUrlUtf8(url.substring(abuseStart + 13, abuseEnd)));
} catch (final StringIndexOutOfBoundsException e) {
if (MainActivity.DEBUG) {
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
}
}
}
}
private void handleCookies(@Nullable final String cookies) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookies: cookies=" + (cookies == null ? "null" : cookies));
}
if (cookies == null) {
return;
}
addYoutubeCookies(cookies);
// add here methods to extract cookies for other services
}
private void addYoutubeCookies(@NonNull final String cookies) {
if (cookies.contains("s_gl=") || cookies.contains("goojf=")
|| cookies.contains("VISITOR_INFO1_LIVE=")
|| cookies.contains("GOOGLE_ABUSE_EXEMPTION=")) {
// youtube seems to also need the other cookies:
addCookie(cookies);
}
}
private void addCookie(final String cookie) {
if (foundCookies.contains(cookie)) {
return;
}
if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
foundCookies += cookie;
} else if (foundCookies.endsWith(";")) {
foundCookies += " " + cookie;
} else {
foundCookies += "; " + cookie;
}
}
}

View File

@@ -1,227 +0,0 @@
package org.schabi.newpipe.error
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NavUtils
import androidx.preference.PreferenceManager
import org.schabi.newpipe.DownloaderImpl
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding
import org.schabi.newpipe.extractor.utils.Utils
import org.schabi.newpipe.util.ThemeHelper
import java.io.UnsupportedEncodingException
/*
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* ReCaptchaActivity.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/>.
*/
class ReCaptchaActivity : AppCompatActivity() {
private lateinit var recaptchaBinding: ActivityRecaptchaBinding
private var foundCookies = ""
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this)
super.onCreate(savedInstanceState)
recaptchaBinding = ActivityRecaptchaBinding.inflate(
layoutInflater
)
setContentView(recaptchaBinding.root)
setSupportActionBar(recaptchaBinding.toolbar)
val url = sanitizeRecaptchaUrl(intent.getStringExtra(RECAPTCHA_URL_EXTRA))
// set return to Cancel by default
setResult(RESULT_CANCELED)
// enable Javascript
val webSettings = recaptchaBinding.reCaptchaWebView.settings
webSettings.javaScriptEnabled = true
webSettings.userAgentString = DownloaderImpl.USER_AGENT
recaptchaBinding.reCaptchaWebView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.url.toString())
}
handleCookiesFromUrl(request.url.toString())
return false
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
handleCookiesFromUrl(url)
}
}
// cleaning cache, history and cookies from webView
recaptchaBinding.reCaptchaWebView.clearCache(true)
recaptchaBinding.reCaptchaWebView.clearHistory()
CookieManager.getInstance().removeAllCookies(null)
recaptchaBinding.reCaptchaWebView.loadUrl(url)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_recaptcha, menu)
val actionBar = supportActionBar
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(false)
actionBar.setTitle(R.string.title_activity_recaptcha)
actionBar.setSubtitle(R.string.subtitle_activity_recaptcha)
}
return true
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
saveCookiesAndFinish()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menu_item_done) {
saveCookiesAndFinish()
return true
}
return false
}
private fun saveCookiesAndFinish() {
// try to get cookies of unclosed page
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.url)
if (MainActivity.DEBUG) {
Log.d(TAG, "saveCookiesAndFinish: foundCookies=$foundCookies")
}
if (foundCookies.isNotEmpty()) {
// save cookies to preferences
val prefs = PreferenceManager.getDefaultSharedPreferences(
applicationContext
)
val key = applicationContext.getString(R.string.recaptcha_cookies_key)
prefs.edit().putString(key, foundCookies).apply()
// give cookies to Downloader class
DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies)
setResult(RESULT_OK)
}
// Navigate to blank page (unloads youtube to prevent background playback)
recaptchaBinding.reCaptchaWebView.loadUrl("about:blank")
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
NavUtils.navigateUpTo(this, intent)
}
private fun handleCookiesFromUrl(url: String?) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookiesFromUrl: url=" + (url ?: "null"))
}
if (url == null) {
return
}
val cookies = CookieManager.getInstance().getCookie(url)
handleCookies(cookies)
// sometimes cookies are inside the url
val abuseStart = url.indexOf("google_abuse=")
if (abuseStart != -1) {
val abuseEnd = url.indexOf("+path")
try {
var abuseCookie: String? = url.substring(abuseStart + 13, abuseEnd)
abuseCookie = Utils.decodeUrlUtf8(abuseCookie)
handleCookies(abuseCookie)
} catch (e: UnsupportedEncodingException) {
if (MainActivity.DEBUG) {
e.printStackTrace()
Log.d(
TAG,
"handleCookiesFromUrl: invalid google abuse starting at " +
abuseStart + " and ending at " + abuseEnd + " for url " + url
)
}
} catch (e: StringIndexOutOfBoundsException) {
if (MainActivity.DEBUG) {
e.printStackTrace()
Log.d(
TAG,
"handleCookiesFromUrl: invalid google abuse starting at " +
abuseStart + " and ending at " + abuseEnd + " for url " + url
)
}
}
}
}
private fun handleCookies(cookies: String?) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookies: cookies=" + (cookies ?: "null"))
}
if (cookies == null) {
return
}
addYoutubeCookies(cookies)
// add here methods to extract cookies for other services
}
private fun addYoutubeCookies(cookies: String) {
if (cookies.contains("s_gl=") || cookies.contains("goojf=") ||
cookies.contains("VISITOR_INFO1_LIVE=") ||
cookies.contains("GOOGLE_ABUSE_EXEMPTION=")
) {
// youtube seems to also need the other cookies:
addCookie(cookies)
}
}
private fun addCookie(cookie: String) {
if (foundCookies.contains(cookie)) {
return
}
foundCookies += if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
cookie
} else if (foundCookies.endsWith(";")) {
" $cookie"
} else {
"; $cookie"
}
}
companion object {
const val RECAPTCHA_REQUEST = 10
const val RECAPTCHA_URL_EXTRA = "recaptcha_url_extra"
val TAG = ReCaptchaActivity::class.java.toString()
const val YT_URL = "https://www.youtube.com"
const val RECAPTCHA_COOKIES_KEY = "recaptcha_cookies"
fun sanitizeRecaptchaUrl(url: String?): String {
return if (url == null || url.trim { it <= ' ' }.isEmpty()) {
YT_URL // YouTube is the most likely service to have thrown a recaptcha
} else {
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "")
}
}
}
}

View File

@@ -0,0 +1,49 @@
package org.schabi.newpipe.error;
/**
* The user actions that can cause an error.
*/
public enum UserAction {
USER_REPORT("user report"),
UI_ERROR("ui error"),
DATABASE_IMPORT_EXPORT("database import or export"),
SUBSCRIPTION_CHANGE("subscription change"),
SUBSCRIPTION_UPDATE("subscription update"),
SUBSCRIPTION_GET("get subscription"),
SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"),
LOAD_IMAGE("load image"),
SOMETHING_ELSE("something else"),
SEARCHED("searched"),
GET_SUGGESTIONS("get suggestions"),
REQUESTED_STREAM("requested stream"),
REQUESTED_CHANNEL("requested channel"),
REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
REQUESTED_COMMENT_REPLIES("requested comment replies"),
REQUESTED_FEED("requested feed"),
REQUESTED_BOOKMARK("bookmark"),
DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("play stream"),
DOWNLOAD_OPEN_DIALOG("download open dialog"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed"),
NEW_STREAMS_NOTIFICATIONS("new streams notifications"),
PREFERENCES_MIGRATION("migration of preferences"),
SHARE_TO_NEWPIPE("share to newpipe"),
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
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");
private final String message;
UserAction(final String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}

View File

@@ -1,32 +0,0 @@
package org.schabi.newpipe.error
/**
* The user actions that can cause an error.
*/
enum class UserAction(val message: String) {
USER_REPORT("user report"), UI_ERROR("ui error"), SUBSCRIPTION_CHANGE("subscription change"), SUBSCRIPTION_UPDATE(
"subscription update"
),
SUBSCRIPTION_GET("get subscription"), SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"), LOAD_IMAGE(
"load image"
),
SOMETHING_ELSE("something else"), SEARCHED("searched"), GET_SUGGESTIONS("get suggestions"), REQUESTED_STREAM(
"requested stream"
),
REQUESTED_CHANNEL("requested channel"), REQUESTED_PLAYLIST("requested playlist"), REQUESTED_KIOSK(
"requested kiosk"
),
REQUESTED_COMMENTS("requested comments"), REQUESTED_COMMENT_REPLIES("requested comment replies"), REQUESTED_FEED(
"requested feed"
),
REQUESTED_BOOKMARK("bookmark"), DELETE_FROM_HISTORY("delete from history"), PLAY_STREAM("play stream"), DOWNLOAD_OPEN_DIALOG(
"download open dialog"
),
DOWNLOAD_POSTPROCESSING("download post-processing"), DOWNLOAD_FAILED("download failed"), NEW_STREAMS_NOTIFICATIONS(
"new streams notifications"
),
PREFERENCES_MIGRATION("migration of preferences"), SHARE_TO_NEWPIPE("share to newpipe"), CHECK_FOR_NEW_APP_VERSION(
"check for new app version"
),
OPEN_INFO_ITEM_DIALOG("open info item dialog")
}

View File

@@ -13,6 +13,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.evernote.android.state.State;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
@@ -22,8 +24,6 @@ import org.schabi.newpipe.util.InfoCache;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> { public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
@State @State
protected AtomicBoolean wasLoading = new AtomicBoolean(); protected AtomicBoolean wasLoading = new AtomicBoolean();
@@ -134,6 +134,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
hideErrorPanel(); hideErrorPanel();
} }
@Override
public void showEmptyState() { public void showEmptyState() {
isLoading.set(false); isLoading.set(false);
if (emptyStateView != null) { if (emptyStateView != null) {

View File

@@ -7,16 +7,57 @@ import android.view.ViewGroup;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.evernote.android.state.State;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorPanelHelper;
public class BlankFragment extends BaseFragment { public class BlankFragment extends BaseFragment {
@State
@Nullable
ErrorInfo errorInfo;
@Nullable
ErrorPanelHelper errorPanel = null;
/**
* Builds a blank fragment that just says the app name and suggests clicking on search.
*/
public BlankFragment() {
this(null);
}
/**
* @param errorInfo if null acts like {@link BlankFragment}, else shows an error panel.
*/
public BlankFragment(@Nullable final ErrorInfo errorInfo) {
this.errorInfo = errorInfo;
}
@Nullable @Nullable
@Override @Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
final Bundle savedInstanceState) { final Bundle savedInstanceState) {
setTitle("NewPipe"); setTitle("NewPipe");
return inflater.inflate(R.layout.fragment_blank, container, false); final View view = inflater.inflate(R.layout.fragment_blank, container, false);
if (errorInfo != null) {
errorPanel = new ErrorPanelHelper(this, view, null);
errorPanel.showError(errorInfo);
view.findViewById(R.id.blank_page_content).setVisibility(View.GONE);
}
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (errorPanel != null) {
errorPanel.dispose();
errorPanel = null;
}
} }
@Override @Override

View File

@@ -36,8 +36,9 @@ import com.google.android.material.tabs.TabLayout;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentMainBinding; import org.schabi.newpipe.databinding.FragmentMainBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.settings.tabs.Tab; import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager; import org.schabi.newpipe.settings.tabs.TabsManager;
@@ -220,7 +221,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public void commitPlaylistTabs() { public void commitPlaylistTabs() {
pagerAdapter.getLocalPlaylistFragments() pagerAdapter.getLocalPlaylistFragments()
.stream() .stream()
.forEach(LocalPlaylistFragment::commitChanges); .forEach(LocalPlaylistFragment::saveImmediate);
} }
private void updateTabLayoutPosition() { private void updateTabLayoutPosition() {
@@ -245,10 +246,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
// change the background and icon color of the tab layout: // change the background and icon color of the tab layout:
// service-colored at the top, app-background-colored at the bottom // service-colored at the top, app-background-colored at the bottom
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(), tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
bottom ? R.attr.colorSecondary : R.attr.colorPrimary)); bottom ? android.R.attr.windowBackground : R.attr.colorPrimary));
@ColorInt final int iconColor = bottom @ColorInt final int iconColor = bottom
? ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent) ? ThemeHelper.resolveColorFromAttr(requireContext(), android.R.attr.colorAccent)
: Color.WHITE; : Color.WHITE;
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32)); tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor)); tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
@@ -282,7 +283,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user * Keep reference to LocalPlaylistFragments, because their data can be modified by the user
* during runtime and changes are not committed immediately. However, in some cases, * during runtime and changes are not committed immediately. However, in some cases,
* the changes need to be committed immediately by calling * the changes need to be committed immediately by calling
* {@link LocalPlaylistFragment#commitChanges()}. * {@link LocalPlaylistFragment#saveImmediate()}.
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called. * The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
*/ */
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>(); private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
@@ -303,9 +304,9 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
final Fragment fragment; final Fragment fragment;
try { try {
fragment = tab.getFragment(context); fragment = tab.getFragment(context);
} catch (final ExtractionException e) { } catch (final Throwable t) {
ErrorUtil.showUiErrorSnackbar(context, "Getting fragment item", e); return new BlankFragment(new ErrorInfo(t, UserAction.GETTING_MAIN_SCREEN_TAB,
return new BlankFragment(); "Tab " + tab.getClass().getSimpleName() + ":" + tab.getTabName(context)));
} }
if (fragment instanceof BaseFragment) { if (fragment instanceof BaseFragment) {

View File

@@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import com.evernote.android.state.State;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Description;
@@ -19,8 +21,6 @@ import org.schabi.newpipe.util.Localization;
import java.util.List; import java.util.List;
import icepick.State;
public class DescriptionFragment extends BaseDescriptionFragment { public class DescriptionFragment extends BaseDescriptionFragment {
@State @State
@@ -30,6 +30,10 @@ public class DescriptionFragment extends BaseDescriptionFragment {
this.streamInfo = streamInfo; this.streamInfo = streamInfo;
} }
public DescriptionFragment() {
// keep empty constructor for State when resuming fragment from memory
}
@Nullable @Nullable
@Override @Override
@@ -89,7 +93,7 @@ public class DescriptionFragment extends BaseDescriptionFragment {
if (streamInfo.getLanguageInfo() != null) { if (streamInfo.getLanguageInfo() != null) {
addMetadataItem(inflater, layout, false, R.string.metadata_language, addMetadataItem(inflater, layout, false, R.string.metadata_language,
streamInfo.getLanguageInfo().getDisplayLanguage(getAppLocale(getContext()))); streamInfo.getLanguageInfo().getDisplayLanguage(getAppLocale()));
} }
addMetadataItem(inflater, layout, true, R.string.metadata_support, addMetadataItem(inflater, layout, true, R.string.metadata_support,

View File

@@ -56,6 +56,7 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.AppBarLayout;
@@ -92,6 +93,7 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerIntentType;
import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.OnKeyDownListener;
@@ -127,7 +129,6 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
@@ -236,11 +237,14 @@ public final class VideoDetailFragment
// Service management // Service management
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void onServiceConnected(final Player connectedPlayer, public void onServiceConnected(@NonNull final PlayerService connectedPlayerService) {
final PlayerService connectedPlayerService,
final boolean playAfterConnect) {
player = connectedPlayer;
playerService = connectedPlayerService; playerService = connectedPlayerService;
}
@Override
public void onPlayerConnected(@NonNull final Player connectedPlayer,
final boolean playAfterConnect) {
player = connectedPlayer;
// It will do nothing if the player is not in fullscreen mode // It will do nothing if the player is not in fullscreen mode
hideSystemUiIfNeeded(); hideSystemUiIfNeeded();
@@ -272,22 +276,29 @@ public final class VideoDetailFragment
updateOverlayPlayQueueButtonVisibility(); updateOverlayPlayQueueButtonVisibility();
} }
@Override
public void onPlayerDisconnected() {
player = null;
// the binding could be null at this point, if the app is finishing
if (binding != null) {
restoreDefaultBrightness();
}
}
@Override @Override
public void onServiceDisconnected() { public void onServiceDisconnected() {
playerService = null; playerService = null;
player = null;
restoreDefaultBrightness();
} }
/*////////////////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////////////////*/
public static VideoDetailFragment getInstance(final int serviceId, public static VideoDetailFragment getInstance(final int serviceId,
@Nullable final String videoUrl, @Nullable final String url,
@NonNull final String name, @NonNull final String name,
@Nullable final PlayQueue queue) { @Nullable final PlayQueue queue) {
final VideoDetailFragment instance = new VideoDetailFragment(); final VideoDetailFragment instance = new VideoDetailFragment();
instance.setInitialData(serviceId, videoUrl, name, queue); instance.setInitialData(serviceId, url, name, queue);
return instance; return instance;
} }
@@ -482,7 +493,7 @@ public final class VideoDetailFragment
// commit previous pending changes to database // commit previous pending changes to database
if (fragment instanceof LocalPlaylistFragment) { if (fragment instanceof LocalPlaylistFragment) {
((LocalPlaylistFragment) fragment).commitChanges(); ((LocalPlaylistFragment) fragment).saveImmediate();
} else if (fragment instanceof MainFragment) { } else if (fragment instanceof MainFragment) {
((MainFragment) fragment).commitPlaylistTabs(); ((MainFragment) fragment).commitPlaylistTabs();
} }
@@ -866,7 +877,7 @@ public final class VideoDetailFragment
} }
} }
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
url == null ? "no url" : url, serviceId))); url == null ? "no url" : url, serviceId, url)));
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@@ -1156,8 +1167,12 @@ public final class VideoDetailFragment
final PlayQueue queue = setupPlayQueueForIntent(false); final PlayQueue queue = setupPlayQueueForIntent(false);
tryAddVideoPlayerView(); tryAddVideoPlayerView();
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), final Context context = requireContext();
PlayerService.class, queue, true, autoPlayEnabled); final Intent playerIntent =
NavigationHelper.getPlayerIntent(context, PlayerService.class, queue,
PlayerIntentType.AllOthers)
.putExtra(Player.PLAY_WHEN_READY, autoPlayEnabled)
.putExtra(Player.RESUME_PLAYBACK, true);
ContextCompat.startForegroundService(activity, playerIntent); ContextCompat.startForegroundService(activity, playerIntent);
} }
@@ -1413,7 +1428,8 @@ public final class VideoDetailFragment
intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER); intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER);
intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER); intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER);
intentFilter.addAction(ACTION_PLAYER_STARTED); intentFilter.addAction(ACTION_PLAYER_STARTED);
activity.registerReceiver(broadcastReceiver, intentFilter); ContextCompat.registerReceiver(activity, broadcastReceiver, intentFilter,
ContextCompat.RECEIVER_EXPORTED);
} }
@@ -1582,8 +1598,8 @@ public final class VideoDetailFragment
} }
if (!info.getErrors().isEmpty()) { if (!info.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(info.getErrors(), showSnackBarError(new ErrorInfo(info.getErrors(), UserAction.REQUESTED_STREAM,
UserAction.REQUESTED_STREAM, info.getUrl(), info)); "Some info not extracted: " + info.getUrl(), info));
} }
} }
@@ -1736,7 +1752,7 @@ public final class VideoDetailFragment
playQueue = queue; playQueue = queue;
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onQueueUpdate() called with: serviceId = [" Log.d(TAG, "onQueueUpdate() called with: serviceId = ["
+ serviceId + "], videoUrl = [" + url + "], name = [" + serviceId + "], url = [" + url + "], name = ["
+ title + "], playQueue = [" + playQueue + "]"); + title + "], playQueue = [" + playQueue + "]");
} }
@@ -1848,13 +1864,16 @@ public final class VideoDetailFragment
@Override @Override
public void onServiceStopped() { public void onServiceStopped() {
setOverlayPlayPauseImage(false); // the binding could be null at this point, if the app is finishing
if (currentInfo != null) { if (binding != null) {
updateOverlayData(currentInfo.getName(), setOverlayPlayPauseImage(false);
currentInfo.getUploaderName(), if (currentInfo != null) {
currentInfo.getThumbnails()); updateOverlayData(currentInfo.getName(),
currentInfo.getUploaderName(),
currentInfo.getThumbnails());
}
updateOverlayPlayQueueButtonVisibility();
} }
updateOverlayPlayQueueButtonVisibility();
} }
@Override @Override

View File

@@ -8,6 +8,9 @@ import android.util.Log;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.evernote.android.state.State;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
@@ -24,7 +27,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
@@ -41,6 +43,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
private final UserAction errorUserAction; private final UserAction errorUserAction;
protected L currentInfo; protected L currentInfo;
@Nullable
protected Page currentNextPage; protected Page currentNextPage;
protected Disposable currentWorker; protected Disposable currentWorker;
@@ -143,14 +146,14 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
currentWorker = loadResult(forceLoad) currentWorker = loadResult(forceLoad)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull L result) -> { .subscribe((@NonNull final L result) -> {
isLoading.set(false); isLoading.set(false);
currentInfo = result; currentInfo = result;
currentNextPage = result.getNextPage(); currentNextPage = result.getNextPage();
handleResult(result); handleResult(result);
}, throwable -> }, throwable ->
showError(new ErrorInfo(throwable, errorUserAction, showError(new ErrorInfo(throwable, errorUserAction,
"Start loading: " + url, serviceId))); "Start loading: " + url, serviceId, url)));
} }
/** /**
@@ -181,7 +184,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
handleNextItems(infoItemsPage); handleNextItems(infoItemsPage);
}, (@NonNull Throwable throwable) -> }, (@NonNull Throwable throwable) ->
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable, dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
errorUserAction, "Loading more items: " + url, serviceId))); errorUserAction, "Loading more items: " + url, serviceId, url)));
} }
private void forbidDownwardFocusScroll() { private void forbidDownwardFocusScroll() {
@@ -207,7 +210,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction, dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
"Get next items of: " + url, serviceId)); "Get next items of: " + url, serviceId, url));
} }
} }
@@ -247,7 +250,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
errorUserAction, "Start loading: " + url, serviceId)); errorUserAction, "Start loading: " + url, serviceId, url));
} }
} }
} }

View File

@@ -10,6 +10,8 @@ import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.evernote.android.state.State;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
@@ -20,8 +22,6 @@ import org.schabi.newpipe.util.Localization;
import java.util.List; import java.util.List;
import icepick.State;
public class ChannelAboutFragment extends BaseDescriptionFragment { public class ChannelAboutFragment extends BaseDescriptionFragment {
@State @State
protected ChannelInfo channelInfo; protected ChannelInfo channelInfo;
@@ -30,6 +30,9 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
this.channelInfo = channelInfo; this.channelInfo = channelInfo;
} }
public ChannelAboutFragment() {
// keep empty constructor for State when resuming fragment from memory
}
@Override @Override
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
@@ -78,9 +81,7 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) { if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers, addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
Localization.localizeNumber( Localization.localizeNumber(channelInfo.getSubscriberCount()));
requireContext(),
channelInfo.getSubscriberCount()));
} }
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars, addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,

View File

@@ -22,8 +22,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils; import androidx.core.graphics.ColorUtils;
import androidx.core.view.MenuProvider;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import com.jakewharton.rxbinding4.view.RxView; import com.jakewharton.rxbinding4.view.RxView;
@@ -49,16 +51,15 @@ import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver; 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.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
@@ -99,6 +100,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
private MenuItem menuRssButton; private MenuItem menuRssButton;
private MenuItem menuNotifyButton; private MenuItem menuNotifyButton;
private SubscriptionEntity channelSubscription; private SubscriptionEntity channelSubscription;
private MenuProvider menuProvider;
public static ChannelFragment getInstance(final int serviceId, final String url, public static ChannelFragment getInstance(final int serviceId, final String url,
final String name) { final String name) {
@@ -118,12 +120,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
// LifeCycle // LifeCycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override @Override
public void onAttach(@NonNull final Context context) { public void onAttach(@NonNull final Context context) {
super.onAttach(context); super.onAttach(context);
@@ -138,6 +134,67 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
return binding.getRoot(); return binding.getRoot();
} }
@Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
menuProvider = new MenuProvider() {
@Override
public void onCreateMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.menu_channel, menu);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
}
@Override
public void onPrepareMenu(@NonNull final Menu menu) {
menuRssButton = menu.findItem(R.id.menu_item_rss);
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
updateRssButton();
updateNotifyButton(channelSubscription);
}
@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;
}
return true;
}
};
activity.addMenuProvider(menuProvider);
}
@Override // called from onViewCreated in BaseFragment.onViewCreated @Override // called from onViewCreated in BaseFragment.onViewCreated
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
@@ -175,6 +232,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
binding.subChannelTitleView.setOnClickListener(openSubChannel); binding.subChannelTitleView.setOnClickListener(openSubChannel);
} }
@Override
public void onDestroyView() {
super.onDestroyView();
if (menuProvider != null) {
activity.removeMenuProvider(menuProvider);
}
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
@@ -183,73 +248,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
} }
disposables.clear(); disposables.clear();
binding = null; binding = null;
menuProvider = null;
} }
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_channel, menu);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
}
@Override
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
super.onPrepareOptionsMenu(menu);
menuRssButton = menu.findItem(R.id.menu_item_rss);
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
updateNotifyButton(channelSubscription);
}
@Override
public boolean onOptionsItemSelected(@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 super.onOptionsItemSelected(item);
}
return true;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Channel Subscription // Channel Subscription
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void monitorSubscription(final ChannelInfo info) { private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> { final Consumer<Throwable> onError = (final Throwable throwable) -> {
animate(binding.channelSubscribeButton, false, 100); animate(binding.channelSubscribeButton, false, 100);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET, showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
"Get subscription status", currentInfo)); "Get subscription status", currentInfo));
@@ -284,14 +291,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
} }
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) { private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
return (@NonNull Object o) -> { return (@NonNull final Object o) -> {
subscriptionManager.insertSubscription(subscription); subscriptionManager.insertSubscription(subscription);
return o; return o;
}; };
} }
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) { private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
return (@NonNull Object o) -> { return (@NonNull final Object o) -> {
subscriptionManager.deleteSubscription(subscription); subscriptionManager.deleteSubscription(subscription);
return o; return o;
}; };
@@ -318,7 +325,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
} }
private Disposable monitorSubscribeButton(final Function<Object, Object> action) { private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
final Consumer<Object> onNext = (@NonNull Object o) -> { final Consumer<Object> onNext = (@NonNull final Object o) -> {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Changed subscription status to this channel!"); Log.d(TAG, "Changed subscription status to this channel!");
} }
@@ -338,7 +345,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
} }
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) { private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
return (List<SubscriptionEntity> subscriptionEntities) -> { return (final List<SubscriptionEntity> subscriptionEntities) -> {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: " Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
+ "subscriptionEntities = [" + subscriptionEntities + "]"); + "subscriptionEntities = [" + subscriptionEntities + "]");
@@ -408,6 +415,13 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA); animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
} }
private void updateRssButton() {
if (menuRssButton == null || currentInfo == null) {
return;
}
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl()));
}
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) { private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
if (menuNotifyButton == null) { if (menuNotifyButton == null) {
return; return;
@@ -563,7 +577,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
isLoading.set(false); isLoading.set(false);
handleResult(result); handleResult(result);
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL, }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
url == null ? "No URL" : url, serviceId))); url == null ? "No URL" : url, serviceId, url)));
} }
@Override @Override
@@ -610,9 +624,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
binding.subChannelAvatarView.setVisibility(View.VISIBLE); binding.subChannelAvatarView.setVisibility(View.VISIBLE);
} }
if (menuRssButton != null) { updateRssButton();
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
}
channelContentNotSupported = false; channelContentNotSupported = false;
for (final Throwable throwable : result.getErrors()) { for (final Throwable throwable : result.getErrors()) {

View File

@@ -9,6 +9,8 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.evernote.android.state.State;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlaylistControlBinding; import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
@@ -32,13 +34,12 @@ import java.util.List;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo> public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
implements PlaylistControlViewHolder { implements PlaylistControlViewHolder {
// states must be protected and not private for IcePick being able to access them // states must be protected and not private for State being able to access them
@State @State
protected ListLinkHandler tabHandler; protected ListLinkHandler tabHandler;
@State @State
@@ -156,6 +157,7 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
} }
} }
@Override
public PlayQueue getPlayQueue() { public PlayQueue getPlayQueue() {
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream() final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance) .filter(StreamInfoItem.class::isInstance)

View File

@@ -12,6 +12,8 @@ import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.text.HtmlCompat; import androidx.core.text.HtmlCompat;
import com.evernote.android.state.State;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding; import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
@@ -26,6 +28,7 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.ImageStrategy; import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextLinkifier; import org.schabi.newpipe.util.text.TextLinkifier;
import org.schabi.newpipe.util.text.LongPressLinkMovementMethod;
import java.util.Queue; import java.util.Queue;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -38,7 +41,8 @@ public final class CommentRepliesFragment
public static final String TAG = CommentRepliesFragment.class.getSimpleName(); public static final String TAG = CommentRepliesFragment.class.getSimpleName();
private CommentsInfoItem commentsInfoItem; // the comment to show replies of @State
CommentsInfoItem commentsInfoItem; // the comment to show replies of
private final CompositeDisposable disposables = new CompositeDisposable(); private final CompositeDisposable disposables = new CompositeDisposable();
@@ -107,7 +111,7 @@ public final class CommentRepliesFragment
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(), TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()), HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
item.getUrl(), disposables, null); item.getUrl(), disposables, null);
binding.commentContent.setMovementMethod(LongPressLinkMovementMethod.getInstance());
return binding.getRoot(); return binding.getRoot();
}; };
} }

View File

@@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import com.evernote.android.state.State;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
@@ -29,7 +31,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import icepick.State;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
/** /**

View File

@@ -352,6 +352,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
}); });
ellipsizer.setContent(description); ellipsizer.setContent(description);
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle()); headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
headerBinding.playlistDescription.setOnClickListener(v -> ellipsizer.toggle());
} else { } else {
headerBinding.playlistDescription.setVisibility(View.GONE); headerBinding.playlistDescription.setVisibility(View.GONE);
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE); headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
@@ -506,7 +507,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
Localization.concatenateStrings( Localization.concatenateStrings(
Localization.localizeStreamCount(activity, streamCount), Localization.localizeStreamCount(activity, streamCount),
Localization.getDurationString(playlistOverallDurationSeconds, Localization.getDurationString(playlistOverallDurationSeconds,
isDurationComplete)) isDurationComplete, true))
); );
} }
} }

View File

@@ -40,6 +40,8 @@ import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.evernote.android.state.State;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentSearchBinding; import org.schabi.newpipe.databinding.FragmentSearchBinding;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
@@ -52,6 +54,7 @@ import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
@@ -77,7 +80,6 @@ import java.util.Queue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
@@ -143,6 +145,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
private final SparseArrayCompat<String> menuItemToFilterName = new SparseArrayCompat<>(); private final SparseArrayCompat<String> menuItemToFilterName = new SparseArrayCompat<>();
private StreamingService service; private StreamingService service;
@Nullable
private Page nextPage; private Page nextPage;
private boolean showLocalSuggestions = true; private boolean showLocalSuggestions = true;
private boolean showRemoteSuggestions = true; private boolean showRemoteSuggestions = true;
@@ -218,6 +221,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) { public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
searchBinding = FragmentSearchBinding.bind(rootView); searchBinding = FragmentSearchBinding.bind(rootView);
super.onViewCreated(rootView, savedInstanceState); super.onViewCreated(rootView, savedInstanceState);
updateService();
// Add the service name to search string hint
// to make it more obvious which platform is being searched.
if (service != null) {
searchEditText.setHint(
getString(R.string.search_with_service_name,
service.getServiceInfo().getName()));
}
showSearchOnStart(); showSearchOnStart();
initSearchListeners(); initSearchListeners();
} }
@@ -550,7 +562,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
} }
}); });
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { searchEditText.setOnFocusChangeListener((final View v, final boolean hasFocus) -> {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onFocusChange() called with: " Log.d(TAG, "onFocusChange() called with: "
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]"); + "v = [" + v + "], hasFocus = [" + hasFocus + "]");
@@ -611,7 +623,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}; };
searchEditText.addTextChangedListener(textWatcher); searchEditText.addTextChangedListener(textWatcher);
searchEditText.setOnEditorActionListener( searchEditText.setOnEditorActionListener(
(TextView v, int actionId, KeyEvent event) -> { (final TextView v, final int actionId, final KeyEvent event) -> {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], " Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
+ "actionId = [" + actionId + "], event = [" + event + "]"); + "actionId = [" + actionId + "], event = [" + event + "]");
@@ -923,7 +935,21 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
infoListAdapter.clearStreamItemList(); infoListAdapter.clearStreamItemList();
showEmptyState(); showEmptyState();
} else { } else {
showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId)); showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId,
getOpenInBrowserUrlForErrors()));
}
}
@Nullable
private String getOpenInBrowserUrlForErrors() {
if (TextUtils.isEmpty(searchString)) {
return null;
}
try {
return service.getSearchQHFactory().getUrl(searchString,
Arrays.asList(contentFilter), sortFilter);
} catch (final NullPointerException | ParsingException ignored) {
return null;
} }
} }
@@ -935,6 +961,20 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
filterItemCheckedId = item.getItemId(); filterItemCheckedId = item.getItemId();
item.setChecked(true); item.setChecked(true);
if (service != null) {
final boolean isNotFiltered = theContentFilter.isEmpty()
|| "all".equals(theContentFilter.get(0));
if (isNotFiltered) {
searchEditText.setHint(
getString(R.string.search_with_service_name,
service.getServiceInfo().getName()));
} else {
searchEditText.setHint(getString(R.string.search_with_service_name_and_filter,
service.getServiceInfo().getName(),
item.getTitle()));
}
}
contentFilter = theContentFilter.toArray(new String[0]); contentFilter = theContentFilter.toArray(new String[0]);
if (!TextUtils.isEmpty(searchString)) { if (!TextUtils.isEmpty(searchString)) {
@@ -997,7 +1037,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
&& !(exceptions.size() == 1 && !(exceptions.size() == 1
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) { && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED, showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
searchString, serviceId)); searchString, serviceId, getOpenInBrowserUrlForErrors()));
} }
searchSuggestion = result.getSearchSuggestion(); searchSuggestion = result.getSearchSuggestion();
@@ -1064,15 +1104,26 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
public void handleNextItems(final ListExtractor.InfoItemsPage<?> result) { public void handleNextItems(final ListExtractor.InfoItemsPage<?> result) {
showListFooter(false); showListFooter(false);
infoListAdapter.addInfoItemList(result.getItems()); infoListAdapter.addInfoItemList(result.getItems());
nextPage = result.getNextPage();
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED, // nextPage should be non-null at this point, because it refers to the page
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", " // whose results are handled here, but let's check it anyway
+ "pageIds: " + nextPage.getIds() + ", " if (nextPage == null) {
+ "pageCookies: " + nextPage.getCookies(), showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
serviceId)); "\"" + searchString + "\" → nextPage == null", serviceId,
getOpenInBrowserUrlForErrors()));
} else {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
+ "pageIds: " + nextPage.getIds() + ", "
+ "pageCookies: " + nextPage.getCookies(),
serviceId, getOpenInBrowserUrlForErrors()));
}
} }
// keep the reassignment of nextPage after the error handling to ensure that nextPage
// still holds the correct value during the error handling
nextPage = result.getNextPage();
super.handleNextItems(result); super.handleNextItems(result);
} }

View File

@@ -10,6 +10,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@@ -18,8 +19,10 @@ import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.ItemViewMode; import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
import java.io.Serializable; import java.io.Serializable;
@@ -173,4 +176,27 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
} }
return mode; return mode;
} }
@Override
protected void showInfoItemDialog(final StreamInfoItem item) {
// Try and attach the InfoItemDialog to the parent fragment of the RelatedItemsFragment
// so that its context is not lost when the RelatedItemsFragment is reinitialized,
// e.g. when a new stream is loaded in a parent VideoDetailFragment.
final Fragment parentFragment = getParentFragment();
if (parentFragment != null) {
try {
new InfoItemDialog.Builder(
parentFragment.getActivity(),
parentFragment.getContext(),
parentFragment,
item
).create().show();
} catch (final IllegalArgumentException e) {
InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
}
} else {
super.showInfoItemDialog(item);
}
}
} }

View File

@@ -113,7 +113,10 @@ public enum StreamDialogDefaultEntry {
DOWNLOAD(R.string.download, (fragment, item) -> DOWNLOAD(R.string.download, (fragment, item) ->
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(), fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
item.getUrl(), info -> { item.getUrl(), info -> {
if (fragment.getContext() != null) { // Ensure the fragment is attached and its state hasn't been saved to avoid
// showing dialog during lifecycle changes or when the activity is paused,
// e.g. by selecting the download option and opening a different fragment.
if (fragment.isAdded() && !fragment.isStateSaved()) {
final DownloadDialog downloadDialog = final DownloadDialog downloadDialog =
new DownloadDialog(fragment.requireContext(), info); new DownloadDialog(fragment.requireContext(), info);
downloadDialog.show(fragment.getChildFragmentManager(), downloadDialog.show(fragment.getChildFragmentManager(),

View File

@@ -1,9 +1,13 @@
package org.schabi.newpipe.info_list.holder; package org.schabi.newpipe.info_list.holder;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById; import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
import android.text.Spanned;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
@@ -25,7 +29,6 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy; import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
import org.schabi.newpipe.util.text.TextEllipsizer; import org.schabi.newpipe.util.text.TextEllipsizer;
public class CommentInfoItemHolder extends InfoItemHolder { public class CommentInfoItemHolder extends InfoItemHolder {
@@ -98,14 +101,16 @@ public class CommentInfoItemHolder extends InfoItemHolder {
} }
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item)); itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
// setup the top row, with pinned icon, author name and comment date // setup the top row, with pinned icon, author name and comment date
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE); itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(), final String uploaderName = Localization.localizeUserName(item.getUploaderName());
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(), itemTitleView.setText(Localization.concatenateStrings(
uploaderName,
Localization.relativeTimeOrTextual(
itemBuilder.getContext(),
item.getUploadDate(),
item.getTextualUploadDate()))); item.getTextualUploadDate())));
// setup bottom row, with likes, heart and replies button // setup bottom row, with likes, heart and replies button
itemLikesCountView.setText( itemLikesCountView.setText(
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount())); Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
@@ -128,7 +133,26 @@ public class CommentInfoItemHolder extends InfoItemHolder {
textEllipsizer.ellipsize(); textEllipsizer.ellipsize();
//noinspection ClickableViewAccessibility //noinspection ClickableViewAccessibility
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); itemContentView.setOnTouchListener((v, event) -> {
final CharSequence text = itemContentView.getText();
if (text instanceof Spanned buffer) {
final int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
final int offset = getOffsetForHorizontalLine(itemContentView, event);
final var links = buffer.getSpans(offset, offset, ClickableSpan.class);
if (links.length != 0) {
if (action == MotionEvent.ACTION_UP) {
links[0].onClick(itemContentView);
}
// we handle events that intersect links, so return true
return true;
}
}
}
return false;
});
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
textEllipsizer.toggle(); textEllipsizer.toggle();

View File

@@ -7,3 +7,16 @@ import androidx.core.os.BundleCompat
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? { inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java) return BundleCompat.getParcelableArrayList(this, key, T::class.java)
} }
fun Bundle?.toDebugString(): String {
if (this == null) {
return "null"
}
val string = StringBuilder("Bundle{")
for (key in this.keySet()) {
@Suppress("DEPRECATION") // we want this[key] to return items of any type
string.append(" ").append(key).append(" => ").append(this[key]).append(";")
}
string.append(" }")
return string.toString()
}

View File

@@ -0,0 +1,7 @@
package org.schabi.newpipe.ktx
import android.content.SharedPreferences
fun SharedPreferences.getStringSafe(key: String, defValue: String): String {
return getString(key, null) ?: defValue
}

View File

@@ -17,8 +17,10 @@ import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.schabi.newpipe.MainActivity
// logs in this class are disabled by default since it's usually not useful,
// you can enable them by setting this flag to MainActivity.DEBUG
private const val DEBUG = false
private const val TAG = "ViewUtils" private const val TAG = "ViewUtils"
/** /**
@@ -38,7 +40,7 @@ fun View.animate(
delay: Long = 0, delay: Long = 0,
execOnEnd: Runnable? = null execOnEnd: Runnable? = null
) { ) {
if (MainActivity.DEBUG) { if (DEBUG) {
val id = try { val id = try {
resources.getResourceEntryName(id) resources.getResourceEntryName(id)
} catch (e: Exception) { } catch (e: Exception) {
@@ -51,7 +53,7 @@ fun View.animate(
Log.d(TAG, "animate(): $msg") Log.d(TAG, "animate(): $msg")
} }
if (isVisible && enterOrExit) { if (isVisible && enterOrExit) {
if (MainActivity.DEBUG) { if (DEBUG) {
Log.d(TAG, "animate(): view was already visible > view = [$this]") Log.d(TAG, "animate(): view was already visible > view = [$this]")
} }
animate().setListener(null).cancel() animate().setListener(null).cancel()
@@ -60,7 +62,7 @@ fun View.animate(
execOnEnd?.run() execOnEnd?.run()
return return
} else if ((isGone || isInvisible) && !enterOrExit) { } else if ((isGone || isInvisible) && !enterOrExit) {
if (MainActivity.DEBUG) { if (DEBUG) {
Log.d(TAG, "animate(): view was already gone > view = [$this]") Log.d(TAG, "animate(): view was already gone > view = [$this]")
} }
animate().setListener(null).cancel() animate().setListener(null).cancel()
@@ -89,7 +91,7 @@ fun View.animate(
* @param colorEnd the background color to end with * @param colorEnd the background color to end with
*/ */
fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) { fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
if (MainActivity.DEBUG) { if (DEBUG) {
Log.d( Log.d(
TAG, TAG,
"animateBackgroundColor() called with: view = [$this], duration = [$duration], " + "animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
@@ -109,7 +111,7 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
} }
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator { fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
if (MainActivity.DEBUG) { if (DEBUG) {
Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this") Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
} }
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat()) val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
@@ -127,7 +129,7 @@ fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
} }
fun View.animateRotation(duration: Long, targetRotation: Int) { fun View.animateRotation(duration: Long, targetRotation: Int) {
if (MainActivity.DEBUG) { if (DEBUG) {
Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this") Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
} }
animate().setListener(null).cancel() animate().setListener(null).cancel()

View File

@@ -194,9 +194,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
if (itemsList != null) { if (itemsList != null) {
animateHideRecyclerViewAllowingScrolling(itemsList); animateHideRecyclerViewAllowingScrolling(itemsList);
} }
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
} }
@Override @Override
@@ -205,9 +202,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
if (itemsList != null) { if (itemsList != null) {
animate(itemsList, true, 200); animate(itemsList, true, 200);
} }
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), true, 200);
}
} }
@Override @Override
@@ -253,9 +247,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
if (itemsList != null) { if (itemsList != null) {
animateHideRecyclerViewAllowingScrolling(itemsList); animateHideRecyclerViewAllowingScrolling(itemsList);
} }
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
} }
@Override @Override

View File

@@ -14,6 +14,7 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.info_list.ItemViewMode; import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.LocalItemHolder; import org.schabi.newpipe.local.holder.LocalItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
@@ -24,6 +25,7 @@ import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder; import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder; import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder; import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder; import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder; import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder; import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
@@ -73,10 +75,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000; private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001; private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002; private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
private static final int LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x2003;
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000; private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001; private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002; private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
private static final int REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x3003;
private final LocalItemBuilder localItemBuilder; private final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems; private final ArrayList<LocalItem> localItems;
@@ -87,6 +91,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private View header = null; private View header = null;
private View footer = null; private View footer = null;
private ItemViewMode itemViewMode = ItemViewMode.LIST; private ItemViewMode itemViewMode = ItemViewMode.LIST;
private boolean useItemHandle = false;
public LocalItemListAdapter(final Context context) { public LocalItemListAdapter(final Context context) {
recordManager = new HistoryRecordManager(context); recordManager = new HistoryRecordManager(context);
@@ -180,6 +185,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
this.itemViewMode = itemViewMode; this.itemViewMode = itemViewMode;
} }
public void setUseItemHandle(final boolean useItemHandle) {
this.useItemHandle = useItemHandle;
}
public void setHeader(final View header) { public void setHeader(final View header) {
final boolean changed = header != this.header; final boolean changed = header != this.header;
this.header = header; this.header = header;
@@ -257,7 +266,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
final LocalItem item = localItems.get(position); final LocalItem item = localItems.get(position);
switch (item.getLocalItemType()) { switch (item.getLocalItemType()) {
case PLAYLIST_LOCAL_ITEM: case PLAYLIST_LOCAL_ITEM:
if (itemViewMode == ItemViewMode.CARD) { if (useItemHandle) {
return LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.CARD) {
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE; return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.GRID) { } else if (itemViewMode == ItemViewMode.GRID) {
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE; return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
@@ -265,7 +276,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
return LOCAL_PLAYLIST_HOLDER_TYPE; return LOCAL_PLAYLIST_HOLDER_TYPE;
} }
case PLAYLIST_REMOTE_ITEM: case PLAYLIST_REMOTE_ITEM:
if (itemViewMode == ItemViewMode.CARD) { if (useItemHandle) {
return REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.CARD) {
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE; return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.GRID) { } else if (itemViewMode == ItemViewMode.GRID) {
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE; return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
@@ -314,12 +327,16 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
return new LocalPlaylistGridItemHolder(localItemBuilder, parent); return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE: case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
return new LocalPlaylistCardItemHolder(localItemBuilder, parent); return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
case LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE:
return new LocalBookmarkPlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_HOLDER_TYPE: case REMOTE_PLAYLIST_HOLDER_TYPE:
return new RemotePlaylistItemHolder(localItemBuilder, parent); return new RemotePlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE: case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
return new RemotePlaylistGridItemHolder(localItemBuilder, parent); return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE: case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
return new RemotePlaylistCardItemHolder(localItemBuilder, parent); return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
case REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE:
return new RemoteBookmarkPlaylistItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_HOLDER_TYPE: case STREAM_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent); return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_GRID_HOLDER_TYPE: case STREAM_PLAYLIST_GRID_HOLDER_TYPE:

View File

@@ -1,10 +1,13 @@
package org.schabi.newpipe.local.bookmark; package org.schabi.newpipe.local.bookmark;
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.text.InputType; import android.text.InputType;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -13,6 +16,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.evernote.android.state.State;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
@@ -27,29 +34,44 @@ import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.debounce.DebounceSavable;
import org.schabi.newpipe.util.debounce.DebounceSaver;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> { public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void>
implements DebounceSavable {
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
@State @State
protected Parcelable itemsListState; Parcelable itemsListState;
private Subscription databaseSubscription; private Subscription databaseSubscription;
private CompositeDisposable disposables = new CompositeDisposable(); private CompositeDisposable disposables = new CompositeDisposable();
private LocalPlaylistManager localPlaylistManager; private LocalPlaylistManager localPlaylistManager;
private RemotePlaylistManager remotePlaylistManager; private RemotePlaylistManager remotePlaylistManager;
private ItemTouchHelper itemTouchHelper;
/* Have the bookmarked playlists been fully loaded from db */
private AtomicBoolean isLoadingComplete;
/* Gives enough time to avoid interrupting user sorting operations */
@Nullable
private DebounceSaver debounceSaver;
private List<Pair<Long, LocalItem.LocalItemType>> deletedItems;
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Creation // Fragment LifeCycle - Creation
@@ -65,6 +87,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
localPlaylistManager = new LocalPlaylistManager(database); localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database); remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable(); disposables = new CompositeDisposable();
isLoadingComplete = new AtomicBoolean();
debounceSaver = new DebounceSaver(3000, this);
deletedItems = new ArrayList<>();
} }
@Nullable @Nullable
@@ -91,10 +118,20 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
// Fragment LifeCycle - Views // Fragment LifeCycle - Views
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
itemListAdapter.setUseItemHandle(true);
}
@Override @Override
protected void initListeners() { protected void initListeners() {
super.initListeners(); super.initListeners();
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
itemListAdapter.setSelectedListener(new OnClickGesture<>() { itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override @Override
public void selected(final LocalItem selectedItem) { public void selected(final LocalItem selectedItem) {
@@ -102,7 +139,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
if (selectedItem instanceof PlaylistMetadataEntry) { if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid, NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
entry.name); entry.name);
} else if (selectedItem instanceof PlaylistRemoteEntity) { } else if (selectedItem instanceof PlaylistRemoteEntity) {
@@ -123,6 +160,14 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem); showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
} }
} }
@Override
public void drag(final LocalItem selectedItem,
final RecyclerView.ViewHolder viewHolder) {
if (itemTouchHelper != null) {
itemTouchHelper.startDrag(viewHolder);
}
}
}); });
} }
@@ -134,8 +179,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
public void startLoading(final boolean forceLoad) { public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad); super.startLoading(forceLoad);
Flowable.combineLatest(localPlaylistManager.getPlaylists(), if (debounceSaver != null) {
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setNoChangesToSave();
}
isLoadingComplete.set(false);
getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
.onBackpressureLatest() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistsSubscriber()); .subscribe(getPlaylistsSubscriber());
@@ -149,6 +199,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
// Save on exit
saveImmediate();
} }
@Override @Override
@@ -163,19 +216,27 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
} }
databaseSubscription = null; databaseSubscription = null;
itemTouchHelper = null;
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (debounceSaver != null) {
debounceSaver.getDebouncedSaveSignal().onComplete();
}
if (disposables != null) { if (disposables != null) {
disposables.dispose(); disposables.dispose();
} }
debounceSaver = null;
disposables = null; disposables = null;
localPlaylistManager = null; localPlaylistManager = null;
remotePlaylistManager = null; remotePlaylistManager = null;
itemsListState = null; itemsListState = null;
isLoadingComplete = null;
deletedItems = null;
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@@ -183,10 +244,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() { private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
return new Subscriber<List<PlaylistLocalItem>>() { return new Subscriber<>() {
@Override @Override
public void onSubscribe(final Subscription s) { public void onSubscribe(final Subscription s) {
showLoading(); showLoading();
isLoadingComplete.set(false);
if (databaseSubscription != null) { if (databaseSubscription != null) {
databaseSubscription.cancel(); databaseSubscription.cancel();
} }
@@ -196,7 +259,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override @Override
public void onNext(final List<PlaylistLocalItem> subscriptions) { public void onNext(final List<PlaylistLocalItem> subscriptions) {
handleResult(subscriptions); if (debounceSaver == null || !debounceSaver.getIsModified()) {
handleResult(subscriptions);
isLoadingComplete.set(true);
}
if (databaseSubscription != null) { if (databaseSubscription != null) {
databaseSubscription.request(1); databaseSubscription.request(1);
} }
@@ -209,7 +275,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
} }
@Override @Override
public void onComplete() { } public void onComplete() {
}
}; };
} }
@@ -244,12 +311,183 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
} }
} }
/*//////////////////////////////////////////////////////////////////////////
// Playlist Metadata Manipulation
//////////////////////////////////////////////////////////////////////////*/
private void changeLocalPlaylistName(final long id, final String name) {
if (localPlaylistManager == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "Updating playlist id=[" + id + "] "
+ "with new name=[" + name + "] items");
}
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Changing playlist name")));
disposables.add(disposable);
}
private void deleteItem(final PlaylistLocalItem item) {
if (itemListAdapter == null) {
return;
}
itemListAdapter.removeItem(item);
if (item instanceof PlaylistMetadataEntry) {
deletedItems.add(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM));
} else if (item instanceof PlaylistRemoteEntity) {
deletedItems.add(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM));
}
if (debounceSaver != null) {
debounceSaver.setHasChangesToSave();
saveImmediate();
}
}
@Override
public void saveImmediate() {
if (itemListAdapter == null) {
return;
}
// List must be loaded and modified in order to save
if (isLoadingComplete == null || debounceSaver == null
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
return;
}
final List<LocalItem> items = itemListAdapter.getItemsList();
final List<PlaylistMetadataEntry> localItemsUpdate = new ArrayList<>();
final List<Long> localItemsDeleteUid = new ArrayList<>();
final List<PlaylistRemoteEntity> remoteItemsUpdate = new ArrayList<>();
final List<Long> remoteItemsDeleteUid = new ArrayList<>();
// Calculate display index
for (int i = 0; i < items.size(); i++) {
final LocalItem item = items.get(i);
if (item instanceof PlaylistMetadataEntry
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
((PlaylistMetadataEntry) item).setDisplayIndex(i);
localItemsUpdate.add((PlaylistMetadataEntry) item);
} else if (item instanceof PlaylistRemoteEntity
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
((PlaylistRemoteEntity) item).setDisplayIndex(i);
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
}
}
// Find deleted items
for (final Pair<Long, LocalItem.LocalItemType> item : deletedItems) {
if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
localItemsDeleteUid.add(item.first);
} else if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
remoteItemsDeleteUid.add(item.first);
}
}
deletedItems.clear();
// 1. Update local playlists
// 2. Update remote playlists
// 3. Set NoChangesToSave
disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid)
.mergeWith(remotePlaylistManager.updatePlaylists(
remoteItemsUpdate, remoteItemsDeleteUid))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
if (debounceSaver != null) {
debounceSaver.setNoChangesToSave();
}
},
throwable -> showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
));
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
// if adding grid layout, also include ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
// with an `if (shouldUseGridLayout()) ...`
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.ACTION_STATE_IDLE) {
@Override
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
final int viewSize,
final int viewSizeOutOfBounds,
final int totalSize,
final long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView,
viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
Math.abs(standardSpeed));
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
public boolean onMove(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder source,
@NonNull final RecyclerView.ViewHolder target) {
// Allow swap LocalBookmarkPlaylistItemHolder and RemoteBookmarkPlaylistItemHolder.
if (itemListAdapter == null
|| source.getItemViewType() != target.getItemViewType()
&& !(
(
(source instanceof LocalBookmarkPlaylistItemHolder)
|| (source instanceof RemoteBookmarkPlaylistItemHolder)
)
&& (
(target instanceof LocalBookmarkPlaylistItemHolder)
|| (target instanceof RemoteBookmarkPlaylistItemHolder)
))
) {
return false;
}
final int sourceIndex = source.getBindingAdapterPosition();
final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped && debounceSaver != null) {
debounceSaver.setHasChangesToSave();
}
return isSwapped;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int swipeDir) {
// Do nothing.
}
};
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Utils // Utils
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid())); showDeleteDialog(item.getName(), item);
} }
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
@@ -257,7 +495,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
final String delete = getString(R.string.delete); final String delete = getString(R.string.delete);
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail); final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
final boolean isThumbnailPermanent = localPlaylistManager final boolean isThumbnailPermanent = localPlaylistManager
.getIsPlaylistThumbnailPermanent(selectedItem.uid); .getIsPlaylistThumbnailPermanent(selectedItem.getUid());
final ArrayList<String> items = new ArrayList<>(); final ArrayList<String> items = new ArrayList<>();
items.add(rename); items.add(rename);
@@ -270,13 +508,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
if (items.get(index).equals(rename)) { if (items.get(index).equals(rename)) {
showRenameDialog(selectedItem); showRenameDialog(selectedItem);
} else if (items.get(index).equals(delete)) { } else if (items.get(index).equals(delete)) {
showDeleteDialog(selectedItem.name, showDeleteDialog(selectedItem.name, selectedItem);
localPlaylistManager.deletePlaylist(selectedItem.uid));
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) { } else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
final long thumbnailStreamId = localPlaylistManager final long thumbnailStreamId = localPlaylistManager
.getAutomaticPlaylistThumbnailStreamId(selectedItem.uid); .getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
localPlaylistManager localPlaylistManager
.changePlaylistThumbnail(selectedItem.uid, thumbnailStreamId, false) .changePlaylistThumbnail(selectedItem.getUid(), thumbnailStreamId, false)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(); .subscribe();
} }
@@ -298,13 +535,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
.setView(dialogBinding.getRoot()) .setView(dialogBinding.getRoot())
.setPositiveButton(R.string.rename_playlist, (dialog, which) -> .setPositiveButton(R.string.rename_playlist, (dialog, which) ->
changeLocalPlaylistName( changeLocalPlaylistName(
selectedItem.uid, selectedItem.getUid(),
dialogBinding.dialogEditText.getText().toString())) dialogBinding.dialogEditText.getText().toString()))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show(); .show();
} }
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) { private void showDeleteDialog(final String name, final PlaylistLocalItem item) {
if (activity == null || disposables == null) { if (activity == null || disposables == null) {
return; return;
} }
@@ -313,35 +550,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
.setTitle(name) .setTitle(name)
.setMessage(R.string.delete_playlist_prompt) .setMessage(R.string.delete_playlist_prompt)
.setCancelable(true) .setCancelable(true)
.setPositiveButton(R.string.delete, (dialog, i) -> .setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item))
disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Deleting playlist")))))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show(); .show();
} }
private void changeLocalPlaylistName(final long id, final String name) {
if (localPlaylistManager == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "Updating playlist id=[" + id + "] "
+ "with new name=[" + name + "] items");
}
localPlaylistManager.renamePlaylist(id, name);
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Changing playlist name")));
disposables.add(disposable);
}
} }

View File

@@ -0,0 +1,95 @@
package org.schabi.newpipe.local.bookmark;
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
/**
* Takes care of remote and local playlists at once, hence "merged".
*/
public final class MergedPlaylistManager {
private MergedPlaylistManager() {
}
public static Flowable<List<PlaylistLocalItem>> getMergedOrderedPlaylists(
final LocalPlaylistManager localPlaylistManager,
final RemotePlaylistManager remotePlaylistManager) {
return Flowable.combineLatest(
localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(),
MergedPlaylistManager::merge
);
}
/**
* Merge localPlaylists and remotePlaylists by the display index.
* If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}.
*
* @param localPlaylists local playlists, already sorted by display index
* @param remotePlaylists remote playlists, already sorted by display index
* @return merged playlists
*/
public static List<PlaylistLocalItem> merge(
final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) {
// This algorithm is similar to the merge operation in merge sort.
final List<PlaylistLocalItem> result = new ArrayList<>(
localPlaylists.size() + remotePlaylists.size());
final List<PlaylistLocalItem> itemsWithSameIndex = new ArrayList<>();
int i = 0;
int j = 0;
while (i < localPlaylists.size()) {
while (j < remotePlaylists.size()) {
if (remotePlaylists.get(j).getDisplayIndex()
<= localPlaylists.get(i).getDisplayIndex()) {
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
j++;
} else {
break;
}
}
addItem(result, localPlaylists.get(i), itemsWithSameIndex);
i++;
}
while (j < remotePlaylists.size()) {
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
j++;
}
addItemsWithSameIndex(result, itemsWithSameIndex);
return result;
}
private static void addItem(final List<PlaylistLocalItem> result,
final PlaylistLocalItem item,
final List<PlaylistLocalItem> itemsWithSameIndex) {
if (!itemsWithSameIndex.isEmpty()
&& itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) {
// The new item has a different display index, add previous items with same
// index to the result.
addItemsWithSameIndex(result, itemsWithSameIndex);
itemsWithSameIndex.clear();
}
itemsWithSameIndex.add(item);
}
private static void addItemsWithSameIndex(final List<PlaylistLocalItem> result,
final List<PlaylistLocalItem> itemsWithSameIndex) {
Collections.sort(itemsWithSameIndex,
Comparator.comparing(PlaylistLocalItem::getOrderingName,
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
result.addAll(itemsWithSameIndex);
}
}

View File

@@ -155,14 +155,15 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT); final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams) playlistDisposables.add(manager.appendToPlaylist(playlist.getUid(), streams)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { .subscribe(ignored -> {
successToast.show(); successToast.show();
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) { if (playlist.thumbnailUrl != null
&& playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
playlistDisposables.add(manager playlistDisposables.add(manager
.changePlaylistThumbnail(playlist.uid, streams.get(0).getUid(), .changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
false) false)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> successToast.show())); .subscribe(ignore -> successToast.show()));

View File

@@ -44,11 +44,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.evernote.android.state.State
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Item import com.xwray.groupie.Item
import com.xwray.groupie.OnItemClickListener import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener import com.xwray.groupie.OnItemLongClickListener
import icepick.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
@@ -269,7 +269,12 @@ class FeedFragment : BaseStateFragment<FeedState>() {
override fun onDestroyOptionsMenu() { override fun onDestroyOptionsMenu() {
super.onDestroyOptionsMenu() super.onDestroyOptionsMenu()
activity?.supportActionBar?.subtitle = null if (
(groupName != "") &&
(activity?.supportActionBar?.subtitle == groupName)
) {
activity?.supportActionBar?.subtitle = null
}
} }
override fun onDestroy() { override fun onDestroy() {
@@ -281,7 +286,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
} }
super.onDestroy() super.onDestroy()
activity?.supportActionBar?.subtitle = null
if (
(groupName != "") &&
(activity?.supportActionBar?.subtitle == groupName)
) {
activity?.supportActionBar?.subtitle = null
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -549,7 +560,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
var typeface = Typeface.DEFAULT var typeface = Typeface.DEFAULT
var backgroundSupplier = { ctx: Context -> var backgroundSupplier = { ctx: Context ->
resolveDrawable(ctx, R.attr.selectableItemBackground) resolveDrawable(ctx, android.R.attr.selectableItemBackground)
} }
if (doCheck) { if (doCheck) {
// If the uploadDate is null or true we should highlight the item // If the uploadDate is null or true we should highlight the item
@@ -562,7 +573,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
LayerDrawable( LayerDrawable(
arrayOf( arrayOf(
resolveDrawable(ctx, R.attr.dashed_border), resolveDrawable(ctx, R.attr.dashed_border),
resolveDrawable(ctx, R.attr.selectableItemBackground) resolveDrawable(ctx, android.R.attr.selectableItemBackground)
) )
) )
} }

View File

@@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) {
// Show individual stream notifications, set channel icon only if there is actually // Show individual stream notifications, set channel icon only if there is actually
// one // one
showStreamNotifications(newStreams, data.serviceId, bitmap) showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
// Show summary notification // Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build()) manager.notify(data.pseudoId, summaryBuilder.build())
@@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) {
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) { override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
// Show individual stream notifications // Show individual stream notifications
showStreamNotifications(newStreams, data.serviceId, null) showStreamNotifications(newStreams, data.serviceId, data.url, null)
// Show summary notification // Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build()) manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected iconLoadingTargets.remove(this) // allow it to be garbage-collected
@@ -118,10 +118,11 @@ class NotificationHelper(val context: Context) {
private fun showStreamNotifications( private fun showStreamNotifications(
newStreams: List<StreamInfoItem>, newStreams: List<StreamInfoItem>,
serviceId: Int, serviceId: Int,
channelUrl: String,
channelIcon: Bitmap? channelIcon: Bitmap?
) { ) {
for (stream in newStreams) { for (stream in newStreams) {
val notification = createStreamNotification(stream, serviceId, channelIcon) val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
manager.notify(stream.url.hashCode(), notification) manager.notify(stream.url.hashCode(), notification)
} }
} }
@@ -129,6 +130,7 @@ class NotificationHelper(val context: Context) {
private fun createStreamNotification( private fun createStreamNotification(
item: StreamInfoItem, item: StreamInfoItem,
serviceId: Int, serviceId: Int,
channelUrl: String,
channelIcon: Bitmap? channelIcon: Bitmap?
): Notification { ): Notification {
return NotificationCompat.Builder( return NotificationCompat.Builder(
@@ -139,7 +141,7 @@ class NotificationHelper(val context: Context) {
.setLargeIcon(channelIcon) .setLargeIcon(channelIcon)
.setContentTitle(item.name) .setContentTitle(item.name)
.setContentText(item.uploaderName) .setContentText(item.uploaderName)
.setGroup(item.uploaderUrl) .setGroup(channelUrl)
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background)) .setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
.setColorized(true) .setColorized(true)
.setAutoCancel(true) .setAutoCancel(true)

View File

@@ -1,6 +1,8 @@
package org.schabi.newpipe.local.feed.notifications package org.schabi.newpipe.local.feed.notifications
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.work.Constraints import androidx.work.Constraints
@@ -83,7 +85,9 @@ class NotificationWorker(
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
.setContentTitle(applicationContext.getString(R.string.feed_notification_loading)) .setContentTitle(applicationContext.getString(R.string.feed_notification_loading))
.build() .build()
setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification)) // ServiceInfo constants are not used below Android Q, so 0 is set here
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0
setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification, serviceType))
} }
companion object { companion object {

View File

@@ -17,8 +17,10 @@ import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.Info import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.feed.FeedInfo import org.schabi.newpipe.extractor.feed.FeedInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.getStringSafe
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.SubscriptionManager import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ChannelTabHelper import org.schabi.newpipe.util.ChannelTabHelper
@@ -69,12 +71,10 @@ class FeedLoadManager(private val context: Context) {
val outdatedThreshold = if (ignoreOutdatedThreshold) { val outdatedThreshold = if (ignoreOutdatedThreshold) {
OffsetDateTime.now(ZoneOffset.UTC) OffsetDateTime.now(ZoneOffset.UTC)
} else { } else {
val thresholdOutdatedSeconds = ( val thresholdOutdatedSeconds = defaultSharedPreferences.getStringSafe(
defaultSharedPreferences.getString( context.getString(R.string.feed_update_threshold_key),
context.getString(R.string.feed_update_threshold_key), context.getString(R.string.feed_update_threshold_default_value)
context.getString(R.string.feed_update_threshold_default_value) ).toInt()
) ?: context.getString(R.string.feed_update_threshold_default_value)
).toInt()
OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(thresholdOutdatedSeconds.toLong()) OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(thresholdOutdatedSeconds.toLong())
} }
@@ -91,6 +91,10 @@ class FeedLoadManager(private val context: Context) {
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold) else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
} }
// like `currentProgress`, but counts the number of YouTube extractions that have begun, so
// they can be properly throttled every once in a while (see doOnNext below)
val youtubeExtractionCount = AtomicInteger()
return outdatedSubscriptions return outdatedSubscriptions
.take(1) .take(1)
.doOnNext { .doOnNext {
@@ -106,6 +110,15 @@ class FeedLoadManager(private val context: Context) {
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.flatMap { Flowable.fromIterable(it) } .flatMap { Flowable.fromIterable(it) }
.takeWhile { !cancelSignal.get() } .takeWhile { !cancelSignal.get() }
.doOnNext { subscriptionEntity ->
// throttle YouTube extractions once every BATCH_SIZE to avoid being rate limited
if (subscriptionEntity.serviceId == ServiceList.YouTube.serviceId) {
val previousCount = youtubeExtractionCount.getAndIncrement()
if (previousCount != 0 && previousCount % BATCH_SIZE == 0) {
Thread.sleep(DELAY_BETWEEN_BATCHES_MILLIS.random())
}
}
}
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2) .parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2) .runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
.filter { !cancelSignal.get() } .filter { !cancelSignal.get() }
@@ -329,7 +342,19 @@ class FeedLoadManager(private val context: Context) {
/** /**
* How many extractions will be running in parallel. * How many extractions will be running in parallel.
*/ */
private const val PARALLEL_EXTRACTIONS = 6 private const val PARALLEL_EXTRACTIONS = 3
/**
* How many YouTube extractions to perform before waiting [DELAY_BETWEEN_BATCHES_MILLIS]
* to avoid being rate limited
*/
private const val BATCH_SIZE = 50
/**
* Wait a random delay in this range once every [BATCH_SIZE] YouTube extractions to avoid
* being rate limited
*/
private val DELAY_BETWEEN_BATCHES_MILLIS = (6000L..12000L)
/** /**
* Number of items to buffer to mass-insert in the database. * Number of items to buffer to mass-insert in the database.

View File

@@ -31,6 +31,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
@@ -200,7 +201,7 @@ class FeedLoadService : Service() {
} }
} }
} }
registerReceiver(broadcastReceiver, IntentFilter(ACTION_CANCEL)) ContextCompat.registerReceiver(this, broadcastReceiver, IntentFilter(ACTION_CANCEL), ContextCompat.RECEIVER_NOT_EXPORTED)
} }
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////

View File

@@ -18,7 +18,7 @@ data class FeedUpdateInfo(
@NotificationMode @NotificationMode
val notificationMode: Int, val notificationMode: Int,
val name: String, val name: String,
val avatarUrl: String, val avatarUrl: String?,
val url: String, val url: String,
val serviceId: Int, val serviceId: Int,
// description and subscriberCount are null if the constructor info is from the fast feed method // description and subscriberCount are null if the constructor info is from the fast feed method

View File

@@ -15,6 +15,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding; import androidx.viewbinding.ViewBinding;
import com.evernote.android.state.State;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
@@ -45,7 +46,6 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
@@ -332,10 +332,6 @@ public class StatisticsPlaylistFragment
StreamDialogDefaultEntry.DELETE, StreamDialogDefaultEntry.DELETE,
(f, i) -> deleteEntry( (f, i) -> deleteEntry(
Math.max(itemListAdapter.getItemsList().indexOf(item), 0))) Math.max(itemListAdapter.getItemsList().indexOf(item), 0)))
.setAction(
StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
(f, i) -> NavigationHelper.playOnBackgroundPlayer(
context, getPlayQueueStartingAt(item), true))
.create() .create()
.show(); .show();
} catch (final IllegalArgumentException e) { } catch (final IllegalArgumentException e) {
@@ -368,6 +364,7 @@ public class StatisticsPlaylistFragment
} }
} }
@Override
public PlayQueue getPlayQueue() { public PlayQueue getPlayQueue() {
return getPlayQueue(0); return getPlayQueue(0);
} }

View File

@@ -0,0 +1,54 @@
package org.schabi.newpipe.local.holder;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.time.format.DateTimeFormatter;
public class LocalBookmarkPlaylistItemHolder extends LocalPlaylistItemHolder {
private final View itemHandleView;
public LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
}
LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemHandleView = itemView.findViewById(R.id.itemHandle);
}
@Override
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistMetadataEntry)) {
return;
}
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemHandleView.setOnTouchListener(getOnTouchListener(item));
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}
private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) {
return (view, motionEvent) -> {
view.performClick();
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
itemBuilder.getOnItemSelectedListener().drag(item,
LocalBookmarkPlaylistItemHolder.this);
}
return false;
};
}
}

View File

@@ -0,0 +1,54 @@
package org.schabi.newpipe.local.holder;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.time.format.DateTimeFormatter;
public class RemoteBookmarkPlaylistItemHolder extends RemotePlaylistItemHolder {
private final View itemHandleView;
public RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
}
RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemHandleView = itemView.findViewById(R.id.itemHandle);
}
@Override
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistRemoteEntity)) {
return;
}
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemHandleView.setOnTouchListener(getOnTouchListener(item));
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}
private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) {
return (view, motionEvent) -> {
view.performClick();
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
itemBuilder.getOnItemSelectedListener().drag(item,
RemoteBookmarkPlaylistItemHolder.this);
}
return false;
};
}
}

View File

@@ -14,6 +14,7 @@ import org.schabi.newpipe.util.ServiceHelper;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
public class RemotePlaylistItemHolder extends PlaylistItemHolder { public class RemotePlaylistItemHolder extends PlaylistItemHolder {
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) { final ViewGroup parent) {
super(infoItemBuilder, parent); super(infoItemBuilder, parent);

View File

@@ -0,0 +1,72 @@
package org.schabi.newpipe.local.playlist
import android.content.Context
import org.schabi.newpipe.R
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
import org.schabi.newpipe.extractor.exceptions.ParsingException
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory
import org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS
import org.schabi.newpipe.local.playlist.PlayListShareMode.WITH_TITLES
import org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST
fun export(
shareMode: PlayListShareMode,
playlist: List<PlaylistStreamEntry>,
context: Context
): String {
return when (shareMode) {
WITH_TITLES -> exportWithTitles(playlist, context)
JUST_URLS -> exportJustUrls(playlist)
YOUTUBE_TEMP_PLAYLIST -> exportAsYoutubeTempPlaylist(playlist)
}
}
fun exportWithTitles(
playlist: List<PlaylistStreamEntry>,
context: Context
): String {
return playlist.asSequence()
.map { it.streamEntity }
.map { entity ->
context.getString(
R.string.video_details_list_item,
entity.title,
entity.url
)
}
.joinToString(separator = "\n")
}
fun exportJustUrls(playlist: List<PlaylistStreamEntry>): String {
return playlist.asSequence()
.map { it.streamEntity.url }
.joinToString(separator = "\n")
}
fun exportAsYoutubeTempPlaylist(playlist: List<PlaylistStreamEntry>): String {
val videoIDs = playlist.asReversed().asSequence()
.map { it.streamEntity.url }
.mapNotNull(::getYouTubeId)
.take(50) // YouTube limitation: temp playlists can't have more than 50 items
.toList()
.asReversed()
.joinToString(separator = ",")
return "https://www.youtube.com/watch_videos?video_ids=$videoIDs"
}
val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFactory.getInstance()
/**
* Gets the video id from a YouTube URL.
*
* @param url YouTube URL
* @return the video id
*/
fun getYouTubeId(url: String): String? {
return try { linkHandler.getId(url) } catch (e: ParsingException) { null }
}

View File

@@ -2,8 +2,13 @@ package org.schabi.newpipe.local.playlist;
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar; import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.local.playlist.ExportPlaylistKt.export;
import static org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS;
import static org.schabi.newpipe.local.playlist.PlayListShareMode.WITH_TITLES;
import static org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
@@ -26,6 +31,7 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding; import androidx.viewbinding.ViewBinding;
import com.evernote.android.state.State;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
@@ -53,27 +59,25 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.PlayButtonHelper; import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.debounce.DebounceSavable;
import org.schabi.newpipe.util.debounce.DebounceSaver;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
implements PlaylistControlViewHolder { implements PlaylistControlViewHolder, DebounceSavable {
/** Save the list 10 seconds after the last change occurred. */
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
@State @State
protected Long playlistId; protected Long playlistId;
@@ -90,13 +94,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private LocalPlaylistManager playlistManager; private LocalPlaylistManager playlistManager;
private Subscription databaseSubscription; private Subscription databaseSubscription;
private PublishSubject<Long> debouncedSaveSignal;
private CompositeDisposable disposables; private CompositeDisposable disposables;
/** Whether the playlist has been fully loaded from db. */ /** Whether the playlist has been fully loaded from db. */
private AtomicBoolean isLoadingComplete; private AtomicBoolean isLoadingComplete;
/** Whether the playlist has been modified (e.g. items reordered or deleted) */ /** Used to debounce saving playlist edits to disk. */
private AtomicBoolean isModified; private DebounceSaver debounceSaver;
/** Flag to prevent simultaneous rewrites of the playlist. */ /** Flag to prevent simultaneous rewrites of the playlist. */
private boolean isRewritingPlaylist = false; private boolean isRewritingPlaylist = false;
@@ -121,12 +124,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext())); playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
debouncedSaveSignal = PublishSubject.create();
disposables = new CompositeDisposable(); disposables = new CompositeDisposable();
isLoadingComplete = new AtomicBoolean(); isLoadingComplete = new AtomicBoolean();
isModified = new AtomicBoolean(); debounceSaver = new DebounceSaver(this);
} }
@Override @Override
@@ -166,17 +168,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return headerBinding; return headerBinding;
} }
/**
* <p>Commit changes immediately if the playlist has been modified.</p>
* Delete operations and other modifications will be committed to ensure that the database
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
*/
public void commitChanges() {
if (isModified != null && isModified.get()) {
saveImmediate();
}
}
@Override @Override
protected void initListeners() { protected void initListeners() {
super.initListeners(); super.initListeners();
@@ -243,10 +234,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (disposables != null) { if (disposables != null) {
disposables.clear(); disposables.clear();
} }
disposables.add(getDebouncedSaver());
if (debounceSaver != null) {
disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setNoChangesToSave();
}
isLoadingComplete.set(false); isLoadingComplete.set(false);
isModified.set(false);
playlistManager.getPlaylistStreams(playlistId) playlistManager.getPlaylistStreams(playlistId)
.onBackpressureLatest() .onBackpressureLatest()
@@ -304,8 +298,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (debouncedSaveSignal != null) { if (debounceSaver != null) {
debouncedSaveSignal.onComplete(); debounceSaver.getDebouncedSaveSignal().onComplete();
} }
if (disposables != null) { if (disposables != null) {
disposables.dispose(); disposables.dispose();
@@ -314,12 +308,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
tabsPagerAdapter.getLocalPlaylistFragments().remove(this); tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
} }
debouncedSaveSignal = null; debounceSaver = null;
playlistManager = null; playlistManager = null;
disposables = null; disposables = null;
isLoadingComplete = null; isLoadingComplete = null;
isModified = null;
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@@ -343,7 +336,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override @Override
public void onNext(final List<PlaylistStreamEntry> streams) { public void onNext(final List<PlaylistStreamEntry> streams) {
// Skip handling the result after it has been modified // Skip handling the result after it has been modified
if (isModified == null || !isModified.get()) { if (debounceSaver == null || !debounceSaver.getIsModified()) {
handleResult(streams); handleResult(streams);
isLoadingComplete.set(true); isLoadingComplete.set(true);
} }
@@ -396,34 +389,41 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
/** /**
* Shares the playlist as a list of stream URLs if {@code shouldSharePlaylistDetails} is * Shares the playlist in one of 3 ways, depending on the value of {@code shareMode}:
* set to {@code false}. Shares the playlist name along with a list of video titles and URLs * <ul>
* if {@code shouldSharePlaylistDetails} is set to {@code true}. * <li>{@code JUST_URLS}: shares the URLs only.</li>
* <li>{@code WITH_TITLES}: each entry in the list is accompanied by its title.</li>
* <li>{@code YOUTUBE_TEMP_PLAYLIST}: shares as a YouTube temporary playlist.</li>
* </ul>
* *
* @param shouldSharePlaylistDetails Whether the playlist details should be included in the * @param shareMode The way the playlist should be shared.
* shared content.
*/ */
private void sharePlaylist(final boolean shouldSharePlaylistDetails) { private void sharePlaylist(final PlayListShareMode shareMode) {
final Context context = requireContext(); final Context context = requireContext();
disposables.add(playlistManager.getPlaylistStreams(playlistId) disposables.add(playlistManager.getPlaylistStreams(playlistId)
.flatMapSingle(playlist -> Single.just(playlist.stream() .flatMapSingle(playlist -> Single.just(export(
.map(PlaylistStreamEntry::getStreamEntity)
.map(streamEntity -> { shareMode,
if (shouldSharePlaylistDetails) { playlist,
return context.getString(R.string.video_details_list_item, context
streamEntity.getTitle(), streamEntity.getUrl()); )))
} else { .observeOn(AndroidSchedulers.mainThread())
return streamEntity.getUrl(); .subscribe(
} urlsText -> {
})
.collect(Collectors.joining("\n")))) final String content = shareMode == WITH_TITLES
.observeOn(AndroidSchedulers.mainThread()) ? context.getString(R.string.share_playlist_content_details,
.subscribe(urlsText -> ShareUtils.shareText( name,
context, name, shouldSharePlaylistDetails urlsText
? context.getString(R.string.share_playlist_content_details, )
name, urlsText) : urlsText), : urlsText;
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
ShareUtils.shareText(context, name, content);
},
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)
)
);
} }
public void removeWatchedStreams(final boolean removePartiallyWatched) { public void removeWatchedStreams(final boolean removePartiallyWatched) {
@@ -495,7 +495,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemListAdapter.clearStreamItemList(); itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(itemsToKeep); itemListAdapter.addItems(itemsToKeep);
saveChanges(); debounceSaver.setHasChangesToSave();
if (thumbnailVideoRemoved) { if (thumbnailVideoRemoved) {
updateThumbnailUrl(); updateThumbnailUrl();
@@ -666,7 +666,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemListAdapter.clearStreamItemList(); itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(itemsToKeep); itemListAdapter.addItems(itemsToKeep);
setStreamCountAndOverallDuration(itemListAdapter.getItemsList()); setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
saveChanges(); debounceSaver.setHasChangesToSave();
hideLoading(); hideLoading();
isRewritingPlaylist = false; isRewritingPlaylist = false;
@@ -685,41 +685,23 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
setStreamCountAndOverallDuration(itemListAdapter.getItemsList()); setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
saveChanges(); debounceSaver.setHasChangesToSave();
} }
private void saveChanges() { /**
if (isModified == null || debouncedSaveSignal == null) { * <p>Commit changes immediately if the playlist has been modified.</p>
return; * Delete operations and other modifications will be committed to ensure that the database
} * is up to date, e.g. when the user adds the just deleted stream from another fragment.
*/
isModified.set(true); @Override
debouncedSaveSignal.onNext(System.currentTimeMillis()); public void saveImmediate() {
}
private Disposable getDebouncedSaver() {
if (debouncedSaveSignal == null) {
return Disposable.empty();
}
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> saveImmediate(), throwable ->
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
"Debounced saver")));
}
private void saveImmediate() {
if (playlistManager == null || itemListAdapter == null) { if (playlistManager == null || itemListAdapter == null) {
return; return;
} }
// List must be loaded and modified in order to save // List must be loaded and modified in order to save
if (isLoadingComplete == null || isModified == null if (isLoadingComplete == null || debounceSaver == null
|| !isLoadingComplete.get() || !isModified.get()) { || !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
Log.w(TAG, "Attempting to save playlist when local playlist "
+ "is not loaded or not modified: playlist id=[" + playlistId + "]");
return; return;
} }
@@ -740,8 +722,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
() -> { () -> {
if (isModified != null) { if (debounceSaver != null) {
isModified.set(false); debounceSaver.setNoChangesToSave();
} }
}, },
throwable -> showError(new ErrorInfo(throwable, throwable -> showError(new ErrorInfo(throwable,
@@ -784,7 +766,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final int targetIndex = target.getBindingAdapterPosition(); final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped) { if (isSwapped) {
saveChanges(); debounceSaver.setHasChangesToSave();
} }
return isSwapped; return isSwapped;
} }
@@ -867,11 +849,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
headerBinding.playlistStreamCount.setText( headerBinding.playlistStreamCount.setText(
Localization.concatenateStrings( Localization.concatenateStrings(
Localization.localizeStreamCount(activity, streamCount), Localization.localizeStreamCount(activity, streamCount),
Localization.getDurationString(playlistOverallDurationSeconds)) Localization.getDurationString(playlistOverallDurationSeconds,
true, true))
); );
} }
} }
@Override
public PlayQueue getPlayQueue() { public PlayQueue getPlayQueue() {
return getPlayQueue(0); return getPlayQueue(0);
} }
@@ -899,13 +883,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private void createShareConfirmationDialog() { private void createShareConfirmationDialog() {
new AlertDialog.Builder(requireContext()) new AlertDialog.Builder(requireContext())
.setTitle(R.string.share_playlist) .setTitle(R.string.share_playlist)
.setMessage(R.string.share_playlist_with_titles_message)
.setCancelable(true) .setCancelable(true)
.setPositiveButton(R.string.share_playlist_with_titles, (dialog, which) -> .setPositiveButton(R.string.share_playlist_with_titles, (dialog, which) ->
sharePlaylist(/* shouldSharePlaylistDetails= */ true) sharePlaylist(WITH_TITLES)
)
.setNeutralButton(R.string.share_playlist_as_youtube_temporary_playlist,
(dialog, which) -> sharePlaylist(YOUTUBE_TEMP_PLAYLIST)
) )
.setNegativeButton(R.string.share_playlist_with_list, (dialog, which) -> .setNegativeButton(R.string.share_playlist_with_list, (dialog, which) ->
sharePlaylist(/* shouldSharePlaylistDetails= */ false) sharePlaylist(JUST_URLS)
) )
.show(); .show();
} }

View File

@@ -19,7 +19,6 @@ import java.util.List;
import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class LocalPlaylistManager { public class LocalPlaylistManager {
@@ -43,10 +42,13 @@ public class LocalPlaylistManager {
return Maybe.empty(); return Maybe.empty();
} }
// Save to the database directly.
// Make sure the new playlist is always on the top of bookmark.
// The index will be reassigned to non-negative number in BookmarkFragment.
return Maybe.fromCallable(() -> database.runInTransaction(() -> { return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final List<Long> streamIds = streamTable.upsertAll(streams); final List<Long> streamIds = streamTable.upsertAll(streams);
final PlaylistEntity newPlaylist = new PlaylistEntity(name, false, final PlaylistEntity newPlaylist = new PlaylistEntity(name, false,
streamIds.get(0)); streamIds.get(0), -1);
return insertJoinEntities(playlistTable.insert(newPlaylist), return insertJoinEntities(playlistTable.insert(newPlaylist),
streamIds, 0); streamIds, 0);
@@ -89,8 +91,20 @@ public class LocalPlaylistManager {
})).subscribeOn(Schedulers.io()); })).subscribeOn(Schedulers.io());
} }
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() { public Completable updatePlaylists(final List<PlaylistMetadataEntry> updateItems,
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); final List<Long> deletedItems) {
final List<PlaylistEntity> items = new ArrayList<>(updateItems.size());
for (final PlaylistMetadataEntry item : updateItems) {
items.add(new PlaylistEntity(item));
}
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
for (final Long uid : deletedItems) {
playlistTable.deletePlaylist(uid);
}
for (final PlaylistEntity item : items) {
playlistTable.upsertPlaylist(item);
}
})).subscribeOn(Schedulers.io());
} }
public Flowable<List<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) { public Flowable<List<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) {
@@ -110,13 +124,12 @@ public class LocalPlaylistManager {
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
} }
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) { public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
} }
public Single<Integer> deletePlaylist(final long playlistId) { public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId)) return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
.subscribeOn(Schedulers.io());
} }
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) { public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {

View File

@@ -0,0 +1,8 @@
package org.schabi.newpipe.local.playlist;
public enum PlayListShareMode {
JUST_URLS,
WITH_TITLES,
YOUTUBE_TEMP_PLAYLIST
}

View File

@@ -7,20 +7,27 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import java.util.List; import java.util.List;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class RemotePlaylistManager { public class RemotePlaylistManager {
private final AppDatabase database;
private final PlaylistRemoteDAO playlistRemoteTable; private final PlaylistRemoteDAO playlistRemoteTable;
public RemotePlaylistManager(final AppDatabase db) { public RemotePlaylistManager(final AppDatabase db) {
database = db;
playlistRemoteTable = db.playlistRemoteDAO(); playlistRemoteTable = db.playlistRemoteDAO();
} }
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() { public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
return playlistRemoteTable.getAll().subscribeOn(Schedulers.io()); return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io());
}
public Flowable<PlaylistRemoteEntity> getPlaylist(final long playlistId) {
return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io());
} }
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) { public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
@@ -33,6 +40,18 @@ public class RemotePlaylistManager {
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
} }
public Completable updatePlaylists(final List<PlaylistRemoteEntity> updateItems,
final List<Long> deletedItems) {
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
for (final Long uid: deletedItems) {
playlistRemoteTable.deletePlaylist(uid);
}
for (final PlaylistRemoteEntity item: updateItems) {
playlistRemoteTable.upsert(item);
}
})).subscribeOn(Schedulers.io());
}
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) { public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
return Single.fromCallable(() -> { return Single.fromCallable(() -> {
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);

View File

@@ -10,13 +10,11 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import icepick.Icepick;
import icepick.State;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class ImportConfirmationDialog extends DialogFragment { public class ImportConfirmationDialog extends DialogFragment {
@State @State
protected Intent resultServiceIntent; protected Intent resultServiceIntent;
@@ -35,7 +33,6 @@ public class ImportConfirmationDialog extends DialogFragment {
@NonNull @NonNull
@Override @Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
assureCorrectAppLanguage(getContext());
return new AlertDialog.Builder(requireContext()) return new AlertDialog.Builder(requireContext())
.setMessage(R.string.import_network_expensive_warning) .setMessage(R.string.import_network_expensive_warning)
.setCancelable(true) .setCancelable(true)
@@ -57,12 +54,12 @@ public class ImportConfirmationDialog extends DialogFragment {
throw new IllegalStateException("Result intent is null"); throw new IllegalStateException("Result intent is null");
} }
Icepick.restoreInstanceState(this, savedInstanceState); Bridge.restoreInstanceState(this, savedInstanceState);
} }
@Override @Override
public void onSaveInstanceState(@NonNull final Bundle outState) { public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Bridge.saveInstanceState(this, outState);
} }
} }

View File

@@ -13,6 +13,7 @@ import android.view.MenuItem
import android.view.SubMenu import android.view.SubMenu
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
@@ -20,11 +21,11 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.evernote.android.state.State
import com.xwray.groupie.Group import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Section import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.GroupieViewHolder import com.xwray.groupie.viewbinding.GroupieViewHolder
import icepick.State
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
@@ -460,6 +461,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
companion object { companion object {
const val JSON_MIME_TYPE = "application/json" val JSON_MIME_TYPE = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension("json") ?: "application/octet-stream"
} }
} }

View File

@@ -100,7 +100,9 @@ class SubscriptionManager(context: Context) {
val subscriptionEntity = subscriptionTable.getSubscription(info.uid) val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
subscriptionEntity.name = info.name subscriptionEntity.name = info.name
subscriptionEntity.avatarUrl = info.avatarUrl
// some services do not provide an avatar URL
info.avatarUrl?.let { subscriptionEntity.avatarUrl = it }
// these two fields are null if the feed info was fetched using the fast feed method // these two fields are null if the feed info was fetched using the fast feed method
info.description?.let { subscriptionEntity.description = it } info.description?.let { subscriptionEntity.description = it }

View File

@@ -27,6 +27,8 @@ import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.core.text.util.LinkifyCompat; import androidx.core.text.util.LinkifyCompat;
import com.evernote.android.state.State;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
@@ -44,8 +46,6 @@ import org.schabi.newpipe.util.ServiceHelper;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import icepick.State;
public class SubscriptionsImportFragment extends BaseFragment { public class SubscriptionsImportFragment extends BaseFragment {
@State @State
int currentServiceId = Constants.NO_SERVICE_ID; int currentServiceId = Constants.NO_SERVICE_ID;
@@ -89,8 +89,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
ErrorUtil.showSnackbar(activity, ErrorUtil.showSnackbar(activity,
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT, new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
ServiceHelper.getNameOfServiceById(currentServiceId),
"Service does not support importing subscriptions", "Service does not support importing subscriptions",
currentServiceId,
R.string.general_error)); R.string.general_error));
activity.finish(); activity.finish();
} }

View File

@@ -18,11 +18,11 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.evernote.android.state.State
import com.livefront.bridge.Bridge
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.OnItemClickListener import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.Section import com.xwray.groupie.Section
import icepick.Icepick
import icepick.State
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
@@ -78,7 +78,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Icepick.restoreInstanceState(this, savedInstanceState) Bridge.restoreInstanceState(this, savedInstanceState)
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext())) setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED
@@ -114,7 +114,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState() iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState() subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
Icepick.saveInstanceState(this, outState) Bridge.saveInstanceState(this, outState)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -11,10 +11,10 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.evernote.android.state.State
import com.livefront.bridge.Bridge
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.TouchCallback import com.xwray.groupie.TouchCallback
import icepick.Icepick
import icepick.State
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
@@ -23,10 +23,6 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import java.util.Collections import java.util.Collections
import kotlin.collections.ArrayList
import kotlin.collections.List
import kotlin.collections.map
import kotlin.collections.sortedBy
class FeedGroupReorderDialog : DialogFragment() { class FeedGroupReorderDialog : DialogFragment() {
private var _binding: DialogFeedGroupReorderBinding? = null private var _binding: DialogFeedGroupReorderBinding? = null
@@ -42,7 +38,7 @@ class FeedGroupReorderDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Icepick.restoreInstanceState(this, savedInstanceState) Bridge.restoreInstanceState(this, savedInstanceState)
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext())) setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
} }
@@ -80,7 +76,7 @@ class FeedGroupReorderDialog : DialogFragment() {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
Icepick.saveInstanceState(this, outState) Bridge.saveInstanceState(this, outState)
} }
private fun handleGroups(list: List<FeedGroupEntity>) { private fun handleGroups(list: List<FeedGroupEntity>) {

View File

@@ -76,7 +76,10 @@ public class SubscriptionsExportService extends BaseImportExportService {
try { try {
outFile = new StoredFileHelper(this, path, "application/json"); outFile = new StoredFileHelper(this, path, "application/json");
outputStream = new SharpOutputStream(outFile.getStream()); // truncate the file before writing to it, otherwise if the new content is smaller than
// the previous file size, the file will retain part of the previous content and be
// corrupted
outputStream = new SharpOutputStream(outFile.openAndTruncateStream());
} catch (final IOException e) { } catch (final IOException e) {
handleError(e); handleError(e);
return START_NOT_STICKY; return START_NOT_STICKY;

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.player;
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
@@ -84,7 +83,6 @@ public final class PlayQueueActivity extends AppCompatActivity
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
@@ -183,7 +181,10 @@ public final class PlayQueueActivity extends AppCompatActivity
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
private void bind() { private void bind() {
// Note: this code should not really exist, and PlayerHolder should be used instead, but
// it will be rewritten when NewPlayer will replace the current player.
final Intent bindIntent = new Intent(this, PlayerService.class); final Intent bindIntent = new Intent(this, PlayerService.class);
bindIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION);
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE); final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
if (!success) { if (!success) {
unbindService(serviceConnection); unbindService(serviceConnection);
@@ -221,7 +222,7 @@ public final class PlayQueueActivity extends AppCompatActivity
Log.d(TAG, "Player service is connected"); Log.d(TAG, "Player service is connected");
if (service instanceof PlayerService.LocalBinder) { if (service instanceof PlayerService.LocalBinder) {
player = ((PlayerService.LocalBinder) service).getPlayer(); player = ((PlayerService.LocalBinder) service).getService().getPlayer();
} }
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) { if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
@@ -569,16 +570,16 @@ public final class PlayQueueActivity extends AppCompatActivity
private void onPlayModeChanged(final int repeatMode, final boolean shuffled) { private void onPlayModeChanged(final int repeatMode, final boolean shuffled) {
switch (repeatMode) { switch (repeatMode) {
case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF: case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF:
queueControlBinding.controlRepeat queueControlBinding.controlRepeat.setImageResource(
.setImageResource(R.drawable.exo_controls_repeat_off); com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_off);
break; break;
case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE: case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE:
queueControlBinding.controlRepeat queueControlBinding.controlRepeat.setImageResource(
.setImageResource(R.drawable.exo_controls_repeat_one); com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_one);
break; break;
case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL: case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL:
queueControlBinding.controlRepeat queueControlBinding.controlRepeat.setImageResource(
.setImageResource(R.drawable.exo_controls_repeat_all); com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_all);
break; break;
} }

View File

@@ -24,12 +24,12 @@ import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJ
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP;
import static com.google.android.exoplayer2.Player.DiscontinuityReason; import static com.google.android.exoplayer2.Player.DiscontinuityReason;
import static com.google.android.exoplayer2.Player.Listener; import static com.google.android.exoplayer2.Player.Listener;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static com.google.android.exoplayer2.Player.RepeatMode; import static com.google.android.exoplayer2.Player.RepeatMode;
import static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs; import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences; import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs; import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs;
@@ -44,7 +44,6 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
@@ -55,11 +54,13 @@ import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.media.AudioManager; import android.media.AudioManager;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.math.MathUtils; import androidx.core.math.MathUtils;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@@ -71,6 +72,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.PositionInfo; import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.text.CueGroup; import com.google.android.exoplayer2.text.CueGroup;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@@ -86,8 +88,8 @@ import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.Image; import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
@@ -107,6 +109,7 @@ import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
@@ -116,11 +119,13 @@ import org.schabi.newpipe.player.ui.PlayerUiList;
import org.schabi.newpipe.player.ui.PopupPlayerUi; import org.schabi.newpipe.player.ui.PopupPlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi; import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -128,9 +133,11 @@ import java.util.stream.IntStream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.disposables.SerialDisposable; import io.reactivex.rxjava3.disposables.SerialDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class Player implements PlaybackListener, Listener { public final class Player implements PlaybackListener, Listener {
public static final boolean DEBUG = MainActivity.DEBUG; public static final boolean DEBUG = MainActivity.DEBUG;
@@ -152,15 +159,13 @@ public final class Player implements PlaybackListener, Listener {
// Intent // Intent
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static final String REPEAT_MODE = "repeat_mode";
public static final String PLAYBACK_QUALITY = "playback_quality"; public static final String PLAYBACK_QUALITY = "playback_quality";
public static final String PLAY_QUEUE_KEY = "play_queue_key"; public static final String PLAY_QUEUE_KEY = "play_queue_key";
public static final String ENQUEUE = "enqueue";
public static final String ENQUEUE_NEXT = "enqueue_next";
public static final String RESUME_PLAYBACK = "resume_playback"; public static final String RESUME_PLAYBACK = "resume_playback";
public static final String PLAY_WHEN_READY = "play_when_ready"; public static final String PLAY_WHEN_READY = "play_when_ready";
public static final String PLAYER_TYPE = "player_type"; public static final String PLAYER_TYPE = "player_type";
public static final String IS_MUTED = "is_muted"; public static final String PLAYER_INTENT_TYPE = "player_intent_type";
public static final String PLAYER_INTENT_DATA = "player_intent_data";
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Time constants // Time constants
@@ -245,6 +250,8 @@ public final class Player implements PlaybackListener, Listener {
private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
@NonNull @NonNull
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
@NonNull
private final CompositeDisposable streamItemDisposable = new CompositeDisposable();
// This is the only listener we need for thumbnail loading, since there is always at most only // This is the only listener we need for thumbnail loading, since there is always at most only
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference, // one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
@@ -269,7 +276,16 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region Constructor //region Constructor
public Player(@NonNull final PlayerService service) { /**
* @param service the service this player resides in
* @param mediaSession used to build the {@link MediaSessionPlayerUi}, lives in the service and
* could possibly be reused with multiple player instances
* @param sessionConnector used to build the {@link MediaSessionPlayerUi}, lives in the service
* and could possibly be reused with multiple player instances
*/
public Player(@NonNull final PlayerService service,
@NonNull final MediaSessionCompat mediaSession,
@NonNull final MediaSessionConnector sessionConnector) {
this.service = service; this.service = service;
context = service; context = service;
prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs = PreferenceManager.getDefaultSharedPreferences(context);
@@ -302,7 +318,7 @@ public final class Player implements PlaybackListener, Listener {
// notification ui in the UIs list, since the notification depends on the media session in // notification ui in the UIs list, since the notification depends on the media session in
// PlayerUi#initPlayer(), and UIs.call() guarantees UI order is preserved. // PlayerUi#initPlayer(), and UIs.call() guarantees UI order is preserved.
UIs = new PlayerUiList( UIs = new PlayerUiList(
new MediaSessionPlayerUi(this), new MediaSessionPlayerUi(this, mediaSession, sessionConnector),
new NotificationPlayerUi(this) new NotificationPlayerUi(this)
); );
} }
@@ -336,49 +352,134 @@ public final class Player implements PlaybackListener, Listener {
@SuppressWarnings("MethodLength") @SuppressWarnings("MethodLength")
public void handleIntent(@NonNull final Intent intent) { public void handleIntent(@NonNull final Intent intent) {
// fail fast if no play queue was provided
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY); final PlayerIntentType playerIntentType = intent.getParcelableExtra(PLAYER_INTENT_TYPE);
if (queueCache == null) { if (playerIntentType == null) {
return; return;
} }
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class); final PlayerType newPlayerType;
if (newQueue == null) { // TODO: this should be in the second switch below, but Im not sure whether I
return; // can move the initUIs stuff without breaking the setup for edge cases somehow.
switch (playerIntentType) {
case TimestampChange -> {
// TODO: this breaks out of the pattern of asking for the permission before
// sending the PlayerIntent, but Im not sure yet how to combine the permissions
// with the new enum approach. Maybe its better that the player asks anyway?
if (!PermissionHelper.isPopupEnabledElseAsk(context)) {
return;
}
newPlayerType = PlayerType.POPUP;
}
default -> {
newPlayerType = PlayerType.retrieveFromIntent(intent);
}
} }
final PlayerType oldPlayerType = playerType; playerType = newPlayerType;
playerType = PlayerType.retrieveFromIntent(intent);
initUIsForCurrentPlayerType(); initUIsForCurrentPlayerType();
// We need to setup audioOnly before super(), see "sourceOf"
isAudioOnly = audioPlayerSelected(); isAudioOnly = audioPlayerSelected();
if (intent.hasExtra(PLAYBACK_QUALITY)) { if (intent.hasExtra(PLAYBACK_QUALITY)) {
videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
} }
// Resolve enqueue intents final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
if (intent.getBooleanExtra(ENQUEUE, false) && playQueue != null) {
playQueue.append(newQueue.getStreams());
return;
// Resolve enqueue next intents switch (playerIntentType) {
} else if (intent.getBooleanExtra(ENQUEUE_NEXT, false) && playQueue != null) { case Enqueue -> {
final int currentIndex = playQueue.getIndex(); if (playQueue != null) {
playQueue.append(newQueue.getStreams()); final PlayQueue newQueue = getPlayQueueFromCache(intent);
playQueue.move(playQueue.size() - 1, currentIndex + 1); if (newQueue == null) {
return;
}
playQueue.append(newQueue.getStreams());
return;
}
// TODO: This falls through to the old logic, there was no playQueue
// yet so we should start the player and add the new video
break;
}
case EnqueueNext -> {
if (playQueue != null) {
final PlayQueue newQueue = getPlayQueueFromCache(intent);
if (newQueue == null) {
return;
}
final PlayQueueItem newItem = newQueue.getStreams().get(0);
newQueue.enqueueNext(newItem, false);
return;
}
// TODO: This falls through to the old logic, there was no playQueue
// yet so we should start the player and add the new video
break;
}
case TimestampChange -> {
final TimestampChangeData dat = intent.getParcelableExtra(PLAYER_INTENT_DATA);
assert dat != null;
final Single<StreamInfo> single =
ExtractorHelper.getStreamInfo(dat.getServiceId(), dat.getUrl(), false);
streamItemDisposable.add(single.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
final @Nullable PlayQueue oldPlayQueue = playQueue;
info.setStartPosition(dat.getSeconds());
final PlayQueueItem playQueueItem = new PlayQueueItem(info);
// If the stream is already playing,
// we can just seek to the appropriate timestamp
if (oldPlayQueue != null
&& playQueueItem.isSameItem(oldPlayQueue.getItem())) {
// Player can have state = IDLE when playback is stopped or failed
// and we should retry in this case
if (simpleExoPlayer.getPlaybackState()
== com.google.android.exoplayer2.Player.STATE_IDLE) {
simpleExoPlayer.prepare();
}
simpleExoPlayer.seekTo(oldPlayQueue.getIndex(),
dat.getSeconds() * 1000L);
simpleExoPlayer.setPlayWhenReady(playWhenReady);
} else {
final PlayQueue newPlayQueue;
// If there is no queue yet, just add our item
if (oldPlayQueue == null) {
newPlayQueue = new SinglePlayQueue(playQueueItem);
// else we add the timestamped stream behind the current video
// and start playing it.
} else {
oldPlayQueue.enqueueNext(playQueueItem, true);
oldPlayQueue.offsetIndex(1);
newPlayQueue = oldPlayQueue;
}
initPlayback(newPlayQueue, playWhenReady);
}
}, throwable -> {
// This will only show a snackbar if the passed context has a root view:
// otherwise it will resort to showing a notification, so we are safe
// here.
ErrorUtil.createNotification(context,
new ErrorInfo(throwable, UserAction.PLAY_ON_POPUP, dat.getUrl(),
null, dat.getUrl()));
}));
return;
}
case AllOthers -> {
// fallthrough; TODO: put other intent data in separate cases
}
}
final PlayQueue newQueue = getPlayQueueFromCache(intent);
if (newQueue == null) {
return; return;
} }
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this); // branching parameters for below
final float playbackSpeed = savedParameters.speed;
final float playbackPitch = savedParameters.pitch;
final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString(
R.string.playback_skip_silence_key), getPlaybackSkipSilence());
final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue); final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue);
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted());
/* /*
* TODO As seen in #7427 this does not work: * TODO As seen in #7427 this does not work:
@@ -393,7 +494,7 @@ public final class Player implements PlaybackListener, Listener {
if (!exoPlayerIsNull() if (!exoPlayerIsNull()
&& newQueue.size() == 1 && newQueue.getItem() != null && newQueue.size() == 1 && newQueue.getItem() != null
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null && playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
&& newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl()) && newQueue.getItem().isSameItem(playQueue.getItem())
&& newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { && newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
// Player can have state = IDLE when playback is stopped or failed // Player can have state = IDLE when playback is stopped or failed
// and we should retry in this case // and we should retry in this case
@@ -419,7 +520,8 @@ public final class Player implements PlaybackListener, Listener {
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false)
&& DependentPreferenceHelper.getResumePlaybackEnabled(context) && DependentPreferenceHelper.getResumePlaybackEnabled(context)
&& !samePlayQueue // !samePlayQueue
&& (playQueue == null || !playQueue.equalStreamsAndIndex(newQueue))
&& !newQueue.isEmpty() && !newQueue.isEmpty()
&& newQueue.getItem() != null && newQueue.getItem() != null
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
@@ -435,34 +537,33 @@ public final class Player implements PlaybackListener, Listener {
newQueue.setRecovery(newQueue.getIndex(), newQueue.setRecovery(newQueue.getIndex(),
state.getProgressMillis()); state.getProgressMillis());
} }
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, initPlayback(newQueue, playWhenReady);
playbackSkipSilence, playWhenReady, isMuted);
}, },
error -> { error -> {
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "Failed to start playback", error); Log.w(TAG, "Failed to start playback", error);
} }
// In case any error we can start playback without history // In case any error we can start playback without history
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, initPlayback(newQueue, playWhenReady);
playbackSkipSilence, playWhenReady, isMuted);
}, },
() -> { () -> {
// Completed but not found in history // Completed but not found in history
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, initPlayback(newQueue, playWhenReady);
playbackSkipSilence, playWhenReady, isMuted);
} }
)); ));
} else { } else {
// Good to go... // Good to go...
// In a case of equal PlayQueues we can re-init old one but only when it is disposed // In a case of equal PlayQueues we can re-init old one but only when it is disposed
initPlayback(samePlayQueue ? playQueue : newQueue, repeatMode, playbackSpeed, initPlayback(samePlayQueue ? playQueue : newQueue, playWhenReady);
playbackPitch, playbackSkipSilence, playWhenReady, isMuted);
} }
}
public void handleIntentPost(final PlayerType oldPlayerType) {
if (oldPlayerType != playerType && playQueue != null) { if (oldPlayerType != playerType && playQueue != null) {
// If playerType changes from one to another we should reload the player // If playerType changes from one to another we should reload the player
// (to disable/enable video stream or to set quality) // (to disable/enable video stream or to set quality)
setRecovery();
reloadPlayQueueManager(); reloadPlayQueueManager();
} }
@@ -470,6 +571,19 @@ public final class Player implements PlaybackListener, Listener {
NavigationHelper.sendPlayerStartedEvent(context); NavigationHelper.sendPlayerStartedEvent(context);
} }
@Nullable
private static PlayQueue getPlayQueueFromCache(@NonNull final Intent intent) {
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY);
if (queueCache == null) {
return null;
}
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class);
if (newQueue == null) {
return null;
}
return newQueue;
}
private void initUIsForCurrentPlayerType() { private void initUIsForCurrentPlayerType() {
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN) if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) { || (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
@@ -503,16 +617,13 @@ public final class Player implements PlaybackListener, Listener {
} }
private void initPlayback(@NonNull final PlayQueue queue, private void initPlayback(@NonNull final PlayQueue queue,
@RepeatMode final int repeatMode, final boolean playOnReady) {
final float playbackSpeed,
final float playbackPitch,
final boolean playbackSkipSilence,
final boolean playOnReady,
final boolean isMuted) {
destroyPlayer(); destroyPlayer();
initPlayer(playOnReady); initPlayer(playOnReady);
setRepeatMode(repeatMode); final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString(
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); R.string.playback_skip_silence_key), getPlaybackSkipSilence());
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
setPlaybackParameters(savedParameters.speed, savedParameters.pitch, playbackSkipSilence);
playQueue = queue; playQueue = queue;
playQueue.init(); playQueue.init();
@@ -520,7 +631,7 @@ public final class Player implements PlaybackListener, Listener {
UIs.call(PlayerUi::initPlayback); UIs.call(PlayerUi::initPlayback);
simpleExoPlayer.setVolume(isMuted ? 0 : 1); simpleExoPlayer.setVolume(isMuted() ? 0 : 1);
notifyQueueUpdateToListeners(); notifyQueueUpdateToListeners();
} }
@@ -602,6 +713,7 @@ public final class Player implements PlaybackListener, Listener {
databaseUpdateDisposable.clear(); databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null); progressUpdateDisposable.set(null);
streamItemDisposable.clear();
cancelLoadingCurrentThumbnail(); cancelLoadingCurrentThumbnail();
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
@@ -647,7 +759,7 @@ public final class Player implements PlaybackListener, Listener {
Log.d(TAG, "onPlaybackShutdown() called"); Log.d(TAG, "onPlaybackShutdown() called");
} }
// destroys the service, which in turn will destroy the player // destroys the service, which in turn will destroy the player
service.stopService(); service.destroyPlayerAndStopService();
} }
public void smoothStopForImmediateReusing() { public void smoothStopForImmediateReusing() {
@@ -719,7 +831,7 @@ public final class Player implements PlaybackListener, Listener {
pause(); pause();
break; break;
case ACTION_CLOSE: case ACTION_CLOSE:
service.stopService(); service.destroyPlayerAndStopService();
break; break;
case ACTION_PLAY_PAUSE: case ACTION_PLAY_PAUSE:
playPause(); playPause();
@@ -743,7 +855,6 @@ public final class Player implements PlaybackListener, Listener {
toggleShuffleModeEnabled(); toggleShuffleModeEnabled();
break; break;
case Intent.ACTION_CONFIGURATION_CHANGED: case Intent.ACTION_CONFIGURATION_CHANGED:
assureCorrectAppLanguage(service);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received"); Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
} }
@@ -756,7 +867,8 @@ public final class Player implements PlaybackListener, Listener {
private void registerBroadcastReceiver() { private void registerBroadcastReceiver() {
// Try to unregister current first // Try to unregister current first
unregisterBroadcastReceiver(); unregisterBroadcastReceiver();
context.registerReceiver(broadcastReceiver, intentFilter); ContextCompat.registerReceiver(context, broadcastReceiver, intentFilter,
ContextCompat.RECEIVER_EXPORTED);
} }
private void unregisterBroadcastReceiver() { private void unregisterBroadcastReceiver() {
@@ -1168,16 +1280,25 @@ public final class Player implements PlaybackListener, Listener {
return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode(); return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
} }
public void setRepeatMode(@RepeatMode final int repeatMode) { public void cycleNextRepeatMode() {
if (!exoPlayerIsNull()) { if (!exoPlayerIsNull()) {
@RepeatMode final int repeatMode;
switch (simpleExoPlayer.getRepeatMode()) {
case REPEAT_MODE_OFF:
repeatMode = REPEAT_MODE_ONE;
break;
case REPEAT_MODE_ONE:
repeatMode = REPEAT_MODE_ALL;
break;
case REPEAT_MODE_ALL:
default:
repeatMode = REPEAT_MODE_OFF;
break;
}
simpleExoPlayer.setRepeatMode(repeatMode); simpleExoPlayer.setRepeatMode(repeatMode);
} }
} }
public void cycleNextRepeatMode() {
setRepeatMode(nextRepeatMode(getRepeatMode()));
}
@Override @Override
public void onRepeatModeChanged(@RepeatMode final int repeatMode) { public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
if (DEBUG) { if (DEBUG) {
@@ -1280,7 +1401,8 @@ public final class Player implements PlaybackListener, Listener {
UserAction.PLAY_STREAM, UserAction.PLAY_STREAM,
"Loading failed for [" + currentMetadata.getTitle() "Loading failed for [" + currentMetadata.getTitle()
+ "]: " + currentMetadata.getStreamUrl(), + "]: " + currentMetadata.getStreamUrl(),
currentMetadata.getServiceId()); currentMetadata.getServiceId(),
currentMetadata.getStreamUrl());
ErrorUtil.createNotification(context, errorInfo); ErrorUtil.createNotification(context, errorInfo);
} }
@@ -1376,6 +1498,19 @@ public final class Player implements PlaybackListener, Listener {
public void onCues(@NonNull final CueGroup cueGroup) { public void onCues(@NonNull final CueGroup cueGroup) {
UIs.call(playerUi -> playerUi.onCues(cueGroup.cues)); UIs.call(playerUi -> playerUi.onCues(cueGroup.cues));
} }
/**
* To be called when the {@code PlaybackPreparer} set in the {@link MediaSessionConnector}
* receives an {@code onPrepare()} call. This function allows restoring the default behavior
* that would happen if there was no playback preparer set, i.e. to just call
* {@code player.prepare()}. You can find the default behavior in `onPlay()` inside the
* {@link MediaSessionConnector} file.
*/
public void onPrepare() {
if (!exoPlayerIsNull()) {
simpleExoPlayer.prepare();
}
}
//endregion //endregion
@@ -1483,7 +1618,7 @@ public final class Player implements PlaybackListener, Listener {
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM, errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
"Player error[type=" + error.getErrorCodeName() "Player error[type=" + error.getErrorCodeName()
+ "] occurred while playing " + currentMetadata.getStreamUrl(), + "] occurred while playing " + currentMetadata.getStreamUrl(),
currentMetadata.getServiceId()); currentMetadata.getServiceId(), currentMetadata.getStreamUrl());
} }
ErrorUtil.createNotification(context, errorInfo); ErrorUtil.createNotification(context, errorInfo);
} }

View File

@@ -0,0 +1,26 @@
package org.schabi.newpipe.player
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
// We model this as an enum class plus one struct for each enum value
// so we can consume it from Java properly. After converting to Kotlin,
// we could switch to a sealed enum class & a proper Kotlin `when` match.
@Parcelize
enum class PlayerIntentType : Parcelable {
Enqueue,
EnqueueNext,
TimestampChange,
AllOthers
}
/**
* A timestamp on the given was clicked and we should switch the playing stream to it.
*/
@Parcelize
data class TimestampChangeData(
val serviceId: Int,
val url: String,
val seconds: Int
) : Parcelable

View File

@@ -19,77 +19,141 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Binder; import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ServiceCompat;
import androidx.media.MediaBrowserServiceCompat;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.ktx.BundleKt;
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl;
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.player.notification.NotificationPlayerUi; import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.List;
import java.util.function.Consumer;
/** /**
* One service for all players. * One service for all players.
*/ */
public final class PlayerService extends Service { public final class PlayerService extends MediaBrowserServiceCompat {
private static final String TAG = PlayerService.class.getSimpleName(); private static final String TAG = PlayerService.class.getSimpleName();
private static final boolean DEBUG = Player.DEBUG; private static final boolean DEBUG = Player.DEBUG;
public static final String SHOULD_START_FOREGROUND_EXTRA = "should_start_foreground_extra";
public static final String BIND_PLAYER_HOLDER_ACTION = "bind_player_holder_action";
// These objects are used to cleanly separate the Service implementation (in this file) and the
// media browser and playback preparer implementations. At the moment the playback preparer is
// only used in conjunction with the media browser.
private MediaBrowserImpl mediaBrowserImpl;
private MediaBrowserPlaybackPreparer mediaBrowserPlaybackPreparer;
// these are instantiated in onCreate() as per
// https://developer.android.com/training/cars/media#browser_workflow
private MediaSessionCompat mediaSession;
private MediaSessionConnector sessionConnector;
@Nullable
private Player player; private Player player;
private final IBinder mBinder = new PlayerService.LocalBinder(this); private final IBinder mBinder = new PlayerService.LocalBinder(this);
/**
* The parameter taken by this {@link Consumer} can be null to indicate the player is being
* stopped.
*/
@Nullable
private Consumer<Player> onPlayerStartedOrStopped = null;
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
//region Service lifecycle
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onCreate() called"); Log.d(TAG, "onCreate() called");
} }
assureCorrectAppLanguage(this);
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
player = new Player(this); mediaBrowserImpl = new MediaBrowserImpl(this, this::notifyChildrenChanged);
/*
Create the player notification and start immediately the service in foreground, // see https://developer.android.com/training/cars/media#browser_workflow
otherwise if nothing is played or initializing the player and its components (especially mediaSession = new MediaSessionCompat(this, "MediaSessionPlayerServ");
loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the setSessionToken(mediaSession.getSessionToken());
service would never be put in the foreground while we said to the system we would do so sessionConnector = new MediaSessionConnector(mediaSession);
*/ sessionConnector.setMetadataDeduplicationEnabled(true);
player.UIs().get(NotificationPlayerUi.class)
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground); mediaBrowserPlaybackPreparer = new MediaBrowserPlaybackPreparer(
this,
sessionConnector::setCustomErrorMessage,
() -> sessionConnector.setCustomErrorMessage(null),
(playWhenReady) -> {
if (player != null) {
player.onPrepare();
}
}
);
sessionConnector.setPlaybackPreparer(mediaBrowserPlaybackPreparer);
// Note: you might be tempted to create the player instance and call startForeground here,
// but be aware that the Android system might start the service just to perform media
// queries. In those cases creating a player instance is a waste of resources, and calling
// startForeground means creating a useless empty notification. In case it's really needed
// the player instance can be created here, but startForeground() should definitely not be
// called here unless the service is actually starting in the foreground, to avoid the
// useless notification.
} }
@Override @Override
public int onStartCommand(final Intent intent, final int flags, final int startId) { public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent Log.d(TAG, "onStartCommand() called with: intent = [" + intent
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras())
+ "], flags = [" + flags + "], startId = [" + startId + "]"); + "], flags = [" + flags + "], startId = [" + startId + "]");
} }
/* // All internal NewPipe intents used to interact with the player, that are sent to the
Be sure that the player notification is set and the service is started in foreground, // PlayerService using startForegroundService(), will have SHOULD_START_FOREGROUND_EXTRA,
otherwise, the app may crash on Android 8+ as the service would never be put in the // to ensure startForeground() is called (otherwise Android will force-crash the app).
foreground while we said to the system we would do so if (intent.getBooleanExtra(SHOULD_START_FOREGROUND_EXTRA, false)) {
The service is always requested to be started in foreground, so always creating a final boolean playerWasNull = (player == null);
notification if there is no one already and starting the service in foreground should if (playerWasNull) {
not create any issues // make sure the player exists, in case the service was resumed
If the service is already started in foreground, requesting it to be started shouldn't player = new Player(this, mediaSession, sessionConnector);
do anything }
*/
if (player != null) { // Be sure that the player notification is set and the service is started in foreground,
// otherwise, the app may crash on Android 8+ as the service would never be put in the
// foreground while we said to the system we would do so. The service is always
// requested to be started in foreground, so always creating a notification if there is
// no one already and starting the service in foreground should not create any issues.
// If the service is already started in foreground, requesting it to be started
// shouldn't do anything.
player.UIs().get(NotificationPlayerUi.class) player.UIs().get(NotificationPlayerUi.class)
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground); .ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
if (playerWasNull && onPlayerStartedOrStopped != null) {
// notify that a new player was created (but do it after creating the foreground
// notification just to make sure we don't incur, due to slowness, in
// "Context.startForegroundService() did not then call Service.startForeground()")
onPlayerStartedOrStopped.accept(player);
}
} }
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
@@ -100,12 +164,14 @@ public final class PlayerService extends Service {
Stop the service in this case, which will be removed from the foreground and its Stop the service in this case, which will be removed from the foreground and its
notification cancelled in its destruction notification cancelled in its destruction
*/ */
stopSelf(); destroyPlayerAndStopService();
return START_NOT_STICKY; return START_NOT_STICKY;
} }
if (player != null) { if (player != null) {
final PlayerType oldPlayerType = player.getPlayerType();
player.handleIntent(intent); player.handleIntent(intent);
player.handleIntentPost(oldPlayerType);
player.UIs().get(MediaSessionPlayerUi.class) player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent)); .ifPresent(ui -> ui.handleMediaButtonIntent(intent));
} }
@@ -142,29 +208,84 @@ public final class PlayerService extends Service {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "destroy() called"); Log.d(TAG, "destroy() called");
} }
super.onDestroy();
cleanup(); cleanup();
mediaBrowserPlaybackPreparer.dispose();
mediaSession.release();
mediaBrowserImpl.dispose();
} }
private void cleanup() { private void cleanup() {
if (player != null) { if (player != null) {
if (onPlayerStartedOrStopped != null) {
// notify that the player is being destroyed
onPlayerStartedOrStopped.accept(null);
}
player.destroy(); player.destroy();
player = null; player = null;
} }
// Should already be handled by MediaSessionPlayerUi, but just to be sure.
mediaSession.setActive(false);
// Should already be handled by NotificationUtil.cancelNotificationAndStopForeground() in
// NotificationPlayerUi, but let's make sure that the foreground service is stopped.
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
} }
public void stopService() { /**
* Destroys the player and allows the player instance to be garbage collected. Sets the media
* session to inactive. Stops the foreground service and removes the player notification
* associated with it. Tries to stop the {@link PlayerService} completely, but this step will
* have no effect in case some service connection still uses the service (e.g. the Android Auto
* system accesses the media browser even when no player is running).
*/
public void destroyPlayerAndStopService() {
if (DEBUG) {
Log.d(TAG, "destroyPlayerAndStopService() called");
}
cleanup(); cleanup();
stopSelf();
// This only really stops the service if there are no other service connections (see docs):
// for example the (Android Auto) media browser binder will block stopService().
// This is why we also stopForeground() above, to make sure the notification is removed.
// If we were to call stopSelf(), then the service would be surely stopped (regardless of
// other service connections), but this would be a waste of resources since the service
// would be immediately restarted by those same connections to perform the queries.
stopService(new Intent(this, PlayerService.class));
} }
@Override @Override
protected void attachBaseContext(final Context base) { protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
} }
//endregion
//region Bind
@Override @Override
public IBinder onBind(final Intent intent) { public IBinder onBind(final Intent intent) {
return mBinder; if (DEBUG) {
Log.d(TAG, "onBind() called with: intent = [" + intent
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras()) + "]");
}
if (BIND_PLAYER_HOLDER_ACTION.equals(intent.getAction())) {
// Note that this binder might be reused multiple times while the service is alive, even
// after unbind() has been called: https://stackoverflow.com/a/8794930 .
return mBinder;
} else if (MediaBrowserServiceCompat.SERVICE_INTERFACE.equals(intent.getAction())) {
// MediaBrowserService also uses its own binder, so for actions related to the media
// browser service, pass the onBind to the superclass.
return super.onBind(intent);
} else {
// This is an unknown request, avoid returning any binder to not leak objects.
return null;
}
} }
public static class LocalBinder extends Binder { public static class LocalBinder extends Binder {
@@ -177,9 +298,51 @@ public final class PlayerService extends Service {
public PlayerService getService() { public PlayerService getService() {
return playerService.get(); return playerService.get();
} }
}
public Player getPlayer() { /**
return playerService.get().player; * @return the current active player instance. May be null, since the player service can outlive
* the player e.g. to respond to Android Auto media browser queries.
*/
@Nullable
public Player getPlayer() {
return player;
}
/**
* Sets the listener that will be called when the player is started or stopped. If a
* {@code null} listener is passed, then the current listener will be unset. The parameter taken
* by the {@link Consumer} can be null to indicate that the player is stopping.
* @param listener the listener to set or unset
*/
public void setPlayerListener(@Nullable final Consumer<Player> listener) {
this.onPlayerStartedOrStopped = listener;
if (listener != null) {
// if there is no player, then `null` will be sent here, to ensure the state is synced
listener.accept(player);
} }
} }
//endregion
//region Media browser
@Override
public BrowserRoot onGetRoot(@NonNull final String clientPackageName,
final int clientUid,
@Nullable final Bundle rootHints) {
return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints);
}
@Override
public void onLoadChildren(@NonNull final String parentId,
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
mediaBrowserImpl.onLoadChildren(parentId, result);
}
@Override
public void onSearch(@NonNull final String query,
final Bundle extras,
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
mediaBrowserImpl.onSearch(query, result);
}
//endregion
} }

View File

@@ -14,10 +14,12 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTvHtml5UserAgent;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5StreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebEmbeddedPlayerStreamingUrl;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.net.Uri; import android.net.Uri;
@@ -270,6 +272,7 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
private static final String RN_PARAMETER = "&rn="; private static final String RN_PARAMETER = "&rn=";
private static final String YOUTUBE_BASE_URL = "https://www.youtube.com"; private static final String YOUTUBE_BASE_URL = "https://www.youtube.com";
private static final byte[] POST_BODY = new byte[] {0x78, 0};
private final boolean allowCrossProtocolRedirects; private final boolean allowCrossProtocolRedirects;
private final boolean rangeParameterEnabled; private final boolean rangeParameterEnabled;
@@ -658,8 +661,11 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
} }
} }
final boolean isTvHtml5StreamingUrl = isTvHtml5StreamingUrl(requestUrl);
if (isWebStreamingUrl(requestUrl) if (isWebStreamingUrl(requestUrl)
|| isTvHtml5SimplyEmbeddedPlayerStreamingUrl(requestUrl)) { || isTvHtml5StreamingUrl
|| isWebEmbeddedPlayerStreamingUrl(requestUrl)) {
httpURLConnection.setRequestProperty(HttpHeaders.ORIGIN, YOUTUBE_BASE_URL); httpURLConnection.setRequestProperty(HttpHeaders.ORIGIN, YOUTUBE_BASE_URL);
httpURLConnection.setRequestProperty(HttpHeaders.REFERER, YOUTUBE_BASE_URL); httpURLConnection.setRequestProperty(HttpHeaders.REFERER, YOUTUBE_BASE_URL);
httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_DEST, "empty"); httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_DEST, "empty");
@@ -679,6 +685,9 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
} else if (isIosStreamingUrl) { } else if (isIosStreamingUrl) {
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
getIosUserAgent(null)); getIosUserAgent(null));
} else if (isTvHtml5StreamingUrl) {
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
getTvHtml5UserAgent());
} else { } else {
// non-mobile user agent // non-mobile user agent
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, DownloaderImpl.USER_AGENT); httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, DownloaderImpl.USER_AGENT);
@@ -687,22 +696,16 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
httpURLConnection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING, httpURLConnection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING,
allowGzip ? "gzip" : "identity"); allowGzip ? "gzip" : "identity");
httpURLConnection.setInstanceFollowRedirects(followRedirects); httpURLConnection.setInstanceFollowRedirects(followRedirects);
httpURLConnection.setDoOutput(httpBody != null); // Most clients use POST requests to fetch contents
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoOutput(true);
httpURLConnection.setFixedLengthStreamingMode(POST_BODY.length);
httpURLConnection.connect();
// Mobile clients uses POST requests to fetch contents final OutputStream os = httpURLConnection.getOutputStream();
httpURLConnection.setRequestMethod(isAndroidStreamingUrl || isIosStreamingUrl os.write(POST_BODY);
? "POST" os.close();
: DataSpec.getStringForHttpMethod(httpMethod));
if (httpBody != null) {
httpURLConnection.setFixedLengthStreamingMode(httpBody.length);
httpURLConnection.connect();
final OutputStream os = httpURLConnection.getOutputStream();
os.write(httpBody);
os.close();
} else {
httpURLConnection.connect();
}
return httpURLConnection; return httpURLConnection;
} }

View File

@@ -1,11 +1,48 @@
package org.schabi.newpipe.player.event; package org.schabi.newpipe.player.event;
import androidx.annotation.NonNull;
import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
/**
* In addition to {@link PlayerServiceEventListener}, provides callbacks for service and player
* connections and disconnections. "Connected" here means that the service (resp. the
* player) is running and is bound to {@link org.schabi.newpipe.player.helper.PlayerHolder}.
* "Disconnected" means that either the service (resp. the player) was stopped completely, or that
* {@link org.schabi.newpipe.player.helper.PlayerHolder} is not bound.
*/
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
void onServiceConnected(Player player, /**
PlayerService playerService, * The player service just connected to {@link org.schabi.newpipe.player.helper.PlayerHolder},
boolean playAfterConnect); * but the player may not be active at this moment, e.g. in case the service is running to
* respond to Android Auto media browser queries without playing anything.
* {@link #onPlayerConnected(Player, boolean)} will be called right after this function if there
* is a player.
*
* @param playerService the newly connected player service
*/
void onServiceConnected(@NonNull PlayerService playerService);
/**
* The player service is already connected and the player was just started.
*
* @param player the newly connected or started player
* @param playAfterConnect whether to open the video player in the video details fragment
*/
void onPlayerConnected(@NonNull Player player, boolean playAfterConnect);
/**
* The player got disconnected, for one of these reasons: the player is getting closed while
* leaving the service open for future media browser queries, the service is stopping
* completely, or {@link org.schabi.newpipe.player.helper.PlayerHolder} is unbinding.
*/
void onPlayerDisconnected();
/**
* The service got disconnected from {@link org.schabi.newpipe.player.helper.PlayerHolder},
* either because {@link org.schabi.newpipe.player.helper.PlayerHolder} is unbinding or because
* the service is stopping completely.
*/
void onServiceDisconnected(); void onServiceDisconnected();
} }

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.player.helper;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation; import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.Player.DEBUG; import static org.schabi.newpipe.player.Player.DEBUG;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.ThemeHelper.resolveDrawable; import static org.schabi.newpipe.util.ThemeHelper.resolveDrawable;
import android.app.Dialog; import android.app.Dialog;
@@ -24,6 +23,9 @@ import androidx.core.math.MathUtils;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding; import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
import org.schabi.newpipe.player.ui.VideoPlayerUi; import org.schabi.newpipe.player.ui.VideoPlayerUi;
@@ -37,9 +39,6 @@ import java.util.function.DoubleConsumer;
import java.util.function.DoubleFunction; import java.util.function.DoubleFunction;
import java.util.function.DoubleSupplier; import java.util.function.DoubleSupplier;
import icepick.Icepick;
import icepick.State;
public class PlaybackParameterDialog extends DialogFragment { public class PlaybackParameterDialog extends DialogFragment {
private static final String TAG = "PlaybackParameterDialog"; private static final String TAG = "PlaybackParameterDialog";
@@ -135,7 +134,7 @@ public class PlaybackParameterDialog extends DialogFragment {
@Override @Override
public void onSaveInstanceState(@NonNull final Bundle outState) { public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Bridge.saveInstanceState(this, outState);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@@ -145,8 +144,7 @@ public class PlaybackParameterDialog extends DialogFragment {
@NonNull @NonNull
@Override @Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
assureCorrectAppLanguage(getContext()); Bridge.restoreInstanceState(this, savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState);
binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater()); binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
initUI(); initUI();
@@ -342,14 +340,14 @@ public class PlaybackParameterDialog extends DialogFragment {
final Map<Boolean, TextView> pitchCtrlModeComponentMapping = final Map<Boolean, TextView> pitchCtrlModeComponentMapping =
getPitchControlModeComponentMappings(); getPitchControlModeComponentMappings();
pitchCtrlModeComponentMapping.forEach((v, textView) -> textView.setBackground( pitchCtrlModeComponentMapping.forEach((v, textView) -> textView.setBackground(
resolveDrawable(requireContext(), R.attr.selectableItemBackground))); resolveDrawable(requireContext(), android.R.attr.selectableItemBackground)));
// Mark the selected textview // Mark the selected textview
final TextView textView = pitchCtrlModeComponentMapping.get(semitones); final TextView textView = pitchCtrlModeComponentMapping.get(semitones);
if (textView != null) { if (textView != null) {
textView.setBackground(new LayerDrawable(new Drawable[]{ textView.setBackground(new LayerDrawable(new Drawable[]{
resolveDrawable(requireContext(), R.attr.dashed_border), resolveDrawable(requireContext(), R.attr.dashed_border),
resolveDrawable(requireContext(), R.attr.selectableItemBackground) resolveDrawable(requireContext(), android.R.attr.selectableItemBackground)
})); }));
} }
@@ -415,14 +413,14 @@ public class PlaybackParameterDialog extends DialogFragment {
// Bring all textviews into a normal state // Bring all textviews into a normal state
final Map<Double, TextView> stepSiteComponentMapping = getStepSizeComponentMappings(); final Map<Double, TextView> stepSiteComponentMapping = getStepSizeComponentMappings();
stepSiteComponentMapping.forEach((v, textView) -> textView.setBackground( stepSiteComponentMapping.forEach((v, textView) -> textView.setBackground(
resolveDrawable(requireContext(), R.attr.selectableItemBackground))); resolveDrawable(requireContext(), android.R.attr.selectableItemBackground)));
// Mark the selected textview // Mark the selected textview
final TextView textView = stepSiteComponentMapping.get(newStepSize); final TextView textView = stepSiteComponentMapping.get(newStepSize);
if (textView != null) { if (textView != null) {
textView.setBackground(new LayerDrawable(new Drawable[]{ textView.setBackground(new LayerDrawable(new Drawable[]{
resolveDrawable(requireContext(), R.attr.dashed_border), resolveDrawable(requireContext(), R.attr.dashed_border),
resolveDrawable(requireContext(), R.attr.selectableItemBackground) resolveDrawable(requireContext(), android.R.attr.selectableItemBackground)
})); }));
} }

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