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

Compare commits

...

693 Commits

Author SHA1 Message Date
Stypox
4227866fcf Improve changelog for v0.23.3 (989) 2022-08-25 10:44:29 +02:00
Stypox
335e682299 Merge pull request #8880 from TeamNewPipe/release-0.23.3
Hotfix release v0.23.3
2022-08-25 10:40:01 +02:00
Stypox
5c0ed22b09 Add changelog for v0.23.3 (989) 2022-08-25 10:23:09 +02:00
Stypox
e1b8a3fbdf Hotfix release v0.23.3 (989) 2022-08-25 10:16:56 +02:00
Stypox
1a432f2ee3 Update jsoup to 1.15.3
This fixes a vulnerability issue related to Cross Site Scripting
2022-08-25 10:15:30 +02:00
Stypox
db45042a56 Update NewPipeExtractor
This removes the usage of the SourceVersion class, which was not available on Android and caused issues such as #8876
2022-08-25 10:14:46 +02:00
Tobi
50f3131f1a Merge pull request #8766 from TeamNewPipe/release-0.23.2
Release v0.23.2 (988)
2022-08-13 08:07:49 +02:00
AudricV
fcaebc838e Release v0.23.2 (988) 2022-08-12 23:50:41 +02:00
AudricV
cde32a8aed Add changelog for v0.23.2 (988) 2022-08-12 23:49:07 +02:00
AudricV
ec3efea05a Update NewPipe Extractor to fix YouTube playback issues 2022-08-12 23:41:12 +02:00
Stypox
5ac71e0579 Merge pull request #8547 from TeamNewPipe/release-0.23.1
Release v0.23.1 (987)
2022-07-05 15:48:14 +02:00
Hosted Weblate
d04ecbcb0a Translated using Weblate (Kazakh)
Currently translated at 2.9% (2 of 68 strings)

Translated using Weblate (French)

Currently translated at 69.1% (47 of 68 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.8% (639 of 640 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 91.2% (584 of 640 strings)

Translated using Weblate (Armenian)

Currently translated at 28.7% (184 of 640 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 99.8% (639 of 640 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Added translation using Weblate (Kazakh)

Translated using Weblate (Bengali)

Currently translated at 20.5% (14 of 68 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Swedish)

Currently translated at 99.3% (636 of 640 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Latvian)

Currently translated at 92.6% (593 of 640 strings)

Translated using Weblate (Bengali)

Currently translated at 19.1% (13 of 68 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.8% (639 of 640 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (639 of 640 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (639 of 640 strings)

Translated using Weblate (French)

Currently translated at 99.8% (639 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (639 of 640 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (68 of 68 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (68 of 68 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 61.7% (42 of 68 strings)

Translated using Weblate (French)

Currently translated at 66.1% (45 of 68 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (68 of 68 strings)

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

Currently translated at 11.7% (8 of 68 strings)

Translated using Weblate (Turkish)

Currently translated at 32.3% (22 of 68 strings)

Translated using Weblate (Bengali)

Currently translated at 88.9% (569 of 640 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.2% (635 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (640 of 640 strings)

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

Currently translated at 97.1% (622 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (French)

Currently translated at 99.8% (639 of 640 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 39.7% (27 of 68 strings)

Translated using Weblate (Polish)

Currently translated at 55.8% (38 of 68 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (68 of 68 strings)

Translated using Weblate (Persian)

Currently translated at 99.5% (637 of 640 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 66.1% (45 of 68 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Andrij Mizyk <andmiz@proton.me>
Co-authored-by: D āvis <dlektauers@gmail.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: David Braz <davidbrazps2@gmail.com>
Co-authored-by: Davit Mayilyan <davit.mayilyan@protonmail.ch>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Erik J <ej_rostock@web.de>
Co-authored-by: Giovanni Donisi <giovannidonisi0701@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Julie WF <julie-99@live.no>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Nizami <nizamismidov4@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: ReVanced <revanced2022@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: S3aBreeze <paperwork@evilcorp.ltd>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: SameenAhnaf <sameenahnaf@yahoo.com>
Co-authored-by: Stypox <stypox@pm.me>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yeldar Kudaibergenov <mail@yeldar.org>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: patrik <jpekman@gmail.com>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: riveravaldez <riveravaldezmail@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: zica <9918800@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/az/
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/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/kk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2022-07-05 15:42:20 +02:00
Stypox
e4987d9a59 Update NewPipeExtractor again 2022-07-04 23:30:20 +02:00
litetex
155c6e94a3 Use correct `NonNull` 2022-07-04 23:19:41 +02:00
litetex
4e285a4e70 Fix compile errors 2022-07-04 23:19:41 +02:00
litetex
9c00e681bb Updated extractor to latest revision 2022-07-04 23:19:41 +02:00
Stypox
160891592b Merge pull request #8564 from Stypox/fix-view-count
Actually fix history view count
2022-07-04 23:06:09 +02:00
Stypox
085d1e0d38 Actually fix wrong view count 2022-07-01 16:07:19 +02:00
Stypox
4ee1cd5826 Release v0.23.1 (987) 2022-06-24 19:01:37 +02:00
Stypox
dc7fce86a5 Add changelog for v0.23.1 (987) 2022-06-24 18:52:45 +02:00
Hosted Weblate
10c9661369 Translated using Weblate (Bengali)
Currently translated at 89.0% (564 of 633 strings)

Translated using Weblate (Finnish)

Currently translated at 95.8% (607 of 633 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 65.5% (415 of 633 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (French)

Currently translated at 100.0% (633 of 633 strings)

Added translation using Weblate (English (Old))

Translated using Weblate (French)

Currently translated at 65.6% (44 of 67 strings)

Translated using Weblate (Basque)

Currently translated at 25.3% (17 of 67 strings)

Translated using Weblate (Filipino)

Currently translated at 5.9% (4 of 67 strings)

Translated using Weblate (Bengali)

Currently translated at 88.3% (559 of 633 strings)

Translated using Weblate (Bengali (India))

Currently translated at 49.2% (312 of 633 strings)

Translated using Weblate (Bengali (India))

Currently translated at 49.2% (312 of 633 strings)

Translated using Weblate (Filipino)

Currently translated at 36.9% (234 of 633 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 90.5% (573 of 633 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (French)

Currently translated at 64.1% (43 of 67 strings)

Translated using Weblate (Dutch)

Currently translated at 70.1% (47 of 67 strings)

Translated using Weblate (Italian)

Currently translated at 38.8% (26 of 67 strings)

Translated using Weblate (Azerbaijani)

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 (Indonesian)

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 (Azerbaijani)

Currently translated at 100.0% (67 of 67 strings)

Translated using Weblate (Azerbaijani)

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 (Azerbaijani)

Currently translated at 100.0% (67 of 67 strings)

Translated using Weblate (Turkish)

Currently translated at 31.3% (21 of 67 strings)

Translated using Weblate (Bengali)

Currently translated at 85.6% (542 of 633 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 99.8% (632 of 633 strings)

Translated using Weblate (Malayalam)

Currently translated at 92.1% (583 of 633 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Vietnamese)

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 (Danish)

Currently translated at 93.0% (589 of 633 strings)

Translated using Weblate (Danish)

Currently translated at 4.4% (3 of 67 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Danish)

Currently translated at 75.0% (475 of 633 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Latvian)

Currently translated at 93.8% (594 of 633 strings)

Translated using Weblate (Latvian)

Currently translated at 93.8% (594 of 633 strings)

Translated using Weblate (Latvian)

Currently translated at 4.4% (3 of 67 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (67 of 67 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (67 of 67 strings)

Translated using Weblate (Belarusian)

Currently translated at 1.4% (1 of 67 strings)

Translated using Weblate (Bengali)

Currently translated at 84.9% (538 of 633 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Danish)

Currently translated at 50.3% (319 of 633 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 63.8% (404 of 633 strings)

Translated using Weblate (Chinese (Traditional))

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 (Portuguese (Brazil))

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Polish)

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 (Ukrainian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Slovak)

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 (Portuguese)

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 (Italian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Serbian)

Currently translated at 92.4% (585 of 633 strings)

Translated using Weblate (Dutch)

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 (Spanish)

Currently translated at 97.7% (619 of 633 strings)

Translated using Weblate (German)

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 (Vietnamese)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Cerins <cerins4141@gmail.com>
Co-authored-by: Corc <nizamismidov4@gmail.com>
Co-authored-by: Cyndaquissshhh <iversonbriones123@gmail.com>
Co-authored-by: Công Phúc <timnguyenaklc1133@gmail.com>
Co-authored-by: D āvis <dlektauers@gmail.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Digiwizkid <subhadiplayek@gmail.com>
Co-authored-by: Edward <edwardchirita@mailbox.org>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Giovanni Donisi <giovannidonisi0701@gmail.com>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Himadri Bhattacharjee <handhimadrink@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jacob <axin6e7weujc@beconfidential.com>
Co-authored-by: Jalaluddin <ju81@ymail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Linkan Majumder <linkan469@gmail.com>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Mathias Hamza Vedsted-Mirza <mathiashamzamirza@outlook.com>
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
Co-authored-by: MohammedSR Vevo <mohammednajmidin@gmail.com>
Co-authored-by: Nicky Db <nickydbruyn@gmail.com>
Co-authored-by: Nikhil Anilkumar <rootshell348@gmail.com>
Co-authored-by: Nikodem Zawirski <nikon96@gmail.com>
Co-authored-by: Nizami <nizamismidov4@gmail.com>
Co-authored-by: Nizami semidov <revanced2022@gmail.com>
Co-authored-by: Onni <onnip@protonmail.com>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Ricardo <contatorms7@tutamail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Steven Felix <stevenfelix505@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Terry Louwers <t.louwers@gmail.com>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: WB <web0nst@tuta.io>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: bomzhellino <adm.bomzh@gmail.com>
Co-authored-by: bruh <quangtrung02hn16@gmail.com>
Co-authored-by: jazzyjabroni <lordcarmack@tuta.io>
Co-authored-by: metezd <itoldyouthat@protonmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: translator <kvb@tuta.io>
Co-authored-by: vmisovic <vladimir.misovic03@gmail.com>
Co-authored-by: zeritti <woodenmo@posteo.de>
Co-authored-by: Éfrit <efrit@posteo.net>
Co-authored-by: Óscar Fernández Díaz <oscfdezdz@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/az/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/be/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/da/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/eu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fil/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translation: NewPipe/Metadata
2022-06-24 15:12:07 +02:00
Stypox
3901ffca17 Merge pull request #8153 from AudricV/delivery-methods-v2
Support delivery methods other than progressive HTTP
2022-06-22 11:21:52 +02:00
AudricV
cbd3308da6 Ensure that progressive contents are URL contents for playback
A ResolverException will be now thrown otherwise.
2022-06-19 15:41:29 +02:00
Stypox
0ad6b3b88e Improve download_dialog.xml unsupported streams notice 2022-06-18 19:16:36 +02:00
Stypox
4e87f5aabc Remove misleading first "Non" from getNonUrlAndNonTorrentStreams 2022-06-18 18:52:32 +02:00
Stypox
2019af831a Refactor PlaybackResolver and fix cacheKeyOf
In commonCacheKeyOf the result of an Objects.hash() was ignored
2022-06-18 18:41:44 +02:00
Stypox
1e076ea63d Wrap debug log in if(DEBUG) 2022-06-18 18:09:12 +02:00
Stypox
4863084fa2 Improve code in VideoDetailFragment 2022-06-18 17:49:04 +02:00
Stypox
7ba79171c7 Refactor creation of DownloadDialog 2022-06-18 17:40:22 +02:00
AudricV
e3c2aea3cc Fix playback of non-URI HLS streams
A custom HlsPlaylistParserFactory cannot be used anymore to play HLS streams.

This needs to be replaced by a custom HlsDataSourceFactory, which returns a ByteArrayDataSource (where the bytes of this DataSource correspond to the bytes of the playlist string) and a specified DataSource for other request types.

This model has two limitations:

- if media requests are relative, the URI from which the manifest comes from (either the manifest URI (preferred) or the master URI (if applicable)) must be returned, otherwise the content will be not playable, as it will be an invalid URL, or it may be treat as something unexpected, for instance as a file for DefaultDataSources;
- if the playlist is a master playlist, endless loops should be encountered because the DataSources created for media playlists will use the master playlist response instead of fetching the corresponding playlist. With the current model of HlsDataSourceFactory, there is no possibility to distinguish the playlist type or the URI that is requested.

If ExoPlayer provides a way to create HlsMediaSources with an HlsPlaylist in the future, it should be used instead of this solution.
2022-06-17 22:01:30 +02:00
AudricV
21c9530e8b Throw a dedicated exception when errors occur in PlaybackResolver
A new exception, ResolverException, a subclass of PlaybackResolver, is now thrown when errors occur in PlaybackResolver, instead of an IOException
2022-06-17 22:01:29 +02:00
AudricV
036196a487 Filter streams using Java 8 Stream's API instead of removing streams with list iterators and add a better toast when there is no audio stream for external players
This ensures to not remove streams from the StreamInfo lists themselves, and so to not have to create list copies.

The toast shown in RouterActivity, when there is no audio stream available for external players, is now shown, in the same case, when pressing the background button in VideoDetailFragment.
2022-06-17 22:01:29 +02:00
AudricV
73855cacb7 Use StreamTypeUtil where possible and add isAudio and isVideo to this utility class 2022-06-17 22:01:26 +02:00
Stypox
8dad6d7e1c Code improvements here and there 2022-06-17 22:00:53 +02:00
Stypox
e5ffa2aa09 Add comments to PlaybackResolver and remove useless @NonNull 2022-06-17 22:00:52 +02:00
Stypox
8445c381c5 Use DownloaderImpl.USER_AGENT directly
instead of passing it as a parameter
2022-06-17 22:00:52 +02:00
Stypox
fa46b7bf85 Add comments and use downloader user agent in YT data source
YoutubeHttpDataSource
2022-06-17 22:00:52 +02:00
Stypox
7ce2250d85 Improve CacheFactory and PlayerDataSource code 2022-06-17 22:00:51 +02:00
Stypox
ef20d9b91a Move stream's cache key generation in PlaybackResolver and improve PlaybackResolver's code 2022-06-17 22:00:51 +02:00
AudricV
fbee310261 Move SimpleCache creation in PlayerDataSource to avoid an IllegalStateException
This IllegalStateException, almost not reproducible, indicates that another SimpleCache instance uses the cache folder, which was so trying to be created at least twice.
Moving the SimpleCache creation in PlayerDataSource should avoid this exception.
2022-06-17 22:00:51 +02:00
AudricV
7d6bf4b0ca Improve dialog of streams for external players and fix use of the wrong codec in the list of available streams in it after a codec change in Video and Audio settings
The VideoDetailFragment will now get video streams dynamically instead of storing them as a field, so the good codec can be chosen by ListHelper.
To select a stream to play, user has now to select the quality in the list of available qualities and then press the new OK button in the alert dialog.
2022-06-17 22:00:50 +02:00
AudricV
210834fbe9 Add support of other delivery methods than progressive HTTP (in the player only)
Detailed changes:

- External players:

  - Add a message instruction about stream selection;
  - Add a message when there is no stream available for external players;
  - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones.

- Player:

  - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones;
  - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters);
  - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams;
  - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents).

- Download dialog:

  - Add message about support of progressive HTTP streams only for downloading;
  - Remove several duplicated code and update relevant usages;
  - Support downloading of contents with an unknown media format.

- ListHelper:

  - Catch NumberFormatException when trying to compare two video streams between them.

- Tests:

  - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor.

- Other places:

  - Fixes deprecation of changes made in the extractor;
  - Improve some code related to the files changed.

- Issues fixed and/or improved with the changes:

  - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor));
  - Crash when loading PeerTube streams with a separated audio;
  - Lack of some streams on some YouTube videos (OTF streams);
  - Loading times of YouTube streams, after a quality change or a playback start;
  - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams);
  - Watchable time of YouTube ended livestreams;
  - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-17 22:00:22 +02:00
Stypox
a59660f421 Merge pull request #8340 from litetex/fix-add-to-playlist
Fix "Add to playlist" not working and cleanup "RouterActivity" choice handling
2022-06-05 11:47:51 +02:00
litetex
be5af0b777 Made statusbar color transparent for RouterActivity (Android 5+)
Fix for https://github.com/TeamNewPipe/NewPipe/pull/8332
2022-06-04 15:22:36 +02:00
Stypox
75e5fe7d27 Merge pull request #8404 from Isira-Seneviratne/Use_AppCompatResources
Use AppCompatResources.
2022-05-30 15:42:04 +02:00
litetex
2985258074 Bonus fix: Made `single_choice_dialog_view` scrollable + use viewbinding 2022-05-28 00:46:28 +02:00
litetex
911ac65d1e Code cleanup 2022-05-28 00:46:27 +02:00
litetex
d2967f514b Improved docs, format and code style 2022-05-28 00:46:27 +02:00
litetex
a68c6a2cfc Reworked incorrect choice handling and centralized it 2022-05-28 00:39:13 +02:00
litetex
733f6aae85 Fix add to playlist 2022-05-28 00:39:13 +02:00
Stypox
1daece3bee Merge pull request #8382 from Isira-Seneviratne/Remove_compat_methods
Remove unnecessary compat method calls.
2022-05-22 21:59:04 +02:00
Stypox
adddd48c1d Merge pull request #8391 from Isira-Seneviratne/Use_JvmOverloads
Use JvmOverloads.
2022-05-22 21:56:17 +02:00
Stypox
8c870cd3ca Merge pull request #8143 from TiA4f8R/image-preview-sharesheet
Add image preview of the content shared where possible in Android share sheet (for Android 10+ devices only)
2022-05-22 21:45:53 +02:00
Stypox
bd5eda92a7 Improvements to sharing content with thumbnail 2022-05-22 21:34:10 +02:00
Jorge Maldonado Ventura
cf09cef6d8 Spanish Readme fix (#8413) 2022-05-19 16:39:08 +03:00
litetex
b3f9f8275d Merge pull request #8406 from TacoTheDank/bumpPlugins
Update AGP and Kotlin
2022-05-18 19:56:32 +02:00
litetex
9597d474d0 Merge pull request #8407 from TacoTheDank/bumpACRAGroupie
Update Groupie and ACRA libraries
2022-05-18 19:56:09 +02:00
TacoTheDank
e6f2e9791c Update Groupie and ACRA libraries 2022-05-16 11:45:56 -04:00
TacoTheDank
31b1370270 Fix AndroidX library order 2022-05-16 10:54:44 -04:00
TacoTheDank
064a4ce798 Update AGP and Kotlin 2022-05-16 10:53:43 -04:00
Isira Seneviratne
ac5843edb0 Merge branch 'dev' into Use_JvmOverloads 2022-05-16 12:43:24 +05:30
Isira Seneviratne
a1f64e4774 Merge branch 'dev' into Remove_compat_methods 2022-05-16 12:36:46 +05:30
Isira Seneviratne
21d2ae709f Merge branch 'dev' into Use_AppCompatResources
# Conflicts:
#	app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
2022-05-16 12:36:00 +05:30
Isira Seneviratne
c5e509f069 Use AppCompatResources. 2022-05-16 12:27:44 +05:30
TiA4f8R
761c0ff9ac [Android 10+] Add image preview of the content shared where possible
These previews will be only available for images cached in the cache used by Picasso.
The Bitmap of the content is compressed in JPEG 90 and saved inside the application cache folder under the name android_share_sheet_image_preview.jpg.
The current image will be, of course, always overwritten by the next one and cleared when the application cache is cleared.
2022-05-14 16:53:24 +02:00
Isira Seneviratne
ce8289e753 Use JvmOverloads. 2022-05-13 07:46:02 +05:30
Stypox
2dd4f8b04a Merge pull request #7458 from litetex/rework-subscription-import-export-ui
Moved subscription import/export options to (overflow) menu
2022-05-12 13:03:12 +02:00
Stypox
b4615f7655 Merge pull request #7355 from petlyh/append-remote-playlist
Add a "Add to playlist" item to the remote playlist menu
2022-05-12 12:03:58 +02:00
petlyh
fcaa787060 Add a "Add to playlist" item to the remote playlist menu 2022-05-10 08:48:21 +02:00
Isira Seneviratne
23c1fc3544 Remove unnecessary compat method calls. 2022-05-10 07:45:01 +05:30
Robin
a4037a8268 Merge pull request #8377 from iTrooz/vol_slider
Make volume progress bar match system volume when we start sliding
2022-05-10 00:33:29 +02:00
iTrooz_
61ee1c61df Make volume progress bar match system volume when we start sliding 2022-05-09 21:40:13 +02:00
litetex
69f95f4148 Use better way to get services 2022-05-09 20:58:10 +02:00
Saurmandal
212a413e93 HI translation to README.md (#8355)
* Add `hi` translation

* Update other Readmes

* lot of corrections (thanks @opusforlife2 !)
2022-05-08 11:39:30 +00:00
litetex
de4b5a8f0f Remove not required use of supplier
from code review
2022-05-07 15:08:38 +02:00
litetex
1228ce277f Removed placeholder prefix 2022-05-07 15:08:37 +02:00
litetex
bd6fdd625a Use material icon 2022-05-07 15:08:37 +02:00
litetex
7de17ad949 Icons for import/export 2022-05-07 15:08:36 +02:00
litetex
7ab11a8379 Used service icons for import 2022-05-07 15:08:36 +02:00
litetex
70e0085596 Converted placeholders to svg
* Required for SubscriptionFragment (otherwise the PopUp-menu uses half of the screen)
* Size reduction
* Fixed/Improved some images:
  * Bandcamp: Was facing in the wrong direction and used an incorrect logo
  * Media CCC: Update logo
  * YT: Added NewPipe logo so that it's not just a rectangle
2022-05-07 15:08:35 +02:00
litetex
f9ccc19df5 Fix popup-menu expand icon color 2022-05-07 15:08:35 +02:00
litetex
5c69568c7f Remove unused `dimens` 2022-05-07 15:08:35 +02:00
litetex
1d69bd48be Moved import/export options to menu 2022-05-07 15:08:34 +02:00
Stypox
5b435c586e Merge pull request #8192 from GGAutomaton/fix-6696
Fix crash when rotating device on unsupported channels
2022-05-06 10:58:59 +02:00
Stypox
71e46d1eca Do not call showContentNotSupportedIfNeeded multiple times 2022-05-06 10:40:08 +02:00
Stypox
238aff7c31 channelContentNotSupported false by default 2022-05-06 10:38:47 +02:00
Stypox
a1435bd566 Merge pull request #8259 from LingYinTianMeng/dev
Fix removing only fully watched videos from playlist
2022-05-05 18:08:41 +02:00
Stypox
59d8c570b7 Readd spaces 2022-05-05 18:04:33 +02:00
Robin
8f34f69397 Merge pull request #8332 from litetex/fix-routeractivity-theming
Fix Routeractivity theming
2022-05-05 14:08:01 +02:00
litetex
47af21d248 Merge pull request #8336 from Mamadou78130/fix8330
Fixed viewed counting
2022-05-04 19:33:36 +02:00
litetex
c2a3c1cb8f Add comment that explains why 0 is used
Co-authored-by: Stypox <stypox@pm.me>
2022-05-04 19:19:45 +02:00
litetex
1e2d76a686 Use non-static method 2022-05-04 19:09:41 +02:00
GGAutomaton
34468c16ad Show "content not supported" if needed 2022-05-04 23:34:07 +08:00
Robin
b84c6b4b32 Merge pull request #8315 from ktprograms/fix-media-button-hide-controls
Fix hiding player controls when playing from media button
2022-05-04 11:28:05 +02:00
Stypox
8395cf8d5a Merge pull request #8349 from litetex/fix-PlaybackParameterDialog-resetting
Fixed accidental reset of ``PlaybackParameterDialog`` on initialization
2022-05-04 09:12:59 +02:00
litetex
c2bf7f09ce Fixed accidental reset of `PlaybackParameterDialog` on initialization 2022-05-03 21:42:09 +02:00
LingYinTianMeng
c2762d3b5e Update LocalPlaylistFragment.java 2022-05-03 09:37:35 +08:00
LingYinTianMeng
01d996a5c0 Merge branch 'TeamNewPipe:dev' into dev 2022-05-03 09:26:32 +08:00
LingYinTianMeng
50739277c4 Update LocalPlaylistFragment.java 2022-05-03 09:21:43 +08:00
Hosted Weblate
0fef4e6e2e Translated using Weblate (Bengali (India))
Currently translated at 45.6% (289 of 633 strings)

Translated using Weblate (Danish)

Currently translated at 47.2% (299 of 633 strings)

Translated using Weblate (Portuguese (Brazil))

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 (Portuguese (Portugal))

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 (Swedish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Tagalog)

Currently translated at 9.4% (60 of 633 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 5.9% (4 of 67 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 14.9% (10 of 67 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (67 of 67 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 61.1% (41 of 67 strings)

Translated using Weblate (Slovak)

Currently translated at 7.4% (5 of 67 strings)

Translated using Weblate (Persian)

Currently translated at 62.6% (42 of 67 strings)

Translated using Weblate (Swedish)

Currently translated at 50.7% (34 of 67 strings)

Translated using Weblate (Spanish)

Currently translated at 59.7% (40 of 67 strings)

Translated using Weblate (Indonesian)

Currently translated at 79.1% (53 of 67 strings)

Translated using Weblate (Polish)

Currently translated at 55.2% (37 of 67 strings)

Translated using Weblate (Hebrew)

Currently translated at 55.2% (37 of 67 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (67 of 67 strings)

Translated using Weblate (Russian)

Currently translated at 17.9% (12 of 67 strings)

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

Currently translated at 10.4% (7 of 67 strings)

Translated using Weblate (Turkish)

Currently translated at 28.3% (19 of 67 strings)

Translated using Weblate (German)

Currently translated at 65.6% (44 of 67 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Filipino)

Currently translated at 18.7% (119 of 633 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Hebrew)

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 (Chinese (Simplified))

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.6% (618 of 633 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Polish)

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 (Ukrainian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Slovak)

Currently translated at 98.5% (624 of 633 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (633 of 633 strings)

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

Currently translated at 90.0% (570 of 633 strings)

Translated using Weblate (Basque)

Currently translated at 95.7% (606 of 633 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Korean)

Currently translated at 71.8% (455 of 633 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (630 of 633 strings)

Translated using Weblate (Russian)

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 (Spanish)

Currently translated at 97.6% (618 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 (Chinese (Simplified))

Currently translated at 99.8% (632 of 633 strings)

Translated using Weblate (German)

Currently translated at 98.1% (621 of 633 strings)

Translated using Weblate (German)

Currently translated at 98.1% (621 of 633 strings)

Translated using Weblate (Swedish)

Currently translated at 99.6% (631 of 633 strings)

Translated using Weblate (Swedish)

Currently translated at 99.6% (631 of 633 strings)

Translated using Weblate (French)

Currently translated at 99.5% (630 of 633 strings)

Translated using Weblate (French)

Currently translated at 99.5% (630 of 633 strings)

Translated using Weblate (French)

Currently translated at 99.3% (629 of 633 strings)

Translated using Weblate (French)

Currently translated at 99.3% (629 of 633 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alberto De Negri <scemottero@hotmail.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andrés Paredes <andresparedeszaa@gmail.com>
Co-authored-by: Ayoub Rejal <ayoubrejal@gmail.com>
Co-authored-by: BurningKarl <karl.welzel@gmail.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: DanieLoche <danieloche@gmail.com>
Co-authored-by: DanieLoche <dloche+weblate@danlo.fr>
Co-authored-by: David Kovács <davidkovacs12321@gmail.com>
Co-authored-by: Deleted User <noreply+43355@weblate.org>
Co-authored-by: Digiwizkid <subhadiplayek@gmail.com>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: JS Ahn <freirepublik@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Jonatan Nyberg <jonatan@autistici.org>
Co-authored-by: Jonathan Soares <jpub@disroot.org>
Co-authored-by: Karl Tammik <karltammik@protonmail.com>
Co-authored-by: Lars <weblate.org@lehtrab.de>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Napstaguy04 <brokenscreen3@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Ricardo <contatorms7@tutamail.com>
Co-authored-by: Simon N <Observeramera@pm.me>
Co-authored-by: TiA4f8R <avdivers84@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: jazzyjabroni <lordcarmack@tuta.io>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: nzgha <nzgha.hw@runbox.com>
Co-authored-by: qqqq1 <qqqq1@hi2.in>
Co-authored-by: sal0max <msal.coding@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Егор Ермаков <eg.ermakov2016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar_LY/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fa/
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/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata

Translated using Weblate (Spanish)

Currently translated at 97.6% (618 of 633 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (633 of 633 strings)

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

Currently translated at 90.0% (570 of 633 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Croatian)

Currently translated at 98.4% (623 of 633 strings)

Translated using Weblate (Filipino)

Currently translated at 28.7% (182 of 633 strings)
2022-05-02 20:46:35 +02:00
Stypox
218012558a Merge pull request #8329 from litetex/cleanup-unused-strings
Removed unused ``strings.xml`` resources
2022-05-02 19:24:22 +02:00
Mamadou WAGUE
e40e86500b Fixed viewed counting 2022-05-02 14:52:43 +02:00
litetex
6f0942ac6e Make sure Routeractivity does the same as MainActivity 2022-05-01 21:59:00 +02:00
litetex
a67927c29c Fix dialogs having incorrect color when opened via RouterActivity 2022-05-01 21:56:49 +02:00
litetex
7e50eed95e Removed unused string resources 2022-05-01 20:50:37 +02:00
Stypox
173b6c3f00 Fix wrong NonNull 2022-04-30 21:46:06 +02:00
Stypox
7646c683b5 Merge pull request #7989 from litetex/refactor-playback-parameter-dialog
Rewrote ``PlaybackParameterDialog``
2022-04-30 17:53:26 +02:00
kt programs
047fe21c14 Fix hiding player controls when playing from media button
DefaultControlDispatcher was removed in ExoPlayer 2.16.0, so the class
extending it that handled play/pause was removed in #8020.

The new solution is to use an instance of ForwardingPlayer. Call
sessionConnector.setPlayer with an instance of ForwardingPlayer that
overrides play() and pause() and calls the callback methods.
2022-04-30 17:43:30 +08:00
Stypox
b59a601b52 Merge branch 'master' into dev 2022-04-29 16:41:18 +02:00
Stypox
ecb8ef6bb1 Merge pull request #8231 from TeamNewPipe/release/0.23.0
Release 0.23.0 (986)
2022-04-29 16:39:47 +02:00
Stypox
cd2eab6ba2 Merge pull request #8302 from Stypox/default-progressive-load-interval
Use 64 KiB as the default progressive load interval
2022-04-29 16:21:19 +02:00
Stypox
6a4d8329c3 Rename progressive_load_interval_exoplayer_default for all languages 2022-04-29 16:11:28 +02:00
Stypox
b8dbb3f073 Use 64 KiB as the default progressive load interval
This ensures a small value is used by default, solving buffering issues at the beginning of videos
2022-04-29 16:10:39 +02:00
Hosted Weblate
9a5decdb28 Translated using Weblate (Bengali (India))
Currently translated at 45.6% (289 of 633 strings)

Translated using Weblate (Danish)

Currently translated at 47.2% (299 of 633 strings)

Translated using Weblate (Portuguese (Brazil))

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 (Portuguese (Portugal))

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 (Swedish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Tagalog)

Currently translated at 9.4% (60 of 633 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 5.9% (4 of 67 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 14.9% (10 of 67 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (67 of 67 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 61.1% (41 of 67 strings)

Translated using Weblate (Slovak)

Currently translated at 7.4% (5 of 67 strings)

Translated using Weblate (Persian)

Currently translated at 62.6% (42 of 67 strings)

Translated using Weblate (Swedish)

Currently translated at 50.7% (34 of 67 strings)

Translated using Weblate (Spanish)

Currently translated at 59.7% (40 of 67 strings)

Translated using Weblate (Indonesian)

Currently translated at 79.1% (53 of 67 strings)

Translated using Weblate (Polish)

Currently translated at 55.2% (37 of 67 strings)

Translated using Weblate (Hebrew)

Currently translated at 55.2% (37 of 67 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (67 of 67 strings)

Translated using Weblate (Russian)

Currently translated at 17.9% (12 of 67 strings)

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

Currently translated at 10.4% (7 of 67 strings)

Translated using Weblate (Turkish)

Currently translated at 28.3% (19 of 67 strings)

Translated using Weblate (German)

Currently translated at 65.6% (44 of 67 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Filipino)

Currently translated at 18.7% (119 of 633 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Hebrew)

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 (Chinese (Simplified))

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.6% (618 of 633 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Polish)

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 (Ukrainian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Slovak)

Currently translated at 98.5% (624 of 633 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (633 of 633 strings)

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

Currently translated at 90.0% (570 of 633 strings)

Translated using Weblate (Basque)

Currently translated at 95.7% (606 of 633 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Korean)

Currently translated at 71.8% (455 of 633 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (630 of 633 strings)

Translated using Weblate (Russian)

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 (Spanish)

Currently translated at 97.6% (618 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 (Chinese (Simplified))

Currently translated at 99.8% (632 of 633 strings)

Translated using Weblate (German)

Currently translated at 98.1% (621 of 633 strings)

Translated using Weblate (German)

Currently translated at 98.1% (621 of 633 strings)

Translated using Weblate (Swedish)

Currently translated at 99.6% (631 of 633 strings)

Translated using Weblate (Swedish)

Currently translated at 99.6% (631 of 633 strings)

Translated using Weblate (French)

Currently translated at 99.5% (630 of 633 strings)

Translated using Weblate (French)

Currently translated at 99.5% (630 of 633 strings)

Translated using Weblate (French)

Currently translated at 99.3% (629 of 633 strings)

Translated using Weblate (French)

Currently translated at 99.3% (629 of 633 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alberto De Negri <scemottero@hotmail.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andrés Paredes <andresparedeszaa@gmail.com>
Co-authored-by: Ayoub Rejal <ayoubrejal@gmail.com>
Co-authored-by: BurningKarl <karl.welzel@gmail.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: DanieLoche <danieloche@gmail.com>
Co-authored-by: DanieLoche <dloche+weblate@danlo.fr>
Co-authored-by: David Kovács <davidkovacs12321@gmail.com>
Co-authored-by: Deleted User <noreply+43355@weblate.org>
Co-authored-by: Digiwizkid <subhadiplayek@gmail.com>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: JS Ahn <freirepublik@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Jonatan Nyberg <jonatan@autistici.org>
Co-authored-by: Jonathan Soares <jpub@disroot.org>
Co-authored-by: Karl Tammik <karltammik@protonmail.com>
Co-authored-by: Lars <weblate.org@lehtrab.de>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Napstaguy04 <brokenscreen3@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Ricardo <contatorms7@tutamail.com>
Co-authored-by: Simon N <Observeramera@pm.me>
Co-authored-by: TiA4f8R <avdivers84@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: jazzyjabroni <lordcarmack@tuta.io>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: nzgha <nzgha.hw@runbox.com>
Co-authored-by: qqqq1 <qqqq1@hi2.in>
Co-authored-by: sal0max <msal.coding@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Егор Ермаков <eg.ermakov2016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar_LY/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fa/
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/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2022-04-28 11:52:53 +02:00
Stypox
31e762d921 Update NewPipeExtractor to 0.22.1 2022-04-28 11:09:04 +02:00
LingYinTianMeng
bb495f567c Merge branch 'TeamNewPipe:dev' into dev 2022-04-27 21:03:09 +08:00
litetex
aa1db617d5 Merge pull request #8269 from Nickoriginal/update_user_agent
Update User-Agent in Downloader
2022-04-23 20:44:05 +02:00
opusforlife2
9b3e43ffc1 Merge pull request #8279 from TiA4f8R/set-maximum-allowed-opacity-for-close-overlay-android-12-and-higher
Adapt opacity of popup close button to allow touches in other apps on Android >=12
2022-04-23 17:39:15 +00:00
TiA4f8R
d5a0f8f23c Set opacity of the popup close button to 0.8 on Android 12 and higher
Setting this opacity should allow touches outside NewPipe when using the popup player.

See https://developer.android.com/reference/android/view/WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE for more details.
2022-04-22 15:09:20 +02:00
Nickoriginal
ec5cfe0019 Update USER_AGENT in DownloaderImpl 2022-04-20 16:15:27 +03:00
LingYinTianMeng
fd5626e9e2 Merge branch 'TeamNewPipe:dev' into dev 2022-04-19 16:36:42 +08:00
litetex
53bf3420e7 Merge pull request #8244 from seanzzy/issue-8058
Fix crash when open NewPipe from notification bar
2022-04-18 16:06:48 +02:00
Yingwei Zheng
127a27315e Fix keyboard showing after the search box acquiring focus (#8227)
* Fix keyboard showing after the search box acquiring focus
* Fix the underlying problem as described in the issue #7647
2022-04-18 16:05:42 +02:00
litetex
671441bdf8 Merge pull request #8206 from TacoTheDank/bumpACRA
Update ACRA library
2022-04-18 16:04:37 +02:00
litetex
5c6e2ed071 Merge pull request #8233 from Stypox/fix-notification-settings-2
Fix new streams notifications preference screen
2022-04-18 15:25:45 +02:00
litetex
254b276a54 Merge pull request #8236 from Stypox/fix-proguard-settings
Fix proguard for notification preference fragment
2022-04-18 15:25:38 +02:00
litetex
31df4e42d7 Merge pull request #8249 from karyogamy/caption-fix-2
Fix disabled caption to no longer automatically re-enable on new player instance
2022-04-18 15:25:29 +02:00
litetex
2b8eb7ed66 Also run CI when target is release branch 2022-04-18 14:28:56 +02:00
karyogamy
29fc0eff38 fixed: added comments for DefaultTrackSelector auto-select fix. 2022-04-17 18:34:31 -04:00
karyogamy
4917da2d2e fixed: disabled caption to no longer automatically re-enable on new player instance. 2022-04-17 13:26:39 -04:00
LingYinTianMeng
8ea98b64aa fix issue #7563 2022-04-17 22:23:03 +08:00
ZiyanZHANG
4904b48f5c Update PlayQueueActivity.java 2022-04-17 18:15:13 +08:00
Stypox
05d5ef602c Fix proguard rules to keep Notifications settings fragment 2022-04-16 21:26:25 +02:00
litetex
a311519314 Fix merge conflicts 2022-04-16 21:24:01 +02:00
litetex
1dc146322c Merged `DrawableResolver into ThemeHelper` 2022-04-16 21:21:57 +02:00
litetex
0f551baf37 Refactored code 2022-04-16 21:21:56 +02:00
litetex
b9190eddfe Update DrawableResolver.kt
Nicer import 😉
2022-04-16 21:21:55 +02:00
litetex
44dada9e60 Use better Kotlin syntax
From the PR review
2022-04-16 21:21:54 +02:00
litetex
1b8c517e3e Removed unused strings 2022-04-16 21:21:53 +02:00
litetex
20602889be Added some doc and abstracted more methods 2022-04-16 21:21:52 +02:00
litetex
4b06536582 Reworked switching to semitones
Using an expandable Tab-like component instead of a combobox
2022-04-16 21:21:51 +02:00
litetex
621b38c98b Code improvements regarding stepSize 2022-04-16 21:21:50 +02:00
litetex
321cf8bf7d Fine tuned dialog 2022-04-16 21:21:49 +02:00
litetex
762cdc812c Reworked/Implemented PlaybackParameterDialog functionallity
* Add support for semitones
* Fixed some minor bugs
* Improved some methods
2022-04-16 21:21:48 +02:00
litetex
dae5aa38a8 Fine tuned dialog (no scrollers in fullscreen on 5in phone) 2022-04-16 21:21:48 +02:00
litetex
7d42e50f5b Shrunk dialog a bit 2022-04-16 21:21:47 +02:00
litetex
a4c083e7f9 Rework dialog
* De-Duplicated some fields
* Use a container for the pitch controls
* Name pitch related elements correctly
2022-04-16 21:21:46 +02:00
litetex
e4f202834c Remove invalid parameters 2022-04-16 21:21:46 +02:00
litetex
6e0c380409 Remove redundant attributes 2022-04-16 21:21:46 +02:00
litetex
4cdf6eda2c Use viewbinding 2022-04-16 21:21:45 +02:00
litetex
652d50173e Major refactoring of PlaybackParameterDialog
* Removed/Renamed methods
* Use ``IcePick``
* Better structuring
* Keep skipSilence when rotating the device (PlayQueueActivity only)
2022-04-16 21:21:45 +02:00
Stypox
fa58a81852 Fix New streams settings snackbar not being hidden on exiting 2022-04-16 19:01:30 +02:00
Stypox
f2fc2cc24a Check whether to enable New streams settings in onCreate to prevent flickering 2022-04-16 19:00:51 +02:00
Stypox
0a2fc08706 Release v0.23.0 (986) 2022-04-16 18:28:23 +02:00
Stypox
6e81f2430b Merge remote-tracking branch 'weblate/dev' into dev 2022-04-16 18:02:49 +02:00
Stypox
509036f162 Add changelog for 0.23.0 2022-04-16 17:50:10 +02:00
Stypox
4040cb36bb Merge branch 'master' into dev 2022-04-15 20:23:14 +02:00
Stypox
53a659c0cf Merge pull request #8222 from TeamNewPipe/release/0.22.2
Update NewPipeExtractor again to fix throttling on age restricted videos
2022-04-15 20:08:44 +02:00
Stypox
0cf412efb3 Merge branch 'master' into dev 2022-04-15 18:46:16 +02:00
litetex
2c7977d3e8 Merge pull request #8199 from TacoTheDank/checklistIcon
Replace checklist drawable
2022-04-15 17:57:07 +02:00
litetex
85b5cb55de Merge pull request #8200 from TacoTheDank/drawerLayoutSimpler
Use simpler DrawerLayout method
2022-04-15 17:52:47 +02:00
litetex
6ed69d8ed8 Merge pull request #8204 from TacoTheDank/bumpPlugins
Update AGP, Gradle, and Kotlin
2022-04-15 17:51:22 +02:00
litetex
0a92ac97d4 Merge pull request #8198 from TacoTheDank/bumpWorkflow
Update action dependencies in workflows
2022-04-15 17:37:30 +02:00
Stypox
955748f00f Merge pull request #8215 from TeamNewPipe/release/0.22.2
Hotfix release v0.22.2
2022-04-15 14:58:56 +02:00
Stypox
bc53bc7cfd Update NewPipeExtractor again to fix throttling on age restricted videos 2022-04-15 13:36:11 +02:00
Stypox
5ce5f84f4a Add hotfix changelogs for NewPipe 0.22.2 (985)
The changelogs were just copied from 982.txt for all languages that had it, since the subject of the hotfix release was the same, i.e. youtube breaking
2022-04-15 11:05:19 +02:00
Stypox
ffa7efedd6 Hotfix release 0.22.2 (985) 2022-04-15 10:57:55 +02:00
Stypox
5e6752db14 Update NewPipeExtractor for hotfix release 2022-04-15 10:54:31 +02:00
TacoTheDank
248ca5ee12 Update ACRA library 2022-04-14 22:08:42 -04:00
TacoTheDank
7fb2973431 Update AGP, Gradle, and Kotlin 2022-04-14 21:10:29 -04:00
TacoTheDank
3a419126f3 Use simpler DrawerLayout method 2022-04-14 16:50:28 -04:00
TacoTheDank
ef5c71374b Replace checklist menu drawable 2022-04-14 15:52:14 -04:00
TacoTheDank
5ccf2d7bcc Reformat heart and seek-triangle drawables 2022-04-14 15:52:01 -04:00
TacoTheDank
c85936bb11 Update action dependencies in workflows 2022-04-14 15:43:56 -04:00
Robin
3fb5073feb Merge pull request #8150 from karyogamy/caption-fix
Fix caption auto-selection not reflected in player GUI
2022-04-14 10:10:53 +02:00
Robin
75df1fa3ac Merge pull request #8191 from litetex/update-extractor
Updated extractor to latest version of the dev-branch
2022-04-14 10:03:30 +02:00
Subham Jena
931906c9f3 Translated using Weblate (Odia)
Currently translated at 3.8% (24 of 617 strings)
2022-04-13 02:08:06 +02:00
WB
b6368b1296 Translated using Weblate (Galician)
Currently translated at 98.5% (608 of 617 strings)
2022-04-13 02:08:05 +02:00
Luciano dos Santos Gonzalez
cb1fa8b5ae Translated using Weblate (Portuguese)
Currently translated at 100.0% (617 of 617 strings)
2022-04-13 02:08:04 +02:00
litetex
601bc96734 Updated to latest version of the Extractor-dev-branch 2022-04-12 22:09:45 +02:00
Tobi
8441aff066 Merge pull request #8175 from Trust04zh/update-doc
Update CONTRIBUTING.md with current checkstyle.xml path
2022-04-12 17:33:40 +02:00
karyogamy
9818f179c4 fixed: auto-generated captions to have lower selection priority as manual captions. 2022-04-11 22:06:43 -04:00
Trust_04zh
91e1d35a10 update to current checkstyle.xml path 2022-04-11 19:59:14 +08:00
litetex
74c9a3dc50 Merge pull request #8146 from GGAutomaton/fix-7825
Use newInstance in PlaylistDialog
2022-04-10 15:04:12 +02:00
karyogamy
55fc3fc177 added: caption language stem utility to support language variant conversion between videos. 2022-04-08 18:21:30 -04:00
karyogamy
724eac9168 fixed: player caption auto-selection not reflected in gui.
fixed: player caption selection skipping on multiple language variants.
2022-04-07 20:02:56 -04:00
Robin
a528cee5f4 Merge pull request #8127 from litetex/fix-SparseItemUtil
Fix `SparseItemUtil` so we don't enqueue twice
2022-04-07 17:21:18 +02:00
Robin
e16917f63a Merge pull request #8139 from TiA4f8R/seamless-transition-video-subtitles-fetch-fix
Fix fetch of video streams (when switching between tracks in a play queue) and subtitles when using a seamless transition between background and video players
2022-04-07 17:13:09 +02:00
Napstaguy04
984d19a9a5 Translated using Weblate (Tagalog)
Currently translated at 9.5% (59 of 617 strings)
2022-04-07 12:11:17 +02:00
Napstaguy04
229481c89c Translated using Weblate (Filipino)
Currently translated at 19.1% (118 of 617 strings)
2022-04-07 12:11:15 +02:00
Muhammad Fahim Zunayed
f9af698521 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 51.3% (317 of 617 strings)
2022-04-07 12:11:15 +02:00
Éfrit
db96d5246f Translated using Weblate (French)
Currently translated at 100.0% (617 of 617 strings)
2022-04-07 12:11:15 +02:00
GGAutomaton
2e771cd65a Fix crash when rotating device on unsupported channels 2022-04-04 23:58:39 +08:00
GGAutomaton
638f227b51 Use newInstance in PlaylistDialog 2022-04-04 13:50:27 +08:00
Napstaguy04
0e73eb568e Added translation using Weblate (Tagalog) 2022-04-04 05:25:55 +02:00
TiA4f8R
3261855b8f Fix fetch of video streams (when switching between tracks in a play queue) and subtitles when using a seamless transition between background and video players
Make the use of the new method setDisabledTrackTypes in DefaultTrackSelector.ParametersBuilder, which disables selection of tracks type for every TrackGroup instead of the current group, which is the current behavior.
This removes the use of the deprecated of setSelectionOverride method.

Note that for progressive media, the content is still fetched, but only for initialization purposes (so requests are pretty small, most of times with a few kilobytes size).
2022-04-03 14:07:56 +02:00
Agnieszka C
3bbabb8416 Translated using Weblate (Polish)
Currently translated at 100.0% (617 of 617 strings)
2022-04-02 21:52:54 +02:00
litetex
629b685f5a Merge pull request #7516 from mauriciocolli/fix-download-dialog-selector
Fix download dialog selector layout
2022-04-02 16:04:21 +02:00
litetex
6b1a6d264b Better naming 2022-04-02 15:44:06 +02:00
litetex
79540a8b9c Fix tests 2022-04-02 15:43:50 +02:00
Mauricio Colli
99d62381b9 Fix download dialog selector layout and add some tests 2022-04-02 15:25:08 +02:00
litetex
860d28e16c Merge pull request #8020 from karyogamy/exo-update-v17
ExoPlayer 2.17.1 update and MediaSource management rework
2022-04-02 14:53:58 +02:00
Stypox
ac00c8f6ae Merge pull request #8115 from litetex/update-newpipe-extractor
Update NewpipeExtractor
2022-04-01 10:50:39 +02:00
litetex
b5fa93eda0 Fix SparseItemUtil loading
* Added a missing `return` statement
* `fetchUploaderUrlIfSparse` now has a similar layout to `fetchItemInfoIfSparse`
2022-03-30 21:11:15 +02:00
Bob
a8573f268b Translated using Weblate (Swahili)
Currently translated at 5.0% (31 of 617 strings)
2022-03-29 00:22:19 +02:00
ssantos
fa141e394b Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (617 of 617 strings)
2022-03-29 00:22:18 +02:00
TXRdev Archive
e72bb87cc1 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (617 of 617 strings)
2022-03-29 00:22:18 +02:00
zica
32b294f1f3 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (617 of 617 strings)
2022-03-29 00:22:17 +02:00
Linerly
70d4369d81 Translated using Weblate (Indonesian)
Currently translated at 100.0% (617 of 617 strings)
2022-03-29 00:22:16 +02:00
pjammo
75f601c154 Translated using Weblate (Italian)
Currently translated at 100.0% (617 of 617 strings)
2022-03-29 00:22:16 +02:00
translator
335cc234c8 Translated using Weblate (French)
Currently translated at 99.6% (615 of 617 strings)
2022-03-29 00:22:15 +02:00
cedspam
5343781e14 Translated using Weblate (French)
Currently translated at 99.6% (615 of 617 strings)
2022-03-29 00:22:15 +02:00
karyogamy
a00bc95acc updated: source loading error for FailedMediaSource to wait for 3 seconds before allowing retry.
updated: minor style fixes.
2022-03-27 13:24:37 -04:00
karyogamy
d289dc8a53 updated: onPlayerError to not catch unspecified source errors so notifications are created.
updated: Throwable usage to Exceptions.
updated: minor styles and documentations.
2022-03-26 20:17:52 -04:00
litetex
93deaa5687 Fixed test compilation 2022-03-26 21:44:16 +01:00
litetex
102c05e927 FIx breaking changes 2022-03-26 21:21:07 +01:00
litetex
cf598dc3cb Update extractor to latest dev-Version 2022-03-26 21:20:41 +01:00
litetex
1ecb0ca081 Merge pull request #7977 from Stypox/error-notification-kitkat
Fix error notification on KitKat
2022-03-25 20:00:48 +01:00
litetex
5459a55406 Merge pull request #8081 from Stypox/remove-pin-notifications-icons
Remove pin and notifications night icons
2022-03-20 17:57:43 +01:00
Stypox
fa1c11f5f9 Remove pin and notifications night icons
They were added by accident in PRs not properly rebased on top of #7518, they can be removed safely.
2022-03-20 11:12:45 +01:00
Stypox
2623f0e360 Merge pull request #2335 from nv95/feature/notifications
New streams notifications
2022-03-20 10:48:48 +01:00
karyogamy
b81eb35f3d added: documentations on lifecycles for FailedMediaSource and LoadedMediaSource.
fixed: onPlaybackSynchronize to rewind when not playing, which was incorrectly removed in previous commit.
fixed: sonar and checkstyle issues.
2022-03-19 22:40:32 -04:00
Stypox
66fffce87c Make "Player notification" PreferenceScreen searchable 2022-03-19 22:44:59 +01:00
Stypox
6e8c9f92cb Merge branch 'dev' into pr2335 2022-03-19 22:29:10 +01:00
litetex
fc61aae20a Merge pull request #8077 from litetex/delete-copyright-file
Delete copyright-file
2022-03-19 22:20:26 +01:00
Stypox
3d9d25df52 Remove backoff criteria: it never kicked in
It never kicked in since we are never returning a retry() Result, but always either success() or failure() (see createWork() function). Also, there is already a default (exponential backoff starting from 30 seconds), so no need to override it.
2022-03-19 21:55:00 +01:00
litetex
5f3db017af Delete copyright
was replaced by LICENSE
2022-03-19 21:39:33 +01:00
karyogamy
69646e5b5d added: documentations to MediaItemTags and Player.
fixed: checkStyle failures.
2022-03-19 15:56:45 -04:00
karyogamy
4e459b3383 updated: ExoPlayer to 2.17.1.
added: MediaItemTag for ManagedMediaSources.
added: silent track for FailedMediaSource.
added: keyframe fast forward at initial playback buffer.
added: error notification on silently skipped streams.
2022-03-19 15:56:45 -04:00
litetex
8c5e8bdf78 Merge pull request #8076 from litetex/update-license
Update license to latest version
2022-03-19 17:58:33 +01:00
litetex
6bc750cab7 Update license to latest version of https://www.gnu.org/licenses/gpl-3.0.txt 2022-03-19 17:39:06 +01:00
litetex
70d9a77e9b Merge pull request #8073 from Stypox/bump-checkstyle
Update checkstyle to 10.0 and fix various related issues
2022-03-19 14:37:52 +01:00
litetex
6d2b5d976d Merge pull request #8068 from TacoTheDank/lintCleaning
Some lint cleaning
2022-03-19 14:37:34 +01:00
litetex
57231382a6 Merge pull request #8066 from TacoTheDank/simpleSeekbarChange
Create stub implementation for OnSeekBarChangeListener
2022-03-19 14:37:10 +01:00
litetex
1f6fc0630d Merge pull request #8065 from TacoTheDank/aboutCleanup
Clean up the about package a bit
2022-03-19 14:36:53 +01:00
David
e5ee405971 Translated using Weblate (Chinese (Traditional))
Currently translated at 60.0% (39 of 65 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
2022-03-19 10:59:16 +01:00
NTFSynergy
5522a7a2e5 Translated using Weblate (Slovak)
Currently translated at 6.1% (4 of 65 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
2022-03-19 10:59:14 +01:00
Filip Marek
cbc718437b Translated using Weblate (Czech)
Currently translated at 4.6% (3 of 65 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
2022-03-19 10:59:14 +01:00
Igor Sorocean
8ca701b882 Translated using Weblate (Romanian)
Currently translated at 9.2% (6 of 65 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ro/
2022-03-19 10:59:13 +01:00
AHOHNMYC
f5e253456c Translated using Weblate (Russian)
Currently translated at 16.9% (11 of 65 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
2022-03-19 10:59:13 +01:00
Marco Santos
8cc29241e2 Translated using Weblate (Filipino)
Currently translated at 18.8% (116 of 617 strings)
2022-03-19 10:59:11 +01:00
subba raidu
4ea0d05c17 Translated using Weblate (Telugu)
Currently translated at 65.1% (402 of 617 strings)
2022-03-19 10:59:09 +01:00
Yaron Shahrabani
7898c33819 Translated using Weblate (Hebrew)
Currently translated at 100.0% (617 of 617 strings)
2022-03-19 10:59:09 +01:00
Danial Behzadi
d0ba87f7ee Translated using Weblate (Persian)
Currently translated at 100.0% (617 of 617 strings)
2022-03-19 10:59:08 +01:00
Agnieszka C
5ee961d3eb Translated using Weblate (Polish)
Currently translated at 100.0% (617 of 617 strings)
2022-03-19 10:59:08 +01:00
Mohammed Anas
ac10e15c15 Translated using Weblate (Arabic)
Currently translated at 100.0% (617 of 617 strings)
2022-03-19 10:59:08 +01:00
zenobit
71159cf0c8 Translated using Weblate (Czech)
Currently translated at 100.0% (617 of 617 strings)
2022-03-19 10:59:07 +01:00
Filip Marek
b2f22ac584 Translated using Weblate (Czech)
Currently translated at 100.0% (617 of 617 strings)
2022-03-19 10:59:07 +01:00
NTFSynergy
8c662c9d7b Translated using Weblate (Slovak)
Currently translated at 100.0% (617 of 617 strings)
2022-03-19 10:59:07 +01:00
bw4518
8cd3083e52 Translated using Weblate (Slovak)
Currently translated at 100.0% (617 of 617 strings)
2022-03-19 10:59:07 +01:00
Ray
06227d4514 Translated using Weblate (Chinese (Traditional, Hong Kong))
Currently translated at 89.3% (551 of 617 strings)
2022-03-19 10:59:06 +01:00
Dar9586
f5ce228059 Translated using Weblate (Italian)
Currently translated at 99.8% (616 of 617 strings)
2022-03-19 10:59:05 +01:00
Stypox
53f8415e9b Use @SuppressWarnings for checkstyle suppressions & warnings
It's better to use @SuppressWarnings instead of the suppressions file, so that the warning suppression is at the place where it acts.
2022-03-18 23:57:11 +01:00
Stypox
710964b47d Update checkstyle to 10.0 and fix various related issues
- Put checkstyle files into checkstyle/ subfolder so that the gradle task does not implicitly depend on the whole project, fixing many warnings during build and possibly increasing build performance.
- Remove unused SuppressionXpathFilter from config file.
- Remove outdated suppressions from suppressions file.
2022-03-18 19:58:59 +01:00
TacoTheDank
4dafe424cf De-duplicate showLicense methods 2022-03-18 13:48:07 -04:00
TacoTheDank
bc4a0a575c Clean up the about package a bit 2022-03-18 13:18:23 -04:00
TacoTheDank
cf213affa2 Annotate some NonNulls, some lint cleaning 2022-03-18 13:15:44 -04:00
litetex
e29aaaf162 Merge pull request #8067 from TacoTheDank/removeUnusedCode
Delete some unused code
2022-03-18 16:22:18 +01:00
TacoTheDank
979a320347 Delete some unused code 2022-03-17 23:26:34 -04:00
TacoTheDank
20bddd8e47 Use Animator.addListener() KTX extension 2022-03-17 22:01:51 -04:00
TacoTheDank
86f335b01f Create stub implementation for OnSeekBarChangeListener 2022-03-17 21:49:04 -04:00
Stypox
102204e293 Merge pull request #8011 from XiangRongLin/extract_view_listeners
Extract view click listeners from Player
2022-03-16 22:28:01 +01:00
litetex
67651354d5 Fixed conflicts 2022-03-16 15:58:46 +01:00
litetex
cefb52471f Better naming 2022-03-16 15:52:30 +01:00
litetex
ee5e0e13b7 Made `onClick` less (cognitive) complex 2022-03-16 15:52:30 +01:00
litetex
30a8f25d52 Refactored code 2022-03-16 15:47:04 +01:00
XiangRongLin
d348c2099e stupid checkstyle 2022-03-16 15:47:04 +01:00
XiangRongLin
6a400dda7b delete unused methods 2022-03-16 15:47:03 +01:00
XiangRongLin
080c4ba680 Extract 2 view click listeners from Player 2022-03-16 15:47:03 +01:00
litetex
37aca3f1c7 Merge pull request #7981 from Stypox/sparse-items-deduplic
Deduplicate code for fetching stream info when sparse
2022-03-16 15:18:10 +01:00
litetex
0158f1363b Merge pull request #7518 from mauriciocolli/remove-icon-duplicates
Remove icon duplicates and fix some theming issues
2022-03-15 21:51:04 +01:00
litetex
f47f2d13fa Merge pull request #5878 from SpinHit/spinhit/addingDeleteConfirmation
Add a confirmation button when deleting all files in downloader
2022-03-15 21:49:46 +01:00
litetex
6fe6f4b3e0 Merge pull request #7978 from TacoTheDank/bumpSomeLibraries
Update some AndroidX libraries
2022-03-15 21:48:49 +01:00
litetex
00e4631b3b Merge pull request #7963 from Stypox/android-tv-player
Improve player UI and navigability for Android TV
2022-03-15 21:41:48 +01:00
litetex
2e7503ff78 Merge branch 'dev' into bumpSomeLibraries 2022-03-15 21:34:41 +01:00
ktprograms
02fa5aa0fa Implement appending queue to playlist in main player (#8008)
This also allows saving a remote playlist locally.

- Add an "Add to playlist" button to the queue menu in the Player.
- Move the appendAllToPlaylist functionality from PlayQueueActivity to
Player.

Fixes: #8004
2022-03-15 18:32:39 +01:00
Stypox
9b4a67276a Fix comments and rearrange code 2022-03-15 15:20:25 +01:00
Stypox
b607a09125 Merge pull request #7975 from TacoTheDank/updateCheckerRewrite
Migrate app update checker to AndroidX Work
2022-03-15 14:20:40 +01:00
Stypox
af89f05011 Merge pull request #7341 from ktprograms/external-play-hide-controls
Fix player controls not hiding if resumed from media button
2022-03-15 13:43:35 +01:00
Hosted Weblate
fed5161fc6 Translated using Weblate (Polish)
Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 23.0% (15 of 65 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Bosnian)

Currently translated at 4.6% (3 of 65 strings)

Translated using Weblate (Bosnian)

Currently translated at 19.7% (122 of 617 strings)

Translated using Weblate (Croatian)

Currently translated at 98.2% (606 of 617 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (614 of 617 strings)

Translated using Weblate (Serbian)

Currently translated at 94.9% (586 of 617 strings)

Translated using Weblate (Sardinian)

Currently translated at 99.6% (615 of 617 strings)

Translated using Weblate (Italian)

Currently translated at 99.5% (614 of 617 strings)

Translated using Weblate (German)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Estonian)

Currently translated at 99.6% (615 of 617 strings)

Translated using Weblate (Tamil)

Currently translated at 55.2% (341 of 617 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.8% (616 of 617 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 94.4% (583 of 617 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (65 of 65 strings)

Translated using Weblate (Japanese)

Currently translated at 10.7% (7 of 65 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (617 of 617 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (614 of 617 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (615 of 615 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (615 of 615 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (615 of 615 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (613 of 615 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (613 of 613 strings)

Added translation using Weblate (Bosnian)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: GnuPGを使うべきだ <dieeeazpnnqbpddh@cock.email>
Co-authored-by: GobinathAL <gobinathal8@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Issa1553 <fairfull.playing@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Jonatan Nyberg <jonatan@autistici.org>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Ricardo <contatorms7@tutamail.com>
Co-authored-by: S3aBreeze <paperwork@evilcorp.ltd>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Terry Louwers <t.louwers@gmail.com>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: Vitor Henrique <vitorhcl00@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: luciana <ludiazsp_182@live.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: sasukeiscool <jaflagasd@gmail.com>
Co-authored-by: Óscar Fernández Díaz <oscfdezdz@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ja/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2022-03-14 09:26:39 +01:00
TacoTheDank
b8b97fa6d4 Convert NewVersionWorker to Kotlin 2022-03-03 13:34:35 -05:00
TacoTheDank
71f141f3f8 Migrate CheckForNewAppVersion to Worker (and rename it) 2022-03-03 13:26:57 -05:00
TacoTheDank
81fef1be19 Migrate CheckForNewAppVersion to JobIntentService 2022-03-03 13:24:12 -05:00
TacoTheDank
0f175de599 Kotlin-ize ReleaseVersionUtil, merge with NewVersionManager 2022-03-03 13:21:50 -05:00
TacoTheDank
1602befc51 Move utility methods out of CheckForNewAppVersion 2022-03-03 13:19:06 -05:00
Stypox
162a838afc Deduplicate code for fetching stream info when sparse
Fixes #7941
2022-03-03 16:54:40 +01:00
Stypox
05a5e4372a Merge pull request #7976 from Stypox/remove-yes-string
Replace `R.string.yes` with `R.string.ok`
2022-03-03 10:17:54 +01:00
Stypox
e588abd4e7 Restore handling SPACE as play-pause only in fullscreen
When not in fullscreen SPACE should be not handled by the player, and hence result in a scroll down
2022-03-03 10:14:58 +01:00
TacoTheDank
f85b206bdf Update some AndroidX libraries 2022-03-02 11:01:01 -05:00
Stypox
5b3bbfce10 Fix playlist item not properly themed 2022-03-02 15:09:42 +01:00
Stypox
2a9733fbaf Fix error notification on KitKat
It was crashing due to a drawable icon being used; also use NotificationManagerCompat
2022-03-02 14:14:40 +01:00
Mauricio Colli
96185faca6 Remove manual menu creation from some fragments
Doing this programmatically is just a no-go when themes are being set
in some other places (the toolbar is using a custom theme, in this
case), so, instead of hunting down the proper theme, just let the
system do its work.
2022-03-02 12:37:44 +01:00
Mauricio Colli
af20b2ce0d Fix duplication of some icons used by the player 2022-03-02 12:33:01 +01:00
Mauricio Colli
919b92a0b5 Add missing tint on drawer image view 2022-03-02 12:31:41 +01:00
Mauricio Colli
3b0153ca7a Fix duplication of icon vectors 2022-03-02 12:31:41 +01:00
Stypox
5f16e4ef87 Replace R.string.yes with R.string.ok
Android doesn't use yes/no but ok/cancel usually, so this should be done here, too
2022-03-02 12:21:25 +01:00
Spinhit
483dc06ecb Add confirmation button before deleting all files.
Co-authored-by: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
2022-03-02 11:31:52 +01:00
Stypox
b8e389c6e8 Merge pull request #7919 from karyogamy/progress-load-interval
Mitigating long buffering on initial video playback
2022-03-01 22:48:33 +01:00
litetex
cde4ee91f8 Minor rework
* Moved settings to a better section
* Made string a bit shorter
2022-03-01 20:14:53 +01:00
karyogamy
ab45efceab - added: variable load check interval for progressive stream.
- added: preferences to allow user setting of above.
2022-03-01 20:14:53 +01:00
Stypox
cbdcf5905f Merge pull request #7728 from ktprograms/remember-playback-adjustment-step-size
Remember adjustment step size for playback controls (speed and pitch)
2022-03-01 10:54:47 +01:00
kt programs
62c0e6605c Remember adjustment step size for playback controls (speed and pitch)
- Add adjustment_step_key to settings_keys.xml to be used when
saving/loading the step size.
- Remove the global stepSize variable and the code that saves it to
outState/loads it from savedInstanceState because it's now saved to
Shared Preferences.
- Move initially setting step size to setupStepSizeSelector to be
consistent with the other view setup methods, using the value loaded
from Shared Preferences.
- Save the step size to Shared Preferences inside setStepSize.

Fixes: #7031
2022-03-01 16:27:20 +08:00
litetex
f1c6988552 Merge pull request #7952 from TacoTheDank/bumpKotlin
Update Kotlin to 1.6.10
2022-02-28 19:42:38 +01:00
litetex
e1197f7253 Merge pull request #7954 from TacoTheDank/bumpInconsequential
Update ConstraintLayout, Room libraries
2022-02-28 19:42:19 +01:00
Stypox
146062d921 Fix player pop-ups not giving feedback on touch/focus 2022-02-27 18:49:16 +01:00
Stypox
96c4201929 Fix controls shown below queue/segments list when using DPAD
Also invert if
2022-02-27 18:49:16 +01:00
Stypox
a0bbcd2fee Fix player queue/segments list buttons not focusable with DPAD
Now the in-player play queue and the segments list are closeable
2022-02-27 18:49:16 +01:00
Stypox
627b4c8b14 Merge pull request #7894 from Stypox/delete-large-land-player
Remove large-land player layout: not actually used
2022-02-27 18:46:51 +01:00
litetex
fd6c352881 Merge pull request #7947 from TacoTheDank/bumpPluginsNGradle
Update AGP and Gradle
2022-02-27 17:48:22 +01:00
Stypox
3f7ba2e3d1 Merge pull request #7565 from haklc/dev
Change pitch by semitones
2022-02-27 09:58:38 +01:00
TacoTheDank
7c180727b9 Update ConstraintLayout, Room libraries 2022-02-26 21:13:52 -05:00
TacoTheDank
19d4e2224b Update Checkstyle to 9.3 2022-02-26 16:10:23 -05:00
TacoTheDank
678edb1846 Update ktlint to 0.44.0 2022-02-26 16:08:10 -05:00
TacoTheDank
ae2ba5771f Update Kotlin to 1.6.10 2022-02-26 16:07:33 -05:00
Stypox
cd9dd2e679 Merge pull request #7951 from TeamNewPipe/master
Merge master back into dev after release 0.22.1
2022-02-26 22:02:40 +01:00
Stypox
47f9ed08e9 Merge pull request #7934 from TeamNewPipe/release/0.22.1
Release 0.22.1
2022-02-26 22:01:30 +01:00
Hosted Weblate
ee3c06394d Translated using Weblate (Swedish)
Currently translated at 49.2% (32 of 65 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 58.4% (38 of 65 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 23.0% (15 of 65 strings)

Translated using Weblate (French)

Currently translated at 66.1% (43 of 65 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 7.6% (5 of 65 strings)

Translated using Weblate (Italian)

Currently translated at 38.4% (25 of 65 strings)

Translated using Weblate (Polish)

Currently translated at 55.3% (36 of 65 strings)

Translated using Weblate (Hebrew)

Currently translated at 53.8% (35 of 65 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (65 of 65 strings)

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

Currently translated at 7.6% (5 of 65 strings)

Translated using Weblate (German)

Currently translated at 66.1% (43 of 65 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (English)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (English)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (English)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (French)

Currently translated at 65.6% (42 of 64 strings)

Translated using Weblate (Dutch)

Currently translated at 71.8% (46 of 64 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (613 of 613 strings)

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

Currently translated at 80.4% (493 of 613 strings)

Translated using Weblate (French)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (613 of 613 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Benedikt Freisen <b.freisen@gmx.net>
Co-authored-by: Corc <nizamismidov4@gmail.com>
Co-authored-by: Guillem <guillemglez@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Issa1553 <fairfull.playing@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Stypox <stypox@pm.me>
Co-authored-by: TiA4f8R <avdivers84@gmail.com>
Co-authored-by: Vitor Henrique <vitorhcl00@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: mm4c <oldblue@vivaldi.net>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Éfrit <efrit@posteo.net>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/az/
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/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl_BE/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2022-02-26 18:52:17 +01:00
Hosted Weblate
ffba1d5037 Translated using Weblate (Swedish)
Currently translated at 49.2% (32 of 65 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 58.4% (38 of 65 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 23.0% (15 of 65 strings)

Translated using Weblate (French)

Currently translated at 66.1% (43 of 65 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 7.6% (5 of 65 strings)

Translated using Weblate (Italian)

Currently translated at 38.4% (25 of 65 strings)

Translated using Weblate (Polish)

Currently translated at 55.3% (36 of 65 strings)

Translated using Weblate (Hebrew)

Currently translated at 53.8% (35 of 65 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (65 of 65 strings)

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

Currently translated at 7.6% (5 of 65 strings)

Translated using Weblate (German)

Currently translated at 66.1% (43 of 65 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (English)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (English)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (English)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (French)

Currently translated at 65.6% (42 of 64 strings)

Translated using Weblate (Dutch)

Currently translated at 71.8% (46 of 64 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (613 of 613 strings)

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

Currently translated at 80.4% (493 of 613 strings)

Translated using Weblate (French)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (613 of 613 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Benedikt Freisen <b.freisen@gmx.net>
Co-authored-by: Corc <nizamismidov4@gmail.com>
Co-authored-by: Guillem <guillemglez@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Issa1553 <fairfull.playing@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Stypox <stypox@pm.me>
Co-authored-by: TiA4f8R <avdivers84@gmail.com>
Co-authored-by: Vitor Henrique <vitorhcl00@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: mm4c <oldblue@vivaldi.net>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Éfrit <efrit@posteo.net>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/az/
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/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl_BE/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2022-02-26 18:46:02 +01:00
litetex
ccc3d38c45 Merge pull request #7910 from avently/equalscheck
Better equals check
2022-02-26 16:20:27 +01:00
litetex
37517c7dd1 Merge pull request #7570 from TeamNewPipe/improvement/infoItemDialogBuilder
Refactor generating InfoItemDialog's
2022-02-26 16:18:39 +01:00
litetex
a4dee77728 Merge pull request #7782 from Atemu/apple-silicon
Fix build on Apple Silicon macs
2022-02-26 16:17:02 +01:00
litetex
a95318a4f9 Merge pull request #7349 from TiA4f8R/seamless-transition-players
Add seamless transition between background and video players when putting the app in background (for video-only streams and audio-only streams only)
2022-02-26 16:16:18 +01:00
litetex
46fad32837 Merge pull request #7905 from Stypox/fix-room-unused-columns
Fix Room warning about unused columns during build
2022-02-26 16:15:01 +01:00
litetex
5be40f62f3 Merge pull request #7904 from Stypox/fix-raw-use-of-parameterized-class
Solve Java warning "Raw use of parameterized class"
2022-02-26 16:14:23 +01:00
litetex
fb75519ff8 Merge pull request #7925 from TacoTheDank/removeCircleImageView
Replace CircleImageView with ShapeableImageView
2022-02-26 16:13:13 +01:00
Stypox
5fea12d8eb Small code improvements
Removed some non-translatable strings and just hardcoded them in the code, like it's being done for other string separators. This also deduplicates some code by using Localization.
Used some Kotlin feature to reduce code.
2022-02-26 10:40:24 +01:00
TacoTheDank
8291098b6d Update AGP and Gradle 2022-02-25 19:36:06 -05:00
TacoTheDank
1a000fecd5 Replace CircleImageView with ShapeableImageView 2022-02-23 15:11:25 -05:00
Stypox
a0dc66abe7 Update android work library version to 2.7.1 2022-02-23 18:16:07 +01:00
Stypox
3d47d73ba9 Add changelog for NewPipe 0.22.1 (984) 2022-02-23 17:13:58 +01:00
Stypox
443ebc46d6 Release 0.22.1 (984) 2022-02-23 15:16:37 +01:00
Stypox
99379ede8a Remove useless title&channel text view focusability 2022-02-23 10:13:03 +01:00
Stypox
4871095a3e Automatically rearrange code in player.xml 2022-02-23 09:16:25 +01:00
Stypox
21dc988e45 Restore focus handling for TVs in player.xml 2022-02-23 09:15:11 +01:00
Avently
01e0dd50ad Added serviceId check while comparing PlayQueues 2022-02-23 00:53:39 +03:00
TobiGr
d3bc184971 Clarify that only StramInfoItems are accepted by the builder 2022-02-21 21:50:30 +01:00
Tobi
c42f29446d Merge pull request #7924 from litetex/revert-7451
Revert "Respect cutouts when playing in MultiWindow"
2022-02-21 21:36:07 +01:00
litetex
1030e09fc1 Merge pull request #7901 from Stypox/player-small-refactor
Small refactor in player class
2022-02-21 20:48:03 +01:00
litetex
96b930cd07 Revert "Respect cutouts when playing in MultiWindow"
This reverts commit c92a90749e.
2022-02-21 20:30:56 +01:00
litetex
de08edb831 Merge pull request #7898 from Stypox/regression-arc
Have fast seek overlay arc go under system ui
2022-02-21 20:19:22 +01:00
TobiGr
ee477b25e5 Move StreamDialogEntry.openChannelFragment to NavigationHelper 2022-02-20 20:26:27 +01:00
TobiGr
277f21d5b2 Move Classes related to InfoItemDIalog into own package 2022-02-20 20:17:04 +01:00
TobiGr
a7d5d9a1d6 Fix rebase 2022-02-20 20:17:04 +01:00
TobiGr
fd0d76e866 Apply feedback
Return this in InfoIrtemDialog.Builder methoods.
Move null checks for InfoIrtemDialog.Builder into constructor.
Fix and add some more docs.
2022-02-20 20:17:04 +01:00
TobiGr
646d8f431c Use identical method names for creating the InfoItemDialog in Fragments 2022-02-20 20:17:04 +01:00
TobiGr
ef0d562702 Use ErrorActivity to notifiy about errors occourred while loading channel details 2022-02-20 20:17:04 +01:00
TobiGr
962fe9c36d Use Context instead of Activity
Improve docs
2022-02-20 20:17:04 +01:00
TobiGr
50e2385e82 Add default entries automatically 2022-02-20 20:17:04 +01:00
TobiGr
1cd3ef5dba Extract entries into beginning and end category 2022-02-20 20:17:04 +01:00
TobiGr
80157fc1be Refactor generating InfoItemDialog's
This commit refactors the way `InfoItemDialog`s are generated. This is necessary because the old way used  the `StreamDialogEntry` enum for most of the dialogs' content generation process. This required static variables and methods to store the entries which are used for the dialog to be build (See e.g.`enabledEntries` and methods like `generateCommands()`). In other words, `StreamDialogEntry` wasn't an enumeration anymore.

To address this issue, a `Builder` is introduced for the `InfoItemDialog`'s genration. The builder also comes with some default entries and and a specific order. Both can be used, but are not enforced. 

A second problem that introduced a structure which was atypical for an enumeration was the usage of non-final attributes within `StreamDialogEntry` instances. These were needed, because the default actions needed to overriden in some cases.

To address this problem, the `StreamDialogEntry` enumeration was renamed to `StreamDialogDefaultEntry` and a new `StreamDialogEntry` class is used instead.
2022-02-20 20:17:04 +01:00
TiA4f8R
c5fc37150d Update JavaDoc of VideoPlaybackResolver.getStreamSourceType() 2022-02-20 19:40:03 +01:00
TiA4f8R
8932adbf88 Apply suggested change and add a note about data consumption for HLS streams in background
ExoPlayer right now fetches HLS video tracks even if you disable them (with setRendererDisabled or setSelectionOverride).
See issue 9282 of ExoPlayer's issue tracker for more information.
2022-02-20 19:40:03 +01:00
TiA4f8R
d27d36b76a Adress requested changes 2022-02-20 19:40:02 +01:00
TiA4f8R
ba804c7d4a Use a enum to understand better what source type is used.
This commit also allows a seamless transition for livestreams.
2022-02-20 19:40:02 +01:00
TiA4f8R
3db37166b4 Apply suggestion 2022-02-20 19:40:02 +01:00
TiA4f8R
bf02a569ee Fix a NullPointerException when the current metadata is null
Reload the play queue manager and set the recovery in this case, like on the current behavior (without this PR).
2022-02-20 19:40:02 +01:00
litetex
015982bed4 Extended Tests for ListHelper#getSortedStreamVideosList
* Fixed expected and actual results. They were reversed...
* Added new method ``getSortedStreamVideosListWithPreferVideoOnlyStreamsTest``
2022-02-20 19:40:01 +01:00
TiA4f8R
a1c5c94753 Add some comments and a JavaDoc 2022-02-20 19:40:01 +01:00
litetex
7a356412d5 Fixed typo 2022-02-20 19:40:01 +01:00
litetex
1ea716a31f Updated checkstyle suppression
Removed fixed problems.
2022-02-20 19:39:58 +01:00
litetex
bb27bf9d34 Resolver: Cleaned up `isVideoStreamVideoOnly`
* Replaced by ``wasLastResolvedVideoAndAudioSeparated``
* Uses an ``Optional`` instead (we can't determine if the video and audio streams are separated when we did not fetch it)
2022-02-20 19:38:41 +01:00
litetex
a489f40b76 Fixed checkstyle problems
Unable to compile!

* Cleaned up ``getMostCompactAudioIndex`` and ``getHighestQualityAudioIndex`` into a new method ``getAudioIndexByHighestRank``
* Removed unreadable code and use Java Streams API
* Tests work as expected
2022-02-20 19:38:40 +01:00
litetex
8ed87e7fbb Improved `ListHelper#getSortedStreamVideosList` 2022-02-20 19:38:40 +01:00
TiA4f8R
cc96ac173c Apply suggestion 2022-02-20 19:38:40 +01:00
TiA4f8R
79f8270c35 Prefer video-only streams to video streams
Prefering video-only streams to video streams for our player will allow us to make seamless transitions on 360 and 720p qualities on YouTube.
External players and the downloader are not affected by this change.
2022-02-20 19:38:40 +01:00
TiA4f8R
336f9f3813 Add seamless transition between background player and video players (for video-only and audio-only streams only)
This is only available when playing video-only streams (and when there is no audio stream and only video streams with audio) and audio-only streams.
For more details about which conditions are required to get this transition, look at the changes in the useVideoSource(boolean) method of the Player class.
2022-02-20 19:38:39 +01:00
Avently
835c5e9d43 Better equals check
It ensures that queues are not the same. Without this check when you have multiple videos in the backstack and navigating back via Back button you'll get duplicated videos
2022-02-19 22:12:31 +03:00
Stypox
4789cf6c31 Use Java streams in AbstractInfoPlayQueue 2022-02-19 18:01:16 +01:00
Stypox
5f1f52b6ce Remove useless constructor in *PlayQueue
Also removes unused <> parameter in AbstractInfoPlayQueue and deduplicates constructor code in extending classes
2022-02-19 17:49:43 +01:00
Stypox
62abfa96b8 Solve Java warning "Raw use of parameterized class" 2022-02-19 17:30:38 +01:00
Stypox
a8a96b7631 Fix Room warning about unused columns during build
The warning was: "The query returns some columns [...] which are not used by ..."
2022-02-19 17:13:57 +01:00
Stypox
af80d96b9e Merge pull request #7659 from litetex/load-enough-initial-data
Load enough initial items (into BaseListFragment and descendants)
2022-02-19 15:43:13 +01:00
Stypox
3c23fb0b13 Small refactor in player class 2022-02-19 13:30:55 +01:00
TobiGr
bba0ea1255 Merge remote-tracking branch 'origin/dev' into feature/notifications 2022-02-19 12:47:47 +01:00
Tobi
750490cd2f Merge pull request #7900 from Stypox/increment-compile-sdk
Change `compileSdk` from 30 to 31
2022-02-19 12:35:13 +01:00
TobiGr
ff8e44e4f3 Merge branch 'dev' into feature/notifications 2022-02-19 12:34:44 +01:00
Stypox
579b8611be Change compileSdk from 30 to 31
This will allow newer libraries to be used, see #7782 and #2335. This should have no changes on the app since the targetSdk stayed the same.
2022-02-19 12:00:04 +01:00
Stypox
da12b92d75 Fix fast seek arc not going under system ui 2022-02-19 11:29:33 +01:00
TobiGr
a3f99bd781 Merge branch 'master' into dev 2022-02-19 10:56:19 +01:00
Tobi
7ae908a466 Merge pull request #7858 from TeamNewPipe/release/0.22.0
Release 0.22.0
2022-02-19 10:51:52 +01:00
Stypox
00767f4bf3 Fix fast seek overlay arc not correctly centered
This happened in fullscreen player: while play-pause, brightness, volume, ... buttons were correctly centered in the app UI, the fast seek overlay was centered in the system UI, causing a mismatch between the two that looked ugly
2022-02-18 22:34:55 +01:00
TobiGr
54f0b3d8b3 Release NewPipe 0.22.0 (983) 2022-02-18 20:39:51 +01:00
Hosted Weblate
08eb70833d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (64 of 64 strings)

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

Currently translated at 6.2% (4 of 64 strings)

Translated using Weblate (Malay)

Currently translated at 60.0% (368 of 613 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (613 of 613 strings)

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

Currently translated at 63.9% (392 of 613 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Dutch)

Currently translated at 64.0% (41 of 64 strings)

Translated using Weblate (Hebrew)

Currently translated at 53.1% (34 of 64 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (German)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Polish)

Currently translated at 54.6% (35 of 64 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 95.2% (584 of 613 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Slovak)

Currently translated at 98.0% (601 of 613 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.5% (610 of 613 strings)

Translated using Weblate (English)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (63 of 63 strings)

Translated using Weblate (Dutch)

Currently translated at 46.0% (29 of 63 strings)

Translated using Weblate (Hebrew)

Currently translated at 52.3% (33 of 63 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (63 of 63 strings)

Translated using Weblate (Interlingua)

Currently translated at 35.0% (215 of 613 strings)

Translated using Weblate (Danish)

Currently translated at 49.1% (301 of 613 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 94.2% (578 of 613 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Arabic)

Currently translated at 99.6% (611 of 613 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Dutch)

Currently translated at 28.5% (18 of 63 strings)

Translated using Weblate (Dutch)

Currently translated at 28.5% (18 of 63 strings)

Translated using Weblate (Swedish)

Currently translated at 48.3% (30 of 62 strings)

Translated using Weblate (French)

Currently translated at 66.1% (41 of 62 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Estonian)

Currently translated at 99.8% (612 of 613 strings)

Translated using Weblate (Catalan)

Currently translated at 99.1% (608 of 613 strings)

Translated using Weblate (Tamil)

Currently translated at 55.4% (340 of 613 strings)

Translated using Weblate (Finnish)

Currently translated at 99.0% (607 of 613 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Armenian)

Currently translated at 19.0% (117 of 613 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (612 of 613 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (612 of 613 strings)

Translated using Weblate (French)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (English)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Catalan)

Currently translated at 99.3% (609 of 613 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (French)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (German)

Currently translated at 100.0% (613 of 613 strings)

Added translation using Weblate (German (Low))

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (613 of 613 strings)

Translated using Weblate (Malay)

Currently translated at 4.8% (3 of 62 strings)

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

Currently translated at 4.8% (3 of 62 strings)

Translated using Weblate (Galician)

Currently translated at 94.9% (581 of 612 strings)

Translated using Weblate (Malay)

Currently translated at 60.1% (368 of 612 strings)

Translated using Weblate (Malay)

Currently translated at 60.1% (368 of 612 strings)

Translated using Weblate (Tamil)

Currently translated at 49.8% (305 of 612 strings)

Translated using Weblate (Finnish)

Currently translated at 97.2% (595 of 612 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (612 of 612 strings)

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

Currently translated at 57.1% (350 of 612 strings)

Translated using Weblate (French)

Currently translated at 99.6% (610 of 612 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 7.6% (47 of 612 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Tamil)

Currently translated at 44.6% (273 of 612 strings)

Translated using Weblate (Arabic)

Currently translated at 99.8% (611 of 612 strings)

Co-authored-by: A <ville.mourujarvi@hostedweblate.mail.kapsi.fi>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Albert Vila <vilacalvo.albert@gmail.com>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Alfonso Scarpino <alfonso.scarpino@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Davit Mayilyan <davit.mayilyan@protonmail.ch>
Co-authored-by: Denys Nykula <nykula@ukr.net>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: GnuPGを使うべきだ <dieeeazpnnqbpddh@cock.email>
Co-authored-by: GobinathAL <gobinathal8@gmail.com>
Co-authored-by: Guillem <guillemglez@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: John Smith <a@shatin.ml>
Co-authored-by: Karl Tammik <karltammik@protonmail.com>
Co-authored-by: Lim Jia Ming <jiaminglimjm@protonmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Mark <theMurk@protonmail.com>
Co-authored-by: Mohammed Anas <6daf084a-8eaf-40fb-86c7-8500077c3b69@anonaddy.me>
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
Co-authored-by: MohammedSR Vevo <mohammednajmidin@gmail.com>
Co-authored-by: My <quentin.lalle@gmail.com>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Ricardo <contatorms7@tutamail.com>
Co-authored-by: S3aBreeze <paperwork@evilcorp.ltd>
Co-authored-by: SamTada <SamLbttyw@protonmail.com>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: THANOS SIOURDAKIS <siourdakisthanos@gmail.com>
Co-authored-by: Thomas Frarke <goetzknecht23@gmail.com>
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: Ville Rantanen <v.r@iki.fi>
Co-authored-by: WB <web0nst@tuta.io>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: gymka <gymka@archlinux.lt>
Co-authored-by: jojo <welwhazosky@gmail.com>
Co-authored-by: mm4c <oldblue@vivaldi.net>
Co-authored-by: nzgha <nzgha.hw@runbox.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: translator <kvb@tuta.io>
Co-authored-by: xainsworth <202120614015@ogr.balikesir.edu.tr>
Co-authored-by: Ács Zoltán <acszoltan111@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ms/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2022-02-18 20:39:51 +01:00
TobiGr
42aafd3a2d Update NewPipe Extractor to v0.21.14 2022-02-18 20:39:51 +01:00
Stypox
7f846429cf Remove large-land player layout: not actually used 2022-02-18 14:05:34 +01:00
litetex
2acaefdb2a Fixed scrolling not working when rotating device 2022-02-17 20:59:41 +01:00
litetex
9c2cdd2513 Removed useless code
https://github.com/TeamNewPipe/NewPipe/pull/7659#discussion_r793752985
2022-02-17 20:59:40 +01:00
litetex
01683aa816 Code improvements 2022-02-17 20:59:39 +01:00
litetex
85f701b94e Fixed listener not re-registering after e.g. a new search is started 2022-02-17 20:59:38 +01:00
litetex
ff7cfe4715 Reverted to loading behavior of #7638 and improved it
The previous/reverted behavior caused unwanted data transmission:
* Removed loading via handleResults/loadMoreItems-callback because the RecyclerView is apparently not immediately updated in the UI when the data is set which causes one load of data to much.
2022-02-17 20:59:38 +01:00
litetex
d3cd3d62b4 Tried to repair #4475 and #3368
* Always recreate the footer so that it's not possible to attach the same instance twice
* Removed support for creating a custom footer as it's never used
* Supply the header with an supplier
  * This might not fix the problem completely as we currently can only create the header once inside Channel, Playlist and RelatedItems-Fragment - allowing creation of multiple headers might be done in the future if the issues still arise
* Other minor fixes
2022-02-17 20:59:36 +01:00
litetex
91c67b085b Code improvements
Removed - partial - stupid code.
2022-02-17 20:59:35 +01:00
litetex
cd8c7ec3c0 Removed InfoListAdapter from checkstyle-suppressions
because if you modify something in the code the suppressions-file no longer matches
2022-02-17 20:59:34 +01:00
litetex
2c51a7970d Improved InfoListAdapter
* Removed unused code
* Cleaned it up
* Made code more readable
2022-02-17 20:59:33 +01:00
litetex
fb362022f7 Load enough initial data into BaseListFragment 2022-02-17 20:59:33 +01:00
litetex
2814ae6d3c Merge pull request #7884 from litetex/improve-image-minimizer
Improved image-minimizer
2022-02-17 19:33:39 +01:00
martin
ed2967ec7d Addressing layout comments 2022-02-17 10:28:50 +01:00
Martin
616fb47983 Merge branch 'TeamNewPipe:dev' into dev 2022-02-17 10:20:44 +01:00
litetex
7225199deb Fixed typo
It was late when I typed this 😆
2022-02-16 20:31:15 +01:00
litetex
c08a4e851b Improved image-minimizer
* Don't minimize images that are too wide -> they will get stretched otherwise
* Don't try to modify the issue/comment when nothing changed
* Fixed typo
2022-02-15 20:09:21 +01:00
Robin
9f8e8c0856 Merge pull request #7679 from TacoTheDank/reportSenderKotlin
Update ACRA library
2022-02-14 15:35:00 +01:00
litetex
e2a7b9ac56 Switch to GitHub issue forms (#7773)
* Switched to GitHub issue forms

See also
* https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#creating-issue-forms
* https://github.blog/changelog/2021-06-23-issues-forms-beta-for-public-repositories/

* Switched expected and actual behavior

* Improved/Reworked issue template

Credits to @TheAssassin

* CI: Ignore changes to issue-templates

* Improved/Reworked issue template

Credits to @opusforlife2 and @mhmdanas

* Further improved the issue templates

* Next round of review

Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
2022-02-12 19:34:08 +00:00
Tobi
5e593f687d Merge pull request #7860 from litetex/fix-settings-search-language
Use the correct app language when searching in the settings
2022-02-11 21:20:08 +01:00
litetex
3223ec04e3 Use the correct app language when searching in the settings 2022-02-11 20:58:44 +01:00
litetex
f388a1af67 Added image-minimizer (#7772)
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
2022-02-11 19:54:12 +00:00
Tobi
c1fe5c8d07 Merge pull request #7852 from TiA4f8R/player-recovery-workaround-play-thumbnail
Set workaround for playback position reset when switching to main player with content thumbnail
2022-02-11 20:49:12 +01:00
TiA4f8R
608e73e2f2 Set workaround for playback position reset when switching to main player with content thumbnail
The workaround set before was not applied when switching to main player with content thumbnail from popup or background player. This commit fixes this by applying the workaround when switching to main player with content thumbnail from popup or background player.
2022-02-11 19:32:13 +01:00
Tobi
2e538b8959 Add changelog for NewPipe 0.22.0 (983) (#7810)
* Add changelog for NewPipe 0.22.0 (983)
Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
2022-02-11 15:25:25 +01:00
litetex
1278fc27ae Merge pull request #7803 from mhmdanas/paths-ignore-doc-dir
Add some stuff to `paths-ignore`
2022-02-09 19:57:47 +01:00
litetex
be95d7fe0f Merge pull request #7704 from Stypox/fix-stream-menu-crash
Fix crash when long-pressing stream while player is starting
2022-02-09 19:53:09 +01:00
Martin
9397ff8dd0 Merge branch 'TeamNewPipe:dev' into dev 2022-02-05 12:35:27 +01:00
martin
906ee75278 Fixed checkstyle violation 2022-02-05 12:31:07 +01:00
Mohammed Anas
377914f1d8 Small changes to license section of README (#7710)
Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
2022-02-04 18:26:37 +00:00
martin
4049abf2c0 Addressed comment in PR 2022-02-04 16:15:55 +01:00
martin
47798febed fetch and merge 2022-02-04 15:34:00 +01:00
Poussinou
5bf439ad9e Update FUNDING.yml (#7682)
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
2022-02-04 10:36:00 +00:00
Mohammed Anas
3b1b23ba2a Add FUNDING.yml to paths-ignore 2022-02-04 10:32:33 +00:00
Mohammed Anas
9274e6417a Add files in doc to paths-ignore 2022-02-04 10:13:10 +00:00
Atemu
67b2503062 app/build.grade: androidxRoomVersion 2.3.0 -> 2.4.1
This version of Room includes a fix for building dependant apps such as NewPipe
on Apple Silicon devices (aarch64-darwin)
2022-02-04 09:56:56 +01:00
Mohammed Anas
dce6565af4 Merge pull request #7776 from litetex/merged-master-into-dev-v0.21.16
Merge ``master`` branch back into ``dev``
2022-02-03 23:41:20 +03:00
Alberto Mosconi
8b3aec5edb Move translated READMEs to subfolder (#7784) 2022-02-03 20:33:27 +00:00
litetex
b0e4f947ea Fixed merge conflict
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
2022-02-03 21:06:25 +01:00
Atemu
3a9cdb28ab app/build.grade: compileSdk 30 -> 31
Required for newer versions of some dependencies
2022-02-03 13:59:41 +01:00
TacoTheDank
79060f0bfe Update ACRA library 2022-02-02 13:12:29 -05:00
litetex
91bcd8766a Merge remote-tracking branch 'upstream/master' into experimental 2022-02-01 22:05:19 +01:00
Robin
e865c4350e Merge pull request #7661 from TiA4f8R/livestreams-improvements
Increase playlist stuck target duration coefficient and catch BehindLiveWindowExceptions properly
2022-02-01 11:38:12 +01:00
TiA4f8R
52cc4a0a05 Add JavaDoc for PlayerDataSource.PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT 2022-01-30 20:42:02 +01:00
TiA4f8R
e103e4817c Apply suggested changes and remove the CustomHlsPlaylistTracker class 2022-01-30 20:42:02 +01:00
TiA4f8R
d0637a8832 Suppress SonarLint NullPointerException warnings in CustomHlsPlaylistTracker
They seem to be wrong, by looking at the class work and at the return of CustomHlsPlaylistTracker's methods.
2022-01-30 20:42:02 +01:00
TiA4f8R
94f774b82d Use a custom HlsPlaylistTracker, based on DefaultHlsPlaylistTracker to allow more stucking on HLS livestreams
ExoPlayer's default behavior is to use a multiplication of target segment by a coefficient (3,5).
This coefficient (and this behavior) cannot be customized without using a custom HlsPlaylistTracker right now.

New behavior is to wait 15 seconds before throwing a PlaylistStuckException.
This should improve a lot HLS live streaming on (very) low-latency livestreams with buffering issues, especially on YouTube with their HLS manifests.
2022-01-30 20:42:01 +01:00
TiA4f8R
651b79d3ed Catch properly BehindLiveWindowExceptions
Instead of trying to reload the play queue manager and then throwing an error, BehindLiveWindowExceptions now make the app seek to the default playback position, like recommended by ExoPlayer.
The buffering state is shown in this case.

Error handling of other exceptions is not changed.
2022-01-30 20:42:01 +01:00
Radplay
9e5b9ca326 Polish translation of README (#7694)
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
2022-01-30 20:10:53 +03:00
litetex
dfa606ef49 Merge pull request #7586 from litetex/add-preference-search
Made preferences searchable
2022-01-30 17:08:04 +01:00
litetex
2886bc3b01 Merge pull request #4833 from vkay94/youtube-rewind-forward
YouTube's Fast Forward/Rewind behavior
2022-01-30 17:07:15 +01:00
Stypox
71c5aaa11e Do not show enqueue button if play queue not ready 2022-01-27 17:11:16 +01:00
Stypox
466db83375 Improve HistoryRecordManager tests (#7700)
* Improve HistoryRecordManager tests

* Improve shuffle as requested
2022-01-27 14:43:51 +01:00
Stypox
17c0fffd73 Merge pull request #7538 from ktprograms/fix-queue-channel-details-not-in-db
Load uploaderUrl when showing Channel Details from Play Queue
2022-01-26 09:58:32 +01:00
litetex
8a069b497f Code cleanup
Co-authored-by: Stypox <stypox@pm.me>
2022-01-25 20:47:53 +01:00
litetex
af79479716 Fixed "Changing the seeks duration does not update the displayed seconds" 2022-01-25 20:44:49 +01:00
litetex
8cfe8c17e3 Merge pull request #7693 from TiA4f8R/fix-first-item-play-queue-always-played
Fix first item always played in the play queue when reloading play queue manager
2022-01-25 19:55:10 +01:00
Stypox
5108d75682 Fix NPE and add some @Nullables
Fix NullPointerException in PlayerHolder.getQueueSize() and add `@Nullable` here and there so that the linter reports risks of NPEs
2022-01-25 17:37:20 +01:00
Stypox
ac53196dcc Merge pull request #7678 from TacoTheDank/gradleDeprecations
Fix some gradle deprecations
2022-01-25 10:17:09 +01:00
ktprograms
1e652b159e Load uploaderUrl when showing Channel Details from Play Queue
This checks if the uploaderUrl is in the database, if not it gets the
uploaderUrl and puts it in the database. This is similar to the fetching
of uploaderUrl when it doesn't exist done in #6919.

Also use createNotification when error occurs in getStreamInfo.
2022-01-25 11:00:34 +08:00
TiA4f8R
ea07d7751b Fix first item played in playlists when switching player type or resolution
The issue was caused by an ExoPlayer change, which when setting a media source, resets the current playback position and the current window index by default.

Also set player recovery in more places to fix playback position not propely set in some cases after a player type switch.
2022-01-24 21:40:16 +01:00
litetex
82de35d724 Use view binding inside `PreferenceViewHolder` 2022-01-24 21:08:52 +01:00
litetex
f55e8ea3aa Use ViewBinding 2022-01-24 21:08:52 +01:00
litetex
7067ebdd12 Fixed imports 2022-01-24 21:08:51 +01:00
litetex
03bb2123f2 Removed breadcrumbs customization 2022-01-24 21:08:51 +01:00
litetex
e2f449f0c8 Code improvements
* Renamed methods so that they are more understandable
* Removed ``SearchIndexItem``
2022-01-24 21:08:50 +01:00
litetex
b16e972710 Improved doc 2022-01-24 21:08:50 +01:00
litetex
37cd71328c Moved `FuzzyScore` to original Apache package 2022-01-24 21:08:49 +01:00
litetex
9b2c86a37b Improved documentation 2022-01-24 21:08:48 +01:00
litetex
ce4dd33eab Fixed problems with Android's lifecycle (restore) 2022-01-24 21:08:48 +01:00
litetex
8bbc3e531c Fixed gitignore and commited missing file 2022-01-24 21:08:47 +01:00
litetex
c5a06243a6 Fixed variable name 2022-01-24 21:08:47 +01:00
litetex
bebd2b449c Removed unused import 2022-01-24 21:08:46 +01:00
litetex
658168eb8d Fixed some sonar warnings 2022-01-24 21:08:45 +01:00
litetex
6b23df0659 Made debug settings searchable (debug only)
* Consolidated main-setttings into a single file
* Debug settings are only enabled in the DEBUG build
* Moved LeakCanary (debug) specific stuff into a small class that's only shipped with the debug build
* Other minor fixes
2022-01-24 21:08:45 +01:00
litetex
d59314801c Code rework 2022-01-24 21:08:44 +01:00
litetex
0f45c69388 Code cleanup + indexing improvements
* Removed unused method
* Only index all settings once -> Saves performance
* Fixed some SonarLint reported problems
2022-01-24 21:08:44 +01:00
litetex
52542e04e8 Added fuzzy searching + Some minor code refactoring 2022-01-24 21:08:43 +01:00
litetex
7fc0a3841a Fine tuning 2022-01-24 21:08:42 +01:00
litetex
22db4175f3 Moved reset-reCAPTCHA-cookie to cache tab and made it read-only
so that the search works as expected
2022-01-24 21:08:42 +01:00
litetex
8fc935b09d Added resource files
Forgot to commit them before...
2022-01-24 21:08:41 +01:00
litetex
07fb319e88 Applied code changes for preference search framework 2022-01-24 21:08:41 +01:00
litetex
12a78a826d Added preference search "framework" 2022-01-24 21:08:40 +01:00
litetex
4a061f20ed Code cleanup 2022-01-24 21:08:39 +01:00
litetex
f3be89b503 Abstracted methods for the Android keyboard 2022-01-24 21:08:39 +01:00
litetex
12acaf29dd Added credit to the project which inspired the preference search 2022-01-24 21:08:38 +01:00
litetex
683d9816cb Removed dead code 2022-01-24 21:08:38 +01:00
Tobi
8802582997 Merge pull request #7699 from litetex/sync-weblate-v2
Sync weblate v2
2022-01-24 20:42:20 +01:00
Ajeje Brazorf
983c98d262 Translated using Weblate (Sardinian)
Currently translated at 100.0% (640 of 640 strings)
2022-01-24 20:15:51 +01:00
Alex25820
c38389672a Translated using Weblate (Swedish)
Currently translated at 100.0% (640 of 640 strings)
2022-01-24 20:15:50 +01:00
litetex
93148400a2 Merge pull request #7689 from litetex/sync-weblate
Sync weblate
2022-01-24 19:59:44 +01:00
Stypox
d5cfcb28fc Merge branch 'dev' into pr2335 2022-01-24 10:25:07 +01:00
Stypox
40ea51e622 Add more checking frequencies, use DurationListPreference 2022-01-24 10:12:25 +01:00
TobiGr
194e43f5cb Remove unused strings
report_player_errors_title and report_player_errors_summary were removed in #7482
2022-01-23 22:40:48 +01:00
retiolus
08c928e1d0 Translated using Weblate (Catalan)
Currently translated at 98.5% (631 of 640 strings)
2022-01-23 21:05:46 +01:00
subba raidu
69dacb34b9 Translated using Weblate (Telugu)
Currently translated at 66.0% (423 of 640 strings)
2022-01-23 21:05:45 +01:00
Danial Behzadi
60c3a2dc9c Translated using Weblate (Persian)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:45 +01:00
TiA4f8R
b8e5e036b2 Translated using Weblate (French)
Currently translated at 99.2% (635 of 640 strings)
2022-01-23 21:05:44 +01:00
VfBFan
2f87305f2d Translated using Weblate (German)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:43 +01:00
translator
15dc99f110 Translated using Weblate (French)
Currently translated at 99.2% (635 of 640 strings)
2022-01-23 21:05:42 +01:00
qqqq1
2d907706ea Translated using Weblate (Persian)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:40 +01:00
ssantos
f5dbb07893 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:40 +01:00
Zi
a437672dc1 Translated using Weblate (Javanese)
Currently translated at 9.5% (61 of 640 strings)
2022-01-23 21:05:39 +01:00
Yaron Shahrabani
388a4860b5 Translated using Weblate (Hebrew)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:39 +01:00
Jeff Huang
4b72ee53b0 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:39 +01:00
Agnieszka C
d77c23ed34 Translated using Weblate (Polish)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:39 +01:00
Rex_sa
31635c122e Translated using Weblate (Arabic)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:38 +01:00
Soare Robert Daniel
afef793fbb Translated using Weblate (Romanian)
Currently translated at 97.9% (627 of 640 strings)
2022-01-23 21:05:38 +01:00
Vasilis K
3bc2ec90ef Translated using Weblate (Greek)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:38 +01:00
Sergio Varela
a3e68c93f8 Translated using Weblate (Basque)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:37 +01:00
ssantos
15e6f1cb3b Translated using Weblate (Portuguese)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:37 +01:00
S3aBreeze
89c540c520 Translated using Weblate (Russian)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:37 +01:00
mm4c
6632720bc3 Translated using Weblate (Dutch)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:37 +01:00
TiA4f8R
b5662c2d07 Translated using Weblate (French)
Currently translated at 99.2% (635 of 640 strings)
2022-01-23 21:05:36 +01:00
ssantos
0f74c2463e Translated using Weblate (Portuguese (Portugal))
Currently translated at 99.8% (639 of 640 strings)
2022-01-23 21:05:36 +01:00
MohammedSR Vevo
fdfdf94cb9 Translated using Weblate (Kurdish (Central))
Currently translated at 99.2% (635 of 640 strings)
2022-01-23 21:05:36 +01:00
Mikkel
8595078053 Translated using Weblate (Danish)
Currently translated at 51.2% (328 of 640 strings)
2022-01-23 21:05:36 +01:00
Priit Jõerüüt
80be089ca9 Translated using Weblate (Estonian)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:35 +01:00
Karl Tammik
96ab2f855e Translated using Weblate (Estonian)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:35 +01:00
Ihor Hordiichuk
4206ae84c1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:35 +01:00
Mohammed Anas
2f21523da9 Translated using Weblate (Arabic)
Currently translated at 99.6% (638 of 640 strings)
2022-01-23 21:05:35 +01:00
Daniel
6c1222ea32 Translated using Weblate (Romanian)
Currently translated at 96.4% (617 of 640 strings)
2022-01-23 21:05:34 +01:00
ssantos
ba50de236c Translated using Weblate (Portuguese)
Currently translated at 99.8% (639 of 640 strings)
2022-01-23 21:05:34 +01:00
translator
bef8882a7c Translated using Weblate (French)
Currently translated at 99.2% (635 of 640 strings)
2022-01-23 21:05:34 +01:00
Allan Nordhøy
0d8b7e23e7 Translated using Weblate (English)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:34 +01:00
retiolus
864c19e7dc Translated using Weblate (Catalan)
Currently translated at 94.8% (607 of 640 strings)
2022-01-23 21:05:34 +01:00
subba raidu
4b0ed9de5d Translated using Weblate (Telugu)
Currently translated at 63.5% (407 of 640 strings)
2022-01-23 21:05:33 +01:00
ToldYouThat
d18a34b766 Translated using Weblate (Turkish)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:33 +01:00
zeritti
0cf24c5d36 Translated using Weblate (Czech)
Currently translated at 100.0% (640 of 640 strings)
2022-01-23 21:05:33 +01:00
Hosted Weblate
fa293e3415 Translated using Weblate (Telugu)
Currently translated at 58.4% (374 of 640 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 94.8% (607 of 640 strings)

Translated using Weblate (Swedish)

Currently translated at 99.6% (638 of 640 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (637 of 640 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Swedish)

Currently translated at 25.8% (16 of 62 strings)

Translated using Weblate (Portuguese)

Currently translated at 67.7% (42 of 62 strings)

Translated using Weblate (Ukrainian)

Currently translated at 82.2% (51 of 62 strings)

Translated using Weblate (Telugu)

Currently translated at 51.4% (328 of 638 strings)

Translated using Weblate (Swedish)

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 (Turkish)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Czech)

Currently translated at 97.1% (620 of 638 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (633 of 638 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (French)

Currently translated at 99.5% (635 of 638 strings)

Translated using Weblate (French)

Currently translated at 99.5% (635 of 638 strings)

Translated using Weblate (Santali)

Currently translated at 13.9% (89 of 638 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 59.6% (37 of 62 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 66.1% (41 of 62 strings)

Translated using Weblate (Persian)

Currently translated at 62.9% (39 of 62 strings)

Translated using Weblate (Spanish)

Currently translated at 58.0% (36 of 62 strings)

Translated using Weblate (Dutch)

Currently translated at 12.9% (8 of 62 strings)

Translated using Weblate (Hebrew)

Currently translated at 51.6% (32 of 62 strings)

Translated using Weblate (Portuguese)

Currently translated at 66.1% (41 of 62 strings)

Translated using Weblate (Ukrainian)

Currently translated at 80.6% (50 of 62 strings)

Translated using Weblate (Dutch)

Currently translated at 98.4% (628 of 638 strings)

Translated using Weblate (Hindi)

Currently translated at 4.9% (3 of 61 strings)

Translated using Weblate (Hungarian)

Currently translated at 6.5% (4 of 61 strings)

Translated using Weblate (Hebrew)

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 (Russian)

Currently translated at 99.8% (637 of 638 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (French)

Currently translated at 99.3% (634 of 638 strings)

Translated using Weblate (Hungarian)

Currently translated at 6.5% (4 of 61 strings)

Translated using Weblate (Hungarian)

Currently translated at 6.5% (4 of 61 strings)

Translated using Weblate (Hungarian)

Currently translated at 4.9% (3 of 61 strings)

Translated using Weblate (Telugu)

Currently translated at 51.0% (326 of 638 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Swedish)

Currently translated at 24.5% (15 of 61 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Croatian)

Currently translated at 98.7% (630 of 638 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (637 of 638 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.0% (632 of 638 strings)

Translated using Weblate (Chinese (Traditional))

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 (Portuguese (Brazil))

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 (Ukrainian)

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 (Greek)

Currently translated at 100.0% (638 of 638 strings)

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

Currently translated at 58.4% (373 of 638 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (638 of 638 strings)

Added translation using Weblate (English (Middle))

Translated using Weblate (Dutch)

Currently translated at 11.4% (7 of 61 strings)

Translated using Weblate (Telugu)

Currently translated at 50.6% (323 of 638 strings)

Translated using Weblate (Hebrew)

Currently translated at 97.9% (625 of 638 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Persian)

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 (Indonesian)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (637 of 638 strings)

Translated using Weblate (Japanese)

Currently translated at 98.5% (629 of 638 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (626 of 638 strings)

Translated using Weblate (French)

Currently translated at 98.5% (629 of 638 strings)

Translated using Weblate (Spanish)

Currently translated at 99.0% (632 of 638 strings)

Translated using Weblate (German)

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (English)

Currently translated at 100.0% (631 of 631 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (631 of 631 strings)

Translated using Weblate (French)

Currently translated at 99.6% (629 of 631 strings)

Translated using Weblate (Telugu)

Currently translated at 43.2% (273 of 631 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (630 of 631 strings)

Translated using Weblate (French)

Currently translated at 99.6% (629 of 631 strings)

Co-authored-by: Abhishek Kumar <abhi7737-22@yahoo.com>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Alfonso Scarpino <alfonso.scarpino@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andreas P <andreas.polnas93@hotmail.com>
Co-authored-by: Andrey F <firsan777@mail.ru>
Co-authored-by: Balázs Meskó <meskobalazs@mailbox.org>
Co-authored-by: Boros Zsombor <zsombor2626@gmail.com>
Co-authored-by: Calcitem <calcitem@outlook.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Florian <flo.site@zaclys.net>
Co-authored-by: GnuPGを使うべきだ <dieeeazpnnqbpddh@cock.email>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Name Protected <notemailprotected@protonmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Ricardo <contatorms7@tutamail.com>
Co-authored-by: S3aBreeze <paperwork@evilcorp.ltd>
Co-authored-by: TiA4f8R <avdivers84@gmail.com>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yuki Monkey <ymonkey32@gmail.com>
Co-authored-by: bomzhellino <adm.bomzh@gmail.com>
Co-authored-by: bruh <quangtrung02hn16@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: evfjunior <evfjunior@protonmail.com>
Co-authored-by: mm4c <oldblue@vivaldi.net>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: ndrnry <ondernuray@gmail.com>
Co-authored-by: nzgha <nzgha@runbox.com>
Co-authored-by: nzgha <nzghafoss.ldxwe@slmail.me>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: subba raidu <raidu4u@gmail.com>
Co-authored-by: translator <kvb@tuta.io>
Co-authored-by: wmd arg <wmdarg3@gmail.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: 刘韬 <lyuutau@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translation: NewPipe/Metadata
2022-01-23 21:05:32 +01:00
Stypox
1531a5112c Merge pull request #7614 from litetex/revert-7568
Revert #7568
2022-01-23 15:54:55 +01:00
litetex
e127db6fa6 Simplified toast showing behavior
after feedback from the review
2022-01-23 15:34:11 +01:00
litetex
49b1649348 Revert "Merge pull request #7568 from vhouriet/vhouriet_feature_already-playing-in-backgroud"
This reverts commit ee19ea66b3, reversing
changes made to 6b490ee547.
2022-01-23 15:34:11 +01:00
Filip Czaplicki
9f503917c2 Android TV banner with text (#7566)
The banner was made by @AioiLight.
2022-01-22 20:29:12 +03:00
litetex
54ef604569 Improved docs 2022-01-21 22:49:25 +01:00
litetex
30ce906f72 Apply seek conditions based on direction
* When rewinding: Check if <0,5s
* When fast-forwarding: Check if player has completed or the current playback has ended

This allows rewinding on the endscreen
2022-01-21 22:49:25 +01:00
litetex
1c20eabb48 Code cleanup 2022-01-21 22:49:25 +01:00
litetex
f8c52c4dac Fixed SonarLint problems
* Removed alphaRelativeDuration as there is no use for it
2022-01-21 22:49:24 +01:00
litetex
345ba74d58 Fixed naming 2022-01-21 22:49:24 +01:00
litetex
d2aaf152a0 Removed related import 2022-01-21 22:49:24 +01:00
litetex
7bf1f3dba6 Removed unused field 2022-01-21 22:49:24 +01:00
litetex
452fe3a8e2 Respect disabled animations 2022-01-21 22:49:24 +01:00
litetex
c25e523df6 Removed all animations to be consistent with the current behavior 2022-01-21 22:49:23 +01:00
litetex
65bb1dcdbf Refactored code 2022-01-21 22:49:23 +01:00
litetex
fe42206e94 Code cleanup and minimization
* Deduplicated and simplified a lot of code
* Fixed ``invalidSeekConditions`` so that it's possible to seek while the player is loading (like currently the case)
2022-01-21 22:49:23 +01:00
TobiGr
dac47d9f52 Replace NotNull annotation with NonNull annotation 2022-01-21 22:49:23 +01:00
Stypox
83a3d11f38 Small improvements to player 2022-01-21 22:49:22 +01:00
Stypox
03d5372525 Fix buttons' selectable item background not working in player 2022-01-21 22:49:22 +01:00
Stypox
a454a41b51 Fix controls not hiding correctly when switching player 2022-01-21 22:49:22 +01:00
Stypox
95631dba46 Convert SecondsView from kotlin synthetics to view binding 2022-01-21 22:49:22 +01:00
Stypox
ee827407aa Fix seek triangles not visible on API 19 2022-01-21 22:49:22 +01:00
vkay94
3aebfa22e9 SeekOverlay: Switch to merge for SecondsView and other adjustments 2022-01-21 22:49:21 +01:00
vkay94
72eb3b4415 SeekOverlay: Add seek overlay logic to player 2022-01-21 22:49:21 +01:00
vkay94
3a40759cd2 SeekOverlay: Add Views 2022-01-21 22:49:21 +01:00
litetex
6f44ced7b6 Merge pull request #7672 from Stypox/fix-froid-icon
Add app icon to fastlane metadata
2022-01-21 22:35:48 +01:00
litetex
81843ddb6e Merge pull request #7670 from XiangRongLin/search_history_test
Ensure order of search history entries in tests
2022-01-21 22:34:48 +01:00
TacoTheDank
23d14ab443 Fix some gradle deprecations 2022-01-21 16:25:11 -05:00
litetex
3d3d94655b Fixed imports 2022-01-21 22:19:52 +01:00
litetex
a6515d5450 Moved timeout control from the tests to the CI pipeline
How fast a tests is executed on a shared CI pipeline is not predictable as the build might be throttled because other builds are running.
Therefore adding extremely short timeouts inside the tests - where they can't be changed - is a bad idea.
Removed them for now.
2022-01-21 22:15:34 +01:00
litetex
2d0da2c7a4 Merge pull request #7317 from TacoTheDank/ignoreStrings
Some untranslatable string improvements
2022-01-21 20:57:45 +01:00
akamayu-ouo
f05affa984 Add traditional Chinese README (#7618)
* Add traditional Chinese README
2022-01-21 20:56:14 +01:00
XiangRongLin
cd265fc31f Make SearchHistoryEntry.kt fields nullable to match java version (#7674) 2022-01-21 16:01:11 +00:00
XiangRongLin
3c21be8fa5 use constant instead of now 2022-01-20 19:14:47 +01:00
litetex
f681b0bb5a Merge pull request #7648 from mhmdanas/small-refactors
Fix false warning
2022-01-19 19:48:07 +01:00
XiangRongLin
d7fbddf6f8 Merge pull request #7668 from litetex/fix-video-reset-on-player-switch
Workaround: Set recovery before switching player
2022-01-19 16:45:10 +01:00
Stypox
993c34911a Add app icon to fastlane metadata
For some reason F-Droid decided not to grab it from Android metadata anymore
2022-01-19 12:03:46 +01:00
XiangRongLin
4a7cfd1a6c Ensure order of search history entries in tests 2022-01-18 18:36:43 +01:00
mhmdanas
402990dd9d Fix false warning 2022-01-18 07:40:59 +03:00
litetex
41faf70da1 Workaround: Set recovery before switching player
Quick fix
2022-01-17 20:52:07 +01:00
litetex
15e3b6301c Merge pull request #7662 from TiA4f8R/fix-npe-share-button-playlists
Fix crash when sharing a playlist which is loading
2022-01-16 19:47:55 +01:00
XiangRongLin
5b9c28b93b Replace JUnit asserts with AssertJ in HistoryRecordManagerTest (#7654)
* Replace JUnit asserts with AssertJ in HistoryRecordManagerTest

They provide a wider range of assertions, which allow for more detailed error messages.
Also convert SearchHistoryEntry to kotlin data class for better error messages, since toString() is implemented.

Co-authored-by: Mohammed Anas <triallax@tutanota.com>
2022-01-16 09:10:45 +01:00
TiA4f8R
6672169707 Fix NullPointerException when sharing a playlist which is loading
Prevent a NullPointerException by adding a null check for currentInfo when sharing a playlist.
2022-01-15 21:19:04 +01:00
XiangRongLin
9ff1baefde Merge pull request #7636 from litetex/show-ci-android-test-failures
CI: Upload test-report when an error occurs
2022-01-10 09:41:12 +01:00
litetex
552734faa5 CI: Upload test-report when an error occurs 2022-01-09 18:09:47 +01:00
TacoTheDank
7268e04361 Remove redundant XML attributes in settings_keys 2022-01-07 21:26:51 -05:00
TacoTheDank
45d8fef00c Update F-Droid VLC link 2022-01-07 21:24:25 -05:00
TacoTheDank
0f83497284 Move untranslatable strings to a donottranslate file 2022-01-07 21:24:01 -05:00
Stypox
1475ff805f Merge pull request #7619 from litetex/removed-list_thumbnail_view_description
Removed "list_thumbnail_view_description"
2022-01-07 22:10:27 +01:00
Stypox
7907182e7e Merge pull request #7036 from Douile/fix/queue-metadata
Load full stream info when enqueuing a stream
2022-01-07 21:57:30 +01:00
litetex
0397a3120f Removed unused string 2022-01-05 15:55:55 +01:00
litetex
cc34734131 Refactored `initNotificationChannels` 2022-01-05 15:48:46 +01:00
litetex
6dcde96f85 Fixed some Sonarlint warnings 2022-01-05 15:31:55 +01:00
litetex
0f457127df Removed "list_thumbnail_view_description"
due to accessibility problems
2022-01-04 16:00:01 +01:00
Douile
064242d962 Remove unecessary interface InfoCallback
Co-authored-by: Stypox <stypox@pm.me>

Replace the unecessary callback interface InfoCallback in favour of the
standard type Consumer<SinglePlayQueue>
2022-01-03 17:52:27 +00:00
litetex
ddcbe27fd3 Fixed search not accepting key input after closing player overlay (#7607)
* Fixed search not accepting key input after closing player overlay

* Made comments easier to understand

* More comments
2022-01-03 11:52:08 +01:00
Stypox
ccbc3af964 Show error notification when new streams notifications failed 2021-12-31 20:04:56 +01:00
Stypox
cd95ec4e12 Merge branch 'dev' into pr2335 2021-12-31 19:20:18 +01:00
Stypox
fcd2d63df4 Don't show any channel notification thumbnail if it could not be loaded 2021-12-31 18:38:35 +01:00
Stypox
e68d49e7df Do not fetch all streams when disabling notifications for a channel 2021-12-31 18:34:02 +01:00
litetex
ee19ea66b3 Merge pull request #7568 from vhouriet/vhouriet_feature_already-playing-in-backgroud
Add Already playing in background toast
2021-12-31 14:54:05 +01:00
litetex
6b490ee547 Merge pull request #7582 from Jaspann/sub-channel-tumbnail-view-fix
Fixes view of sub-channel icon when not in use
2021-12-31 14:52:32 +01:00
litetex
e127697fff Merge pull request #7589 from litetex/bump-extractor
Updated NewPipeExtractor
2021-12-29 14:44:51 +01:00
Jaspann
558c9147a2 Move sub icon visibility line 2021-12-28 15:54:36 -05:00
litetex
4147c7c1d1 Updated NewPipeExtractor 2021-12-27 18:41:53 +01:00
Tobi
45ef9b0278 Merge pull request #7577 from dkramer95/feature/pinned-comment
Added ability to see pinned comment
2021-12-27 16:51:36 +01:00
litetex
fc0e709817 Fixed usage of wrong string 2021-12-27 00:14:04 +01:00
litetex
b67bf16d4f Minified code 2021-12-26 23:57:54 +01:00
William Parker
fb3be544ce Fixed code styling 2021-12-26 00:12:48 -05:00
Jaspann
53f5741317 Fixes view of sub-channel icon when not in use 2021-12-25 18:10:05 -05:00
Martin
5134080f87 Merge branch 'TeamNewPipe:dev' into dev 2021-12-25 15:14:24 +01:00
David Kramer
07015973d2 Added ability to see pinned comment 2021-12-24 11:29:34 -06:00
Tobi
215880207e Merge pull request #7562 from TeamNewPipe/code-quality
Small improvements to code quality and readability
2021-12-23 23:54:29 +01:00
TobiGr
41c4ab5739 Merge branch 'master' into dev 2021-12-23 23:41:37 +01:00
vhouriet
989bcbf895 Add Already playing in background toast 2021-12-23 22:12:38 +01:00
Martin
3e44856d01 Merge branch 'TeamNewPipe:dev' into dev 2021-12-23 15:44:09 +01:00
XiangRongLin
19dd9d266a Merge pull request #6855 from TeamNewPipe/fix/inspection
Fix readme link
2021-12-23 11:47:03 +01:00
XiangRongLin
05370dbb94 Merge pull request #7556 from Jaspann/improve-hashtags
Fixes hashtags that use non-English characters.
2021-12-23 11:45:52 +01:00
TobiGr
f3edc69897 Fix incorrect link in Japanese README 2021-12-22 21:16:45 +01:00
Robin
f6cad2d9cf Merge pull request #7555 from litetex/remove-mediaparser
Remove/Revert MediaParser support (Android 11+) due to bugs
2021-12-22 17:32:28 +01:00
Martin
bd1c0033eb Merge branch 'TeamNewPipe:dev' into dev 2021-12-22 10:47:13 +01:00
XiangRongLin
dc67628ba5 Log test-android CI job in stacktrace mode 2021-12-22 09:55:16 +01:00
TobiGr
37b8a9375f Small improvements to code quality and readability
Add annotations to methods and parameters.
Replace Jetbrains' @NotNull with Androidx' @NonNull annotatiation.
Make class variables static if possible.
Use constants for some Strings.
Simplify if conditions.
2021-12-21 20:55:41 +01:00
TobiGr
d71af9a625 Introduce constants for some Strings that indicate no data in Tab class 2021-12-21 20:53:17 +01:00
TobiGr
a163d5461d Make PlayerHolder.bound private 2021-12-21 20:51:18 +01:00
martin
5514616372 Change pitch by semitones 2021-12-21 18:17:48 +01:00
Jaspann
a274baf5cd Adds comment to HASHTAGS_PATTERN 2021-12-20 14:13:40 -05:00
XiangRongLin
96eb1425f8 Merge pull request #7552 from XiangRongLin/readd_sonar
Readd sonar CI job
2021-12-20 03:32:28 -05:00
Jaspann
361760be0a Allows multilingual support in hashtags 2021-12-19 20:33:55 -05:00
litetex
eea2768633 Removed/Reverted MediaParser support (Android 11+) due to bugs 2021-12-19 20:16:10 +01:00
Robin
d3562c70f5 Merge pull request #7451 from Cheechaii/respect-cutout-in-multiwindow
Respect cutouts when playing in MultiWindow
2021-12-19 13:23:28 +01:00
XiangRongLin
e06342eacf Readd sonar CI job 2021-12-19 12:18:37 +01:00
litetex
e8d909553d Merge pull request #7499 from TacoTheDank/bumpVersions
Update a bunch of libraries
2021-12-18 22:52:11 +01:00
litetex
b21d231e3a Merge pull request #7452 from litetex/show-alert-when-file-manager-not-found
Show an alert-dialog when no appropriate file-manager was found
2021-12-14 20:01:02 +01:00
litetex
4058277b7a Merge pull request #7482 from TeamNewPipe/unify-error-reporting
Unify error reporting and add error notification
2021-12-14 19:58:41 +01:00
XiangRongLin
dd9772cde2 Merge pull request #7491 from Stypox/fix-search-order
Fix order of local search results
2021-12-14 09:07:17 -05:00
TacoTheDank
a924f819a9 Update a bunch of libraries 2021-12-12 21:26:59 -05:00
TacoTheDank
156bbad5b5 Update Gradle 2021-12-12 20:03:42 -05:00
Stypox
01f3ed0e5e Fix loading icon in streams notifications 2021-12-12 20:18:16 +01:00
Stypox
2963cd5c6e Add HistoryRecordManagerTest 2021-12-12 16:00:16 +01:00
Stypox
7d6688f497 Add DatabaseMocker to mock NewPipeDatabase 2021-12-12 15:59:04 +01:00
litetex
b056faa97f Merge pull request #7500 from TacoTheDank/bumpKtlint
Bump ktlint, Checkstyle, Java version
2021-12-12 15:07:24 +01:00
Douile
3ff00ff50e Fix lambda code formatting
Co-authored-by: Stypox <stypox@pm.me>
2021-12-12 13:04:32 +00:00
Tom
baee915db5 Remove unecessary line
Co-authored-by: Stypox <stypox@pm.me>
2021-12-12 12:51:01 +00:00
Tobi
4e6dcc693b Merge pull request #7524 from TeamNewPipe/master
Sync changes from ``master`` back to ``dev``
2021-12-11 17:32:29 +01:00
TobiGr
19fd7bc37e Reduce power consumption
Only schedule the chek for new streams if the user enaled the check. Cancel the worker when the user disables the notifications.
2021-12-10 23:52:37 +01:00
Cheechaii
c92a90749e Respect cutouts when playing in MultiWindow 2021-12-09 20:58:05 +01:00
TobiGr
779d3dce6f Add app:singleLineTitle="false" to preferences 2021-12-08 21:14:32 +01:00
litetex
e806f8c4e6 Android 10+ only allows SAF -> Respect that in the dialog 2021-12-08 20:22:26 +01:00
Stypox
8a5e2ffa57 Fix order of local search results 2021-12-08 13:59:32 +01:00
litetex
ad405d9e0b Merge pull request #7481 from mhmdanas/add-fsfe-tube-link-support
Add FSFE-Tube PeerTube instance
2021-12-07 21:55:17 +01:00
litetex
b9ee14ac30 Update string-resource
Co-authored-by: Stypox <stypox@pm.me>
2021-12-07 21:31:12 +01:00
Stypox
bb49b1cfb1 Add javadoc to ErrorUtil and ErrorActivity 2021-12-07 18:14:53 +01:00
TobiGr
3ade2bb6ec Merge remote-tracking branch 'origin/dev' into notifications 2021-12-07 17:29:37 +01:00
litetex
4fc9443b4f Merge pull request #7497 from TeamNewPipe/fix-qm16xe
Disable media tunneling on Philips QM16XE
2021-12-06 20:54:07 +01:00
litetex
581ede022e Merge pull request #7498 from litetex/fix-get-it-on-fdroid
Updated/Fixed "Get it on F-Droid"
2021-12-06 20:35:45 +01:00
TacoTheDank
f86fc03c46 Update Checkstyle to 9.2 2021-12-04 21:30:23 -05:00
TacoTheDank
75db002369 Update ktlint to 0.43.2 2021-12-04 21:22:29 -05:00
TacoTheDank
dbfa4e554b Update Sonarqube to 3.3 2021-12-04 21:22:15 -05:00
TacoTheDank
84d87a2e60 Move plugins to the DSL block 2021-12-04 21:16:37 -05:00
litetex
9e3577e77b Updated/Fixed "Get it on F-Droid" 2021-12-04 23:46:40 +01:00
Robin
41a0dc1abd Disable media tunneling on Philips QM16XE 2021-12-04 22:23:09 +01:00
Stypox
950956ebf2 Also show a toast on error notification
since the notification is silent, also show a toast, otherwise the user is confused
2021-12-04 10:50:27 +01:00
Stypox
c000c1d455 Add debug prefs to show error snackbar/notification 2021-12-04 10:36:36 +01:00
Stypox
c8e2ab4c83 Remove PlayerErrorHandler and correctly set ErrorInfo msg 2021-12-04 10:36:36 +01:00
Stypox
397f93b079 Prevent exception from being serialized in ErrorInfo
The wrong @Decorator was put in the wrong place to mark the throwable fieldd as transient, now this is fixed and the exception is not serialized. So if a non-serializable throwable is passed, that's not an issue, since it's not going to be serialized. The need for EnsureExceptionSerializable is also gone.
2021-12-04 10:36:36 +01:00
Stypox
09d137f740 Add PendingIntent to ErrorUtil.createNotification 2021-12-04 10:36:36 +01:00
Stypox
81f740d409 Replace ErrorActivity with ErrorUtil 2021-12-04 10:36:36 +01:00
Stypox
1d2642f1e3 Create ErrorUtil class with three ways to report errors
Activity, snackbar and notification
2021-12-04 10:36:31 +01:00
Douile
7cd3603bbb Fetch sparse items when playing in background or popup 2021-12-03 22:38:03 +00:00
Douile
ec7de2a6dc Fix StreamType check, missing import, and styling errors 2021-12-03 21:53:36 +00:00
Tom
3d1a3606c9 Remove unused variable
Co-authored-by: Stypox <stypox@pm.me>
2021-12-03 21:30:26 +00:00
Douile
6472e9b6b6 Remove unused code 2021-12-03 21:29:34 +00:00
mhmdanas
7adebbe989 Add FSFE-Tube PeerTube instance 2021-12-01 20:24:53 +03:00
TobiGr
fd1155928e Fix deciding which streams are new 2021-11-30 23:31:44 +01:00
litetex
744cfe5672 Removed unused import 2021-11-29 21:13:22 +01:00
litetex
17724a901c Removed annotations due to wrong warnings 2021-11-29 21:03:59 +01:00
TobiGr
a8fe2d7e83 Fix "unsage use" warnings 2021-11-28 17:09:20 +01:00
litetex
b2323859e5 Refactoring + deduplicated code 2021-11-28 14:07:45 +01:00
litetex
68e7fcf8ee Fixed typos 2021-11-27 23:39:17 +01:00
litetex
f78983b16b Show an alert/dialog when no appropriate file-manager was found 2021-11-27 15:52:54 +01:00
Tom
91611fcae4 Don't fetch uneeded stream info for live streams
Co-authored-by: Stypox <stypox@pm.me>
2021-11-23 15:22:11 +00:00
TobiGr
8ce996e065 Only check for new streams of subscriptions with enabled notifications automatically 2021-11-21 22:53:10 +01:00
TobiGr
892a1df280 Merge remote-tracking branch 'origin/dev' into notifications-1 2021-11-21 22:15:09 +01:00
litetex
44fa98497f Update app/src/main/res/values/strings.xml
Co-authored-by: Stypox <stypox@pm.me>
2021-11-21 19:42:41 +01:00
litetex
cfd5d7ae35 Update app/src/main/res/values/strings.xml
Removed "-"

Co-authored-by: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
2021-11-06 21:51:33 +01:00
litetex
7b4e5dd107 Reworked player-notfication
* Fixed ``release`` ``main_settings.xml``
* Renamed "Notification" to "Player-Notification" (also reset all translations)
2021-11-05 14:10:53 +01:00
litetex
1289b1a283 Code cleanup 2021-11-05 13:17:33 +01:00
ktprograms
2934841152 Enable play/pause with space key even when not in fullscreen player 2021-11-03 08:26:13 +08:00
litetex
5ae72d1ed2 Removed unknown/unused file 2021-11-03 00:11:44 +01:00
litetex
bc68836c8d Reworked menu_channel.xml 2021-11-02 23:59:48 +01:00
litetex
f0112a2de2 Added some lines to improve code-readability 2021-11-02 23:36:46 +01:00
litetex
94219b78e7 Fixed typos 2021-11-02 23:22:59 +01:00
litetex
0f4b6d7d9f Improved code readablity 2021-11-02 23:22:52 +01:00
litetex
58418bcf46 Improved code readability 2021-11-02 22:57:31 +01:00
litetex
e4cd52060c Reformatted code so that it's better readable 2021-11-02 22:48:49 +01:00
litetex
4f8552835e Better naming for a test class that does database migrations 2021-11-02 22:43:23 +01:00
litetex
707f2835a8 Restructured build.gradle/androidxWorkVersion 2021-11-02 22:26:05 +01:00
TobiGr
1130aba7ca Merge remote-tracking branch 'origin/dev' into notifications-1 2021-11-02 07:56:09 +01:00
ktprograms
34ab93c9bd Fix player controls not hiding if resumed from media button 2021-11-01 11:50:33 +08:00
Douile
bc2f0f9f3e Update stream state in database after loading 2021-10-28 01:11:53 +01:00
TobiGr
2d2b96420f Add comments and improve code formatting 2021-10-25 15:06:18 +02:00
TobiGr
77aaa15082 Fix toggling the system's settings for app notification
Do not open the setting for a specific notification channel (Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS), but the settings for all notifications by the app (Settings.ACTION_APP_NOTIFICATION_SETTINGS)
2021-10-25 13:59:55 +02:00
TobiGr
80bf47493e Fix check wether the app's notifications are disabled via system settings
Add comments
Rename a few methods
2021-10-22 21:24:43 +02:00
TobiGr
7d4c7718aa comments & rename 2021-10-18 13:11:50 +02:00
TobiGr
793ff1a728 Add a few comments and rename a few methods 2021-10-15 20:57:54 +02:00
Tobi
4f7cdcce55 Update app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
Co-authored-by: litetex <40789489+litetex@users.noreply.github.com>
2021-10-15 20:22:12 +02:00
TobiGr
64a7978c7f Rename NotificationMode.ENABLED_DEFAULT to NotificationMode.ENABLED 2021-10-15 19:59:06 +02:00
TobiGr
7c6140b331 Remove unused code 2021-10-15 19:57:31 +02:00
TobiGr
16d4a034e2 Merge remote-tracking branch 'origin/dev' into notifications 2021-10-14 21:15:43 +02:00
TobiGr
55c51ad49d Rename isStreamExist -> doesStreamExist 2021-10-11 23:20:52 +02:00
TobiGr
cea14c9d0d Merge remote-tracking branch 'origin/dev' into notifications-1 2021-10-11 16:37:49 +02:00
Koitharu
fb0473da39 Merge branch 'dev' of https://github.com/TeamNewPipe/NewPipe into feature/notifications 2021-09-20 07:26:01 +03:00
Douile
62d36126ea Load full stream info when enqueuing a stream
This commit calls getStreamInfo causing a full network fetch of stream
info (I believe only if required) when adding a stream item to the
queue. This should prevent UI issues of missing metadata when queueing
videos that have been fast-loaded and are missing metadata.

Fixes #7035
2021-09-19 00:15:56 +01:00
Koitharu
9d249904bd Toggle all subscriptions notification mode 2021-09-07 13:30:26 +03:00
Koitharu
111dc4963d Ignore feed update threshold when run from NotificationWorker 2021-09-07 13:30:26 +03:00
Koitharu
5a6d0455ec Migrate NotificationIcon to Picasso 2021-09-07 13:30:26 +03:00
Koitharu
a5b9fe4c35 Refactor FeedLoadService to use it within the notification worker 2021-09-07 13:30:26 +03:00
Koitharu
c95aec9da6 Fix database test 2021-09-07 13:30:25 +03:00
Koitharu
e0c674bc9e Move player notification settings into appearance section 2021-09-07 13:30:25 +03:00
Vasiliy
da9bd1d420 Notifications about new streams 2021-09-07 13:30:16 +03:00
926 changed files with 22437 additions and 10083 deletions

View File

@@ -68,7 +68,7 @@ The [checkStyle](https://github.com/checkstyle/checkstyle) plugin verifies that
- 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.
- Under the "Use a local Checkstyle file" bullet, click on `Browse` and, enter `checkstyle` folder under the project's root path and pick the file named `checkstyle.xml`.
- 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.

1
.github/FUNDING.yml vendored
View File

@@ -1 +1,2 @@
liberapay: TeamNewPipe
custom: 'https://newpipe.net/donate/'

View File

@@ -1,65 +0,0 @@
---
name: Bug report
about: Create a bug report to help us improve
labels: bug
assignees: ''
---
<!--
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible.
-->
<!-- 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 am using the latest version - x.xx.x <!-- Check https://github.com/TeamNewPipe/NewPipe/releases -->
- [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
- [ ] This issue contains only one bug. I will open one issue for every bug report I want to file.
### Steps to reproduce the bug
<!--
1. Go to '...'
2. Press on '....'
3. Swipe down to '....'
-->
<!-- If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. -->
### Actual behavior
<!-- Tell us what happens with the steps given above. -->
### Expected behavior
<!-- Tell us what you expect to happen. -->
### Screenshots/Screen recordings
<!-- If applicable, add screenshots or a screen recording to help explain your problem. GitHub supports uploading them directly in the issue text box. If your file is too big for Github to accept, feel free to paste a link from an image/video hoster here instead. -->
<!-- DON'T POST SCREENSHOTS OF THE ERROR PAGE. Use the buttons given on the error page to paste the error as text in the Logs section below. -->
### Logs
<!-- If your bug includes a crash (where you're shown the Error Report page with a bunch of info), tap on "Copy formatted report" at the bottom and paste it here: -->
<!-- That's right, here! -->
<!-- Please fill this section if you did not provide a log generated by NewPipe -->
### Device info
- Android version/Custom ROM version:
- Device model:

113
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,113 @@
name: Bug report
description: Create a bug report to help us improve
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thank you for helping to make NewPipe better by reporting a bug. :hugs:
Please fill in as much information as possible about your bug so that we don't have to play "information ping-pong" and can help you immediately.
- type: checkboxes
id: checklist
attributes:
label: "Checklist"
options:
- label: "I am able to reproduce the bug with the [latest version](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
required: true
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
required: true
- label: "This issue contains only one bug."
required: true
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
required: true
- type: input
id: app-version
attributes:
label: Affected version
description: "In which NewPipe version did you encounter the bug?"
placeholder: "x.xx.x - Can be seen in the app from the 'About' section in the sidebar"
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce the bug
description: |
What did you do for the bug to show up?
If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.
placeholder: |
1. Go to '...'
2. Press on '....'
3. Swipe down to '....'
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: |
Tell us what you expect to happen.
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
description: |
Tell us what happens with the steps given above.
- type: textarea
id: screen-media
attributes:
label: Screenshots/Screen recordings
description: |
A picture or video is worth a thousand words.
If applicable, add screenshots or a screen recording to help explain your problem.
GitHub supports uploading them directly in the text box.
If your file is too big for Github to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead.
:heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE.
Instead, follow the instructions in the "Logs" section below.
- type: textarea
id: logs
attributes:
label: Logs
description: |
If your bug includes a crash (where you're shown the Error Report page with a bunch of info), tap on "Copy formatted report" at the bottom and paste it here.
- type: input
id: device-os-info
attributes:
label: Affected Android/Custom ROM version
description: |
With what operating system (+ version) did you encounter the bug?
placeholder: "Example: Android 12 / LineageOS 18.1"
- type: input
id: device-model-info
attributes:
label: Affected device model
description: |
On what device did you encounter the bug?
placeholder: "Example: Huawei P20 lite (ANE-LX1) / Samsung Galaxy S20"
- type: textarea
id: additional-information
attributes:
label: Additional information
description: |
Any other information you'd like to include, for instance that
* the affected device is foldable or a TV
* you have disabled all animations on your device
* your cat disabled your network connection
* ...

View File

@@ -1,24 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
labels: enhancement
assignees: ''
---
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you are currently unable to do so for any reason, 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 tab). -->
### 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 -->
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
- [ ] This issue contains only one feature request. I will open one issue for every feature I want to request.
#### What feature do you want?
<!-- Explain how you want the app's look or behavior to change to suit your needs. -->
#### Why do you want this feature?
<!-- Describe any problem or limitation you come across while using the app which would be solved by this feature. -->

View File

@@ -0,0 +1,51 @@
name: Feature request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thank you for helping to make NewPipe better by suggesting a feature. :hugs:
Your ideas are highly welcome! The app is made for you, the users, after all.
- type: checkboxes
id: checklist
attributes:
label: "Checklist"
options:
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
required: true
- label: "This issue contains only one feature request."
required: true
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
required: true
- type: textarea
id: feature-description
attributes:
label: Feature description
description: |
Explain how you want the app's look or behavior to change to suit your needs.
validations:
required: true
- type: textarea
id: why-is-the-feature-requested
attributes:
label: Why do you want this feature?
description: |
Describe any problem or limitation you come across while using the app which would be solved by this feature.
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: Additional information
description: Any other information you'd like to include, for instance sketches, mockups, pictures of cats, etc.

View File

@@ -1,24 +0,0 @@
---
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!* -->

35
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Question
description: Ask about anything NewPipe-related
labels: [question]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this issue! :hugs:
Note that you can also ask questions on our [IRC channel](https://web.libera.chat/#newpipe).
- type: checkboxes
id: checklist
attributes:
label: "Checklist"
options:
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
required: true
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
required: true
- type: textarea
id: what-is-the-question
attributes:
label: What is/are your question(s)?
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: Additional information
description: Any other information you'd like to include, for instance sketches, mockups, pictures of cats, etc.

View File

@@ -6,26 +6,33 @@ on:
branches:
- dev
- master
- release/**
paths-ignore:
- 'README*.md'
- 'README.md'
- 'doc/**'
- 'fastlane/**'
- 'assets/**'
- '.github/**/*.md'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
push:
branches:
- dev
- master
paths-ignore:
- 'README*.md'
- 'README.md'
- 'doc/**'
- 'fastlane/**'
- 'assets/**'
- '.github/**/*.md'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
jobs:
build-and-test-jvm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- name: create and checkout branch
@@ -34,17 +41,17 @@ jobs:
run: git checkout -B ${{ github.head_ref }}
- name: set up JDK 11
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
java-version: 11
distribution: "temurin"
cache: 'gradle'
- name: Build debug APK and run jvm tests
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
- name: Upload APK
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: app
path: app/build/outputs/apk/debug/*.apk
@@ -52,15 +59,16 @@ jobs:
test-android:
# macos has hardware acceleration. See android-emulator-runner action
runs-on: macos-latest
timeout-minutes: 20
strategy:
matrix:
# api-level 19 is min sdk, but throws errors related to desugaring
# api-level 19 is min sdk, but throws errors related to desugaring
api-level: [ 21, 29 ]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
java-version: 11
distribution: "temurin"
@@ -72,31 +80,38 @@ jobs:
api-level: ${{ matrix.api-level }}
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
emulator-build: 7425822
script: ./gradlew connectedCheck
script: ./gradlew connectedCheck --stacktrace
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
uses: actions/upload-artifact@v3
if: failure()
with:
name: android-test-report-api${{ matrix.api-level }}
path: app/build/reports/androidTests/connected/**
# 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
sonar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
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: "temurin"
# cache: 'gradle'
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: 11 # Sonar requires JDK 11
distribution: "temurin"
cache: 'gradle'
# - name: Cache SonarCloud packages
# uses: actions/cache@v2
# with:
# path: ~/.sonar/cache
# key: ${{ runner.os }}-sonar
# restore-keys: ${{ runner.os }}-sonar
- name: Cache SonarCloud packages
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
# - 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
- 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

130
.github/workflows/image-minimizer.js vendored Normal file
View File

@@ -0,0 +1,130 @@
/*
* Script for minimizing big images (jpg,gif,png) when they are uploaded to GitHub and not edited otherwise
*/
module.exports = async ({github, context}) => {
const IGNORE_KEY = '<!-- IGNORE IMAGE MINIFY -->';
const IGNORE_ALT_NAME_END = 'ignoreImageMinify';
// Targeted maximum height
const IMG_MAX_HEIGHT_PX = 600;
// maximum width of GitHub issues/comments
const IMG_MAX_WIDTH_PX = 800;
// all images that have a lower aspect ratio (-> have a smaller width) than this will be minimized
const MIN_ASPECT_RATIO = IMG_MAX_WIDTH_PX / IMG_MAX_HEIGHT_PX
// Get the body of the image
let initialBody = null;
if (context.eventName == 'issue_comment') {
initialBody = context.payload.comment.body;
} else if (context.eventName == 'issues') {
initialBody = context.payload.issue.body;
} else {
console.log('Aborting: No body found');
return;
}
console.log(`Found body: \n${initialBody}\n`);
// Check if we should ignore the currently processing element
if (initialBody.includes(IGNORE_KEY)) {
console.log('Ignoring: Body contains IGNORE_KEY');
return;
}
// Regex for finding images (simple variant) ![ALT_TEXT](https://*.githubusercontent.com/<number>/<variousHexStringsAnd->.<fileExtension>)
const REGEX_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
// Check if we found something
let foundSimpleImages = REGEX_IMAGE_LOOKUP.test(initialBody);
if (!foundSimpleImages) {
console.log('Found no simple images to process');
return;
}
console.log('Found at least one simple image to process');
// Require the probe lib for getting the image dimensions
const probe = require('probe-image-size');
var wasMatchModified = false;
// Try to find and replace the images with minimized ones
let newBody = await replaceAsync(initialBody, REGEX_IMAGE_LOOKUP, async (match, g1, g2) => {
console.log(`Found match '${match}'`);
if (g1.endsWith(IGNORE_ALT_NAME_END)) {
console.log(`Ignoring match '${match}': IGNORE_ALT_NAME_END`);
return match;
}
let shouldModify = false;
try {
console.log(`Probing ${g2}`);
let probeResult = await probe(g2);
if (probeResult == null) {
throw 'No probeResult';
}
if (probeResult.hUnits != 'px') {
throw `Unexpected probeResult.hUnits (expected px but got ${probeResult.hUnits})`;
}
if (probeResult.height <= 0) {
throw `Unexpected probeResult.height (height is invalid: ${probeResult.height})`;
}
if (probeResult.wUnits != 'px') {
throw `Unexpected probeResult.wUnits (expected px but got ${probeResult.wUnits})`;
}
if (probeResult.width <= 0) {
throw `Unexpected probeResult.width (width is invalid: ${probeResult.width})`;
}
console.log(`Probing resulted in ${probeResult.width}x${probeResult.height}px`);
shouldModify = probeResult.height > IMG_MAX_HEIGHT_PX && (probeResult.width / probeResult.height) < MIN_ASPECT_RATIO;
} catch(e) {
console.log('Probing failed:', e);
// Immediately abort
return match;
}
if (shouldModify) {
wasMatchModified = true;
console.log(`Modifying match '${match}'`);
return `<img alt="${g1}" src="${g2}" height=${IMG_MAX_HEIGHT_PX} />`;
}
console.log(`Match '${match}' is ok/will not be modified`);
return match;
});
if (!wasMatchModified) {
console.log('Nothing was modified. Skipping update');
return;
}
// Update the corresponding element
if (context.eventName == 'issue_comment') {
console.log('Updating comment with id', context.payload.comment.id);
await github.rest.issues.updateComment({
comment_id: context.payload.comment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: newBody
})
} else if (context.eventName == 'issues') {
console.log('Updating issue', context.payload.issue.number);
await github.rest.issues.update({
issue_number: context.payload.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: newBody
});
}
// Asnyc replace function from https://stackoverflow.com/a/48032528
async function replaceAsync(str, regex, asyncFn) {
const promises = [];
str.replace(regex, (match, ...args) => {
const promise = asyncFn(match, ...args);
promises.push(promise);
});
const data = await Promise.all(promises);
return str.replace(regex, () => data.shift());
}
}

29
.github/workflows/image-minimizer.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Image Minimizer
on:
issue_comment:
types: [created, edited]
issues:
types: [opened, edited]
jobs:
try-minimize:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install probe-image-size
run: npm i probe-image-size@7.2.3 --ignore-scripts
- name: Minimize simple images
uses: actions/github-script@v6
timeout-minutes: 3
with:
script: |
const script = require('.github/workflows/image-minimizer.js');
await script({github, context});

4
.gitignore vendored
View File

@@ -8,8 +8,8 @@ captures/
*~
.weblate
*.class
**/debug/
**/release/
app/debug/
app/release/
# vscode / eclipse files
*.classpath

View File

@@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -2,7 +2,7 @@
<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-en.svg" alt="Get it on F-Droid" height=80/></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>
@@ -17,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), [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).*
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [हिन्दी](doc/README.hi.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
@@ -140,7 +140,7 @@ Therefore, the app does not collect any data without your consent. NewPipe's pri
## License
[![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
NewPipe is Free Software: You can use, study, share, and improve it at
will. Specifically you can redistribute and/or modify it under the terms of the
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
published by the Free Software Foundation, either version 3 of the License, or

View File

@@ -1,24 +1,23 @@
plugins {
id "org.sonarqube" version "3.1.1"
id "com.android.application"
id "kotlin-android"
id "kotlin-kapt"
id "kotlin-parcelize"
id "checkstyle"
id "org.sonarqube" version "3.3"
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'checkstyle'
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
compileSdk 31
buildToolsVersion '31.0.0'
defaultConfig {
applicationId "org.schabi.newpipe"
resValue "string", "app_name", "NewPipe"
minSdkVersion 19
targetSdkVersion 29
versionCode 982
versionName "0.21.16"
minSdk 19
targetSdk 29
versionCode 989
versionName "0.23.3"
multiDexEnabled true
@@ -66,7 +65,7 @@ android {
}
}
lintOptions {
lint {
checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
@@ -80,13 +79,13 @@ android {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
encoding 'utf-8'
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
jvmTarget = JavaVersion.VERSION_11
}
sourceSets {
@@ -99,20 +98,22 @@ android {
}
ext {
checkstyleVersion = '8.38'
checkstyleVersion = '10.0'
androidxLifecycleVersion = '2.3.1'
androidxRoomVersion = '2.3.0'
androidxRoomVersion = '2.4.2'
androidxWorkVersion = '2.7.1'
icepickVersion = '3.2.0'
exoPlayerVersion = '2.14.2'
googleAutoServiceVersion = '1.0'
groupieVersion = '2.10.0'
exoPlayerVersion = '2.17.1'
googleAutoServiceVersion = '1.0.1'
groupieVersion = '2.10.1'
markwonVersion = '4.6.2'
leakCanaryVersion = '2.5'
stethoVersion = '1.6.0'
mockitoVersion = '3.6.0'
mockitoVersion = '4.0.0'
assertJVersion = '3.22.0'
}
configurations {
@@ -121,7 +122,7 @@ configurations {
}
checkstyle {
getConfigDirectory().set(rootProject.file("."))
getConfigDirectory().set(rootProject.file("checkstyle"))
ignoreFailures false
showViolations true
toolVersion = checkstyleVersion
@@ -189,11 +190,11 @@ dependencies {
// 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.13'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:6a858368c86bc9a55abee586eb6c733e86c26b97'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
ktlint 'com.pinterest:ktlint:0.40.0'
ktlint 'com.pinterest:ktlint:0.44.0'
/** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
@@ -201,16 +202,16 @@ dependencies {
/** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.media:media:1.4.3'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.media:media:1.5.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
@@ -220,7 +221,9 @@ dependencies {
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.webkit:webkit:1.4.0'
implementation 'com.google.android.material:material:1.2.1'
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
implementation 'com.google.android.material:material:1.5.0'
/** Third-party libraries **/
// Instance state boilerplate elimination
@@ -228,7 +231,7 @@ dependencies {
kapt "frankiesardo:icepick-processor:${icepickVersion}"
// HTML parser
implementation "org.jsoup:jsoup:1.13.1"
implementation "org.jsoup:jsoup:1.15.3"
// HTTP client
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
@@ -246,8 +249,6 @@ dependencies {
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
// Circular ImageView
implementation "de.hdodenhof:circleimageview:3.1.0"
// Image loading
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
implementation "com.squareup.picasso:picasso:2.8"
@@ -260,19 +261,19 @@ dependencies {
implementation "com.nononsenseapps:filepicker:4.2.1"
// Crash reporting
implementation "ch.acra:acra-core:5.7.0"
implementation "ch.acra:acra-core:5.9.3"
// Properly restarting
implementation 'com.jakewharton:process-phoenix:2.1.2'
// Reactive extensions for Java VM
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
implementation "io.reactivex.rxjava3:rxjava:3.0.13"
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
// RxJava binding APIs for Android UI widgets
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.1.Final"
implementation "org.ocpsoft.prettytime:prettytime:5.0.2.Final"
/** Debugging **/
// Memory leak detection
@@ -288,11 +289,10 @@ dependencies {
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0", {
exclude module: 'support-annotations'
}
androidTestImplementation "org.assertj:assertj-core:${assertJVersion}"
}
static String getGitWorkingBranch() {

View File

@@ -51,3 +51,6 @@
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
}
# for some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
-keep class org.schabi.newpipe.settings.notifications.** { *; }

View File

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

View File

@@ -0,0 +1,130 @@
package org.schabi.newpipe.database
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.schabi.newpipe.extractor.stream.StreamType
@RunWith(AndroidJUnit4::class)
class DatabaseMigrationTest {
companion object {
private const val DEFAULT_SERVICE_ID = 0
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
private const val DEFAULT_TITLE = "Test Title"
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
private const val DEFAULT_DURATION = 480L
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
private const val DEFAULT_SECOND_SERVICE_ID = 0
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
}
@get:Rule
val testHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrateDatabaseFrom2to3() {
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
databaseInV2.run {
insert(
"streams", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
put("title", DEFAULT_TITLE)
put("stream_type", DEFAULT_TYPE.name)
put("duration", DEFAULT_DURATION)
put("uploader", DEFAULT_UPLOADER_NAME)
put("thumbnail_url", DEFAULT_THUMBNAIL)
}
)
insert(
"streams", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
}
)
insert(
"streams", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
}
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
true, Migrations.MIGRATION_2_3
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_4,
true, Migrations.MIGRATION_3_4
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_5,
true, Migrations.MIGRATION_4_5
)
val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
// Only expect 2, the one with the null url will be ignored
assertEquals(2, listFromDB.size)
val streamFromMigratedDatabase = listFromDB[0]
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
assertNull(streamFromMigratedDatabase.viewCount)
assertNull(streamFromMigratedDatabase.textualUploadDate)
assertNull(streamFromMigratedDatabase.uploadDate)
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
val secondStreamFromMigratedDatabase = listFromDB[1]
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
assertEquals("", secondStreamFromMigratedDatabase.title)
// Should fallback to VIDEO_STREAM
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
assertEquals(0, secondStreamFromMigratedDatabase.duration)
assertEquals("", secondStreamFromMigratedDatabase.uploader)
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
assertNull(secondStreamFromMigratedDatabase.viewCount)
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
assertNull(secondStreamFromMigratedDatabase.uploadDate)
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
}
private fun getMigratedDatabase(): AppDatabase {
val database: AppDatabase = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java, AppDatabase.DATABASE_NAME
)
.build()
testHelper.closeWhenFinished(database)
return database
}
}

View File

@@ -0,0 +1,178 @@
package org.schabi.newpipe.local.history
import androidx.test.core.app.ApplicationProvider
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.schabi.newpipe.database.AppDatabase
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
import org.schabi.newpipe.testUtil.TestDatabase
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
class HistoryRecordManagerTest {
private lateinit var manager: HistoryRecordManager
private lateinit var database: AppDatabase
@get:Rule
val trampolineScheduler = TrampolineSchedulerRule()
@Before
fun setup() {
database = TestDatabase.createReplacingNewPipeDatabase()
manager = HistoryRecordManager(ApplicationProvider.getApplicationContext())
}
@After
fun cleanUp() {
database.close()
}
@Test
fun onSearched() {
manager.onSearched(0, "Hello").test().await().assertValue(1)
// For some reason the Flowable returned by getAll() never completes, so we can't assert
// that the number of Lists it returns is exactly 1, we can only check if the first List is
// correct. Why on earth has a Flowable been used instead of a Single for getAll()?!?
val entities = database.searchHistoryDAO().all.blockingFirst()
assertThat(entities).hasSize(1)
assertThat(entities[0].id).isEqualTo(1)
assertThat(entities[0].serviceId).isEqualTo(0)
assertThat(entities[0].search).isEqualTo("Hello")
}
@Test
fun deleteSearchHistory() {
val entries = listOf(
SearchHistoryEntry(time.minusSeconds(1), 0, "A"),
SearchHistoryEntry(time.minusSeconds(2), 2, "A"),
SearchHistoryEntry(time.minusSeconds(3), 1, "B"),
SearchHistoryEntry(time.minusSeconds(4), 0, "B"),
)
// make sure all 4 were inserted
database.searchHistoryDAO().insertAll(entries)
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
// try to delete only "A" entries, "B" entries should be untouched
manager.deleteSearchHistory("A").test().await().assertValue(2)
val entities = database.searchHistoryDAO().all.blockingFirst()
assertThat(entities).hasSize(2)
assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
.containsExactly(*entries.subList(2, 4).toTypedArray())
// assert that nothing happens if we delete a search query that does exist in the db
manager.deleteSearchHistory("A").test().await().assertValue(0)
val entities2 = database.searchHistoryDAO().all.blockingFirst()
assertThat(entities2).hasSize(2)
assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
.containsExactly(*entries.subList(2, 4).toTypedArray())
// delete all remaining entries
manager.deleteSearchHistory("B").test().await().assertValue(2)
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
}
@Test
fun deleteCompleteSearchHistory() {
val entries = listOf(
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
SearchHistoryEntry(time.minusSeconds(2), 2, "B"),
SearchHistoryEntry(time.minusSeconds(3), 0, "C"),
)
// make sure all 3 were inserted
database.searchHistoryDAO().insertAll(entries)
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
// should remove everything
manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
}
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
// shuffle to make sure the order of items returned by queries depends only on
// SearchHistoryEntry.creationDate, not on the actual insertion time, so that we can
// verify that the `ORDER BY` clause does its job
database.searchHistoryDAO().insertAll(relatedSearches.shuffled())
// make sure all entries were inserted
assertEquals(
relatedSearches.size,
database.searchHistoryDAO().all.blockingFirst().size
)
}
@Test
fun getRelatedSearches_emptyQuery() {
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
// make sure correct number of searches is returned and in correct order
val searches = manager.getRelatedSearches("", 6, 4).blockingFirst()
assertThat(searches).containsExactly(
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
RELATED_SEARCHES_ENTRIES[4].search, // B
RELATED_SEARCHES_ENTRIES[5].search, // AA
RELATED_SEARCHES_ENTRIES[2].search, // BA
)
}
@Test
fun getRelatedSearches_emptyQuery_manyDuplicates() {
insertShuffledRelatedSearches(
listOf(
SearchHistoryEntry(time.minusSeconds(9), 3, "A"),
SearchHistoryEntry(time.minusSeconds(8), 3, "AB"),
SearchHistoryEntry(time.minusSeconds(7), 3, "A"),
SearchHistoryEntry(time.minusSeconds(6), 3, "A"),
SearchHistoryEntry(time.minusSeconds(5), 3, "BA"),
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
SearchHistoryEntry(time.minusSeconds(3), 3, "A"),
SearchHistoryEntry(time.minusSeconds(2), 0, "A"),
SearchHistoryEntry(time.minusSeconds(1), 2, "AA"),
)
)
val searches = manager.getRelatedSearches("", 9, 3).blockingFirst()
assertThat(searches).containsExactly("AA", "A", "BA")
}
@Test
fun getRelatedSearched_nonEmptyQuery() {
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
// make sure correct number of searches is returned and in correct order
val searches = manager.getRelatedSearches("A", 3, 5).blockingFirst()
assertThat(searches).containsExactly(
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
RELATED_SEARCHES_ENTRIES[5].search, // AA
RELATED_SEARCHES_ENTRIES[1].search, // BA
)
// also make sure that the string comparison is case insensitive
val searches2 = manager.getRelatedSearches("a", 3, 5).blockingFirst()
assertThat(searches).isEqualTo(searches2)
}
companion object {
private val time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC)
private val RELATED_SEARCHES_ENTRIES = listOf(
SearchHistoryEntry(time.minusSeconds(7), 2, "AC"),
SearchHistoryEntry(time.minusSeconds(6), 0, "ABC"),
SearchHistoryEntry(time.minusSeconds(5), 1, "BA"),
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
SearchHistoryEntry(time.minusSeconds(2), 0, "B"),
SearchHistoryEntry(time.minusSeconds(3), 2, "AA"),
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
)
}
}

View File

@@ -1,17 +1,14 @@
package org.schabi.newpipe.local.playlist
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.Timeout
import org.schabi.newpipe.database.AppDatabase
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.testUtil.TestDatabase
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
import java.util.concurrent.TimeUnit
class LocalPlaylistManagerTest {
@@ -21,18 +18,9 @@ class LocalPlaylistManagerTest {
@get:Rule
val trampolineScheduler = TrampolineSchedulerRule()
@get:Rule
val timeout = Timeout(10, TimeUnit.SECONDS)
@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
)
.allowMainThreadQueries()
.build()
database = TestDatabase.createReplacingNewPipeDatabase()
manager = LocalPlaylistManager(database)
}

View File

@@ -0,0 +1,32 @@
package org.schabi.newpipe.testUtil
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertSame
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.AppDatabase
class TestDatabase {
companion object {
fun createReplacingNewPipeDatabase(): AppDatabase {
val database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
)
.allowMainThreadQueries()
.build()
val databaseField = NewPipeDatabase::class.java.getDeclaredField("databaseInstance")
databaseField.isAccessible = true
databaseField.set(NewPipeDatabase::class, database)
assertSame(
"Mocking database failed!",
database,
NewPipeDatabase.getInstance(ApplicationProvider.getApplicationContext())
)
return database
}
}
}

View File

@@ -0,0 +1,214 @@
package org.schabi.newpipe.util
import android.content.Context
import android.util.SparseArray
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.Spinner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.extractor.stream.AudioStream
import org.schabi.newpipe.extractor.stream.Stream
import org.schabi.newpipe.extractor.stream.SubtitlesStream
import org.schabi.newpipe.extractor.stream.VideoStream
@MediumTest
@RunWith(AndroidJUnit4::class)
class StreamItemAdapterTest {
private lateinit var context: Context
private lateinit var spinner: Spinner
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
UiThreadStatement.runOnUiThread {
spinner = Spinner(context)
}
}
@Test
fun videoStreams_noSecondaryStream() {
val adapter = StreamItemAdapter<VideoStream, AudioStream>(
context,
getVideoStreams(true, true, true, true),
null
)
spinner.adapter = adapter
assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
assertIconVisibility(spinner, 1, VISIBLE, VISIBLE)
assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
}
@Test
fun videoStreams_hasSecondaryStream() {
val adapter = StreamItemAdapter(
context,
getVideoStreams(false, true, false, true),
getAudioStreams(false, true, false, true)
)
spinner.adapter = adapter
assertIconVisibility(spinner, 0, GONE, GONE)
assertIconVisibility(spinner, 1, GONE, GONE)
assertIconVisibility(spinner, 2, GONE, GONE)
assertIconVisibility(spinner, 3, GONE, GONE)
}
@Test
fun videoStreams_Mixed() {
val adapter = StreamItemAdapter(
context,
getVideoStreams(true, true, true, true, true, false, true, true),
getAudioStreams(false, true, false, false, false, true, true, true)
)
spinner.adapter = adapter
assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
assertIconVisibility(spinner, 1, GONE, INVISIBLE)
assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
assertIconVisibility(spinner, 4, VISIBLE, VISIBLE)
assertIconVisibility(spinner, 5, GONE, INVISIBLE)
assertIconVisibility(spinner, 6, GONE, INVISIBLE)
assertIconVisibility(spinner, 7, GONE, INVISIBLE)
}
@Test
fun subtitleStreams_noIcon() {
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
context,
StreamItemAdapter.StreamSizeWrapper(
(0 until 5).map {
SubtitlesStream.Builder()
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.SRT)
.setLanguageCode("pt-BR")
.setAutoGenerated(false)
.build()
},
context
),
null
)
spinner.adapter = adapter
for (i in 0 until spinner.count) {
assertIconVisibility(spinner, i, GONE, GONE)
}
}
@Test
fun audioStreams_noIcon() {
val adapter = StreamItemAdapter<AudioStream, Stream>(
context,
StreamItemAdapter.StreamSizeWrapper(
(0 until 5).map {
AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com/$it", true)
.setMediaFormat(MediaFormat.OPUS)
.setAverageBitrate(192)
.build()
},
context
),
null
)
spinner.adapter = adapter
for (i in 0 until spinner.count) {
assertIconVisibility(spinner, i, GONE, GONE)
}
}
/**
* @return a list of video streams, in which their video only property mirrors the provided
* [videoOnly] vararg.
*/
private fun getVideoStreams(vararg videoOnly: Boolean) =
StreamItemAdapter.StreamSizeWrapper(
videoOnly.map {
VideoStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.MPEG_4)
.setResolution("720p")
.setIsVideoOnly(it)
.build()
},
context
)
/**
* @return a list of audio streams, containing valid and null elements mirroring the provided
* [shouldBeValid] vararg.
*/
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
getSecondaryStreamsFromList(
shouldBeValid.map {
if (it) {
AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com", true)
.setMediaFormat(MediaFormat.OPUS)
.setAverageBitrate(192)
.build()
} else {
null
}
}
)
/**
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
*/
private fun assertIconVisibility(
spinner: Spinner,
position: Int,
normalVisibility: Int,
dropDownVisibility: Int
) {
spinner.setSelection(position)
spinner.adapter.getView(position, null, spinner).run {
Assert.assertEquals(
"normal visibility (pos=[$position]) is not correct",
findViewById<View>(R.id.wo_sound_icon).visibility,
normalVisibility,
)
}
spinner.adapter.getDropDownView(position, null, spinner).run {
Assert.assertEquals(
"drop down visibility (pos=[$position]) is not correct",
findViewById<View>(R.id.wo_sound_icon).visibility,
dropDownVisibility
)
}
}
/**
* Helper function that builds a secondary stream list.
*/
private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) =
SparseArray<SecondaryStreamHelper<T>?>(streams.size).apply {
streams.forEachIndexed { index, stream ->
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
SecondaryStreamHelper(
StreamItemAdapter.StreamSizeWrapper(streams, context),
it
)
}
put(index, secondaryStreamHelper)
}
}
}

View File

@@ -0,0 +1,20 @@
package org.schabi.newpipe.settings;
import android.content.Intent;
import leakcanary.LeakCanary;
/**
* Build variant dependent (BVD) leak canary API implementation for the debug settings fragment.
* This class is loaded via reflection by
* {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}.
*/
@SuppressWarnings("unused") // Class is used but loaded via reflection
public class DebugSettingsBVDLeakCanary
implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI {
@Override
public Intent getNewLeakDisplayActivityIntent() {
return LeakCanary.INSTANCE.newLeakDisplayActivityIntent();
}
}

View File

@@ -1,42 +0,0 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
import androidx.preference.Preference;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.PicassoHelper;
import leakcanary.LeakCanary;
public class DebugSettingsFragment extends BasePreferenceFragment {
@Override
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));
final Preference showImageIndicatorsPreference
= findPreference(getString(R.string.show_image_indicators_key));
final Preference crashTheAppPreference
= findPreference(getString(R.string.crash_the_app_key));
assert showMemoryLeaksPreference != null;
assert showImageIndicatorsPreference != null;
assert crashTheAppPreference != null;
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
return true;
});
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
return true;
});
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
throw new RuntimeException();
});
}
}

View File

@@ -338,6 +338,7 @@
<data android:host="video.ploud.fr" />
<data android:host="video.lqdn.fr" />
<data android:host="skeptikon.fr" />
<data android:host="media.fsfe.org" />
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
@@ -380,9 +381,6 @@
<service
android:name=".RouterActivity$FetcherService"
android:exported="false" />
<service
android:name=".CheckForNewAppVersion"
android:exported="false" />
<!-- opting out of sending metrics to Google in Android System WebView -->
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />

View File

@@ -63,6 +63,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
return consumed == dy;
}
@Override
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final AppBarLayout child,
@NonNull final MotionEvent ev) {

View File

@@ -0,0 +1,148 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.text.similarity;
import java.util.Locale;
/**
* A matching algorithm that is similar to the searching algorithms implemented in editors such
* as Sublime Text, TextMate, Atom and others.
*
* <p>
* One point is given for every matched character. Subsequent matches yield two bonus points.
* A higher score indicates a higher similarity.
* </p>
*
* <p>
* This code has been adapted from Apache Commons Lang 3.3.
* </p>
*
* @since 1.0
*
* Note: This class was forked from
* <a href="https://git.io/JyYJg">
* apache/commons-text (8cfdafc) FuzzyScore.java
* </a>
*/
public class FuzzyScore {
/**
* Locale used to change the case of text.
*/
private final Locale locale;
/**
* This returns a {@link Locale}-specific {@link FuzzyScore}.
*
* @param locale The string matching logic is case insensitive.
A {@link Locale} is necessary to normalize both Strings to lower case.
* @throws IllegalArgumentException
* This is thrown if the {@link Locale} parameter is {@code null}.
*/
public FuzzyScore(final Locale locale) {
if (locale == null) {
throw new IllegalArgumentException("Locale must not be null");
}
this.locale = locale;
}
/**
* Find the Fuzzy Score which indicates the similarity score between two
* Strings.
*
* <pre>
* score.fuzzyScore(null, null) = IllegalArgumentException
* score.fuzzyScore("not null", null) = IllegalArgumentException
* score.fuzzyScore(null, "not null") = IllegalArgumentException
* score.fuzzyScore("", "") = 0
* score.fuzzyScore("Workshop", "b") = 0
* score.fuzzyScore("Room", "o") = 1
* score.fuzzyScore("Workshop", "w") = 1
* score.fuzzyScore("Workshop", "ws") = 2
* score.fuzzyScore("Workshop", "wo") = 4
* score.fuzzyScore("Apache Software Foundation", "asf") = 3
* </pre>
*
* @param term a full term that should be matched against, must not be null
* @param query the query that will be matched against a term, must not be
* null
* @return result score
* @throws IllegalArgumentException if the term or query is {@code null}
*/
public Integer fuzzyScore(final CharSequence term, final CharSequence query) {
if (term == null || query == null) {
throw new IllegalArgumentException("CharSequences must not be null");
}
// fuzzy logic is case insensitive. We normalize the Strings to lower
// case right from the start. Turning characters to lower case
// via Character.toLowerCase(char) is unfortunately insufficient
// as it does not accept a locale.
final String termLowerCase = term.toString().toLowerCase(locale);
final String queryLowerCase = query.toString().toLowerCase(locale);
// the resulting score
int score = 0;
// the position in the term which will be scanned next for potential
// query character matches
int termIndex = 0;
// index of the previously matched character in the term
int previousMatchingCharacterIndex = Integer.MIN_VALUE;
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) {
final char queryChar = queryLowerCase.charAt(queryIndex);
boolean termCharacterMatchFound = false;
for (; termIndex < termLowerCase.length()
&& !termCharacterMatchFound; termIndex++) {
final char termChar = termLowerCase.charAt(termIndex);
if (queryChar == termChar) {
// simple character matches result in one point
score++;
// subsequent character matches further improve
// the score.
if (previousMatchingCharacterIndex + 1 == termIndex) {
score += 2;
}
previousMatchingCharacterIndex = termIndex;
// we can leave the nested loop. Every character in the
// query can match at most one character in the term.
termCharacterMatchFound = true;
}
}
}
return score;
}
/**
* Gets the locale.
*
* @return The locale
*/
public Locale getLocale() {
return locale;
}
}

View File

@@ -13,13 +13,8 @@ import androidx.preference.PreferenceManager;
import com.jakewharton.processphoenix.ProcessPhoenix;
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;
@@ -32,7 +27,7 @@ import org.schabi.newpipe.util.StateSaver;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -210,45 +205,52 @@ public class App extends MultiDexApplication {
return;
}
try {
final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
.setBuildConfigClass(BuildConfig.class)
.build();
ACRA.init(this, acraConfig);
} catch (final ACRAConfigurationException exception) {
exception.printStackTrace();
ErrorActivity.reportError(this, new ErrorInfo(exception,
UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report"));
}
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
.withBuildConfigClass(BuildConfig.class);
ACRA.init(this, acraConfig);
}
private void initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
final NotificationChannelCompat mainChannel = new NotificationChannelCompat
final List<NotificationChannelCompat> notificationChannelCompats = new ArrayList<>();
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description))
.build();
.build());
final NotificationChannelCompat appUpdateChannel = new NotificationChannelCompat
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.app_update_notification_channel_name))
.setDescription(getString(R.string.app_update_notification_channel_description))
.build();
.build());
final NotificationChannelCompat hashChannel = new NotificationChannelCompat
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH)
NotificationManagerCompat.IMPORTANCE_HIGH)
.setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description))
.build();
.build());
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build());
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(getString(R.string.streams_notification_channel_name))
.setDescription(getString(R.string.streams_notification_channel_description))
.build());
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.createNotificationChannelsCompat(Arrays.asList(mainChannel,
appUpdateChannel, hashChannel));
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
}
protected boolean isDisposedRxExceptionsReported() {

View File

@@ -1,264 +0,0 @@
package org.schabi.newpipe;
import android.app.Application;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
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.pm.PackageInfoCompat;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;
public final class CheckForNewAppVersion extends IntentService {
public CheckForNewAppVersion() {
super("CheckForNewAppVersion");
}
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
// Public key of the certificate that is used in NewPipe release versions
private static final String RELEASE_CERT_PUBLIC_KEY_SHA1
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
/**
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
*
* @param application The application
* @return String with the APK's SHA1 fingerprint in hexadecimal
*/
@NonNull
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
final List<Signature> signatures;
try {
signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
application.getPackageName());
} catch (final PackageManager.NameNotFoundException e) {
ErrorActivity.reportError(application, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
return "";
}
if (signatures.isEmpty()) {
return "";
}
final X509Certificate c;
try {
final byte[] cert = signatures.get(0).toByteArray();
final InputStream input = new ByteArrayInputStream(cert);
final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input);
} catch (final CertificateException e) {
ErrorActivity.reportError(application, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
return "";
}
try {
final MessageDigest md = MessageDigest.getInstance("SHA1");
final byte[] publicKey = md.digest(c.getEncoded());
return byte2HexFormatted(publicKey);
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
ErrorActivity.reportError(application, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
return "";
}
}
private static String byte2HexFormatted(final byte[] arr) {
final StringBuilder str = new StringBuilder(arr.length * 2);
for (int i = 0; i < arr.length; i++) {
String h = Integer.toHexString(arr[i]);
final int l = h.length();
if (l == 1) {
h = "0" + h;
}
if (l > 2) {
h = h.substring(l - 2, l);
}
str.append(h.toUpperCase());
if (i < (arr.length - 1)) {
str.append(':');
}
}
return str.toString();
}
/**
* Method to compare the current and latest available app version.
* If a newer version is available, we show the update notification.
*
* @param application The application
* @param versionName Name of new version
* @param apkLocationUrl Url with the new apk
* @param versionCode Code of new version
*/
private static void compareAppVersionAndShowNotification(@NonNull final Application application,
final String versionName,
final String apkLocationUrl,
final int versionCode) {
if (BuildConfig.VERSION_CODE >= versionCode) {
return;
}
// A pending intent to open the apk location url in the browser.
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);
final String channelId = application
.getString(R.string.app_update_notification_channel_id);
final NotificationCompat.Builder notificationBuilder
= new NotificationCompat.Builder(application, channelId)
.setSmallIcon(R.drawable.ic_newpipe_update)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setContentTitle(application
.getString(R.string.app_update_notification_content_title))
.setContentText(application
.getString(R.string.app_update_notification_content_text)
+ " " + versionName);
final NotificationManagerCompat notificationManager
= NotificationManagerCompat.from(application);
notificationManager.notify(2000, notificationBuilder.build());
}
public static boolean isReleaseApk(@NonNull final App app) {
return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1);
}
private void checkNewVersion() throws IOException, ReCaptchaException {
final App app = App.getApp();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
final NewVersionManager manager = new NewVersionManager();
// Check if the current apk is a github one or not.
if (!isReleaseApk(app)) {
return;
}
// Check if the last request has happened a certain time ago
// to reduce the number of API requests.
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
if (!manager.isExpired(expiry)) {
return;
}
// Make a network request to get latest NewPipe data.
final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
handleResponse(response, manager, prefs, app);
}
private void handleResponse(@NonNull final Response response,
@NonNull final NewVersionManager manager,
@NonNull final SharedPreferences prefs,
@NonNull final App app) {
try {
// Store a timestamp which needs to be exceeded,
// before a new request to the API is made.
final long newExpiry = manager
.coerceExpiry(response.getHeader("expires"));
prefs.edit()
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
.apply();
} catch (final Exception e) {
if (DEBUG) {
Log.w(TAG, "Could not extract and save new expiry date", e);
}
}
// Parse the json from the response.
try {
final JsonObject githubStableObject = JsonParser.object()
.from(response.responseBody()).getObject("flavors")
.getObject("github").getObject("stable");
final String versionName = githubStableObject
.getString("version");
final int versionCode = githubStableObject
.getInt("version_code");
final String apkLocationUrl = githubStableObject
.getString("apk");
compareAppVersionAndShowNotification(app, versionName,
apkLocationUrl, versionCode);
} catch (final JsonParserException e) {
// Most likely something is wrong in data received from NEWPIPE_API_URL.
// Do not alarm user and fail silently.
if (DEBUG) {
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
}
}
}
/**
* Start a new service which
* checks if all conditions for performing a version check are met,
* fetches the API endpoint {@link #NEWPIPE_API_URL} containing info
* about the latest NewPipe version
* and displays a notification about ana available update.
* <br>
* Following conditions need to be met, before data is request from the server:
* <ul>
* <li> The app is signed with the correct signing key (by TeamNewPipe / schabi).
* If the signing key differs from the one used upstream, the update cannot be installed.</li>
* <li>The user enabled searching for and notifying about updates in the settings.</li>
* <li>The app did not recently check for updates.
* We do not want to make unnecessary connections and DOS our servers.</li>
* </ul>
* <b>Must not be executed</b> when the app is in background.
*/
public static void startNewVersionCheckService() {
final Intent intent = new Intent(App.getApp().getApplicationContext(),
CheckForNewAppVersion.class);
App.getApp().startService(intent);
}
@Override
protected void onHandleIntent(@Nullable final Intent intent) {
try {
checkNewVersion();
} catch (final IOException e) {
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e);
} catch (final ReCaptchaException e) {
Log.e(TAG, "ReCaptchaException should never happen here.", e);
}
}
}

View File

@@ -43,7 +43,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; rv:78.0) Gecko/20100101 Firefox/78.0";
= "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.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

@@ -20,7 +20,6 @@
package org.schabi.newpipe;
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.content.BroadcastReceiver;
@@ -63,7 +62,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.error.ErrorUtil;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@@ -72,6 +71,7 @@ import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder;
@@ -157,13 +157,16 @@ public class MainActivity extends AppCompatActivity {
try {
setupDrawer();
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e);
ErrorUtil.showUiErrorSnackbar(this, "Setting up drawer", e);
}
if (DeviceUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(this);
}
openMiniPlayerUponPlayerStarted();
// Schedule worker for checking for new streams and creating corresponding notifications
// if this is enabled by the user.
NotificationWorker.initialize(this);
}
@Override
@@ -174,10 +177,9 @@ public class MainActivity extends AppCompatActivity {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
// Start the service which is checking all conditions
// Start the worker which is checking all conditions
// and eventually searching for a new version.
// The service searching for a new NewPipe version must not be started in background.
startNewVersionCheckService();
NewVersionWorker.enqueueNewVersionCheckingWork(app);
}
}
@@ -214,7 +216,7 @@ public class MainActivity extends AppCompatActivity {
/**
* Builds the drawer menu for the current service.
*
* @throws ExtractionException
* @throws ExtractionException if the service didn't provide available kiosks
*/
private void addDrawerMenuForCurrentService() throws ExtractionException {
//Tabs
@@ -227,7 +229,7 @@ public class MainActivity extends AppCompatActivity {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks, this));
.setIcon(KioskTranslator.getKioskIcon(ks));
kioskId++;
}
@@ -266,7 +268,7 @@ public class MainActivity extends AppCompatActivity {
try {
tabSelected(item);
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e);
ErrorUtil.showUiErrorSnackbar(this, "Selecting main page tab", e);
}
break;
case R.id.menu_options_about_group:
@@ -372,7 +374,7 @@ public class MainActivity extends AppCompatActivity {
try {
addDrawerMenuForCurrentService();
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e);
ErrorUtil.showUiErrorSnackbar(this, "Showing main page tabs", e);
}
}
}
@@ -475,7 +477,7 @@ public class MainActivity extends AppCompatActivity {
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
getString(R.string.drawer_header_description) + selectedServiceName);
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up service toggle", e);
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
}
final SharedPreferences sharedPreferences
@@ -719,7 +721,7 @@ public class MainActivity extends AppCompatActivity {
if (toggle != null) {
toggle.syncState();
toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> mainBinding.getRoot()
.openDrawer(GravityCompat.START));
.open());
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
}
} else {
@@ -785,7 +787,7 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
}
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Handling intent", e);
ErrorUtil.showUiErrorSnackbar(this, "Handling intent", e);
}
}

View File

@@ -1,5 +1,11 @@
package org.schabi.newpipe;
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
import android.content.Context;
import android.database.Cursor;
@@ -8,11 +14,6 @@ import androidx.room.Room;
import org.schabi.newpipe.database.AppDatabase;
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
public final class NewPipeDatabase {
private static volatile AppDatabase databaseInstance;
@@ -23,7 +24,7 @@ public final class NewPipeDatabase {
private static AppDatabase getDatabase(final Context context) {
return Room
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
.build();
}

View File

@@ -1,28 +0,0 @@
package org.schabi.newpipe
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
class NewVersionManager {
fun isExpired(expiry: Long): Boolean {
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
}
/**
* Coerce expiry date time in between 6 hours and 72 hours from now
*
* @return Epoch second of expiry date time
*/
fun coerceExpiry(expiryString: String?): Long {
val now = ZonedDateTime.now()
return expiryString?.let {
var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
expiry = maxOf(expiry, now.plusHours(6))
expiry = minOf(expiry, now.plusHours(72))
expiry.toEpochSecond()
} ?: now.plusHours(6).toEpochSecond()
}
}

View File

@@ -0,0 +1,163 @@
package org.schabi.newpipe
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkRequest
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
import java.io.IOException
class NewVersionWorker(
context: Context,
workerParams: WorkerParameters
) : Worker(context, workerParams) {
/**
* Method to compare the current and latest available app version.
* If a newer version is available, we show the update notification.
*
* @param versionName Name of new version
* @param apkLocationUrl Url with the new apk
* @param versionCode Code of new version
*/
private fun compareAppVersionAndShowNotification(
versionName: String,
apkLocationUrl: String?,
versionCode: Int
) {
if (BuildConfig.VERSION_CODE >= versionCode) {
return
}
val app = App.getApp()
// A pending intent to open the apk location url in the browser.
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(app, 0, intent, 0)
val channelId = app.getString(R.string.app_update_notification_channel_id)
val notificationBuilder = NotificationCompat.Builder(app, channelId)
.setSmallIcon(R.drawable.ic_newpipe_update)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setContentTitle(app.getString(R.string.app_update_notification_content_title))
.setContentText(
app.getString(R.string.app_update_notification_content_text) +
" " + versionName
)
val notificationManager = NotificationManagerCompat.from(app)
notificationManager.notify(2000, notificationBuilder.build())
}
@Throws(IOException::class, ReCaptchaException::class)
private fun checkNewVersion() {
// Check if the current apk is a github one or not.
if (!isReleaseApk()) {
return
}
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
// Check if the last request has happened a certain time ago
// to reduce the number of API requests.
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
if (!isLastUpdateCheckExpired(expiry)) {
return
}
// Make a network request to get latest NewPipe data.
val response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL)
handleResponse(response)
}
private fun handleResponse(response: Response) {
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
try {
// Store a timestamp which needs to be exceeded,
// before a new request to the API is made.
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
prefs.edit {
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
}
} catch (e: Exception) {
if (DEBUG) {
Log.w(TAG, "Could not extract and save new expiry date", e)
}
}
// Parse the json from the response.
try {
val githubStableObject = JsonParser.`object`()
.from(response.responseBody()).getObject("flavors")
.getObject("github").getObject("stable")
val versionName = githubStableObject.getString("version")
val versionCode = githubStableObject.getInt("version_code")
val apkLocationUrl = githubStableObject.getString("apk")
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
} catch (e: JsonParserException) {
// Most likely something is wrong in data received from NEWPIPE_API_URL.
// Do not alarm user and fail silently.
if (DEBUG) {
Log.w(TAG, "Could not get NewPipe API: invalid json", e)
}
}
}
override fun doWork(): Result {
try {
checkNewVersion()
} catch (e: IOException) {
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e)
return Result.failure()
} catch (e: ReCaptchaException) {
Log.e(TAG, "ReCaptchaException should never happen here.", e)
return Result.failure()
}
return Result.success()
}
companion object {
private val DEBUG = MainActivity.DEBUG
private val TAG = NewVersionWorker::class.java.simpleName
private const val NEWPIPE_API_URL = "https://newpipe.net/api/data.json"
/**
* Start a new worker which
* checks if all conditions for performing a version check are met,
* fetches the API endpoint [.NEWPIPE_API_URL] containing info
* about the latest NewPipe version
* and displays a notification about ana available update.
* <br></br>
* Following conditions need to be met, before data is request from the server:
*
* * The app is signed with the correct signing key (by TeamNewPipe / schabi).
* If the signing key differs from the one used upstream, the update cannot be installed.
* * The user enabled searching for and notifying about updates in the settings.
* * The app did not recently check for updates.
* We do not want to make unnecessary connections and DOS our servers.
*
*/
@JvmStatic
fun enqueueNewVersionCheckingWork(context: Context) {
val workRequest: WorkRequest =
OneTimeWorkRequest.Builder(NewVersionWorker::class.java).build()
WorkManager.getInstance(context).enqueue(workRequest)
}
}
}

View File

@@ -14,6 +14,7 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SparseItemUtil;
import java.util.Collections;
@@ -61,11 +62,14 @@ public final class QueueItemMenuUtil {
return true;
case R.id.menu_item_channel_details:
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
NavigationHelper.openChannelFragmentUsingIntent(context, item.getServiceId(),
item.getUploaderUrl(), item.getUploader());
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),

View File

@@ -24,12 +24,12 @@ import android.widget.Toast;
import androidx.annotation.DrawableRes;
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.core.app.NotificationCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.widget.TextViewCompat;
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
@@ -37,8 +37,8 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
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.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.Info;
@@ -58,7 +58,6 @@ 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.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer;
@@ -71,7 +70,7 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
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.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper;
@@ -127,8 +126,10 @@ public class RouterActivity extends AppCompatActivity {
}
}
ThemeHelper.setDayNightMode(this);
setTheme(ThemeHelper.isLightThemeSelected(this)
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
Localization.assureCorrectAppLanguage(this);
}
@Override
@@ -231,7 +232,7 @@ public class RouterActivity extends AppCompatActivity {
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
} else {
ErrorActivity.reportError(context, errorInfo);
ErrorUtil.createNotification(context, errorInfo);
}
if (context instanceof RouterActivity) {
@@ -257,80 +258,122 @@ public class RouterActivity extends AppCompatActivity {
protected void onSuccess() {
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(this);
final String selectedChoiceKey = preferences
.getString(getString(R.string.preferred_open_action_key),
getString(R.string.preferred_open_action_default));
final String showInfoKey = getString(R.string.show_info_key);
final String videoPlayerKey = getString(R.string.video_player_key);
final String backgroundPlayerKey = getString(R.string.background_player_key);
final String popupPlayerKey = getString(R.string.popup_player_key);
final String downloadKey = getString(R.string.download_key);
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
final ChoiceAvailabilityChecker choiceChecker = new ChoiceAvailabilityChecker(
getChoicesForService(currentService, currentLinkType),
preferences.getString(getString(R.string.preferred_open_action_key),
getString(R.string.preferred_open_action_default)));
if (selectedChoiceKey.equals(alwaysAskKey)) {
final List<AdapterChoiceItem> choices
= getChoicesForService(currentService, currentLinkType);
// Check for non-player related choices
if (choiceChecker.isAvailableAndSelected(
R.string.show_info_key,
R.string.download_key,
R.string.add_to_playlist_key)) {
handleChoice(choiceChecker.getSelectedChoiceKey());
return;
}
// Check if the choice is player related
if (choiceChecker.isAvailableAndSelected(
R.string.video_player_key,
R.string.background_player_key,
R.string.popup_player_key)) {
final String selectedChoice = choiceChecker.getSelectedChoiceKey();
switch (choices.size()) {
case 1:
handleChoice(choices.get(0).key);
break;
case 0:
handleChoice(showInfoKey);
break;
default:
showDialog(choices);
break;
}
} else if (selectedChoiceKey.equals(showInfoKey)) {
handleChoice(showInfoKey);
} else if (selectedChoiceKey.equals(downloadKey)) {
handleChoice(downloadKey);
} else {
final boolean isExtVideoEnabled = preferences.getBoolean(
getString(R.string.use_external_video_player_key), false);
final boolean isExtAudioEnabled = preferences.getBoolean(
getString(R.string.use_external_audio_player_key), false);
final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey)
|| selectedChoiceKey.equals(popupPlayerKey);
final boolean isAudioPlayerSelected = selectedChoiceKey.equals(backgroundPlayerKey);
final boolean isVideoPlayerSelected =
selectedChoice.equals(getString(R.string.video_player_key))
|| selectedChoice.equals(getString(R.string.popup_player_key));
final boolean isAudioPlayerSelected =
selectedChoice.equals(getString(R.string.background_player_key));
if (currentLinkType != LinkType.STREAM) {
if (isExtAudioEnabled && isAudioPlayerSelected
|| isExtVideoEnabled && isVideoPlayerSelected) {
Toast.makeText(this, R.string.external_player_unsupported_link_type,
Toast.LENGTH_LONG).show();
handleChoice(showInfoKey);
return;
}
if (currentLinkType != LinkType.STREAM
&& ((isExtAudioEnabled && isAudioPlayerSelected)
|| (isExtVideoEnabled && isVideoPlayerSelected))
) {
Toast.makeText(this, R.string.external_player_unsupported_link_type,
Toast.LENGTH_LONG).show();
handleChoice(getString(R.string.show_info_key));
return;
}
final List<StreamingService.ServiceInfo.MediaCapability> capabilities
= currentService.getServiceInfo().getMediaCapabilities();
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
currentService.getServiceInfo().getMediaCapabilities();
boolean serviceSupportsChoice = false;
if (isVideoPlayerSelected) {
serviceSupportsChoice = capabilities.contains(VIDEO);
} else if (selectedChoiceKey.equals(backgroundPlayerKey)) {
serviceSupportsChoice = capabilities.contains(AUDIO);
}
if (serviceSupportsChoice) {
handleChoice(selectedChoiceKey);
// Check if the service supports the choice
if ((isVideoPlayerSelected && capabilities.contains(VIDEO))
|| (isAudioPlayerSelected && capabilities.contains(AUDIO))) {
handleChoice(selectedChoice);
} else {
handleChoice(showInfoKey);
handleChoice(getString(R.string.show_info_key));
}
return;
}
// Default / Ask always
final List<AdapterChoiceItem> availableChoices = choiceChecker.getAvailableChoices();
switch (availableChoices.size()) {
case 1:
handleChoice(availableChoices.get(0).key);
break;
case 0:
handleChoice(getString(R.string.show_info_key));
break;
default:
showDialog(availableChoices);
break;
}
}
/**
* This is a helper class for checking if the choices are available and/or selected.
*/
class ChoiceAvailabilityChecker {
private final List<AdapterChoiceItem> availableChoices;
private final String selectedChoiceKey;
ChoiceAvailabilityChecker(
@NonNull final List<AdapterChoiceItem> availableChoices,
@NonNull final String selectedChoiceKey) {
this.availableChoices = availableChoices;
this.selectedChoiceKey = selectedChoiceKey;
}
public List<AdapterChoiceItem> getAvailableChoices() {
return availableChoices;
}
public String getSelectedChoiceKey() {
return selectedChoiceKey;
}
public boolean isAvailableAndSelected(@StringRes final int... wantedKeys) {
return Arrays.stream(wantedKeys).anyMatch(this::isAvailableAndSelected);
}
public boolean isAvailableAndSelected(@StringRes final int wantedKey) {
final String wanted = getString(wantedKey);
// Check if the wanted option is selected
if (!selectedChoiceKey.equals(wanted)) {
return false;
}
// Check if it's available
return availableChoices.stream().anyMatch(item -> wanted.equals(item.key));
}
}
private void showDialog(final List<AdapterChoiceItem> choices) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final Context themeWrapperContext = getThemeWrapperContext();
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(getLayoutInflater())
.list;
final Context themeWrapperContext = getThemeWrapperContext();
final LayoutInflater layoutInflater = LayoutInflater.from(themeWrapperContext);
final SingleChoiceDialogViewBinding binding =
SingleChoiceDialogViewBinding.inflate(layoutInflater);
final RadioGroup radioGroup = binding.list;
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
final int indexOfChild = radioGroup.indexOfChild(
@@ -349,21 +392,19 @@ public class RouterActivity extends AppCompatActivity {
alertDialogChoice = new AlertDialog.Builder(themeWrapperContext)
.setTitle(R.string.preferred_open_action_share_menu_title)
.setView(radioGroup)
.setView(binding.getRoot())
.setCancelable(true)
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
.setPositiveButton(R.string.always, dialogButtonsClickListener)
.setOnDismissListener((dialog) -> {
.setOnDismissListener(dialog -> {
if (!selectionIsDownload && !selectionIsAddToPlaylist) {
finish();
}
})
.create();
//noinspection CodeBlock2Expr
alertDialogChoice.setOnShowListener(dialog -> {
setDialogButtonsState(alertDialogChoice, radioGroup.getCheckedRadioButtonId() != -1);
});
alertDialogChoice.setOnShowListener(dialog -> setDialogButtonsState(
alertDialogChoice, radioGroup.getCheckedRadioButtonId() != -1));
radioGroup.setOnCheckedChangeListener((group, checkedId) ->
setDialogButtonsState(alertDialogChoice, true));
@@ -383,9 +424,10 @@ public class RouterActivity extends AppCompatActivity {
int id = 12345;
for (final AdapterChoiceItem item : choices) {
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
final RadioButton radioButton = ListRadioIconItemBinding.inflate(layoutInflater)
.getRoot();
radioButton.setText(item.description);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(
AppCompatResources.getDrawable(themeWrapperContext, item.icon),
null, null, null);
radioButton.setChecked(false);
@@ -425,87 +467,64 @@ public class RouterActivity extends AppCompatActivity {
private List<AdapterChoiceItem> getChoicesForService(final StreamingService service,
final LinkType linkType) {
final Context context = getThemeWrapperContext();
final List<AdapterChoiceItem> returnList = new ArrayList<>();
final List<StreamingService.ServiceInfo.MediaCapability> capabilities
= service.getServiceInfo().getMediaCapabilities();
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(this);
final boolean isExtVideoEnabled = preferences.getBoolean(
getString(R.string.use_external_video_player_key), false);
final boolean isExtAudioEnabled = preferences.getBoolean(
getString(R.string.use_external_audio_player_key), false);
final AdapterChoiceItem videoPlayer = new AdapterChoiceItem(
getString(R.string.video_player_key), getString(R.string.video_player),
R.drawable.ic_play_arrow);
final AdapterChoiceItem showInfo = new AdapterChoiceItem(
getString(R.string.show_info_key), getString(R.string.show_info),
R.drawable.ic_info_outline);
final AdapterChoiceItem popupPlayer = new AdapterChoiceItem(
getString(R.string.popup_player_key), getString(R.string.popup_player),
R.drawable.ic_picture_in_picture);
final AdapterChoiceItem videoPlayer = new AdapterChoiceItem(
getString(R.string.video_player_key), getString(R.string.video_player),
R.drawable.ic_play_arrow);
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
getString(R.string.background_player_key), getString(R.string.background_player),
R.drawable.ic_headset);
final AdapterChoiceItem addToPlaylist = new AdapterChoiceItem(
getString(R.string.add_to_playlist_key), getString(R.string.add_to_playlist),
R.drawable.ic_add);
final AdapterChoiceItem popupPlayer = new AdapterChoiceItem(
getString(R.string.popup_player_key), getString(R.string.popup_player),
R.drawable.ic_picture_in_picture);
final List<AdapterChoiceItem> returnedItems = new ArrayList<>();
returnedItems.add(showInfo); // Always present
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
service.getServiceInfo().getMediaCapabilities();
if (linkType == LinkType.STREAM) {
if (isExtVideoEnabled) {
// show both "show info" and "video player", they are two different activities
returnList.add(showInfo);
returnList.add(videoPlayer);
} else {
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
if (capabilities.contains(VIDEO)
&& PlayerHelper.isAutoplayAllowedByUser(context)
&& playerType == null || playerType == MainPlayer.PlayerType.VIDEO) {
// show only "video player" since the details activity will be opened and the
// video will be auto played there. Since "show info" would do the exact same
// thing, use that as a key to let VideoDetailFragment load the stream instead
// of using FetcherService (see comment in handleChoice())
returnList.add(new AdapterChoiceItem(
showInfo.key, videoPlayer.description, videoPlayer.icon));
} else {
// show only "show info" if video player is not applicable, auto play is
// disabled or a video is playing in a player different than the main one
returnList.add(showInfo);
}
}
if (capabilities.contains(VIDEO)) {
returnList.add(popupPlayer);
returnedItems.add(videoPlayer);
returnedItems.add(popupPlayer);
}
if (capabilities.contains(AUDIO)) {
returnList.add(backgroundPlayer);
returnedItems.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),
returnedItems.add(new AdapterChoiceItem(getString(R.string.download_key),
getString(R.string.download),
R.drawable.ic_file_download));
// Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can
// not be added to a playlist
returnList.add(addToPlaylist);
returnedItems.add(new AdapterChoiceItem(getString(R.string.add_to_playlist_key),
getString(R.string.add_to_playlist),
R.drawable.ic_add));
} else {
returnList.add(showInfo);
// LinkType.NONE is never present because it's filtered out before
// channels and playlist can be played as they contain a list of videos
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(this);
final boolean isExtVideoEnabled = preferences.getBoolean(
getString(R.string.use_external_video_player_key), false);
final boolean isExtAudioEnabled = preferences.getBoolean(
getString(R.string.use_external_audio_player_key), false);
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
returnList.add(videoPlayer);
returnList.add(popupPlayer);
returnedItems.add(videoPlayer);
returnedItems.add(popupPlayer);
}
if (capabilities.contains(AUDIO) && !isExtAudioEnabled) {
returnList.add(backgroundPlayer);
returnedItems.add(backgroundPlayer);
}
}
return returnList;
return returnedItems;
}
private Context getThemeWrapperContext() {
@@ -567,7 +586,8 @@ public class RouterActivity extends AppCompatActivity {
// stop and bypass FetcherService if InfoScreen was selected since
// StreamDetailFragment can fetch data itself
if (selectedChoiceKey.equals(getString(R.string.show_info_key))) {
if (selectedChoiceKey.equals(getString(R.string.show_info_key))
|| canHandleChoiceLikeShowInfo(selectedChoiceKey)) {
disposables.add(Observable
.fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl))
.subscribeOn(Schedulers.io())
@@ -590,6 +610,30 @@ public class RouterActivity extends AppCompatActivity {
finish();
}
private boolean canHandleChoiceLikeShowInfo(final String selectedChoiceKey) {
if (!selectedChoiceKey.equals(getString(R.string.video_player_key))) {
return false;
}
// "video player" can be handled like "show info" (because VideoDetailFragment can load
// the stream instead of FetcherService) when...
// ...Autoplay is enabled
if (!PlayerHelper.isAutoplayAllowedByUser(getThemeWrapperContext())) {
return false;
}
final boolean isExtVideoEnabled = PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(getString(R.string.use_external_video_player_key), false);
// ...it's not done via an external player
if (isExtVideoEnabled) {
return false;
}
// ...the player is not running or in normal Video-mode/type
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
return playerType == null || playerType == MainPlayer.PlayerType.VIDEO;
}
private void openAddToPlaylistDialog() {
// Getting the stream info usually takes a moment
// Notifying the user here to ensure that no confusion arises
@@ -631,22 +675,13 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
final List<VideoStream> sortedVideoStreams = ListHelper
.getSortedStreamVideosList(this, result.getVideoStreams(),
result.getVideoOnlyStreams(), false);
final int selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(this, sortedVideoStreams);
final DownloadDialog downloadDialog = new DownloadDialog(this, result);
downloadDialog.setOnDismissListener(dialog -> finish());
final FragmentManager fm = getSupportFragmentManager();
final DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(result.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setOnDismissListener(dialog -> finish());
downloadDialog.show(fm, "downloadDialog");
fm.executePendingTransactions();
}, throwable ->
showUnsupportedUrlDialog(currentUrl)));
}, throwable -> showUnsupportedUrlDialog(currentUrl)));
}
@Override
@@ -672,8 +707,8 @@ public class RouterActivity extends AppCompatActivity {
final int icon;
AdapterChoiceItem(final String key, final String description, final int icon) {
this.description = description;
this.key = key;
this.description = description;
this.icon = icon;
}
}

View File

@@ -10,7 +10,6 @@ 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
@@ -21,30 +20,28 @@ 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)
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")
}
) { tab, position ->
tab.setText(mAboutStateAdapter.getPageTitle(position))
}.attach()
}
@@ -75,13 +72,14 @@ class AboutActivity : AppCompatActivity() {
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
FragmentAboutBinding.inflate(inflater, container, false).apply {
aboutAppVersion.text = BuildConfig.VERSION_NAME
aboutGithubLink.openLink(R.string.github_url)
aboutDonationLink.openLink(R.string.donation_url)
aboutWebsiteLink.openLink(R.string.website_url)
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
return root
}
}
}
@@ -90,17 +88,29 @@ class AboutActivity : AppCompatActivity() {
* one of the sections/tabs/pages.
*/
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
private val posAbout = 0
private val posLicense = 1
private val totalCount = 2
override fun createFragment(position: Int): Fragment {
return when (position) {
POS_ABOUT -> AboutFragment()
POS_LICENSE -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
posAbout -> AboutFragment()
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
}
}
override fun getItemCount(): Int {
// Show 2 total pages.
return TOTAL_COUNT
return totalCount
}
fun getPageTitle(position: Int): Int {
return when (position) {
posAbout -> R.string.tab_about
posLicense -> R.string.tab_licenses
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
}
}
}
@@ -117,10 +127,6 @@ class AboutActivity : AppCompatActivity() {
"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
@@ -185,10 +191,11 @@ class AboutActivity : AppCompatActivity() {
SoftwareComponent(
"RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
)
),
SoftwareComponent(
"SearchPreference", "2018", "ByteHamster",
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
),
)
private const val POS_ABOUT = 0
private const val POS_LICENSE = 1
private const val TOTAL_COUNT = 2
}
}

View File

@@ -87,60 +87,50 @@ object LicenseFragmentHelper {
return context.getString(color).substring(3)
}
@JvmStatic
fun showLicense(context: Context?, license: License): Disposable {
return showLicense(context, license) { alertDialog ->
alertDialog.setPositiveButton(R.string.ok) { dialog, _ ->
dialog.dismiss()
}
}
}
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
return showLicense(context, component.license) { alertDialog ->
alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ ->
dialog.dismiss()
}
alertDialog.setNeutralButton(R.string.open_website_license) { _, _ ->
ShareUtils.openUrlInBrowser(context!!, component.link)
}
}
}
private fun showLicense(
context: Context?,
license: License,
block: (AlertDialog.Builder) -> Unit
): Disposable {
return if (context == null) {
Disposable.empty()
} else {
Observable.fromCallable { getFormattedLicense(context, license) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense: String ->
.subscribe { formattedLicense ->
val webViewData = Base64.encodeToString(
formattedLicense
.toByteArray(StandardCharsets.UTF_8),
Base64.NO_PADDING
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.ok)
) { 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)
AlertDialog.Builder(context).apply {
setTitle(license.name)
setView(webView)
Localization.assureCorrectAppLanguage(context)
block(this)
show()
}
alert.show()
}
}
}

View File

@@ -1,5 +1,7 @@
package org.schabi.newpipe.database;
import static org.schabi.newpipe.database.Migrations.DB_VER_5;
import androidx.room.Database;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
@@ -27,8 +29,6 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import static org.schabi.newpipe.database.Migrations.DB_VER_4;
@TypeConverters({Converters.class})
@Database(
entities = {
@@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_4;
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
FeedLastUpdatedEntity.class
},
version = DB_VER_4
version = DB_VER_5
)
public abstract class AppDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "newpipe.db";

View File

@@ -22,6 +22,7 @@ public final class Migrations {
public static final int DB_VER_2 = 2;
public static final int DB_VER_3 = 3;
public static final int DB_VER_4 = 4;
public static final int DB_VER_5 = 5;
private static final String TAG = Migrations.class.getName();
public static final boolean DEBUG = MainActivity.DEBUG;
@@ -179,5 +180,14 @@ public final class Migrations {
}
};
private Migrations() { }
public static final Migration MIGRATION_4_5 = new Migration(DB_VER_4, DB_VER_5) {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
+ "INTEGER NOT NULL DEFAULT 0");
}
};
private Migrations() {
}
}

View File

@@ -12,6 +12,7 @@ import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@@ -252,4 +253,21 @@ abstract class FeedDAO {
"""
)
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
@Query(
"""
SELECT s.* FROM subscriptions s
LEFT JOIN feed_last_updated lu
ON s.uid = lu.subscription_id
WHERE
(lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold)
AND s.notification_mode = :notificationMode
"""
)
abstract fun getOutdatedWithNotificationMode(
outdatedThreshold: OffsetDateTime,
@NotificationMode notificationMode: Int
): Flowable<List<SubscriptionEntity>>
}

View File

@@ -19,6 +19,7 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE
@Dao
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
String ORDER_BY_MAX_CREATION_DATE = " ORDER BY MAX(" + CREATION_DATE + ") DESC";
@Query("SELECT * FROM " + TABLE_NAME
+ " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
@@ -36,16 +37,16 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
@Override
Flowable<List<SearchHistoryEntry>> getAll();
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE
+ " LIMIT :limit")
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " GROUP BY " + SEARCH
+ ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
Flowable<List<String>> getUniqueEntries(int limit);
@Query("SELECT * FROM " + TABLE_NAME
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
+ " GROUP BY " + SEARCH + " LIMIT :limit")
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
+ " GROUP BY " + SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
Flowable<List<String>> getSimilarEntries(String query, int limit);
}

View File

@@ -3,6 +3,7 @@ package org.schabi.newpipe.database.history.dao;
import androidx.annotation.Nullable;
import androidx.room.Dao;
import androidx.room.Query;
import androidx.room.RewriteQueriesToDropUnusedColumns;
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
@@ -67,6 +68,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteStreamHistory(long streamId);
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM " + STREAM_TABLE
// Select the latest entry and watch count for each stream id on history table

View File

@@ -1,79 +0,0 @@
package org.schabi.newpipe.database.history.model;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import java.time.OffsetDateTime;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
indices = {@Index(value = SEARCH)})
public class SearchHistoryEntry {
public static final String ID = "id";
public static final String TABLE_NAME = "search_history";
public static final String SERVICE_ID = "service_id";
public static final String CREATION_DATE = "creation_date";
public static final String SEARCH = "search";
@ColumnInfo(name = ID)
@PrimaryKey(autoGenerate = true)
private long id;
@ColumnInfo(name = CREATION_DATE)
private OffsetDateTime creationDate;
@ColumnInfo(name = SERVICE_ID)
private int serviceId;
@ColumnInfo(name = SEARCH)
private String search;
public SearchHistoryEntry(final OffsetDateTime creationDate, final int serviceId,
final String search) {
this.serviceId = serviceId;
this.creationDate = creationDate;
this.search = search;
}
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
public OffsetDateTime getCreationDate() {
return creationDate;
}
public void setCreationDate(final OffsetDateTime creationDate) {
this.creationDate = creationDate;
}
public int getServiceId() {
return serviceId;
}
public void setServiceId(final int serviceId) {
this.serviceId = serviceId;
}
public String getSearch() {
return search;
}
public void setSearch(final String search) {
this.search = search;
}
@Ignore
public boolean hasEqualValues(final SearchHistoryEntry otherEntry) {
return getServiceId() == otherEntry.getServiceId()
&& getSearch().equals(otherEntry.getSearch());
}
}

View File

@@ -0,0 +1,40 @@
package org.schabi.newpipe.database.history.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import java.time.OffsetDateTime
@Entity(
tableName = SearchHistoryEntry.TABLE_NAME,
indices = [Index(value = [SearchHistoryEntry.SEARCH])]
)
data class SearchHistoryEntry(
@field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?,
@field:ColumnInfo(
name = SERVICE_ID
) var serviceId: Int,
@field:ColumnInfo(name = SEARCH) var search: String?
) {
@ColumnInfo(name = ID)
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@Ignore
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
return (
serviceId == otherEntry.serviceId &&
search == otherEntry.search
)
}
companion object {
const val ID = "id"
const val TABLE_NAME = "search_history"
const val SERVICE_ID = "service_id"
const val CREATION_DATE = "creation_date"
const val SEARCH = "search"
}
}

View File

@@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.Index;
import org.schabi.newpipe.database.stream.model.StreamEntity;
@@ -42,18 +41,19 @@ public class StreamHistoryEntity {
@ColumnInfo(name = STREAM_REPEAT_COUNT)
private long repeatCount;
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate,
/**
* @param streamUid the stream id this history item will refer to
* @param accessDate the last time the stream was accessed
* @param repeatCount the total number of views this stream received
*/
public StreamHistoryEntity(final long streamUid,
@NonNull final OffsetDateTime accessDate,
final long repeatCount) {
this.streamUid = streamUid;
this.accessDate = accessDate;
this.repeatCount = repeatCount;
}
@Ignore
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate) {
this(streamUid, accessDate, 1);
}
public long getStreamUid() {
return streamUid;
}

View File

@@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.dao;
import androidx.room.Dao;
import androidx.room.Query;
import androidx.room.RewriteQueriesToDropUnusedColumns;
import androidx.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
@@ -52,6 +53,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
Flowable<Integer> getMaximumIndexOf(long playlistId);
@RewriteQueriesToDropUnusedColumns
@Transaction
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
// get ids of streams of the given playlist

View File

@@ -12,8 +12,7 @@ import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
import org.schabi.newpipe.util.StreamTypeUtil
import java.time.OffsetDateTime
@Dao
@@ -39,6 +38,9 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
@Query("SELECT COUNT(*) != 0 FROM streams WHERE url = :url AND service_id = :serviceId")
internal abstract fun exists(serviceId: Int, url: String): Boolean
@Query(
"""
SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration
@@ -88,8 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
?: throw IllegalStateException("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
if (!isNewerStreamLive) {
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
// Use the existent upload date if the newer stream does not have a better precision
// (i.e. is an approximation). This is done to prevent unnecessary changes.

View File

@@ -0,0 +1,14 @@
package org.schabi.newpipe.database.subscription;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@IntDef({NotificationMode.DISABLED, NotificationMode.ENABLED})
@Retention(RetentionPolicy.SOURCE)
public @interface NotificationMode {
int DISABLED = 0;
int ENABLED = 1;
//other values reserved for the future
}

View File

@@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RewriteQueriesToDropUnusedColumns
import androidx.room.Transaction
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
@@ -31,6 +32,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
)
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
@RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT * FROM subscriptions s
@@ -47,6 +49,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
currentGroupId: Long
): Flowable<List<SubscriptionEntity>>
@RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT * FROM subscriptions s

View File

@@ -26,6 +26,7 @@ public class SubscriptionEntity {
public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url";
public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
public static final String SUBSCRIPTION_DESCRIPTION = "description";
public static final String SUBSCRIPTION_NOTIFICATION_MODE = "notification_mode";
@PrimaryKey(autoGenerate = true)
private long uid = 0;
@@ -48,6 +49,9 @@ public class SubscriptionEntity {
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
private String description;
@ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE)
private int notificationMode;
@Ignore
public static SubscriptionEntity from(@NonNull final ChannelInfo info) {
final SubscriptionEntity result = new SubscriptionEntity();
@@ -114,6 +118,15 @@ public class SubscriptionEntity {
this.description = description;
}
@NotificationMode
public int getNotificationMode() {
return notificationMode;
}
public void setNotificationMode(@NotificationMode final int notificationMode) {
this.notificationMode = notificationMode;
}
@Ignore
public void setData(final String n, final String au, final String d, final Long sc) {
this.setName(n);

View File

@@ -41,8 +41,8 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DownloadDialogBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
@@ -53,6 +53,7 @@ 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.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.FilePickerActivityHelper;
@@ -60,15 +61,16 @@ import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SecondaryStreamHelper;
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import icepick.Icepick;
import icepick.State;
@@ -80,6 +82,8 @@ import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState;
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment
@@ -90,17 +94,17 @@ public class DownloadDialog extends DialogFragment
@State
StreamInfo currentInfo;
@State
StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
StreamSizeWrapper<AudioStream> wrappedAudioStreams;
@State
StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
StreamSizeWrapper<VideoStream> wrappedVideoStreams;
@State
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
@State
int selectedVideoIndex = 0;
int selectedVideoIndex; // set in the constructor
@State
int selectedAudioIndex = 0;
int selectedAudioIndex = 0; // default to the first item
@State
int selectedSubtitleIndex = 0;
int selectedSubtitleIndex = 0; // default to the first item
@Nullable
private OnDismissListener onDismissListener = null;
@@ -141,77 +145,43 @@ public class DownloadDialog extends DialogFragment
// Instance creation
//////////////////////////////////////////////////////////////////////////*/
public static DownloadDialog newInstance(final StreamInfo info) {
final DownloadDialog dialog = new DownloadDialog();
dialog.setInfo(info);
return dialog;
}
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
.getSortedStreamVideosList(context, info.getVideoStreams(),
info.getVideoOnlyStreams(), false));
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
final DownloadDialog instance = newInstance(info);
instance.setVideoStreams(streamsList);
instance.setSelectedVideoStream(selectedStreamIndex);
instance.setAudioStreams(info.getAudioStreams());
instance.setSubtitleStreams(info.getSubtitles());
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// Setters
//////////////////////////////////////////////////////////////////////////*/
private void setInfo(final StreamInfo info) {
/**
* Create a new download dialog with the video, audio and subtitle streams from the provided
* stream info. Video streams and video-only streams will be put into a single list menu,
* sorted according to their resolution and the default video resolution will be selected.
*
* @param context the context to use just to obtain preferences and strings (will not be stored)
* @param info the info from which to obtain downloadable streams and other info (e.g. title)
*/
public DownloadDialog(final Context context, @NonNull final StreamInfo info) {
this.currentInfo = info;
// TODO: Adapt this code when the downloader support other types of stream deliveries
final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList(
context,
getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP),
getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP),
false,
false
);
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context);
this.wrappedAudioStreams = new StreamSizeWrapper<>(
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP), context);
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
}
public void setAudioStreams(final List<AudioStream> audioStreams) {
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
}
public void setAudioStreams(final StreamSizeWrapper<AudioStream> was) {
this.wrappedAudioStreams = was;
}
public void setVideoStreams(final List<VideoStream> videoStreams) {
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
}
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
this.wrappedVideoStreams = wvs;
}
public void setSubtitleStreams(final List<SubtitlesStream> subtitleStreams) {
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
}
public void setSubtitleStreams(
final StreamSizeWrapper<SubtitlesStream> wss) {
this.wrappedSubtitleStreams = wss;
}
public void setSelectedVideoStream(final int svi) {
this.selectedVideoIndex = svi;
}
public void setSelectedAudioStream(final int sai) {
this.selectedAudioIndex = sai;
}
public void setSelectedSubtitleStream(final int ssi) {
this.selectedSubtitleIndex = ssi;
}
/**
* @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)}
*/
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener;
}
/*//////////////////////////////////////////////////////////////////////////
// Android lifecycle
//////////////////////////////////////////////////////////////////////////*/
@@ -247,11 +217,16 @@ public class DownloadDialog extends DialogFragment
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams
.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
audioStream));
} else if (DEBUG) {
Log.w(TAG, "No audio stream candidates for video format "
+ videoStreams.get(i).getFormat().name());
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
if (mediaFormat != null) {
Log.w(TAG, "No audio stream candidates for video format "
+ mediaFormat.name());
} else {
Log.w(TAG, "No audio stream candidates for unknown video format");
}
}
}
@@ -286,7 +261,8 @@ public class DownloadDialog extends DialogFragment
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) {
if (DEBUG) {
Log.d(TAG, "onCreateView() called with: "
@@ -297,14 +273,15 @@ public class DownloadDialog extends DialogFragment
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
public void onViewCreated(@NonNull final View view,
@Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
dialogBinding = DownloadDialogBinding.bind(view);
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName()));
selectedAudioIndex = ListHelper
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
.getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
@@ -320,21 +297,16 @@ public class DownloadDialog extends DialogFragment
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
dialogBinding.threadsCount.setText(String.valueOf(threads));
dialogBinding.threads.setProgress(threads - 1);
dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
@Override
public void onProgressChanged(final SeekBar seekbar, final int progress,
public void onProgressChanged(@NonNull final SeekBar seekbar,
final int progress,
final boolean fromUser) {
final int newProgress = progress + 1;
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
.apply();
dialogBinding.threadsCount.setText(String.valueOf(newProgress));
}
@Override
public void onStartTrackingTouch(final SeekBar p1) { }
@Override
public void onStopTrackingTouch(final SeekBar p1) { }
});
fetchStreamsSize();
@@ -402,7 +374,7 @@ public class DownloadDialog extends DialogFragment
== R.id.video_button) {
setupVideoSpinner();
}
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
}, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading video stream size",
currentInfo.getServiceId()))));
@@ -412,7 +384,7 @@ public class DownloadDialog extends DialogFragment
== R.id.audio_button) {
setupAudioSpinner();
}
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
}, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading audio stream size",
currentInfo.getServiceId()))));
@@ -422,7 +394,7 @@ public class DownloadDialog extends DialogFragment
== R.id.subtitle_button) {
setupSubtitleSpinner();
}
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
}, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading subtitle stream size",
currentInfo.getServiceId()))));
@@ -473,7 +445,7 @@ public class DownloadDialog extends DialogFragment
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
}
private void requestDownloadSaveAsResult(final ActivityResult result) {
private void requestDownloadSaveAsResult(@NonNull final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK) {
return;
}
@@ -490,8 +462,8 @@ public class DownloadDialog extends DialogFragment
return;
}
final DocumentFile docFile
= DocumentFile.fromSingleUri(context, result.getData().getData());
final DocumentFile docFile = DocumentFile.fromSingleUri(context,
result.getData().getData());
if (docFile == null) {
showFailedDialog(R.string.general_error);
return;
@@ -502,7 +474,7 @@ public class DownloadDialog extends DialogFragment
docFile.getType());
}
private void requestDownloadPickFolderResult(final ActivityResult result,
private void requestDownloadPickFolderResult(@NonNull final ActivityResult result,
final String key,
final String tag) {
if (result.getResultCode() != Activity.RESULT_OK) {
@@ -522,12 +494,11 @@ public class DownloadDialog extends DialogFragment
StoredDirectoryHelper.PERMISSION_FLAGS);
}
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putString(key, uri.toString()).apply();
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key,
uri.toString()).apply();
try {
final StoredDirectoryHelper mainStorage
= new StoredDirectoryHelper(context, uri, tag);
final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, tag);
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
filenameTmp, mimeTmp);
} catch (final IOException e) {
@@ -565,8 +536,10 @@ public class DownloadDialog extends DialogFragment
}
@Override
public void onItemSelected(final AdapterView<?> parent, final View view,
final int position, final long id) {
public void onItemSelected(final AdapterView<?> parent,
final View view,
final int position,
final long id) {
if (DEBUG) {
Log.d(TAG, "onItemSelected() called with: "
+ "parent = [" + parent + "], view = [" + view + "], "
@@ -601,14 +574,16 @@ public class DownloadDialog extends DialogFragment
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE
: View.GONE);
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE
: View.GONE);
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
? View.VISIBLE : View.GONE);
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
getString(R.string.last_download_type_video_key));
getString(R.string.last_download_type_video_key));
if (isVideoStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
@@ -644,7 +619,7 @@ public class DownloadDialog extends DialogFragment
dialogBinding.subtitleButton.setEnabled(enabled);
}
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
int candidate = 0;
@@ -670,8 +645,10 @@ public class DownloadDialog extends DialogFragment
return candidate;
}
@NonNull
private String getNameEditText() {
final String str = dialogBinding.fileName.getText().toString().trim();
final String str = Objects.requireNonNull(dialogBinding.fileName.getText()).toString()
.trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
}
@@ -687,7 +664,8 @@ public class DownloadDialog extends DialogFragment
}
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
launcher.launch(StoredDirectoryHelper.getPicker(context));
NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.getPicker(context), TAG,
context);
}
private void prepareSelectedDownload() {
@@ -708,7 +686,7 @@ public class DownloadDialog extends DialogFragment
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else {
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.suffix;
}
@@ -717,22 +695,30 @@ public class DownloadDialog extends DialogFragment
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
mimeTmp = format.mimeType;
filenameTmp += format.suffix;
if (format != null) {
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();
mimeTmp = format.mimeType;
filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
if (format != null) {
mimeTmp = format.mimeType;
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.suffix;
} else if (format != null) {
filenameTmp += format.suffix;
}
break;
default:
throw new RuntimeException("No stream selected");
}
if (!askForSavePath
&& (mainStorage == null
if (!askForSavePath && (mainStorage == null
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|| mainStorage.isInvalidSafStorage())) {
// Pick new download folder if one of:
@@ -766,14 +752,16 @@ public class DownloadDialog extends DialogFragment
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
}
requestDownloadSaveAsLauncher.launch(StoredFileHelper.getNewPicker(context,
filenameTmp, mimeTmp, initialPath));
NoFileManagerSafeGuard.launchSafe(requestDownloadSaveAsLauncher,
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), TAG,
context);
return;
}
// check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp);
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)
@@ -781,7 +769,8 @@ public class DownloadDialog extends DialogFragment
}
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
final Uri targetFile, final String filename,
final Uri targetFile,
final String filename,
final String mime) {
StoredFileHelper storage;
@@ -799,7 +788,7 @@ public class DownloadDialog extends DialogFragment
mainStorage.getTag());
}
} catch (final Exception e) {
ErrorActivity.reportErrorInSnackbar(this,
ErrorUtil.createNotification(requireContext(),
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
return;
}
@@ -942,7 +931,7 @@ public class DownloadDialog extends DialogFragment
storage.truncate();
}
} catch (final IOException e) {
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
Log.e(TAG, "Failed to truncate the file: " + storage.getUri().toString(), e);
showFailedDialog(R.string.overwrite_failed);
return;
}
@@ -987,8 +976,8 @@ public class DownloadDialog extends DialogFragment
}
psArgs = null;
final long videoSize = wrappedVideoStreams
.getSizeInBytes((VideoStream) selectedStream);
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
@@ -1004,7 +993,7 @@ public class DownloadDialog extends DialogFragment
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
psArgs = new String[] {
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
@@ -1015,17 +1004,22 @@ public class DownloadDialog extends DialogFragment
}
if (secondaryStream == null) {
urls = new String[]{
selectedStream.getUrl()
urls = new String[] {
selectedStream.getContent()
};
recoveryInfo = new MissionRecoveryInfo[]{
recoveryInfo = new MissionRecoveryInfo[] {
new MissionRecoveryInfo(selectedStream)
};
} else {
urls = new String[]{
selectedStream.getUrl(), secondaryStream.getUrl()
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
throw new IllegalArgumentException("Unsupported stream delivery format"
+ secondaryStream.getDeliveryMethod());
}
urls = new String[] {
selectedStream.getContent(), secondaryStream.getContent()
};
recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream),
recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
new MissionRecoveryInfo(secondaryStream)};
}

View File

@@ -33,12 +33,11 @@ public class AcraReportSender implements ReportSender {
@Override
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
ErrorActivity.reportError(context, new ErrorInfo(
ErrorUtil.openActivity(context, new ErrorInfo(
new String[]{report.getString(ReportField.STACK_TRACE)},
UserAction.UI_ERROR,
ErrorInfo.SERVICE_NONE,
"ACRA report",
R.string.app_ui_crash,
null));
R.string.app_ui_crash));
}
}

View File

@@ -1,103 +0,0 @@
package org.schabi.newpipe.error;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Ensures that a Exception is serializable.
* This is
*/
public final class EnsureExceptionSerializable {
private static final String TAG = "EnsureExSerializable";
private EnsureExceptionSerializable() {
// No instance
}
/**
* Ensures that an exception is serializable.
* <br/>
* If that is not the case a {@link WorkaroundNotSerializableException} is created.
*
* @param exception
* @return if an exception is not serializable a new {@link WorkaroundNotSerializableException}
* otherwise the exception from the parameter
*/
public static Exception ensureSerializable(@NonNull final Exception exception) {
return checkIfSerializable(exception)
? exception
: WorkaroundNotSerializableException.create(exception);
}
public static boolean checkIfSerializable(@NonNull final Exception exception) {
try {
// Check by creating a new ObjectOutputStream which does the serialization
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)
) {
oos.writeObject(exception);
oos.flush();
bos.toByteArray();
}
return true;
} catch (final IOException ex) {
Log.d(TAG, "Exception is not serializable", ex);
return false;
}
}
public static class WorkaroundNotSerializableException extends Exception {
protected WorkaroundNotSerializableException(
final Throwable notSerializableException,
final Throwable cause) {
super(notSerializableException.toString(), cause);
setStackTrace(notSerializableException.getStackTrace());
}
protected WorkaroundNotSerializableException(final Throwable notSerializableException) {
super(notSerializableException.toString());
setStackTrace(notSerializableException.getStackTrace());
}
public static WorkaroundNotSerializableException create(
@NonNull final Exception notSerializableException
) {
// Build a list of the exception + all causes
final List<Throwable> throwableList = new ArrayList<>();
int pos = 0;
Throwable throwableToProcess = notSerializableException;
while (throwableToProcess != null) {
throwableList.add(throwableToProcess);
pos++;
throwableToProcess = throwableToProcess.getCause();
}
// Reverse list so that it starts with the last one
Collections.reverse(throwableList);
// Build exception stack
WorkaroundNotSerializableException cause = null;
for (final Throwable t : throwableList) {
cause = cause == null
? new WorkaroundNotSerializableException(t)
: new WorkaroundNotSerializableException(t, cause);
}
return cause;
}
}
}

View File

@@ -1,9 +1,10 @@
package org.schabi.newpipe.error;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -11,15 +12,12 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
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.fragment.app.Fragment;
import com.google.android.material.snackbar.Snackbar;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig;
@@ -27,15 +25,13 @@ 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.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/*
* Created by Christian Schabesberger on 24.10.15.
*
@@ -56,6 +52,10 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This activity is used to show error details and allow reporting them in various ways. Use {@link
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
*/
public class ErrorActivity extends AppCompatActivity {
// LOG TAGS
public static final String TAG = ErrorActivity.class.toString();
@@ -77,67 +77,6 @@ public class ErrorActivity extends AppCompatActivity {
private ActivityErrorBinding activityErrorBinding;
/**
* Reports a new error by starting a new activity.
* <br/>
* Ensure that the data within errorInfo is serializable otherwise
* an exception will be thrown!<br/>
* {@link EnsureExceptionSerializable} might help.
*
* @param context
* @param errorInfo
*/
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 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, Snackbar.LENGTH_LONG)
.setActionTextColor(Color.YELLOW)
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
reportError(context, errorInfo)).show();
} else {
reportError(context, errorInfo);
}
}
////////////////////////////////////////////////////////////////////////
// Activity lifecycle

View File

@@ -2,16 +2,18 @@ package org.schabi.newpipe.error
import android.os.Parcelable
import androidx.annotation.StringRes
import com.google.android.exoplayer2.ExoPlaybackException
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.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 org.schabi.newpipe.util.ServiceHelper
import java.io.PrintWriter
import java.io.StringWriter
@@ -21,11 +23,14 @@ class ErrorInfo(
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
val messageStringId: Int
) : Parcelable {
// no need to store throwable, all data for report is in other variables
// also, the throwable might not be serializable, see TeamNewPipe/NewPipe#7302
@IgnoredOnParcel
var throwable: Throwable? = null
private constructor(
throwable: Throwable,
userAction: UserAction,
@@ -36,9 +41,10 @@ class ErrorInfo(
userAction,
serviceName,
request,
getMessageStringId(throwable, userAction),
throwable
)
getMessageStringId(throwable, userAction)
) {
this.throwable = throwable
}
private constructor(
throwable: List<Throwable>,
@@ -50,15 +56,16 @@ class ErrorInfo(
userAction,
serviceName,
request,
getMessageStringId(throwable.firstOrNull(), userAction),
throwable.firstOrNull()
)
getMessageStringId(throwable.firstOrNull(), userAction)
) {
this.throwable = 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)
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
this(throwable, userAction, getInfoServiceName(info), request)
@@ -66,7 +73,7 @@ class ErrorInfo(
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)
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
this(throwable, userAction, getInfoServiceName(info), request)
@@ -88,7 +95,7 @@ class ErrorInfo(
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
private fun getInfoServiceName(info: Info?) =
if (info == null) SERVICE_NONE else NewPipe.getNameOfService(info.serviceId)
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
@StringRes
private fun getMessageStringId(
@@ -102,6 +109,13 @@ class ErrorInfo(
throwable is ContentNotSupportedException -> R.string.content_not_supported
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
throwable is ExtractionException -> R.string.parsing_error
throwable is ExoPlaybackException -> {
when (throwable.type) {
ExoPlaybackException.TYPE_SOURCE -> R.string.player_stream_failure
ExoPlaybackException.TYPE_UNEXPECTED -> R.string.player_recoverable_failure
else -> R.string.player_unrecoverable_failure
}
}
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

View File

@@ -15,7 +15,6 @@ 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
@@ -106,7 +105,7 @@ class ErrorPanelHelper(
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
errorServiceInfoTextView.text = context.resources.getString(
R.string.service_provides_reason,
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
ServiceHelper.getSelectedService(context)?.serviceInfo?.name ?: "<unknown>"
)
errorServiceInfoTextView.isVisible = true
@@ -118,7 +117,7 @@ class ErrorPanelHelper(
showAndSetErrorButtonAction(
R.string.error_snackbar_action
) {
ErrorActivity.reportError(context, errorInfo)
ErrorUtil.openActivity(context, errorInfo)
}
errorTextView.setText(getExceptionDescription(errorInfo.throwable))
@@ -178,7 +177,7 @@ class ErrorPanelHelper(
val DEBUG: Boolean = MainActivity.DEBUG
@StringRes
public fun getExceptionDescription(throwable: Throwable?): Int {
fun getExceptionDescription(throwable: Throwable?): Int {
return when (throwable) {
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
is GeographicRestrictionException -> R.string.georestricted_content

View File

@@ -0,0 +1,164 @@
package org.schabi.newpipe.error
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.view.View
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import org.schabi.newpipe.R
/**
* This class contains all of the methods that should be used to let the user know that an error has
* occurred in the least intrusive way possible for each case. This class is for unexpected errors,
* for handled errors (e.g. network errors) use e.g. [ErrorPanelHelper] instead.
* - Use a snackbar if the exception is not critical and it happens in a place where a root view
* is available.
* - Use a notification if the exception happens inside a background service (player, subscription
* import, ...) or there is no activity/fragment from which to extract a root view.
* - Finally use the error activity only as a last resort in case the exception is critical and
* happens in an open activity (since the workflow would be interrupted anyway in that case).
*/
class ErrorUtil {
companion object {
private const val ERROR_REPORT_NOTIFICATION_ID = 5340681
/**
* Starts a new error activity allowing the user to report the provided error. Only use this
* method directly as a last resort in case the exception is critical and happens in an open
* activity (since the workflow would be interrupted anyway in that case). So never use this
* for background services.
*
* @param context the context to use to start the new activity
* @param errorInfo the error info to be reported
*/
@JvmStatic
fun openActivity(context: Context, errorInfo: ErrorInfo) {
context.startActivity(getErrorActivityIntent(context, errorInfo))
}
/**
* Show a bottom snackbar to the user, with a report button that opens the error activity.
* Use this method if the exception is not critical and it happens in a place where a root
* view is available.
*
* @param context will be used to obtain the root view if it is an [Activity]; if no root
* view can be found an error notification is shown instead
* @param errorInfo the error info to be reported
*/
@JvmStatic
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
val rootView = if (context is Activity) context.findViewById<View>(R.id.content) else null
showSnackbar(context, rootView, errorInfo)
}
/**
* Show a bottom snackbar to the user, with a report button that opens the error activity.
* Use this method if the exception is not critical and it happens in a place where a root
* view is available.
*
* @param fragment will be used to obtain the root view if it has a connected [Activity]; if
* no root view can be found an error notification is shown instead
* @param errorInfo the error info to be reported
*/
@JvmStatic
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
var rootView = fragment.view
if (rootView == null && fragment.activity != null) {
rootView = fragment.requireActivity().findViewById(R.id.content)
}
showSnackbar(fragment.requireContext(), rootView, errorInfo)
}
/**
* Shortcut to calling [showSnackbar] with an [ErrorInfo] of type [UserAction.UI_ERROR]
*/
@JvmStatic
fun showUiErrorSnackbar(context: Context, request: String, throwable: Throwable) {
showSnackbar(context, ErrorInfo(throwable, UserAction.UI_ERROR, request))
}
/**
* Shortcut to calling [showSnackbar] with an [ErrorInfo] of type [UserAction.UI_ERROR]
*/
@JvmStatic
fun showUiErrorSnackbar(fragment: Fragment, request: String, throwable: Throwable) {
showSnackbar(fragment, ErrorInfo(throwable, UserAction.UI_ERROR, request))
}
/**
* Create an error notification. Tapping on the notification opens the error activity. Use
* this method if the exception happens inside a background service (player, subscription
* import, ...) or there is no activity/fragment from which to extract a root view.
*
* @param context the context to use to show the notification
* @param errorInfo the error info to be reported; the error message
* [ErrorInfo.messageStringId] will be shown in the notification
* description
*/
@JvmStatic
fun createNotification(context: Context, errorInfo: ErrorInfo) {
var pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
pendingIntentFlags = pendingIntentFlags or PendingIntent.FLAG_IMMUTABLE
}
val notificationBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(
context,
context.getString(R.string.error_report_channel_id)
)
.setSmallIcon(
// the vector drawable icon causes crashes on KitKat devices
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
R.drawable.ic_bug_report
else
android.R.drawable.stat_notify_error
)
.setContentTitle(context.getString(R.string.error_report_notification_title))
.setContentText(context.getString(errorInfo.messageStringId))
.setAutoCancel(true)
.setContentIntent(
PendingIntent.getActivity(
context,
0,
getErrorActivityIntent(context, errorInfo),
pendingIntentFlags
)
)
NotificationManagerCompat.from(context)
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
// since the notification is silent, also show a toast, otherwise the user is confused
Toast.makeText(context, R.string.error_report_notification_toast, Toast.LENGTH_SHORT)
.show()
}
private fun getErrorActivityIntent(context: Context, errorInfo: ErrorInfo): Intent {
val intent = Intent(context, ErrorActivity::class.java)
intent.putExtra(ErrorActivity.ERROR_INFO, errorInfo)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return intent
}
private fun showSnackbar(context: Context, rootView: View?, errorInfo: ErrorInfo) {
if (rootView == null) {
// fallback to showing a notification if no root view is available
createNotification(context, errorInfo)
} else {
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
.setActionTextColor(Color.YELLOW)
.setAction(context.getString(R.string.error_snackbar_action).uppercase()) {
openActivity(context, errorInfo)
}.show()
}
}
}
}

View File

@@ -26,10 +26,11 @@ public enum UserAction {
DOWNLOAD_OPEN_DIALOG("download open dialog"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed"),
NEW_STREAMS_NOTIFICATIONS("new streams notifications"),
PREFERENCES_MIGRATION("migration of preferences"),
SHARE_TO_NEWPIPE("share to newpipe"),
CHECK_FOR_NEW_APP_VERSION("check for new app version");
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
OPEN_INFO_ITEM_DIALOG("open info item dialog");
private final String message;

View File

@@ -7,12 +7,13 @@ import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorPanelHelper;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.util.InfoCache;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -198,9 +199,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
}
/**
* Show a SnackBar and only call
* {@link ErrorActivity#reportErrorInSnackbar(androidx.fragment.app.Fragment, ErrorInfo)}
* IF we a find a valid view (otherwise the error screen appears).
* Directly calls {@link ErrorUtil#showSnackbar(Fragment, ErrorInfo)}, that shows a snackbar if
* a valid view can be found, otherwise creates an error report notification.
*
* @param errorInfo The error information
*/
@@ -208,6 +208,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (DEBUG) {
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
}
ErrorActivity.reportErrorInSnackbar(this, errorInfo);
ErrorUtil.showSnackbar(this, errorInfo);
}
}

View File

@@ -23,7 +23,7 @@ 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.error.ErrorUtil;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager;
@@ -145,7 +145,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
NavigationHelper.openSearchFragment(getFM(),
ServiceHelper.getSelectedServiceId(activity), "");
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Opening search fragment", e);
ErrorUtil.showUiErrorSnackbar(this, "Opening search fragment", e);
}
return true;
}
@@ -227,16 +227,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public Fragment getItem(final int position) {
final Tab tab = internalTabsList.get(position);
Throwable throwable = null;
Fragment fragment = null;
final Fragment fragment;
try {
fragment = tab.getFragment(context);
} catch (final ExtractionException e) {
throwable = e;
}
if (throwable != null) {
ErrorActivity.reportUiErrorInSnackbar(context, "Getting fragment item", throwable);
ErrorUtil.showUiErrorSnackbar(context, "Getting fragment item", e);
return new BlankFragment();
}

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe.fragments;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
@@ -10,7 +11,7 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager;
*/
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
@Override
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
int pastVisibleItems = 0;

View File

@@ -84,7 +84,7 @@ public class DescriptionFragment extends BaseFragment {
private void setupDescription() {
final Description description = streamInfo.getDescription();
if (description == null || isEmpty(description.getContent())
|| description == Description.emptyDescription) {
|| description == Description.EMPTY_DESCRIPTION) {
binding.detailDescriptionView.setVisibility(View.GONE);
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
return;

View File

@@ -1,5 +1,7 @@
package org.schabi.newpipe.fragments.detail;
import androidx.annotation.NonNull;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import java.io.Serializable;
@@ -46,6 +48,7 @@ class StackItem implements Serializable {
return playQueue;
}
@NonNull
@Override
public String toString() {
return getServiceId() + ":" + getUrl() + " > " + getTitle();

View File

@@ -31,6 +31,7 @@ import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
@@ -43,7 +44,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
@@ -55,8 +56,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
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.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
@@ -94,6 +95,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
@@ -121,6 +123,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientat
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo>
@@ -186,8 +189,6 @@ public final class VideoDetailFragment
@Nullable
private Disposable positionSubscriber = null;
private List<VideoStream> sortedVideoStreams;
private int selectedVideoStreamIndex = -1;
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
private BroadcastReceiver broadcastReceiver;
@@ -500,6 +501,10 @@ public final class VideoDetailFragment
break;
case R.id.detail_thumbnail_root_layout:
autoPlayEnabled = true; // forcefully start playing
// FIXME Workaround #7427
if (isPlayerAvailable()) {
player.setRecovery();
}
openVideoPlayerAutoFullscreen();
break;
case R.id.detail_title_root_layout:
@@ -533,7 +538,7 @@ public final class VideoDetailFragment
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
subChannelUrl, subChannelName);
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
}
}
@@ -659,8 +664,7 @@ public final class VideoDetailFragment
binding.detailControlsCrashThePlayer.setOnClickListener(
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
this.getContext(),
this.player,
getLayoutInflater())
this.player)
);
}
@@ -685,7 +689,7 @@ public final class VideoDetailFragment
});
setupBottomPlayer();
if (!playerHolder.bound) {
if (!playerHolder.isBound()) {
setHeightThumbnail();
} else {
playerHolder.startService(false, this);
@@ -1089,19 +1093,31 @@ public final class VideoDetailFragment
}
private void openBackgroundPlayer(final boolean append) {
final AudioStream audioStream = currentInfo.getAudioStreams()
.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
final boolean useExternalAudioPlayer = PreferenceManager
.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
toggleFullscreenIfInFullscreenMode();
if (isPlayerAvailable()) {
// FIXME Workaround #7427
player.setRecovery();
}
if (!useExternalAudioPlayer) {
openNormalBackgroundPlayer(append);
} else {
startOnExternalPlayer(activity, currentInfo, audioStream);
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
currentInfo.getAudioStreams());
final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams);
if (index == -1) {
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
return;
}
startOnExternalPlayer(activity, currentInfo, audioStreams.get(index));
}
}
@@ -1114,6 +1130,9 @@ public final class VideoDetailFragment
// See UI changes while remote playQueue changes
if (!isPlayerAvailable()) {
playerHolder.startService(false, this);
} else {
// FIXME Workaround #7427
player.setRecovery();
}
toggleFullscreenIfInFullscreenMode();
@@ -1434,7 +1453,7 @@ public final class VideoDetailFragment
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
// Rebound to the service if it was closed via notification or mini player
if (!playerHolder.bound) {
if (!playerHolder.isBound()) {
playerHolder.startService(
false, VideoDetailFragment.this);
}
@@ -1521,6 +1540,8 @@ public final class VideoDetailFragment
animate(binding.detailThumbnailPlayButton, true, 200);
binding.detailVideoTitleView.setText(title);
binding.detailSubChannelThumbnailView.setVisibility(View.GONE);
if (!isEmpty(info.getSubChannelName())) {
displayBothUploaderAndSubChannel(info);
} else if (!isEmpty(info.getUploaderName())) {
@@ -1599,13 +1620,6 @@ public final class VideoDetailFragment
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
sortedVideoStreams = ListHelper.getSortedStreamVideosList(
activity,
info.getVideoStreams(),
info.getVideoOnlyStreams(),
false);
selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(activity, sortedVideoStreams);
updateProgressInfo(info);
initThumbnailViews(info);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
@@ -1631,8 +1645,8 @@ public final class VideoDetailFragment
}
}
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
|| info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE);
binding.detailControlsDownload.setVisibility(
StreamTypeUtil.isLiveStream(info.getStreamType()) ? View.GONE : View.VISIBLE);
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
? View.GONE : View.VISIBLE);
@@ -1673,17 +1687,11 @@ public final class VideoDetailFragment
}
try {
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
final DownloadDialog downloadDialog = new DownloadDialog(activity, currentInfo);
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) {
ErrorActivity.reportErrorInSnackbar(activity,
new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog",
currentInfo));
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
"Showing download dialog", currentInfo));
}
}
@@ -1709,8 +1717,7 @@ public final class VideoDetailFragment
binding.detailPositionView.setVisibility(View.GONE);
// TODO: Remove this check when separation of concerns is done.
// (live streams weren't getting updated because they are mixed)
if (!info.getStreamType().equals(StreamType.LIVE_STREAM)
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
return;
}
} else {
@@ -1870,9 +1877,8 @@ public final class VideoDetailFragment
}
@Override
public void onPlayerError(final ExoPlaybackException error) {
if (error.type == ExoPlaybackException.TYPE_SOURCE
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
public void onPlayerError(final PlaybackException error, final boolean isCatchableException) {
if (!isCatchableException) {
// Properly exit from fullscreen
toggleFullscreenIfInFullscreenMode();
hideMainPlayerOnLoadingNewStream();
@@ -2139,25 +2145,52 @@ public final class VideoDetailFragment
}
private void showExternalPlaybackDialog() {
if (sortedVideoStreams == null) {
if (currentInfo == null) {
return;
}
final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()];
for (int i = 0; i < sortedVideoStreams.size(); i++) {
resolutions[i] = sortedVideoStreams.get(i).getResolution();
}
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
ShareUtils.openUrlInBrowser(requireActivity(), url)
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.select_quality_external_players);
builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
ShareUtils.openUrlInBrowser(requireActivity(), url));
final List<VideoStream> videoStreamsForExternalPlayers =
ListHelper.getSortedStreamVideosList(
activity,
getUrlAndNonTorrentStreams(currentInfo.getVideoStreams()),
getUrlAndNonTorrentStreams(currentInfo.getVideoOnlyStreams()),
false,
false
);
// Maybe there are no video streams available, show just `open in browser` button
if (resolutions.length > 0) {
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> {
dialog.dismiss();
startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i));
}
);
if (videoStreamsForExternalPlayers.isEmpty()) {
builder.setMessage(R.string.no_video_streams_available_for_external_players);
builder.setPositiveButton(R.string.ok, null);
} else {
final int selectedVideoStreamIndexForExternalPlayers =
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
final CharSequence[] resolutions =
new CharSequence[videoStreamsForExternalPlayers.size()];
for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
}
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
null);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.ok, (dialog, i) -> {
final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
// We don't have to manage the index validity because if there is no stream
// available for external players, this code will be not executed and if there is
// no stream which matches the default resolution, 0 is returned by
// ListHelper.getDefaultResolutionIndex.
// The index cannot be outside the bounds of the list as its always between 0 and
// the list size - 1, .
startOnExternalPlayer(activity, currentInfo,
videoStreamsForExternalPlayers.get(index));
});
}
builder.show();
}
@@ -2198,12 +2231,20 @@ public final class VideoDetailFragment
mainFragment.setDescendantFocusability(afterDescendants);
toolbar.setDescendantFocusability(afterDescendants);
((ViewGroup) requireView()).setDescendantFocusability(blockDescendants);
mainFragment.requestFocus();
// Only focus the mainFragment if the mainFragment (e.g. search-results)
// or the toolbar (e.g. Textfield for search) don't have focus.
// This was done to fix problems with the keyboard input, see also #7490
if (!mainFragment.hasFocus() && !toolbar.hasFocus()) {
mainFragment.requestFocus();
}
} else {
mainFragment.setDescendantFocusability(blockDescendants);
toolbar.setDescendantFocusability(blockDescendants);
((ViewGroup) requireView()).setDescendantFocusability(afterDescendants);
binding.detailThumbnailRootLayout.requestFocus();
// Only focus the player if it not already has focus
if (!binding.getRoot().hasFocus()) {
binding.detailThumbnailRootLayout.requestFocus();
}
}
}

View File

@@ -1,5 +1,9 @@
package org.schabi.newpipe.fragments.detail;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED;
import android.content.Context;
import android.util.Log;
import android.view.ContextThemeWrapper;
@@ -15,6 +19,7 @@ import androidx.appcompat.app.AlertDialog;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackException;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
@@ -51,7 +56,8 @@ public final class VideoDetailPlayerCrasher {
exceptionTypes.put(
"Source",
() -> ExoPlaybackException.createForSource(
new IOException(defaultMsg)
new IOException(defaultMsg),
ERROR_CODE_BEHIND_LIVE_WINDOW
)
);
exceptionTypes.put(
@@ -61,13 +67,16 @@ public final class VideoDetailPlayerCrasher {
"Dummy renderer",
0,
null,
C.FORMAT_HANDLED
C.FORMAT_HANDLED,
/*isRecoverable=*/false,
ERROR_CODE_DECODING_FAILED
)
);
exceptionTypes.put(
"Unexpected",
() -> ExoPlaybackException.createForUnexpected(
new RuntimeException(defaultMsg)
new RuntimeException(defaultMsg),
ERROR_CODE_UNSPECIFIED
)
);
exceptionTypes.put(
@@ -88,8 +97,7 @@ public final class VideoDetailPlayerCrasher {
public static void onCrashThePlayer(
@NonNull final Context context,
@Nullable final Player player,
@NonNull final LayoutInflater layoutInflater
@Nullable final Player player
) {
if (player == null) {
Log.d(TAG, "Player is not available");
@@ -100,16 +108,15 @@ public final class VideoDetailPlayerCrasher {
}
// -- Build the dialog/UI --
final Context themeWrapperContext = getThemeWrapperContext(context);
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater)
.list;
final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context))
final SingleChoiceDialogViewBinding binding =
SingleChoiceDialogViewBinding.inflate(inflater);
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapperContext)
.setTitle("Choose an exception")
.setView(radioGroup)
.setView(binding.getRoot())
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.create();
@@ -127,11 +134,9 @@ public final class VideoDetailPlayerCrasher {
);
radioButton.setOnClickListener(v -> {
tryCrashPlayerWith(player, entry.getValue().get());
if (alertDialog != null) {
alertDialog.cancel();
}
alertDialog.cancel();
});
radioGroup.addView(radioButton);
binding.list.addView(radioButton);
}
alertDialog.show();
@@ -139,7 +144,7 @@ public final class VideoDetailPlayerCrasher {
/**
* Note that this method does not crash the underlying exoplayer directly (it's not possible).
* It simply supplies a Exception to {@link Player#onPlayerError(ExoPlaybackException)}.
* It simply supplies a Exception to {@link Player#onPlayerError(PlaybackException)}.
* @param player
* @param exception
*/

View File

@@ -1,6 +1,8 @@
package org.schabi.newpipe.fragments.list;
import android.app.Activity;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
@@ -17,37 +19,26 @@ import androidx.appcompat.app.ActionBar;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
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.error.ErrorUtil;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.player.helper.PlayerHolder;
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;
import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.views.SuperScrollLayoutManager;
import java.util.ArrayList;
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;
import java.util.function.Supplier;
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, StateSaver.WriteRead,
@@ -79,11 +70,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
}
}
@Override
public void onDetach() {
super.onDetach();
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -220,14 +206,10 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
@Nullable
protected ViewBinding getListHeader() {
protected Supplier<View> getListHeaderSupplier() {
return null;
}
protected ViewBinding getListFooter() {
return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
}
protected RecyclerView.LayoutManager getListLayoutManager() {
return new SuperScrollLayoutManager(activity);
}
@@ -252,11 +234,10 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setUseGridVariant(useGrid);
infoListAdapter.setFooter(getListFooter().getRoot());
final ViewBinding listHeader = getListHeader();
if (listHeader != null) {
infoListAdapter.setHeader(listHeader.getRoot());
final Supplier<View> listHeaderSupplier = getListHeaderSupplier();
if (listHeaderSupplier != null) {
infoListAdapter.setHeaderSupplier(listHeaderSupplier);
}
itemsList.setAdapter(infoListAdapter);
@@ -271,7 +252,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
@Override
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final StreamInfoItem selectedItem) {
onStreamSelected(selectedItem);
@@ -279,11 +260,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
@Override
public void held(final StreamInfoItem selectedItem) {
showStreamDialog(selectedItem);
showInfoItemDialog(selectedItem);
}
});
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final ChannelInfoItem selectedItem) {
try {
@@ -293,13 +274,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(
ErrorUtil.showUiErrorSnackbar(
BaseListFragment.this, "Opening channel fragment", e);
}
}
});
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final PlaylistInfoItem selectedItem) {
try {
@@ -309,28 +290,105 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(BaseListFragment.this,
ErrorUtil.showUiErrorSnackbar(BaseListFragment.this,
"Opening playlist fragment", e);
}
}
});
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final CommentsInfoItem selectedItem) {
onItemSelected(selectedItem);
}
});
// Ensure that there is always a scroll listener (e.g. when rotating the device)
useNormalItemListScrollListener();
}
/**
* Removes all listeners and adds the normal scroll listener to the {@link #itemsList}.
*/
protected void useNormalItemListScrollListener() {
if (DEBUG) {
Log.d(TAG, "useNormalItemListScrollListener called");
}
itemsList.clearOnScrollListeners();
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener());
}
/**
* Removes all listeners and adds the initial scroll listener to the {@link #itemsList}.
* <br/>
* Which tries to load more items when not enough are in the view (not scrollable)
* and more are available.
* <br/>
* Note: This method only works because "This callback will also be called if visible
* item range changes after a layout calculation. In that case, dx and dy will be 0."
* - which might be unexpected because no actual scrolling occurs...
* <br/>
* This listener will be replaced by DefaultItemListOnScrolledDownListener when
* <ul>
* <li>the view was actually scrolled</li>
* <li>the view is scrollable</li>
* <li>no more items can be loaded</li>
* </ul>
*/
protected void useInitialItemListLoadScrollListener() {
if (DEBUG) {
Log.d(TAG, "useInitialItemListLoadScrollListener called");
}
itemsList.clearOnScrollListeners();
itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener() {
@Override
public void onScrolledDown(final RecyclerView recyclerView) {
onScrollToBottom();
public void onScrolled(@NonNull final RecyclerView recyclerView,
final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy != 0) {
log("Vertical scroll occurred");
useNormalItemListScrollListener();
return;
}
if (isLoading.get()) {
log("Still loading data -> Skipping");
return;
}
if (!hasMoreItems()) {
log("No more items to load");
useNormalItemListScrollListener();
return;
}
if (itemsList.canScrollVertically(1)
|| itemsList.canScrollVertically(-1)) {
log("View is scrollable");
useNormalItemListScrollListener();
return;
}
log("Loading more data");
loadMoreItems();
}
private void log(final String msg) {
if (DEBUG) {
Log.d(TAG, "initItemListLoadScrollListener - " + msg);
}
}
});
}
class DefaultItemListOnScrolledDownListener extends OnScrollBelowItemsListener {
@Override
public void onScrolledDown(final RecyclerView recyclerView) {
onScrollToBottom();
}
}
private void onStreamSelected(final StreamInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
@@ -344,55 +402,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
}
}
protected void showStreamDialog(final StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || activity == null) {
return;
protected void showInfoItemDialog(final StreamInfoItem item) {
try {
new InfoItemDialog.Builder(getActivity(), getContext(), this, item).create().show();
} catch (final IllegalArgumentException e) {
InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
}
final List<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getInstance().isPlayerOpen()) {
entries.add(StreamDialogEntry.enqueue);
if (PlayerHolder.getInstance().getQueueSize() > 1) {
entries.add(StreamDialogEntry.enqueue_next);
}
}
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share
));
} else {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share
));
}
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
// show "mark as watched" only when watch history is enabled
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
entries.add(
StreamDialogEntry.mark_as_watched
);
}
if (!isNullOrEmpty(item.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
StreamDialogEntry.setEnabledEntries(entries);
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -418,6 +433,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void startLoading(final boolean forceLoad) {
useInitialItemListLoadScrollListener();
super.startLoading(forceLoad);
}
protected abstract void loadMoreItems();
protected abstract boolean hasMoreItems();

View File

@@ -9,6 +9,7 @@ import androidx.annotation.NonNull;
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.ListInfo;
import org.schabi.newpipe.extractor.Page;
@@ -27,8 +28,8 @@ import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public abstract class BaseListInfoFragment<I extends ListInfo>
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInfo<I>>
extends BaseListFragment<L, ListExtractor.InfoItemsPage<I>> {
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@State
@@ -37,7 +38,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
protected String url;
private final UserAction errorUserAction;
protected I currentInfo;
protected L currentInfo;
protected Page currentNextPage;
protected Disposable currentWorker;
@@ -65,7 +66,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
super.onResume();
// Check if it was loading when the fragment was stopped/paused,
if (wasLoading.getAndSet(false)) {
if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) {
if (hasMoreItems() && !infoListAdapter.getItemsList().isEmpty()) {
loadMoreItems();
} else {
doInitialLoadLogic();
@@ -97,7 +98,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@SuppressWarnings("unchecked")
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects);
currentInfo = (I) savedObjects.poll();
currentInfo = (L) savedObjects.poll();
currentNextPage = (Page) savedObjects.poll();
}
@@ -105,6 +106,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void doInitialLoadLogic() {
if (DEBUG) {
Log.d(TAG, "doInitialLoadLogic() called");
@@ -123,7 +125,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
* @param forceLoad allow or disallow the result to come from the cache
* @return Rx {@link Single} containing the {@link ListInfo}
*/
protected abstract Single<I> loadResult(boolean forceLoad);
protected abstract Single<L> loadResult(boolean forceLoad);
@Override
public void startLoading(final boolean forceLoad) {
@@ -139,7 +141,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
currentWorker = loadResult(forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull I result) -> {
.subscribe((@NonNull L result) -> {
isLoading.set(false);
currentInfo = result;
currentNextPage = result.getNextPage();
@@ -156,8 +158,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
*
* @return Rx {@link Single} containing the {@link ListExtractor.InfoItemsPage}
*/
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
protected abstract Single<ListExtractor.InfoItemsPage<I>> loadMoreItemsLogic();
@Override
protected void loadMoreItems() {
isLoading.set(true);
@@ -171,9 +174,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally(this::allowDownwardFocusScroll)
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
.subscribe(infoItemsPage -> {
isLoading.set(false);
handleNextItems(InfoItemsPage);
handleNextItems(infoItemsPage);
}, (@NonNull Throwable throwable) ->
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
errorUserAction, "Loading more items: " + url, serviceId)));
@@ -192,7 +195,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage<I> result) {
super.handleNextItems(result);
currentNextPage = result.getNextPage();
@@ -216,14 +219,14 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void handleResult(@NonNull final I result) {
public void handleResult(@NonNull final L result) {
super.handleResult(result);
name = result.getName();
setTitle(name);
if (infoListAdapter.getItemsList().isEmpty()) {
if (result.getRelatedItems().size() > 0) {
if (!result.getRelatedItems().isEmpty()) {
infoListAdapter.addInfoItemList(result.getRelatedItems());
showListFooter(hasMoreItems());
} else {
@@ -240,7 +243,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
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);
errors.removeIf(ContentNotSupportedException.class::isInstance);
if (!errors.isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),

View File

@@ -1,6 +1,11 @@
package org.schabi.newpipe.fragments.list.channel;
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
@@ -17,19 +22,19 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.core.content.ContextCompat;
import androidx.viewbinding.ViewBinding;
import com.google.android.material.snackbar.Snackbar;
import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.NotificationMode;
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.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
@@ -37,19 +42,21 @@ 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.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
@@ -61,11 +68,7 @@ import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, ChannelInfo>
implements View.OnClickListener {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
@@ -74,6 +77,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
private final CompositeDisposable disposables = new CompositeDisposable();
private Disposable subscribeButtonMonitor;
private boolean channelContentNotSupported = false;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
@@ -85,6 +90,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
private PlaylistControlBinding playlistControlBinding;
private MenuItem menuRssButton;
private MenuItem menuNotifyButton;
public static ChannelFragment getInstance(final int serviceId, final String url,
final String name) {
@@ -126,6 +132,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
channelBinding = FragmentChannelBinding.bind(rootView);
showContentNotSupportedIfNeeded();
}
@Override
@@ -145,12 +152,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
//////////////////////////////////////////////////////////////////////////*/
@Override
protected ViewBinding getListHeader() {
protected Supplier<View> getListHeaderSupplier() {
headerBinding = ChannelHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
return headerBinding;
return headerBinding::getRoot;
}
@Override
@@ -180,13 +187,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
menuRssButton = menu.findItem(R.id.menu_item_rss);
}
}
private void openRssFeed() {
final ChannelInfo info = currentInfo;
if (info != null) {
ShareUtils.openUrlInBrowser(requireContext(), info.getFeedUrl(), false);
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
}
}
@@ -196,8 +197,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.menu_item_rss:
openRssFeed();
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(
requireContext(), currentInfo.getFeedUrl(), false);
}
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
@@ -237,15 +246,22 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
.subscribe(getSubscribeUpdateMonitor(info), onError));
disposables.add(observable
// Some updates are very rapid
// (for example when calling the updateSubscription(info))
// so only update the UI for the latest emission
// ("sync" the subscribe button's state)
.debounce(100, TimeUnit.MILLISECONDS)
.map(List::isEmpty)
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
updateSubscribeButton(!subscriptionEntities.isEmpty()), onError));
.subscribe(isEmpty -> updateSubscribeButton(!isEmpty), onError));
disposables.add(observable
.map(List::isEmpty)
.distinctUntilChanged()
.skip(1) // channel has just been opened
.filter(x -> NotificationHelper.areNewStreamsNotificationsEnabled(requireContext()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(isEmpty -> {
if (!isEmpty) {
showNotifySnackbar();
}
}, onError));
}
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
@@ -325,6 +341,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
info.getAvatarUrl(),
info.getDescription(),
info.getSubscriberCount());
updateNotifyButton(null);
subscribeButtonMonitor = monitorSubscribeButton(
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
} else {
@@ -332,6 +349,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
Log.d(TAG, "Found subscription to this channel!");
}
final SubscriptionEntity subscription = subscriptionEntities.get(0);
updateNotifyButton(subscription);
subscribeButtonMonitor = monitorSubscribeButton(
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
}
@@ -374,12 +392,51 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
AnimationType.LIGHT_SCALE_AND_ALPHA);
}
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
if (menuNotifyButton == null) {
return;
}
if (subscription != null) {
menuNotifyButton.setEnabled(
NotificationHelper.areNewStreamsNotificationsEnabled(requireContext())
);
menuNotifyButton.setChecked(
subscription.getNotificationMode() == NotificationMode.ENABLED
);
}
menuNotifyButton.setVisible(subscription != null);
}
private void setNotify(final boolean isEnabled) {
disposables.add(
subscriptionManager
.updateNotificationMode(
currentInfo.getServiceId(),
currentInfo.getUrl(),
isEnabled ? NotificationMode.ENABLED : NotificationMode.DISABLED)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
);
}
/**
* Show a snackbar with the option to enable notifications on new streams for this channel.
*/
private void showNotifySnackbar() {
Snackbar.make(itemsList, R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
.setAction(R.string.get_notified, v -> setNotify(true))
.setActionTextColor(Color.YELLOW)
.show();
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
}
@@ -407,7 +464,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
currentInfo.getParentChannelUrl(),
currentInfo.getParentChannelName());
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
}
} else if (DEBUG) {
Log.i(TAG, "Can't open parent channel because we got no channel URL");
@@ -470,9 +527,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
playlistControlBinding.getRoot().setVisibility(View.GONE);
}
channelContentNotSupported = false;
for (final Throwable throwable : result.getErrors()) {
if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported();
channelContentNotSupported = true;
showContentNotSupportedIfNeeded();
break;
}
}
@@ -504,7 +564,13 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
});
}
private void showContentNotSupported() {
private void showContentNotSupportedIfNeeded() {
// channelBinding might not be initialized when handleResult() is called
// (e.g. after rotating the screen, #6696)
if (!channelContentNotSupported || channelBinding == null) {
return;
}
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
channelBinding.channelKaomoji.setText("(︶︹︺)");
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
@@ -516,12 +582,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
}
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> streamItems = new ArrayList<>();
for (final InfoItem i : infoListAdapter.getItemsList()) {
if (i instanceof StreamInfoItem) {
streamItems.add((StreamInfoItem) i);
}
}
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList());
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
currentInfo.getNextPage(), streamItems, index);
}

View File

@@ -15,6 +15,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -22,7 +23,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, CommentsInfo> {
private final CompositeDisposable disposables = new CompositeDisposable();
private TextView emptyStateDesc;
@@ -67,7 +68,7 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
}

View File

@@ -21,6 +21,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
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.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.KioskTranslator;
@@ -53,7 +54,7 @@ import io.reactivex.rxjava3.core.Single;
* </p>
*/
public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
public class KioskFragment extends BaseListInfoFragment<StreamInfoItem, KioskInfo> {
@State
String kioskId = "";
String kioskTranslatedName;
@@ -145,7 +146,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
}
@Override
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
public Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage);
}

View File

@@ -1,6 +1,8 @@
package org.schabi.newpipe.fragments.list.playlist;
import android.app.Activity;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
@@ -15,17 +17,20 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.viewbinding.ViewBinding;
import com.google.android.material.shape.CornerFamily;
import com.google.android.material.shape.ShapeAppearanceModel;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
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.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
@@ -33,26 +38,25 @@ import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
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.util.ExtractorHelper;
import org.schabi.newpipe.util.PicassoHelper;
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.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.StreamDialogEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
@@ -60,11 +64,7 @@ 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.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo> {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
@@ -120,12 +120,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected ViewBinding getListHeader() {
protected Supplier<View> getListHeaderSupplier() {
headerBinding = PlaylistHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
return headerBinding;
return headerBinding::getRoot;
}
@Override
@@ -140,60 +140,22 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
@Override
protected void showStreamDialog(final StreamInfoItem item) {
protected void showInfoItemDialog(final StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || activity == null) {
return;
try {
final InfoItemDialog.Builder dialogBuilder =
new InfoItemDialog.Builder(getActivity(), context, this, item);
dialogBuilder
.setAction(
StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
(f, infoItem) -> NavigationHelper.playOnBackgroundPlayer(
context, getPlayQueueStartingAt(infoItem), true))
.create()
.show();
} catch (final IllegalArgumentException e) {
InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
}
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getInstance().isPlayerOpen()) {
entries.add(StreamDialogEntry.enqueue);
if (PlayerHolder.getInstance().getQueueSize() > 1) {
entries.add(StreamDialogEntry.enqueue_next);
}
}
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share
));
} else {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share
));
}
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
// show "mark as watched" only when watch history is enabled
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
entries.add(
StreamDialogEntry.mark_as_watched
);
}
if (!isNullOrEmpty(item.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
NavigationHelper.playOnBackgroundPlayer(context,
getPlayQueueStartingAt(infoItem), true));
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
}
@Override
@@ -249,7 +211,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage);
}
@@ -268,11 +230,25 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url, currentInfo.getThumbnailUrl());
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name, url,
currentInfo.getThumbnailUrl());
}
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
break;
case R.id.menu_item_append_playlist:
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
break;
default:
return super.onOptionsItemSelected(item);
}
@@ -310,7 +286,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
result.getUploaderUrl(), result.getUploaderName());
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
}
});
}
@@ -325,9 +301,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
// this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown
headerBinding.uploaderAvatarView.setDisableCircularTransformation(true);
headerBinding.uploaderAvatarView.setBorderColor(
getResources().getColor(R.color.transparent_background_color));
final ShapeAppearanceModel model = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, 0f)
.build(); // this turns the image back into a square
headerBinding.uploaderAvatarView.setShapeAppearanceModel(model);
headerBinding.uploaderAvatarView.setStrokeColor(AppCompatResources
.getColorStateList(requireContext(), R.color.transparent_background_color));
headerBinding.uploaderAvatarView.setImageDrawable(
AppCompatResources.getDrawable(requireContext(),
R.drawable.ic_radio)
@@ -410,7 +389,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
return new Subscriber<List<PlaylistRemoteEntity>>() {
return new Subscriber<>() {
@Override
public void onSubscribe(final Subscription s) {
if (bookmarkReactor != null) {

View File

@@ -1,5 +1,10 @@
package org.schabi.newpipe.fragments.list.search;
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static java.util.Arrays.asList;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -20,7 +25,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
@@ -29,17 +33,15 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
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.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
@@ -61,6 +63,7 @@ import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.KeyboardUtil;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
@@ -68,12 +71,11 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@@ -84,11 +86,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
import static java.util.Arrays.asList;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>>
implements BackPressable {
/*//////////////////////////////////////////////////////////////////////////
@@ -225,8 +222,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
try {
service = NewPipe.getService(serviceId);
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this,
"Getting service for id " + serviceId, e);
ErrorUtil.showUiErrorSnackbar(this, "Getting service for id " + serviceId, e);
}
}
@@ -673,31 +669,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) {
Log.d(TAG, "showKeyboardSearch() called");
}
if (searchEditText == null) {
return;
}
if (searchEditText.requestFocus()) {
final InputMethodManager imm = ContextCompat.getSystemService(activity,
InputMethodManager.class);
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED);
}
KeyboardUtil.showKeyboard(activity, searchEditText);
}
private void hideKeyboardSearch() {
if (DEBUG) {
Log.d(TAG, "hideKeyboardSearch() called");
}
if (searchEditText == null) {
return;
}
final InputMethodManager imm = ContextCompat.getSystemService(activity,
InputMethodManager.class);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
InputMethodManager.RESULT_UNCHANGED_SHOWN);
searchEditText.clearFocus();
KeyboardUtil.hideKeyboard(activity, searchEditText);
}
private void showDeleteSuggestionDialog(final SuggestionItem item) {
@@ -727,7 +707,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
public boolean onBackPressed() {
if (suggestionsPanelVisible
&& infoListAdapter.getItemsList().size() > 0
&& !infoListAdapter.getItemsList().isEmpty()
&& !isLoading.get()) {
hideSuggestionsPanel();
hideKeyboardSearch();
@@ -743,13 +723,10 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
return historyRecordManager
.getRelatedSearches(query, similarQueryLimit, 25)
.toObservable()
.map(searchHistoryEntries -> {
final Set<SuggestionItem> result = new HashSet<>(); // remove duplicates
for (final SearchHistoryEntry entry : searchHistoryEntries) {
result.add(new SuggestionItem(true, entry.getSearch()));
}
return new ArrayList<>(result);
});
.map(searchHistoryEntries ->
searchHistoryEntries.stream()
.map(entry -> new SuggestionItem(true, entry))
.collect(Collectors.toList()));
}
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
@@ -34,8 +35,10 @@ public class SuggestionListAdapter
this.listener = listener;
}
@NonNull
@Override
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent,
final int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context)
.inflate(R.layout.item_search_suggestion, parent, false));
}

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe.fragments.list.videos;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -12,11 +11,11 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
@@ -24,14 +23,14 @@ import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.util.RelatedItemInfo;
import java.io.Serializable;
import java.util.function.Supplier;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemInfo>
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String INFO_KEY = "related_info_key";
private final CompositeDisposable disposables = new CompositeDisposable();
private RelatedItemInfo relatedItemInfo;
/*//////////////////////////////////////////////////////////////////////////
@@ -54,11 +53,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@@ -66,12 +60,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
return inflater.inflate(R.layout.fragment_related_items, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
disposables.clear();
}
@Override
public void onDestroyView() {
headerBinding = null;
@@ -79,26 +67,27 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
}
@Override
protected ViewBinding getListHeader() {
if (relatedItemInfo != null && relatedItemInfo.getRelatedItems() != null) {
headerBinding = RelatedItemsHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
final SharedPreferences pref = PreferenceManager
.getDefaultSharedPreferences(requireContext());
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
headerBinding.autoplaySwitch.setChecked(autoplay);
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
.putBoolean(getString(R.string.auto_queue_key), b).apply());
return headerBinding;
} else {
protected Supplier<View> getListHeaderSupplier() {
if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) {
return null;
}
headerBinding = RelatedItemsHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
final SharedPreferences pref = PreferenceManager
.getDefaultSharedPreferences(requireContext());
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
headerBinding.autoplaySwitch.setChecked(autoplay);
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
.putBoolean(getString(R.string.auto_queue_key), b).apply());
return headerBinding::getRoot;
}
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
protected Single<ListExtractor.InfoItemsPage<InfoItem>> loadMoreItemsLogic() {
return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage);
}
@@ -128,7 +117,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
}
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -137,11 +125,13 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
@Override
public void setTitle(final String title) {
// Nothing to do - override parent
}
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
// Nothing to do - override parent
}
private void setInitialData(final StreamInfo info) {
@@ -169,11 +159,10 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String s) {
final SharedPreferences pref =
PreferenceManager.getDefaultSharedPreferences(requireContext());
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
if (headerBinding != null) {
headerBinding.autoplaySwitch.setChecked(autoplay);
headerBinding.autoplaySwitch.setChecked(
sharedPreferences.getBoolean(
getString(R.string.auto_queue_key), false));
}
}

View File

@@ -1,54 +0,0 @@
package org.schabi.newpipe.info_list;
import android.app.Activity;
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;
public class InfoItemDialog {
private final AlertDialog dialog;
public InfoItemDialog(@NonNull final Activity activity,
@NonNull final StreamInfoItem info,
@NonNull final String[] commands,
@NonNull final DialogInterface.OnClickListener actions) {
this(activity, commands, actions, info.getName(), info.getUploaderName());
}
public InfoItemDialog(@NonNull final Activity activity,
@NonNull final String[] commands,
@NonNull final DialogInterface.OnClickListener actions,
@NonNull final String title,
@Nullable final String additionalDetail) {
final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
bannerView.setSelected(true);
final TextView titleView = bannerView.findViewById(R.id.itemTitleView);
titleView.setText(title);
final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
if (additionalDetail != null) {
detailsView.setText(additionalDetail);
detailsView.setVisibility(View.VISIBLE);
} else {
detailsView.setVisibility(View.GONE);
}
dialog = new AlertDialog.Builder(activity)
.setCustomTitle(bannerView)
.setItems(commands, actions)
.create();
}
public void show() {
dialog.show();
}
}

View File

@@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -10,7 +11,7 @@ import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
@@ -34,6 +35,7 @@ import org.schabi.newpipe.util.OnClickGesture;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/*
* Created by Christian Schabesberger on 01.08.16.
@@ -74,18 +76,20 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
private static final int COMMENT_HOLDER_TYPE = 0x401;
private final LayoutInflater layoutInflater;
private final InfoItemBuilder infoItemBuilder;
private final ArrayList<InfoItem> infoItemList;
private final List<InfoItem> infoItemList;
private final HistoryRecordManager recordManager;
private boolean useMiniVariant = false;
private boolean useGridVariant = false;
private boolean showFooter = false;
private View header = null;
private View footer = null;
private Supplier<View> headerSupplier = null;
public InfoListAdapter(final Context context) {
this.recordManager = new HistoryRecordManager(context);
layoutInflater = LayoutInflater.from(context);
recordManager = new HistoryRecordManager(context);
infoItemBuilder = new InfoItemBuilder(context);
infoItemList = new ArrayList<>();
}
@@ -129,12 +133,12 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
if (DEBUG) {
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
+ "infoItemList.size() = " + infoItemList.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "hasHeader = " + hasHeader() + ", "
+ "showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
if (footer != null && showFooter) {
if (showFooter) {
final int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(offsetStart, footerNow);
@@ -145,43 +149,6 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}
}
public void setInfoItemList(final List<? extends InfoItem> data) {
infoItemList.clear();
infoItemList.addAll(data);
notifyDataSetChanged();
}
public void addInfoItem(@Nullable final InfoItem data) {
if (data == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "addInfoItem() before > infoItemList.size() = "
+ infoItemList.size() + ", thread = " + Thread.currentThread());
}
final int positionInserted = sizeConsideringHeaderOffset();
infoItemList.add(data);
if (DEBUG) {
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", "
+ "infoItemList.size() = " + infoItemList.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "showFooter = " + showFooter);
}
notifyItemInserted(positionInserted);
if (footer != null && showFooter) {
final int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(positionInserted, footerNow);
if (DEBUG) {
Log.d(TAG, "addInfoItem() footer from " + positionInserted
+ " to " + footerNow);
}
}
}
public void clearStreamItemList() {
if (infoItemList.isEmpty()) {
return;
@@ -190,16 +157,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
notifyDataSetChanged();
}
public void setHeader(final View header) {
final boolean changed = header != this.header;
this.header = header;
public void setHeaderSupplier(@Nullable final Supplier<View> headerSupplier) {
final boolean changed = headerSupplier != this.headerSupplier;
this.headerSupplier = headerSupplier;
if (changed) {
notifyDataSetChanged();
}
}
public void setFooter(final View view) {
this.footer = view;
protected boolean hasHeader() {
return this.headerSupplier != null;
}
public void showFooter(final boolean show) {
@@ -219,48 +186,49 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}
private int sizeConsideringHeaderOffset() {
final int i = infoItemList.size() + (header != null ? 1 : 0);
final int i = infoItemList.size() + (hasHeader() ? 1 : 0);
if (DEBUG) {
Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
}
return i;
}
public ArrayList<InfoItem> getItemsList() {
public List<InfoItem> getItemsList() {
return infoItemList;
}
@Override
public int getItemCount() {
int count = infoItemList.size();
if (header != null) {
if (hasHeader()) {
count++;
}
if (footer != null && showFooter) {
if (showFooter) {
count++;
}
if (DEBUG) {
Log.d(TAG, "getItemCount() called with: "
+ "count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "hasHeader = " + hasHeader() + ", "
+ "showFooter = " + showFooter);
}
return count;
}
@SuppressWarnings("FinalParameters")
@Override
public int getItemViewType(int position) {
if (DEBUG) {
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
}
if (header != null && position == 0) {
if (hasHeader() && position == 0) {
return HEADER_TYPE;
} else if (header != null) {
} else if (hasHeader()) {
position--;
}
if (footer != null && position == infoItemList.size() && showFooter) {
if (position == infoItemList.size() && showFooter) {
return FOOTER_TYPE;
}
final InfoItem item = infoItemList.get(position);
@@ -290,10 +258,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
+ "parent = [" + parent + "], type = [" + type + "]");
}
switch (type) {
// #4475 and #3368
// Always create a new instance otherwise the same instance
// is sometimes reused which causes a crash
case HEADER_TYPE:
return new HFHolder(header);
return new HFHolder(headerSupplier.get());
case FOOTER_TYPE:
return new HFHolder(footer);
return new HFHolder(PignateFooterBinding
.inflate(layoutInflater, parent, false)
.getRoot()
);
case MINI_STREAM_HOLDER_TYPE:
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
case STREAM_HOLDER_TYPE:
@@ -322,42 +296,17 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
final int position) {
if (DEBUG) {
Log.d(TAG, "onBindViewHolder() called with: "
+ "holder = [" + holder.getClass().getSimpleName() + "], "
+ "position = [" + position + "]");
}
if (holder instanceof InfoItemHolder) {
// If header isn't null, offset the items by -1
if (header != null) {
position--;
}
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager);
} else if (holder instanceof HFHolder && position == 0 && header != null) {
((HFHolder) holder).view = header;
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset()
&& footer != null && showFooter) {
((HFHolder) holder).view = footer;
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position,
@NonNull final List<Object> payloads) {
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
for (final Object payload : payloads) {
if (payload instanceof StreamStateEntity) {
((InfoItemHolder) holder).updateState(infoItemList
.get(header == null ? position : position - 1), recordManager);
} else if (payload instanceof Boolean) {
((InfoItemHolder) holder).updateState(infoItemList
.get(header == null ? position : position - 1), recordManager);
}
}
} else {
onBindViewHolder(holder, position);
((InfoItemHolder) holder).updateFromItem(
// If header is present, offset the items by -1
infoItemList.get(hasHeader() ? position - 1 : position), recordManager);
}
}
@@ -371,12 +320,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
};
}
public static class HFHolder extends RecyclerView.ViewHolder {
public View view;
static class HFHolder extends RecyclerView.ViewHolder {
HFHolder(final View v) {
super(v);
view = v;
}
}
}

View File

@@ -0,0 +1,354 @@
package org.schabi.newpipe.info_list.dialog;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
/**
* Dialog for a {@link StreamInfoItem}.
* The dialog's content are actions that can be performed on the {@link StreamInfoItem}.
* This dialog is mostly used for longpress context menus.
*/
public final class InfoItemDialog {
private static final String TAG = Build.class.getSimpleName();
/**
* Ideally, {@link InfoItemDialog} would extend {@link AlertDialog}.
* However, extending {@link AlertDialog} requires many additional lines
* and brings more complexity to this class, especially the constructor.
* To circumvent this, an {@link AlertDialog.Builder} is used in the constructor.
* Its result is stored in this class variable to allow access via the {@link #show()} method.
*/
private final AlertDialog dialog;
private InfoItemDialog(@NonNull final Activity activity,
@NonNull final Fragment fragment,
@NonNull final StreamInfoItem info,
@NonNull final List<StreamDialogEntry> entries) {
// Create the dialog's title
final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
bannerView.setSelected(true);
final TextView titleView = bannerView.findViewById(R.id.itemTitleView);
titleView.setText(info.getName());
final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
if (info.getUploaderName() != null) {
detailsView.setText(info.getUploaderName());
detailsView.setVisibility(View.VISIBLE);
} else {
detailsView.setVisibility(View.GONE);
}
// Get the entry's descriptions which are displayed in the dialog
final String[] items = entries.stream()
.map(entry -> entry.getString(activity)).toArray(String[]::new);
// Call an entry's action / onClick method when the entry is selected.
final DialogInterface.OnClickListener action = (d, index) ->
entries.get(index).action.onClick(fragment, info);
dialog = new AlertDialog.Builder(activity)
.setCustomTitle(bannerView)
.setItems(items, action)
.create();
}
public void show() {
dialog.show();
}
/**
* <p>Builder to generate a {@link InfoItemDialog} for a {@link StreamInfoItem}.</p>
* Use {@link #addEntry(StreamDialogDefaultEntry)}
* and {@link #addAllEntries(StreamDialogDefaultEntry...)} to add options to the dialog.
* <br>
* Custom actions for entries can be set using
* {@link #setAction(StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)}.
*/
public static class Builder {
@NonNull private final Activity activity;
@NonNull private final Context context;
@NonNull private final StreamInfoItem infoItem;
@NonNull private final Fragment fragment;
@NonNull private final List<StreamDialogEntry> entries = new ArrayList<>();
private final boolean addDefaultEntriesAutomatically;
/**
* <p>Create a {@link Builder builder} instance for a {@link StreamInfoItem}
* that automatically adds the some default entries
* at the top and bottom of the dialog.</p>
* The dialog has the following structure:
* <pre>
* + - - - - - - - - - - - - - - - - - - - - - -+
* | ENQUEUE |
* | ENQUEUE_NEXT |
* | START_ON_BACKGROUND |
* | START_ON_POPUP |
* + - - - - - - - - - - - - - - - - - - - - - -+
* | entries added manually with |
* | addEntry() and addAllEntries() |
* + - - - - - - - - - - - - - - - - - - - - - -+
* | APPEND_PLAYLIST |
* | SHARE |
* | OPEN_IN_BROWSER |
* | PLAY_WITH_KODI |
* | MARK_AS_WATCHED |
* | SHOW_CHANNEL_DETAILS |
* + - - - - - - - - - - - - - - - - - - - - - -+
* </pre>
* Please note that some entries are not added depending on the user's preferences,
* the item's {@link StreamType} and the current player state.
*
* @param activity
* @param context
* @param fragment
* @param infoItem the item for this dialog; all entries and their actions work with
* this {@link StreamInfoItem}
* @throws IllegalArgumentException if <code>activity, context</code>
* or resources is <code>null</code>
*/
public Builder(final Activity activity,
final Context context,
@NonNull final Fragment fragment,
@NonNull final StreamInfoItem infoItem) {
this(activity, context, fragment, infoItem, true);
}
/**
* <p>Create an instance of this {@link Builder} for a {@link StreamInfoItem}.</p>
* <p>If {@code addDefaultEntriesAutomatically} is set to {@code true},
* some default entries are added to the top and bottom of the dialog.</p>
* The dialog has the following structure:
* <pre>
* + - - - - - - - - - - - - - - - - - - - - - -+
* | ENQUEUE |
* | ENQUEUE_NEXT |
* | START_ON_BACKGROUND |
* | START_ON_POPUP |
* + - - - - - - - - - - - - - - - - - - - - - -+
* | entries added manually with |
* | addEntry() and addAllEntries() |
* + - - - - - - - - - - - - - - - - - - - - - -+
* | APPEND_PLAYLIST |
* | SHARE |
* | OPEN_IN_BROWSER |
* | PLAY_WITH_KODI |
* | MARK_AS_WATCHED |
* | SHOW_CHANNEL_DETAILS |
* + - - - - - - - - - - - - - - - - - - - - - -+
* </pre>
* Please note that some entries are not added depending on the user's preferences,
* the item's {@link StreamType} and the current player state.
*
* @param activity
* @param context
* @param fragment
* @param infoItem
* @param addDefaultEntriesAutomatically
* whether default entries added with {@link #addDefaultBeginningEntries()}
* and {@link #addDefaultEndEntries()} are added automatically when generating
* the {@link InfoItemDialog}.
* <br/>
* Entries added with {@link #addEntry(StreamDialogDefaultEntry)} and
* {@link #addAllEntries(StreamDialogDefaultEntry...)} are added in between.
* @throws IllegalArgumentException if <code>activity, context</code>
* or resources is <code>null</code>
*/
public Builder(final Activity activity,
final Context context,
@NonNull final Fragment fragment,
@NonNull final StreamInfoItem infoItem,
final boolean addDefaultEntriesAutomatically) {
if (activity == null || context == null || context.getResources() == null) {
if (DEBUG) {
Log.d(TAG, "activity, context or resources is null: activity = "
+ activity + ", context = " + context);
}
throw new IllegalArgumentException("activity, context or resources is null");
}
this.activity = activity;
this.context = context;
this.fragment = fragment;
this.infoItem = infoItem;
this.addDefaultEntriesAutomatically = addDefaultEntriesAutomatically;
if (addDefaultEntriesAutomatically) {
addDefaultBeginningEntries();
}
}
/**
* Adds a new entry and appends it to the current entry list.
* @param entry the entry to add
* @return the current {@link Builder} instance
*/
public Builder addEntry(@NonNull final StreamDialogDefaultEntry entry) {
entries.add(entry.toStreamDialogEntry());
return this;
}
/**
* Adds new entries. These are appended to the current entry list.
* @param newEntries the entries to add
* @return the current {@link Builder} instance
*/
public Builder addAllEntries(@NonNull final StreamDialogDefaultEntry... newEntries) {
Stream.of(newEntries).forEach(this::addEntry);
return this;
}
/**
* <p>Change an entries' action that is called when the entry is selected.</p>
* <p><strong>Warning:</strong> Only use this method when the entry has been already added.
* Changing the action of an entry which has not been added to the Builder yet
* does not have an effect.</p>
* @param entry the entry to change
* @param action the action to perform when the entry is selected
* @return the current {@link Builder} instance
*/
public Builder setAction(@NonNull final StreamDialogDefaultEntry entry,
@NonNull final StreamDialogEntry.StreamDialogEntryAction action) {
for (int i = 0; i < entries.size(); i++) {
if (entries.get(i).resource == entry.resource) {
entries.set(i, new StreamDialogEntry(entry.resource, action));
return this;
}
}
return this;
}
/**
* Adds {@link StreamDialogDefaultEntry#ENQUEUE} if the player is open and
* {@link StreamDialogDefaultEntry#ENQUEUE_NEXT} if there are multiple streams
* in the play queue.
* @return the current {@link Builder} instance
*/
public Builder addEnqueueEntriesIfNeeded() {
if (PlayerHolder.getInstance().isPlayQueueReady()) {
addEntry(StreamDialogDefaultEntry.ENQUEUE);
if (PlayerHolder.getInstance().getQueueSize() > 1) {
addEntry(StreamDialogDefaultEntry.ENQUEUE_NEXT);
}
}
return this;
}
/**
* Adds the {@link StreamDialogDefaultEntry#START_HERE_ON_BACKGROUND}.
* If the {@link #infoItem} is not a pure audio (live) stream,
* {@link StreamDialogDefaultEntry#START_HERE_ON_POPUP} is added, too.
* @return the current {@link Builder} instance
*/
public Builder addStartHereEntries() {
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
if (!StreamTypeUtil.isAudio(infoItem.getStreamType())) {
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
}
return this;
}
/**
* Adds {@link StreamDialogDefaultEntry#MARK_AS_WATCHED} if the watch history is enabled
* and the stream is not a livestream.
* @return the current {@link Builder} instance
*/
public Builder addMarkAsWatchedEntryIfNeeded() {
final boolean isWatchHistoryEnabled = PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
if (isWatchHistoryEnabled && !StreamTypeUtil.isLiveStream(infoItem.getStreamType())) {
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
}
return this;
}
/**
* Adds the {@link StreamDialogDefaultEntry#PLAY_WITH_KODI} entry if it is needed.
* @return the current {@link Builder} instance
*/
public Builder addPlayWithKodiEntryIfNeeded() {
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
addEntry(StreamDialogDefaultEntry.PLAY_WITH_KODI);
}
return this;
}
/**
* Add the entries which are usually at the top of the action list.
* <br/>
* This method adds the "enqueue" (see {@link #addEnqueueEntriesIfNeeded()})
* and "start here" (see {@link #addStartHereEntries()} entries.
* @return the current {@link Builder} instance
*/
public Builder addDefaultBeginningEntries() {
addEnqueueEntriesIfNeeded();
addStartHereEntries();
return this;
}
/**
* Add the entries which are usually at the bottom of the action list.
* @return the current {@link Builder} instance
*/
public Builder addDefaultEndEntries() {
addAllEntries(
StreamDialogDefaultEntry.APPEND_PLAYLIST,
StreamDialogDefaultEntry.SHARE,
StreamDialogDefaultEntry.OPEN_IN_BROWSER
);
addPlayWithKodiEntryIfNeeded();
addMarkAsWatchedEntryIfNeeded();
addEntry(StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS);
return this;
}
/**
* Creates the {@link InfoItemDialog}.
* @return a new instance of {@link InfoItemDialog}
*/
public InfoItemDialog create() {
if (addDefaultEntriesAutomatically) {
addDefaultEndEntries();
}
return new InfoItemDialog(this.activity, this.fragment, this.infoItem, this.entries);
}
public static void reportErrorDuringInitialization(final Throwable throwable,
final InfoItem item) {
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
throwable,
UserAction.OPEN_INFO_ITEM_DIALOG,
"none",
item.getServiceId()));
}
}
}

View File

@@ -0,0 +1,142 @@
package org.schabi.newpipe.info_list.dialog;
import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment;
import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse;
import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.Collections;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
/**
* <p>
* This enum provides entries that are accepted
* by the {@link InfoItemDialog.Builder}.
* </p>
* <p>
* These entries contain a String {@link #resource} which is displayed in the dialog and
* a default {@link #action} that is executed
* when the entry is selected (via <code>onClick()</code>).
* <br/>
* They action can be overridden by using the Builder's
* {@link InfoItemDialog.Builder#setAction(
* StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)}
* method.
* </p>
*/
public enum StreamDialogDefaultEntry {
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) ->
fetchUploaderUrlIfSparse(fragment.requireContext(), item.getServiceId(), item.getUrl(),
item.getUploaderUrl(), url -> openChannelFragment(fragment, item, url))
),
/**
* Enqueues the stream automatically to the current PlayerType.
*/
ENQUEUE(R.string.enqueue_stream, (fragment, item) ->
fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
NavigationHelper.enqueueOnPlayer(fragment.getContext(), singlePlayQueue))
),
/**
* Enqueues the stream automatically to the current PlayerType
* after the currently playing stream.
*/
ENQUEUE_NEXT(R.string.enqueue_next_stream, (fragment, item) ->
fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), singlePlayQueue))
),
START_HERE_ON_BACKGROUND(R.string.start_here_on_background, (fragment, item) ->
fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
NavigationHelper.playOnBackgroundPlayer(
fragment.getContext(), singlePlayQueue, true))),
START_HERE_ON_POPUP(R.string.start_here_on_popup, (fragment, item) ->
fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
NavigationHelper.playOnPopupPlayer(fragment.getContext(), singlePlayQueue, true))),
SET_AS_PLAYLIST_THUMBNAIL(R.string.set_as_playlist_thumbnail, (fragment, item) -> {
throw new UnsupportedOperationException("This needs to be implemented manually "
+ "by using InfoItemDialog.Builder.setAction()");
}),
DELETE(R.string.delete, (fragment, item) -> {
throw new UnsupportedOperationException("This needs to be implemented manually "
+ "by using InfoItemDialog.Builder.setAction()");
}),
/**
* Opens a {@link PlaylistDialog} to either append the stream to a playlist
* or create a new playlist if there are no local playlists.
*/
APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) ->
PlaylistDialog.createCorrespondingDialog(
fragment.getContext(),
Collections.singletonList(new StreamEntity(item)),
dialog -> dialog.show(
fragment.getParentFragmentManager(),
"StreamDialogEntry@"
+ (dialog instanceof PlaylistAppendDialog ? "append" : "create")
+ "_playlist"
)
)
),
PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) -> {
final Uri videoUrl = Uri.parse(item.getUrl());
try {
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
} catch (final Exception e) {
KoreUtils.showInstallKoreDialog(fragment.requireActivity());
}
}),
SHARE(R.string.share, (fragment, item) ->
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
item.getThumbnailUrl())),
OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) ->
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
MARK_AS_WATCHED(R.string.mark_as_watched, (fragment, item) ->
new HistoryRecordManager(fragment.getContext())
.markAsWatched(item)
.onErrorComplete()
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
);
@StringRes
public final int resource;
@NonNull
public final StreamDialogEntry.StreamDialogEntryAction action;
StreamDialogDefaultEntry(@StringRes final int resource,
@NonNull final StreamDialogEntry.StreamDialogEntryAction action) {
this.resource = resource;
this.action = action;
}
@NonNull
public StreamDialogEntry toStreamDialogEntry() {
return new StreamDialogEntry(resource, action);
}
}

View File

@@ -0,0 +1,31 @@
package org.schabi.newpipe.info_list.dialog;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
public class StreamDialogEntry {
@StringRes
public final int resource;
@NonNull
public final StreamDialogEntryAction action;
public StreamDialogEntry(@StringRes final int resource,
@NonNull final StreamDialogEntryAction action) {
this.resource = resource;
this.action = action;
}
public String getString(@NonNull final Context context) {
return context.getString(resource);
}
public interface StreamDialogEntryAction {
void onClick(Fragment fragment, StreamInfoItem infoItem);
}
}

View File

@@ -1,6 +1,7 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
@@ -11,10 +12,8 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import de.hdodenhof.circleimageview.CircleImageView;
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
public final CircleImageView itemThumbnailView;
public final ImageView itemThumbnailView;
public final TextView itemTitleView;
private final TextView itemAdditionalDetailView;

View File

@@ -34,12 +34,14 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
public final TextView itemTitleView;
private final ImageView itemHeartView;
private final ImageView itemPinnedView;
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comments_item, parent);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
}
@Override
@@ -55,5 +57,7 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
itemTitleView.setText(item.getUploaderName());
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
}
}

View File

@@ -7,13 +7,14 @@ import android.text.util.Linkify;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
@@ -28,8 +29,6 @@ import org.schabi.newpipe.util.PicassoHelper;
import java.util.regex.Matcher;
import de.hdodenhof.circleimageview.CircleImageView;
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private static final String TAG = "CommentsMiniIIHolder";
@@ -40,7 +39,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private final int commentVerticalPadding;
private final RelativeLayout itemRoot;
public final CircleImageView itemThumbnailView;
public final ImageView itemThumbnailView;
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemPublishedTime;
@@ -171,7 +170,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
item.getUploaderUrl(),
item.getUploaderName());
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(activity, "Opening channel fragment", e);
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
}
}

View File

@@ -11,12 +11,12 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit;
@@ -70,8 +70,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
} else {
itemProgressView.setVisibility(View.GONE);
}
} else if (item.getStreamType() == StreamType.LIVE_STREAM
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
} else if (StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemDurationView.setText(R.string.duration_live);
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.live_duration_background_color));
@@ -96,9 +95,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
case VIDEO_STREAM:
case LIVE_STREAM:
case AUDIO_LIVE_STREAM:
case POST_LIVE_STREAM:
case POST_LIVE_AUDIO_STREAM:
enableLongClick(item);
break;
case FILE:
case NONE:
default:
disableLongClick();
@@ -114,7 +114,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final StreamStateEntity state
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
if (state != null && item.getDuration() > 0
&& item.getStreamType() != StreamType.LIVE_STREAM) {
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS

View File

@@ -75,6 +75,7 @@ fun View.animate(
}
animate().setListener(null).cancel()
isVisible = true
when (animationType) {
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
@@ -299,14 +300,7 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
}
}
fun View.slideUp(
duration: Long,
delay: Long,
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float
) {
slideUp(duration, delay, translationPercent, null)
}
@JvmOverloads
fun View.slideUp(
duration: Long,
delay: Long = 0L,

View File

@@ -228,6 +228,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
return count;
}
@SuppressWarnings("FinalParameters")
@Override
public int getItemViewType(int position) {
if (DEBUG) {
@@ -300,6 +301,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
}
}
@SuppressWarnings("FinalParameters")
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
if (DEBUG) {

View File

@@ -33,8 +33,16 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
public PlaylistAppendDialog(final List<StreamEntity> streamEntities) {
super(streamEntities);
/**
* Create a new instance of {@link PlaylistAppendDialog}.
*
* @param streamEntities a list of {@link StreamEntity} to be added to playlists
* @return a new instance of {@link PlaylistAppendDialog}
*/
public static PlaylistAppendDialog newInstance(final List<StreamEntity> streamEntities) {
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
dialog.setStreamEntities(streamEntities);
return dialog;
}
/*//////////////////////////////////////////////////////////////////////////
@@ -103,13 +111,14 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
// Helper
//////////////////////////////////////////////////////////////////////////*/
/** Display create playlist dialog. */
public void openCreatePlaylistDialog() {
if (getStreamEntities() == null || !isAdded()) {
return;
}
final PlaylistCreationDialog playlistCreationDialog =
new PlaylistCreationDialog(getStreamEntities());
PlaylistCreationDialog.newInstance(getStreamEntities());
// Move the dismissListener to the new dialog.
playlistCreationDialog.setOnDismissListener(this.getOnDismissListener());
this.setOnDismissListener(null);

View File

@@ -21,8 +21,17 @@ import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
public final class PlaylistCreationDialog extends PlaylistDialog {
public PlaylistCreationDialog(final List<StreamEntity> streamEntities) {
super(streamEntities);
/**
* Create a new instance of {@link PlaylistCreationDialog}.
*
* @param streamEntities a list of {@link StreamEntity} to be added to playlists
* @return a new instance of {@link PlaylistCreationDialog}
*/
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streamEntities) {
final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
dialog.setStreamEntities(streamEntities);
return dialog;
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -31,10 +31,6 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
private org.schabi.newpipe.util.SavedState savedState;
public PlaylistDialog(final List<StreamEntity> streamEntities) {
this.streamEntities = streamEntities;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -97,7 +93,7 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
}
@Override
public void onSaveInstanceState(final Bundle outState) {
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
if (getActivity() != null) {
savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(),
@@ -120,6 +116,10 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
this.onDismissListener = onDismissListener;
}
protected void setStreamEntities(final List<StreamEntity> streamEntities) {
this.streamEntities = streamEntities;
}
/*//////////////////////////////////////////////////////////////////////////
// Dialog creation
//////////////////////////////////////////////////////////////////////////*/
@@ -143,8 +143,8 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
.observeOn(AndroidSchedulers.mainThread())
.subscribe(hasPlaylists ->
onExec.accept(hasPlaylists
? new PlaylistAppendDialog(streamEntities)
: new PlaylistCreationDialog(streamEntities))
? PlaylistAppendDialog.newInstance(streamEntities)
: PlaylistCreationDialog.newInstance(streamEntities))
);
}
}

View File

@@ -14,6 +14,7 @@ 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.database.subscription.NotificationMode
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
@@ -57,6 +58,11 @@ class FeedDatabaseManager(context: Context) {
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)
fun outdatedSubscriptionsWithNotificationMode(
outdatedThreshold: OffsetDateTime,
@NotificationMode notificationMode: Int
) = feedTable.getOutdatedWithNotificationMode(outdatedThreshold, notificationMode)
fun notLoadedCount(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable<Long> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.notLoadedCount()
@@ -72,6 +78,10 @@ class FeedDatabaseManager(context: Context) {
fun markAsOutdated(subscriptionId: Long) = feedTable
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
fun doesStreamExist(stream: StreamInfoItem): Boolean {
return streamTable.exists(stream.serviceId, stream.url)
}
fun upsertAll(
subscriptionId: Long,
items: List<StreamInfoItem>,

View File

@@ -25,7 +25,6 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.os.Parcelable
@@ -37,7 +36,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.annotation.AttrRes
import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
@@ -50,7 +48,6 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Item
import com.xwray.groupie.OnAsyncUpdateListener
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State
@@ -68,25 +65,22 @@ 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.info_list.dialog.InfoItemDialog
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
import org.schabi.newpipe.ktx.slideUp
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.player.helper.PlayerHolder
import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.StreamDialogEntry
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.util.ArrayList
import java.util.function.Consumer
class FeedFragment : BaseStateFragment<FeedState>() {
@@ -143,7 +137,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
val factory = FeedViewModel.Factory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
groupAdapter = GroupieAdapter().apply {
setOnItemClickListener(listenerStreamItem)
@@ -271,7 +265,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
override fun onDestroyView() {
// Ensure that all animations are canceled
feedBinding.newItemsLoadedButton?.clearAnimation()
tryGetNewItemsLoadedButton()?.clearAnimation()
feedBinding.itemsList.adapter = null
_feedBinding = null
@@ -356,53 +350,12 @@ class FeedFragment : BaseStateFragment<FeedState>() {
feedBinding.loadingProgressBar.max = progressState.maxProgress
}
private fun showStreamDialog(item: StreamInfoItem) {
private fun showInfoItemDialog(item: StreamInfoItem) {
val context = context
val activity: Activity? = getActivity()
if (context == null || context.resources == null || activity == null) return
val entries = ArrayList<StreamDialogEntry>()
if (PlayerHolder.getInstance().isPlayerOpen) {
entries.add(StreamDialogEntry.enqueue)
if (PlayerHolder.getInstance().queueSize > 1) {
entries.add(StreamDialogEntry.enqueue_next)
}
}
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
)
)
}
// show "mark as watched" only when watch history is enabled
if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) {
entries.add(
StreamDialogEntry.mark_as_watched
)
}
entries.add(StreamDialogEntry.show_channel_details)
StreamDialogEntry.setEnabledEntries(entries)
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
StreamDialogEntry.clickOn(which, this, item)
}.show()
InfoItemDialog.Builder(activity, context, this, item).create().show()
}
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
@@ -418,7 +371,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
override fun onItemLongClick(item: Item<*>, view: View): Boolean {
if (item is StreamItem && !isRefreshing) {
showStreamDialog(item.streamWithState.stream.toStreamInfoItem())
showInfoItemDialog(item.streamWithState.stream.toStreamInfoItem())
return true
}
return false
@@ -438,14 +391,11 @@ class FeedFragment : BaseStateFragment<FeedState>() {
// This need to be saved in a variable as the update occurs async
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
groupAdapter.updateAsync(
loadedState.items, false,
OnAsyncUpdateListener {
oldOldestSubscriptionUpdate?.run {
highlightNewItemsAfter(oldOldestSubscriptionUpdate)
}
groupAdapter.updateAsync(loadedState.items, false) {
oldOldestSubscriptionUpdate?.run {
highlightNewItemsAfter(oldOldestSubscriptionUpdate)
}
)
}
listState?.run {
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
@@ -497,8 +447,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
subscriptionEntity ->
{ subscriptionEntity ->
handleFeedNotAvailable(
subscriptionEntity,
t.cause,
@@ -629,19 +578,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
lastNewItemsCount = highlightCount
}
private fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
return androidx.core.content.ContextCompat.getDrawable(
context,
android.util.TypedValue().apply {
context.theme.resolveAttribute(
attrResId,
this,
true
)
}.resourceId
)
}
private fun showNewItemsLoaded() {
tryGetNewItemsLoadedButton()?.clearAnimation()
tryGetNewItemsLoadedButton()

View File

@@ -56,7 +56,7 @@ class FeedViewModel(
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
var streamItems = if (event is SuccessResultEvent || event is IdleEvent)
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
feedDatabaseManager
.getStreams(groupId, showPlayedItems)
.blockingGet(arrayListOf())

View File

@@ -14,6 +14,8 @@ 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.POST_LIVE_AUDIO_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper
@@ -109,7 +111,7 @@ data class StreamItem(
}
override fun isLongClickable() = when (stream.streamType) {
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM, POST_LIVE_STREAM, POST_LIVE_AUDIO_STREAM -> true
else -> false
}

View File

@@ -0,0 +1,145 @@
package org.schabi.newpipe.local.feed.notifications
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.PicassoHelper
/**
* Helper for everything related to show notifications about new streams to the user.
*/
class NotificationHelper(val context: Context) {
private val manager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
/**
* Show a notification about new streams from a single channel.
* Opening the notification will open the corresponding channel page.
*/
fun displayNewStreamsNotification(data: FeedUpdateInfo) {
val newStreams: List<StreamInfoItem> = data.newStreams
val summary = context.resources.getQuantityString(
R.plurals.new_streams, newStreams.size, newStreams.size
)
val builder = NotificationCompat.Builder(
context,
context.getString(R.string.streams_notification_channel_id)
)
.setContentTitle(Localization.concatenateStrings(data.name, summary))
.setContentText(
data.listInfo.relatedItems.joinToString(
context.getString(R.string.enumeration_comma)
) { x -> x.name }
)
.setNumber(newStreams.size)
.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
.setColorized(true)
.setAutoCancel(true)
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
// Build style
val style = NotificationCompat.InboxStyle()
newStreams.forEach { style.addLine(it.name) }
style.setSummaryText(summary)
style.setBigContentTitle(data.name)
builder.setStyle(style)
// open the channel page when clicking on the notification
builder.setContentIntent(
PendingIntent.getActivity(
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.FLAG_IMMUTABLE
else
0
)
)
PicassoHelper.loadNotificationIcon(data.avatarUrl) { bitmap ->
bitmap?.let { builder.setLargeIcon(it) } // set only if != null
manager.notify(data.pseudoId, builder.build())
}
}
companion object {
/**
* Check whether notifications are enabled on the device.
* Users can disable them via the system settings for a single app.
* If this is the case, the app cannot create any notifications
* and display them to the user.
* <br>
* On Android 26 and above, notification channels are used by NewPipe.
* These can be configured by the user, too.
* The notification channel for new streams is also checked by this method.
*
* @param context Context
* @return <code>true</code> if notifications are allowed and can be displayed;
* <code>false</code> otherwise
*/
fun areNotificationsEnabledOnDevice(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = context.getString(R.string.streams_notification_channel_id)
val manager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
val enabled = manager.areNotificationsEnabled()
val channel = manager.getNotificationChannel(channelId)
val importance = channel?.importance
enabled && channel != null && importance != NotificationManager.IMPORTANCE_NONE
} else {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
/**
* Whether the user enabled the notifications for new streams in the app settings.
*/
@JvmStatic
fun areNewStreamsNotificationsEnabled(context: Context): Boolean {
return (
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.enable_streams_notifications), false) &&
areNotificationsEnabledOnDevice(context)
)
}
/**
* Open the system's notification settings for NewPipe on Android Oreo (API 26) and later.
* Open the system's app settings for NewPipe on previous Android versions.
*/
fun openNewPipeSystemNotificationSettings(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
} else {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:" + context.packageName)
context.startActivity(intent)
}
}
}
}

View File

@@ -0,0 +1,170 @@
package org.schabi.newpipe.local.feed.notifications
import android.content.Context
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.rxjava3.RxWorker
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import org.schabi.newpipe.App
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.local.feed.service.FeedLoadManager
import org.schabi.newpipe.local.feed.service.FeedLoadService
import java.util.concurrent.TimeUnit
/*
* Worker which checks for new streams of subscribed channels
* in intervals which can be set by the user in the settings.
*/
class NotificationWorker(
appContext: Context,
workerParams: WorkerParameters,
) : RxWorker(appContext, workerParams) {
private val notificationHelper by lazy {
NotificationHelper(appContext)
}
private val feedLoadManager = FeedLoadManager(appContext)
override fun createWork(): Single<Result> = if (areNotificationsEnabled(applicationContext)) {
feedLoadManager.startLoading(
ignoreOutdatedThreshold = true,
groupId = FeedLoadManager.GROUP_NOTIFICATION_ENABLED
)
.doOnSubscribe { showLoadingFeedForegroundNotification() }
.map { feed ->
// filter out feedUpdateInfo items (i.e. channels) with nothing new
feed.mapNotNull {
it.value?.takeIf { feedUpdateInfo ->
feedUpdateInfo.newStreams.isNotEmpty()
}
}
}
.observeOn(AndroidSchedulers.mainThread()) // Picasso requires calls from main thread
.map { feedUpdateInfoList ->
// display notifications for each feedUpdateInfo (i.e. channel)
feedUpdateInfoList.forEach { feedUpdateInfo ->
notificationHelper.displayNewStreamsNotification(feedUpdateInfo)
}
return@map Result.success()
}
.doOnError { throwable ->
Log.e(TAG, "Error while displaying streams notifications", throwable)
ErrorUtil.createNotification(
applicationContext,
ErrorInfo(throwable, UserAction.NEW_STREAMS_NOTIFICATIONS, "main worker")
)
}
.onErrorReturnItem(Result.failure())
} else {
// the user can disable streams notifications in the device's app settings
Single.just(Result.success())
}
private fun showLoadingFeedForegroundNotification() {
val notification = NotificationCompat.Builder(
applicationContext,
applicationContext.getString(R.string.notification_channel_id)
).setOngoing(true)
.setProgress(-1, -1, true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setContentTitle(applicationContext.getString(R.string.feed_notification_loading))
.build()
setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification))
}
companion object {
private val TAG = NotificationWorker::class.java.simpleName
private const val WORK_TAG = App.PACKAGE_NAME + "_streams_notifications"
private fun areNotificationsEnabled(context: Context) =
NotificationHelper.areNewStreamsNotificationsEnabled(context) &&
NotificationHelper.areNotificationsEnabledOnDevice(context)
/**
* Schedules a task for the [NotificationWorker]
* if the (device and in-app) notifications are enabled,
* otherwise [cancel]s all scheduled tasks.
*/
@JvmStatic
fun initialize(context: Context) {
if (areNotificationsEnabled(context)) {
schedule(context)
} else {
cancel(context)
}
}
/**
* @param context the context to use
* @param options configuration options for the scheduler
* @param force Force the scheduler to use the new options
* by replacing the previously used worker.
*/
fun schedule(context: Context, options: ScheduleOptions, force: Boolean = false) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(
if (options.isRequireNonMeteredNetwork) {
NetworkType.UNMETERED
} else {
NetworkType.CONNECTED
}
).build()
val request = PeriodicWorkRequest.Builder(
NotificationWorker::class.java,
options.interval,
TimeUnit.MILLISECONDS
).setConstraints(constraints)
.addTag(WORK_TAG)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
WORK_TAG,
if (force) {
ExistingPeriodicWorkPolicy.REPLACE
} else {
ExistingPeriodicWorkPolicy.KEEP
},
request
)
}
@JvmStatic
fun schedule(context: Context) = schedule(context, ScheduleOptions.from(context))
/**
* Check for new streams immediately
*/
@JvmStatic
fun runNow(context: Context) {
val request = OneTimeWorkRequestBuilder<NotificationWorker>()
.addTag(WORK_TAG)
.build()
WorkManager.getInstance(context).enqueue(request)
}
/**
* Cancels all current work related to the [NotificationWorker].
*/
@JvmStatic
fun cancel(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG)
}
}
}

View File

@@ -0,0 +1,37 @@
package org.schabi.newpipe.local.feed.notifications
import android.content.Context
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
import java.util.concurrent.TimeUnit
/**
* Information for the Scheduler which checks for new streams.
* See [NotificationWorker]
*/
data class ScheduleOptions(
val interval: Long,
val isRequireNonMeteredNetwork: Boolean
) {
companion object {
fun from(context: Context): ScheduleOptions {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
return ScheduleOptions(
interval = TimeUnit.SECONDS.toMillis(
preferences.getString(
context.getString(R.string.streams_notifications_interval_key),
null
)?.toLongOrNull() ?: context.getString(
R.string.streams_notifications_interval_default
).toLong()
),
isRequireNonMeteredNetwork = preferences.getString(
context.getString(R.string.streams_notifications_network_key),
context.getString(R.string.streams_notifications_network_default)
) == context.getString(R.string.streams_notifications_network_wifi)
)
}
}
}

View File

@@ -0,0 +1,270 @@
package org.schabi.newpipe.local.feed.service
import android.content.Context
import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Notification
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExtractorHelper
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class FeedLoadManager(private val context: Context) {
private val subscriptionManager = SubscriptionManager(context)
private val feedDatabaseManager = FeedDatabaseManager(context)
private val notificationUpdater = PublishProcessor.create<String>()
private val currentProgress = AtomicInteger(-1)
private val maxProgress = AtomicInteger(-1)
private val cancelSignal = AtomicBoolean()
private val feedResultsHolder = FeedResultsHolder()
val notification: Flowable<FeedLoadState> = notificationUpdater.map { description ->
FeedLoadState(description, maxProgress.get(), currentProgress.get())
}
/**
* Start checking for new streams of a subscription group.
* @param groupId The ID of the subscription group to load. When using
* [FeedGroupEntity.GROUP_ALL_ID], all subscriptions are loaded. When using
* [GROUP_NOTIFICATION_ENABLED], only subscriptions with enabled notifications for new streams
* are loaded. Using an id of a group created by the user results in that specific group to be
* loaded.
* @param ignoreOutdatedThreshold When `false`, only subscriptions which have not been updated
* within the `feed_update_threshold` are checked for updates. This threshold can be set by
* the user in the app settings. When `true`, all subscriptions are checked for new streams.
*/
fun startLoading(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
ignoreOutdatedThreshold: Boolean = false,
): Single<List<Notification<FeedUpdateInfo>>> {
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val useFeedExtractor = defaultSharedPreferences.getBoolean(
context.getString(R.string.feed_use_dedicated_fetch_method_key),
false
)
val outdatedThreshold = if (ignoreOutdatedThreshold) {
OffsetDateTime.now(ZoneOffset.UTC)
} else {
val thresholdOutdatedSeconds = (
defaultSharedPreferences.getString(
context.getString(R.string.feed_update_threshold_key),
context.getString(R.string.feed_update_threshold_default_value)
) ?: context.getString(R.string.feed_update_threshold_default_value)
).toInt()
OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(thresholdOutdatedSeconds.toLong())
}
/**
* subscriptions which have not been updated within the feed updated threshold
*/
val outdatedSubscriptions = when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold)
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
outdatedThreshold, NotificationMode.ENABLED
)
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
}
return outdatedSubscriptions
.take(1)
.doOnNext {
currentProgress.set(0)
maxProgress.set(it.size)
}
.filter { it.isNotEmpty() }
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
notificationUpdater.onNext("")
broadcastProgress()
}
.observeOn(Schedulers.io())
.flatMap { Flowable.fromIterable(it) }
.takeWhile { !cancelSignal.get() }
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
.filter { !cancelSignal.get() }
.map { subscriptionEntity ->
var error: Throwable? = null
try {
// check for and load new streams
// either by using the dedicated feed method or by getting the channel info
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(
FeedUpdateInfo(
subscriptionEntity,
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 =
FeedLoadService.RequestException(subscriptionEntity.uid, request, error!!)
return@map Notification.createOnError<FeedUpdateInfo>(wrapper)
}
}
.sequential()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(NotificationConsumer())
.observeOn(Schedulers.io())
.buffer(BUFFER_COUNT_BEFORE_INSERT)
.doOnNext(DatabaseConsumer())
.subscribeOn(Schedulers.io())
.toList()
.flatMap { x -> postProcessFeed().toSingleDefault(x.flatten()) }
}
fun cancel() {
cancelSignal.set(true)
}
private fun broadcastProgress() {
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(currentProgress.get(), maxProgress.get()))
}
/**
* Keep the feed and the stream tables small
* to reduce loading times when trying to display the feed.
* <br>
* Remove streams from the feed which are older than [FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE].
* Remove streams from the database which are not linked / used by any table.
*/
private fun postProcessFeed() = Completable.fromRunnable {
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(R.string.feed_processing_message))
feedDatabaseManager.removeOrphansOrOlderStreams()
FeedEventManager.postEvent(FeedEventManager.Event.SuccessResultEvent(feedResultsHolder.itemsErrors))
}.doOnSubscribe {
currentProgress.set(-1)
maxProgress.set(-1)
notificationUpdater.onNext(context.getString(R.string.feed_processing_message))
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(R.string.feed_processing_message))
}.subscribeOn(Schedulers.io())
private inner class NotificationConsumer : Consumer<Notification<FeedUpdateInfo>> {
override fun accept(item: Notification<FeedUpdateInfo>) {
currentProgress.incrementAndGet()
notificationUpdater.onNext(item.value?.name.orEmpty())
broadcastProgress()
}
}
private inner class DatabaseConsumer : Consumer<List<Notification<FeedUpdateInfo>>> {
override fun accept(list: List<Notification<FeedUpdateInfo>>) {
feedDatabaseManager.database().runInTransaction {
for (notification in list) {
when {
notification.isOnNext -> {
val subscriptionId = notification.value!!.uid
val info = notification.value!!.listInfo
notification.value!!.newStreams = filterNewStreams(
notification.value!!.listInfo.relatedItems
)
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
subscriptionManager.updateFromInfo(subscriptionId, info)
if (info.errors.isNotEmpty()) {
feedResultsHolder.addErrors(
FeedLoadService.RequestException.wrapList(
subscriptionId,
info
)
)
feedDatabaseManager.markAsOutdated(subscriptionId)
}
}
notification.isOnError -> {
val error = notification.error
feedResultsHolder.addError(error!!)
if (error is FeedLoadService.RequestException) {
feedDatabaseManager.markAsOutdated(error.subscriptionId)
}
}
}
}
}
}
private fun filterNewStreams(list: List<StreamInfoItem>): List<StreamInfoItem> {
return list.filter {
!feedDatabaseManager.doesStreamExist(it) &&
it.uploadDate != null &&
// Streams older than this date are automatically removed from the feed.
// Therefore, streams which are not in the database,
// but older than this date, are considered old.
it.uploadDate!!.offsetDateTime().isAfter(
FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE
)
}
}
}
companion object {
/**
* Constant used to check for updates of subscriptions with [NotificationMode.ENABLED].
*/
const val GROUP_NOTIFICATION_ENABLED = -2L
/**
* How many extractions will be running in parallel.
*/
private const val PARALLEL_EXTRACTIONS = 6
/**
* Number of items to buffer to mass-insert in the database.
*/
private const val BUFFER_COUNT_BEFORE_INSERT = 20
}
}

View File

@@ -31,41 +31,24 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Notification
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.functions.Function
import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import org.schabi.newpipe.App
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
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
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExtractorHelper
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class FeedLoadService : Service() {
companion object {
private val TAG = FeedLoadService::class.java.simpleName
private const val NOTIFICATION_ID = 7293450
const val NOTIFICATION_ID = 7293450
private const val ACTION_CANCEL = App.PACKAGE_NAME + ".local.feed.service.FeedLoadService.CANCEL"
/**
@@ -73,27 +56,13 @@ class FeedLoadService : Service() {
*/
private const val NOTIFICATION_SAMPLING_PERIOD = 1500
/**
* How many extractions will be running in parallel.
*/
private const val PARALLEL_EXTRACTIONS = 6
/**
* Number of items to buffer to mass-insert in the database.
*/
private const val BUFFER_COUNT_BEFORE_INSERT = 20
const val EXTRA_GROUP_ID: String = "FeedLoadService.EXTRA_GROUP_ID"
}
private var loadingSubscription: Subscription? = null
private lateinit var subscriptionManager: SubscriptionManager
private var loadingDisposable: Disposable? = null
private var notificationDisposable: Disposable? = null
private lateinit var feedDatabaseManager: FeedDatabaseManager
private lateinit var feedResultsHolder: ResultsHolder
private var disposables = CompositeDisposable()
private var notificationUpdater = PublishProcessor.create<String>()
private lateinit var feedLoadManager: FeedLoadManager
// /////////////////////////////////////////////////////////////////////////
// Lifecycle
@@ -101,8 +70,7 @@ class FeedLoadService : Service() {
override fun onCreate() {
super.onCreate()
subscriptionManager = SubscriptionManager(this)
feedDatabaseManager = FeedDatabaseManager(this)
feedLoadManager = FeedLoadManager(this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -114,40 +82,45 @@ class FeedLoadService : Service() {
)
}
if (intent == null || loadingSubscription != null) {
if (intent == null || loadingDisposable != null) {
return START_NOT_STICKY
}
setupNotification()
setupBroadcastReceiver()
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
val useFeedExtractor = defaultSharedPreferences
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
val thresholdOutdatedSecondsString = defaultSharedPreferences
.getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value))
val thresholdOutdatedSeconds = thresholdOutdatedSecondsString!!.toInt()
startLoading(groupId, useFeedExtractor, thresholdOutdatedSeconds)
loadingDisposable = feedLoadManager.startLoading(groupId)
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
startForeground(NOTIFICATION_ID, notificationBuilder.build())
}
.subscribe { _, error ->
// There seems to be a bug in the kotlin plugin as it tells you when
// building that this can't be null:
// "Condition 'error != null' is always 'true'"
// However it can indeed be null
// The suppression may be removed in further versions
@Suppress("SENSELESS_COMPARISON")
if (error != null) {
Log.e(TAG, "Error while storing result", error)
handleError(error)
return@subscribe
}
stopService()
}
return START_NOT_STICKY
}
private fun disposeAll() {
unregisterReceiver(broadcastReceiver)
loadingSubscription?.cancel()
loadingSubscription = null
disposables.dispose()
loadingDisposable?.dispose()
notificationDisposable?.dispose()
}
private fun stopService() {
disposeAll()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
notificationManager.cancel(NOTIFICATION_ID)
stopSelf()
}
@@ -171,190 +144,6 @@ class FeedLoadService : Service() {
}
}
private fun startLoading(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, useFeedExtractor: Boolean, thresholdOutdatedSeconds: Int) {
feedResultsHolder = ResultsHolder()
val outdatedThreshold = OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(thresholdOutdatedSeconds.toLong())
val subscriptions = when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold)
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
}
subscriptions
.take(1)
.doOnNext {
currentProgress.set(0)
maxProgress.set(it.size)
}
.filter { it.isNotEmpty() }
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
startForeground(NOTIFICATION_ID, notificationBuilder.build())
updateNotificationProgress(null)
broadcastProgress()
}
.observeOn(Schedulers.io())
.flatMap { Flowable.fromIterable(it) }
.takeWhile { !cancelSignal.get() }
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
.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, error!!)
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
}
}
.sequential()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(notificationsConsumer)
.observeOn(Schedulers.io())
.buffer(BUFFER_COUNT_BEFORE_INSERT)
.doOnNext(databaseConsumer)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(resultSubscriber)
}
private fun broadcastProgress() {
postEvent(ProgressEvent(currentProgress.get(), maxProgress.get()))
}
private val resultSubscriber
get() = object : Subscriber<List<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>> {
override fun onSubscribe(s: Subscription) {
loadingSubscription = s
s.request(java.lang.Long.MAX_VALUE)
}
override fun onNext(notification: List<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>) {
if (DEBUG) Log.v(TAG, "onNext() → $notification")
}
override fun onError(error: Throwable) {
handleError(error)
}
override fun onComplete() {
if (maxProgress.get() == 0) {
postEvent(FeedEventManager.Event.IdleEvent)
stopService()
return
}
currentProgress.set(-1)
maxProgress.set(-1)
notificationUpdater.onNext(getString(R.string.feed_processing_message))
postEvent(ProgressEvent(R.string.feed_processing_message))
disposables.add(
Single
.fromCallable {
feedResultsHolder.ready()
postEvent(ProgressEvent(R.string.feed_processing_message))
feedDatabaseManager.removeOrphansOrOlderStreams()
postEvent(SuccessResultEvent(feedResultsHolder.itemsErrors))
true
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { _, throwable ->
// There seems to be a bug in the kotlin plugin as it tells you when
// building that this can't be null:
// "Condition 'throwable != null' is always 'true'"
// However it can indeed be null
// The suppression may be removed in further versions
@Suppress("SENSELESS_COMPARISON")
if (throwable != null) {
Log.e(TAG, "Error while storing result", throwable)
handleError(throwable)
return@subscribe
}
stopService()
}
)
}
}
private val databaseConsumer: Consumer<List<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>>
get() = Consumer {
feedDatabaseManager.database().runInTransaction {
for (notification in it) {
if (notification.isOnNext) {
val subscriptionId = notification.value!!.first
val info = notification.value!!.second
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
subscriptionManager.updateFromInfo(subscriptionId, info)
if (info.errors.isNotEmpty()) {
feedResultsHolder.addErrors(RequestException.wrapList(subscriptionId, info))
feedDatabaseManager.markAsOutdated(subscriptionId)
}
} else if (notification.isOnError) {
val error = notification.error!!
feedResultsHolder.addError(error)
if (error is RequestException) {
feedDatabaseManager.markAsOutdated(error.subscriptionId)
}
}
}
}
}
private val notificationsConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer { onItemCompleted(it.value?.second?.name) }
private fun onItemCompleted(updateDescription: String?) {
currentProgress.incrementAndGet()
notificationUpdater.onNext(updateDescription ?: "")
broadcastProgress()
}
// /////////////////////////////////////////////////////////////////////////
// Notification
// /////////////////////////////////////////////////////////////////////////
@@ -362,13 +151,12 @@ class FeedLoadService : Service() {
private lateinit var notificationManager: NotificationManagerCompat
private lateinit var notificationBuilder: NotificationCompat.Builder
private var currentProgress = AtomicInteger(-1)
private var maxProgress = AtomicInteger(-1)
private fun createNotification(): NotificationCompat.Builder {
val cancelActionIntent = PendingIntent.getBroadcast(
this,
NOTIFICATION_ID, Intent(ACTION_CANCEL), 0
NOTIFICATION_ID,
Intent(ACTION_CANCEL),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
)
return NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
@@ -384,33 +172,36 @@ class FeedLoadService : Service() {
notificationManager = NotificationManagerCompat.from(this)
notificationBuilder = createNotification()
val throttleAfterFirstEmission = Function { flow: Flowable<String> ->
val throttleAfterFirstEmission = Function { flow: Flowable<FeedLoadState> ->
flow.take(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS))
}
disposables.add(
notificationUpdater
.publish(throttleAfterFirstEmission)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateNotificationProgress)
)
notificationDisposable = feedLoadManager.notification
.publish(throttleAfterFirstEmission)
.observeOn(AndroidSchedulers.mainThread())
.doOnTerminate { notificationManager.cancel(NOTIFICATION_ID) }
.subscribe(this::updateNotificationProgress)
}
private fun updateNotificationProgress(updateDescription: String?) {
notificationBuilder.setProgress(maxProgress.get(), currentProgress.get(), maxProgress.get() == -1)
private fun updateNotificationProgress(state: FeedLoadState) {
notificationBuilder.setProgress(state.maxProgress, state.currentProgress, state.maxProgress == -1)
if (maxProgress.get() == -1) {
if (state.maxProgress == -1) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) notificationBuilder.setContentInfo(null)
if (!updateDescription.isNullOrEmpty()) notificationBuilder.setContentText(updateDescription)
notificationBuilder.setContentText(updateDescription)
if (state.updateDescription.isNotEmpty()) notificationBuilder.setContentText(state.updateDescription)
notificationBuilder.setContentText(state.updateDescription)
} else {
val progressText = this.currentProgress.toString() + "/" + maxProgress
val progressText = state.currentProgress.toString() + "/" + state.maxProgress
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (!updateDescription.isNullOrEmpty()) notificationBuilder.setContentText("$updateDescription ($progressText)")
if (state.updateDescription.isNotEmpty()) {
notificationBuilder.setContentText("${state.updateDescription} ($progressText)")
}
} else {
notificationBuilder.setContentInfo(progressText)
if (!updateDescription.isNullOrEmpty()) notificationBuilder.setContentText(updateDescription)
if (state.updateDescription.isNotEmpty()) {
notificationBuilder.setContentText(state.updateDescription)
}
}
}
@@ -422,13 +213,12 @@ class FeedLoadService : Service() {
// /////////////////////////////////////////////////////////////////////////
private lateinit var broadcastReceiver: BroadcastReceiver
private val cancelSignal = AtomicBoolean()
private fun setupBroadcastReceiver() {
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == ACTION_CANCEL) {
cancelSignal.set(true)
feedLoadManager.cancel()
}
}
}
@@ -443,29 +233,4 @@ class FeedLoadService : Service() {
postEvent(ErrorResultEvent(error))
stopService()
}
// /////////////////////////////////////////////////////////////////////////
// Results Holder
// /////////////////////////////////////////////////////////////////////////
class ResultsHolder {
/**
* List of errors that may have happen during loading.
*/
internal lateinit var itemsErrors: List<Throwable>
private val itemsErrorsHolder: MutableList<Throwable> = ArrayList()
fun addError(error: Throwable) {
itemsErrorsHolder.add(error)
}
fun addErrors(errors: List<Throwable>) {
itemsErrorsHolder.addAll(errors)
}
fun ready() {
itemsErrors = itemsErrorsHolder.toList()
}
}
}

View File

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

View File

@@ -0,0 +1,19 @@
package org.schabi.newpipe.local.feed.service
class FeedResultsHolder {
/**
* List of errors that may have happen during loading.
*/
val itemsErrors: List<Throwable>
get() = itemsErrorsHolder
private val itemsErrorsHolder: MutableList<Throwable> = ArrayList()
fun addError(error: Throwable) {
itemsErrorsHolder.add(error)
}
fun addErrors(errors: List<Throwable>) {
itemsErrorsHolder.addAll(errors)
}
}

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