1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-01-14 10:42:40 +00:00

Compare commits

..

517 Commits

Author SHA1 Message Date
Tobi
b58f7856a1 Merge pull request #6716 from TeamNewPipe/release_0.21.7
Hotfix release 0.21.7
2021-07-20 20:05:10 +02:00
Stypox
44a6429267 v0.21.7 (973) changelog 2021-07-20 18:41:55 +02:00
Stypox
472bde9eea Release 0.21.7 (973) 2021-07-20 18:30:28 +02:00
Stypox
f5962375f8 Call DownloadDialog dismiss() in the correct way 2021-07-20 18:25:44 +02:00
Stypox
4e33f2dcb6 Improve method order in DownloadDialog and add separator comments 2021-07-20 18:25:30 +02:00
TacoTheDank
dce874bbc7 Fix onActivityResult deprecation in MissionsFragment 2021-07-20 18:25:05 +02:00
TacoTheDank
7d69dfa62a Fix onActivityResult deprecation in DownloadDialog 2021-07-20 18:24:55 +02:00
TacoTheDank
a56f17cc3b Fix onActivityResult deprecation in DownloadSettingsFragment 2021-07-20 18:24:43 +02:00
TacoTheDank
7be7a32d70 Update AndroidX Fragment to 1.3.5 2021-07-20 18:24:33 +02:00
Stypox
a7dd3af4e5 Fix grid span count calculation; remove duplicate methods 2021-07-20 18:20:44 +02:00
Tobi
b795c5f017 Merge pull request #6576 from TeamNewPipe/release_0.21.6
Release 0.21.6
2021-07-12 15:56:21 +02:00
TobiGr
1e4686463b Release 0.21.6 (972) 2021-07-12 15:47:55 +02:00
TobiGr
4e9631a8d8 Update extractor version to 0.21.6 2021-07-12 15:47:55 +02:00
Hosted Weblate
3a83062670 Translated using Weblate (Polish)
Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Bavarian)

Currently translated at 8.7% (59 of 672 strings)

Translated using Weblate (Bavarian)

Currently translated at 1.8% (1 of 53 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (672 of 672 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Bert <robert@it-aicher.de>
Co-authored-by: Evo <weblate@verahawk.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bar/
Translation: NewPipe/Metadata
2021-07-12 15:47:11 +02:00
Hosted Weblate
fd3d46c813 Translated using Weblate (Polish)
Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Somali)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
2021-07-10 17:02:34 +02:00
Tobi
ab838fd84f Merge pull request #6639 from TeamNewPipe/fix/db_transaction
Fix crash when refreshing feed after importing database or subscriptions
2021-07-10 17:00:42 +02:00
TobiGr
9ca2691a2c Add close() method to NewPipeDatabase 2021-07-10 14:46:51 +02:00
TobiGr
7c3f5a62c5 Fix crash when refreshing feed after importing database or subscriptions
The database was not closed correctly
2021-07-10 13:09:01 +02:00
Tobi
a73a4afcad Fix APK testing section (#6623)
* Fix APK testing section

Correct the instructions to download a test APK

* Update .github/PULL_REQUEST_TEMPLATE.md

Co-authored-by: Mohammed Anas <32234660+mhmdanas@users.noreply.github.com>

Co-authored-by: Mohammed Anas <32234660+mhmdanas@users.noreply.github.com>
2021-07-09 14:47:44 +00:00
TobiGr
4ea2d8e7ba Removed translations with incorrect number of arguments for formatting 2021-07-09 10:32:10 +02:00
TobiGr
bb386fea16 Remove unused translations of "downloads_storage_ask_summary_kitkat" 2021-07-09 10:25:54 +02:00
Hosted Weblate
82cdb0fdb3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Bavarian)

Currently translated at 6.6% (45 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Added translation using Weblate (Bavarian)

Translated using Weblate (Somali)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (German)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (French)

Currently translated at 67.9% (36 of 53 strings)

Translated using Weblate (Polish)

Currently translated at 49.0% (26 of 53 strings)

Translated using Weblate (Ukrainian)

Currently translated at 52.8% (28 of 53 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 18.8% (10 of 53 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Bert <robert@it-aicher.de>
Co-authored-by: Evo <weblate@verahawk.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
Co-authored-by: Terry Louwers <t.louwers@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: translator <yasinoc375@advew.com>
Co-authored-by: webweblate <webweblate@riseup.net>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translation: NewPipe/Metadata
2021-07-09 10:14:03 +02:00
Tobi
a94dacf03c Merge pull request #6622 from TeamNewPipe/fix/RepeatModeChange
Fix crash when using repeatButtonin tablet layout
2021-07-08 17:45:22 +02:00
TobiGr
de312eb768 Fix ClassCastException
See https://github.com/TeamNewPipe/NewPipe/issues/6577#issuecomment-876095378

java.lang.ClassCastException: android.widget.ImageButton cannot be cast to androidx.appcompat.widget.AppCompatImageButton
	at org.schabi.newpipe.player.Player.onRepeatModeChanged(Player.java:2263)
	at com.google.android.exoplayer2.ExoPlayerImpl.lambda$setRepeatMode$2(ExoPlayerImpl.java:564)
	at com.google.android.exoplayer2.-$$Lambda$ExoPlayerImpl$rgrcbaqP9Y8LgzdByBnAfUO4ydU.invokeListener(lambda)
	at com.google.android.exoplayer2.BasePlayer$ListenerHolder.invoke(BasePlayer.java:279)
	at com.google.android.exoplayer2.ExoPlayerImpl.invokeAll(ExoPlayerImpl.java:1498)
	at com.google.android.exoplayer2.ExoPlayerImpl.lambda$notifyListeners$6(ExoPlayerImpl.java:1318)
	at com.google.android.exoplayer2.-$$Lambda$ExoPlayerImpl$b59raXxaB-trjwE5bgpZInm1QnU.run(lambda)
	at com.google.android.exoplayer2.ExoPlayerImpl.notifyListeners(ExoPlayerImpl.java:1328)
	at com.google.android.exoplayer2.ExoPlayerImpl.notifyListeners(ExoPlayerImpl.java:1318)
	at com.google.android.exoplayer2.ExoPlayerImpl.setRepeatMode(ExoPlayerImpl.java:564)
	at com.google.android.exoplayer2.SimpleExoPlayer.setRepeatMode(SimpleExoPlayer.java:1636)
	at org.schabi.newpipe.player.Player.setRepeatMode(Player.java:2253)
	at org.schabi.newpipe.player.Player.onRepeatClicked(Player.java:2232)
	at org.schabi.newpipe.player.Player.onBroadcastReceived(Player.java:1123)
	at org.schabi.newpipe.player.Player.access$200(Player.java:190)
	at org.schabi.newpipe.player.Player$3.onReceive(Player.java:1060)
	at android.app.LoadedApk$ReceiverDispatcher$Args.run(LoadedApk.java:1185)
	... 7 more
2021-07-08 10:34:32 +02:00
Stypox
29aa1de4e3 Merge pull request #6617 from litetex/improve-wording-in-bug-report
Fixed a typo / make the wording consistent in the bug_report.md file
2021-07-07 19:12:38 +02:00
litetex
09435a1b63 Update bug_report.md
Fixed a typo / made the wording consistent
2021-07-06 21:45:15 +02:00
opusforlife2
5ac418aa61 Merge pull request #6593 from mhmdanas/add-question-template
Add question template
2021-07-06 04:45:39 +00:00
Mohammed Anas
d8a0a74d47 Allow asking multiple questions at once 2021-07-05 13:12:54 +00:00
Tobi
3931c0d200 Merge pull request #6545 from chr56/fix_zh
[Localization]Fix Simplified Chinese Plural
2021-07-04 20:30:36 +02:00
Hosted Weblate
e26607fbd1 Translated using Weblate (Bengali)
Currently translated at 22.6% (12 of 53 strings)

Translated using Weblate (Bengali)

Currently translated at 92.4% (621 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 99.2% (667 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 99.2% (667 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 99.1% (666 of 672 strings)

Translated using Weblate (Hungarian)

Currently translated at 85.8% (577 of 672 strings)

Translated using Weblate (Serbian)

Currently translated at 20.7% (11 of 53 strings)

Translated using Weblate (Ukrainian)

Currently translated at 7.5% (4 of 53 strings)

Translated using Weblate (Galician)

Currently translated at 90.6% (609 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Esperanto)

Currently translated at 82.7% (556 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 98.3% (661 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 98.3% (661 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Polish)

Currently translated at 49.0% (26 of 53 strings)

Translated using Weblate (Lithuanian)

Currently translated at 5.6% (3 of 53 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Slovak)

Currently translated at 99.4% (668 of 672 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (670 of 672 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (French)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (German)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (English)

Currently translated at 99.8% (671 of 672 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Co-authored-by: David Braz <davidbrazps2@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: GM <muziejusinfo@gmail.com>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Josu <bi000@protonmail.com>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Web0nst <web0nst@tuta.io>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: tdayris-perso <tdayris@tutanota.de>
Co-authored-by: Ács Zoltán <acszoltan111@gmail.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2021-07-04 17:53:05 +02:00
TobiGr
a63683e6b8 Update extractor version 2021-07-04 17:46:07 +02:00
Tobi
83b198f6fe Merge pull request #6570 from Redirion/fixnpe
Fix NPE when connecting via BT
2021-07-03 11:18:02 +02:00
mhmdanas
23f6e1084b Add question template 2021-07-02 00:31:20 +03:00
TobiGr
99335bab7a Add changelog for 0.21.6 (972) 2021-06-26 17:34:09 +02:00
Hosted Weblate
33fbc889fb Translated using Weblate (Polish)
Currently translated at 48.0% (25 of 52 strings)

Translated using Weblate (Interlingua)

Currently translated at 40.9% (275 of 672 strings)

Translated using Weblate (Telugu)

Currently translated at 20.9% (141 of 672 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Serbian)

Currently translated at 19.2% (10 of 52 strings)

Translated using Weblate (Portuguese)

Currently translated at 51.9% (27 of 52 strings)

Translated using Weblate (Estonian)

Currently translated at 96.1% (646 of 672 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Esperanto)

Currently translated at 82.2% (553 of 672 strings)

Translated using Weblate (Portuguese)

Currently translated at 97.0% (652 of 672 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (French)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.8% (664 of 672 strings)

Translated using Weblate (Hebrew)

Currently translated at 48.0% (25 of 52 strings)

Translated using Weblate (Bengali)

Currently translated at 21.1% (11 of 52 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.3% (641 of 672 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (52 of 52 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (52 of 52 strings)

Translated using Weblate (German)

Currently translated at 57.6% (30 of 52 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (670 of 672 strings)

Translated using Weblate (French)

Currently translated at 99.5% (669 of 672 strings)

Translated using Weblate (German)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (52 of 52 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.4% (668 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.4% (668 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (665 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (665 of 672 strings)

Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Co-authored-by: Babul Hossain <babulssf@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Karl Tammik <karltammik@protonmail.com>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Michal L <michalrmsmi@wp.pl>
Co-authored-by: Rama Devi <nramadevini@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: Volk VDolgu <volkvdolg@gmail.com>
Co-authored-by: WaldiS <sto@tutanota.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: bruh <quangtrung02hn16@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: tdayris-perso <tdayris@tutanota.de>
Co-authored-by: whenwesober <naomi16i_1298q@cikuh.com>
Co-authored-by: zeritti <woodenmo@posteo.de>
Co-authored-by: zmni <zamani.karmana@gmail.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
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/sr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2021-06-26 15:57:58 +02:00
Robin
201e5ee09d Fix NPE when connecting via BT 2021-06-25 09:14:15 +02:00
bopol
c398308872 Merge pull request #6539 from TeamNewPipe/screenshots_for_UI_changes
Ask for screenshots/video of UI changes in PR template
2021-06-24 17:25:15 +02:00
opusforlife2
090c063644 Address Bopol's review
Changed the location, added option to delete, and also fixed a tiny grammar thingy.
2021-06-24 15:15:12 +00:00
opusforlife2
ec40c8ed1e Merge pull request #6482 from Stypox/build-instructions
Update Build instructions to make it easier for contributors to start contributing.
2021-06-24 15:02:59 +00:00
TiA4f8R
78a99526a9 Merge pull request #6557 from B0pol/invidious-instances
Update Invidious instances and support Piped links
2021-06-24 12:24:11 +02:00
Stypox
25914b0263 Third round of review 2021-06-24 10:46:53 +02:00
bopol
0da8e28651 Merge pull request #6558 from Douile/fix/searchfragment-null-service
Fix null pointer exception when displaying SearchFragment
2021-06-24 09:16:00 +02:00
bopol
d7dcfa5729 Update Extractor version 2021-06-24 09:00:15 +02:00
bopol
65824ff64d Update Invidious instances 2021-06-24 08:59:02 +02:00
bopol
63cad7ebb0 Merge pull request #6562 from TeamNewPipe/disable_instrumented_ci
Comment out test-android CI job
2021-06-24 08:55:12 +02:00
XiangRongLin
b996fa7eef Add comments explaining decisions in test-android ci job 2021-06-23 20:16:23 +02:00
XiangRongLin
5ebf3726ed Comment out test-android CI job 2021-06-23 20:14:27 +02:00
TiA4f8R
484c852efd Merge pull request #6556 from B0pol/kotlin-style
Refactoring by Android Studio
2021-06-23 18:39:53 +02:00
bopol
25cf8dc20a Refactoring by Android Studio 2021-06-23 14:30:01 +02:00
Douile
384ca66205 #6522: Fix null pointer exception when displaying SearchFragment
It seems due to #6394 updating the FragmentX library there was a
change to the order of lifecycle calls, as such onResume() was no longer
before onCreateOptionsMenu() creating a null pointer exception when
using service in onCreateOptionsMenu() as it is only set in onResume().

By moving the initialization of service to onStart() which still happens
before onCreateOptionsMenu() this crash can be avoided. This commit also
adds a check for a null service to prevent future crashes for similar
issues.
2021-06-22 16:52:02 +01:00
chr_56
46bfec66cb Localization: Fix Simplified Chinese Plural
one -> other
2021-06-22 18:52:28 +08:00
opusforlife2
a9e85abd7f Ask for screenshots/video of UI changes
If the PR author has changed the UI of the app, they should include screenshots or a video so that developers and users alike can pinpoint exactly what changed.
2021-06-20 21:15:25 +00:00
Mohammed Anas
62b4f333bb Don't enable SAF on Fire TV (#6516) 2021-06-20 19:01:06 +00:00
bopol
6c575511be Merge pull request #6528 from TacoTheDank/annotate-methods
Annotate some overridden methods and parameters as NonNull
2021-06-20 09:26:05 +02:00
TacoTheDank
79deff3261 Annotate some overridden methods and parameters as NonNull 2021-06-19 18:37:02 -04:00
Stypox
74ad488f4a Merge pull request #6394 from TacoTheDank/androidx-fragment-134
Update AndroidX Fragment to 1.3.4
2021-06-18 13:14:30 +02:00
Stypox
0db3406ad8 Fix SonarLint references and explanation 2021-06-18 12:26:50 +02:00
TobiGr
6e377dd3c5 Translated using Weblate (Serbian)
Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Kurdish)

Currently translated at 1.9% (1 of 51 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 87.8% (586 of 667 strings)

Translated using Weblate (Estonian)

Currently translated at 92.2% (615 of 667 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (666 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Punjabi)

Currently translated at 3.9% (2 of 51 strings)

Translated using Weblate (Greek)

Currently translated at 31.3% (16 of 51 strings)

Translated using Weblate (Bengali)

Currently translated at 89.6% (598 of 667 strings)

Translated using Weblate (Bengali (India))

Currently translated at 54.1% (361 of 667 strings)

Translated using Weblate (Interlingua)

Currently translated at 41.0% (274 of 667 strings)

Translated using Weblate (Punjabi)

Currently translated at 98.9% (660 of 667 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 31.3% (16 of 51 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Hebrew)

Currently translated at 47.0% (24 of 51 strings)

Translated using Weblate (Interlingua)

Currently translated at 39.8% (266 of 667 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 27.4% (14 of 51 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (666 of 667 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.4% (663 of 667 strings)

Translated using Weblate (Japanese)

Currently translated at 98.3% (656 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (662 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Santali)

Currently translated at 14.2% (95 of 667 strings)

Translated using Weblate (Arabic)

Currently translated at 66.6% (34 of 51 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Polish)

Currently translated at 99.7% (665 of 667 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (666 of 667 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (French)

Currently translated at 98.9% (660 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (667 of 667 strings)

Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Co-authored-by: C. Rüdinger <Mail-an-CR@web.de>
Co-authored-by: David Braz <davidbrazps2@gmail.com>
Co-authored-by: Digiwizkid <subhadiplayek@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Karl Tammik <karltammik@protonmail.com>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Michal L <michalrmsmi@wp.pl>
Co-authored-by: MohammedSR Vevo <mohammednajmidin@gmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Santiago <abridgement_phasiron@aleeas.com>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: SomeRetardedThatTranslatesStuff <the.eumitosis@simplelogin.fr>
Co-authored-by: THANOS SIOURDAKIS <siourdakisthanos@gmail.com>
Co-authored-by: Tari <tumbleweed@tuta.io>
Co-authored-by: TobiGr <tobigr@mail.de>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: WaldiS <sto@tutanota.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yngvar Skjaldulfsson <irrorate_browns@slmail.me>
Co-authored-by: bomzhellino <adm.bomzh@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: thami simo <simo.azad@gmail.com>
Co-authored-by: zeritti <woodenmo@posteo.de>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Обилић <mudo2233@tutanota.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/el/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ku/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translation: NewPipe/Metadata

Translated using Weblate (English)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (French)

Currently translated at 98.9% (660 of 667 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (666 of 667 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.4% (663 of 667 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Bengali)

Currently translated at 93.1% (621 of 667 strings)
2021-06-18 09:33:18 +02:00
Tobi
be676ad93c Merge pull request #3371 from mauriciocolli/feed-hide-played-items
Add ability to hide played items in a feed
2021-06-18 09:18:48 +02:00
Robin
0803d9f2b5 Merge pull request #6504 from evermind-zz/fixes-choice-dialog
dismiss choice dialog in onStop() to avoid a leaked window Exception:
2021-06-17 21:17:35 +02:00
Stypox
841fb4cfc5 Merge pull request #6495 from danielmbutler/Replace-System.exit-calls
Replace the System.exit calls with getActivity.finishAffinity()
2021-06-17 21:07:48 +02:00
Stypox
8b3e32b6eb First round of review 2021-06-17 20:47:22 +02:00
danielmbutler
90de75968d Replaced System.Exit calls with FinishAffinity and Start Activity calls
Implemented "RestartApp" method defined in NavigationHelper.java.
 This method is used in ExitActivity.java and ContentSettingsFragment.java
2021-06-17 17:18:15 +01:00
TobiGr
2de9d7b4a7 Merge branch 'master' into dev 2021-06-17 08:20:43 +02:00
evermind
a9ab2f54ea dismiss choice dialog in onStop() to avoid a leaked window Exception:
E/WindowManager: android.view.WindowLeaked: Activity org.schabi.newpipe.RouterActivity has leaked window DecorView@d99fe3b[] that was originally added here
        at android.view.ViewRootImpl.<init>(ViewRootImpl.java:418)
2021-06-17 06:53:47 +02:00
TacoTheDank
a1432e939f Fix onActivityResult deprecation in SubscriptionFragment 2021-06-15 22:09:19 -04:00
TacoTheDank
cae160b5be Fix onActivityResult deprecation in SubscriptionsImportFragment 2021-06-15 22:09:07 -04:00
TacoTheDank
aa4e5da146 Fix onActivityResult deprecation in ContentSettingsFragment 2021-06-15 22:08:55 -04:00
TacoTheDank
1061fca6a3 Add super.onRequestPermissionsResult where missing 2021-06-15 22:08:41 -04:00
TacoTheDank
e4885e3c52 Fix some older deprecations from previous Fragment versions 2021-06-15 22:08:25 -04:00
Tobi
a98c0bdec7 Merge pull request #6493 from TeamNewPipe/hotfix-v0.21.5
Hotfix release 0.21.5
2021-06-15 23:58:52 +02:00
Stypox
d6e0bd8c26 Merge pull request #6059 from Dakkaron/manual-tablet-mode-setting
Adds manual tablet mode setting
2021-06-15 21:46:19 +02:00
Dakkaron
e01ef42d31 Adds manual tablet mode setting
Co-authored-by: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
2021-06-15 21:43:06 +02:00
Stypox
92910eb227 Merge pull request #5523 from TiA4f8R/share-improvements
Recognize timestamps and hashtags in descriptions and do some sharing fixes and improvements
2021-06-15 20:56:37 +02:00
TobiGr
cdfe686322 v0.21.5 (971) changelog 2021-06-15 19:14:42 +02:00
bopol
553943ab93 Release NewPipe 0.21.5 (971) 2021-06-15 19:12:24 +02:00
Stypox
32df4d39a4 Reshow feed if grid/list view mode changed 2021-06-15 18:40:25 +02:00
litetex
1281ea858c Added itemsListHeaderDuration to large-hand player 2021-06-15 18:00:48 +02:00
Robin
30a303f873 Increase buffer for playback after rebuffer 2021-06-15 18:00:19 +02:00
Stypox
fdb6679d2d Make list stream item ConstraintLayouts and use chain 2021-06-15 14:17:40 +02:00
Stypox
7145b117cc Fix long press menu in feed 2021-06-14 19:02:57 +02:00
Stypox
4698d07323 Do not hide feed buttons (show/hide & help) behind three-dots menu 2021-06-14 19:02:57 +02:00
Stypox
2142f05a88 Fix hiding finished streams in groups; new stream state validity condition
Consider stream state valid also if >1/4 of video was watched
2021-06-14 19:01:01 +02:00
Stypox
6063ff063b CONTRIBUTING.md: add "Build instructions" section and improve ordering 2021-06-13 21:06:59 +02:00
Tobi
547a1a9970 Merge pull request #6480 from litetex/fixMissingitemsListHeaderDuration
Added itemsListHeaderDuration to large-hand player
2021-06-12 13:36:08 +02:00
litetex
8c52a812d9 Added itemsListHeaderDuration to large-hand player 2021-06-11 22:00:15 +02:00
TiA4f8R
4eef498d24 Only call addClickListenersOnTimestamps if relatedInfo is instance of StreamInfo 2021-06-11 14:08:08 +02:00
TiA4f8R
32b0bdb98c Fix the compile error 2021-06-11 12:19:29 +02:00
Stypox
edfe0f9c30 Fix disposables handling for text linkifier
also use differently Markwon methods to convert plain text to markdown
2021-06-11 12:12:12 +02:00
Stypox
eef418a757 Improve text linkifier function parameters 2021-06-11 12:12:11 +02:00
TiA4f8R
218f25c171 Annotate params and methods with NonNull 2021-06-11 12:12:10 +02:00
TiA4f8R
f02df6d80c Fix an error and add a new method in the ShareUtils class
Fix the error due to the rebase on the dev branch of this branch
Add a shareText method in the ShareUtils class which has 3 parameters and calls
the original shareText method with an empty string for the
imagePreviewUrl param.
2021-06-11 12:12:09 +02:00
TiA4f8R
da4d379b22 Initial work: use disposables for timestamps parsing in YouTube video descriptions and YouTube comments 2021-06-11 12:12:08 +02:00
TiA4f8R
f13f4cc5d2 Split handleUrl method into two methods
Split handleURL method, now private, into two methods:
handleUrlCommentsTimestamp and handleUrlDescriptionTimestamp. Code is
now more proper.
2021-06-11 12:12:07 +02:00
TiA4f8R
a79badd783 Adress requested changes and try some cleanup in handleUrl method of InternalUrlsHandler class 2021-06-11 12:12:06 +02:00
TiA4f8R
2702700d10 Don't use a chooser for other intents than opening a content in a browser or sharing a content to other apps
Use an ACTION_CHOOSER intent has a negative impact for user experience, because user cannot set as default an activity for an intent
2021-06-11 12:12:05 +02:00
TiA4f8R
267686fd37 Initial work: add support for opening hashtags in plain text descriptions
This commit adds supports for opening hashtags in plain text descriptions, using the same logic as timestamps.
Every hashtag opens a search on the current service with the text in the hashtag.
Also use a better regular expression for parsing timestamps.
2021-06-11 12:12:04 +02:00
TiA4f8R
e5df2f65b8 Move some classes to a new subpackage and adress requested changes
Rename URLHandler and KoreUtil classes to InternalUrlsHandler and KoreUtils.
Move InternalUrlsHandler, KoreUtils, TextLinkfier, ShareUtils classes to external_communication subpackage.
Remove unused param showPreviewText in shareText method of ShareUtils class.
Add initial work to be able to display an image preview of the content shared (not for downloads).
Use a better regular expression to parse timestamps in plain text descriptions.
2021-06-11 12:12:03 +02:00
TiA4f8R
d6decc05d7 Move some classes to a new subpackage and adress requested changes
Rename URLHandler and KoreUtil classes to InternalUrlsHandler and KoreUtils.
Move InternalUrlsHandler, KoreUtils, TextLinkfier, ShareUtils classes to external_communication subpackage.
Remove unused param showPreviewText in shareText method of ShareUtils class.
Add initial work to be able to display an image preview of the content shared (not for downloads).
Use a better regular expression to parse timestamps in plain text descriptions.
2021-06-11 12:12:02 +02:00
TiA4f8R
d85afd6435 Initial work to add the image of the content in the share sheet
Also do some fixes when sharing a file in downloads and some improvements in JavaDocs of ShareUtils class.
2021-06-11 12:12:00 +02:00
TiA4f8R
2fb86364ab Fix title of the subject when sharing an URL 2021-06-11 12:11:59 +02:00
TiA4f8R
c972940338 Apply suggested changes and fix some warnings 2021-06-11 12:11:58 +02:00
TiA4f8R
6abdd2a6d8 Try to change message of the system chooser for the update notification
This commit tries to change the title of the system chooser shown, which is from Android System ("Open links with"), when no defaut browser is present, for the update notification.
2021-06-11 12:11:57 +02:00
TiA4f8R
9e9d1a04e4 Fix toast shown when falling back to Google Play Store URL and the action of Open with Kodi button in the player
Add a boolean param, showToast, in ShareUtils.openIntentInApp and only show toast "No app on your device can open this" if this boolean is true.
Fix the action of play with Kodi button by applying the fix provided in #5599 (adding the flag Intent.FLAG_ACTIVITY_NEW_TASK to the intent in NavigationHelper.playWithKore method).
Do also some cleanup in viewWithFileProvider and shareFile methods of MissionAdapter class.
2021-06-11 12:11:56 +02:00
TiA4f8R
ae9349e36c Initial work: add support for opening timestamps in plain text descriptions
This commit adds support for opening plain text timestamps by parsing the description text using a regular expression, add a click listener for each timestamp which opens the popup player at the indicated time in the timestamp.
In order to do this, playOnPopup method of the URLHandler class. Also, handleUrl method of this class has been renamed to canHandleUrl.
2021-06-11 12:11:55 +02:00
TiA4f8R
4031777606 Open recognized timestamps in the description of contents in the popup player
This commit adds support of opening recognized timestamps in the popup
player instead of starting an intent which opens the YouTube website with
the video timestamp.
2021-06-11 12:11:44 +02:00
TacoTheDank
9591f14551 Update AndroidX Fragment to 1.3.4 2021-06-10 12:51:30 -04:00
Stypox
06d10cf9aa Merge pull request #6313 from Isira-Seneviratne/Update_Room_to_2.3.0
Update Room to 2.3.0.
2021-06-09 16:28:55 +02:00
Stypox
0113ad5e14 Correctly save stream progress at the end of a video 2021-06-09 15:53:51 +02:00
Stypox
e58feadba9 Remove IN HISTORY label on stream info items 2021-06-09 15:53:51 +02:00
Stypox
360f5ac6f7 Save playback state even if stream is finished and add isFinished() 2021-06-09 15:53:51 +02:00
Mauricio Colli
e846f69e38 Add ability to hide played items in a feed
- Use components from the new Groupie list library for displaying the
feed list.
2021-06-09 15:53:51 +02:00
Tobi
56cd84c1fe Merge pull request #6444 from TeamNewPipe/increasebufferafterdepletion
Increase buffer for playback after rebuffer
2021-06-08 17:02:18 +02:00
Stypox
a2eead521f Merge pull request #6346 from Imericxu/tabs-style-check
Resolve Tabs style checks
2021-06-08 14:49:14 +02:00
Stypox
a2fd5ae20c Update app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java 2021-06-08 14:45:00 +02:00
Stypox
543440e38d Update app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java 2021-06-08 14:44:54 +02:00
Stypox
0b64382ef6 Merge pull request #6436 from Abanoub8/add_url_button
Add copy url function to share button
2021-06-08 14:37:10 +02:00
Stypox
bede758507 Use url at current time for long click on player share button 2021-06-08 14:34:51 +02:00
Abanoub Sameh
5532666ad5 Long press on player share button to copy url to clipboard 2021-06-08 14:23:33 +02:00
Stypox
63cff25616 Merge pull request #6314 from Peyman-hme/Add-Open-website-button-to-webview-in-license-fragment-issue-#6296
Add "Open website" button to WebView in license fragment
2021-06-08 11:37:06 +02:00
Stypox
5e2735aaa2 Merge pull request #5462 from Isira-Seneviratne/Convert_abstract_classes_to_interfaces
Convert the abstract class DAOs to interfaces.
2021-06-08 11:32:10 +02:00
Peyman-hme
6fc0d8fce4 Add Open website string resources 2021-06-08 11:29:25 +02:00
Peyman-hme
e0c1ca1209 Change "Ok" Button to "Dismiss" and also change "Open Website" button to neutral button 2021-06-08 11:29:00 +02:00
Peyman-hme
3dc4ed1764 Add "Open website" button in webview in license fragment, issue #6296 2021-06-08 11:29:00 +02:00
Tobi
f63a4ee2ae Merge pull request #5876 from TeamNewPipe/terminated-channels
Better error handling of terminated channels
2021-06-08 10:57:24 +02:00
Tobi
c96bdfcb32 Merge pull request #5415 from Stypox/saf
Support SAF properly
2021-06-08 10:55:38 +02:00
Stypox
2a99e0e435 Reimplement storing backup import/export path
#6319 and #6402 were reverted before adding SAF changes, and have been readded at the end of SAF changes
2021-06-08 10:41:24 +02:00
Stypox
5ffc667bea Remove misplaced comment 2021-06-08 10:40:45 +02:00
Stypox
21b8df0375 Check if file really exists before asking user to overwrite it 2021-06-08 10:40:45 +02:00
Stypox
b78ac7d2e9 Fix strange behaviour when app loses access to saf download folder 2021-06-08 10:40:45 +02:00
Stypox
114dc8ffa0 Pass mime type so that SAF treats file extension correctly 2021-06-08 10:40:45 +02:00
Stypox
eea43d5a73 Add comments to SharpStreams 2021-06-08 10:40:45 +02:00
Stypox
bcb1cf6603 Improve SAF switch descriptions in settings 2021-06-08 10:40:45 +02:00
Stypox
6a0c5a874c Fix ContentSettingsManager tests 2021-06-08 10:40:45 +02:00
Stypox
1e8b3826dc Add setting migration to promote using SAF 2021-06-08 10:40:45 +02:00
wb9688
7efe62ee80 Only ask for storage permissions when not using SAF 2021-06-08 10:40:45 +02:00
wb9688
febb21a01d Fix non-SAF actions 2021-06-08 10:40:44 +02:00
wb9688
cb4e6159c4 Use file picker for export DB 2021-06-08 10:40:44 +02:00
wb9688
1164ea52f9 Move Stored(File|Directory)Helper into NewPipe 2021-06-08 10:40:44 +02:00
wb9688
0f75024e03 Support SAF properly 2021-06-08 10:40:44 +02:00
Stypox
1e09a1768e Revert all commits related to ContentSettingsFragment
Revert "Annotate methode parameters as NonNull"
This reverts commit 004907d306.

Revert "Commit path immediately when import backup"
This reverts commit 05eb0d0fbe.

Revert "Set ImportExportDataPath only on successful import"
This reverts commit f13a1b04e6.

Revert "Set ImportExportDataPath only on successful export"
This reverts commit fd4408e572.

Revert "Invert if condition in ContentSettingsFragment.setImportExportDataPath for better readability"
This reverts commit 92ab9cae27.

Revert "Move ContentSettingsFragment.isValidPath to helpers and add unit test for it."
This reverts commit fa2b11b768.

Revert "Save backup import/export location for feature import/exports"
This reverts commit 82f43ac6a6.

Remove FilePathHelperTest file
2021-06-08 10:40:44 +02:00
Hosted Weblate
7c78d963d9 Translated using Weblate (Indonesian)
Currently translated at 100.0% (51 of 51 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: bomzhellino <adm.bomzh@gmail.com>
Co-authored-by: whenwesober <naomi16i_1298q@cikuh.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translation: NewPipe/Metadata
2021-06-08 10:33:15 +02:00
TobiGr
b57ecae565 Fix getting error cause
Fix error dialog not shown when getting elemets from first subscription failed.
2021-06-08 10:12:36 +02:00
Stypox
89317d4abc Fix feed loading and show a dialog for each invalid subscription 2021-06-08 10:12:36 +02:00
TobiGr
c5dd3dc7a9 Improve error message when loading feed
Add name of unavailable channel
2021-06-08 10:12:36 +02:00
TobiGr
ccc46971b4 Show detailed error message when an account has been terminated by the service 2021-06-08 10:12:36 +02:00
TobiGr
6ad4b425e4 Better error handling of terminated channels when loading feed 2021-06-08 10:12:36 +02:00
Tobi
761e01c3b9 Merge pull request #6443 from Stypox/invalid-attr
Use constraint layout for play queue item
2021-06-08 10:05:38 +02:00
Tobi
70b9330b61 Merge pull request #6432 from TeamNewPipe/release_0.21.4
Release 0.21.4
2021-06-07 22:41:30 +02:00
TobiGr
f1e8667945 NewPipe 0.21.4 (970) 2021-06-07 21:40:02 +02:00
Tobi
509f501696 Merge pull request #6438 from TeamNewPipe/fix/metadataView
Increase width of metadata text view
2021-06-07 21:39:24 +02:00
TobiGr
3fe0368486 Update NewPIpe extractor to 0.21.4 2021-06-07 20:45:26 +02:00
Hosted Weblate
8f027e274e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (51 of 51 strings)

Translated using Weblate (German)

Currently translated at 56.8% (29 of 51 strings)

Translated using Weblate (Polish)

Currently translated at 99.6% (655 of 657 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (French)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (German)

Currently translated at 56.8% (29 of 51 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (51 of 51 strings)

Translated using Weblate (Interlingua)

Currently translated at 40.1% (264 of 657 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (French)

Currently translated at 99.8% (656 of 657 strings)

Translated using Weblate (German)

Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Michal L <michalrmsmi@wp.pl>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: TiA4f8R <avdivers84@gmail.com>
Co-authored-by: TobiGr <tobigr@mail.de>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2021-06-07 20:43:54 +02:00
Robin
3b0045917c Increase buffer for playback after rebuffer 2021-06-07 09:25:49 +02:00
Stypox
a102fc9cad Use constraint layout for play queue item
Also remove invalid ic_selected attribute
2021-06-07 08:07:27 +02:00
Stypox
f6bca68da2 Merge pull request #6442 from Douile/fix/useless-duration
Hide queue duration when displaying segment panel
2021-06-07 07:11:13 +02:00
Douile
d921e2e61b Hide queue duration when displaying segment panel
Fixes #6441
2021-06-07 02:10:44 +01:00
TobiGr
0f7ed0ec70 Kotlin auto formatting 2021-06-06 11:57:14 +02:00
TobiGr
49b12ea4f8 Increase space for metadata typeview
Some translations come with longer words causing them to wrap at strange positions
2021-06-06 11:57:14 +02:00
Tobi
69fc466323 Update 970.txt
Fixed typo
2021-06-05 23:37:47 +02:00
Tobi
81d00f2e97 Run CI on PRs which target the master branch
This is a fix for the release PRs
2021-06-05 17:34:27 +02:00
TobiGr
ded6540422 Update extractor version 2021-06-05 17:26:16 +02:00
TobiGr
583a028529 Add changelog for 0.21.4 2021-06-05 17:23:57 +02:00
Tobi
f1bb56e2fb Merge pull request #6430 from TeamNewPipe/localization
Localization updates and small code improvements
2021-06-05 17:20:37 +02:00
TobiGr
f583dd47ac Annotate StoredFileHelper.createSAF() as NonNull 2021-06-05 16:34:22 +02:00
TobiGr
7e3b3453c0 Suppress a warning 2021-06-05 16:13:12 +02:00
TobiGr
abc354f516 Use reqireContext(), requireActivity() instead of getters 2021-06-05 16:13:12 +02:00
TobiGr
79efffe12f Replace three dots with ellipsis symbol 2021-06-05 16:13:12 +02:00
Hosted Weblate
25130db371 Translated using Weblate (German)
Currently translated at 56.0% (28 of 50 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (German)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Interlingua)

Currently translated at 40.0% (263 of 657 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (656 of 657 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Russian)

Currently translated at 98.9% (650 of 657 strings)

Translated using Weblate (French)

Currently translated at 99.8% (656 of 657 strings)

Translated using Weblate (German)

Currently translated at 98.9% (650 of 657 strings)

Translated using Weblate (English)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Interlingua)

Currently translated at 34.6% (222 of 641 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Serbian)

Currently translated at 16.0% (8 of 50 strings)

Translated using Weblate (Odia)

Currently translated at 4.2% (27 of 641 strings)

Translated using Weblate (Odia)

Currently translated at 2.0% (1 of 50 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 99.3% (637 of 641 strings)

Translated using Weblate (Swedish)

Currently translated at 87.3% (560 of 641 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (French)

Currently translated at 70.0% (35 of 50 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Ukrainian)

Currently translated at 8.0% (4 of 50 strings)

Translated using Weblate (Lithuanian)

Currently translated at 4.0% (2 of 50 strings)

Translated using Weblate (Bengali)

Currently translated at 93.7% (601 of 641 strings)

Translated using Weblate (Interlingua)

Currently translated at 34.3% (220 of 641 strings)

Translated using Weblate (Ukrainian)

Currently translated at 8.0% (4 of 50 strings)

Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Co-authored-by: David Braz <davidbrazps2@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hritik R <hpujhari7@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jacque Fresco <aidter@use.startmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Juraj Timko <timko@netrix.sk>
Co-authored-by: Kristoffer Grundström <swedishsailfishosuser@tutanota.com>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: Louis Morel <louismorel03@outlook.fr>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: S3aBreeze <paperwork@evilcorp.ltd>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: Terry Louwers <t.louwers@gmail.com>
Co-authored-by: TobiGr <tobigr@mail.de>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Zhiheng Xu <xeric.2002@gmail.com>
Co-authored-by: bomzhellino <adm.bomzh@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: gymka <gymka@archlinux.lt>
Co-authored-by: jul-debug <jul333666@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: whenwesober <naomi16i_1298q@cikuh.com>
Co-authored-by: Обилић <mudo2233@tutanota.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/or/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2021-06-05 16:08:27 +02:00
Tobi
932eb94f9d Merge pull request #6429 from TiA4f8R/fix-play-with-kodi-player-button
Fix Play with Kodi button in Player always prompts to install Kore
2021-06-05 14:32:09 +02:00
Stypox
9bf4eff173 Merge pull request #6421 from TeamNewPipe/fix/playback_speed
Fix playback speed not being updated in PlayQueueAcitvity
2021-06-05 13:46:45 +02:00
TiA4f8R
9fc3ddeab7 Fix Play with Kodi button in Player always prompts to install Kore 2021-06-05 12:48:34 +02:00
TobiGr
98fdbec442 Fix playback speed not being updated in PlayQueuAcitvity
Fixes #6419
2021-06-05 12:24:59 +02:00
bopol
332b90d6c1 Merge pull request #6337 from litetex/fix-broken-yt-liked-comments
[YouTube] Fix broken likes in comments
2021-06-04 22:32:48 +02:00
Tobi
db2e03eb14 Merge pull request #6402 from XiangRongLin/save_backup_bug
Fix and improve setting import and export paths
2021-06-04 22:22:09 +02:00
TobiGr
8ed8b94ec7 Update extractor version to ff11c2df2a22cc10d9fd7e59538d10ca7ccbaffb 2021-06-04 16:20:20 +02:00
Tobi
63c9308f59 Merge pull request #5946 from Stypox/metadata
Show content metadata below the description
2021-06-03 20:51:33 +02:00
litetex
1306a777fc Using Localization.shortCount() and old likeCount 2021-06-03 14:40:00 +02:00
Tobi
f739ed7581 Merge pull request #6280 from TiA4f8R/open-in-browser-long-press-menu
Add Open in browser option to long-press menu
2021-06-03 13:45:25 +02:00
TiA4f8R
b4d6015464 Add the Open in browser option when long-pressing a stream for local and online playlists and for history 2021-06-03 13:21:31 +02:00
TiA4f8R
b9aaafdb30 Add Open in browser option to long-press menu
Add Open in browser option to long-press menu when long pressing a stream and a subscription
2021-06-03 13:15:08 +02:00
Tobi
71aa6c6e92 Merge pull request #6309 from mhmdanas/fix-channel-details-long-press-menu-on-feeds
Add "Show channel details" option in remote (non-local) playlists
2021-06-03 12:25:50 +02:00
Tobi
f98d2631e5 Merge pull request #6242 from evermind-zz/fixes-for-upstream
fix Rotation crash on „Video not available“ page (#5941)
2021-06-03 12:22:23 +02:00
Stypox
9e94c81ef2 Always show scrollbars for metadata tags 2021-06-02 21:23:48 +02:00
Stypox
d025ef11f8 Sort tags in metadata section 2021-06-02 21:23:42 +02:00
TiA4f8R
fe7536e374 Change NewPipe IRC links in the contribution guidelines (#6415)
* Change NewPipe IRC links in the contribution guidelines

* Remove redundant line break

Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>

Co-authored-by: Mohammed Anas <32234660+mhmdanas@users.noreply.github.com>
Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
2021-06-02 16:27:08 +00:00
Stypox
14256137e8 Use chips to show tags in metadata section
Clicking on chips opens the search fragment
Long clicking copies to clipboard
2021-06-02 16:15:02 +02:00
Stypox
bc3e43ac58 Add button to allow selecting text in the description
Since now selection is disabled by default, this fixes #5453
2021-06-02 14:36:34 +02:00
Stypox
d0d5373be9 Allow copying metadata to clipboard 2021-06-02 14:19:25 +02:00
Stypox
997267bad1 Show video metadata below the decription 2021-06-02 14:19:25 +02:00
Robin
ef6d0cc4b1 Merge pull request #6414 from mhmdanas/upgrade-prettytime-5.0.1
Upgrade PrettyTime to 5.0.1
2021-06-02 13:47:42 +02:00
mhmdanas
ffad244e1e Upgrade PrettyTime to 5.0.1 2021-06-02 01:14:43 +03:00
Tobi
fdee7c3d06 Merge pull request #6410 from sauravrao637/6409
Fixed channel description textView
2021-06-01 11:02:47 +02:00
camo0112
142cde975f fixed channel description textView 2021-06-01 10:43:57 +05:30
XiangRongLin
004907d306 Annotate methode parameters as NonNull 2021-05-31 15:09:57 +02:00
XiangRongLin
05eb0d0fbe Commit path immediately when import backup 2021-05-31 12:40:14 +02:00
XiangRongLin
f13a1b04e6 Set ImportExportDataPath only on successful import
Also set the folder instead of the file itself as path
2021-05-31 12:38:21 +02:00
XiangRongLin
fd4408e572 Set ImportExportDataPath only on successful export 2021-05-31 12:36:21 +02:00
TiA4f8R
a84ab7413c Change IRC chat button link in READMEs (#6393)
* Change the IRC chat button link in the original README

* Change the IRC chat button link in the Spanish README

* Change the IRC chat button link in the Japanese README

* Change the IRC chat button link in the Korean README

* Change the IRC chat button link in the Brazilian Portuguese README

* Change the IRC chat button link in the Romanian README

* Change the IRC chat button link in the Somali README

* Change the IRC chat button link in the Turkish README
2021-05-29 14:18:15 +02:00
Tobi
62b593da08 Merge pull request #6118 from sauravrao637/errorHandlingInSubcribe
Error handling in subscribe() in DownloadDialog
2021-05-29 11:30:36 +02:00
Tobi
0eb69b6659 Merge pull request #6353 from Imericxu/fix-pasting-rich-text-in-search
Fix unwanted rich text formatting when pasting
2021-05-29 09:12:07 +02:00
Robin
67b83388b1 Merge pull request #6387 from TeamNewPipe/empty_fragment_design
Fix empty fragment text not centered when text longer than one line
2021-05-28 17:48:38 +02:00
Tobi
ecc998aea8 Merge pull request #6373 from TeamNewPipe/bandcamp-links
Improve Bandcamp intent filters
2021-05-28 15:17:00 +02:00
TobiGr
6956d16f0e Update dependency for core library desugaring
Hopefully fixes reproducible builds.
2021-05-28 15:07:30 +02:00
TobiGr
f1bc4f5c20 Update junit from 4.13.1 to 4.13.2
https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.13.2.md
2021-05-28 15:04:39 +02:00
Tobi
f134e2d02a Merge pull request #6385 from sauravrao637/6371
Option for download is redundant and thus removed when linkType is CHANNEL or PLAYLIST
2021-05-28 14:23:04 +02:00
TobiGr
6ec72ef945 Fix empty fragment text not centered when text longer than one line 2021-05-28 12:43:21 +02:00
Tobi
e8d518cd6c Add Libera webchat link 2021-05-28 11:59:27 +02:00
camo0112
b564433ff6 Option for download is redundant and thus removed when linkType is CHANNEL or PLAYLIST 2021-05-28 03:38:53 +05:30
TiA4f8R
79f7dcd1a3 Merge pull request #6384 from TeamNewPipe/change-matrix-irc-link
Update Matrix link of NewPipe IRC from Freenode to Libera.chat
2021-05-27 23:09:19 +02:00
TiA4f8R
23ee9b7867 Update Matrix link of NewPipe IRC from Freenode to Libera.chat 2021-05-27 22:07:45 +02:00
TobiGr
afbf36900f Merge branch 'master' into dev 2021-05-27 20:04:16 +02:00
Tobi
2829851e49 Merge pull request #6358 from TeamNewPipe/release_0.21.3
Release 0.21.3
2021-05-27 16:36:43 +02:00
TobiGr
2b8fda3511 Release NewPipe 0.21.3 (969) 2021-05-27 16:36:00 +02:00
TobiGr
d31959990e Update extractor version to 0.21.3 2021-05-27 16:36:00 +02:00
Tobi
26c535db84 Change IRC link
Move from freenode webchat to libera chat
2021-05-27 09:56:27 +02:00
Zhiheng Xu
ea1b910d7e Simplify code 2021-05-26 12:01:58 -04:00
Zhiheng Xu
8f4c6fb6ac Add comment 2021-05-26 12:01:58 -04:00
Zhiheng Xu
9b1861417c Add formatting removal on paste for search
Closes #5912
2021-05-26 12:01:58 -04:00
Eric Xu
448989f32f Add PlaylistTab.hashCode() matching equals 2021-05-26 12:01:30 -04:00
Eric Xu
2fc26bc154 Refactor PlaylistTab.equals 2021-05-26 12:01:30 -04:00
Eric Xu
1812249d37 Add ChannelTab.hashCode() matching equals 2021-05-26 12:01:30 -04:00
Eric Xu
14bbaccb9f Refactor ChannelTab.equals 2021-05-26 12:01:30 -04:00
Eric Xu
d2b03afcf4 Add KioskTab.hashCode() matching equals 2021-05-26 12:01:30 -04:00
Eric Xu
1cac3895dc Refactor KioskTab.equals 2021-05-26 12:01:30 -04:00
Eric Xu
01aab25889 Add Tab.hashCode() to go with equals 2021-05-26 12:01:30 -04:00
Eric Xu
96d731dfc7 Refactor equals method 2021-05-26 12:01:30 -04:00
XiangRongLin
8080c32b1f Merge pull request #6345 from Imericxu/test-and-update-playqueue
Test and clean up PlayQueue
2021-05-26 17:46:24 +02:00
XiangRongLin
4b27aec196 Merge pull request #6360 from litetex/used-adoptopenjdk-instead-of-zulu
Use AdoptOpenJDK instead of Zulu in GitHub actions
2021-05-26 17:42:04 +02:00
Fynn Godau
38fb510375 [Bandcamp] Increase accuracy of intent filters 2021-05-26 15:11:38 +02:00
Fynn Godau
6422e31b10 [Bandcamp] Handles links without /
Co-authored-by: yashpalgoyal1304 <yashpalgoyal1304@gmail.com>
2021-05-26 15:08:43 +02:00
Zhiheng Xu
c0f47195a2 Remove Enclosed.class runner
Does not affect Gradle tests and only benefits IDE workflow
2021-05-24 13:03:52 -04:00
Zhiheng Xu
40f66977c7 Rewrite addToHistory test without using reflection 2021-05-24 12:20:19 -04:00
Zhiheng Xu
e518c0dc14 Rename mockPlayQueue(…) to makePlayQueue(…) 2021-05-24 12:20:19 -04:00
Zhiheng Xu
2e161a1f45 Change shuffle() guard to check for size <= 2
After testing the app, I realized that shuffling a queue with size 2
does nothing
2021-05-24 12:20:19 -04:00
Zhiheng Xu
5ab6e84044 Remove redundant clearing of list 2021-05-24 12:20:19 -04:00
Zhiheng Xu
e1a6347c4e Refactor shuffle and update documentation
- Add early return for invalid sizes to shuffle

 - Rename variables to be more descriptive

 - Refactor moving list element, removing unnecessary operations

 - Unwrap if clause for adding to history because the condition is
   guaranteed by the guard clause

 - Inline the value 0 for the ReorderEvent

 - Update documentation to reflect new changes
2021-05-24 12:20:19 -04:00
Zhiheng Xu
bf8e8798d9 Add test for setIndex 2021-05-24 12:20:19 -04:00
Zhiheng Xu
08949ee347 Refactor static methods to package private
Stops Android Studio from "recognizing" them as tests
2021-05-24 12:20:19 -04:00
Eric Xu
92a67bb8cb Rearrange fields
Final fields should be arranged first
2021-05-24 12:20:19 -04:00
Eric Xu
363bbf5fd3 Test getItem(int) 2021-05-24 12:20:19 -04:00
Eric Xu
77f6940336 Refactor making a PlayQueueItem to static method 2021-05-24 12:20:19 -04:00
Eric Xu
e8eeac6735 Resolve TODO in indexOf(...)
PlayQueueItem overrides equals and hashCode, so using indexOf is
perfectly fine.
2021-05-24 12:20:19 -04:00
Eric Xu
775fbc9a75 Rewrite setIndex(int) to pass unit tests
Original did not cover the case of when streams is empty and
documentation does not specify any input restrictions.

There's an ambiguity with broadcasting an event between the
documentation and the actual code (see TODO).
2021-05-24 12:20:19 -04:00
Eric Xu
8d0f2d371d Test PlayQueue.setIndex(...) 2021-05-24 12:20:19 -04:00
Eric Xu
8efe2859b8 Refactor assignments to field declaration
Assignments that don't require the constructor can be moved out.
2021-05-24 12:20:19 -04:00
Eric Xu
441c68ead2 Add hashCode() to match equals(other) 2021-05-24 12:20:19 -04:00
Eric Xu
882b235a78 Test PlayQueue equals 2021-05-24 12:20:19 -04:00
Eric Xu
4cd1f201f5 Refactor streams to initialize with values 2021-05-24 12:20:19 -04:00
Eric Xu
013c59f904 Refactor ArrayList fields to List 2021-05-24 12:20:19 -04:00
Eric Xu
57474e2dab Refactor and optimize equals
- Remove multiple casts of obj

 - Simply use object equals on the streams because PlayQueueItem’s
   equals already compares urls
2021-05-24 12:20:19 -04:00
TobiGr
139ced885d Update extractor version 2021-05-23 22:56:44 +02:00
litetex
10b1da135e Update ci.yml 2021-05-23 14:54:49 +02:00
Tobi
f0bb2e8687 Merge pull request #6307 from TeamNewPipe/peertube_hls
Add support for PeerTube HLS streams
2021-05-23 12:07:31 +02:00
TobiGr
4643ccef6f Fix incorrcetly formatted translations 2021-05-23 12:06:08 +02:00
TobiGr
753ca7cb53 Update translations
Translated using Weblate (French)
Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Arabic)

Currently translated at 99.6% (639 of 641 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 97.5% (625 of 641 strings)

Translated using Weblate (Sardinian)

Currently translated at 99.6% (639 of 641 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 13.5% (87 of 641 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Dutch)

Currently translated at 99.6% (639 of 641 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Estonian)

Currently translated at 95.9% (615 of 641 strings)

Translated using Weblate (Bengali)

Currently translated at 93.7% (601 of 641 strings)

Translated using Weblate (French)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Lithuanian)

Currently translated at 69.5% (446 of 641 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (639 of 641 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (641 of 641 strings)
2021-05-23 11:55:34 +02:00
TobiGr
87d2f33e55 Add support for PeerTube HLS streams 2021-05-23 11:53:35 +02:00
XiangRongLin
fc7944d287 Merge pull request #6319 from ATofighi/feat-6039-store-backup-location
Save backup import/export location for future import/exports
2021-05-21 21:08:33 +02:00
XiangRongLin
376e5c1546 Remove unnecessary conversion between file and path 2021-05-21 20:24:11 +02:00
Alireza Tofighi
e8ad947d37 Split up FilePathHelperTest tests in simpler methods 2021-05-21 22:44:38 +04:30
Alireza Tofighi
067528211f Add more tests for FilePathUtils.isValidDirectoryPath for better coverage 2021-05-21 20:28:42 +04:30
Alireza Tofighi
92ab9cae27 Invert if condition in ContentSettingsFragment.setImportExportDataPath for better readability 2021-05-21 20:24:57 +04:30
Alireza Tofighi
fa2b11b768 Move ContentSettingsFragment.isValidPath to helpers and add unit test for it. 2021-05-21 20:21:58 +04:30
Alireza Tofighi
82f43ac6a6 Save backup import/export location for feature import/exports 2021-05-17 21:57:27 +04:30
David BrazSan
c7660b8c2d Excluding forgotten parts of the original raw (#6317) 2021-05-17 14:59:48 +00:00
XiangRongLin
847831c195 Merge pull request #6308 from mhmdanas/make-minor-changes
Make some minor changes
2021-05-17 12:14:00 +02:00
XiangRongLin
e0b246431f Merge pull request #6312 from ATofighi/fix-6298-report-page-back-not-working
Fix ErrorActivity actionbar back button not working
2021-05-17 12:12:12 +02:00
Isira Seneviratne
3b1c4b043d Convert Converters to a Kotlin object. 2021-05-16 11:14:15 +05:30
Isira Seneviratne
e8b8391868 Rename .java to .kt 2021-05-16 11:14:15 +05:30
Isira Seneviratne
cd0a87785e Update Room to 2.3.0. 2021-05-16 11:04:52 +05:30
Alireza Tofighi
c808beec30 Fix ErrorActivity actionbar back button not working
The issue is because of R.id.home != android.R.id.home
2021-05-16 02:19:52 +04:30
yashpalgoyal1304
2d4a3c2554 Generalised/tense-corrected the example (#6302)
Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>

Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
2021-05-15 17:34:24 +00:00
mhmdanas
b2b9938484 Put show channel details option in more long-press menus 2021-05-15 19:20:27 +03:00
mhmdanas
eb1cefe2fa Make some minor changes 2021-05-15 18:48:16 +03:00
Tobi
5eb5dbddde Merge pull request #6294 from Redirion/exodowngrade
Downgrade ExoPlayer to 2.12
2021-05-15 10:58:41 +02:00
TobiGr
bfe3eff5ff Fix warnings for translations 2021-05-15 10:04:27 +02:00
TobiGr
e7936e6c9a Fix warnings for translations 2021-05-15 09:57:24 +02:00
TobiGr
514f92e6f2 Update translations
Translated using Weblate (English)
Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Serbian)

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (636 of 639 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Slovenian)

Currently translated at 82.7% (529 of 639 strings)

Translated using Weblate (Slovenian)

Currently translated at 82.7% (529 of 639 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Estonian)

Currently translated at 94.5% (604 of 639 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Hebrew)

Currently translated at 46.0% (23 of 50 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/

Translated using Weblate (Serbian)

Currently translated at 16.0% (8 of 50 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (49 of 50 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/

Translated using Weblate (Estonian)

Currently translated at 10.0% (5 of 50 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/et/

Translated using Weblate (German)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 99.6% (639 of 641 strings)

Translated using Weblate (Ukrainian)

Currently translated at 8.0% (4 of 50 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
2021-05-15 09:41:15 +02:00
iamthesenate1
68fd7a031f Add No Login Required translation 2021-05-14 22:34:13 +02:00
Robin
95f61542b5 TEST: Downgrade ExoPlayer to 2.12 2021-05-14 14:47:53 +02:00
Tobi
9fc6f19702 Merge pull request #6288 from Redirion/debugtunnellingoff
Add debug option to disable media tunneling
2021-05-14 13:04:35 +02:00
Robin
4038617d59 removed _text_ from show_original_time_ago and disable_media_tunneling 2021-05-14 12:25:07 +02:00
Robin
98ccd577d6 Fixed checkstyle 2021-05-14 12:15:48 +02:00
Robin
1d43a2362c Add debug option to disable media tunneling 2021-05-14 12:05:52 +02:00
TobiGr
0ff675171b Update extractor version 2021-05-14 09:23:50 +02:00
TobiGr
59594c6637 Add changelog for 0.21.3 2021-05-14 00:03:07 +02:00
Tobi
9595733563 Merge pull request #6274 from mhmdanas/remove-extra-newline-in-translation
Remove extra newlines in strings
2021-05-12 22:19:35 +02:00
mhmdanas
5eb1d49857 Remove extra newlines in strings 2021-05-12 22:16:12 +03:00
TobiGr
fa1fdbf73e Fix translations
Fix outdated slovenian translation
2021-05-12 16:27:51 +02:00
TobiGr
52e52b3ca1 Update translations
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 62.1% (397 of 639 strings)

Translated using Weblate (Bengali)

Currently translated at 90.6% (579 of 639 strings)

Translated using Weblate (Italian)

Currently translated at 48.9% (24 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (49 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/

Translated using Weblate (Tamazight (Central Atlas))

Currently translated at 32.8% (210 of 639 strings)

Added translation using Weblate (Marathi)

Added translation using Weblate (Marathi)

Translated using Weblate (Spanish)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Hindi)

Currently translated at 85.4% (546 of 639 strings)

Translated using Weblate (Marathi)

Currently translated at 10.9% (70 of 639 strings)

Translated using Weblate (Bengali)

Currently translated at 90.9% (581 of 639 strings)

Translated using Weblate (Bengali)

Currently translated at 20.4% (10 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/

Translated using Weblate (English)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Hungarian)

Currently translated at 87.6% (560 of 639 strings)

Translated using Weblate (Serbian)

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Estonian)

Currently translated at 85.4% (546 of 639 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Bengali)

Currently translated at 91.8% (587 of 639 strings)

Translated using Weblate (Turkish)

Currently translated at 30.6% (15 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/

Translated using Weblate (Romanian)

Currently translated at 8.1% (4 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ro/

Translated using Weblate (Portuguese)

Currently translated at 53.0% (26 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/

Translated using Weblate (Spanish)

Currently translated at 26.5% (13 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/

Translated using Weblate (Serbian)

Currently translated at 14.2% (7 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/

Translated using Weblate (Estonian)

Currently translated at 8.1% (4 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/et/

Translated using Weblate (Estonian)

Currently translated at 8.1% (4 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/et/

Translated using Weblate (Marathi)

Currently translated at 11.8% (76 of 639 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Hungarian)

Currently translated at 91.7% (586 of 639 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Estonian)

Currently translated at 89.2% (570 of 639 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Estonian)

Currently translated at 90.7% (580 of 639 strings)
2021-05-12 16:27:43 +02:00
Saurav Rao
5b4fbe32b1 Fix trying to delete object not in list (#6127)
* fix trying to delete object by index -1

* correction in checkstyle-supressions.xml

Co-authored-by: camo0112 <56369484+camo0112@users.noreply.github.com>
2021-05-12 14:33:00 +02:00
litetex
31ea44ccf1 Fixed player not automatically playing (#6266)
* Fixed player not automatically playing

Should also fix https://github.com/TeamNewPipe/NewPipe/issues/6179

* Added comment
2021-05-12 10:20:39 +02:00
Tobi
d26ca194b3 Merge pull request #5671 from ix5/download-deleter-snackbars
Downloader: Deleter: Dismiss previous Snackbars
2021-05-05 10:46:46 +02:00
evermind
a012e26d63 fix Rotation crash on „Video not available“ page (#5941)
The EmptyFragment should not have a constructor at all.
Now a static methods creates the Fragment and arguments
are handled via a Bundle.
2021-05-05 08:55:54 +02:00
Saurav Rao
f80b1fb2fe added NoLoginRequired (#5994)
Co-authored-by: camo0112 <56369484+camo0112@users.noreply.github.com>
2021-05-04 17:57:19 +00:00
ix5
38ed07caa7 Downloader: Deleter: Dismiss previous Snackbars
Bug:
In Downloader view, while deleting items, Snackbars at the
bottom of the UI keep queuing up. You need to wait for all
of them to dismiss themselves for files to actually be
deleted. If you close NewPipe before all snackbars are
dismissed, your files will not be deleted and show up again
next time you start NewPipe.

Fix:
When running append(), trigger the commit() action
immediately and cancel all delayed callbacks for commit().

This prevents Snackbars from stacking up in reverse order.

Fixes: https://github.com/TeamNewPipe/NewPipe/issues/5660
2021-05-04 19:44:09 +02:00
Tobi
72ee4be495 Merge pull request #6152 from Isira-Seneviratne/Use_Animator_addListener
Use Animator.addListener() extension.
2021-05-04 19:37:03 +02:00
Robin
c85b97a484 Fix: fragmentSize is below the minimum (#6238)
noticed logs " CacheDataSink: fragmentSize is below the minimum recommended value of 2097152. This may cause poor cache performance.

this fixes the issue by using ExoPlayers MIN_RECOMMENDED_FRAGMENT_SIZE. Unfortunately that field is private:
aeb306a164/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java (L123)
2021-05-04 19:08:25 +02:00
Tobi
c7510c628f Merge pull request #6189 from mhmdanas/fix-some-warnings
Fix some warnings
2021-05-04 18:54:17 +02:00
Tobi
3ca1e550fe Merge pull request #5997 from TeamNewPipe/reChaptcha
Do not set reCaptcha cookie when there is no cookie stored
2021-05-04 18:53:12 +02:00
Tobi
01e8944077 Merge pull request #6206 from Redirion/exo2133
Update ExoPlayer to 2.13.3
2021-05-04 11:58:21 +02:00
Yağızhan
d6ab3298a3 Added Turkish README (#6219) 2021-05-02 16:55:49 +00:00
Robin
97b28bba4d Update ExoPlayer to 2.13.3 2021-04-30 11:48:01 +02:00
mhmdanas
7f6674a0e6 Remove strings without default values 2021-04-30 01:02:57 +03:00
mhmdanas
2c1df5f875 Actually fix the parentFile warning 2021-04-30 01:02:46 +03:00
mhmdanas
e7ae215ab0 Fix some warnings 2021-04-28 00:28:36 +03:00
TobiGr
4a9c790652 Merge branch 'master' into dev 2021-04-26 19:13:17 +02:00
Tobi
91ca680911 Merge pull request #6053 from TeamNewPipe/release_0.21.2
Release NewPipe 0.21.2 (968)
2021-04-26 18:54:28 +02:00
TobiGr
01376aba86 Update extractor version
FIx SoundCloud
2021-04-26 18:09:02 +02:00
TobiGr
d56ffa3531 Added translation using Weblate (Latin)
Added translation using Weblate (Latin)

Translated using Weblate (Slovak)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Malayalam)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 2.0% (1 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/

Translated using Weblate (Hebrew)

Currently translated at 44.8% (22 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/

Translated using Weblate (Malayalam)

Currently translated at 6.1% (3 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ml/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (49 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (49 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/

Translated using Weblate (Somali)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Latin)

Currently translated at 8.1% (52 of 639 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (639 of 639 strings)

Added translation using Weblate (Dutch (Middle))

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 62.1% (397 of 639 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Bengali)

Currently translated at 89.6% (573 of 639 strings)

Translated using Weblate (German)

Currently translated at 53.0% (26 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 26.5% (13 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/

Translated using Weblate (Tamazight (Central Atlas))

Currently translated at 31.1% (199 of 639 strings)

Translated using Weblate (Latin)

Currently translated at 16.4% (105 of 639 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (639 of 639 strings)
2021-04-26 12:01:55 +02:00
TobiGr
5a5a24bf1a Added translation using Weblate (Latin)
Added translation using Weblate (Latin)

Translated using Weblate (Slovak)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Malayalam)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 2.0% (1 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/

Translated using Weblate (Hebrew)

Currently translated at 44.8% (22 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/

Translated using Weblate (Malayalam)

Currently translated at 6.1% (3 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ml/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (49 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (49 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/

Translated using Weblate (Somali)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Latin)

Currently translated at 8.1% (52 of 639 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (639 of 639 strings)

Added translation using Weblate (Dutch (Middle))

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 62.1% (397 of 639 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Bengali)

Currently translated at 89.6% (573 of 639 strings)

Translated using Weblate (German)

Currently translated at 53.0% (26 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 26.5% (13 of 49 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/

Translated using Weblate (Tamazight (Central Atlas))

Currently translated at 31.1% (199 of 639 strings)

Translated using Weblate (Latin)

Currently translated at 16.4% (105 of 639 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (639 of 639 strings)
2021-04-26 11:59:35 +02:00
TobiGr
3d2c65b398 Update extractor version 2021-04-26 11:54:43 +02:00
Tobi
bacb35fb1c Merge pull request #6151 from TeamNewPipe/share-dialog-color
Fix black drawables in night themes in share dialog (RouterActivity)
2021-04-26 11:51:50 +02:00
Tobi
0a2ed805a2 Merge pull request #6161 from TeamNewPipe/player-buffering
Fix invisible buffering / loading indicator in player
2021-04-25 18:10:02 +02:00
TobiGr
e70c153cd3 Fix black drawables in night themes in share dialog (RouterActivity) 2021-04-25 11:36:40 +02:00
TobiGr
b54c2b7f57 Fix invisible buffering / loading indicator in player 2021-04-25 11:36:00 +02:00
Isira Seneviratne
3fe80ec5ac Use Animator.addListener() extension. 2021-04-24 07:45:12 +05:30
Saurav Rao
e52048c69e Update shot_10.png to include Bandcamp in the list of services (#6140)
* update shot_10.png

* updated shot_10.png

Co-authored-by: camo0112 <56369484+camo0112@users.noreply.github.com>
2021-04-23 09:39:08 +00:00
Robin
ceb930aed6 Merge pull request #6139 from subraizada3/dev
Only show 'download has started' toast when download is started
2021-04-22 09:56:14 +02:00
Sub Raizada
e775037366 Only show 'download has started' toast when a download has started 2021-04-22 02:45:04 +00:00
camo0112
4357e02c58 user can report the error 2021-04-20 16:21:20 +05:30
camo0112
67c0ceedc9 Error handling in subscribe() in DownloadDialog 2021-04-20 13:06:40 +05:30
Robin
0039312a64 Merge pull request #6109 from sauravrao637/codeImrovement
Replaced if/else with switch in ErrorActivity, supress false lint warning
2021-04-19 16:45:45 +02:00
camo0112
57f1152751 suppress false warning (which affect only library projects) 2021-04-19 19:26:15 +05:30
Robin
bfb9be1225 Merge pull request #6037 from mhmdanas/allow-installation-on-external-storage
Allow installation on external storage
2021-04-19 15:01:10 +02:00
TobiGr
8837b54aab Release NewPipe 0.21.2 (968) 2021-04-19 14:46:52 +02:00
TobiGr
8ab5a4d394 Update extractor version 2021-04-19 14:46:37 +02:00
Tobi
29bcf94d50 Merge pull request #6051 from TeamNewPipe/bandcamp_segments
[Bandcamp] Add Radio stream segments
2021-04-19 14:43:34 +02:00
TobiGr
dd68bf8eeb Update translations
Translated using Weblate (French)
Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (633 of 639 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (633 of 639 strings)

Translated using Weblate (English)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (638 of 639 strings)

Translated using Weblate (German)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (German)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (German)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (French)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Japanese)

Currently translated at 98.7% (631 of 639 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.6% (637 of 639 strings)

Translated using Weblate (Basque)

Currently translated at 99.6% (637 of 639 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.6% (637 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 93.1% (595 of 639 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (637 of 639 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.6% (637 of 639 strings)

Translated using Weblate (Hindi)

Currently translated at 82.1% (525 of 639 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.6% (637 of 639 strings)

Translated using Weblate (German)

Currently translated at 51.0% (24 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/

Translated using Weblate (Arabic)

Currently translated at 72.3% (34 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/

Translated using Weblate (Slovak)

Currently translated at 4.2% (2 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/

Translated using Weblate (Somali)

Currently translated at 99.6% (637 of 639 strings)

Translated using Weblate (German)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (German)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Ukrainian)

Currently translated at 94.5% (604 of 639 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Hindi)

Currently translated at 86.0% (550 of 639 strings)

Translated using Weblate (Tamil)

Currently translated at 37.2% (238 of 639 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Basque)

Currently translated at 33.3% (16 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/eu/

Translated using Weblate (Hebrew)

Currently translated at 43.7% (21 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/

Translated using Weblate (Italian)

Currently translated at 45.8% (22 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (48 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (48 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/

Translated using Weblate (English (United Kingdom))

Currently translated at 13.6% (87 of 639 strings)

Translated using Weblate (Spanish)

Currently translated at 99.5% (636 of 639 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Romanian)

Currently translated at 6.2% (3 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ro/

Translated using Weblate (Russian)

Currently translated at 10.4% (5 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/

Translated using Weblate (Russian)

Currently translated at 10.4% (5 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/

Translated using Weblate (Italian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 99.6% (637 of 639 strings)

Translated using Weblate (Russian)

Currently translated at 16.6% (8 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/

Translated using Weblate (German)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Serbian)

Currently translated at 98.4% (629 of 639 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (635 of 639 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 61.9% (396 of 639 strings)

Translated using Weblate (Tamil)

Currently translated at 38.4% (246 of 639 strings)

Translated using Weblate (Malay)

Currently translated at 64.6% (413 of 639 strings)

Translated using Weblate (Bengali)

Currently translated at 87.9% (562 of 639 strings)

Translated using Weblate (Bengali)

Currently translated at 18.7% (9 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/

Translated using Weblate (Serbian)

Currently translated at 10.4% (5 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/

Translated using Weblate (Somali)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Latvian)

Currently translated at 4.1% (2 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lv/

Translated using Weblate (Latvian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Esperanto)

Currently translated at 86.6% (554 of 639 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Polish)

Currently translated at 52.0% (25 of 48 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
2021-04-19 14:29:17 +02:00
camo0112
fc4dd4524a Replaced if/else with switch 2021-04-19 10:52:52 +05:30
Stypox
c12ac64678 Merge pull request #6060 from mhmdanas/fix-black-high-volume-gesture-icon
Fix ic_volume_up and ic_volume_off being black
2021-04-15 16:17:22 +02:00
XiangRongLin
264044272a Merge pull request #6079 from TacoTheDank/dev
Update actions/setup-java to v2
2021-04-15 11:25:11 +02:00
TacoTheDank
c74162c586 Update actions/setup-java to v2 2021-04-14 11:13:05 -04:00
mhmdanas
fa6ff4e5eb Fix ic_volume_off being black 2021-04-13 20:10:36 +03:00
mhmdanas
01bbc50c68 Fix high volume gesture being black 2021-04-12 15:30:31 +03:00
TobiGr
e5457e5029 [Bandcamp] Add Radio stream segments
Display channel / creator / artist name when given
2021-04-11 23:14:23 +02:00
TobiGr
b025bdf0c7 Merge branch 'master' into dev 2021-04-11 21:12:02 +02:00
TobiGr
600e156c4c Release 0.21.1 (967) 2021-04-10 11:19:48 +02:00
TobiGr
13ba708adc Add changelog for NewPipe 0.21.2 2021-04-10 11:19:09 +02:00
mhmdanas
edf8bf2c9d Allow installation on external storage 2021-04-10 12:15:33 +03:00
XiangRongLin
c9e0bf4f02 Merge pull request #5963 from sauravrao637/IssueFix#5959
Fix display of channel details when it has no videos
2021-04-09 11:15:51 +02:00
camo0112
8f9eaa22e6 Fix display of channel details when it has no videos 2021-04-09 12:45:43 +05:30
Tom
3fcd580491 Add queue time (#6023)
* Add queue time
2021-04-08 10:41:41 +02:00
Davide Beatrici
cf3cc2e984 Disable media tunneling on cvt_mt5886_eu_1g (#6024)
* Disable media tunneling on cvt_mt5886_eu_1g

Follow-up to 4a9d21062a.
2021-04-08 05:21:57 +02:00
TobiGr
76322d8089 Update extractor 2021-04-07 10:37:25 +02:00
Robin
9e29d8d692 Merge pull request #6001 from TacoTheDank/appcompat-alertdialog
Migrate rest of AlertDialogs to AppCompat
2021-04-07 05:07:24 +02:00
wangear
5d5f8b4d51 Fix NullPointerException: Attempt to get length of null array in MainActivity (#5999)
* Fixed Unable to start activity ComponentInfo{org.schabi.newpipe/org.schabi.newpipe.MainActivity}: java.lang.NullPointerException: Attempt to get length of null array #5996
issue : #5996
changed :
- Checked null
2021-04-06 14:13:13 +02:00
Robin
4d74be881d Merge pull request #6004 from krlvm/list-stream-improvements
Stream item improvements
2021-04-06 10:24:51 +02:00
krlvm
425a312151 Fix list stream item details overflow when there's free space 2021-04-04 21:07:43 +03:00
krlvm
ea294e8e5d Add ellipsis to list stream item uploader 2021-04-04 20:52:44 +03:00
TacoTheDank
e75d0de135 Migrate rest of AlertDialogs to AppCompat 2021-04-04 10:26:38 -04:00
TobiGr
81cacbd917 Update translations
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 62.2% (397 of 638 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.8% (46 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/

Translated using Weblate (Sardinian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hungarian)

Currently translated at 84.4% (539 of 638 strings)

Translated using Weblate (Serbian)

Currently translated at 90.5% (578 of 638 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (636 of 638 strings)

Translated using Weblate (Japanese)

Currently translated at 98.9% (631 of 638 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Basque)

Currently translated at 99.8% (637 of 638 strings)

Translated using Weblate (Arabic)

Currently translated at 99.0% (632 of 638 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Bengali)

Currently translated at 87.7% (560 of 638 strings)

Translated using Weblate (Basque)

Currently translated at 31.9% (15 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/eu/

Translated using Weblate (Hebrew)

Currently translated at 42.5% (20 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/

Translated using Weblate (Italian)

Currently translated at 44.6% (21 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (47 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (47 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/

Translated using Weblate (Slovak)

Currently translated at 4.2% (2 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (47 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/

Translated using Weblate (Vietnamese)

Currently translated at 99.6% (636 of 638 strings)
2021-04-04 15:14:31 +02:00
TobiGr
c0c78ae9bb Do not set reChaptache cookie when there is no cookie stored
In DownloaderImpl#getCookies(String url) the reChaptcha cookie is set if it is not null. For this reason, the cookie was set in every request.
2021-04-04 12:27:12 +02:00
Tobi
eb572e8d8f Merge pull request #5995 from XiangRongLin/fix_release
Fix release build by updating icon reference
2021-04-04 12:14:26 +02:00
XiangRongLin
781c499806 Fix release build by updating icon reference
Was changed in DayNight Theme, but only in debug resource and not release resource.
2021-04-04 09:22:42 +02:00
Tobi
a3d74ea444 Merge pull request #5927 from krlvm/daynight
Migrate to Android DayNight Theme, fix Light Theme, minor UI improvements
2021-04-03 20:29:24 +02:00
TobiGr
86a19aa037 Update extractor 2021-04-03 20:28:57 +02:00
krlvm
e484339cca Merge branch 'dev' into daynight 2021-04-03 00:08:26 +03:00
krlvm
6b5a1d0202 Fix Player controls colors on KitKat
AppCompatImageButton ignores "tint" theme attribute on API 19, therefore, they had to be specified directly, these attributes can be removed after the KitKat support is dropped.
2021-04-02 23:58:10 +03:00
krlvm
24247fd6a6 Fix quality & caption popups colors on KitKat 2021-04-02 23:46:14 +03:00
Stypox
2af20d5c40 Merge pull request #5974 from fynngodau/related-items
Rename related streams to related items
2021-04-02 20:34:39 +02:00
krlvm
dfb983c3cf Remove unused themes 2021-04-02 20:25:22 +03:00
PulkitGarg67
e1a6b69f9a Functionality to rename playlist name from playlist interface 2021-04-01 21:30:15 +02:00
Saurav Rao
8df935f5fe NullPointerException on screen rotation during exit fix (#5976) 2021-04-01 15:07:21 +02:00
Fynn Godau
04c5acd1d7 Use matching NPE version 2021-03-31 22:35:24 +02:00
Fynn Godau
c9766d25ef Rename related streams to related items 2021-03-31 22:33:22 +02:00
Tobi
aaea661b70 Merge pull request #5935 from Stypox/fix-notification-settings
Fix hidden icon to the left of notification actions in settings
2021-03-31 22:18:26 +02:00
krlvm
7061859112 Fix subscription picker colors on API 19 2021-03-31 20:17:58 +03:00
Tobi
66c24af3d2 Merge pull request #5969 from Redirion/tnnlwrkarnd
Disable media tunneling on Hi3798MV200
2021-03-31 19:02:00 +02:00
Stypox
083c315fd6 Merge pull request #5867 from TacoTheDank/rearrange-libraries
Rearrange libraries
2021-03-31 18:18:23 +02:00
Stypox
29b44a181b Merge pull request #5971 from sauravrao637/issueFix5968
App Crashes when no videos in last played and user interacts with playlistController  fix
2021-03-31 18:11:30 +02:00
krlvm
4fdc5ea646 Fix player controls colors on large screens 2021-03-31 19:07:27 +03:00
TacoTheDank
e17bfa029c Make the dependencies block much easier to read 2021-03-31 11:49:55 -04:00
TacoTheDank
279e4c2fa8 Update Android Studio 2021-03-31 11:44:43 -04:00
camo0112
856a39855e IssueFixed#5968 2021-03-31 20:48:02 +05:30
Robin
4a9d21062a Disabled tunneling on Hi3798MV200 2021-03-31 16:38:50 +02:00
krlvm
ad8f3aa6c9 Fix Kiosk Selector colors in Light theme 2021-03-31 16:49:28 +03:00
Stypox
43f85408be Merge pull request #5944 from Stypox/fix-search-menu
Fix some random NullPointerExceptions
2021-03-31 15:21:13 +02:00
Stypox
5739caaa5a Merge pull request #5856 from Redirion/exo213
Update to ExoPlayer 2.13.2
2021-03-31 14:29:16 +02:00
wangear
73cfa5499d Fix overlapping fonts and crash on tapping anywhere on video after long-pressing 'Popup' button (#5813)
* Overlapping fonts #5096
issue : #5096

* Overlapping fonts #5096
issue : #5096
changed :
- If additional textView is overlapped, only title view shows.

* Overlapping fonts #5096
issue : #5096
changed :
- Remove treeObserve and hiding logic.
- RelativeLayout -> ConstraintLayout.
- layout size fixed -> wrap_content.
- if text size is bigger, layout height bigger too.

* Overlapping fonts #5096
issue : #5096
changed :
- remove unusable variable

* Crash on tapping anywhere on video after long-pressing 'Popup' button #5804
issue : #5804
changed :
- checked null
- fixed NullPointerException.
2021-03-31 10:10:14 +02:00
Tobi
8f0323fb8d Merge pull request #5360 from XiangRongLin/instrumented_tests
Run Instrumented tests in CI pipeline
2021-03-31 10:08:05 +02:00
Robin
83d16932a4 Update to ExoPlayer 2.13.2 2021-03-31 10:05:44 +02:00
Stypox
84e3f6ca18 Merge pull request #5881 from TacoTheDank/about_revamp
About package internal revamp
2021-03-31 09:56:12 +02:00
Stypox
5d6a568308 Fix random NullPointerException in NotificationActionsPreference 2021-03-31 09:12:47 +02:00
Stypox
3e8cba745a Fix random NullPointerException in PlaylistFragment 2021-03-31 09:12:47 +02:00
Stypox
be4d12789d Fix random NullPointerException in SearchFragment 2021-03-31 09:12:45 +02:00
Tobi
00fbfb5a56 Merge pull request #5866 from TacoTheDank/update-gradle-wrapper
Update gradle wrapper
2021-03-30 10:36:48 +02:00
TacoTheDank
27d0f7f277 Convert AboutFragment clickListeners to a function extension 2021-03-29 19:07:12 -04:00
TacoTheDank
fb1aab2a49 Converted the about package to Kotlin 2021-03-29 19:06:32 -04:00
TacoTheDank
cc72fa4793 Renamed things pertaining to the about package 2021-03-29 19:05:57 -04:00
krlvm
e9c60eff85 Cleanup 2021-03-30 00:03:30 +03:00
krlvm
5b7c87ee79 Fix FilePicker crash 2021-03-29 23:15:02 +03:00
krlvm
c8b4685fc9 Fix ErrorActivity colors 2021-03-29 22:48:28 +03:00
krlvm
561d5675f7 Fix Downloader colors 2021-03-29 21:35:00 +03:00
krlvm
c906cb57ee Fix colors in setting fragments, cleanup 2021-03-29 21:19:17 +03:00
Tom
84de865daf Allow the user to pause while a video is buffering (#5929)
Fix pause while buffering
Use getPlayWhenReady wrapper everywhere playWhenReady is checked
Remove duplicate `playPause()` code
2021-03-29 18:00:00 +02:00
krlvm
24a264d78c Fix Volume Off gesture overlay 2021-03-29 16:10:56 +03:00
krlvm
62c3c7ac21 Fix Toolbar Search input pointer and cursor color 2021-03-29 14:47:46 +03:00
krlvm
a7d6ad5162 Fix Subscription Picker's Toolbar colors 2021-03-29 14:44:29 +03:00
XiangRongLin
0dbb212d13 Add instrumented tests to CI workflow 2021-03-29 10:18:12 +02:00
krlvm
8002cc2771 Refactor Styles, disable colored navigation bar when we can't be sure that navigation bar buttons will have enough contrast (API < 27) 2021-03-28 22:51:44 +03:00
krlvm
8d64eac853 Red Splash Screen for API < 23 2021-03-28 22:35:03 +03:00
krlvm
80d1c5b9f5 Fix Repeat Button color in Player 2021-03-28 22:04:54 +03:00
krlvm
7175f27da8 Fix File Picker Toolbar colors 2021-03-28 21:49:41 +03:00
krlvm
d6f9aace8c Fix White text in Play Queue in Light Theme, remove shade 2021-03-28 21:38:15 +03:00
krlvm
aeccb5b472 Fix Toolbar colors in Play Queue 2021-03-28 20:44:20 +03:00
krlvm
09a7b7718a Fix text color in playlist overlay 2021-03-28 20:43:52 +03:00
TotalCaesar659
ef2c76efaf Update URLs to HTTPS (#5942) 2021-03-28 17:00:14 +00:00
krlvm
69793049c3 Fix subscription selector V overlay color, add ripples 2021-03-28 19:50:45 +03:00
krlvm
67942a906a Fix colors in PeerTube Settings Fragment 2021-03-28 19:36:07 +03:00
Mohammed Anas
6a9cae3de8 Add channel details option to long-press menu (#5851)
Add dialog item to open channel details
Use `List` as type of `entries`
Put channel details item last
Only show channel option when channel is present
2021-03-28 18:32:40 +02:00
krlvm
0afdac5683 Fix Light Player Popups in Dark Theme, make Player Controls Overlay always Dark 2021-03-28 18:48:51 +03:00
krlvm
609d09a8e2 Remove unused SuggestionListAdapter.resolveResourceIdFromAttr 2021-03-28 15:12:59 +03:00
krlvm
01e8654fbd Fix Search colors on KitKat 2021-03-28 14:55:54 +03:00
krlvm
f477ab84d5 Fix tab highlight opacity on KitKat 2021-03-28 14:43:11 +03:00
Stypox
9f59d4baa3 Add note about "Fixes" prefix to auto-link issues 2021-03-28 13:41:33 +02:00
TiA4f8R
2e9a1d958c Change the old NewPipe domain to the new domain
Change in READMEs newpipe.schabi.org to newpipe.net for F-Droid custom repository installation link
2021-03-28 13:23:25 +02:00
krlvm
e4f2c58933 Set Search text cursor using accent color instead of drawable 2021-03-28 12:24:29 +03:00
krlvm
a9e8b3e06b Remove ThemeHelper.resolveResourceIdFromAttr 2021-03-28 12:23:52 +03:00
Stypox
daa5b7827a Merge pull request #5879 from TacoTheDank/clean-oncreatepreferences
Clean up unnecessary onCreate in settings fragments
2021-03-28 10:48:10 +02:00
Stypox
dd00152485 Fix hidden icon to the left of notification actions in settings 2021-03-28 10:36:47 +02:00
krlvm
df52a6ea6b Update app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt
Co-authored-by: Stypox <stypox@pm.me>
2021-03-28 00:15:22 +03:00
krlvm
7d5197e6fd Fix Dark elements in Dark Theme 2021-03-27 19:15:53 +03:00
krlvm
81e08d0cc4 Show ripples on tab bar with the same color as the toolbar 2021-03-27 18:33:28 +03:00
krlvm
32a159d48f Fix error in tests 2021-03-27 18:11:39 +03:00
krlvm
44f3a7484d Follow system theme by default 2021-03-27 17:53:07 +03:00
krlvm
38eb8e40ea Colored Navigation Bar in Splash Screen 2021-03-27 17:51:02 +03:00
krlvm
fd14c8cdce Fix Dark elements in Light Theme 2021-03-27 17:46:05 +03:00
iamthesenate1
216da63276 Update README.ro.md (#5921)
Delete old warning about F-Droid not being up to date
2021-03-27 13:21:41 +00:00
Stypox
08d8f2564a Merge pull request #5882 from TacoTheDank/fragmentcontainerview
Replace FrameLayout with FragmentContainerView where applicable
2021-03-27 08:31:32 +01:00
krlvm
1d51002173 Fix Dark toolbar text in Light theme 2021-03-27 01:28:28 +03:00
krlvm
610d0b272e Make Navigation Bar buttons color black in Light Theme 2021-03-26 23:47:46 +03:00
krlvm
b3e2418b93 Migrate to DayNight Theme 2021-03-26 23:08:25 +03:00
Tobi
464d0e50b0 Merge pull request #5871 from Stypox/release_0.21.0
Release 0.21.0
2021-03-26 08:31:59 +01:00
Stypox
7411c54f9e Release 0.21.0 (966) 2021-03-25 23:02:14 +01:00
TobiGr
7c74deb700 Update translations
Translated using Weblate (German)
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (French)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (German)

Currently translated at 48.9% (23 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/

Translated using Weblate (Hebrew)

Currently translated at 40.4% (19 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/

Translated using Weblate (Arabic)

Currently translated at 68.0% (32 of 47 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/

Translated using Weblate (Somali)

Currently translated at 100.0% (638 of 638 strings)
2021-03-25 22:15:44 +01:00
TobiGr
e63165e80f Update extractor version to 0.21.0 2021-03-25 22:13:55 +01:00
XiangRongLin
e2bc9dfacd Merge pull request #5906 from TeamNewPipe/sonar
Disable sonar job
2021-03-25 19:39:21 +01:00
Stypox
48789dbab7 Add changelog for 0.21.0 (966) 2021-03-25 10:41:15 +01:00
TobiGr
196f0f0475 Update translations
Translated using Weblate (French)
Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Indonesian)

Currently translated at 23.9% (11 of 46 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
2021-03-25 10:29:55 +01:00
Stypox
085b59f2e1 Merge pull request #5907 from TeamNewPipe/B0pol-patch-1
Update strings.xml
2021-03-25 08:54:09 +01:00
Stypox
2fdc2664ff Merge pull request #5904 from TeamNewPipe/weblate-update
Update weblate and use BCP47 language codes
2021-03-25 08:13:03 +01:00
bopol
a33a5c5527 Update strings.xml
fix typo
2021-03-24 19:44:25 +01:00
Tobi
a7c0f37904 Disable sonar job
For some reason, sonar started to analyse very old PRs. This comments out the sonar job until further investigation is done.
2021-03-24 19:10:00 +01:00
TobiGr
4889ab3462 Use BCP47 language codes for fastlane
Closes #5750
2021-03-24 18:16:40 +01:00
TobiGr
3923deeaad Update translations
Translated using Weblate (German)
Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (French)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (French)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Slovenian)

Currently translated at 77.2% (489 of 633 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (632 of 633 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Santali)

Currently translated at 12.9% (82 of 633 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 13.5% (86 of 633 strings)

Translated using Weblate (Somali)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (French)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (630 of 633 strings)

Translated using Weblate (Korean)

Currently translated at 82.9% (525 of 633 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Esperanto)

Currently translated at 87.0% (551 of 633 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Catalan)

Currently translated at 97.9% (620 of 633 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 97.1% (615 of 633 strings)

Translated using Weblate (Malayalam)

Currently translated at 89.4% (566 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (French)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (633 of 633 strings)
2021-03-24 18:09:52 +01:00
Tobi
80d6fff0ca Merge pull request #5649 from FireMasterK/ff-ua
Change UA to privacy.resistFingerprinting.
2021-03-24 17:59:17 +01:00
Tobi
fe43b4da39 Merge pull request #5896 from Stypox/fix-error-panel
Fix error panel created in onViewCreated() but disposed in onDestroy()
2021-03-24 11:32:07 +01:00
Tobi
67afd05e22 Merge pull request #5899 from Stypox/fix-settings-theme
Fix settings switches are not red anymore
2021-03-24 11:24:27 +01:00
Stypox
0fcaf20221 Fix settings switches are not red anymore
Reverts part of 731c65cd59
2021-03-24 10:16:24 +01:00
Stypox
0277b94b37 Fix error panel created in onViewCreated() but disposed in onDestroy() 2021-03-24 09:27:17 +01:00
TacoTheDank
6a9d5fd4cc Replace FrameLayout with FragmentContainerView where applicable 2021-03-22 17:46:06 -04:00
TacoTheDank
a83106f717 Clean up unnecessary onCreate in settings fragments 2021-03-22 16:38:36 -04:00
TacoTheDank
4c2a6e346d Update gradle wrapper 2021-03-20 21:48:43 -04:00
TacoTheDank
cae63a7ada Add gradle wrapper validation action 2021-03-20 21:47:19 -04:00
Stypox
c7efa8c4f1 Merge pull request #5519 from WoodyMats/release_0.20.9
Add toast to inform the user that download started.
2021-03-20 22:19:13 +01:00
Matskidis Giannis
bf6645e829 Show Toast when download starts
Add toast to inform the user that download started and add the right string in values.
2021-03-20 22:18:01 +01:00
XiangRongLin
ea1b42510c Merge pull request #5844 from TeamNewPipe/sonar_workflow
Sonar workflow
2021-03-20 18:22:28 +01:00
XiangRongLin
72818ffa42 Add sonar plugin and github actions file for sonar analysis
Copied from sonarcloud.io setup page with minor adjustments. Adjusted branch to 'dev' and updated versions
2021-03-20 13:58:17 +01:00
XiangRongLin
985308bf0c Set the whole configDir instead of only configFile for checkstyle 2021-03-20 13:58:17 +01:00
Stypox
86381696f4 Merge pull request #5457 from Redirion/exo123
Update to ExoPlayer 2.12.3
2021-03-18 18:59:56 +01:00
Robin
08b960cc6e Merge pull request #3632 from B0pol/device_theme
Add settings to match device's theme (dark & black)
2021-03-18 15:11:40 +01:00
Stypox
731c65cd59 Refactor ThemeHelper 2021-03-18 12:39:29 +01:00
bopol
a85e8a29ff Use a list for night themes
Also remove unused strings
2021-03-18 12:12:04 +01:00
bopol
22b2f52f8c Use a switch preference to follow device theme 2021-03-18 11:23:55 +01:00
bopol
a713ce2126 Add settings for device theme (dark & black)
fix bugs related to isLightThemeSelected not handling device themes
such as license having dark background when it should be white
2021-03-18 11:17:06 +01:00
Stypox
4fac3cf304 Merge pull request #5230 from Isira-Seneviratne/Update_prettytime
Update prettytime.
2021-03-18 08:59:40 +01:00
Isira Seneviratne
74e20a8c52 Use PrettyTime's new formatUnrounded(OffsetDateTime) method.
Also change the types of the relevant variables from Calendar to OffsetDateTime.
2021-03-18 06:38:12 +05:30
Isira Seneviratne
8cf4ba25f5 Update prettytime to 5.0.0.Final. 2021-03-18 06:38:12 +05:30
Stypox
feb65cf8f3 Merge pull request #5792 from TeamNewPipe/resize_mode
Fix last resize mode not being restored correctly
2021-03-17 09:07:44 +01:00
Stypox
93592d23f4 Merge pull request #5820 from TeamNewPipe/mini_player_title
Fix empty stream title in minimized player
2021-03-17 08:56:50 +01:00
Jeremiah Dicharry
338a4837bc Add Spanish README.md (#5829) 2021-03-16 23:38:10 +01:00
mhmdanas
8e19fe535c Re-add badgs to localized README's 2021-03-16 23:35:35 +01:00
TobiGr
2aeccc0c5c Translated using Weblate (Bengali (Bangladesh))
Currently translated at 62.8% (396 of 630 strings)

Translated using Weblate (Bengali)

Currently translated at 86.5% (545 of 630 strings)

Translated using Weblate (Bengali)

Currently translated at 19.5% (9 of 46 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/
2021-03-16 09:23:48 +01:00
TobiGr
8db1234a59 Update translations
Translated using Weblate (Chinese (Simplified))
Currently translated at 84.7% (39 of 46 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/

Translated using Weblate (Chinese (Simplified))

Currently translated at 84.7% (39 of 46 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/

Translated using Weblate (Indonesian)

Currently translated at 99.0% (624 of 630 strings)

Translated using Weblate (Odia)

Currently translated at 0.0% (0 of 46 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/or/

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (French)

Currently translated at 99.6% (628 of 630 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Polish)

Currently translated at 99.6% (628 of 630 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Estonian)

Currently translated at 72.6% (458 of 630 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Danish)

Currently translated at 63.8% (402 of 630 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 86.9% (40 of 46 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/

Translated using Weblate (Polish)

Currently translated at 99.6% (628 of 630 strings)

Translated using Weblate (Polish)

Currently translated at 99.6% (628 of 630 strings)

Translated using Weblate (German)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Estonian)

Currently translated at 78.8% (497 of 630 strings)

Translated using Weblate (Malay)

Currently translated at 65.7% (414 of 630 strings)

Translated using Weblate (Indonesian)

Currently translated at 21.7% (10 of 46 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/

Translated using Weblate (Portuguese (Portugal))

Currently translated at 56.5% (26 of 46 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
2021-03-16 09:23:48 +01:00
Tobi
80fb351ad3 Merge pull request #5830 from mhmdanas/remove-outdated-fdroid-version-note
Re-add F-Droid badge and remove outdated note
2021-03-15 08:43:51 +01:00
mhmdanas
523f85d4d1 Uncomment F-Droid badge and remove outdated note 2021-03-15 08:21:55 +03:00
Tobi
bfff500915 Merge pull request #5818 from TeamNewPipe/thumbnail_size
[background player] Fix very small thumbnails
2021-03-14 21:12:52 +01:00
Tobi
3e83bb0d95 Merge pull request #3741 from fynngodau/bandcamp
Bandcamp support
2021-03-14 20:00:40 +01:00
TobiGr
bdaee25e61 Use latest extractor version 2021-03-14 17:52:15 +01:00
TobiGr
71d3227791 Fix bottom controls being out of the screen
This was caused by too large end screen thumbnails enlarging the whole palyer. Fixed by scaling the thumbnail.

Ensure that the player does not use the whole screen height in detail fragment to keep the additional content like title, comments, etc. available.
2021-03-14 17:52:15 +01:00
TobiGr
a28aa6a8c4 Add Bandcamp to supported services section in README 2021-03-14 17:52:15 +01:00
TobiGr
292e103073 Ignore ContentNotSupportedException caused by Bandcamp fan pages 2021-03-14 17:52:15 +01:00
Fynn Godau
39a3f03e79 Bandcamp support 2021-03-14 17:52:15 +01:00
Tobi
404a6c12a6 Merge pull request #5148 from Stypox/error-panel
Show improved error panel instead of annoying snackbar or crashing
2021-03-14 17:41:27 +01:00
Tobi
8271409afe Merge pull request #5562 from mbarashkov/hardware_keyboard_space_shortcut_v2
Implement "pause/play" toggle on hardware keyboard space button.
2021-03-14 13:08:44 +01:00
TobiGr
985f659026 Fix empty stream title in minimized player 2021-03-13 20:12:57 +01:00
TobiGr
7c36cbffd0 [background player] Fix very small thumbnails 2021-03-13 18:17:30 +01:00
Stypox
285ea4e3fd Better handle url not supported in RouterActivity
Make sure the url not supported dialog is only shown when the url is really not supported, not on any ExtractionException
2021-03-12 23:21:54 +01:00
Stypox
8ce18647f1 Improve email subject for error reporting 2021-03-12 23:21:54 +01:00
Stypox
aee0478235 FeedFragment: fix view binding and show loading indicator correctly 2021-03-12 23:21:54 +01:00
Stypox
c3cf1d81c2 Fix error panel and search fragment state saving 2021-03-12 23:21:54 +01:00
Stypox
c2b6cec37d Hide meta info panel in search when starting a new search 2021-03-12 23:21:54 +01:00
Stypox
b265cabc22 Fix views not scrollable when showing error panel 2021-03-12 23:21:54 +01:00
Stypox
463dd8ea74 Completely remove return activity, now outdated 2021-03-12 23:21:54 +01:00
Stypox
0263125e11 Fix tests 2021-03-12 23:21:54 +01:00
Stypox
1fc8e4c148 Optimize imports and solve checkstyle issues 2021-03-12 23:21:53 +01:00
Stypox
c43bca6007 Add report/solve-recaptcha button in error panel
It will be shown even when nothing could be loaded not due to a network error, and the user can choose to ignore or report it.

Also improve error reporting arguments
Also completely refactor error activity
Also improve some code here and there
2021-03-12 23:21:49 +01:00
Tobi
4c31636d19 Merge pull request #5795 from TeamNewPipe/ci-only-on-master
Fix: CI only on dev
2021-03-09 11:50:47 +01:00
Poolitzer
3a61ab59f2 adding master to push trigger
Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2021-03-09 11:21:40 +01:00
Poolitzer
1db3c57ef0 change master to dev 2021-03-08 13:19:18 +01:00
Poolitzer
eeaf3496d5 Fix: CI only on master 2021-03-08 13:15:22 +01:00
Tobi
70f421b787 Add links to Matrix and IRC chat when opening new issues (#5764) 2021-03-08 09:43:05 +00:00
TobiGr
86fa629591 Fix last resize mode not being restored correctly
I think the settings key "last_resize_mode" is ambiguous. While it is used to get the recently used resize mode, someone thought while working on the resize mode switcher, that the old (to be replaced) resize mode should be stored. 
Fixes #5613
2021-03-08 09:46:33 +01:00
Stypox
553b80164b Move all error-related classes into error package 2021-03-07 17:49:28 +01:00
TobiGr
8518933ca8 Update translations
Translated using Weblate (Russian)
Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (French)

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (624 of 624 strings)
2021-03-07 16:53:38 +01:00
Tobi
ea53b7d4ad Merge pull request #5385 from TiA4f8R/soundcloud-error-improvements
Better error messages for SoundCloud and YouTube unavailable contents
2021-03-07 16:35:56 +01:00
TiA4f8R
37a96d063f Add different error messages for SoundCloud and YouTube unavailable contents
Add new error strings for the six new exceptions created in the extractor and catch these exceptions. Extractor is, of course, updated with this PR.
2021-03-07 15:33:25 +01:00
Tobi
b360920472 Merge pull request #5772 from iamthesenate1/dev
Add spcace after the comma, before Romanian in Readme.md.
2021-03-06 12:13:38 +01:00
iamthesenate1
9e1744f904 Add spcace after the comma, before Rommanian. 2021-03-06 12:20:01 +02:00
TobiGr
85a468bda9 Update translations: app
Translated using Weblate (French)
Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Slovenian)

Currently translated at 75.1% (469 of 624 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Albanian)

Currently translated at 98.2% (613 of 624 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 96.4% (602 of 624 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (624 of 624 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 100.0% (624 of 624 strings)
2021-03-05 22:45:00 +01:00
FireMasterK
b236bb407b Change UA to privacy.resistFingerprinting. 2021-02-20 16:49:37 +05:30
Stypox
8978187c64 Improve code style and fix some warnings
Removed a textTrack null check on a now- NonNull method
Added a error type switch case (TIMEOUT)
2021-02-16 16:54:44 +01:00
Robin
eba0b07782 Update to ExoPlayer 2.12.3 2021-02-16 16:42:51 +01:00
Mikhail Barashkov
80161c36c6 Apply the space button shortcut to large screens as well 2021-02-12 12:17:46 +03:00
Mikhail Barashkov
aea912f499 Implement "pause/play" toggle on hardware keyboard space button. 2021-02-12 11:58:15 +03:00
Isira Seneviratne
a3c4a10721 Convert the abstract class DAOs to interfaces. 2021-01-20 06:29:50 +05:30
949 changed files with 14825 additions and 7566 deletions

View File

@@ -3,9 +3,9 @@ NewPipe contribution guidelines
## 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.
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
@@ -25,22 +25,57 @@ You'll see exactly what is sent, be able to add your comments, and then send it.
## Code contribution
* If you want to help out with an existing bug report or feature request, leave a comment on that issue saying you want to try your hand at it.
* If there is no existing issue for what you want to work on, open a new one describing your changes. This gives the team and the community a chance to give feedback before you spend time on something that is already in development, should be done differently, or should be avoided completely.
* Stick to NewPipe's style conventions of [checkStyle](https://github.com/checkstyle/checkstyle). It runs each time you build the project.
* Do not bring non-free software (e.g. binary blobs) into the project. Make sure you do not introduce Google
libraries.
### Guidelines
* Stick to NewPipe's *style conventions* of [checkStyle](https://github.com/checkstyle/checkstyle) and [ktlint](https://github.com/pinterest/ktlint). They run each time you build the project.
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
* 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.
* Please test (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That makes the maintainers' jobs way easier.
* Please show intention to maintain your features and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
* In particular **do not bring non-free software** (e.g. binary blobs) into the project. Make sure you do not introduce any closed-source library from Google.
### Before starting development
* If you want to help out with an existing bug report or feature request, **leave a comment** on that issue saying you want to try your hand at it.
* If there is no existing issue for what you want to work on, **open a new one** describing the changes you are planning to introduce. This gives the team and the community a chance to give **feedback** before you spend time on something that is already in development, should be done differently, or should be avoided completely.
* Please show **intention to maintain your features** and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
* 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.
### 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.
* Please **test** (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
* Respond if someone requests changes or otherwise raises issues about your PRs.
* Send PRs that only cover one specific issue/solution/bug. Do not send PRs that are huge and consist of multiple independent solutions.
* Try to figure out yourself why builds on our CI fail.
* Make sure your PR is **up-to-date** with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must *rebase* your branch on the `dev` branch manually and resolve the conflicts on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). Doing this makes the maintainers' job way easier.
## IDE setup & building the app
### Basic setup
NewPipe is developed using [Android Studio](https://developer.android.com/studio/). Learn more about how to install it and how it works in the [official documentation](https://developer.android.com/studio/intro). In particular, make sure you have accepted Android Studio's SDK licences. Once Android Studio is ready, setting up the NewPipe project is fairly simple:
- Clone the NewPipe repository with `git clone https://github.com/TeamNewPipe/NewPipe.git` (or use the link from your own fork, if you want to open a PR).
- Open the folder you just cloned with Android Studio.
- Build and run it just like you would do with any other app, with the green triangle in the top bar.
You may find [SonarLint](https://www.sonarlint.org/intellij)'s **inspections** useful in helping you to write good code and prevent bugs.
### checkStyle setup
The [checkStyle](https://github.com/checkstyle/checkstyle) plugin verifies that Java code abides by the project style. It runs automatically each time you build the project. If you want to view errors directly in the editor, instead of having to skim through the build output, you can install an Android Studio plugin:
- Go to `File -> Settings -> Plugins`, search for `checkstyle` and install `CheckStyle-IDEA`.
- Go to `File -> Settings -> Tools -> Checkstyle`.
- Add NewPipe's configuration file by clicking the `+` in the right toolbar of the "Configuration File" list.
- Under the "Use a local Checkstyle file" bullet, click on `Browse` and pick the file named `checkstyle.xml` in the project's root folder.
- Enable "Store relative to project location" so that moving the directory around does not create issues.
- Insert a description in the top bar, then click `Next` and then `Finish`.
- Activate the configuration file you just added by enabling the checkbox on the left.
- Click `Ok` and you are done.
### ktlint setup
The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as checkStyle for Kotlin files. Installing the related plugin is as simple as going to `File -> Settings -> Plugins`, searching for `ktlint` and installing `Ktlint (unofficial)`.
## Communication
* The [#newpipe](irc:irc.freenode.net/newpipe) channel on freenode has the core team and other developers in it. [Click here for webchat](https://webchat.freenode.net/?channels=newpipe)!
* You can also use a Matrix account to join the Newpipe channel at [#freenode_#newpipe:matrix.org](https://matrix.to/#/#freenode_#newpipe:matrix.org).
* Post suggestions, changes, ideas etc. on GitHub or IRC.
* 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 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.
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC.

View File

@@ -33,7 +33,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
### Actual behaviour
### Actual behavior
<!-- Tell us what happens with the steps given above. -->

View File

@@ -1 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 💬 IRC
url: https://web.libera.chat/#newpipe
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

24
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: Question
about: Ask about anything NewPipe-related
labels: question
assignees: ''
---
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
### Checklist
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O (If there's already an issue but you'd like to see if something changed, just make a comment on the issue instead of opening a new one.) -->
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
#### What's your question(s)?
#### Additional context
<!-- Add any other context, like screenshots or links, about the question here.
Example: *Here's a photo of my cat!* -->

View File

@@ -12,18 +12,23 @@
- create clones
- take over the world
#### Before/After Screenshots/Screen Record
<!-- If your PR changes the app's UI in any way, please include screenshots or a video showing exactly what changed, so that developers and users can pinpoint it easily. Delete this if it doesn't apply to your PR.-->
- Before:
- After:
#### Fixes the following issue(s)
<!-- Also add any other links relevant to your change. -->
-
<!-- Prefix issues with "Fixes" so that GitHub closes them when the PR is merged (note that each "Fixes #" should be in its own item). Also add any other relevant links. -->
- Fixes #
#### Relies on the following changes
<!-- Delete this if it doesn't apply to you. -->
<!-- Delete this if it doesn't apply to your PR. -->
-
#### APK testing
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
<!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.-->
On the website the APK can be found by going to the "Checks" tab below the title and then on "artifacts" on the right.
The APK can be found by going to the "Checks" tab below the title. On the left pane, click on "CI", scroll down to "artifacts" and click "app" to download the zip file which contains the debug APK of this PR.
#### Due diligence
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).

View File

@@ -1,22 +1,32 @@
name: CI
on: [push, pull_request]
on:
pull_request:
branches:
- dev
- master
push:
branches:
- dev
- master
jobs:
build-and-test:
build-and-test-jvm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
- name: create and checkout branch
# push events already checked out the branch
if: github.event_name == 'pull_request'
run: git checkout -B ${{ github.head_ref }}
- name: set up JDK 1.8
uses: actions/setup-java@v1.4.3
- name: set up JDK 8
uses: actions/setup-java@v2
with:
java-version: 1.8
java-version: 8
distribution: "adopt"
- name: Cache Gradle dependencies
uses: actions/cache@v2
@@ -25,7 +35,7 @@ jobs:
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Build debug APK and run Tests
- name: Build debug APK and run jvm tests
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
- name: Upload APK
@@ -33,3 +43,66 @@ jobs:
with:
name: app
path: app/build/outputs/apk/debug/*.apk
# Disabled until emulator works again. see https://github.com/TeamNewPipe/NewPipe/pull/6560
# test-android:
# macos has hardware acceleration. See android-emulator-runner action
# runs-on: macos-latest
# strategy:
# matrix:
# api-level 19 is min sdk, but throws errors related to desugaring
# api-level: [21, 29]
# steps:
# - uses: actions/checkout@v2
#
# - name: set up JDK 8
# uses: actions/setup-java@v2
# with:
# java-version: 8
# distribution: "adopt"
#
# - name: Cache Gradle dependencies
# uses: actions/cache@v2
# with:
# path: ~/.gradle/caches
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
# restore-keys: ${{ runner.os }}-gradle
#
# - name: Run android tests
# uses: reactivecircus/android-emulator-runner@v2
# with:
# api-level: ${{ matrix.api-level }}
# script: ./gradlew connectedCheck
# sonar:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# with:
# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
# - name: Set up JDK 11
# uses: actions/setup-java@v2
# with:
# java-version: 11 # Sonar requires JDK 11
# distribution: "adopt"
# - name: Cache SonarCloud packages
# uses: actions/cache@v2
# with:
# path: ~/.sonar/cache
# key: ${{ runner.os }}-sonar
# restore-keys: ${{ runner.os }}-sonar
# - name: Cache Gradle packages
# uses: actions/cache@v2
# with:
# path: ~/.gradle/caches
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
# restore-keys: ${{ runner.os }}-gradle
# - name: Build and analyze
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# run: ./gradlew build sonarqube --info

140
README.es.md Normal file
View File

@@ -0,0 +1,140 @@
<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>
<h4 align="center">Una interfaz de streaming lijera y libre para Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/badge/Lanzamiento-v0.20.11-blue.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/Licencia-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://hosted.weblate.org/engage/newpipe/es/" alt="Estado de la traducción"><img src="https://hosted.weblate.org/widgets/newpipe/es/svg-badge.svg"></a>
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/Canal%20de%20IRC%20-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#capturas-de-pantalla">Capturas de pantalla</a> &bull; <a href="#descripción">Descripción</a> &bull; <a href="#características">Características</a> &bull; <a href="#installación-y-actualizaciones">Installación y actualizaciones</a> &bull; <a href="#contribución">Contribución</a> &bull; <a href="#donar">Donar</a> &bull; <a href="#licencias">Licencias</a></p>
<p align="center"><a href="https://newpipe.net">Sitio web</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">Preguntas Frecuentes</a> &bull; <a href="https://newpipe.net/press/">Prensa</a></p>
<hr>
*Lea esto en otros idiomas: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
<b>AVISO: ESTA ES UNA VERSIÓN BETA, POR LO TANTO, PUEDE ENCONTRAR BUGS (ERRORES). SI ENCUENTRA UNO, ABRA UN ISSUE A TRAVÉS DE NUESTRO REPOSITORIO GITHUB.</b>
<b>COLOCAR NEWPIPE O CUALQUIER FORK (BIFURCACIÓN) REALIZADO DE ELLO EN GOOGLE PLAY STORE VIOLA SUS TÉRMINOS Y CONDICIONES.</b>
## Capturas de pantalla
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
## Descripción
NewPipe no usa ninguna librería de framework de Google, ni la API de YouTube. Los sitios web solamente se analizan para extraer la información requerida, asi que esta app se puede usar sin los servicios de Google instalados. Además, no se necesita una cuenta de YouTube para usar NewPipe, lo cual es un software libre de copyleft.
### Características
* Buscar videos
* Mostrar información general sobre videos
* Mirar videos de YouTube
* Escuchar audio de YouTube
* Modo popup (reproductor flotante)
* Elegir reproductor para mirar el video
* Descargar videos
* Descargar solamente audio
* Abrir video en Kodi
* Mostrar videos próximos/relacionados
* Buscar a través de YouTube en un idioma específico
* Mirar/Bloquear materiales restringidas por edad.
* Mostrar información general sobre canales
* Buscar canales
* Mirar videos de un canal
* Apoyo Orbot/Tor (todavía no directamente)
* Apoyo 1080p/2K/4K
* Ver historias
* Subscribirse a canales
* Buscar historias
* Buscar/mirar listas de reproducción
* Mirar listas de reproducción en fila
* Poner videos en fila
* Listas locales de reproducción
* Subtítulos
* Apoyo de medios en directo
* Mostrar comentarios
### Servicios apoyados
NewPipe apoya varios servicios. Nuestras [documentaciones](https://teamnewpipe.github.io/documentation/) proveen más información en como se puede agregar un servicio nuevo a la app y el extractor. Por favor contáctenos si pretende agregar uno nuevo. Actualmente los servicios apoyados son:
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
* Bandcamp \[beta\]
<!-- Brecha escondida para mantener compatibles los enlaces viejos. -->
<span id="actualizaciones"></span>
## Installación y actualizaciones
Se puede instalar NewPipe usando uno de los métodos siguientes:
1. Agregar nuestro repositorio personalizado a F-Droid e instalarlo desde allí. Las instrucciones están aquí: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
2. Descargar el archivo APK del enlace [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalarlo.
3. Actualizar a través de F-Droid. Este es el método más lento para obtener la actualización, como F-Droid debe reconocer cambios, construir el APK aparte, firmarlo con una clave, y finalmente empujar la actualización a los usuarios.
4. Construir un APK de depuración por si mismo. Este es el modo más rápido para realizar nuevas características en su dispositivo, pero es mucho más complicado, asi que recomendamos uno de los otros métodos.
Recomendamos el método 1 para la mayoría de usuarios. Los APKs instalados usando método 1 o 2 son compatibles el uno con el otro, pero no con las instalaciones usando método 3. Esta es debida a la misma clave digital (la nuestra), siendo utilizado en los métodos 1 y 2, pero una clave digital diferente (la de F-Droid) siendo utilizado en el método 3. Construir un APK de depuración usando método 4 excluye una clave enteramente. Firmando con claves digitales ayuda a asegurar de que un
usuario no esté engañado para instalar una actualización maliciosa a una app.
Mientras tanto, si quiere cambiar los fuentes por alguna razón (por ejemplo, la funcionalidad del nucleo de NewPipe se rompe y F-Droid aun no tiene la actualización), recomendamos el siguiente procedimiento:
1. Repaldear sus datos a través de Ajustes > Contenido > Exporta base de datos para guardar su historia, subscripciones, y listas de reproducción
2. Desinstalar NewPipe
3. Descargar el APK del nuevo fuente e instalarlo.
4. Importar los datos del paso 1 a través de Ajustes > Contenido > Importa base de datos.
## Contribución
Si tiene ideas, traducciónes, cambios de diseño, limpieza de código, o cambios grandes de código, su ayuda es siempre bienvenida.
Cuanto más realizamos, mejor se pone la aplicación!
Si quiere involucrarse, fíjese en nuestras [notas de contribución](.github/CONTRIBUTING.md).
<a href="https://hosted.weblate.org/engage/newpipe/es/">
<img src="https://hosted.weblate.org/widgets/newpipe/es/287x66-grey.png" alt="Estado de la traducción" />
</a>
## Donar
Si le gusta el NewPipe estaremos felices con una donación. O puede enviar bitcoin o donar a través de Bountysource o Liberapay. Para obtener más información sobre como donar a NewPipe, por favor visita nuestro [sitio web](https://newpipe.net/donate).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Política de privacidad
El proyecto NewPipe tiene como objetivo proveer una experience privada y anónima para usar servicios de medios web.
Por lo tanto, la app no colecciona ningunos datos sin su consentimiento. La politica de privacidad de NewPipe explica en detalle los datos enviados y almacenados cuando envia un informe de error, o comentario en nuestro blog. Puede encontrar el documento [aqui](https://newpipe.net/legal/privacy/).
## Licencia
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe es Software Libre: Puede usar, estudiar, compartir, y mejorarlo a su voluntad. Especificamente puede redistribuir y/o modificarlo bajo los términos de la [GNU General Public License](https://www.gnu.org/licenses/gpl.html) como publicado por la Free Software Foundation, o versión 3 de la licencia, o (en su opción) cualquier versión posterior.

View File

@@ -2,15 +2,14 @@
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">自由で軽量な Android 向けストリーミングフロントエンド</h4>
<!-- F-Droid is extremely out of date, so we hide this for now. -->
<!-- <p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-ja.svg"></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-ja.svg"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub リリース"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg"></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="ライセンス: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="ビルド状態"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="翻訳状態"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC チャンネル: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://web.libera.chat/#newpipe" alt="IRC チャンネル: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource 寄付"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
@@ -18,7 +17,7 @@
<p align="center"><a href="https://newpipe.net">ウェブサイト</a> &bull; <a href="https://newpipe.net/blog/">ブログ</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">ニュース</a></p>
<hr>
*他の言語で読む: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md), [日本語](README.ja.md), [Română](README.ro.md) *
*他の言語で読む: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md)*
<b>注意: これはベータ版のため、バグが発生する可能性があります。もしバグが発生した場合、GitHub のリポジトリで Issue を開いてください。</b>
@@ -84,6 +83,7 @@ NewPipe は複数のサービスに対応しています。[ドキュメント](
* SoundCloud \[ベータ\]
* media.ccc.de \[ベータ\]
* PeerTube インスタンス \[ベータ\]
* Bandcamp \[ベータ\]
<!-- Hidden span to keep old links compatible. -->
<span id="updates"></span>
@@ -143,7 +143,7 @@ NewPipe プロジェクトはメディアウェブサービスを使用する上
<span id="license"></span>
## ライセンス
[![GNU GPLv3 のロゴ](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
[![GNU GPLv3 のロゴ](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe はフリーソフトウェアなので、あなたはあなたの望むように使用、習得、共有、改善を行えます。
具体的には、フリーソフトウェア財団により公開された [GNU General Public License](https://www.gnu.org/licenses/gpl.html) のバージョン3のライセンスもしくは、(あなたの選択で) いずれかの後継バージョンの規約の元で配布または改変を行うことができます。

View File

@@ -1,14 +1,15 @@
<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>
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></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-ko.svg"></a></p>
<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://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://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" 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://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
@@ -16,7 +17,7 @@
<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>
*Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md).*
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
<b>경고: 이 버전은 베타 버전이므로, 버그가 발생할 수도 있습니다. 만약 버그가 발생하였다면, 우리의 GITHUB 저장소에서 ISSUE를 열람하여 주십시오.</b>
@@ -79,6 +80,7 @@ NewPipe는 여러가지 서비스를 지원합니다. 우리의 [문서](https:/
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
* Bandcamp \[beta\]
## Updates
NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로 인해), 결국 릴리즈가 발생할 것입니다. 이것들의 형식은 x.xx.x 입니다.
@@ -137,7 +139,7 @@ NewPipe 프로젝트는 미디어 웹 서비스를 사용하는 것에 대한
그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.net/legal/privacy/)에서 확인할 수 있습니다.
## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe는 자유 소프트웨어입니다: 당신의 마음대로 이것을 사용하고, 연구하고, 공유하고, 개선할 수 있습니다.
구체적으로 당신은 자유 소프트웨어 재단에서 발행되는, 버전 3 또는 (당신의 선택에 따라)이후 버전의,

View File

@@ -2,15 +2,14 @@
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
<!-- F-Droid is extremely out of date, so we hide this for now. -->
<!-- <p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p> -->
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<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://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://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" 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://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
@@ -18,7 +17,7 @@
<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>
*Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md),[Română](README.ro.md) .*
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
@@ -46,6 +45,7 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit
### Features
* Search videos
* No Login Required
* Display general info about videos
* Watch YouTube videos
* Listen to YouTube videos
@@ -81,20 +81,21 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
* Bandcamp \[beta\]
<!-- Hidden span to keep old links compatible. -->
<span id="updates"></span>
## Installation and updates
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.schabi.org/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.
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, then push the update to users. (**IMPORTANT**: as of the time of writing, an issue is preventing releases later than 0.20.1 from being published. Thus, till this issue is solved, if you want to use F-Droid, we recommend method 1.)
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, 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.
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each 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.
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the 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
2. Uninstall NewPipe
3. Download the APK from the new source and install it
@@ -137,7 +138,7 @@ The NewPipe project aims to provide a private, anonymous experience for using me
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe is Free Software: You can use, study share and improve it at your
will. Specifically you can redistribute and/or modify it under the terms of the

View File

@@ -1,14 +1,16 @@
<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>
<h4 align="center">Uma interface de streaming leve e gratuita para Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></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-pt-br.svg"></a></p>
<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://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://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" 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://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
@@ -16,7 +18,7 @@
<p align="center"><a href="https://newpipe.net">Site</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>
*Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md).*
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
<b>AVISO: ESTA É UMA VERSÃO BETA, PORTANTO, VOCÊ PODE ENCONTRAR BUGS. ENCONTROU ALGUM, ABRA UM ISSUE ATRAVÉS DO NOSSO REPOSITÓRIO GITHUB.</b>
@@ -79,6 +81,7 @@ O NewPipe suporta vários serviços. Nosso [documentação](https://teamnewpipe.
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
* Bandcamp \[beta\]
## Atualizações
Quando uma alteração no código NewPipe (devido à adição de recursos ou fixação de bugs), eventualmente ocorrerá uma versão. Estes estão no formato x.xx.x . A fim de obter esta nova versão, você pode:
@@ -90,7 +93,7 @@ Quando uma alteração no código NewPipe (devido à adição de recursos ou fix
Recomendamos o método 2 para a maioria dos usuários. Os APKs instalados usando o método 2 ou 3 são compatíveis entre si, mas não com aqueles instalados usando o método 4. Isso se deve à mesma chave de assinatura (nossa) sendo usada para 2 e 3, mas uma chave de assinatura diferente (F-Droid's) está sendo usada para 4. Construir um APK depuração usando o método 1 exclui totalmente uma chave. Assinar chaves ajudam a garantir que um usuário não seja enganado para instalar uma atualização maliciosa em um aplicativo.
Enquanto isso, se você quiser trocar de fontes por algum motivo (por exemplo, a funcionalidade principal do NewPipe foi quebrada e o F-Droid ainda não tem a atualização), recomendamos seguir este procedimento:
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlistsFaça backup de seus dados através de Configurações > Conteúdo > Exportar Base de Dados para que você mantenha seu histórico, inscrições e playlists
1. Faça backup de seus dados através de Configurações > Conteúdo > Exportar Base de Dados para que você mantenha seu histórico, inscrições e playlists
2. Desinstale o NewPipe
3. Baixe o APK da nova fonte e instale-o
4. Importe os dados da etapa 1 via Configurações > Conteúdo > Inportar Banco de Dados
@@ -132,7 +135,7 @@ O projeto NewPipe tem como objetivo proporcionar uma experiência privada e anô
Portanto, o aplicativo não coleta nenhum dado sem o seu consentimento. A política de privacidade da NewPipe explica em detalhes quais dados são enviados e armazenados quando você envia um relatório de erro ou comenta em nosso blog. Você pode encontrar o documento [aqui](https://newpipe.net/legal/privacy/).
## Licença
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe é Software Livre: Você pode usar, estudar compartilhamento e melhorá-lo à sua vontade.
Especificamente, você pode redistribuir e/ou modificá-lo sob os termos do

View File

@@ -2,15 +2,14 @@
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">Un front-end de streaming „uşor” liber, pentru Android.</h4>
<!-- F-Droid is extremely out of date, so we hide this for now. -->
<!-- <p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></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-ro.svg"></a></p>
<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://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://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" 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://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
@@ -18,7 +17,7 @@
<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/">Presă</a></p>
<hr>
*Citiţi în alte limbi: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md)*
*Citiţi în alte limbi: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
<b>Atenţionare: ACEASTA ESTE O VERSIUNE BETA, AŞA CĂ S-AR PUTE SĂ ÎNTÂLNIŢI ERORI. DACĂ SE ÎNTÂMPLĂ ACEST LUCRU, DESCHIDEŢI UN ISSUE PRIN REPSITORY-UL NOSTRU GITHUB.</b>
@@ -46,6 +45,7 @@ NewPipe nu foloseşte nici-o bibliotecă Google framework sau API-ul Youtube. We
### Funcţii
* Căutarea videoclipurilor
* Nu este necesară logarea
* Afişarea informaţiilor generale despre videoclipuri
* Urmărirea videoclipurilor Youtube
* Ascultarea videoclipurilor Youtube
@@ -81,15 +81,16 @@ NewPipe suportă servicii multiple. [Documentele](https://teamnewpipe.github.io/
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* Instanţe PeerTube \[beta\]
* Bandcamp \[beta\]
<!-- Hidden span to keep old links compatible. -->
<span id="updates"></span>
## Instalare şi actualizări
Puteţi instala NewPipe folosind una dintre următoarele metode:
1. Adăugaţi depozitul nostru F-droid personalizat. Instrucţiunile sunt aici: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
1. Adăugaţi depozitul nostru F-droid personalizat. Instrucţiunile sunt aici: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
2. Descărcaţi APK-ul din [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) şi instalaţi-l.
3. Actualizaţi via F-Droid. Aceasta este cea mai lentă metodă de a obţine actualizări, deoarece F-Droid trebuie să recunoască schimbările, să constriască APK-ul, să îl semneze, iar apoi să îl trimită utilizatorilor. (**IMPORTANT**: în momentul scrierii, o problemă împiedică versiunile mai noi de 0.20.1 să fie publicate. Aşa că, dacă doriţi să folosiţi F-droid, până această problemă este rezolvată, vă recomandăm metoda 1.)
3. Actualizaţi via F-Droid. Aceasta este cea mai lentă metodă de a obţine actualizări, deoarece F-Droid trebuie să recunoască schimbările, să constriască APK-ul, să îl semneze, iar apoi să îl trimită utilizatorilor.
4. Construiţi un APK de depanare. Aceasta este cea mai rapidă metodă de a primi funcţii noi, dar este mult mai complicată, aşa că vă recomandăm să folosiţi una dintre celelalte metode.
Recomandăm metoda 1 pentru majoritatea utilizatorilor. APK-urile din metodele 1 şi 2 suntcompatibile una cu cealaltă, dar nu cu cele din metoda 3. Acest lucru se datorează faptului că aceeași cheie de semnare (a noastră) este utilizată pentru 1 și 2, dar o altă cheie de semnare (F-Droid) este utilizată pentru 3. Construirea unui APK de depanare folosind metoda 4 exclude o cheie în întregime. Cheile de semnare vă asigură că un utilizator nu este păcălit să instaleze o actualizare rău intenționată a unei aplicații.
@@ -137,7 +138,7 @@ Proiectul NewPipe îşi propune să furnizeze o experienţă privată şi anonim
Prin urmare, aplicaţia nu colectează niciun fel de informaţii fără acordul dumneavoastră. Politica de confidențialitate a NewPipe explică în detaliu ce date sunt trimise și stocate atunci când trimiteți un raport de blocare sau comentați pe blogul nostru. Puteți găsi documentul [aici](https://newpipe.net/legal/privacy/).
## Licenţă
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe este Software Gratuit: Îl puteţi folosi şi împărtăşi cum doriţi. Mai exact, îl puteți redistribui și / sau modifica în conformitate cu termenii
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) aşa cum a fost publicat de Free Software Foundation, fie versiunea 3 a Licenței, fie

View File

@@ -1,14 +1,15 @@
<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>
<h4 align="center">App bilaash ah oo fudud looguna talagalay in Android-ka wax loogu daawado.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></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-so.svg"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="Siidaynta GitHub "><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="Laysinka: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Darajada Dhismaha"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Heerka Turjimaada"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="Kanaalka IRC: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://web.libera.chat/#newpipe" alt="Kanaalka IRC: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Kuwa Bountysource "><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
@@ -16,7 +17,7 @@
<p align="center"><a href="https://newpipe.net">Website-ka</a> &bull; <a href="https://newpipe.net/blog/">Maqaalada</a> &bull; <a href="https://newpipe.net/FAQ/">Su'aalaha Aalaa La-iswaydiiyo</a> &bull; <a href="https://newpipe.net/press/">Warbaahinta</a></p>
<hr>
*Ku akhri luuqad kale: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md).*
*Ku akhri luuqad kale: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
<b>DIGNIIN: MIDKAN, NOOCA APP-KA EE HADDA WALI TIJAABO AYUU KU JIRAA, SIDAA DARTEED CILLADO AYAAD LA KULMI KARTAA. HADAAD LA KULANTO, KA FUR ARIN SHARAXAYA QAYBTANADA ARRIMAHA EE GITHUB-KA.</b>
@@ -79,6 +80,7 @@ NewPipe wuxuu taageeraa adeegyo badan. [warqadan](https://teamnewpipe.github.io/
* SoundCloud \[tijaabo\]
* media.ccc.de \[tijaabo\]
* PeerTube instances \[tijaabo\]
* Bandcamp \[tijaabo\]
## Kushubida iyo cusboonaysiinta
Marka koodhka NewPipe isbadal ku dhaco (wax cusub oo lagusoo kordhiyay ama cilad bixin), ugu dambayn waxaa lasii daayaa mid cusub (Siidayn). Siidaynta qaabkeedu waa x.xx.x . Si aad midka cusub u hesho, waxaad samayn kartaa:
@@ -131,6 +133,6 @@ Mashruuca NewPipe waxay ujeedadiisu tahay inuu bixiyo wax kuu gaar ah, oo adoon
Sidaa darteed, app-ku wax xog ah ma uruuriyo fasaxaaga la'aantii. Siyaasada Sirdhawrka NewPipe ayaa si faahfaahsan u sharaxda waxii xog ah ee la diro markaad cillad wariso, ama aad bogganaga faallo ka dhiibato. Warqada waxaad ka heli kartaa [halkan](https://newpipe.net/legal/privacy/).
## Laysinka
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe waa barnaamij bilaash ah oon lahayn xuquuqda daabacaada: Waad isticmaali kartaa, waad wadaagi kartaa waadna hormarin kartaa hadaad rabto. Gaar ahaan waad sii daabici kartaa ama wax baad ka badali kartaa ayadoo la raacayo shuruudaha sharciga guud ee [GNU](https://www.gnu.org/licenses/gpl.html) sida ay soosaareen Ururka Barnaamijyada Bilaashka ah, soosaarista 3aad ee laysinka, ama (hadaad doonto) nooc walba oo kasii dambeeyay laysinkii 3aad.

145
README.tr.md Normal file
View File

@@ -0,0 +1,145 @@
<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>
<h4 align="center">Android için hafif ve özgür bir akış arayüzü.</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-tr.svg"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub sürümleri"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="Lisans: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Derleme Durumu"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Çeviri Durumu"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="https://web.libera.chat/#newpipe" alt="IRC kanalı: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource ödülleri"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#ekran-fotoğrafları">Ekran fotoğrafları</a> &bull; <a href="#açıklama">Açıklama</a> &bull; <a href="#özellikler">Özellikler</a> &bull; <a href="#kurulum-ve-güncellemeler">Kurulum ve güncellemeler</a> &bull; <a href="#katkıda-bulunma">Katkıda bulunma</a> &bull; <a href="#bağış">Bağış</a> &bull; <a href="#lisans">Lisans</a></p>
<p align="center"><a href="https://newpipe.net">Web sitesi</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">SSS</a> &bull; <a href="https://newpipe.net/press/">Basın</a></p>
<hr>
*Bu sayfayı diğer dillerde okuyun: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
<b>UYARI: BU SÜRÜM BETA SÜRÜMÜDÜR, BU NEDENLE HATALARLA KARŞILAŞABİLİRSİNİZ. HATA BULURSANIZ BU GITHUB DEPOSUNDA BUNU BİLDİRİN.</b>
<b>GOOGLE PLAY STORE'A NEWPIPE VEYA BAŞKA BİR KOPYASINI KOYMAK, PLAY STORE ŞARTLARINI VE KOŞULLARINI İHLAL EDER.</b>
## Ekran fotoğrafları
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
## Açıklama
NewPipe herhangi bir Google çerçeve kütüphanesini, ya da YouTube API hizmetlerini kullanmaz. Gerekli web hizmetleri yalnızca gerekli bilgileri almak için kaynak olarak kullanılır, bu nedenle bu uygulama Google hizmetleri yüklü olmayan cihazlarda da kullanılabilir. Ayrıca, copyleft özgür yazılımı olan NewPipe'ı kullanmak için bir YouTube hesabına ihtiyacınız yoktur.
### Özellikler
* Video arama
* Videolar hakkında genel bilgileri görüntüleme
* YouTube videoları izleme
* YouTube videolarını dinleme
* Pop-up modu (hareketli oynatıcı)
* Video izlemek için akış oynatıcısını seçme
* Video indirme
* Sadece ses indirme
* Videoyu Kodi'de açma
* Sonraki video/ilgili videolar
* YouTube'u belirli bir dilde arayın
* Yaş sınırlı içeriği izleme/engelleme
* Kanallar hakkındaki genel bilgileri görüntüleme
* Kanal arama
* Bir kanaldaki videoları izleme
* Orbot/Tor desteği (henüz direkt olarak değil)
* 1080p/2K/4K desteği
* Geçmişi görme
* Kanallara abone olma
* Geçmişte arama
* Oynatma listesi arama/oynatma
* Çalma listelerini sıralayıp oynatın
* Videoları sırayla oynatın
* Yerel oynatma listeleri
* Altyazılar
* Canlı yayın desteği
* Yorumları görme
### Desteklenen servisler
NewPipe birden fazla hizmeti destekler. Uygulamaya ve ayıklayıcıya yeni bir hizmet ekleme konusunda daha fazla bilgiye [kılavuzlarımızdan](https://teamnewpipe.github.io/documentation/) ulaşabilirsiniz. Yeni bir hizmet eklemek istiyorsanız lütfen bizimle iletişime geçin. Şu anda desteklenen hizmetler şunlardır:
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube \[beta\]
* Bandcamp \[beta\]
<!-- Eski bağlantıları uyumlu tutmak için gizli span. -->
<span id="updates"></span>
## Kurulum ve güncellemeler
Aşağıdaki yöntemlerden birini kullanarak NewPipe'ı kurabilirsiniz:
1. Özel depomuzu F-Droid'e ekleyin ve oradan yükleyin. Kılavuzu şurada bulabilirsiniz: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
2. APK'yı [Github sürümlerinden](https://github.com/TeamNewPipe/NewPipe/releases) indirin ve kurun.
3. F-Droid ile güncelleyin. Bu, güncellemeleri almanın en yavaş yöntemidir, çünkü F-Droid değişiklikleri tanımalı, APK'yı kendisi oluşturmalı, imzalamalı ve ardından güncellemeyi kullanıcılara dağıtmalıdır.
4. Kendiniz bir APK derleyin. Bu yöntem, cihazınızda yeni özellikler edinmenin en hızlı yoludur, ancak çok daha karmaşıktır, bu nedenle diğer yöntemlerden birini kullanmanızı öneririz.
Çoğu kullanıcı için yöntem 1'i öneririz. Yöntem 1 veya 2 kullanılarak yüklenen APK'lar birbiriyle uyumludur, ancak yöntem 3 kullanılarak yüklenenlerle uyumlu değildir. Bu durum, 1 ve 2 için kullanılan aynı imzalama anahtarıın (bizim anahtarımız) 3 için kullanılan imzalama anahtarından (F-Droid'in anahtarı) farklı olmasından kaynaklanmaktadır. Yöntem 4 kullanılarak oluşturulan deneysel APK'larda anahtar yoktur. İmzalama anahtarları, bir kullanıcının bir uygulamaya kötü amaçlı bir güncelleme yüklemek için kandırılmadığından emin olmanıza yardımcı olur.
Bu arada, herhangi bir nedenle kaynakları değiştirmek istiyorsanız (örneğin, NewPipe'ın temel bir işlevi bozuldu ve F-Droid tarafında henüz bir güncelleme yayınlanmadı), bu prosedürü izlemenizi öneririz:
1. Verilerinizi yedekleyin. `NewPipe Ayarları > İçerik > Veritabanını dışa aktar` seçeneklerini izleyerek aboneliklerinizi, oynatma listelerinizi ve geçmişinizi yedekleyin.
2. NewPipe'ı kaldırın
3. APK dosyasını yeni bir kaynaktan indirin ve yükleyin
4. `Ayarlar > İçerik > Veritabanını içe aktar` seçeneklerini izleyerek 1. adımdaki verileri içe aktarın
## Katkıda bulunma
Fikirleriniz, çevirileriniz, tasarım değişiklikleriniz, kod temizlemeniz veya ağır kod değişiklikleriniz olsun, yardımınıza her zaman açığız.
Yapılan her değişiklikle NewPipe daha da iyi bir konuma geliyor!
Eğer yer almak istiyorsanız, [katkı sağlayanlar için hazırladığımız notları](.github/CONTRIBUTING.md) kontrol edin.
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Çeviri istatistikleri" />
</a>
## Bağış
NewPipe'ı beğendiyseniz, yapacağınız bağışlar bizi motive eder. Bitcoin gönderebilir veya Bountysource veya Liberapay aracılığıyla bağış yapabilirsiniz. NewPipe'a bağış yapma hakkında daha fazla bilgi için lütfen [web sitemizi](https://newpipe.net/donate) ziyaret edin.
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR kodu" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="liberapay.com üzerinde NewPipe'ı ziyaret edin" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Liberapay aracılığıyla bağış yapın" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="bountysource.com üzerinde NewPipe'ı ziyaret edin" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Ne kadar ödül kazanabileceğinizi kontrol edin."></a></td>
</tr>
</table>
## Gizlilik politikası
NewPipe projesi, çevrimiçi akış hizmetlerini kullanmak için özel, özgür ve anonim bir deneyim sunmayı amaçlamaktadır.
Bu doğrultuda, uygulama sizin izniniz olmadan herhangi bir veri toplamaz. NewPipe'ın Gizlilik Politikası, bir çökme raporu gönderdiğinizde veya blogumuzda yorum yaptığınızda hangi verilerin gönderildiğini ve saklandığını ayrıntılı olarak açıklar. İlgili belgeyi [burada](https://newpipe.net/legal/privacy/) bulabilirsiniz.
## Lisans
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe özgür bir yazılımdır. Kendi başınıza kullanabilir, öğrenebilir, paylaşabilir
ve geliştirebilirsiniz. Free Software Foundation tarafından yayınlanan GNU Genel Kamu Lisansı,
Lisansın 3. sürümü veya (isteğe bağlı olarak) daha sonraki bir sürümü şartları ve
koşulları altında yeniden dağıtabilir ve/veya değiştirebilirsiniz.

View File

@@ -1,3 +1,7 @@
plugins {
id "org.sonarqube" version "3.1.1"
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
@@ -13,8 +17,8 @@ android {
resValue "string", "app_name", "NewPipe"
minSdkVersion 19
targetSdkVersion 29
versionCode 965
versionName "0.20.11"
versionCode 973
versionName "0.21.7"
multiDexEnabled true
@@ -62,6 +66,9 @@ android {
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
abortOnError false
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
// 5.0, avoid using them in switch case statements"), which affects only library projects
disable 'NonConstantResourceId'
}
compileOptions {
@@ -92,16 +99,19 @@ android {
}
ext {
icepickVersion = '3.2.0'
checkstyleVersion = '8.38'
stethoVersion = '1.5.1'
leakCanaryVersion = '2.5'
exoPlayerVersion = '2.11.8'
androidxLifecycleVersion = '2.2.0'
androidxRoomVersion = '2.3.0-alpha03'
androidxRoomVersion = '2.3.0'
icepickVersion = '3.2.0'
exoPlayerVersion = '2.12.3'
googleAutoServiceVersion = '1.0-rc7'
groupieVersion = '2.8.1'
markwonVersion = '4.6.0'
googleAutoServiceVersion = '1.0-rc7'
leakCanaryVersion = '2.5'
stethoVersion = '1.5.1'
mockitoVersion = '3.6.0'
}
@@ -111,7 +121,7 @@ configurations {
}
checkstyle {
configFile rootProject.file('checkstyle.xml')
configDir rootProject.file(".")
ignoreFailures false
showViolations true
toolVersion = checkstyleVersion
@@ -158,84 +168,113 @@ afterEvaluate {
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
sonarqube {
properties {
property "sonar.projectKey", "TeamNewPipe_NewPipe"
property "sonar.organization", "teamnewpipe"
property "sonar.host.url", "https://sonarcloud.io"
}
}
dependencies {
/** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
/** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle
// 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
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.6'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
ktlint 'com.pinterest:ktlint:0.40.0'
/** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
implementation "frankiesardo:icepick:${icepickVersion}"
kapt "frankiesardo:icepick-processor:${icepickVersion}"
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
ktlint "com.pinterest:ktlint:0.40.0"
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
implementation "androidx.multidex:multidex:2.0.1"
// NewPipe dependencies
// You can use a local version by uncommenting a few lines in settings.gradle
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.20.11'
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
implementation "org.jsoup:jsoup:1.13.1"
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
implementation "com.squareup.okhttp3:okhttp:3.12.13"
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
implementation "com.google.android.material:material:1.2.1"
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
/** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.media:media:1.2.1'
implementation 'androidx.webkit:webkit:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.3.5'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.media:media:1.2.1'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.webkit:webkit:1.4.0'
implementation 'com.google.android.material:material:1.2.1'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
/** Third-party libraries **/
// Instance state boilerplate elimination
implementation "frankiesardo:icepick:${icepickVersion}"
kapt "frankiesardo:icepick-processor:${icepickVersion}"
// HTML parser
implementation "org.jsoup:jsoup:1.13.1"
// HTTP client
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
implementation "com.squareup.okhttp3:okhttp:3.12.13"
// Media player
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
// Metadata generator for service descriptors
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
// Manager for complex RecyclerView layouts
implementation "com.xwray:groupie:${groupieVersion}"
implementation "com.xwray:groupie-viewbinding:${groupieVersion}"
// Circular ImageView
implementation "de.hdodenhof:circleimageview:3.1.0"
// Image loading
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
// Markdown library for Android
implementation "io.noties.markwon:core:${markwonVersion}"
implementation "io.noties.markwon:linkify:${markwonVersion}"
// File picker
implementation "com.nononsenseapps:filepicker:4.2.1"
// Crash reporting
implementation "ch.acra:acra-core:5.7.0"
// Reactive extensions for Java VM
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
// RxJava binding APIs for Android UI widgets
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final"
// Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.1.Final"
testImplementation 'junit:junit:4.13.1'
/** Debugging **/
// Memory leak detection
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
// Debug bridge for Android
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
/** Testing **/
testImplementation 'junit:junit:4.13.2'
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"

View File

@@ -0,0 +1,46 @@
package org.schabi.newpipe.error;
import android.os.Parcel;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Instrumented tests for {@link ErrorInfo}.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ErrorInfoTest {
@Test
public void errorInfoTestParcelable() {
final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
// Obtain a Parcel object and write the parcelable object to it:
final Parcel parcel = Parcel.obtain();
info.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
final ErrorInfo infoFromParcel = (ErrorInfo) ErrorInfo.CREATOR.createFromParcel(parcel);
assertTrue(Arrays.toString(infoFromParcel.getStackTraces())
.contains(ErrorInfoTest.class.getSimpleName()));
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
infoFromParcel.getServiceName());
assertEquals("request", infoFromParcel.getRequest());
assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId());
parcel.recycle();
}
}

View File

@@ -1,38 +0,0 @@
package org.schabi.newpipe.report;
import android.os.Parcel;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.schabi.newpipe.R;
import static org.junit.Assert.assertEquals;
/**
* Instrumented tests for {@link ErrorInfo}.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ErrorInfoTest {
@Test
public void errorInfoTestParcelable() {
final ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request",
R.string.general_error);
// Obtain a Parcel object and write the parcelable object to it:
final Parcel parcel = Parcel.obtain();
info.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
final ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
assertEquals("youtube", infoFromParcel.getServiceName());
assertEquals("request", infoFromParcel.getRequest());
assertEquals(R.string.general_error, infoFromParcel.getMessage());
parcel.recycle();
}
}

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.settings;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import org.schabi.newpipe.R;
@@ -11,8 +10,8 @@ import leakcanary.LeakCanary;
public class DebugSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.debug_settings);
final Preference showMemoryLeaksPreference
= findPreference(getString(R.string.show_memory_leaks_key));
@@ -31,9 +30,4 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
throw new RuntimeException();
});
}
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.debug_settings);
}
}

View File

@@ -6,50 +6,50 @@
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.VideoAudioSettingsFragment"
android:icon="?attr/ic_headset"
android:icon="@drawable/ic_headset"
android:title="@string/settings_category_video_audio_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.DownloadSettingsFragment"
android:icon="?attr/ic_file_download"
android:icon="@drawable/ic_file_download"
android:title="@string/settings_category_downloads_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.AppearanceSettingsFragment"
android:icon="?attr/ic_palette"
android:icon="@drawable/ic_palette"
android:title="@string/settings_category_appearance_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.HistorySettingsFragment"
android:icon="?attr/ic_history"
android:icon="@drawable/ic_history"
android:title="@string/settings_category_history_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.ContentSettingsFragment"
android:icon="?attr/ic_language"
android:icon="@drawable/ic_language"
android:title="@string/content"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.NotificationSettingsFragment"
android:icon="?attr/ic_play_arrow"
android:icon="@drawable/ic_play_arrow"
android:title="@string/settings_category_notification_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment"
android:icon="?attr/ic_settings_update"
android:icon="@drawable/ic_cloud_download"
android:key="update_pref_screen_key"
android:title="@string/settings_category_updates_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.DebugSettingsFragment"
android:icon="?attr/ic_bug_report"
android:icon="@drawable/ic_bug_report"
android:key="@string/debug_pref_screen_key"
android:title="@string/settings_category_debug_title"
app:iconSpaceReserved="false" />

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.schabi.newpipe">
package="org.schabi.newpipe"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -21,7 +22,6 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:logo="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
android:theme="@style/OpeningTheme"
android:resizeableActivity="true"
tools:ignore="AllowBackup">
@@ -85,7 +85,7 @@
android:name=".ExitActivity"
android:label="@string/general_error"
android:theme="@android:style/Theme.NoDisplay" />
<activity android:name=".report.ErrorActivity" />
<activity android:name=".error.ErrorActivity" />
<!-- giga get related -->
<activity
@@ -106,7 +106,7 @@
</activity>
<activity
android:name=".ReCaptchaActivity"
android:name=".error.ReCaptchaActivity"
android:label="@string/recaptcha" />
<provider
@@ -231,11 +231,10 @@
<data android:host="invidious.snopyta.org" />
<data android:host="yewtu.be" />
<data android:host="tube.connect.cafe" />
<data android:host="invidious.zapashcanon.fr" />
<data android:host="invidious.kavin.rocks" />
<data android:host="invidious.tube" />
<data android:host="invidious-us.kavin.rocks" />
<data android:host="piped.kavin.rocks" />
<data android:host="invidious.site" />
<data android:host="invidious.xyz" />
<data android:host="vid.mint.lgbt" />
<data android:host="invidiou.site" />
<data android:host="invidious.fdn.fr" />
@@ -243,6 +242,15 @@
<data android:host="invidious.zee.li" />
<data android:host="vid.puffyan.us" />
<data android:host="ytprivate.com" />
<data android:host="invidious.namazso.eu" />
<data android:host="invidious.silkky.cloud" />
<data android:host="invidious.exonip.de" />
<data android:host="inv.riverside.rocks" />
<data android:host="invidious.blamefran.net" />
<data android:host="invidious.moomoo.me" />
<data android:host="ytb.trom.tf" />
<data android:host="yt.cyberhost.uk" />
<data android:host="y.com.cm" />
<data android:pathPrefix="/" />
</intent-filter>
@@ -317,6 +325,35 @@
<data android:pathPrefix="/accounts/" />
<data android:pathPrefix="/video-channels/" />
</intent-filter>
<!-- Bandcamp filter for tracks, albums and playlists -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="*.bandcamp.com"/>
</intent-filter>
<!-- Bandcamp filter for radio -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:sspPattern="bandcamp.com/?show=*"/>
</intent-filter>
</activity>
<service
android:name=".RouterActivity$FetcherService"

View File

@@ -63,8 +63,9 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
return consumed == dy;
}
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
final MotionEvent ev) {
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final AppBarLayout child,
@NonNull final MotionEvent ev) {
for (final Integer element : skipInterceptionOfElements) {
final View view = child.findViewById(element);
if (view != null) {

View File

@@ -1,47 +0,0 @@
package org.schabi.newpipe;
/*
* Created by Christian Schabesberger on 24.12.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* ActivityCommunicator.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/>.
*/
/**
* Singleton:
* Used to send data between certain Activity/Services within the same process.
* This can be considered as an ugly hack inside the Android universe.
**/
public class ActivityCommunicator {
private static ActivityCommunicator activityCommunicator;
private volatile Class returnActivity;
public static ActivityCommunicator getCommunicator() {
if (activityCommunicator == null) {
activityCommunicator = new ActivityCommunicator();
}
return activityCommunicator;
}
public Class getReturnActivity() {
return returnActivity;
}
public void setReturnActivity(final Class returnActivity) {
this.returnActivity = returnActivity;
}
}

View File

@@ -20,13 +20,14 @@ import org.acra.ACRA;
import org.acra.config.ACRAConfigurationException;
import org.acra.config.CoreConfiguration;
import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
@@ -90,7 +91,7 @@ public class App extends MultiDexApplication {
app = this;
// Initialize settings first because others inits can use its values
SettingsActivity.initSettings(this);
NewPipeSettings.initSettings(this);
NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this),
@@ -129,7 +130,7 @@ public class App extends MultiDexApplication {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, ""));
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
}
@@ -224,14 +225,10 @@ public class App extends MultiDexApplication {
.setBuildConfigClass(BuildConfig.class)
.build();
ACRA.init(this, acraConfig);
} catch (final ACRAConfigurationException ace) {
ace.printStackTrace();
ErrorActivity.reportError(this,
ace,
null,
null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not initialize ACRA crash report", R.string.app_ui_crash));
} catch (final ACRAConfigurationException exception) {
exception.printStackTrace();
ErrorActivity.reportError(this, new ErrorInfo(exception,
UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report"));
}
}

View File

@@ -10,19 +10,22 @@ import android.content.pm.Signature;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.MessageDigest;
@@ -31,9 +34,11 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class CheckForNewAppVersion {
private CheckForNewAppVersion() { }
@@ -58,9 +63,8 @@ public final class CheckForNewAppVersion {
packageInfo = application.getPackageManager().getPackageInfo(
application.getPackageName(), PackageManager.GET_SIGNATURES);
} catch (final PackageManager.NameNotFoundException e) {
ErrorActivity.reportError(application, e, null, null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not find package info", R.string.app_ui_crash));
ErrorActivity.reportError(application, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
return "";
}
@@ -72,9 +76,8 @@ public final class CheckForNewAppVersion {
final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input);
} catch (final CertificateException e) {
ErrorActivity.reportError(application, e, null, null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Certificate error", R.string.app_ui_crash));
ErrorActivity.reportError(application, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
return "";
}
@@ -83,9 +86,8 @@ public final class CheckForNewAppVersion {
final byte[] publicKey = md.digest(c.getEncoded());
return byte2HexFormatted(publicKey);
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
ErrorActivity.reportError(application, e, null, null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not retrieve SHA1 key", R.string.app_ui_crash));
ErrorActivity.reportError(application, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
return "";
}
}
@@ -127,13 +129,8 @@ public final class CheckForNewAppVersion {
if (BuildConfig.VERSION_CODE < versionCode) {
// A pending intent to open the apk location url in the browser.
final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
final Intent intent = new Intent(Intent.ACTION_CHOOSER);
intent.putExtra(Intent.EXTRA_INTENT, viewIntent);
intent.putExtra(Intent.EXTRA_TITLE, R.string.open_with);
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final PendingIntent pendingIntent
= PendingIntent.getActivity(application, 0, intent, 0);

View File

@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response;
@@ -43,7 +44,7 @@ import static org.schabi.newpipe.MainActivity.DEBUG;
public final class DownloaderImpl extends Downloader {
public static final String USER_AGENT
= "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0";
= "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
= "youtube_restricted_mode_key";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";

View File

@@ -6,6 +6,8 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import org.schabi.newpipe.util.NavigationHelper;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* ExitActivity.java is part of NewPipe.
@@ -48,6 +50,6 @@ public class ExitActivity extends Activity {
finish();
}
System.exit(0);
NavigationHelper.restartApp(this);
}
}

View File

@@ -60,6 +60,7 @@ import org.schabi.newpipe.databinding.DrawerHeaderBinding;
import org.schabi.newpipe.databinding.DrawerLayoutBinding;
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@@ -72,7 +73,6 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KioskTranslator;
@@ -95,6 +95,7 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@SuppressWarnings("ConstantConditions")
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
private ActivityMainBinding mainBinding;
@@ -133,6 +134,8 @@ public class MainActivity extends AppCompatActivity {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
TLSSocketFactoryCompat.setAsDefault();
}
ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
assureCorrectAppLanguage(this);
@@ -153,7 +156,7 @@ public class MainActivity extends AppCompatActivity {
try {
setupDrawer();
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e);
}
if (DeviceUtils.isTv(this)) {
@@ -180,27 +183,27 @@ public class MainActivity extends AppCompatActivity {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
R.string.tab_subscriptions)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
.setIcon(R.drawable.ic_tv);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
.setIcon(R.drawable.ic_rss_feed);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
.setIcon(R.drawable.ic_bookmark);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
.setIcon(R.drawable.ic_file_download);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
.setIcon(R.drawable.ic_history);
//Settings and About
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
.setIcon(R.drawable.ic_settings);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
.setIcon(R.drawable.ic_info_outline);
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
@@ -238,7 +241,7 @@ public class MainActivity extends AppCompatActivity {
try {
tabSelected(item);
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e);
}
break;
case R.id.menu_options_about_group:
@@ -340,13 +343,13 @@ public class MainActivity extends AppCompatActivity {
try {
showTabs();
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e);
}
}
}
private void showServices() {
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up);
for (final StreamingService s : NewPipe.getServices()) {
final String title = s.getServiceInfo().getName()
@@ -412,7 +415,7 @@ public class MainActivity extends AppCompatActivity {
}
private void showTabs() throws ExtractionException {
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down);
//Tabs
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
@@ -430,27 +433,27 @@ public class MainActivity extends AppCompatActivity {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
.setIcon(R.drawable.ic_tv);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
.setIcon(R.drawable.ic_rss_feed);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
.setIcon(R.drawable.ic_bookmark);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
.setIcon(R.drawable.ic_file_download);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
.setIcon(R.drawable.ic_history);
//Settings and About
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
.setIcon(R.drawable.ic_settings);
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
.setIcon(R.drawable.ic_info_outline);
}
@Override
@@ -487,7 +490,7 @@ public class MainActivity extends AppCompatActivity {
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
getString(R.string.drawer_header_description) + selectedServiceName);
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up service toggle", e);
}
final SharedPreferences sharedPreferences
@@ -600,6 +603,7 @@ public class MainActivity extends AppCompatActivity {
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
for (final int i : grantResults) {
if (i == PackageManager.PERMISSION_DENIED) {
return;
@@ -679,19 +683,16 @@ public class MainActivity extends AppCompatActivity {
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (DEBUG) {
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
}
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
onHomeButtonPressed();
return true;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == android.R.id.home) {
onHomeButtonPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -799,7 +800,7 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
}
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiErrorInSnackbar(this, "Handling intent", e);
}
}

View File

@@ -51,4 +51,15 @@ public final class NewPipeDatabase {
throw new RuntimeException("Checkpoint was blocked from completing");
}
}
public static void close() {
if (databaseInstance != null) {
synchronized (NewPipeDatabase.class) {
if (databaseInstance != null) {
databaseInstance.close();
databaseInstance = null;
}
}
}
}
}

View File

@@ -33,15 +33,29 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.StreamingService.LinkType;
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.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.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
@@ -49,14 +63,13 @@ import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.urlfinder.UrlFinder;
import org.schabi.newpipe.views.FocusOverlayView;
@@ -78,19 +91,11 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
/**
* Get the url from the intent and open it in the chosen preferred player.
*/
public class RouterActivity extends AppCompatActivity {
public static final String INTERNAL_ROUTE_KEY = "internalRoute";
/**
* Removes invisible separators (\p{Z}) and punctuation characters including
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
* more details.
*/
private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
protected final CompositeDisposable disposables = new CompositeDisposable();
@State
protected int currentServiceId = -1;
@@ -100,9 +105,9 @@ public class RouterActivity extends AppCompatActivity {
protected int selectedRadioPosition = -1;
protected int selectedPreviously = -1;
protected String currentUrl;
protected boolean internalRoute = false;
private StreamingService currentService;
private boolean selectionIsDownload = false;
private AlertDialog alertDialogChoice = null;
@Override
protected void onCreate(final Bundle savedInstanceState) {
@@ -123,7 +128,16 @@ public class RouterActivity extends AppCompatActivity {
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
protected void onStop() {
super.onStop();
// we need to dismiss the dialog before leaving the activity or we get leaks
if (alertDialogChoice != null) {
alertDialogChoice.dismiss();
}
}
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
@@ -145,37 +159,79 @@ public class RouterActivity extends AppCompatActivity {
private void handleUrl(final String url) {
disposables.add(Observable
.fromCallable(() -> {
if (currentServiceId == -1) {
currentService = NewPipe.getServiceByUrl(url);
currentServiceId = currentService.getServiceId();
currentLinkType = currentService.getLinkTypeByUrl(url);
currentUrl = url;
} else {
currentService = NewPipe.getService(currentServiceId);
}
try {
if (currentServiceId == -1) {
currentService = NewPipe.getServiceByUrl(url);
currentServiceId = currentService.getServiceId();
currentLinkType = currentService.getLinkTypeByUrl(url);
currentUrl = url;
} else {
currentService = NewPipe.getService(currentServiceId);
}
return currentLinkType != LinkType.NONE;
// return whether the url was found to be supported or not
return currentLinkType != LinkType.NONE;
} catch (final ExtractionException e) {
// this can be reached only when the url is completely unsupported
return false;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result) {
.subscribe(isUrlSupported -> {
if (isUrlSupported) {
onSuccess();
} else {
showUnsupportedUrlDialog(url);
}
}, throwable -> handleError(throwable, url)));
}, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url))));
}
private void handleError(final Throwable throwable, final String url) {
throwable.printStackTrace();
/**
* @param context the context. It will be {@code finish()}ed at the end of the handling if it is
* an instance of {@link RouterActivity}.
* @param errorInfo the error information
*/
private static void handleError(final Context context, final ErrorInfo errorInfo) {
if (errorInfo.getThrowable() != null) {
errorInfo.getThrowable().printStackTrace();
}
if (throwable instanceof ExtractionException) {
showUnsupportedUrlDialog(url);
if (errorInfo.getThrowable() instanceof ReCaptchaException) {
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
final Intent intent = new Intent(context, ReCaptchaActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
} else if (errorInfo.getThrowable() != null
&& 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 {
ExtractorHelper.handleGeneralException(this, -1, url, throwable,
UserAction.SOMETHING_ELSE, null);
finish();
ErrorActivity.reportError(context, errorInfo);
}
if (context instanceof RouterActivity) {
((RouterActivity) context).finish();
}
}
@@ -184,7 +240,7 @@ public class RouterActivity extends AppCompatActivity {
new AlertDialog.Builder(context)
.setTitle(R.string.unsupported_url)
.setMessage(R.string.unsupported_url_dialog_message)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_share))
.setIcon(R.drawable.ic_share)
.setPositiveButton(R.string.open_in_browser,
(dialog, which) -> ShareUtils.openUrlInBrowser(this, url))
.setNegativeButton(R.string.share,
@@ -287,7 +343,7 @@ public class RouterActivity extends AppCompatActivity {
}
};
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapperContext)
alertDialogChoice = new AlertDialog.Builder(themeWrapperContext)
.setTitle(R.string.preferred_open_action_share_menu_title)
.setView(radioGroup)
.setCancelable(true)
@@ -301,12 +357,12 @@ public class RouterActivity extends AppCompatActivity {
.create();
//noinspection CodeBlock2Expr
alertDialog.setOnShowListener(dialog -> {
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
alertDialogChoice.setOnShowListener(dialog -> {
setDialogButtonsState(alertDialogChoice, radioGroup.getCheckedRadioButtonId() != -1);
});
radioGroup.setOnCheckedChangeListener((group, checkedId) ->
setDialogButtonsState(alertDialog, true));
setDialogButtonsState(alertDialogChoice, true));
final View.OnClickListener radioButtonsClickListener = v -> {
final int indexOfChild = radioGroup.indexOfChild(v);
if (indexOfChild == -1) {
@@ -326,7 +382,7 @@ public class RouterActivity extends AppCompatActivity {
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
radioButton.setText(item.description);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
AppCompatResources.getDrawable(getApplicationContext(), item.icon),
AppCompatResources.getDrawable(themeWrapperContext, item.icon),
null, null, null);
radioButton.setChecked(false);
radioButton.setId(id++);
@@ -356,10 +412,10 @@ public class RouterActivity extends AppCompatActivity {
}
selectedPreviously = selectedRadioPosition;
alertDialog.show();
alertDialogChoice.show();
if (DeviceUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(alertDialog);
FocusOverlayView.setupFocusObserver(alertDialogChoice);
}
}
@@ -380,16 +436,16 @@ public class RouterActivity extends AppCompatActivity {
final AdapterChoiceItem videoPlayer = new AdapterChoiceItem(
getString(R.string.video_player_key), getString(R.string.video_player),
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow));
R.drawable.ic_play_arrow);
final AdapterChoiceItem showInfo = new AdapterChoiceItem(
getString(R.string.show_info_key), getString(R.string.show_info),
resolveResourceIdFromAttr(context, R.attr.ic_info_outline));
R.drawable.ic_info_outline);
final AdapterChoiceItem popupPlayer = new AdapterChoiceItem(
getString(R.string.popup_player_key), getString(R.string.popup_player),
resolveResourceIdFromAttr(context, R.attr.ic_popup));
R.drawable.ic_picture_in_picture);
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
getString(R.string.background_player_key), getString(R.string.background_player),
resolveResourceIdFromAttr(context, R.attr.ic_headset));
R.drawable.ic_headset);
if (linkType == LinkType.STREAM) {
if (isExtVideoEnabled) {
@@ -420,6 +476,11 @@ public class RouterActivity extends AppCompatActivity {
if (capabilities.contains(AUDIO)) {
returnList.add(backgroundPlayer);
}
// download is redundant for linkType CHANNEL AND PLAYLIST (till playlist downloading is
// not supported )
returnList.add(new AdapterChoiceItem(getString(R.string.download_key),
getString(R.string.download),
R.drawable.ic_file_download));
} else {
returnList.add(showInfo);
@@ -432,10 +493,6 @@ public class RouterActivity extends AppCompatActivity {
}
}
returnList.add(new AdapterChoiceItem(getString(R.string.download_key),
getString(R.string.download),
resolveResourceIdFromAttr(context, R.attr.ic_file_download)));
return returnList;
}
@@ -500,7 +557,8 @@ public class RouterActivity extends AppCompatActivity {
.subscribe(intent -> {
startActivity(intent);
finish();
}, throwable -> handleError(throwable, currentUrl))
}, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl)))
);
return;
}
@@ -531,9 +589,9 @@ public class RouterActivity extends AppCompatActivity {
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(result.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setOnDismissListener(dialog -> finish());
downloadDialog.show(fm, "downloadDialog");
fm.executePendingTransactions();
downloadDialog.requireDialog().setOnDismissListener(dialog -> finish());
}, throwable ->
showUnsupportedUrlDialog(currentUrl)));
}
@@ -542,6 +600,7 @@ public class RouterActivity extends AppCompatActivity {
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
for (final int i : grantResults) {
if (i == PackageManager.PERMISSION_DENIED) {
finish();
@@ -580,6 +639,7 @@ public class RouterActivity extends AppCompatActivity {
this.playerChoice = playerChoice;
}
@NonNull
@Override
public String toString() {
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
@@ -646,9 +706,9 @@ public class RouterActivity extends AppCompatActivity {
if (fetcher != null) {
fetcher.dispose();
}
}, throwable -> ExtractorHelper.handleGeneralException(this,
choice.serviceId, choice.url, throwable, finalUserAction,
", opened with " + choice.playerChoice));
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
choice.url + " opened with " + choice.playerChoice,
choice.serviceId)));
}
}

View File

@@ -1,192 +0,0 @@
package org.schabi.newpipe.about;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.tabs.TabLayoutMediator;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityAboutBinding;
import org.schabi.newpipe.databinding.FragmentAboutBinding;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.ShareUtils.openUrlInBrowser;
public class AboutActivity extends AppCompatActivity {
/**
* List of all software components.
*/
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = {
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
"https://github.com/ACRA/acra", StandardLicenses.APACHE2),
new SoftwareComponent("AndroidX", "2005 - 2011", "The Android Open Source Project",
"https://developer.android.com/jetpack", StandardLicenses.APACHE2),
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
"https://github.com/hdodenhof/CircleImageView",
StandardLicenses.APACHE2),
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google, Inc.",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
new SoftwareComponent("GigaGet", "2014 - 2015", "Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3),
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
"https://github.com/lisawray/groupie", StandardLicenses.MIT),
new SoftwareComponent("Icepick", "2015", "Frankie Sardo",
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1),
new SoftwareComponent("Jsoup", "2009 - 2020", "Jonathan Hedley",
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
new SoftwareComponent("Markwon", "2019", "Dimitry Ivanov",
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
new SoftwareComponent("Material Components for Android", "2016 - 2020", "Google, Inc.",
"https://github.com/material-components/material-components-android",
StandardLicenses.APACHE2),
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
"https://github.com/spacecowboy/NoNonsense-FilePicker",
StandardLicenses.MPL2),
new SoftwareComponent("OkHttp", "2019", "Square, Inc.",
"https://square.github.io/okhttp/", StandardLicenses.APACHE2),
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
new SoftwareComponent("RxBinding", "2015", "Jake Wharton",
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
"https://github.com/nostra13/Android-Universal-Image-Loader",
StandardLicenses.APACHE2),
};
private static final int POS_ABOUT = 0;
private static final int POS_LICENSE = 1;
private static final int TOTAL_COUNT = 2;
@Override
protected void onCreate(final Bundle savedInstanceState) {
assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this);
setTitle(getString(R.string.title_activity_about));
final ActivityAboutBinding aboutBinding = ActivityAboutBinding.inflate(getLayoutInflater());
setContentView(aboutBinding.getRoot());
setSupportActionBar(aboutBinding.toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
final SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(this);
// Set up the ViewPager with the sections adapter.
aboutBinding.container.setAdapter(mSectionsPagerAdapter);
new TabLayoutMediator(aboutBinding.tabs, aboutBinding.container, (tab, position) -> {
switch (position) {
default:
case POS_ABOUT:
tab.setText(R.string.tab_about);
break;
case POS_LICENSE:
tab.setText(R.string.tab_licenses);
break;
}
}).attach();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* A placeholder fragment containing a simple view.
*/
public static class AboutFragment extends Fragment {
public AboutFragment() {
}
/**
* Created a new instance of this fragment for the given section number.
*
* @return New instance of {@link AboutFragment}
*/
public static AboutFragment newInstance() {
return new AboutFragment();
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
final FragmentAboutBinding aboutBinding =
FragmentAboutBinding.inflate(inflater, container, false);
final Context context = getContext();
aboutBinding.appVersion.setText(BuildConfig.VERSION_NAME);
aboutBinding.githubLink.setOnClickListener(nv ->
openUrlInBrowser(context, context.getString(R.string.github_url), false));
aboutBinding.donationLink.setOnClickListener(v ->
openUrlInBrowser(context, context.getString(R.string.donation_url), false));
aboutBinding.websiteLink.setOnClickListener(nv ->
openUrlInBrowser(context, context.getString(R.string.website_url), false));
aboutBinding.privacyPolicyLink.setOnClickListener(v ->
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url),
false));
return aboutBinding.getRoot();
}
}
/**
* A {@link FragmentStateAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public static class SectionsPagerAdapter extends FragmentStateAdapter {
public SectionsPagerAdapter(final FragmentActivity fa) {
super(fa);
}
@NonNull
@Override
public Fragment createFragment(final int position) {
switch (position) {
default:
case POS_ABOUT:
return AboutFragment.newInstance();
case POS_LICENSE:
return LicenseFragment.newInstance(SOFTWARE_COMPONENTS);
}
}
@Override
public int getItemCount() {
// Show 2 total pages.
return TOTAL_COUNT;
}
}
}

View File

@@ -0,0 +1,191 @@
package org.schabi.newpipe.about
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityAboutBinding
import org.schabi.newpipe.databinding.FragmentAboutBinding
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Localization.assureCorrectAppLanguage(this)
super.onCreate(savedInstanceState)
ThemeHelper.setTheme(this)
title = getString(R.string.title_activity_about)
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(aboutBinding.root)
setSupportActionBar(aboutBinding.aboutToolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
val mAboutStateAdapter = AboutStateAdapter(this)
// Set up the ViewPager with the sections adapter.
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
TabLayoutMediator(
aboutBinding.aboutTabLayout,
aboutBinding.aboutViewPager2
) { tab: TabLayout.Tab, position: Int ->
when (position) {
POS_ABOUT -> tab.setText(R.string.tab_about)
POS_LICENSE -> tab.setText(R.string.tab_licenses)
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
}
}.attach()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}
/**
* A placeholder fragment containing a simple view.
*/
class AboutFragment : Fragment() {
private fun Button.openLink(url: Int) {
setOnClickListener {
ShareUtils.openUrlInBrowser(
context,
requireContext().getString(url),
false
)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val aboutBinding = FragmentAboutBinding.inflate(inflater, container, false)
aboutBinding.aboutAppVersion.text = BuildConfig.VERSION_NAME
aboutBinding.aboutGithubLink.openLink(R.string.github_url)
aboutBinding.aboutDonationLink.openLink(R.string.donation_url)
aboutBinding.aboutWebsiteLink.openLink(R.string.website_url)
aboutBinding.aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
return aboutBinding.root
}
}
/**
* A [FragmentStateAdapter] that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun createFragment(position: Int): Fragment {
return when (position) {
POS_ABOUT -> AboutFragment()
POS_LICENSE -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
}
}
override fun getItemCount(): Int {
// Show 2 total pages.
return TOTAL_COUNT
}
}
companion object {
/**
* List of all software components.
*/
private val SOFTWARE_COMPONENTS = arrayOf(
SoftwareComponent(
"ACRA", "2013", "Kevin Gaudin",
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
),
SoftwareComponent(
"AndroidX", "2005 - 2011", "The Android Open Source Project",
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
),
SoftwareComponent(
"CircleImageView", "2014 - 2020", "Henning Dodenhof",
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2
),
SoftwareComponent(
"ExoPlayer", "2014 - 2020", "Google, Inc.",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
),
SoftwareComponent(
"GigaGet", "2014 - 2015", "Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3
),
SoftwareComponent(
"Groupie", "2016", "Lisa Wray",
"https://github.com/lisawray/groupie", StandardLicenses.MIT
),
SoftwareComponent(
"Icepick", "2015", "Frankie Sardo",
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1
),
SoftwareComponent(
"Jsoup", "2009 - 2020", "Jonathan Hedley",
"https://github.com/jhy/jsoup", StandardLicenses.MIT
),
SoftwareComponent(
"Markwon", "2019", "Dimitry Ivanov",
"https://github.com/noties/Markwon", StandardLicenses.APACHE2
),
SoftwareComponent(
"Material Components for Android", "2016 - 2020", "Google, Inc.",
"https://github.com/material-components/material-components-android",
StandardLicenses.APACHE2
),
SoftwareComponent(
"NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3
),
SoftwareComponent(
"NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2
),
SoftwareComponent(
"OkHttp", "2019", "Square, Inc.",
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
),
SoftwareComponent(
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
),
SoftwareComponent(
"RxAndroid", "2015", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
),
SoftwareComponent(
"RxBinding", "2015", "Jake Wharton",
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2
),
SoftwareComponent(
"RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
),
SoftwareComponent(
"Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
"https://github.com/nostra13/Android-Universal-Image-Loader",
StandardLicenses.APACHE2
)
)
private const val POS_ABOUT = 0
private const val POS_LICENSE = 1
private const val TOTAL_COUNT = 2
}
}

View File

@@ -1,145 +0,0 @@
package org.schabi.newpipe.about;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentLicensesBinding;
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding;
import org.schabi.newpipe.util.ShareUtils;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
/**
* Fragment containing the software licenses.
*/
public class LicenseFragment extends Fragment {
private static final String ARG_COMPONENTS = "components";
private static final String LICENSE_KEY = "ACTIVE_LICENSE";
private SoftwareComponent[] softwareComponents;
private SoftwareComponent componentForContextMenu;
private License activeLicense;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
final Bundle bundle = new Bundle();
bundle.putParcelableArray(ARG_COMPONENTS, Objects.requireNonNull(softwareComponents));
final LicenseFragment fragment = new LicenseFragment();
fragment.setArguments(bundle);
return fragment;
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
softwareComponents = (SoftwareComponent[]) getArguments()
.getParcelableArray(ARG_COMPONENTS);
if (savedInstanceState != null) {
final Serializable license = savedInstanceState.getSerializable(LICENSE_KEY);
if (license != null) {
activeLicense = (License) license;
}
}
// Sort components by name
Arrays.sort(softwareComponents, Comparator.comparing(SoftwareComponent::getName));
}
@Override
public void onDestroy() {
compositeDisposable.dispose();
super.onDestroy();
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
final FragmentLicensesBinding binding = FragmentLicensesBinding
.inflate(inflater, container, false);
binding.appReadLicense.setOnClickListener(v -> {
activeLicense = StandardLicenses.GPL3;
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
StandardLicenses.GPL3));
});
for (final SoftwareComponent component : softwareComponents) {
final ItemSoftwareComponentBinding componentBinding = ItemSoftwareComponentBinding
.inflate(inflater, container, false);
componentBinding.name.setText(component.getName());
componentBinding.copyright.setText(getString(R.string.copyright,
component.getYears(),
component.getCopyrightOwner(),
component.getLicense().getAbbreviation()));
final View root = componentBinding.getRoot();
root.setTag(component);
root.setOnClickListener(v -> {
activeLicense = component.getLicense();
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
component.getLicense()));
});
binding.softwareComponents.addView(root);
registerForContextMenu(root);
}
if (activeLicense != null) {
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
activeLicense));
}
return binding.getRoot();
}
@Override
public void onCreateContextMenu(final ContextMenu menu, final View v,
final ContextMenu.ContextMenuInfo menuInfo) {
final MenuInflater inflater = getActivity().getMenuInflater();
final SoftwareComponent component = (SoftwareComponent) v.getTag();
menu.setHeaderTitle(component.getName());
inflater.inflate(R.menu.software_component, menu);
super.onCreateContextMenu(menu, v, menuInfo);
componentForContextMenu = (SoftwareComponent) v.getTag();
}
@Override
public boolean onContextItemSelected(@NonNull final MenuItem item) {
// item.getMenuInfo() is null so we use the tag of the view
final SoftwareComponent component = componentForContextMenu;
if (component == null) {
return false;
}
switch (item.getItemId()) {
case R.id.action_website:
ShareUtils.openUrlInBrowser(getActivity(), component.getLink());
return true;
case R.id.action_show_license:
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
component.getLicense()));
}
return false;
}
@Override
public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
if (activeLicense != null) {
savedInstanceState.putSerializable(LICENSE_KEY, activeLicense);
}
}
}

View File

@@ -0,0 +1,102 @@
package org.schabi.newpipe.about
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R
import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
import org.schabi.newpipe.databinding.FragmentLicensesBinding
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
import java.util.Arrays
import java.util.Objects
/**
* Fragment containing the software licenses.
*/
class LicenseFragment : Fragment() {
private lateinit var softwareComponents: Array<SoftwareComponent>
private var activeLicense: License? = null
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
softwareComponents =
arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
if (savedInstanceState != null) {
val license = savedInstanceState.getSerializable(LICENSE_KEY)
if (license != null) {
activeLicense = license as License?
}
}
// Sort components by name
Arrays.sort(softwareComponents, Comparator.comparing(SoftwareComponent::name))
}
override fun onDestroy() {
compositeDisposable.dispose()
super.onDestroy()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
binding.licensesAppReadLicense.setOnClickListener {
activeLicense = StandardLicenses.GPL3
compositeDisposable.add(
showLicense(activity, StandardLicenses.GPL3)
)
}
for (component in softwareComponents) {
val componentBinding = ItemSoftwareComponentBinding
.inflate(inflater, container, false)
componentBinding.name.text = component.name
componentBinding.copyright.text = getString(
R.string.copyright,
component.years,
component.copyrightOwner,
component.license.abbreviation
)
val root: View = componentBinding.root
root.tag = component
root.setOnClickListener {
activeLicense = component.license
compositeDisposable.add(
showLicense(activity, component)
)
}
binding.licensesSoftwareComponents.addView(root)
registerForContextMenu(root)
}
if (activeLicense != null) {
compositeDisposable.add(
showLicense(activity, activeLicense!!)
)
}
return binding.root
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
if (activeLicense != null) {
savedInstanceState.putSerializable(LICENSE_KEY, activeLicense)
}
}
companion object {
private const val ARG_COMPONENTS = "components"
private const val LICENSE_KEY = "ACTIVE_LICENSE"
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
val fragment = LicenseFragment()
fragment.arguments =
bundleOf(ARG_COMPONENTS to Objects.requireNonNull(softwareComponents))
return fragment
}
}
}

View File

@@ -1,109 +0,0 @@
package org.schabi.newpipe.about;
import android.content.Context;
import android.util.Base64;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public final class LicenseFragmentHelper {
private LicenseFragmentHelper() { }
/**
* @param context the context to use
* @param license the license
* @return String which contains a HTML formatted license page
* styled according to the context's theme
*/
private static String getFormattedLicense(@NonNull final Context context,
@NonNull final License license) {
final StringBuilder licenseContent = new StringBuilder();
final String webViewData;
try (BufferedReader in = new BufferedReader(new InputStreamReader(
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8))) {
String str;
while ((str = in.readLine()) != null) {
licenseContent.append(str);
}
// split the HTML file and insert the stylesheet into the HEAD of the file
webViewData = licenseContent.toString().replace("</head>",
"<style>" + getLicenseStylesheet(context) + "</style></head>");
} catch (final IOException e) {
throw new IllegalArgumentException(
"Could not get license file: " + license.getFilename(), e);
}
return webViewData;
}
/**
* @param context the Android context
* @return String which is a CSS stylesheet according to the context's theme
*/
private static String getLicenseStylesheet(@NonNull final Context context) {
final boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
return "body{padding:12px 15px;margin:0;"
+ "background:#" + getHexRGBColor(context, isLightTheme
? R.color.light_license_background_color
: R.color.dark_license_background_color) + ";"
+ "color:#" + getHexRGBColor(context, isLightTheme
? R.color.light_license_text_color
: R.color.dark_license_text_color) + "}"
+ "a[href]{color:#" + getHexRGBColor(context, isLightTheme
? R.color.light_youtube_primary_color
: R.color.dark_youtube_primary_color) + "}"
+ "pre{white-space:pre-wrap}";
}
/**
* Cast R.color to a hexadecimal color value.
*
* @param context the context to use
* @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values
*/
private static String getHexRGBColor(@NonNull final Context context, final int color) {
return context.getResources().getString(color).substring(3);
}
static Disposable showLicense(@Nullable final Context context, @NonNull final License license) {
if (context == null) {
return Disposable.empty();
}
return Observable.fromCallable(() -> getFormattedLicense(context, license))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(formattedLicense -> {
final String webViewData = Base64.encodeToString(formattedLicense
.getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING);
final WebView webView = new WebView(context);
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64");
final AlertDialog.Builder alert = new AlertDialog.Builder(context);
alert.setTitle(license.getName());
alert.setView(webView);
assureCorrectAppLanguage(context);
alert.setNegativeButton(context.getString(R.string.finish),
(dialog, which) -> dialog.dismiss());
alert.show();
});
}
}

View File

@@ -0,0 +1,147 @@
package org.schabi.newpipe.about
import android.content.Context
import android.util.Base64
import android.webkit.WebView
import androidx.appcompat.app.AlertDialog
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
object LicenseFragmentHelper {
/**
* @param context the context to use
* @param license the license
* @return String which contains a HTML formatted license page
* styled according to the context's theme
*/
private fun getFormattedLicense(context: Context, license: License): String {
val licenseContent = StringBuilder()
val webViewData: String
try {
BufferedReader(
InputStreamReader(
context.assets.open(license.filename),
StandardCharsets.UTF_8
)
).use { `in` ->
var str: String?
while (`in`.readLine().also { str = it } != null) {
licenseContent.append(str)
}
// split the HTML file and insert the stylesheet into the HEAD of the file
webViewData = "$licenseContent".replace(
"</head>",
"<style>" + getLicenseStylesheet(context) + "</style></head>"
)
}
} catch (e: IOException) {
throw IllegalArgumentException(
"Could not get license file: " + license.filename, e
)
}
return webViewData
}
/**
* @param context the Android context
* @return String which is a CSS stylesheet according to the context's theme
*/
private fun getLicenseStylesheet(context: Context): String {
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
return (
"body{padding:12px 15px;margin:0;" + "background:#" + getHexRGBColor(
context,
if (isLightTheme) R.color.light_license_background_color
else R.color.dark_license_background_color
) + ";" + "color:#" + getHexRGBColor(
context,
if (isLightTheme) R.color.light_license_text_color
else R.color.dark_license_text_color
) + "}" + "a[href]{color:#" + getHexRGBColor(
context,
if (isLightTheme) R.color.light_youtube_primary_color
else R.color.dark_youtube_primary_color
) + "}" + "pre{white-space:pre-wrap}"
)
}
/**
* Cast R.color to a hexadecimal color value.
*
* @param context the context to use
* @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values
*/
private fun getHexRGBColor(context: Context, color: Int): String {
return context.getString(color).substring(3)
}
@JvmStatic
fun showLicense(context: Context?, license: License): Disposable {
return if (context == null) {
Disposable.empty()
} else {
Observable.fromCallable { getFormattedLicense(context, license) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense: String ->
val webViewData = Base64.encodeToString(
formattedLicense
.toByteArray(StandardCharsets.UTF_8),
Base64.NO_PADDING
)
val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
val alert = AlertDialog.Builder(context)
alert.setTitle(license.name)
alert.setView(webView)
Localization.assureCorrectAppLanguage(context)
alert.setNegativeButton(
context.getString(R.string.finish)
) { dialog, _ -> dialog.dismiss() }
alert.show()
}
}
}
@JvmStatic
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
return if (context == null) {
Disposable.empty()
} else {
Observable.fromCallable { getFormattedLicense(context, component.license) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense: String ->
val webViewData = Base64.encodeToString(
formattedLicense
.toByteArray(StandardCharsets.UTF_8),
Base64.NO_PADDING
)
val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
val alert = AlertDialog.Builder(context)
alert.setTitle(component.license.name)
alert.setView(webView)
Localization.assureCorrectAppLanguage(context)
alert.setPositiveButton(
R.string.dismiss
) { dialog, _ -> dialog.dismiss() }
alert.setNeutralButton(R.string.open_website_license) { _, _ ->
ShareUtils.openUrlInBrowser(context, component.link)
}
alert.show()
}
}
}
}

View File

@@ -1,19 +0,0 @@
package org.schabi.newpipe.about;
/**
* Class containing information about standard software licenses.
*/
public final class StandardLicenses {
public static final License GPL3
= new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
public static final License APACHE2
= new License("Apache License, Version 2.0", "ALv2", "apache2.html");
public static final License MPL2
= new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
public static final License MIT
= new License("MIT License", "MIT", "mit.html");
public static final License EPL1
= new License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html");
private StandardLicenses() { }
}

View File

@@ -0,0 +1,21 @@
package org.schabi.newpipe.about
/**
* Class containing information about standard software licenses.
*/
object StandardLicenses {
@JvmField
val GPL3 = License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html")
@JvmField
val APACHE2 = License("Apache License, Version 2.0", "ALv2", "apache2.html")
@JvmField
val MPL2 = License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html")
@JvmField
val MIT = License("MIT License", "MIT", "mit.html")
@JvmField
val EPL1 = License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html")
}

View File

@@ -1,64 +0,0 @@
package org.schabi.newpipe.database;
import androidx.room.TypeConverter;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.local.subscription.FeedGroupIcon;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
public final class Converters {
private Converters() { }
/**
* Convert a long value to a {@link OffsetDateTime}.
*
* @param value the long value
* @return the {@code OffsetDateTime}
*/
@TypeConverter
public static OffsetDateTime offsetDateTimeFromTimestamp(final Long value) {
return value == null ? null : OffsetDateTime.ofInstant(Instant.ofEpochMilli(value),
ZoneOffset.UTC);
}
/**
* Convert a {@link OffsetDateTime} to a long value.
*
* @param offsetDateTime the {@code OffsetDateTime}
* @return the long value
*/
@TypeConverter
public static Long offsetDateTimeToTimestamp(final OffsetDateTime offsetDateTime) {
return offsetDateTime == null ? null : offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC)
.toInstant().toEpochMilli();
}
@TypeConverter
public static StreamType streamTypeOf(final String value) {
return StreamType.valueOf(value);
}
@TypeConverter
public static String stringOf(final StreamType streamType) {
return streamType.name();
}
@TypeConverter
public static Integer integerOf(final FeedGroupIcon feedGroupIcon) {
return feedGroupIcon.getId();
}
@TypeConverter
public static FeedGroupIcon feedGroupIconOf(final Integer id) {
for (final FeedGroupIcon icon : FeedGroupIcon.values()) {
if (icon.getId() == id) {
return icon;
}
}
throw new IllegalArgumentException("There's no feed group icon with the id \"" + id + "\"");
}
}

View File

@@ -0,0 +1,52 @@
package org.schabi.newpipe.database
import androidx.room.TypeConverter
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
object Converters {
/**
* Convert a long value to a [OffsetDateTime].
*
* @param value the long value
* @return the `OffsetDateTime`
*/
@TypeConverter
fun offsetDateTimeFromTimestamp(value: Long?): OffsetDateTime? {
return value?.let { OffsetDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) }
}
/**
* Convert a [OffsetDateTime] to a long value.
*
* @param offsetDateTime the `OffsetDateTime`
* @return the long value
*/
@TypeConverter
fun offsetDateTimeToTimestamp(offsetDateTime: OffsetDateTime?): Long? {
return offsetDateTime?.withOffsetSameInstant(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
}
@TypeConverter
fun streamTypeOf(value: String): StreamType {
return StreamType.valueOf(value)
}
@TypeConverter
fun stringOf(streamType: StreamType): String {
return streamType.name
}
@TypeConverter
fun integerOf(feedGroupIcon: FeedGroupIcon): Int {
return feedGroupIcon.id
}
@TypeConverter
fun feedGroupIconOf(id: Int): FeedGroupIcon {
return FeedGroupIcon.values().first { it.id == id }
}
}

View File

@@ -9,7 +9,8 @@ import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@@ -20,21 +21,34 @@ abstract class FeedDAO {
@Query(
"""
SELECT s.* FROM streams s
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
abstract fun getAllStreams(): Flowable<List<StreamWithState>>
@Query(
"""
SELECT s.* FROM streams s
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
@@ -42,16 +56,88 @@ abstract class FeedDAO {
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
INNER JOIN feed_group fg
ON fg.uid = fgs.group_id
WHERE fgs.group_id = :groupId
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
abstract fun getAllStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
/**
* @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @return all of the non-live, never-played and non-finished streams in the feed
* (all of the cited conditions must hold for a stream to be in the returned list)
*/
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE (
sh.stream_id IS NULL
OR sst.stream_id IS NULL
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
OR sst.progress_time < s.duration * 1000 * 3 / 4
OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM'
)
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
/**
* @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @param groupId the group id to get streams of
* @return all of the non-live, never-played and non-finished streams for the given feed group
* (all of the cited conditions must hold for a stream to be in the returned list)
*/
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
WHERE fgs.group_id = :groupId
AND (
sh.stream_id IS NULL
OR sst.stream_id IS NULL
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
OR sst.progress_time < s.duration * 1000 * 3 / 4
OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM'
)
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
@Query(
"""

View File

@@ -21,7 +21,7 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WA
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao
@@ -80,7 +80,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
+ " LEFT JOIN "
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
+ STREAM_PROGRESS_TIME
+ STREAM_PROGRESS_MILLIS
+ " FROM " + STREAM_STATE_TABLE + " )"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();

View File

@@ -7,14 +7,13 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import kotlin.jvm.Throws
data class PlaylistStreamEntry(
@Embedded
val streamEntity: StreamEntity,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0")
val progressTime: Long,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS, defaultValue = "0")
val progressMillis: Long,
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
val streamId: Long,

View File

@@ -14,26 +14,26 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
@Dao
public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
@Override
@Query("SELECT * FROM " + PLAYLIST_TABLE)
public abstract Flowable<List<PlaylistEntity>> getAll();
Flowable<List<PlaylistEntity>> getAll();
@Override
@Query("DELETE FROM " + PLAYLIST_TABLE)
public abstract int deleteAll();
int deleteAll();
@Override
public Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
default Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(long playlistId);
int deletePlaylist(long playlistId);
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
public abstract Flowable<Long> getCount();
Flowable<Long> getCount();
}

View File

@@ -17,31 +17,31 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
@Dao
public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity> {
public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
@Override
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
public abstract Flowable<List<PlaylistRemoteEntity>> getAll();
Flowable<List<PlaylistRemoteEntity>> getAll();
@Override
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
public abstract int deleteAll();
int deleteAll();
@Override
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
abstract Long getPlaylistIdInternal(long serviceId, String url);
Long getPlaylistIdInternal(long serviceId, String url);
@Transaction
public long upsert(final PlaylistRemoteEntity playlist) {
default long upsert(final PlaylistRemoteEntity playlist) {
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
if (playlistId == null) {
@@ -55,5 +55,5 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(long playlistId);
int deletePlaylist(long playlistId);
}

View File

@@ -25,32 +25,32 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
@Override
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
public abstract Flowable<List<PlaylistStreamEntity>> getAll();
Flowable<List<PlaylistStreamEntity>> getAll();
@Override
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
public abstract int deleteAll();
int deleteAll();
@Override
public Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
default Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract void deleteBatch(long playlistId);
void deleteBatch(long playlistId);
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract Flowable<Integer> getMaximumIndexOf(long playlistId);
Flowable<Integer> getMaximumIndexOf(long playlistId);
@Transaction
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
@@ -64,12 +64,12 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
+ " LEFT JOIN "
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
+ STREAM_PROGRESS_TIME
+ STREAM_PROGRESS_MILLIS
+ " FROM " + STREAM_STATE_TABLE + " )"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
+ " ORDER BY " + JOIN_INDEX + " ASC")
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
@Transaction
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", "
@@ -80,5 +80,5 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " GROUP BY " + JOIN_PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
}

View File

@@ -5,7 +5,7 @@ import androidx.room.Embedded
import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import java.time.OffsetDateTime
@@ -13,8 +13,8 @@ class StreamStatisticsEntry(
@Embedded
val streamEntity: StreamEntity,
@ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0")
val progressTime: Long,
@ColumnInfo(name = STREAM_PROGRESS_MILLIS, defaultValue = "0")
val progressMillis: Long,
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
val streamId: Long,

View File

@@ -0,0 +1,14 @@
package org.schabi.newpipe.database.stream
import androidx.room.ColumnInfo
import androidx.room.Embedded
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity
data class StreamWithState(
@Embedded
val stream: StreamEntity,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS)
val stateProgressMillis: Long?
)

View File

@@ -17,31 +17,31 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_ST
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao
public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> {
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
@Override
@Query("SELECT * FROM " + STREAM_STATE_TABLE)
public abstract Flowable<List<StreamStateEntity>> getAll();
Flowable<List<StreamStateEntity>> getAll();
@Override
@Query("DELETE FROM " + STREAM_STATE_TABLE)
public abstract int deleteAll();
int deleteAll();
@Override
public Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
default Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract Flowable<List<StreamStateEntity>> getState(long streamId);
Flowable<List<StreamStateEntity>> getState(long streamId);
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteState(long streamId);
int deleteState(long streamId);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract void silentInsertInternal(StreamStateEntity streamState);
void silentInsertInternal(StreamStateEntity streamState);
@Transaction
public long upsert(final StreamStateEntity stream) {
default long upsert(final StreamStateEntity stream) {
silentInsertInternal(stream);
return update(stream);
}

View File

@@ -5,7 +5,7 @@ import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import java.util.concurrent.TimeUnit;
import java.util.Objects;
import static androidx.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
@@ -25,26 +25,31 @@ public class StreamStateEntity {
// This additional field is required for the SQL query because 'stream_id' is used
// for some other joins already
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
public static final String STREAM_PROGRESS_TIME = "progress_time";
public static final String STREAM_PROGRESS_MILLIS = "progress_time";
/**
* Playback state will not be saved, if playback time is less than this threshold.
* Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
*/
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
/**
* Playback state will not be saved, if time left is less than this threshold.
* Stream will be considered finished if the playback time left exceeds this threshold
* (60000ms = 60s).
* @see #isFinished(long)
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
*/
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000;
@ColumnInfo(name = JOIN_STREAM_ID)
private long streamUid;
@ColumnInfo(name = STREAM_PROGRESS_TIME)
private long progressTime;
@ColumnInfo(name = STREAM_PROGRESS_MILLIS)
private long progressMillis;
public StreamStateEntity(final long streamUid, final long progressTime) {
public StreamStateEntity(final long streamUid, final long progressMillis) {
this.streamUid = streamUid;
this.progressTime = progressTime;
this.progressMillis = progressMillis;
}
public long getStreamUid() {
@@ -55,27 +60,53 @@ public class StreamStateEntity {
this.streamUid = streamUid;
}
public long getProgressTime() {
return progressTime;
public long getProgressMillis() {
return progressMillis;
}
public void setProgressTime(final long progressTime) {
this.progressTime = progressTime;
public void setProgressMillis(final long progressMillis) {
this.progressMillis = progressMillis;
}
public boolean isValid(final int durationInSeconds) {
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
/**
* The state will be considered valid, and thus be saved, if the progress is more than {@link
* #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS} or at least 1/4 of the video length.
* @param durationInSeconds the duration of the stream connected with this state, in seconds
* @return whether this stream state entity should be saved or not
*/
public boolean isValid(final long durationInSeconds) {
return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|| progressMillis > durationInSeconds * 1000 / 4;
}
/**
* The video will be considered as finished, if the time left is less than {@link
* #PLAYBACK_FINISHED_END_MILLISECONDS} and the progress is at least 3/4 of the video length.
* The state will be saved anyway, so that it can be shown under stream info items, but the
* player will not resume if a state is considered as finished. Finished streams are also the
* ones that can be filtered out in the feed fragment.
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
* @param durationInSeconds the duration of the stream connected with this state, in seconds
* @return whether the stream is finished or not
*/
public boolean isFinished(final long durationInSeconds) {
return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS
&& progressMillis >= durationInSeconds * 1000 * 3 / 4;
}
@Override
public boolean equals(@Nullable final Object obj) {
if (obj instanceof StreamStateEntity) {
return ((StreamStateEntity) obj).streamUid == streamUid
&& ((StreamStateEntity) obj).progressTime == progressTime;
&& ((StreamStateEntity) obj).progressMillis == progressMillis;
} else {
return false;
}
}
@Override
public int hashCode() {
return Objects.hash(streamUid, progressMillis);
}
}

View File

@@ -3,6 +3,8 @@ package org.schabi.newpipe.download;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
@@ -20,6 +22,9 @@ import android.widget.RadioGroup;
import android.widget.SeekBar;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -35,8 +40,10 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.databinding.DownloadDialogBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization;
@@ -45,10 +52,9 @@ import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper;
@@ -61,7 +67,6 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -69,8 +74,6 @@ import icepick.Icepick;
import icepick.State;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
@@ -83,7 +86,6 @@ public class DownloadDialog extends DialogFragment
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment";
private static final boolean DEBUG = MainActivity.DEBUG;
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230;
@State
StreamInfo currentInfo;
@@ -100,6 +102,9 @@ public class DownloadDialog extends DialogFragment
@State
int selectedSubtitleIndex = 0;
@Nullable
private OnDismissListener onDismissListener = null;
private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null;
@@ -117,6 +122,25 @@ public class DownloadDialog extends DialogFragment
private SharedPreferences prefs;
// Variables for file name and MIME type when picking new folder because it's not set yet
private String filenameTmp;
private String mimeTmp;
private final ActivityResultLauncher<Intent> requestDownloadSaveAsLauncher =
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadSaveAsResult);
private final ActivityResultLauncher<Intent> requestDownloadPickAudioFolderLauncher =
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadPickAudioFolderResult);
private final ActivityResultLauncher<Intent> requestDownloadPickVideoFolderLauncher =
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
/*//////////////////////////////////////////////////////////////////////////
// Instance creation
//////////////////////////////////////////////////////////////////////////*/
public static DownloadDialog newInstance(final StreamInfo info) {
final DownloadDialog dialog = new DownloadDialog();
dialog.setInfo(info);
@@ -138,6 +162,11 @@ public class DownloadDialog extends DialogFragment
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// Setters
//////////////////////////////////////////////////////////////////////////*/
private void setInfo(final StreamInfo info) {
this.currentInfo = info;
}
@@ -154,10 +183,6 @@ public class DownloadDialog extends DialogFragment
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
this.wrappedVideoStreams = wvs;
}
@@ -183,6 +208,14 @@ public class DownloadDialog extends DialogFragment
this.selectedSubtitleIndex = ssi;
}
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener;
}
/*//////////////////////////////////////////////////////////////////////////
// Android lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -193,7 +226,7 @@ public class DownloadDialog extends DialogFragment
if (!PermissionHelper.checkStoragePermissions(getActivity(),
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
getDialog().dismiss();
dismiss();
return;
}
@@ -252,10 +285,6 @@ public class DownloadDialog extends DialogFragment
}, Context.BIND_AUTO_CREATE);
}
/*//////////////////////////////////////////////////////////////////////////
// Inits
//////////////////////////////////////////////////////////////////////////*/
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
@@ -311,27 +340,35 @@ public class DownloadDialog extends DialogFragment
fetchStreamsSize();
}
private void fetchStreamsSize() {
disposables.clear();
private void initToolbar(final Toolbar toolbar) {
if (DEBUG) {
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
}
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
setupVideoSpinner();
toolbar.setTitle(R.string.download_dialog_title);
toolbar.setNavigationIcon(R.drawable.ic_arrow_back);
toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(v -> dismiss());
toolbar.setNavigationContentDescription(R.string.cancel);
okButton = toolbar.findViewById(R.id.okay);
okButton.setEnabled(false); // disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload();
return true;
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
setupAudioSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
setupSubtitleSpinner();
}
}));
return false;
});
}
@Override
public void onDismiss(@NonNull final DialogInterface dialog) {
super.onDismiss(dialog);
if (onDismissListener != null) {
onDismissListener.onDismiss(dialog);
}
}
@Override
@@ -346,80 +383,51 @@ public class DownloadDialog extends DialogFragment
super.onDestroyView();
}
/*//////////////////////////////////////////////////////////////////////////
// Radio group Video&Audio options - Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
/*//////////////////////////////////////////////////////////////////////////
// Streams Spinner Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) {
if (data.getData() == null) {
showFailedDialog(R.string.general_error);
return;
}
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
final File file = Utils.getFileForUri(data.getData());
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
StoredFileHelper.DEFAULT_MIME);
return;
}
final DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
if (docFile == null) {
showFailedDialog(R.string.general_error);
return;
}
// check if the selected file was previously used
checkSelectedDownload(null, data.getData(), docFile.getName(),
docFile.getType());
}
}
private void initToolbar(final Toolbar toolbar) {
if (DEBUG) {
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
}
toolbar.setTitle(R.string.download_dialog_title);
toolbar.setNavigationIcon(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_arrow_back));
toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(v -> requireDialog().dismiss());
toolbar.setNavigationContentDescription(R.string.cancel);
okButton = toolbar.findViewById(R.id.okay);
okButton.setEnabled(false); // disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload();
if (getActivity() instanceof RouterActivity) {
getActivity().finish();
}
return true;
}
return false;
});
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
// Video, audio and subtitle spinners
//////////////////////////////////////////////////////////////////////////*/
private void fetchStreamsSize() {
disposables.clear();
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.video_button) {
setupVideoSpinner();
}
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading video stream size",
currentInfo.getServiceId()))));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.audio_button) {
setupAudioSpinner();
}
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading audio stream size",
currentInfo.getServiceId()))));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.subtitle_button) {
setupSubtitleSpinner();
}
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading subtitle stream size",
currentInfo.getServiceId()))));
}
private void setupAudioSpinner() {
if (getContext() == null) {
return;
@@ -450,6 +458,88 @@ public class DownloadDialog extends DialogFragment
setRadioButtonsState(true);
}
/*//////////////////////////////////////////////////////////////////////////
// Activity results
//////////////////////////////////////////////////////////////////////////*/
private void requestDownloadPickAudioFolderResult(final ActivityResult result) {
requestDownloadPickFolderResult(
result, getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO);
}
private void requestDownloadPickVideoFolderResult(final ActivityResult result) {
requestDownloadPickFolderResult(
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
}
private void requestDownloadSaveAsResult(final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK) {
return;
}
if (result.getData() == null || result.getData().getData() == null) {
showFailedDialog(R.string.general_error);
return;
}
if (FilePickerActivityHelper.isOwnFileUri(context, result.getData().getData())) {
final File file = Utils.getFileForUri(result.getData().getData());
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
StoredFileHelper.DEFAULT_MIME);
return;
}
final DocumentFile docFile
= DocumentFile.fromSingleUri(context, result.getData().getData());
if (docFile == null) {
showFailedDialog(R.string.general_error);
return;
}
// check if the selected file was previously used
checkSelectedDownload(null, result.getData().getData(), docFile.getName(),
docFile.getType());
}
private void requestDownloadPickFolderResult(final ActivityResult result,
final String key,
final String tag) {
if (result.getResultCode() != Activity.RESULT_OK) {
return;
}
if (result.getData() == null || result.getData().getData() == null) {
showFailedDialog(R.string.general_error);
return;
}
Uri uri = result.getData().getData();
if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
uri = Uri.fromFile(Utils.getFileForUri(uri));
} else {
context.grantUriPermission(context.getPackageName(), uri,
StoredDirectoryHelper.PERMISSION_FLAGS);
}
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putString(key, uri.toString()).apply();
try {
final StoredDirectoryHelper mainStorage
= new StoredDirectoryHelper(context, uri, tag);
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
filenameTmp, mimeTmp);
} catch (final IOException e) {
showFailedDialog(R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Listeners
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId) {
if (DEBUG) {
@@ -499,6 +589,11 @@ public class DownloadDialog extends DialogFragment
public void onNothingSelected(final AdapterView<?> parent) {
}
/*//////////////////////////////////////////////////////////////////////////
// Download
//////////////////////////////////////////////////////////////////////////*/
protected void setupDownloadOptions() {
setRadioButtonsState(false);
@@ -511,7 +606,7 @@ public class DownloadDialog extends DialogFragment
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
? View.VISIBLE : View.GONE);
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
getString(R.string.last_download_type_video_key));
@@ -539,7 +634,7 @@ public class DownloadDialog extends DialogFragment
} else {
Toast.makeText(getContext(), R.string.no_streams_available_download,
Toast.LENGTH_SHORT).show();
getDialog().dismiss();
dismiss();
}
}
@@ -591,98 +686,97 @@ public class DownloadDialog extends DialogFragment
.show();
}
private void showErrorActivity(final Exception e) {
ErrorActivity.reportError(
context,
Collections.singletonList(e),
null,
null,
ErrorInfo
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
);
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
launcher.launch(StoredDirectoryHelper.getPicker(context));
}
private void prepareSelectedDownload() {
final StoredDirectoryHelper mainStorage;
final MediaFormat format;
final String mime;
final String selectedMediaType;
// first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic
String filename = getNameEditText().concat(".");
filenameTmp = getNameEditText().concat(".");
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
switch (format) {
case WEBMA_OPUS:
mime = "audio/ogg";
filename += "opus";
break;
default:
mime = format.mimeType;
filename += format.suffix;
break;
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else {
mimeTmp = format.mimeType;
filenameTmp += format.suffix;
}
break;
case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
mime = format.mimeType;
filename += format.suffix;
mimeTmp = format.mimeType;
filenameTmp += format.suffix;
break;
case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mime = format.mimeType;
filename += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
mimeTmp = format.mimeType;
filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
break;
default:
throw new RuntimeException("No stream selected");
}
if (mainStorage == null || askForSavePath) {
// This part is called if with SAF preferred:
// * older android version running
// * save path not defined (via download settings)
// * the user checked the "ask where to download" option
if (!askForSavePath
&& (mainStorage == null
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|| mainStorage.isInvalidSafStorage())) {
// Pick new download folder if one of:
// - Download folder is not set
// - Download folder uses SAF while SAF is disabled
// - Download folder doesn't use SAF while SAF is enabled
// - Download folder uses SAF but the user manually revoked access to it
Toast.makeText(context, getString(R.string.no_dir_yet),
Toast.LENGTH_LONG).show();
if (!askForSavePath) {
Toast.makeText(context, getString(R.string.no_available_dir),
Toast.LENGTH_LONG).show();
}
if (NewPipeSettings.useStorageAccessFramework(context)) {
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS,
filename, mime);
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
launchDirectoryPicker(requestDownloadPickAudioFolderLauncher);
} else {
File initialSavePath;
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
} else {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
}
initialSavePath = new File(initialSavePath, filename);
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context,
initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS);
launchDirectoryPicker(requestDownloadPickVideoFolderLauncher);
}
return;
}
if (askForSavePath) {
final Uri initialPath;
if (NewPipeSettings.useStorageAccessFramework(context)) {
initialPath = null;
} else {
final File initialSavePath;
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
} else {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
}
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
}
requestDownloadSaveAsLauncher.launch(StoredFileHelper.getNewPicker(context,
filenameTmp, mimeTmp, initialPath));
return;
}
// check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp);
// remember the last media type downloaded by the user
prefs.edit()
.putString(getString(R.string.last_used_download_type), selectedMediaType)
prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
.apply();
}
@@ -705,19 +799,19 @@ public class DownloadDialog extends DialogFragment
mainStorage.getTag());
}
} catch (final Exception e) {
showErrorActivity(e);
ErrorActivity.reportErrorInSnackbar(this,
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
return;
}
// check if is our file
// get state of potential mission referring to the same file
final MissionState state = downloadManager.checkForExistingMission(storage);
@StringRes
final int msgBtn;
@StringRes
final int msgBody;
@StringRes final int msgBtn;
@StringRes final int msgBody;
// this switch checks if there is already a mission referring to the same file
switch (state) {
case Finished:
case Finished: // there is already a finished mission
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_finished_warning;
break;
@@ -729,7 +823,7 @@ public class DownloadDialog extends DialogFragment
msgBtn = R.string.generate_unique_name;
msgBody = R.string.download_already_running;
break;
case None:
case None: // there is no mission referring to the same file
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
@@ -764,7 +858,7 @@ public class DownloadDialog extends DialogFragment
msgBody = R.string.overwrite_unrelated_warning;
break;
default:
return;
return; // unreachable
}
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
@@ -938,6 +1032,9 @@ public class DownloadDialog extends DialogFragment
DownloadManagerService.startMission(context, urls, storage, kind, threads,
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
Toast.makeText(context, getString(R.string.download_has_started),
Toast.LENGTH_SHORT).show();
dismiss();
}
}

View File

@@ -1,9 +1,10 @@
package org.schabi.newpipe.report;
package org.schabi.newpipe.error;
import android.content.Context;
import androidx.annotation.NonNull;
import org.acra.ReportField;
import org.acra.data.CrashReportData;
import org.acra.sender.ReportSender;
import org.schabi.newpipe.R;
@@ -32,8 +33,12 @@ public class AcraReportSender implements ReportSender {
@Override
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
ErrorActivity.reportError(context, report,
ErrorInfo.make(UserAction.UI_ERROR, "none",
"App crash, UI failure", R.string.app_ui_crash));
ErrorActivity.reportError(context, new ErrorInfo(
new String[]{report.getString(ReportField.STACK_TRACE)},
UserAction.UI_ERROR,
ErrorInfo.SERVICE_NONE,
"ACRA report",
R.string.app_ui_crash,
null));
}
}

View File

@@ -1,4 +1,4 @@
package org.schabi.newpipe.report;
package org.schabi.newpipe.error;
import android.content.Context;

View File

@@ -1,14 +1,12 @@
package org.schabi.newpipe.report;
package org.schabi.newpipe.error;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
@@ -17,31 +15,24 @@ import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NavUtils;
import androidx.fragment.app.Fragment;
import com.google.android.material.snackbar.Snackbar;
import com.grack.nanojson.JsonWriter;
import org.acra.ReportField;
import org.acra.data.CrashReportData;
import org.schabi.newpipe.ActivityCommunicator;
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.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.Vector;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@@ -70,113 +61,84 @@ public class ErrorActivity extends AppCompatActivity {
public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_LIST = "error_list";
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT
= "Exception in NewPipe " + BuildConfig.VERSION_NAME;
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL
= "https://github.com/TeamNewPipe/NewPipe/issues";
private String[] errorList;
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private ErrorInfo errorInfo;
private Class returnActivity;
private String currentTimeStamp;
private ActivityErrorBinding activityErrorBinding;
public static void reportUiError(final AppCompatActivity activity, final Throwable el) {
reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
public static void reportError(final Context context, final ErrorInfo errorInfo) {
final Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
public static void reportError(final Context context, final List<Throwable> el,
final Class returnActivity, final View rootView,
final ErrorInfo errorInfo) {
public static void reportErrorInSnackbar(final Context context, final ErrorInfo errorInfo) {
final View rootView = context instanceof Activity
? ((Activity) context).findViewById(android.R.id.content) : null;
reportErrorInSnackbar(context, rootView, errorInfo);
}
public static void reportErrorInSnackbar(final Fragment fragment, final ErrorInfo errorInfo) {
View rootView = fragment.getView();
if (rootView == null && fragment.getActivity() != null) {
rootView = fragment.getActivity().findViewById(android.R.id.content);
}
reportErrorInSnackbar(fragment.requireContext(), rootView, errorInfo);
}
public static void reportUiErrorInSnackbar(final Context context,
final String request,
final Throwable throwable) {
reportErrorInSnackbar(context, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
}
public static void reportUiErrorInSnackbar(final Fragment fragment,
final String request,
final Throwable throwable) {
reportErrorInSnackbar(fragment, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
}
////////////////////////////////////////////////////////////////////////
// Utils
////////////////////////////////////////////////////////////////////////
private static void reportErrorInSnackbar(final Context context,
@Nullable final View rootView,
final ErrorInfo errorInfo) {
if (rootView != null) {
Snackbar.make(rootView, R.string.error_snackbar_message, 3 * 1000)
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
.setActionTextColor(Color.YELLOW)
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
startErrorActivity(returnActivity, context, errorInfo, el)).show();
reportError(context, errorInfo)).show();
} else {
startErrorActivity(returnActivity, context, errorInfo, el);
reportError(context, errorInfo);
}
}
private static void startErrorActivity(final Class returnActivity, final Context context,
final ErrorInfo errorInfo, final List<Throwable> el) {
final ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
ac.setReturnActivity(returnActivity);
final Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
intent.putExtra(ERROR_LIST, elToSl(el));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
public static void reportError(final Context context, final Throwable e,
final Class returnActivity, final View rootView,
final ErrorInfo errorInfo) {
List<Throwable> el = null;
if (e != null) {
el = new Vector<>();
el.add(e);
}
reportError(context, el, returnActivity, rootView, errorInfo);
}
// async call
public static void reportError(final Handler handler, final Context context,
final Throwable e, final Class returnActivity,
final View rootView, final ErrorInfo errorInfo) {
List<Throwable> el = null;
if (e != null) {
el = new Vector<>();
el.add(e);
}
reportError(handler, context, el, returnActivity, rootView, errorInfo);
}
// async call
public static void reportError(final Handler handler, final Context context,
final List<Throwable> el, final Class returnActivity,
final View rootView, final ErrorInfo errorInfo) {
handler.post(() -> reportError(context, el, returnActivity, rootView, errorInfo));
}
public static void reportError(final Context context, final CrashReportData report,
final ErrorInfo errorInfo) {
final String[] el = {report.getString(ReportField.STACK_TRACE)};
final Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
intent.putExtra(ERROR_LIST, el);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
private static String getStackTrace(final Throwable throwable) {
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw, true);
throwable.printStackTrace(pw);
return sw.getBuffer().toString();
}
// errorList to StringList
private static String[] elToSl(final List<Throwable> stackTraces) {
final String[] out = new String[stackTraces.size()];
for (int i = 0; i < stackTraces.size(); i++) {
out[i] = getStackTrace(stackTraces.get(i));
}
return out;
}
////////////////////////////////////////////////////////////////////////
// Activity lifecycle
////////////////////////////////////////////////////////////////////////
@Override
protected void onCreate(final Bundle savedInstanceState) {
assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this);
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
@@ -193,38 +155,28 @@ public class ErrorActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true);
}
final ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
returnActivity = ac.getReturnActivity();
errorInfo = intent.getParcelableExtra(ERROR_INFO);
errorList = intent.getStringArrayExtra(ERROR_LIST);
// important add guru meditation
addGuruMeditation();
currentTimeStamp = getCurrentTimeStamp();
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL"));
activityErrorBinding.errorReportCopyButton.setOnClickListener(v -> {
ShareUtils.copyToClipboard(this, buildMarkdown());
});
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
ShareUtils.copyToClipboard(this, buildMarkdown()));
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB"));
// normal bugreport
buildInfo(errorInfo);
if (errorInfo.getMessage() != 0) {
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage());
} else {
activityErrorBinding.errorMessageView.setVisibility(View.GONE);
activityErrorBinding.messageWhatHappenedView.setVisibility(View.GONE);
}
activityErrorBinding.errorView.setText(formErrorText(errorList));
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
// print stack trace once again for debugging:
for (final String e : errorList) {
for (final String e : errorInfo.getStackTraces()) {
Log.e(TAG, e);
}
}
@@ -238,16 +190,17 @@ public class ErrorActivity extends AppCompatActivity {
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
switch (id) {
switch (item.getItemId()) {
case android.R.id.home:
goToReturnActivity();
break;
onBackPressed();
return true;
case R.id.menu_item_share_error:
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
break;
ShareUtils.shareText(getApplicationContext(),
getString(R.string.error_report_title), buildJson());
return true;
default:
return false;
}
return false;
}
private void openPrivacyPolicyDialog(final Context context, final String action) {
@@ -264,15 +217,14 @@ public class ErrorActivity extends AppCompatActivity {
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)
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
+ getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson());
if (i.resolveActivity(getPackageManager()) != null) {
ShareUtils.openIntentInApp(context, i);
}
ShareUtils.openIntentInApp(context, i, true);
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
}
})
.setNegativeButton(R.string.decline, (dialog, which) -> {
// do nothing
@@ -310,17 +262,6 @@ public class ErrorActivity extends AppCompatActivity {
return checkedReturnActivity;
}
private void goToReturnActivity() {
final Class<? extends Activity> checkedReturnActivity = getReturnActivity(returnActivity);
if (checkedReturnActivity == null) {
super.onBackPressed();
} else {
final Intent intent = new Intent(this, checkedReturnActivity);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(this, intent);
}
}
private void buildInfo(final ErrorInfo info) {
String text = "";
@@ -355,7 +296,7 @@ public class ErrorActivity extends AppCompatActivity {
.value("version", BuildConfig.VERSION_NAME)
.value("os", getOsString())
.value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorList))
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString())
.end()
@@ -393,27 +334,27 @@ public class ErrorActivity extends AppCompatActivity {
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorList.length > 1) {
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorList.length)
.append(errorInfo.getStackTraces().length)
.append(")</b></summary><p>\n");
}
// add the logs
for (int i = 0; i < errorList.length; i++) {
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
htmlErrorReport.append("<details><summary><b>Crash log ");
if (errorList.length > 1) {
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1);
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorList[i]).append("\n```\n")
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
.append("</details>\n");
}
// make sure to close everything
if (errorList.length > 1) {
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append("</p></details>\n");
}
htmlErrorReport.append("<hr>\n");
@@ -460,17 +401,4 @@ public class ErrorActivity extends AppCompatActivity {
text += "\n" + getString(R.string.guru_meditation);
activityErrorBinding.errorSorryView.setText(text);
}
@Override
public void onBackPressed() {
//super.onBackPressed();
goToReturnActivity();
}
public String getCurrentTimeStamp() {
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df.format(new Date());
}
}

View File

@@ -0,0 +1,115 @@
package org.schabi.newpipe.error
import android.os.Parcelable
import androidx.annotation.StringRes
import kotlinx.android.parcel.Parcelize
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
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.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
import org.schabi.newpipe.ktx.isNetworkRelated
import java.io.PrintWriter
import java.io.StringWriter
@Parcelize
class ErrorInfo(
val stackTraces: Array<String>,
val userAction: UserAction,
val serviceName: String,
val request: String,
val messageStringId: Int,
@Transient // no need to store throwable, all data for report is in other variables
var throwable: Throwable? = null
) : Parcelable {
private constructor(
throwable: Throwable,
userAction: UserAction,
serviceName: String,
request: String
) : this(
throwableToStringList(throwable),
userAction,
serviceName,
request,
getMessageStringId(throwable, userAction),
throwable
)
private constructor(
throwable: List<Throwable>,
userAction: UserAction,
serviceName: String,
request: String
) : this(
throwableListToStringList(throwable),
userAction,
serviceName,
request,
getMessageStringId(throwable.firstOrNull(), userAction),
throwable.firstOrNull()
)
// constructors with single throwable
constructor(throwable: Throwable, userAction: UserAction, request: String) :
this(throwable, userAction, SERVICE_NONE, request)
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
this(throwable, userAction, NewPipe.getNameOfService(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, NewPipe.getNameOfService(serviceId), request)
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
this(throwable, userAction, getInfoServiceName(info), request)
companion object {
const val SERVICE_NONE = "none"
private fun getStackTrace(throwable: Throwable): String {
StringWriter().use { stringWriter ->
PrintWriter(stringWriter, true).use { printWriter ->
throwable.printStackTrace(printWriter)
return stringWriter.buffer.toString()
}
}
}
fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable))
fun throwableListToStringList(throwable: List<Throwable>) =
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
private fun getInfoServiceName(info: Info?) =
if (info == null) SERVICE_NONE else NewPipe.getNameOfService(info.serviceId)
@StringRes
private fun getMessageStringId(
throwable: Throwable?,
action: UserAction
): Int {
return when {
throwable is AccountTerminatedException -> R.string.account_terminated
throwable is ContentNotAvailableException -> R.string.content_not_available
throwable != null && throwable.isNetworkRelated -> R.string.network_error
throwable is ContentNotSupportedException -> R.string.content_not_supported
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
throwable is ExtractionException -> R.string.parsing_error
action == UserAction.UI_ERROR -> R.string.app_ui_crash
action == UserAction.REQUESTED_COMMENTS -> R.string.error_unable_to_load_comments
action == UserAction.SUBSCRIPTION_CHANGE -> R.string.subscription_change_failed
action == UserAction.SUBSCRIPTION_UPDATE -> R.string.subscription_update_failed
action == UserAction.LOAD_IMAGE -> R.string.could_not_load_thumbnails
action == UserAction.DOWNLOAD_OPEN_DIALOG -> R.string.could_not_setup_download_menu
else -> R.string.general_error
}
}
}
}

View File

@@ -0,0 +1,165 @@
package org.schabi.newpipe.error
import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.jakewharton.rxbinding4.view.clicks
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.NewPipe
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.isInterruptedCaused
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
import java.util.concurrent.TimeUnit
class ErrorPanelHelper(
private val fragment: Fragment,
rootView: View,
onRetry: Runnable
) {
private val context: Context = rootView.context!!
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view)
private val errorServiceInfoTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_info_view)
private val errorServiceExplenationTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_explenation_view)
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
private var errorDisposable: Disposable? = null
init {
errorDisposable = errorButtonRetry.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onRetry.run() }
}
fun showError(errorInfo: ErrorInfo) {
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [$errorInfo.throwable]")
}
return
}
errorButtonAction.isVisible = true
if (errorInfo.throwable is ReCaptchaException) {
errorButtonAction.setText(R.string.recaptcha_solve)
errorButtonAction.setOnClickListener {
// Starting ReCaptcha Challenge Activity
val intent = Intent(context, ReCaptchaActivity::class.java)
intent.putExtra(
ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
(errorInfo.throwable as ReCaptchaException).url
)
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
errorButtonAction.setOnClickListener(null)
}
errorTextView.setText(R.string.recaptcha_request_toast)
// additional info is only provided by AccountTerminatedException
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
errorButtonRetry.isVisible = true
} else if (errorInfo.throwable is AccountTerminatedException) {
errorButtonRetry.isVisible = false
errorButtonAction.isVisible = false
errorTextView.setText(R.string.account_terminated)
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
errorServiceInfoTextView.setText(
context.resources.getString(
R.string.service_provides_reason,
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
)
)
errorServiceExplenationTextView.setText(
(errorInfo.throwable as AccountTerminatedException).message
)
errorServiceInfoTextView.isVisible = true
errorServiceExplenationTextView.isVisible = true
} else {
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
}
} else {
errorButtonAction.setText(R.string.error_snackbar_action)
errorButtonAction.setOnClickListener {
ErrorActivity.reportError(context, errorInfo)
}
// additional info is only provided by AccountTerminatedException
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
// hide retry button by default, then show only if not unavailable/unsupported content
errorButtonRetry.isVisible = false
errorTextView.setText(
when (errorInfo.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
errorButtonRetry.isVisible = true
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
R.string.network_error
} else {
R.string.error_snackbar_message
}
}
}
)
}
errorPanelRoot.animate(true, 300)
}
fun showTextError(errorString: String) {
errorButtonAction.isVisible = false
errorButtonRetry.isVisible = false
errorTextView.text = errorString
}
fun hide() {
errorButtonAction.setOnClickListener(null)
errorPanelRoot.animate(false, 150)
}
fun isVisible(): Boolean {
return errorPanelRoot.isVisible
}
fun dispose() {
errorButtonAction.setOnClickListener(null)
errorButtonRetry.setOnClickListener(null)
errorDisposable?.dispose()
}
companion object {
val TAG: String = ErrorPanelHelper::class.simpleName!!
val DEBUG: Boolean = MainActivity.DEBUG
}
}

View File

@@ -1,4 +1,4 @@
package org.schabi.newpipe;
package org.schabi.newpipe.error;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -20,6 +20,9 @@ import androidx.preference.PreferenceManager;
import androidx.webkit.WebViewClientCompat;
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.UnsupportedEncodingException;

View File

@@ -1,4 +1,4 @@
package org.schabi.newpipe.report;
package org.schabi.newpipe.error;
/**
* The user actions that can cause an error.
@@ -6,9 +6,12 @@ package org.schabi.newpipe.report;
public enum UserAction {
USER_REPORT("user report"),
UI_ERROR("ui error"),
SUBSCRIPTION("subscription"),
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"),
SOMETHING_ELSE("something else"),
SEARCHED("searched"),
GET_SUGGESTIONS("get suggestions"),
REQUESTED_STREAM("requested stream"),
@@ -17,11 +20,15 @@ public enum UserAction {
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
REQUESTED_FEED("requested feed"),
REQUESTED_BOOKMARK("bookmark"),
DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream"),
PLAY_STREAM("play stream"),
DOWNLOAD_OPEN_DIALOG("download open dialog"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed"),
PREFERENCES_MIGRATION("migration of preferences");
PREFERENCES_MIGRATION("migration of preferences"),
SHARE_TO_NEWPIPE("share to newpipe"),
CHECK_FOR_NEW_APP_VERSION("check for new app version");
private final String message;

View File

@@ -1,42 +1,23 @@
package org.schabi.newpipe.fragments;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorPanelHelper;
import org.schabi.newpipe.util.InfoCache;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
@@ -50,11 +31,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Nullable
private ProgressBar loadingProgressBar;
private Disposable errorDisposable;
protected View errorPanelRoot;
private Button errorButtonRetry;
private TextView errorTextView;
private ErrorPanelHelper errorPanelHelper;
@Nullable
@State
protected ErrorInfo lastPanelError = null;
@Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
@@ -69,10 +49,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
}
@Override
public void onDestroy() {
super.onDestroy();
if (errorDisposable != null) {
errorDisposable.dispose();
public void onResume() {
super.onResume();
if (lastPanelError != null) {
showError(lastPanelError);
}
}
@@ -83,22 +63,17 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
emptyStateView = rootView.findViewById(R.id.empty_state_view);
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
errorPanelRoot = rootView.findViewById(R.id.error_panel);
errorButtonRetry = rootView.findViewById(R.id.error_button_retry);
errorTextView = rootView.findViewById(R.id.error_message_view);
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
}
@Override
protected void initListeners() {
super.initListeners();
errorDisposable = RxView.clicks(errorButtonRetry)
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(o -> onRetryButtonClicked());
public void onDestroyView() {
super.onDestroyView();
if (errorPanelHelper != null) {
errorPanelHelper.dispose();
}
}
protected void onRetryButtonClicked() {
@@ -137,7 +112,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (loadingProgressBar != null) {
animate(loadingProgressBar, true, 400);
}
animate(errorPanelRoot, false, 150);
hideErrorPanel();
}
@Override
@@ -148,10 +123,9 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0);
}
animate(errorPanelRoot, false, 150);
hideErrorPanel();
}
@Override
public void showEmptyState() {
isLoading.set(false);
if (emptyStateView != null) {
@@ -160,26 +134,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0);
}
animate(errorPanelRoot, false, 150);
}
@Override
public void showError(final String message, final boolean showRetryButton) {
if (DEBUG) {
Log.d(TAG, "showError() called with: "
+ "message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
}
isLoading.set(false);
InfoCache.getInstance().clearCache();
hideLoading();
errorTextView.setText(message);
if (showRetryButton) {
animate(errorButtonRetry, true, 600);
} else {
animate(errorButtonRetry, false, 0);
}
animate(errorPanelRoot, true, 300);
hideErrorPanel();
}
@Override
@@ -190,120 +145,69 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
hideLoading();
}
@Override
public void handleError() {
isLoading.set(false);
InfoCache.getInstance().clearCache();
if (emptyStateView != null) {
animate(emptyStateView, false, 150);
}
if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Error handling
//////////////////////////////////////////////////////////////////////////*/
/**
* Default implementation handles some general exceptions.
*
* @param exception The exception that should be handled
* @return If the exception was handled
*/
protected boolean onError(final Throwable exception) {
if (DEBUG) {
Log.d(TAG, "onError() called with: exception = [" + exception + "]");
}
isLoading.set(false);
public final void showError(final ErrorInfo errorInfo) {
handleError();
if (isDetached() || isRemoving()) {
if (DEBUG) {
Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
Log.w(TAG, "showError() is detached or removing = [" + errorInfo + "]");
}
return true;
return;
}
if (ExceptionUtils.isInterruptedCaused(exception)) {
errorPanelHelper.showError(errorInfo);
lastPanelError = errorInfo;
}
public final void showTextError(@NonNull final String errorString) {
handleError();
if (isDetached() || isRemoving()) {
if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
Log.w(TAG, "showTextError() is detached or removing = [" + errorString + "]");
}
return true;
return;
}
if (exception instanceof ReCaptchaException) {
onReCaptchaException((ReCaptchaException) exception);
return true;
} else if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
return true;
} else if (ExceptionUtils.isNetworkRelated(exception)) {
showError(getString(R.string.network_error), true);
return true;
} else if (exception instanceof ContentNotSupportedException) {
showError(getString(R.string.content_not_supported), false);
return true;
}
return false;
errorPanelHelper.showTextError(errorString);
}
public void onReCaptchaException(final ReCaptchaException exception) {
if (DEBUG) {
Log.d(TAG, "onReCaptchaException() called");
}
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
final Intent intent = new Intent(activity, ReCaptchaActivity.class);
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl());
startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST);
showError(getString(R.string.recaptcha_request_toast), false);
public final void hideErrorPanel() {
errorPanelHelper.hide();
lastPanelError = null;
}
public void onUnrecoverableError(final Throwable exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName,
request, errorId);
}
public void onUnrecoverableError(final List<Throwable> exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
if (DEBUG) {
Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
}
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
request == null ? "none" : request, errorId));
}
public void showSnackBarError(final Throwable exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request,
errorId);
public final boolean isErrorPanelVisible() {
return errorPanelHelper.isVisible();
}
/**
* Show a SnackBar and only call
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorInfo)}
* {@link ErrorActivity#reportErrorInSnackbar(androidx.fragment.app.Fragment, ErrorInfo)}
* IF we a find a valid view (otherwise the error screen appears).
*
* @param exception List of the exceptions to show
* @param userAction The user action that caused the exception
* @param serviceName The service where the exception happened
* @param request The page that was requested
* @param errorId The ID of the error
* @param errorInfo The error information
*/
public void showSnackBarError(final List<Throwable> exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
public void showSnackBarError(final ErrorInfo errorInfo) {
if (DEBUG) {
Log.d(TAG, "showSnackBarError() called with: "
+ "exception = [" + exception + "], userAction = [" + userAction + "], "
+ "request = [" + request + "], errorId = [" + errorId + "]");
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
}
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
if (rootView == null && getView() != null) {
rootView = getView();
}
if (rootView == null) {
return;
}
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
ErrorInfo.make(userAction, serviceName, request, errorId));
ErrorActivity.reportErrorInSnackbar(this, errorInfo);
}
}

View File

@@ -11,9 +11,23 @@ import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
public class EmptyFragment extends BaseFragment {
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
public static final EmptyFragment newInstance(final boolean showMessage) {
final EmptyFragment emptyFragment = new EmptyFragment();
final Bundle bundle = new Bundle(1);
bundle.putBoolean(SHOW_MESSAGE, showMessage);
emptyFragment.setArguments(bundle);
return emptyFragment;
}
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_empty, container, false);
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
view.findViewById(R.id.empty_state_view).setVisibility(
showMessage ? View.VISIBLE : View.GONE);
return view;
}
}

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.fragments;
import android.content.Context;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@@ -14,7 +13,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
@@ -25,15 +23,12 @@ import com.google.android.material.tabs.TabLayout;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentMainBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.List;
@@ -90,10 +85,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
binding = FragmentMainBinding.bind(rootView);
binding.mainTabLayout.setTabIconTint(ColorStateList.valueOf(
ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)));
binding.mainTabLayout.setupWithViewPager(binding.pager);
binding.mainTabLayout.addOnTabSelectedListener(this);
binding.mainTabLayout.setTabRippleColor(binding.mainTabLayout.getTabRippleColor()
.withAlpha(32));
setupTabs();
}
@@ -128,13 +123,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
inflater.inflate(R.menu.main_fragment_menu, menu);
inflater.inflate(R.menu.menu_main_fragment, menu);
final ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
@@ -144,15 +140,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_search:
try {
NavigationHelper.openSearchFragment(getFM(),
ServiceHelper.getSelectedServiceId(activity), "");
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
}
return true;
if (item.getItemId() == R.id.action_search) {
try {
NavigationHelper.openSearchFragment(getFM(),
ServiceHelper.getSelectedServiceId(activity), "");
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Opening search fragment", e);
}
return true;
}
return super.onOptionsItemSelected(item);
}
@@ -241,8 +236,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
if (throwable != null) {
ErrorActivity.reportError(context, throwable, null, null, ErrorInfo
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
ErrorActivity.reportUiErrorInSnackbar(context, "Getting fragment item", throwable);
return new BlankFragment();
}
@@ -254,7 +248,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
@Override
public int getItemPosition(final Object object) {
public int getItemPosition(@NonNull final Object object) {
// Causes adapter to reload all Fragments when
// notifyDataSetChanged is called
return POSITION_NONE;

View File

@@ -7,7 +7,7 @@ public interface ViewContract<I> {
void showEmptyState();
void showError(String message, boolean showRetryButton);
void handleResult(I result);
void handleError();
}

View File

@@ -4,30 +4,45 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.text.HtmlCompat;
import com.google.android.material.chip.Chip;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
import org.schabi.newpipe.databinding.ItemMetadataBinding;
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.TextLinkifier;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.external_communication.TextLinkifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import icepick.State;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
public class DescriptionFragment extends BaseFragment {
@State
StreamInfo streamInfo = null;
@Nullable
Disposable descriptionDisposable = null;
final CompositeDisposable descriptionDisposables = new CompositeDisposable();
FragmentDescriptionBinding binding;
public DescriptionFragment() {
}
@@ -40,54 +55,212 @@ public class DescriptionFragment extends BaseFragment {
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
final FragmentDescriptionBinding binding =
FragmentDescriptionBinding.inflate(inflater, container, false);
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
if (streamInfo != null) {
setupUploadDate(binding.detailUploadDateView);
setupDescription(binding.detailDescriptionView);
setupUploadDate();
setupDescription();
setupMetadata(inflater, binding.detailMetadataLayout);
}
return binding.getRoot();
}
@Override
public void onDestroy() {
descriptionDisposables.clear();
super.onDestroy();
if (descriptionDisposable != null) {
descriptionDisposable.dispose();
}
}
private void setupUploadDate(final TextView uploadDateTextView) {
private void setupUploadDate() {
if (streamInfo.getUploadDate() != null) {
uploadDateTextView.setText(Localization
binding.detailUploadDateView.setText(Localization
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
} else {
uploadDateTextView.setVisibility(View.GONE);
binding.detailUploadDateView.setVisibility(View.GONE);
}
}
private void setupDescription(final TextView descriptionTextView) {
private void setupDescription() {
final Description description = streamInfo.getDescription();
if (description == null || isEmpty(description.getContent())
|| description == Description.emptyDescription) {
descriptionTextView.setText("");
binding.detailDescriptionView.setVisibility(View.GONE);
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
return;
}
// start with disabled state. This also loads description content (!)
disableDescriptionSelection();
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
disableDescriptionSelection();
} else {
// enable selection only when button is clicked to prevent flickering
enableDescriptionSelection();
}
});
}
private void enableDescriptionSelection() {
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
binding.detailDescriptionView.setTextIsSelectable(true);
final String buttonLabel = getString(R.string.description_select_disable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
}
private void disableDescriptionSelection() {
// show description content again, otherwise some links are not clickable
loadDescriptionContent();
binding.detailDescriptionNoteView.setVisibility(View.GONE);
binding.detailDescriptionView.setTextIsSelectable(false);
final String buttonLabel = getString(R.string.description_select_enable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
}
private void loadDescriptionContent() {
final Description description = streamInfo.getDescription();
switch (description.getType()) {
case Description.HTML:
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(),
description.getContent(), descriptionTextView,
HtmlCompat.FROM_HTML_MODE_LEGACY);
TextLinkifier.createLinksFromHtmlBlock(binding.detailDescriptionView,
description.getContent(), HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo,
descriptionDisposables);
break;
case Description.MARKDOWN:
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(),
description.getContent(), descriptionTextView);
TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView,
description.getContent(), streamInfo, descriptionDisposables);
break;
case Description.PLAIN_TEXT: default:
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(),
description.getContent(), descriptionTextView);
TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView,
description.getContent(), streamInfo, descriptionDisposables);
break;
}
}
private void setupMetadata(final LayoutInflater inflater,
final LinearLayout layout) {
addMetadataItem(inflater, layout, false,
R.string.metadata_category, streamInfo.getCategory());
addTagsMetadataItem(inflater, layout);
addMetadataItem(inflater, layout, false,
R.string.metadata_licence, streamInfo.getLicence());
addPrivacyMetadataItem(inflater, layout);
if (streamInfo.getAgeLimit() != NO_AGE_LIMIT) {
addMetadataItem(inflater, layout, false,
R.string.metadata_age_limit, String.valueOf(streamInfo.getAgeLimit()));
}
if (streamInfo.getLanguageInfo() != null) {
addMetadataItem(inflater, layout, false,
R.string.metadata_language, streamInfo.getLanguageInfo().getDisplayLanguage());
}
addMetadataItem(inflater, layout, true,
R.string.metadata_support, streamInfo.getSupportInfo());
addMetadataItem(inflater, layout, true,
R.string.metadata_host, streamInfo.getHost());
addMetadataItem(inflater, layout, true,
R.string.metadata_thumbnail_url, streamInfo.getThumbnailUrl());
}
private void addMetadataItem(final LayoutInflater inflater,
final LinearLayout layout,
final boolean linkifyContent,
@StringRes final int type,
@Nullable final String content) {
if (isBlank(content)) {
return;
}
final ItemMetadataBinding itemBinding
= ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
ShareUtils.copyToClipboard(requireContext(), content);
return true;
});
if (linkifyContent) {
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null,
descriptionDisposables);
} else {
itemBinding.metadataContentView.setText(content);
}
layout.addView(itemBinding.getRoot());
}
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
final ItemMetadataTagsBinding itemBinding
= ItemMetadataTagsBinding.inflate(inflater, layout, false);
final List<String> tags = new ArrayList<>(streamInfo.getTags());
Collections.sort(tags);
for (final String tag : tags) {
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
itemBinding.metadataTagsChips, false);
chip.setText(tag);
chip.setOnClickListener(this::onTagClick);
chip.setOnLongClickListener(this::onTagLongClick);
itemBinding.metadataTagsChips.addView(chip);
}
layout.addView(itemBinding.getRoot());
}
}
private void onTagClick(final View chip) {
if (getParentFragment() != null) {
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
streamInfo.getServiceId(), ((Chip) chip).getText().toString());
}
}
private boolean onTagLongClick(final View chip) {
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
return true;
}
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
if (streamInfo.getPrivacy() != null) {
@StringRes final int contentRes;
switch (streamInfo.getPrivacy()) {
case PUBLIC:
contentRes = R.string.metadata_privacy_public;
break;
case UNLISTED:
contentRes = R.string.metadata_privacy_unlisted;
break;
case PRIVATE:
contentRes = R.string.metadata_privacy_private;
break;
case INTERNAL:
contentRes = R.string.metadata_privacy_internal;
break;
case OTHER: default:
contentRes = 0;
break;
}
if (contentRes != 0) {
addMetadataItem(inflater, layout, false,
R.string.metadata_privacy, getString(contentRes));
}
}
}
}

View File

@@ -37,12 +37,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.ExoPlaybackException;
@@ -56,14 +54,16 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
@@ -71,8 +71,9 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
@@ -86,19 +87,16 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
@@ -151,10 +149,11 @@ public final class VideoDetailFragment
private static final String COMMENTS_TAB_TAG = "COMMENTS";
private static final String RELATED_TAB_TAG = "NEXT VIDEO";
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
// tabs
private boolean showComments;
private boolean showRelatedStreams;
private boolean showRelatedItems;
private boolean showDescription;
private String selectedTabTag;
@AttrRes @NonNull final List<Integer> tabIcons = new ArrayList<>();
@@ -281,7 +280,7 @@ public final class VideoDetailFragment
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
showComments = prefs.getBoolean(getString(R.string.show_comments_key), true);
showRelatedStreams = prefs.getBoolean(getString(R.string.show_next_video_key), true);
showRelatedItems = prefs.getBoolean(getString(R.string.show_next_video_key), true);
showDescription = prefs.getBoolean(getString(R.string.show_description_key), true);
selectedTabTag = prefs.getString(
getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG);
@@ -414,7 +413,7 @@ public final class VideoDetailFragment
showComments = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (key.equals(getString(R.string.show_next_video_key))) {
showRelatedStreams = sharedPreferences.getBoolean(key, true);
showRelatedItems = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (key.equals(getString(R.string.show_description_key))) {
showComments = sharedPreferences.getBoolean(key, true);
@@ -455,8 +454,8 @@ public final class VideoDetailFragment
break;
case R.id.detail_controls_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(),
currentInfo.getName(), currentInfo.getUrl());
ShareUtils.shareText(requireContext(), currentInfo.getName(),
currentInfo.getUrl(), currentInfo.getThumbnailUrl());
}
break;
case R.id.detail_controls_open_in_browser:
@@ -473,7 +472,7 @@ public final class VideoDetailFragment
if (DEBUG) {
Log.i(TAG, "Failed to start kore", e);
}
KoreUtil.showInstallKoreDialog(requireContext());
KoreUtils.showInstallKoreDialog(requireContext());
}
}
break;
@@ -526,7 +525,7 @@ public final class VideoDetailFragment
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
subChannelUrl, subChannelName);
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
}
}
@@ -632,7 +631,7 @@ public final class VideoDetailFragment
binding.detailControlsShare.setOnClickListener(this);
binding.detailControlsOpenInBrowser.setOnClickListener(this);
binding.detailControlsPlayWithKodi.setOnClickListener(this);
binding.detailControlsPlayWithKodi.setVisibility(KoreUtil.shouldShowPlayWithKodi(
binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi(
requireContext(), serviceId) ? View.VISIBLE : View.GONE);
binding.overlayThumbnail.setOnClickListener(this);
@@ -684,13 +683,12 @@ public final class VideoDetailFragment
binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!isEmpty(info.getThumbnailUrl())) {
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
@Override
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE,
imageUri, info));
}
};
@@ -906,10 +904,8 @@ public final class VideoDetailFragment
openVideoPlayer();
}
}
}, throwable -> {
isLoading.set(false);
onError(throwable);
});
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
url == null ? "no url" : url, serviceId)));
}
/*//////////////////////////////////////////////////////////////////////////
@@ -927,23 +923,27 @@ public final class VideoDetailFragment
if (shouldShowComments()) {
pageAdapter.addFragment(
CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG);
tabIcons.add(R.drawable.ic_comment_white_24dp);
tabIcons.add(R.drawable.ic_comment);
tabContentDescriptions.add(R.string.comments_tab_description);
}
if (showRelatedStreams && binding.relatedStreamsLayout == null) {
//temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG);
tabIcons.add(R.drawable.ic_art_track_white_24dp);
tabContentDescriptions.add(R.string.related_streams_tab_description);
if (showRelatedItems && binding.relatedItemsLayout == null) {
// temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(EmptyFragment.newInstance(false), RELATED_TAB_TAG);
tabIcons.add(R.drawable.ic_art_track);
tabContentDescriptions.add(R.string.related_items_tab_description);
}
if (showDescription) {
// temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(new Fragment(), DESCRIPTION_TAB_TAG);
tabIcons.add(R.drawable.ic_description_white_24dp);
pageAdapter.addFragment(EmptyFragment.newInstance(false), DESCRIPTION_TAB_TAG);
tabIcons.add(R.drawable.ic_description);
tabContentDescriptions.add(R.string.description_tab_description);
}
if (pageAdapter.getCount() == 0) {
pageAdapter.addFragment(EmptyFragment.newInstance(true), EMPTY_TAB_TAG);
}
pageAdapter.notifyDataSetUpdate();
if (pageAdapter.getCount() >= 2) {
@@ -974,14 +974,14 @@ public final class VideoDetailFragment
}
private void updateTabs(@NonNull final StreamInfo info) {
if (showRelatedStreams) {
if (binding.relatedStreamsLayout == null) { // phone
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(info));
if (showRelatedItems) {
if (binding.relatedItemsLayout == null) { // phone
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedItemsFragment.getInstance(info));
} else { // tablet + TV
getChildFragmentManager().beginTransaction()
.replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(info))
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
.commitAllowingStateLoss();
binding.relatedStreamsLayout.setVisibility(
binding.relatedItemsLayout.setVisibility(
player != null && player.isFullscreen() ? View.GONE : View.VISIBLE);
}
}
@@ -1009,6 +1009,12 @@ public final class VideoDetailFragment
}
public void updateTabLayoutVisibility() {
if (binding == null) {
//If binding is null we do not need to and should not do anything with its object(s)
return;
}
if (pageAdapter.getCount() < 2 || binding.viewPager.getVisibility() != View.VISIBLE) {
// hide tab layout if there is only one tab or if the view pager is also hidden
binding.tabLayout.setVisibility(View.GONE);
@@ -1327,12 +1333,12 @@ public final class VideoDetailFragment
}
@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
public void handleError() {
super.handleError();
setErrorImage(R.drawable.not_available_monkey);
if (binding.relatedStreamsLayout != null) { // hide related streams for tablets
binding.relatedStreamsLayout.setVisibility(View.INVISIBLE);
if (binding.relatedItemsLayout != null) { // hide related streams for tablets
binding.relatedItemsLayout.setVisibility(View.INVISIBLE);
}
// hide comments / related streams / description tabs
@@ -1341,8 +1347,8 @@ public final class VideoDetailFragment
}
private void hideAgeRestrictedContent() {
showError(getString(R.string.restricted_video,
getString(R.string.show_age_restricted_content_title)), false);
showTextError(getString(R.string.restricted_video,
getString(R.string.show_age_restricted_content_title)));
}
private void setupBroadcastReceiver() {
@@ -1426,12 +1432,12 @@ public final class VideoDetailFragment
binding.detailTitleRootLayout.setClickable(false);
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
if (binding.relatedStreamsLayout != null) {
if (showRelatedStreams) {
binding.relatedStreamsLayout.setVisibility(
if (binding.relatedItemsLayout != null) {
if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility(
player != null && player.isFullscreen() ? View.GONE : View.INVISIBLE);
} else {
binding.relatedStreamsLayout.setVisibility(View.GONE);
binding.relatedItemsLayout.setVisibility(View.GONE);
}
}
@@ -1540,19 +1546,27 @@ public final class VideoDetailFragment
.getDefaultResolutionIndex(activity, sortedVideoStreams);
updateProgressInfo(info);
initThumbnailViews(info);
disposables.add(showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator));
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables);
if (player == null || player.isStopped()) {
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
}
if (!info.getErrors().isEmpty()) {
showSnackBarError(info.getErrors(),
UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(info.getServiceId()),
info.getUrl(),
0);
// Bandcamp fan pages are not yet supported and thus a ContentNotAvailableException is
// thrown. This is not an error and thus should not be shown to the user.
for (final Throwable throwable : info.getErrors()) {
if (throwable instanceof ContentNotSupportedException
&& "Fan pages are not supported".equals(throwable.getMessage())) {
info.getErrors().remove(throwable);
}
}
if (!info.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(info.getErrors(),
UserAction.REQUESTED_STREAM, info.getUrl(), info));
}
}
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
@@ -1592,6 +1606,10 @@ public final class VideoDetailFragment
}
public void openDownloadDialog() {
if (currentInfo == null) {
return;
}
try {
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams);
@@ -1601,18 +1619,9 @@ public final class VideoDetailFragment
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) {
final ErrorInfo info = ErrorInfo.make(UserAction.UI_ERROR,
ServiceList.all()
.get(currentInfo
.getServiceId())
.getServiceInfo()
.getName(), "",
R.string.could_not_setup_download_menu);
ErrorActivity.reportError(activity,
e,
activity.getClass(),
activity.findViewById(android.R.id.content), info);
ErrorActivity.reportErrorInSnackbar(activity,
new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog",
currentInfo));
}
}
@@ -1620,24 +1629,6 @@ public final class VideoDetailFragment
// Stream Results
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
final int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException
? R.string.youtube_signature_deobfuscation_error
: exception instanceof ExtractionException
? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}
private void updateProgressInfo(@NonNull final StreamInfo info) {
if (positionSubscriber != null) {
positionSubscriber.dispose();
@@ -1678,7 +1669,7 @@ public final class VideoDetailFragment
.onErrorComplete()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> {
showPlaybackProgress(state.getProgressTime(), info.getDuration() * 1000);
showPlaybackProgress(state.getProgressMillis(), info.getDuration() * 1000);
animate(binding.positionView, true, 500);
animate(binding.detailPositionView, true, 500);
}, e -> {
@@ -1853,12 +1844,13 @@ public final class VideoDetailFragment
if (fullscreen) {
hideSystemUiIfNeeded();
binding.overlayPlayPauseButton.requestFocus();
} else {
showSystemUi();
}
if (binding.relatedStreamsLayout != null) {
binding.relatedStreamsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
if (binding.relatedItemsLayout != null) {
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
}
scrollToTop();
@@ -2278,7 +2270,7 @@ public final class VideoDetailFragment
private void updateOverlayData(@Nullable final String overlayTitle,
@Nullable final String uploader,
@Nullable final String thumbnailUrl) {
binding.overlayTitleTextView.setText(isEmpty(title) ? "" : title);
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!isEmpty(thumbnailUrl)) {
@@ -2288,11 +2280,10 @@ public final class VideoDetailFragment
}
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
final int attr = playerIsPlaying
? R.attr.ic_pause
: R.attr.ic_play_arrow;
binding.overlayPlayPauseButton.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(activity, attr));
final int drawable = playerIsPlaying
? R.drawable.ic_pause
: R.drawable.ic_play_arrow;
binding.overlayPlayPauseButton.setImageResource(drawable);
}
private void setOverlayLook(final AppBarLayout appBar,

View File

@@ -14,7 +14,6 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -22,6 +21,7 @@ import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
@@ -33,8 +33,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver;
@@ -46,7 +45,9 @@ import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, StateSaver.WriteRead,
@@ -124,8 +125,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
/**
* If the default implementation of {@link StateSaver.WriteRead} should be used.
*
* @see StateSaver
* @param useDefaultStateSaving Whether the default implementation should be used
* @see StateSaver
*/
public void setUseDefaultStateSaving(final boolean useDefaultStateSaving) {
this.useDefaultStateSaving = useDefaultStateSaving;
@@ -292,7 +293,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiErrorInSnackbar(
BaseListFragment.this, "Opening channel fragment", e);
}
}
});
@@ -307,7 +309,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiErrorInSnackbar(BaseListFragment.this,
"Opening playlist fragment", e);
}
}
});
@@ -348,7 +351,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
return;
}
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
final List<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) {
entries.add(StreamDialogEntry.enqueue);
@@ -359,7 +362,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
StreamDialogEntry.append_playlist,
StreamDialogEntry.share
));
} else {
} else {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
@@ -367,9 +370,14 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
StreamDialogEntry.share
));
}
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
if (!isNullOrEmpty(item.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
StreamDialogEntry.setEnabledEntries(entries);
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
@@ -381,7 +389,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
@@ -406,23 +415,23 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animateHideRecyclerViewAllowingScrolling(itemsList);
}
@Override
public void hideLoading() {
super.hideLoading();
animate(itemsList, true, 300);
}
@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
animate(itemsList, false, 200);
}
@Override
public void showEmptyState() {
super.showEmptyState();
showListFooter(false);
animateHideRecyclerViewAllowingScrolling(itemsList);
}
@Override
@@ -439,6 +448,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
isLoading.set(false);
}
@Override
public void handleError() {
super.handleError();
showListFooter(false);
animateHideRecyclerViewAllowingScrolling(itemsList);
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) {

View File

@@ -7,12 +7,18 @@ import android.view.View;
import androidx.annotation.NonNull;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.views.NewPipeRecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import icepick.State;
@@ -30,10 +36,15 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@State
protected String url;
private final UserAction errorUserAction;
protected I currentInfo;
protected Page currentNextPage;
protected Disposable currentWorker;
protected BaseListInfoFragment(final UserAction errorUserAction) {
this.errorUserAction = errorUserAction;
}
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
@@ -133,7 +144,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
currentInfo = result;
currentNextPage = result.getNextPage();
handleResult(result);
}, this::onError);
}, throwable ->
showError(new ErrorInfo(throwable, errorUserAction,
"Start loading: " + url, serviceId)));
}
/**
@@ -161,10 +174,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
isLoading.set(false);
handleNextItems(InfoItemsPage);
}, (@NonNull Throwable throwable) -> {
isLoading.set(false);
onError(throwable);
});
}, (@NonNull Throwable throwable) ->
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
errorUserAction, "Loading more items: " + url, serviceId)));
}
private void forbidDownwardFocusScroll() {
@@ -182,10 +194,16 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
currentNextPage = result.getNextPage();
infoListAdapter.addInfoItemList(result.getItems());
showListFooter(hasMoreItems());
if (!result.getErrors().isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
"Get next items of: " + url, serviceId));
}
}
@Override
@@ -210,7 +228,23 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
showListFooter(hasMoreItems());
} else {
infoListAdapter.clearStreamItemList();
showEmptyState();
// showEmptyState should be called only if there is no item as
// well as no header in infoListAdapter
if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) {
showEmptyState();
}
}
}
if (!result.getErrors().isEmpty()) {
final List<Throwable> errors = new ArrayList<>(result.getErrors());
// handling ContentNotSupportedException not to show the error but an appropriate string
// so that crashes won't be sent uselessly and the user will understand what happened
errors.removeIf(throwable -> throwable instanceof ContentNotSupportedException);
if (!errors.isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
errorUserAction, "Start loading: " + url, serviceId));
}
}
}
@@ -224,4 +258,14 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
this.url = u;
this.name = !TextUtils.isEmpty(title) ? title : "";
}
private void dynamicallyShowErrorPanelOrSnackbar(final ErrorInfo errorInfo) {
if (infoListAdapter.getItemCount() == 0) {
// show error panel only if no items already visible
showError(errorInfo);
} else {
isLoading.set(false);
showSnackBarError(errorInfo);
}
}
}

View File

@@ -16,7 +16,6 @@ import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.viewbinding.ViewBinding;
@@ -27,25 +26,24 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
import org.schabi.newpipe.databinding.FragmentChannelBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
@@ -91,6 +89,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
return instance;
}
public ChannelFragment() {
super(UserAction.REQUESTED_CHANNEL);
}
@Override
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
@@ -162,7 +164,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
final ActionBar supportActionBar = activity.getSupportActionBar();
if (useAsFrontPage && supportActionBar != null) {
@@ -201,7 +204,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl());
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
currentInfo.getAvatarUrl());
}
break;
default:
@@ -217,9 +221,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> {
animate(headerBinding.channelSubscribeButton, false, 100);
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Get subscription status", 0);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
"Get subscription status", currentInfo));
};
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
@@ -269,11 +272,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
};
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
onUnrecoverableError(throwable,
UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(info.getServiceId()),
"Updating Subscription for " + info.getUrl(),
R.string.subscription_update_failed);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE,
"Updating subscription for " + info.getUrl(), info));
disposables.add(subscriptionManager.updateChannelInfo(info)
.subscribeOn(Schedulers.io())
@@ -290,11 +290,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
};
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
onUnrecoverableError(throwable,
UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Subscription Change",
R.string.subscription_change_failed);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_CHANGE,
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
/* Emit clicks from main thread unto io thread */
return RxView.clicks(subscribeButton)
@@ -408,7 +405,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
currentInfo.getParentChannelUrl(),
currentInfo.getParentChannelName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
}
} else if (DEBUG) {
Log.i(TAG, "Can't open parent channel because we got no channel URL");
@@ -454,8 +451,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
headerBinding.subChannelTitleView.setText(String.format(
getString(R.string.channel_created_by),
currentInfo.getParentChannelName())
getString(R.string.channel_created_by),
currentInfo.getParentChannelName())
);
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
@@ -467,29 +464,21 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
}
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
// PlaylistControls should be visible only if there is some item in
// infoListAdapter other than header
if (infoListAdapter.getItemCount() != 1) {
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
} else {
playlistControlBinding.getRoot().setVisibility(View.GONE);
}
final List<Throwable> errors = new ArrayList<>(result.getErrors());
if (!errors.isEmpty()) {
// handling ContentNotSupportedException not to show the error but an appropriate string
// so that crashes won't be sent uselessly and the user will understand what happened
errors.removeIf(throwable -> {
if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported();
}
return throwable instanceof ContentNotSupportedException;
});
if (!errors.isEmpty()) {
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
for (final Throwable throwable : result.getErrors()) {
if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported();
}
}
if (disposables != null) {
disposables.clear();
}
disposables.clear();
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
@@ -539,38 +528,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
currentInfo.getNextPage(), streamItems, index);
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
final int errorId = exception instanceof ExtractionException
? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/

View File

@@ -11,12 +11,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import io.reactivex.rxjava3.core.Single;
@@ -25,13 +24,17 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
private final CompositeDisposable disposables = new CompositeDisposable();
public static CommentsFragment getInstance(final int serviceId, final String url,
public static CommentsFragment getInstance(final int serviceId, final String url,
final String name) {
final CommentsFragment instance = new CommentsFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
public CommentsFragment() {
super(UserAction.REQUESTED_COMMENTS);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -67,52 +70,13 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
}
@Override
public void handleResult(@NonNull final CommentsInfo result) {
super.handleResult(result);
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
disposables.clear();
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId), "Get next page of: " + url,
R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -121,7 +85,8 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
public void setTitle(final String title) { }
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { }
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) { }
@Override
protected boolean isGridLayout() {

View File

@@ -2,14 +2,16 @@ package org.schabi.newpipe.fragments.list.kiosk;
import android.os.Bundle;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskList;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ServiceHelper;
public class DefaultKioskFragment extends KioskFragment {
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -46,8 +48,8 @@ public class DefaultKioskFragment extends KioskFragment {
currentInfo = null;
currentNextPage = null;
} catch (final ExtractionException e) {
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none",
"Loading default kiosk from selected service", 0);
showError(new ErrorInfo(e, UserAction.REQUESTED_KIOSK,
"Loading default kiosk for selected service"));
}
}
}

View File

@@ -12,6 +12,8 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
@@ -20,7 +22,6 @@ import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization;
@@ -28,8 +29,6 @@ import org.schabi.newpipe.util.Localization;
import icepick.State;
import io.reactivex.rxjava3.core.Single;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
/**
* Created by Christian Schabesberger on 23.09.17.
* <p>
@@ -82,6 +81,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
return instance;
}
public KioskFragment() {
super(UserAction.REQUESTED_KIOSK);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -102,9 +105,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
try {
setTitle(kioskTranslatedName);
} catch (final Exception e) {
onUnrecoverableError(e, UserAction.UI_ERROR,
"none",
"none", R.string.app_ui_crash);
showSnackBarError(new ErrorInfo(e, UserAction.UI_ERROR, "Setting kiosk title"));
}
}
}
@@ -130,7 +131,8 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
final ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null && useAsFrontPage) {
@@ -157,34 +159,11 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animate(itemsList, false, 100);
}
@Override
public void handleResult(@NonNull final KioskInfo result) {
super.handleResult(result);
name = kioskTranslatedName;
setTitle(kioskTranslatedName);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_KIOSK,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId),
"Get next page of: " + url, 0);
}
}
}

View File

@@ -14,7 +14,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.viewbinding.ViewBinding;
@@ -25,11 +24,12 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@@ -40,14 +40,12 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.StreamDialogEntry;
import java.util.ArrayList;
@@ -61,8 +59,9 @@ import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private CompositeDisposable disposables;
@@ -87,6 +86,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
return instance;
}
public PlaylistFragment() {
super(UserAction.REQUESTED_PLAYLIST);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -158,9 +161,15 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
StreamDialogEntry.share
));
}
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
if (!isNullOrEmpty(item.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
@@ -172,7 +181,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
@@ -242,7 +252,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url);
ShareUtils.shareText(requireContext(), name, url, currentInfo.getThumbnailUrl());
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
@@ -262,7 +272,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
public void showLoading() {
super.showLoading();
animate(headerBinding.getRoot(), false, 200);
animate(itemsList, false, 100);
animateHideRecyclerViewAllowingScrolling(itemsList);
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
animate(headerBinding.uploaderLayout, false, 200);
@@ -284,7 +294,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
result.getUploaderUrl(), result.getUploaderName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
}
});
}
@@ -304,7 +314,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
getResources().getColor(R.color.transparent_background_color));
headerBinding.uploaderAvatarView.setImageDrawable(
AppCompatResources.getDrawable(requireContext(),
resolveResourceIdFromAttr(requireContext(), R.attr.ic_radio))
R.drawable.ic_radio)
);
} else {
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView,
@@ -315,8 +325,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
.localizeStreamCount(getContext(), result.getStreamCount()));
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
result.getUrl(), result));
}
remotePlaylistManager.getPlaylist(result)
@@ -363,33 +373,6 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
);
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(serviceId), "Get next page of: " + url, 0);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
final int errorId = exception instanceof ExtractionException
? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -434,8 +417,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
@Override
public void onError(final Throwable t) {
PlaylistFragment.this.onError(t);
public void onError(final Throwable throwable) {
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Get playlist bookmarks"));
}
@Override
@@ -446,7 +430,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@Override
public void setTitle(final String title) {
super.setTitle(title);
headerBinding.playlistTitleView.setText(title);
if (headerBinding != null) {
headerBinding.playlistTitleView.setText(title);
}
}
private void onBookmarkClicked() {
@@ -460,12 +446,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
if (currentInfo != null && playlistEntity == null) {
action = remotePlaylistManager.onBookmark(currentInfo)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
.subscribe(ignored -> { /* Do nothing */ }, throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Adding playlist bookmark")));
} else if (playlistEntity != null) {
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
.observeOn(AndroidSchedulers.mainThread())
.doFinally(() -> playlistEntity = null)
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
.subscribe(ignored -> { /* Do nothing */ }, throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Deleting playlist bookmark")));
} else {
action = Disposable.empty();
}
@@ -478,13 +468,13 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
return;
}
final int iconAttr = playlistEntity == null
? R.attr.ic_playlist_add : R.attr.ic_playlist_check;
final int drawable = playlistEntity == null
? R.drawable.ic_playlist_add : R.drawable.ic_playlist_add_check;
final int titleRes = playlistEntity == null
? R.string.bookmark_playlist : R.string.unbookmark_playlist;
playlistBookmarkButton.setIcon(resolveResourceIdFromAttr(activity, iconAttr));
playlistBookmarkButton.setIcon(drawable);
playlistBookmarkButton.setTitle(titleRes);
}
}

View File

@@ -9,6 +9,7 @@ import android.text.Editable;
import android.text.Html;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.CharacterStyle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -35,16 +36,18 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.databinding.FragmentSearchBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
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.SearchInfo;
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
@@ -54,9 +57,6 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -140,7 +140,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@State
boolean wasSearchFocused = false;
private Map<Integer, String> menuItemToFilterName;
@Nullable private Map<Integer, String> menuItemToFilterName = null;
private StreamingService service;
private Page nextPage;
private boolean isSuggestionsEnabled = true;
@@ -162,11 +162,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
private EditText searchEditText;
private View searchClear;
private TextView correctSuggestion;
private TextView metaInfoTextView;
private View metaInfoSeparator;
private View suggestionsPanel;
private boolean suggestionsPanelVisible = false;
/*////////////////////////////////////////////////////////////////////////*/
@@ -232,6 +227,25 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
initSearchListeners();
}
private void updateService() {
try {
service = NewPipe.getService(serviceId);
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this,
"Getting service for id " + serviceId, e);
}
}
@Override
public void onStart() {
if (DEBUG) {
Log.d(TAG, "onStart() called");
}
super.onStart();
updateService();
}
@Override
public void onPause() {
super.onPause();
@@ -255,23 +269,19 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
super.onResume();
try {
service = NewPipe.getService(serviceId);
} catch (final Exception e) {
ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(),
requireActivity().findViewById(android.R.id.content),
ErrorInfo.make(UserAction.UI_ERROR,
"",
"", R.string.general_error));
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver();
}
if (!TextUtils.isEmpty(searchString)) {
if (wasLoading.getAndSet(false)) {
search(searchString, contentFilter, sortFilter);
return;
} else if (infoListAdapter.getItemsList().isEmpty()) {
if (savedState == null) {
search(searchString, contentFilter, sortFilter);
} else if (!isLoading.get() && !wasSearchFocused) {
return;
} else if (!isLoading.get() && !wasSearchFocused && lastPanelError == null) {
infoListAdapter.clearStreamItemList();
showEmptyState();
}
@@ -280,12 +290,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
handleSearchSuggestion();
disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
metaInfoTextView, metaInfoSeparator));
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver();
}
showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator,
disposables);
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
showKeyboardSearch();
@@ -367,10 +374,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
correctSuggestion = rootView.findViewById(R.id.correct_suggestion);
metaInfoTextView = rootView.findViewById(R.id.search_meta_info_text_view);
metaInfoSeparator = rootView.findViewById(R.id.search_meta_info_separator);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -413,7 +416,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchEditText.setText("");
showKeyboardSearch();
}
animate(errorPanelRoot, false, 200);
hideErrorPanel();
}
}
@@ -422,7 +425,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
final ActionBar supportActionBar = activity.getSupportActionBar();
@@ -436,6 +440,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
int itemId = 0;
boolean isFirstItem = true;
final Context c = getContext();
if (service == null) {
Log.w(TAG, "onCreateOptionsMenu() called with null service");
updateService();
}
for (final String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
if (filter.equals(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS)) {
final MenuItem musicItem = menu.add(2,
@@ -466,11 +476,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final List<String> cf = new ArrayList<>(1);
cf.add(menuItemToFilterName.get(item.getItemId()));
changeContentFilter(item, cf);
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (menuItemToFilterName != null) {
final List<String> cf = new ArrayList<>(1);
cf.add(menuItemToFilterName.get(item.getItemId()));
changeContentFilter(item, cf);
}
return true;
}
@@ -497,6 +508,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
+ lastSearchedString);
}
searchEditText.setText(searchString);
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
searchEditText.setHintTextColor(searchEditText.getTextColors().withAlpha(128));
}
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
searchToolbarContainer.setTranslationX(100);
@@ -540,7 +554,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]");
}
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
if (isSuggestionsEnabled && !isErrorPanelVisible()) {
showSuggestionsPanel();
}
if (DeviceUtils.isTv(getContext())) {
@@ -553,8 +567,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "onFocusChange() called with: "
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
}
if (isSuggestionsEnabled && hasFocus
&& errorPanelRoot.getVisibility() != View.VISIBLE) {
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) {
showSuggestionsPanel();
}
});
@@ -596,6 +609,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
public void afterTextChanged(final Editable s) {
// Remove rich text formatting
for (final CharacterStyle span : s.getSpans(0, s.length(), CharacterStyle.class)) {
s.removeSpan(span);
}
final String newText = searchEditText.getText().toString();
suggestionPublisher.onNext(newText);
}
@@ -704,9 +722,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.DELETE_FROM_HISTORY, "none",
"Deleting item failed", R.string.general_error));
throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY,
"Deleting item failed")));
disposables.add(onDelete);
})
.show();
@@ -733,14 +751,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
suggestionDisposable.dispose();
}
final Observable<String> observable = suggestionPublisher
suggestionDisposable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWithItem(searchString != null
? searchString
: "")
.filter(ss -> isSuggestionsEnabled);
suggestionDisposable = observable
.filter(ss -> isSuggestionsEnabled)
.switchMap(query -> {
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
.getRelatedSearches(query, 3, 25);
@@ -763,8 +779,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.suggestionsFor(serviceId, query)
.onErrorReturn(throwable -> {
if (!ExceptionUtils.isNetworkRelated(throwable)) {
showSnackBarError(throwable, UserAction.GET_SUGGESTIONS,
NewPipe.getNameOfService(serviceId), searchString, 0);
showSnackBarError(new ErrorInfo(throwable,
UserAction.GET_SUGGESTIONS, searchString, serviceId));
}
return new ArrayList<>();
})
@@ -800,7 +816,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (listNotification.isOnNext()) {
handleSuggestions(listNotification.getValue());
} else if (listNotification.isOnError()) {
onSuggestionError(listNotification.getError());
showError(new ErrorInfo(listNotification.getError(),
UserAction.GET_SUGGESTIONS, searchString, serviceId));
}
});
}
@@ -832,8 +849,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribe(intent -> {
getFM().popBackStackImmediate();
activity.startActivity(intent);
}, throwable ->
showError(getString(R.string.unsupported_url), false)));
}, throwable -> showTextError(getString(R.string.unsupported_url))));
return;
}
} catch (final Exception ignored) {
@@ -844,15 +860,16 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
this.searchString = theSearchString;
infoListAdapter.clearStreamItemList();
hideSuggestionsPanel();
showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoSeparator, disposables);
hideKeyboardSearch();
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> {
},
error -> showSnackBarError(error, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), theSearchString, 0)
ignored -> { },
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
theSearchString, serviceId))
));
suggestionPublisher.onNext(theSearchString);
startLoading(false);
@@ -872,7 +889,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
.subscribe(this::handleResult, this::onError);
.subscribe(this::handleResult, this::onItemError);
}
@@ -895,7 +912,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
.subscribe(this::handleNextItems, this::onError);
.subscribe(this::handleNextItems, this::onItemError);
}
@Override
@@ -909,6 +926,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
hideKeyboardSearch();
}
private void onItemError(final Throwable exception) {
if (exception instanceof SearchExtractor.NothingFoundException) {
infoListAdapter.clearStreamItemList();
showEmptyState();
} else {
showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId));
}
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -945,26 +971,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchBinding.suggestionsList.smoothScrollToPosition(0);
searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));
if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) {
if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading();
}
}
public void onSuggestionError(final Throwable exception) {
if (DEBUG) {
Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
}
if (super.onError(exception)) {
return;
}
final int errorId = exception instanceof ParsingException
? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS,
NewPipe.getNameOfService(serviceId), searchString, errorId);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@@ -975,13 +986,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
showListFooter(false);
}
@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
hideSuggestionsPanel();
hideKeyboardSearch();
}
/*//////////////////////////////////////////////////////////////////////////
// Search Results
//////////////////////////////////////////////////////////////////////////*/
@@ -992,8 +996,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (!exceptions.isEmpty()
&& !(exceptions.size() == 1
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), searchString, 0);
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
searchString, serviceId));
}
searchSuggestion = result.getSearchSuggestion();
@@ -1002,8 +1006,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// List<MetaInfo> cannot be bundled without creating some containers
metaInfo = new MetaInfo[result.getMetaInfo().size()];
metaInfo = result.getMetaInfo().toArray(metaInfo);
disposables.add(showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView,
metaInfoSeparator));
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoSeparator, disposables);
handleSearchSuggestion();
@@ -1061,33 +1065,20 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
nextPage = result.getNextPage();
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId),
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
+ "pageIds: " + nextPage.getIds() + ", "
+ "pageCookies: " + nextPage.getCookies(), 0);
+ "pageCookies: " + nextPage.getCookies(),
serviceId));
}
super.handleNextItems(result);
}
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
if (exception instanceof SearchExtractor.NothingFoundException) {
infoListAdapter.clearStreamItemList();
showEmptyState();
} else {
final int errorId = exception instanceof ParsingException
? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), searchString, errorId);
}
return true;
public void handleError() {
super.handleError();
hideSuggestionsPanel();
hideKeyboardSearch();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -1113,9 +1104,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.DELETE_FROM_HISTORY, "none",
"Deleting item failed", R.string.general_error));
throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
disposables.add(onDelete);
}
}

View File

@@ -1,14 +1,12 @@
package org.schabi.newpipe.fragments.list.search;
import android.content.Context;
import android.content.res.TypedArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.AttrRes;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
@@ -117,16 +115,8 @@ public class SuggestionListAdapter
queryView = rootView.findViewById(R.id.suggestion_search);
insertView = rootView.findViewById(R.id.suggestion_insert);
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.ic_history);
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.ic_search);
}
private static int resolveResourceIdFromAttr(final Context context,
@AttrRes final int attr) {
final TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
final int attributeResourceId = a.getResourceId(0, 0);
a.recycle();
return attributeResourceId;
historyResId = R.drawable.ic_history;
searchResId = R.drawable.ic_search;
}
private void updateFrom(final SuggestionItem item) {

View File

@@ -15,38 +15,41 @@ import androidx.preference.PreferenceManager;
import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding;
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.RelatedStreamInfo;
import org.schabi.newpipe.util.RelatedItemInfo;
import java.io.Serializable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo>
public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String INFO_KEY = "related_info_key";
private final CompositeDisposable disposables = new CompositeDisposable();
private RelatedStreamInfo relatedStreamInfo;
private RelatedItemInfo relatedItemInfo;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private RelatedStreamsHeaderBinding headerBinding;
private RelatedItemsHeaderBinding headerBinding;
public static RelatedVideosFragment getInstance(final StreamInfo info) {
final RelatedVideosFragment instance = new RelatedVideosFragment();
public static RelatedItemsFragment getInstance(final StreamInfo info) {
final RelatedItemsFragment instance = new RelatedItemsFragment();
instance.setInitialData(info);
return instance;
}
public RelatedItemsFragment() {
super(UserAction.REQUESTED_STREAM);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -60,7 +63,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_related_streams, container, false);
return inflater.inflate(R.layout.fragment_related_items, container, false);
}
@Override
@@ -77,8 +80,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
@Override
protected ViewBinding getListHeader() {
if (relatedStreamInfo != null && relatedStreamInfo.getRelatedItems() != null) {
headerBinding = RelatedStreamsHeaderBinding
if (relatedItemInfo != null && relatedItemInfo.getRelatedItems() != null) {
headerBinding = RelatedItemsHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
final SharedPreferences pref = PreferenceManager
@@ -104,8 +107,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<RelatedStreamInfo> loadResult(final boolean forceLoad) {
return Single.fromCallable(() -> relatedStreamInfo);
protected Single<RelatedItemInfo> loadResult(final boolean forceLoad) {
return Single.fromCallable(() -> relatedItemInfo);
}
@Override
@@ -117,7 +120,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
}
@Override
public void handleResult(@NonNull final RelatedStreamInfo result) {
public void handleResult(@NonNull final RelatedItemInfo result) {
super.handleResult(result);
if (headerBinding != null) {
@@ -125,43 +128,9 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
}
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
disposables.clear();
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId), url, R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -171,30 +140,29 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
}
private void setInitialData(final StreamInfo info) {
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
if (this.relatedStreamInfo == null) {
this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
if (this.relatedItemInfo == null) {
this.relatedItemInfo = RelatedItemInfo.getInfo(info);
}
}
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, relatedStreamInfo);
outState.putSerializable(INFO_KEY, relatedItemInfo);
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
super.onRestoreInstanceState(savedState);
if (savedState != null) {
final Serializable serializable = savedState.getSerializable(INFO_KEY);
if (serializable instanceof RelatedStreamInfo) {
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
}
final Serializable serializable = savedState.getSerializable(INFO_KEY);
if (serializable instanceof RelatedItemInfo) {
this.relatedItemInfo = (RelatedItemInfo) serializable;
}
}

View File

@@ -1,13 +1,13 @@
package org.schabi.newpipe.info_list;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe.info_list
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.nostra13.universalimageloader.core.ImageLoader
@@ -29,6 +30,17 @@ class StreamSegmentItem(
)
}
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
if (item.channelName == null) {
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.GONE
// When the channel name is displayed there is less space
// and thus the segment title needs to be only one line height.
// But when there is no channel name displayed, the title can be two lines long.
// The default maxLines value is set to 1 to display all elements in the AS preview,
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
} else {
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
}
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }

View File

@@ -14,17 +14,17 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -137,7 +137,10 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
}
if (item.getLikeCount() >= 0) {
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
itemLikesCountView.setText(
Localization.shortCount(
itemBuilder.getContext(),
item.getLikeCount()));
} else {
itemLikesCountView.setText("-");
}
@@ -171,15 +174,15 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
if (TextUtils.isEmpty(item.getUploaderUrl())) {
return;
}
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
try {
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getUploaderUrl(),
item.getUploaderName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
ErrorActivity.reportUiErrorInSnackbar(activity, "Opening channel fragment", e);
}
}

View File

@@ -4,8 +4,6 @@ import android.text.TextUtils;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@@ -14,6 +12,8 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization;
import androidx.preference.PreferenceManager;
import static org.schabi.newpipe.MainActivity.DEBUG;
/*

View File

@@ -66,7 +66,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state2.getProgressTime()));
.toSeconds(state2.getProgressMillis()));
} else {
itemProgressView.setVisibility(View.GONE);
}
@@ -121,10 +121,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime()));
.toSeconds(state.getProgressMillis()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime()));
.toSeconds(state.getProgressMillis()));
ViewUtils.animate(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {

View File

@@ -1,29 +0,0 @@
package org.schabi.newpipe.ktx
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.temporal.ChronoField
import java.util.Calendar
import java.util.Date
import java.util.GregorianCalendar
import java.util.TimeZone
// This method is a modified version of GregorianCalendar.from(ZonedDateTime).
// Math.addExact() and Math.multiplyExact() are desugared even though lint displays a warning.
@SuppressWarnings("NewApi")
fun OffsetDateTime.toCalendar(): Calendar {
val cal = GregorianCalendar(TimeZone.getTimeZone("UTC"))
val offsetDateTimeUTC = withOffsetSameInstant(ZoneOffset.UTC)
cal.gregorianChange = Date(Long.MIN_VALUE)
cal.firstDayOfWeek = Calendar.MONDAY
cal.minimalDaysInFirstWeek = 4
try {
cal.timeInMillis = Math.addExact(
Math.multiplyExact(offsetDateTimeUTC.toEpochSecond(), 1000),
offsetDateTimeUTC[ChronoField.MILLI_OF_SECOND].toLong()
)
} catch (ex: ArithmeticException) {
throw IllegalArgumentException(ex)
}
return cal
}

View File

@@ -2,13 +2,12 @@
package org.schabi.newpipe.ktx
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.util.Log
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.core.animation.addListener
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.schabi.newpipe.MainActivity
@@ -34,14 +33,6 @@ fun TextView.animateTextColor(duration: Long, @ColorInt colorStart: Int, @ColorI
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
viewPropertyAnimator.duration = duration
viewPropertyAnimator.addUpdateListener { setTextColor(it.animatedValue as Int) }
viewPropertyAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
setTextColor(colorEnd)
}
override fun onAnimationCancel(animation: Animator) {
setTextColor(colorEnd)
}
})
viewPropertyAnimator.addListener(onCancel = { setTextColor(colorEnd) }, onEnd = { setTextColor(colorEnd) })
viewPropertyAnimator.start()
}

View File

@@ -58,10 +58,8 @@ tailrec fun Throwable?.hasCause(checkSubtypes: Boolean, vararg causesToCheck: Cl
if (causeClass.isAssignableFrom(this.javaClass)) {
return true
}
} else {
if (causeClass == this.javaClass) {
return true
}
} else if (causeClass == this.javaClass) {
return true
}
}

View File

@@ -11,6 +11,7 @@ import android.util.Log
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import androidx.core.animation.addListener
import androidx.core.view.ViewCompat
import androidx.core.view.isGone
import androidx.core.view.isInvisible
@@ -106,15 +107,10 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
viewPropertyAnimator.addUpdateListener { animation: ValueAnimator ->
backgroundTintListCompat = ColorStateList(empty, intArrayOf(animation.animatedValue as Int))
}
viewPropertyAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd))
}
override fun onAnimationCancel(animation: Animator) {
onAnimationEnd(animation)
}
})
viewPropertyAnimator.addListener(
onCancel = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) },
onEnd = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) }
)
viewPropertyAnimator.start()
}
@@ -134,17 +130,16 @@ fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
layoutParams.height = value.toInt()
requestLayout()
}
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
animator.addListener(
onCancel = {
layoutParams.height = targetHeight
requestLayout()
},
onEnd = {
layoutParams.height = targetHeight
requestLayout()
}
override fun onAnimationCancel(animation: Animator) {
layoutParams.height = targetHeight
requestLayout()
}
})
)
animator.start()
return animator
}
@@ -319,6 +314,16 @@ fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0)
.start()
}
/**
* Instead of hiding normally using [animate], which would make
* the recycler view unable to capture touches after being hidden, this just animates the alpha
* value setting it to `0.0` after `200` milliseconds.
*/
fun View.animateHideRecyclerViewAllowingScrolling() {
// not hiding normally because the view needs to still capture touches and allow scroll
animate().alpha(0.0f).setDuration(200).start()
}
enum class AnimationType {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
}

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.local;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
@@ -9,6 +8,7 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
@@ -24,6 +24,8 @@ import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.ListViewContract;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
/**
* This fragment is design to be used with persistent data such as
@@ -75,7 +77,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
super.onResume();
if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = isGridLayout();
final boolean useGrid = shouldUseGridLayout(requireContext());
itemsList.setLayoutManager(
useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setUseGridVariant(useGrid);
@@ -119,7 +121,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
itemListAdapter = new LocalItemListAdapter(activity);
final boolean useGrid = isGridLayout();
final boolean useGrid = shouldUseGridLayout(requireContext());
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
@@ -144,7 +146,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
@@ -184,7 +187,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
public void showLoading() {
super.showLoading();
if (itemsList != null) {
animate(itemsList, false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList);
}
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
@@ -202,19 +205,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
}
}
@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
if (itemsList != null) {
animate(itemsList, false, 200);
}
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
}
@Override
public void showEmptyState() {
super.showEmptyState();
@@ -249,9 +239,18 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
}
@Override
protected boolean onError(final Throwable exception) {
public void handleError() {
super.handleError();
resetFragment();
return super.onError(exception);
showListFooter(false);
if (itemsList != null) {
animateHideRecyclerViewAllowingScrolling(itemsList);
}
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
}
@Override
@@ -261,17 +260,4 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
updateFlags |= LIST_MODE_UPDATE_FLAG;
}
}
protected boolean isGridLayout() {
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity)
.getString(getString(R.string.list_view_mode_key),
getString(R.string.list_view_mode_value));
if ("auto".equals(listMode)) {
final Configuration configuration = getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
} else {
return "grid".equals(listMode);
}
}
}

View File

@@ -126,8 +126,19 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
public void removeItem(final LocalItem data) {
final int index = localItems.indexOf(data);
localItems.remove(index);
notifyItemRemoved(index + (header != null ? 1 : 0));
if (index != -1) {
localItems.remove(index);
notifyItemRemoved(index + (header != null ? 1 : 0));
} else {
// this happens when
// 1) removeItem is called on infoItemDuplicate as in showStreamItemDialog of
// LocalPlaylistFragment in this case need to implement delete object by it's duplicate
// OR
// 2)data not in itemList and UI is still not updated so notifyDataSetChanged()
notifyDataSetChanged();
}
}
public boolean swapItems(final int fromAdapterPosition, final int toAdapterPosition) {

View File

@@ -1,7 +1,5 @@
package org.schabi.newpipe.local.bookmark;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
@@ -12,6 +10,7 @@ import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentManager;
import org.reactivestreams.Subscriber;
@@ -23,10 +22,11 @@ import org.schabi.newpipe.database.LocalItem;
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.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@@ -206,7 +206,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override
public void onError(final Throwable exception) {
BookmarkFragment.this.onError(exception);
showError(new ErrorInfo(exception,
UserAction.REQUESTED_BOOKMARK, "Loading playlists"));
}
@Override
@@ -237,17 +238,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Bookmark", R.string.general_error);
return true;
}
@Override
protected void resetFragment() {
super.resetFragment();
@@ -269,7 +259,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
final EditText editText = dialogView.findViewById(R.id.playlist_name_edit_text);
editText.setText(selectedItem.name);
final Builder builder = new AlertDialog.Builder(activity);
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setView(dialogView)
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
changeLocalPlaylistName(selectedItem.uid, editText.getText().toString()))
@@ -295,8 +285,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
.setPositiveButton(R.string.delete, (dialog, i) ->
disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { /*Do nothing on success*/ }, this::onError))
)
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Deleting playlist")))))
.setNegativeButton(R.string.cancel, null)
.show();
}
@@ -314,7 +306,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
localPlaylistManager.renamePlaylist(id, name);
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, this::onError);
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Changing playlist name")));
disposables.add(disposable);
}
}

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe.local.dialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.View;
@@ -9,6 +8,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
@@ -46,7 +46,7 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
final EditText nameInput = dialogView.findViewById(R.id.playlist_name);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext())
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext())
.setTitle(R.string.create_playlist)
.setView(dialogView)
.setCancelable(true)
@@ -54,7 +54,7 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
.setPositiveButton(R.string.create, (dialogInterface, i) -> {
final String name = nameInput.getText().toString();
final LocalPlaylistManager playlistManager =
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
final Toast successToast = Toast.makeText(getActivity(),
R.string.playlist_creation_success,
Toast.LENGTH_SHORT);

View File

@@ -12,6 +12,7 @@ import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
@@ -38,16 +39,19 @@ class FeedDatabaseManager(context: Context) {
fun database() = database
fun asStreamItems(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable<List<StreamInfoItem>> {
val streams = when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.getAllStreams()
else -> feedTable.getAllStreamsFromGroup(groupId)
}
return streams.map {
val items = ArrayList<StreamInfoItem>(it.size)
it.mapTo(items) { stream -> stream.toStreamInfoItem() }
return@map items
fun getStreams(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
getPlayedStreams: Boolean = true
): Flowable<List<StreamWithState>> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> {
if (getPlayedStreams) feedTable.getAllStreams()
else feedTable.getLiveOrNotPlayedStreams()
}
else -> {
if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId)
else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId)
}
}
}
@@ -60,8 +64,10 @@ class FeedDatabaseManager(context: Context) {
}
}
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) =
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun outdatedSubscriptionsForGroup(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
outdatedThreshold: OffsetDateTime
) = feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun markAsOutdated(subscriptionId: Long) = feedTable
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
@@ -93,10 +99,7 @@ class FeedDatabaseManager(context: Context) {
}
feedTable.setLastUpdatedForSubscription(
FeedLastUpdatedEntity(
subscriptionId,
OffsetDateTime.now(ZoneOffset.UTC)
)
FeedLastUpdatedEntity(subscriptionId, OffsetDateTime.now(ZoneOffset.UTC))
)
}
@@ -108,7 +111,12 @@ class FeedDatabaseManager(context: Context) {
fun clear() {
feedTable.deleteAll()
val deletedOrphans = streamTable.deleteOrphans()
if (DEBUG) Log.d(this::class.java.simpleName, "clear() → streamTable.deleteOrphans() → $deletedOrphans")
if (DEBUG) {
Log.d(
this::class.java.simpleName,
"clear() → streamTable.deleteOrphans() → $deletedOrphans"
)
}
}
// /////////////////////////////////////////////////////////////////////////
@@ -122,7 +130,8 @@ class FeedDatabaseManager(context: Context) {
}
fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>): Completable {
return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
return Completable
.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}

View File

@@ -19,7 +19,10 @@
package org.schabi.newpipe.local.feed
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@@ -28,40 +31,74 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.FragmentFeedBinding
import org.schabi.newpipe.fragments.list.BaseListFragment
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.info_list.InfoItemDialog
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.player.helper.PlayerHolder
import org.schabi.newpipe.util.Localization
import java.util.Calendar
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.StreamDialogEntry
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCount
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.util.ArrayList
class FeedFragment : BaseListFragment<FeedState, Unit>() {
class FeedFragment : BaseStateFragment<FeedState>() {
private var _feedBinding: FragmentFeedBinding? = null
private val feedBinding get() = _feedBinding!!
private val errorBinding get() = _feedBinding!!.errorPanel
private val disposables = CompositeDisposable()
private lateinit var viewModel: FeedViewModel
@State
@JvmField
var listState: Parcelable? = null
@State @JvmField var listState: Parcelable? = null
private var groupId = FeedGroupEntity.GROUP_ALL_ID
private var groupName = ""
private var oldestSubscriptionUpdate: Calendar? = null
private var oldestSubscriptionUpdate: OffsetDateTime? = null
private lateinit var groupAdapter: GroupAdapter<GroupieViewHolder>
@State @JvmField var showPlayedItems: Boolean = true
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
private var updateListViewModeOnResume = false
init {
setHasOptionsMenu(true)
setUseDefaultStateSaving(false)
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -70,6 +107,14 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
?: FeedGroupEntity.GROUP_ALL_ID
groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
onSettingsChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key.equals(getString(R.string.list_view_mode_key))) {
updateListViewModeOnResume = true
}
}
PreferenceManager.getDefaultSharedPreferences(activity)
.registerOnSharedPreferenceChangeListener(onSettingsChangeListener)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -81,8 +126,17 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
_feedBinding = FragmentFeedBinding.bind(rootView)
super.onViewCreated(rootView, savedInstanceState)
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems)
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
groupAdapter = GroupAdapter<GroupieViewHolder>().apply {
setOnItemClickListener(listenerStreamItem)
setOnItemLongClickListener(listenerStreamItem)
}
feedBinding.itemsList.adapter = groupAdapter
setupListViewMode()
}
override fun onPause() {
@@ -93,6 +147,23 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun onResume() {
super.onResume()
updateRelativeTimeViews()
if (updateListViewModeOnResume) {
updateListViewModeOnResume = false
setupListViewMode()
if (viewModel.stateLiveData.value != null) {
handleResult(viewModel.stateLiveData.value!!)
}
}
}
fun setupListViewMode() {
// does everything needed to setup the layouts for grid or list modes
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCount(context) else 1
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
@@ -106,7 +177,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun initListeners() {
super.initListeners()
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
feedBinding.swiperefresh.setOnRefreshListener { reloadContent() }
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
}
// /////////////////////////////////////////////////////////////////////////
@@ -115,21 +186,21 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
activity.supportActionBar?.setTitle(R.string.fragment_feed_title)
activity.supportActionBar?.subtitle = groupName
inflater.inflate(R.menu.menu_feed_fragment, menu)
if (useAsFrontPage) {
menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menu_item_feed_help) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val usingDedicatedMethod = sharedPreferences.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
val usingDedicatedMethod = sharedPreferences
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
val enableDisableButtonText = when {
usingDedicatedMethod -> R.string.feed_use_dedicated_fetch_method_disable_button
else -> R.string.feed_use_dedicated_fetch_method_enable_button
@@ -146,6 +217,10 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
.create()
.show()
return true
} else if (item.itemId == R.id.menu_item_feed_toggle_played_items) {
showPlayedItems = !item.isChecked
updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(showPlayedItems)
}
return super.onOptionsItemSelected(item)
@@ -157,64 +232,57 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
}
override fun onDestroy() {
disposables.dispose()
if (onSettingsChangeListener != null) {
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(onSettingsChangeListener)
onSettingsChangeListener = null
}
super.onDestroy()
activity?.supportActionBar?.subtitle = null
}
override fun onDestroyView() {
feedBinding.itemsList.adapter = null
_feedBinding = null
super.onDestroyView()
}
// /////////////////////////////////////////////////////////////////////////
private fun updateTogglePlayedItemsButton(menuItem: MenuItem) {
menuItem.isChecked = showPlayedItems
menuItem.icon = AppCompatResources.getDrawable(
requireContext(),
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
)
}
// //////////////////////////////////////////////////////////////////////////
// Handling
// /////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////
override fun showLoading() {
super.showLoading()
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
feedBinding.refreshRootView.animate(false, 0)
feedBinding.itemsList.animate(false, 0)
feedBinding.loadingProgressBar.animate(true, 200)
feedBinding.loadingProgressText.animate(true, 200)
feedBinding.emptyStateView.root.animate(false, 0)
errorBinding.root.animate(false, 0)
feedBinding.swipeRefreshLayout.isRefreshing = true
}
override fun hideLoading() {
super.hideLoading()
feedBinding.itemsList.animate(true, 0)
feedBinding.refreshRootView.animate(true, 200)
feedBinding.itemsList.animate(true, 300)
feedBinding.loadingProgressBar.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0)
feedBinding.emptyStateView.root.animate(false, 0)
errorBinding.root.animate(false, 0)
feedBinding.swiperefresh.isRefreshing = false
feedBinding.swipeRefreshLayout.isRefreshing = false
}
override fun showEmptyState() {
super.showEmptyState()
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
feedBinding.refreshRootView.animate(true, 200)
feedBinding.itemsList.animate(false, 0)
feedBinding.loadingProgressBar.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0)
feedBinding.emptyStateView.root.animate(true, 800)
errorBinding.root.animate(false, 0)
}
override fun showError(message: String, showRetryButton: Boolean) {
infoListAdapter.clearStreamItemList()
feedBinding.refreshRootView.animate(false, 120)
feedBinding.itemsList.animate(false, 120)
feedBinding.loadingProgressBar.animate(false, 120)
feedBinding.loadingProgressText.animate(false, 120)
errorBinding.errorMessageView.text = message
errorBinding.errorButtonRetry.animate(showRetryButton, if (showRetryButton) 600 else 0)
errorBinding.root.animate(true, 300)
feedBinding.swipeRefreshLayout.isRefreshing = false
}
override fun handleResult(result: FeedState) {
@@ -227,6 +295,14 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
updateRefreshViewState()
}
override fun handleError() {
super.handleError()
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
feedBinding.refreshRootView.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0)
feedBinding.swipeRefreshLayout.isRefreshing = false
}
private fun handleProgressState(progressState: FeedState.ProgressState) {
showLoading()
@@ -248,30 +324,95 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
feedBinding.loadingProgressBar.max = progressState.maxProgress
}
private fun showStreamDialog(item: StreamInfoItem) {
val context = context
val activity: Activity? = getActivity()
if (context == null || context.resources == null || activity == null) return
val entries = ArrayList<StreamDialogEntry>()
if (PlayerHolder.getType() != null) {
entries.add(StreamDialogEntry.enqueue)
}
if (item.streamType == StreamType.AUDIO_STREAM) {
entries.addAll(
listOf(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share,
StreamDialogEntry.open_in_browser
)
)
} else {
entries.addAll(
listOf(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share,
StreamDialogEntry.open_in_browser
)
)
}
StreamDialogEntry.setEnabledEntries(entries)
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
StreamDialogEntry.clickOn(which, this, item)
}.show()
}
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
override fun onItemClick(item: Item<*>, view: View) {
if (item is StreamItem) {
val stream = item.streamWithState.stream
NavigationHelper.openVideoDetailFragment(
requireContext(), fm,
stream.serviceId, stream.url, stream.title, null, false
)
}
}
override fun onItemLongClick(item: Item<*>, view: View): Boolean {
if (item is StreamItem) {
showStreamDialog(item.streamWithState.stream.toStreamInfoItem())
return true
}
return false
}
}
@SuppressLint("StringFormatMatches")
private fun handleLoadedState(loadedState: FeedState.LoadedState) {
infoListAdapter.setInfoItemList(loadedState.items)
val itemVersion = if (shouldUseGridLayout(context)) {
StreamItem.ItemVersion.GRID
} else {
StreamItem.ItemVersion.NORMAL
}
loadedState.items.forEach { it.itemVersion = itemVersion }
groupAdapter.updateAsync(loadedState.items, false, null)
listState?.run {
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
listState = null
}
oldestSubscriptionUpdate = loadedState.oldestUpdate
val loadedCount = loadedState.notLoadedCount > 0
feedBinding.refreshSubtitleText.isVisible = loadedCount
if (loadedCount) {
val feedsNotLoaded = loadedState.notLoadedCount > 0
feedBinding.refreshSubtitleText.isVisible = feedsNotLoaded
if (feedsNotLoaded) {
feedBinding.refreshSubtitleText.text = getString(
R.string.feed_subscription_not_loaded_count,
loadedState.notLoadedCount
)
}
if (loadedState.itemsErrors.isNotEmpty()) {
showSnackBarError(
loadedState.itemsErrors, UserAction.REQUESTED_FEED,
"none", "Loading feed", R.string.general_error
)
if (oldestSubscriptionUpdate != loadedState.oldestUpdate ||
(oldestSubscriptionUpdate == null && loadedState.oldestUpdate == null)
) {
// ignore errors if they have already been handled for the current update
handleItemsErrors(loadedState.itemsErrors)
}
oldestSubscriptionUpdate = loadedState.oldestUpdate
if (loadedState.items.isEmpty()) {
showEmptyState()
@@ -281,26 +422,94 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
}
private fun handleErrorState(errorState: FeedState.ErrorState): Boolean {
hideLoading()
errorState.error?.let {
onError(errorState.error)
return true
return if (errorState.error == null) {
hideLoading()
false
} else {
showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed"))
true
}
return false
}
private fun handleItemsErrors(errors: List<Throwable>) {
errors.forEachIndexed { i, t ->
if (t is FeedLoadService.RequestException &&
t.cause is ContentNotAvailableException
) {
Single.fromCallable {
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
.getSubscription(t.subscriptionId)
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
subscriptionEntity ->
handleFeedNotAvailable(
subscriptionEntity,
t.cause,
errors.subList(i + 1, errors.size)
)
},
{ throwable -> throwable.printStackTrace() }
)
return // this will be called on the remaining errors by handleFeedNotAvailable()
}
}
}
private fun handleFeedNotAvailable(
subscriptionEntity: SubscriptionEntity,
@Nullable cause: Throwable?,
nextItemsErrors: List<Throwable>
) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val isFastFeedModeEnabled = sharedPreferences.getBoolean(
getString(R.string.feed_use_dedicated_fetch_method_key), false
)
val builder = AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_load_error)
.setPositiveButton(
R.string.unsubscribe
) { _, _ ->
SubscriptionManager(requireContext()).deleteSubscription(
subscriptionEntity.serviceId, subscriptionEntity.url
).subscribe()
handleItemsErrors(nextItemsErrors)
}
.setNegativeButton(R.string.cancel) { _, _ -> }
var message = getString(R.string.feed_load_error_account_info, subscriptionEntity.name)
if (cause is AccountTerminatedException) {
message += "\n" + getString(R.string.feed_load_error_terminated)
} else if (cause is ContentNotAvailableException) {
if (isFastFeedModeEnabled) {
message += "\n" + getString(R.string.feed_load_error_fast_unknown)
builder.setNeutralButton(R.string.feed_use_dedicated_fetch_method_disable_button) { _, _ ->
sharedPreferences.edit {
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
}
}
} else if (!isNullOrEmpty(cause.message)) {
message += "\n" + cause.message
}
}
builder.setMessage(message).create().show()
}
private fun updateRelativeTimeViews() {
updateRefreshViewState()
infoListAdapter.notifyDataSetChanged()
groupAdapter.notifyItemRangeChanged(
0, groupAdapter.itemCount,
StreamItem.UPDATE_RELATIVE_TIME
)
}
private fun updateRefreshViewState() {
val oldestSubscriptionUpdateText = when {
oldestSubscriptionUpdate != null -> Localization.relativeTime(oldestSubscriptionUpdate!!)
else -> ""
}
feedBinding.refreshText.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText)
feedBinding.refreshText.text = getString(
R.string.feed_oldest_subscription_update,
oldestSubscriptionUpdate?.let { Localization.relativeTime(it) } ?: ""
)
}
// /////////////////////////////////////////////////////////////////////////
@@ -308,8 +517,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
// /////////////////////////////////////////////////////////////////////////
override fun doInitialLoadLogic() {}
override fun loadMoreItems() {}
override fun hasMoreItems() = false
override fun reloadContent() {
getActivity()?.startService(
@@ -320,18 +527,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
listState = null
}
override fun onError(exception: Throwable): Boolean {
if (super.onError(exception)) return true
if (useAsFrontPage) {
showSnackBarError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
return true
}
onUnrecoverableError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
return true
}
companion object {
const val KEY_GROUP_ID = "ARG_GROUP_ID"
const val KEY_GROUP_NAME = "ARG_GROUP_NAME"

View File

@@ -1,8 +1,8 @@
package org.schabi.newpipe.local.feed
import androidx.annotation.StringRes
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import java.util.Calendar
import org.schabi.newpipe.local.feed.item.StreamItem
import java.time.OffsetDateTime
sealed class FeedState {
data class ProgressState(
@@ -12,8 +12,8 @@ sealed class FeedState {
) : FeedState()
data class LoadedState(
val items: List<StreamInfoItem>,
val oldestUpdate: Calendar? = null,
val items: List<StreamItem>,
val oldestUpdate: OffsetDateTime? = null,
val notLoadedCount: Long,
val itemsErrors: List<Throwable> = emptyList()
) : FeedState()

View File

@@ -8,10 +8,11 @@ import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.Function4
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.toCalendar
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedEventManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
@@ -21,26 +22,33 @@ import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() {
class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedViewModel(context.applicationContext, groupId) as T
}
}
class FeedViewModel(
applicationContext: Context,
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
initialShowPlayedItems: Boolean = true
) : ViewModel() {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
private val streamItems = toggleShowPlayedItems
.startWithItem(initialShowPlayedItems)
.distinctUntilChanged()
.switchMap { showPlayedItems ->
feedDatabaseManager.getStreams(groupId, showPlayedItems)
}
private val mutableStateLiveData = MutableLiveData<FeedState>()
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
private var combineDisposable = Flowable
.combineLatest(
FeedEventManager.events(),
feedDatabaseManager.asStreamItems(groupId),
streamItems,
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> ->
Function4 { t1: FeedEventManager.Event, t2: List<StreamWithState>,
t3: Long, t4: List<OffsetDateTime> ->
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
}
)
@@ -48,13 +56,11 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
val oldestUpdateCalendar = oldestUpdate?.toCalendar()
mutableStateLiveData.postValue(
when (event) {
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount)
is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount)
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors)
is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
is ErrorResultEvent -> FeedState.ErrorState(event.error)
}
)
@@ -69,5 +75,20 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
combineDisposable.dispose()
}
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamInfoItem>, val t3: Long, val t4: OffsetDateTime?)
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamWithState>, val t3: Long, val t4: OffsetDateTime?)
fun togglePlayedItems(showPlayedItems: Boolean) {
toggleShowPlayedItems.onNext(showPlayedItems)
}
class Factory(
private val context: Context,
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
private val showPlayedItems: Boolean
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T
}
}
}

View File

@@ -0,0 +1,153 @@
package org.schabi.newpipe.local.feed.item
import android.content.Context
import android.text.TextUtils
import android.view.View
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.databinding.ListStreamItemBinding
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.ImageDisplayConstants
import org.schabi.newpipe.util.Localization
import java.util.concurrent.TimeUnit
data class StreamItem(
val streamWithState: StreamWithState,
var itemVersion: ItemVersion = ItemVersion.NORMAL
) : BindableItem<ListStreamItemBinding>() {
companion object {
const val UPDATE_RELATIVE_TIME = 1
}
private val stream: StreamEntity = streamWithState.stream
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
override fun getId(): Long = stream.uid
enum class ItemVersion { NORMAL, MINI, GRID }
override fun getLayout(): Int = when (itemVersion) {
ItemVersion.NORMAL -> R.layout.list_stream_item
ItemVersion.MINI -> R.layout.list_stream_mini_item
ItemVersion.GRID -> R.layout.list_stream_grid_item
}
override fun initializeViewBinding(view: View) = ListStreamItemBinding.bind(view)
override fun bind(viewBinding: ListStreamItemBinding, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(UPDATE_RELATIVE_TIME)) {
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
}
return
}
super.bind(viewBinding, position, payloads)
}
override fun bind(viewBinding: ListStreamItemBinding, position: Int) {
viewBinding.itemVideoTitleView.text = stream.title
viewBinding.itemUploaderView.text = stream.uploader
val isLiveStream = stream.streamType == LIVE_STREAM || stream.streamType == AUDIO_LIVE_STREAM
if (stream.duration > 0) {
viewBinding.itemDurationView.text = Localization.getDurationString(stream.duration)
viewBinding.itemDurationView.setBackgroundColor(
ContextCompat.getColor(
viewBinding.itemDurationView.context,
R.color.duration_background_color
)
)
viewBinding.itemDurationView.visibility = View.VISIBLE
if (stateProgressTime != null) {
viewBinding.itemProgressView.visibility = View.VISIBLE
viewBinding.itemProgressView.max = stream.duration.toInt()
viewBinding.itemProgressView.progress = TimeUnit.MILLISECONDS.toSeconds(stateProgressTime).toInt()
} else {
viewBinding.itemProgressView.visibility = View.GONE
}
} else if (isLiveStream) {
viewBinding.itemDurationView.setText(R.string.duration_live)
viewBinding.itemDurationView.setBackgroundColor(
ContextCompat.getColor(
viewBinding.itemDurationView.context,
R.color.live_duration_background_color
)
)
viewBinding.itemDurationView.visibility = View.VISIBLE
viewBinding.itemProgressView.visibility = View.GONE
} else {
viewBinding.itemDurationView.visibility = View.GONE
viewBinding.itemProgressView.visibility = View.GONE
}
ImageLoader.getInstance().displayImage(
stream.thumbnailUrl, viewBinding.itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
}
}
override fun isLongClickable() = when (stream.streamType) {
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true
else -> false
}
private fun getStreamInfoDetailLine(context: Context): String {
var viewsAndDate = ""
val viewCount = stream.viewCount
if (viewCount != null && viewCount >= 0) {
viewsAndDate = when (stream.streamType) {
AUDIO_LIVE_STREAM -> Localization.listeningCount(context, viewCount)
LIVE_STREAM -> Localization.shortWatchingCount(context, viewCount)
else -> Localization.shortViewCount(context, viewCount)
}
}
val uploadDate = getFormattedRelativeUploadDate(context)
return when {
!TextUtils.isEmpty(uploadDate) -> when {
viewsAndDate.isEmpty() -> uploadDate!!
else -> Localization.concatenateStrings(viewsAndDate, uploadDate)
}
else -> viewsAndDate
}
}
private fun getFormattedRelativeUploadDate(context: Context): String? {
val uploadDate = stream.uploadDate
return if (uploadDate != null) {
var formattedRelativeTime = Localization.relativeTime(uploadDate)
if (MainActivity.DEBUG) {
val key = context.getString(R.string.show_original_time_ago_key)
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, false)) {
formattedRelativeTime += " (" + stream.textualUploadDate + ")"
}
}
formattedRelativeTime
} else {
stream.textualUploadDate
}
}
override fun getSpanSize(spanCount: Int, position: Int): Int {
return if (itemVersion == ItemVersion.GRID) 1 else spanCount
}
}

View File

@@ -48,9 +48,7 @@ import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
@@ -58,7 +56,6 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResul
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExtractorHelper
import java.io.IOException
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.TimeUnit
@@ -162,7 +159,7 @@ class FeedLoadService : Service() {
// Loading & Handling
// /////////////////////////////////////////////////////////////////////////
private class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
companion object {
fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> {
val toReturn = ArrayList<Throwable>(info.errors.size)
@@ -209,29 +206,40 @@ class FeedLoadService : Service() {
.filter { !cancelSignal.get() }
.map { subscriptionEntity ->
var error: Throwable? = null
try {
val listInfo = if (useFeedExtractor) {
ExtractorHelper
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
}
.blockingGet()
} else {
ExtractorHelper
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
}
.blockingGet()
} as ListInfo<StreamInfoItem>
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
} catch (e: Throwable) {
if (error == null) {
// do this to prevent blockingGet() from wrapping into RuntimeException
error = e
}
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
val wrapper = RequestException(subscriptionEntity.uid, request, e)
val wrapper = RequestException(subscriptionEntity.uid, request, error!!)
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
}
}
.sequential()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(errorHandlingConsumer)
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(notificationsConsumer)
@@ -331,24 +339,6 @@ class FeedLoadService : Service() {
}
}
private val errorHandlingConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer {
if (it.isOnError) {
var error = it.error!!
if (error is RequestException) error = error.cause!!
val cause = error.cause
when {
error is ReCaptchaException -> throw error
cause is ReCaptchaException -> throw cause
error is IOException -> throw error
cause is IOException -> throw cause
error.isNetworkRelated -> throw IOException(error)
}
}
}
private val notificationsConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer { onItemCompleted(it.value?.second?.name) }

View File

@@ -211,11 +211,11 @@ public class HistoryRecordManager {
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
return queueItem.getStream()
.map((info) -> streamTable.upsert(new StreamEntity(info)))
.map(info -> streamTable.upsert(new StreamEntity(info)))
.flatMapPublisher(streamStateTable::getState)
.firstElement()
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
.filter(state -> state.isValid((int) queueItem.getDuration()))
.filter(state -> state.isValid(queueItem.getDuration()))
.subscribeOn(Schedulers.io());
}
@@ -224,18 +224,16 @@ public class HistoryRecordManager {
.flatMapPublisher(streamStateTable::getState)
.firstElement()
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
.filter(state -> state.isValid((int) info.getDuration()))
.filter(state -> state.isValid(info.getDuration()))
.subscribeOn(Schedulers.io());
}
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
return Completable.fromAction(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
final StreamStateEntity state = new StreamStateEntity(streamId, progressTime);
if (state.isValid((int) info.getDuration())) {
final StreamStateEntity state = new StreamStateEntity(streamId, progressMillis);
if (state.isValid(info.getDuration())) {
streamStateTable.upsert(state);
} else {
streamStateTable.deleteState(streamId);
}
})).subscribeOn(Schedulers.io());
}

View File

@@ -14,7 +14,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.viewbinding.ViewBinding;
import com.google.android.material.snackbar.Snackbar;
@@ -27,6 +26,8 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog;
@@ -34,27 +35,26 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.settings.HistorySettingsFragment;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public class StatisticsPlaylistFragment
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
private final CompositeDisposable disposables = new CompositeDisposable();
@@ -111,7 +111,8 @@ public class StatisticsPlaylistFragment
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_history, menu);
}
@@ -163,48 +164,11 @@ public class StatisticsPlaylistFragment
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_history_clear:
new AlertDialog.Builder(activity)
.setTitle(R.string.delete_view_history_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
final Disposable onDelete = recordManager.deleteWholeStreamHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getContext(),
R.string.watch_history_deleted,
Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete view history",
R.string.general_error)));
final Disposable onClearOrphans = recordManager.removeOrphanedRecords()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> {
},
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete search history",
R.string.general_error)));
disposables.add(onClearOrphans);
disposables.add(onDelete);
}))
.create()
.show();
break;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == R.id.action_history_clear) {
HistorySettingsFragment
.openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables);
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@@ -228,7 +192,7 @@ public class StatisticsPlaylistFragment
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
itemsListState = Objects.requireNonNull(itemsList.getLayoutManager()).onSaveInstanceState();
}
@Override
@@ -287,7 +251,8 @@ public class StatisticsPlaylistFragment
@Override
public void onError(final Throwable exception) {
StatisticsPlaylistFragment.this.onError(exception);
showError(
new ErrorInfo(exception, UserAction.SOMETHING_ELSE, "History Statistics"));
}
@Override
@@ -313,7 +278,7 @@ public class StatisticsPlaylistFragment
}
itemListAdapter.addItems(processResult(result));
if (itemsListState != null) {
if (itemsListState != null && itemsList.getLayoutManager() != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
@@ -341,17 +306,6 @@ public class StatisticsPlaylistFragment
}
}
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "History Statistics", R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -360,14 +314,13 @@ public class StatisticsPlaylistFragment
if (sortMode == StatisticSortMode.LAST_PLAYED) {
sortMode = StatisticSortMode.MOST_PLAYED;
setTitle(getString(R.string.title_most_played));
headerBinding.sortButtonIcon.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_history));
headerBinding.sortButtonIcon.setImageResource(R.drawable.ic_history);
headerBinding.sortButtonText.setText(R.string.title_last_played);
} else {
sortMode = StatisticSortMode.LAST_PLAYED;
setTitle(getString(R.string.title_last_played));
headerBinding.sortButtonIcon.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_filter_list));
R.drawable.ic_filter_list);
headerBinding.sortButtonText.setText(R.string.title_most_played);
}
startLoading(true);
@@ -406,9 +359,15 @@ public class StatisticsPlaylistFragment
StreamDialogEntry.share
));
}
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
@@ -439,9 +398,8 @@ public class StatisticsPlaylistFragment
Toast.LENGTH_SHORT).show();
}
},
throwable -> showSnackBarError(throwable,
UserAction.DELETE_FROM_HISTORY, "none",
"Deleting item failed", R.string.general_error));
throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY, "Deleting item")));
disposables.add(onDelete);
}

View File

@@ -68,11 +68,11 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
if (item.getProgressTime() > 0) {
if (item.getProgressMillis() > 0) {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
} else {
itemProgressView.setVisibility(View.GONE);
}
@@ -109,14 +109,14 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) {
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
ViewUtils.animate(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {

View File

@@ -96,11 +96,11 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
if (item.getProgressTime() > 0) {
if (item.getProgressMillis() > 0) {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
} else {
itemProgressView.setVisibility(View.GONE);
}
@@ -140,14 +140,14 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) {
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
ViewUtils.animate(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {

View File

@@ -34,6 +34,8 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog;
@@ -42,8 +44,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@@ -65,13 +66,14 @@ import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
// 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;
@State
protected Long playlistId;
@State
@@ -110,7 +112,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
debouncedSaveSignal = PublishSubject.create();
disposables = new CompositeDisposable();
@@ -247,7 +249,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
@@ -334,35 +337,37 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onError(final Throwable exception) {
LocalPlaylistFragment.this.onError(exception);
showError(new ErrorInfo(exception, UserAction.REQUESTED_BOOKMARK,
"Loading local playlist"));
}
@Override
public void onComplete() { }
public void onComplete() {
}
};
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_remove_watched:
if (!isRemovingWatched) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning)
.setTitle(R.string.remove_watched_popup_title)
.setPositiveButton(R.string.yes,
(DialogInterface d, int id) -> removeWatchedStreams(false))
.setNeutralButton(
R.string.remove_watched_popup_yes_and_partially_watched_videos,
(DialogInterface d, int id) -> removeWatchedStreams(true))
.setNegativeButton(R.string.cancel,
(DialogInterface d, int id) -> d.cancel())
.create()
.show();
}
break;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == R.id.menu_item_remove_watched) {
if (!isRemovingWatched) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning)
.setTitle(R.string.remove_watched_popup_title)
.setPositiveButton(R.string.yes,
(DialogInterface d, int id) -> removeWatchedStreams(false))
.setNeutralButton(
R.string.remove_watched_popup_yes_and_partially_watched_videos,
(DialogInterface d, int id) -> removeWatchedStreams(true))
.setNegativeButton(R.string.cancel,
(DialogInterface d, int id) -> d.cancel())
.create()
.show();
}
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
createRenameDialog();
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@@ -420,7 +425,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
playlistItem.getStreamId());
final boolean hasState = streamStatesIter.next() != null;
if (indexInHistory < 0 || hasState) {
if (indexInHistory < 0 || hasState) {
notWatchedItems.add(playlistItem);
} else if (!thumbnailVideoRemoved
&& playlistManager.getPlaylistThumbnail(playlistId)
@@ -455,7 +460,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
hideLoading();
isRemovingWatched = false;
}, this::onError));
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Removing watched videos, partially watched=" + removePartiallyWatched))));
}
@Override
@@ -511,17 +517,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
}
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Local Playlist", R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist Metadata/Streams Manipulation
//////////////////////////////////////////////////////////////////////////*/
@@ -562,7 +557,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final Disposable disposable = playlistManager.renamePlaylist(playlistId, title)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, this::onError);
.subscribe(longs -> { /*Do nothing on success*/ }, throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Renaming playlist")));
disposables.add(disposable);
}
@@ -583,7 +580,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final Disposable disposable = playlistManager
.changePlaylistThumbnail(playlistId, thumbnailUrl)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> successToast.show(), this::onError);
.subscribe(ignore -> successToast.show(), throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Changing playlist thumbnail")));
disposables.add(disposable);
}
@@ -632,7 +631,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> saveImmediate(), this::onError);
.subscribe(ignored -> saveImmediate(), throwable ->
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
"Debounced saver")));
}
private void saveImmediate() {
@@ -669,7 +670,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
isModified.set(false);
}
},
this::onError
throwable -> showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
);
disposables.add(disposable);
}
@@ -677,13 +679,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
int directions = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
if (isGridLayout()) {
if (shouldUseGridLayout(requireContext())) {
directions |= ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
}
return new ItemTouchHelper.SimpleCallback(directions,
ItemTouchHelper.ACTION_STATE_IDLE) {
@Override
public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView,
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
final int viewSize,
final int viewSizeOutOfBounds,
final int totalSize,
@@ -696,9 +698,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
@Override
public boolean onMove(final RecyclerView recyclerView,
final RecyclerView.ViewHolder source,
final RecyclerView.ViewHolder target) {
public boolean onMove(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder source,
@NonNull final RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()
|| itemListAdapter == null) {
return false;
@@ -724,7 +726,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
@Override
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { }
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int swipeDir) {
}
};
}
@@ -757,7 +761,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
StreamDialogEntry.append_playlist,
StreamDialogEntry.share
));
} else {
} else {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
@@ -767,9 +771,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
StreamDialogEntry.share
));
}
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->

View File

@@ -1,10 +1,7 @@
package org.schabi.newpipe.local.subscription
import android.content.Context
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes
import org.schabi.newpipe.R
import org.schabi.newpipe.util.ThemeHelper
enum class FeedGroupIcon(
/**
@@ -13,51 +10,51 @@ enum class FeedGroupIcon(
val id: Int,
/**
* The attribute that points to a drawable resource. "R.attr" is used here to support multiple themes.
* The drawable resource.
*/
@AttrRes val drawableResourceAttr: Int
@DrawableRes val drawableResource: Int
) {
ALL(0, R.attr.ic_asterisk),
MUSIC(1, R.attr.ic_music_note),
EDUCATION(2, R.attr.ic_school),
FITNESS(3, R.attr.ic_fitness_center),
SPACE(4, R.attr.ic_telescope),
COMPUTER(5, R.attr.ic_computer),
GAMING(6, R.attr.ic_videogame_asset),
SPORTS(7, R.attr.ic_sports),
NEWS(8, R.attr.ic_megaphone),
FAVORITES(9, R.attr.ic_heart),
CAR(10, R.attr.ic_car),
MOTORCYCLE(11, R.attr.ic_motorcycle),
TREND(12, R.attr.ic_trending_up),
MOVIE(13, R.attr.ic_movie),
BACKUP(14, R.attr.ic_backup),
ART(15, R.attr.ic_palette),
PERSON(16, R.attr.ic_person),
PEOPLE(17, R.attr.ic_people),
MONEY(18, R.attr.ic_money),
KIDS(19, R.attr.ic_child_care),
FOOD(20, R.attr.ic_fastfood),
SMILE(21, R.attr.ic_smile),
EXPLORE(22, R.attr.ic_explore),
RESTAURANT(23, R.attr.ic_restaurant),
MIC(24, R.attr.ic_mic),
HEADSET(25, R.attr.ic_headset),
RADIO(26, R.attr.ic_radio),
SHOPPING_CART(27, R.attr.ic_shopping_cart),
WATCH_LATER(28, R.attr.ic_watch_later),
WORK(29, R.attr.ic_work),
HOT(30, R.attr.ic_kiosk_hot),
CHANNEL(31, R.attr.ic_channel),
BOOKMARK(32, R.attr.ic_bookmark),
PETS(33, R.attr.ic_pets),
WORLD(34, R.attr.ic_world),
STAR(35, R.attr.ic_stars),
SUN(36, R.attr.ic_sunny),
RSS(37, R.attr.ic_rss);
ALL(0, R.drawable.ic_asterisk),
MUSIC(1, R.drawable.ic_music_note),
EDUCATION(2, R.drawable.ic_school),
FITNESS(3, R.drawable.ic_fitness_center),
SPACE(4, R.drawable.ic_telescope),
COMPUTER(5, R.drawable.ic_computer),
GAMING(6, R.drawable.ic_videogame_asset),
SPORTS(7, R.drawable.ic_directions_bike),
NEWS(8, R.drawable.ic_megaphone),
FAVORITES(9, R.drawable.ic_favorite),
CAR(10, R.drawable.ic_directions_car),
MOTORCYCLE(11, R.drawable.ic_motorcycle),
TREND(12, R.drawable.ic_trending_up),
MOVIE(13, R.drawable.ic_movie),
BACKUP(14, R.drawable.ic_backup),
ART(15, R.drawable.ic_palette),
PERSON(16, R.drawable.ic_person),
PEOPLE(17, R.drawable.ic_people),
MONEY(18, R.drawable.ic_attach_money),
KIDS(19, R.drawable.ic_child_care),
FOOD(20, R.drawable.ic_fastfood),
SMILE(21, R.drawable.ic_insert_emoticon),
EXPLORE(22, R.drawable.ic_explore),
RESTAURANT(23, R.drawable.ic_restaurant),
MIC(24, R.drawable.ic_mic),
HEADSET(25, R.drawable.ic_headset),
RADIO(26, R.drawable.ic_radio),
SHOPPING_CART(27, R.drawable.ic_shopping_cart),
WATCH_LATER(28, R.drawable.ic_watch_later),
WORK(29, R.drawable.ic_work),
HOT(30, R.drawable.ic_whatshot),
CHANNEL(31, R.drawable.ic_tv),
BOOKMARK(32, R.drawable.ic_bookmark),
PETS(33, R.drawable.ic_pets),
WORLD(34, R.drawable.ic_public),
STAR(35, R.drawable.ic_stars),
SUN(36, R.drawable.ic_wb_sunny),
RSS(37, R.drawable.ic_rss_feed);
@DrawableRes
fun getDrawableRes(context: Context): Int {
return ThemeHelper.resolveResourceIdFromAttr(context, drawableResourceAttr)
fun getDrawableRes(): Int {
return drawableResource
}
}

View File

@@ -1,17 +1,16 @@
package org.schabi.newpipe.local.subscription;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import icepick.Icepick;
import icepick.State;
@@ -24,13 +23,9 @@ public class ImportConfirmationDialog extends DialogFragment {
public static void show(@NonNull final Fragment fragment,
@NonNull final Intent resultServiceIntent) {
if (fragment.getFragmentManager() == null) {
return;
}
final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog();
confirmationDialog.setResultServiceIntent(resultServiceIntent);
confirmationDialog.show(fragment.getFragmentManager(), null);
confirmationDialog.show(fragment.getParentFragmentManager(), null);
}
public void setResultServiceIntent(final Intent resultServiceIntent) {
@@ -41,7 +36,7 @@ public class ImportConfirmationDialog extends DialogFragment {
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
assureCorrectAppLanguage(getContext());
return new AlertDialog.Builder(getContext(), ThemeHelper.getDialogTheme(getContext()))
return new AlertDialog.Builder(requireContext())
.setMessage(R.string.import_network_expensive_warning)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)

View File

@@ -1,15 +1,12 @@
package org.schabi.newpipe.local.subscription
import android.app.Activity
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.os.Bundle
import android.os.Environment
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.Menu
@@ -17,11 +14,12 @@ import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProvider
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import com.nononsenseapps.filepicker.Utils
import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item
@@ -34,6 +32,8 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogTitleBinding
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.ktx.animate
@@ -50,24 +50,20 @@ import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.KEY_FILE_PATH
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.util.FilePickerActivityHelper
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ShareUtils
import org.schabi.newpipe.util.ThemeHelper
import java.io.File
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCount
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.floor
import kotlin.math.max
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private var _binding: FragmentSubscriptionBinding? = null
@@ -86,6 +82,11 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
private val subscriptionsSection = Section()
private val requestExportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestExportResult)
private val requestImportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestImportResult)
@State
@JvmField
var itemsListState: Parcelable? = null
@@ -188,43 +189,39 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
private fun onImportPreviousSelected() {
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE)
requestImportLauncher.launch(StoredFileHelper.getPicker(activity))
}
private fun onExportSelected() {
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
val exportName = "newpipe_subscriptions_$date.json"
val exportFile = File(Environment.getExternalStorageDirectory(), exportName)
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE)
requestExportLauncher.launch(
StoredFileHelper.getNewPicker(activity, exportName, "application/json", null)
)
}
private fun openReorderDialog() {
FeedGroupReorderDialog().show(requireFragmentManager(), null)
FeedGroupReorderDialog().show(parentFragmentManager, null)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (data != null && data.data != null && resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_EXPORT_CODE) {
val exportFile = Utils.getFileForUri(data.data!!)
if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) {
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show()
} else {
activity.startService(
Intent(activity, SubscriptionsExportService::class.java)
.putExtra(KEY_FILE_PATH, exportFile.absolutePath)
)
}
} else if (requestCode == REQUEST_IMPORT_CODE) {
val path = Utils.getFileForUri(data.data!!).absolutePath
ImportConfirmationDialog.show(
this,
Intent(activity, SubscriptionsImportService::class.java)
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
.putExtra(KEY_VALUE, path)
)
}
fun requestExportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
activity.startService(
Intent(activity, SubscriptionsExportService::class.java)
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, result.data?.data)
)
}
}
fun requestImportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
ImportConfirmationDialog.show(
this,
Intent(activity, SubscriptionsImportService::class.java)
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
.putExtra(KEY_VALUE, result.data?.data)
)
}
}
@@ -256,7 +253,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
feedGroupsSortMenuItem = HeaderWithMenuItem(
getString(R.string.feed_groups_header_title),
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
R.drawable.ic_sort,
menuItemOnClickListener = ::openReorderDialog
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
@@ -280,25 +277,32 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView)
val shouldUseGridLayout = shouldUseGridLayout()
groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCount(context) else 1
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
binding.itemsList.adapter = groupAdapter
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(this::handleResult) })
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, { it?.let(this::handleFeedGroups) })
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) }
}
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
val commands = arrayOf(getString(R.string.share), getString(R.string.unsubscribe))
val commands = arrayOf(
getString(R.string.share),
getString(R.string.open_in_browser),
getString(R.string.unsubscribe)
)
val actions = DialogInterface.OnClickListener { _, i ->
when (i) {
0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url)
1 -> deleteChannel(selectedItem)
0 -> ShareUtils.shareText(
requireContext(), selectedItem.name, selectedItem.url,
selectedItem.thumbnailUrl
)
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
2 -> deleteChannel(selectedItem)
}
}
@@ -352,7 +356,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun handleResult(result: SubscriptionState) {
super.handleResult(result)
val shouldUseGridLayout = shouldUseGridLayout()
val shouldUseGridLayout = shouldUseGridLayout(context)
when (result) {
is SubscriptionState.LoadedState -> {
result.subscriptions.forEach {
@@ -381,7 +385,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
}
is SubscriptionState.ErrorState -> {
result.error?.let { onError(result.error) }
result.error?.let {
showError(ErrorInfo(result.error, UserAction.SOMETHING_ELSE, "Subscriptions"))
}
}
}
}
@@ -411,46 +417,4 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
super.hideLoading()
binding.itemsList.animate(true, 200)
}
// /////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
// /////////////////////////////////////////////////////////////////////////
override fun onError(exception: Throwable): Boolean {
if (super.onError(exception)) return true
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error)
return true
}
// /////////////////////////////////////////////////////////////////////////
// Grid Mode
// /////////////////////////////////////////////////////////////////////////
// TODO: Move these out of this class, as it can be reused
private fun shouldUseGridLayout(): Boolean {
val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
return when (listMode) {
getString(R.string.list_view_mode_auto_key) -> {
val configuration = resources.configuration
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
}
getString(R.string.list_view_mode_grid_key) -> true
else -> false
}
}
private fun getGridSpanCount(): Int {
val minWidth = resources.getDimensionPixelSize(R.dimen.channel_item_grid_min_width)
return max(1, floor(resources.displayMetrics.widthPixels / minWidth.toDouble()).toInt())
}
companion object {
private const val REQUEST_EXPORT_CODE = 666
private const val REQUEST_IMPORT_CODE = 667
}
}

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